diff --git a/src/fetchTelegramMessages.ts b/src/fetchTelegramMessages.ts index b2f945b..df72084 100644 --- a/src/fetchTelegramMessages.ts +++ b/src/fetchTelegramMessages.ts @@ -1,48 +1,72 @@ -import { TelegramClient } from "telegram"; -import { Api } from "telegram"; +import { Api, TelegramClient } from "telegram"; +import { trackApiKeyUsage } from './utils/redisUtils'; -export type TelegramMessage = { id: string; content: string; channelId: string }; +export type TelegramMessages = { id: string; content: string; channelId: string }; export async function fetchTelegramMessages( - client: TelegramClient, - channel: string -): Promise { - if (!channel) { - throw new Error("TG_CHANNEL environment variable is not set."); + client: TelegramClient, + channel: string +): Promise { + if (!channel) { + throw new Error("TG_CHANNEL environment variable is not set."); + } + const apiId = process.env.API_ID; + + // Fetch channel entity to get the actual channel ID + let entity: Api.Channel; + try { + const resolved = await client.getEntity(channel); + if (resolved instanceof Api.Channel) { + entity = resolved; + } else if (resolved instanceof Api.ChannelForbidden) { + throw new Error(`TG_CHANNEL "${channel}" is a private/forbidden channel; cannot fetch history.`); + } else { + throw new Error(`TG_CHANNEL "${channel}" is not a channel-type peer.`); } - // Fetch channel entity to get the actual channel ID - let entity: Api.Channel; + } catch (e) { + throw new Error(`Failed to resolve TG_CHANNEL "${channel}": ${e instanceof Error ? e.message : e}`); + } + + const channelId = String(entity.id); + + // Fetch the latest 10 messages + const messages = await client.invoke( + new Api.messages.GetHistory({ + peer: entity, + limit: 10, + }) + ); + + const out: TelegramMessages[] = []; + + if ("messages" in messages) { + for (const msg of messages.messages as any[]) { + const id = typeof msg?.id === 'number' || typeof msg?.id === 'string' ? String(msg.id) : null; + const content = typeof msg?.message === 'string' ? msg.message : ''; + if (!id || !content) continue; // skip service/media-only messages + const formatted = { id, content, channelId }; + out.push(formatted); + console.log(formatted); + } + } else { + console.log("No messages property found in response:", messages); + } + + // Track API usage after successful fetch + if (apiId) { + let accountId: string; try { - const resolved = await client.getEntity(channel); - if (resolved instanceof Api.Channel) { - entity = resolved; - } else if (resolved instanceof Api.ChannelForbidden) { - throw new Error(`TG_CHANNEL "${channel}" is a private/forbidden channel; cannot fetch history.`); - } else { - throw new Error(`TG_CHANNEL "${channel}" is not a channel-type peer.`); - } + const me = await client.getMe(); + if (me && me.id) { + accountId = String(me.id); + } else { + throw new Error('Unable to determine Telegram account ID'); + } } catch (e) { - throw new Error(`Failed to resolve TG_CHANNEL \"${channel}\": ${e instanceof Error ? e.message : e}`); + throw new Error('Unable to determine Telegram account ID'); } - const channelId = String(entity.id); - const messages = await client.invoke( - new Api.messages.GetHistory({ - peer: entity, - limit: 10, - }) - ); - const out: TelegramMessage[] = []; - if ("messages" in messages) { - for (const msg of messages.messages as any[]) { - const id = typeof msg?.id === 'number' || typeof msg?.id === 'string' ? String(msg.id) : null; - const content = typeof msg?.message === 'string' ? msg.message : ''; - if (!id || !content) continue; // skip service/media-only - const formatted = { id, content, channelId }; - out.push(formatted); - console.log(formatted); - } - } else { - console.log("No messages property found in response:", messages); - } - return out; + await trackApiKeyUsage({ accountId, platform: 'telegram' }); + } + + return out; } diff --git a/src/index.ts b/src/index.ts index 18fff1b..0281120 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ -import { TelegramClient } from "telegram"; -import { StringSession } from "telegram/sessions"; +import 'dotenv/config'; import input from "input"; // interactive input for login import cron from 'node-cron'; -import { runRedisOperation } from './utils/redisUtils'; +import { TelegramClient } from "telegram"; +import { StringSession } from "telegram/sessions"; import { fetchTelegramMessages } from './fetchTelegramMessages'; -import 'dotenv/config'; +import { getApiKeyUsage } from './utils/redisUtils'; // Replace these with your values const apiId = Number(process.env.API_ID); @@ -42,6 +42,15 @@ async function startTelegramCron() { // Run once at startup try { await fetchTelegramMessages(client, process.env.TG_CHANNEL!); + // Print Telegram API usage by accountId (not API_ID) + const me = await client.getMe(); + const accountId = String(me.id); + const usage = await getApiKeyUsage({accountId, platform:'telegram'}); + console.log('Telegram API usage:', { + total_requests: usage.total_requests, + last_request: usage.last_request, + account_id: usage.account_id + }); } catch (err) { console.error("Startup Telegram fetch failed:", err); } @@ -51,6 +60,7 @@ async function startTelegramCron() { console.log('Refetching Telegram messages...'); try { await fetchTelegramMessages(client, process.env.TG_CHANNEL!); + // No duplicate print of Telegram API usage } catch (err) { console.error('Scheduled Telegram fetch failed:', err); } @@ -64,14 +74,5 @@ startTelegramCron().catch((err) => { }); -async function main() { - await runRedisOperation(async (client) => { - await client.set('test-key', 'hello-redis'); - const value = await client.get('test-key'); - console.log('Read value from Redis:', value); - }); -} - -main(); diff --git a/src/twitterApi.ts b/src/twitterApi.ts index b2cf22e..eb65be7 100644 --- a/src/twitterApi.ts +++ b/src/twitterApi.ts @@ -1,8 +1,38 @@ -import cron from 'node-cron'; import dotenv from 'dotenv'; +import cron from 'node-cron'; +import { getApiKeyUsage } from './utils/redisUtils'; +import { trackApiKeyUsage } from './utils/redisUtils'; +const AUTH_TOKEN = process.env.AUTH_TOKEN; dotenv.config(); -export async function fetchHomeTimeline(seenTweetIds: string[] = []): Promise> { +async function fetchViewerAccount(): Promise<{ screenName: string; userId: string } | null> { + const url = "https://x.com/i/api/graphql/jMaTSZ5dqXctUg5f97R6xw/Viewer"; + + const headers = { + "authorization": `Bearer ${process.env.BEARER}`, + "x-csrf-token": process.env.CSRF_TOKEN as string, + "cookie": `auth_token=${process.env.AUTH_TOKEN}; ct0=${process.env.CSRF_TOKEN}`, + }; + + const res = await fetch(url, { method: "GET", headers }); + if (!res.ok) { + console.error("Viewer API request failed:", res.status, res.statusText); + return null; + } + + const data = await res.json(); + const user = data?.data?.viewer?.user_results?.result; + if (!user) return null; + + return { + screenName: user.legacy?.screen_name, + userId: user.rest_id, + }; +} + +export async function fetchHomeTimeline( + seenTweetIds: string[] = [] +): Promise> { const queryId = "wEpbv0WrfwV6y2Wlf0fxBQ"; const url = `https://x.com/i/api/graphql/${queryId}/HomeTimeline`; @@ -93,13 +123,12 @@ export async function fetchHomeTimeline(seenTweetIds: string[] = []): Promise = []; + const tweets: Array<{ content: string; id: string; authorId: string }> = []; const seenTweetIdsSet = new Set(seenTweetIds); try { - // Twitter's actual GraphQL HomeTimeline response structure const instructions = timeline?.home?.home_timeline_urt?.instructions || []; for (const instruction of instructions) { @@ -124,6 +153,13 @@ export async function fetchHomeTimeline(seenTweetIds: string[] = []): Promise { console.log('Refetching Twitter timeline...'); try { const timeline = await fetchHomeTimeline(); - // Process the timeline data here (save to DB, send to another service, etc.) console.log('Fetched timeline:', timeline); } catch (err) { console.error('Scheduled Twitter timeline fetch failed:', err); } }); - diff --git a/src/utils/redisUtils.ts b/src/utils/redisUtils.ts index ba8f23c..fd38d19 100644 --- a/src/utils/redisUtils.ts +++ b/src/utils/redisUtils.ts @@ -1,25 +1,90 @@ -import { createClient, RedisClientType } from 'redis'; +import crypto from 'crypto'; +import { createClient } from 'redis'; -export async function runRedisOperation(operation: (client: RedisClientType) => Promise): Promise { - const client: RedisClientType = createClient({ - url: 'redis://localhost:6379' - }); +// Singleton Redis client +const redisClient = createClient({ url: 'redis://localhost:6379' }); +let redisConnected = false; - client.on('error', (err) => { - console.error('Redis Client Error', err); - }); +redisClient.on('error', (err) => { + console.error('Redis Client Error', err); +}); + +async function ensureRedisConnected() { + if (!redisConnected) { + await redisClient.connect(); + redisConnected = true; + } +} +export async function trackApiKeyUsage({ accountId, platform }: { accountId: string, platform: 'telegram' | 'twitter' }): Promise { + if (!accountId?.trim()) { + console.warn('trackApiKeyUsage: empty accountId; skipping'); + return; + } try { - await client.connect(); - await operation(client); + await ensureRedisConnected(); + let key: string; + if (platform === 'twitter') { + key = `twitter_accounts:${accountId}`; + } else if (platform === 'telegram') { + key = `telegram_accounts:${accountId}`; + } else { + throw new Error('trackApiKeyUsage: platform must be "twitter" or "telegram"'); + } + const now = new Date().toISOString(); + await redisClient + .multi() + .hIncrBy(key, 'total_requests', 1) + .hSet(key, { + last_request: now, + account_id: accountId, + }) + .exec(); } catch (err) { - console.error('Redis operation failed:', err); - throw err; - } finally { - try { - await client.quit(); - } catch (quitErr) { - console.error('Error during Redis client quit:', quitErr); + console.warn('trackApiKeyUsage: non-fatal Redis error; proceeding without usage update', err); + } +} + +/** + * Get API usage stats from Redis. + * @param accountId The account ID to query + * @param platform The platform ('telegram' or 'twitter') + * @returns Object with total_requests and last_request + */ + +interface dataType { + accountId: string, + platform: 'telegram' | 'twitter' +} + +export async function getApiKeyUsage(data: dataType): Promise<{ total_requests: number; last_request: string | null; account_id?: string }> { + const { accountId, platform } = data; + let result: { total_requests: number; last_request: string | null; account_id?: string } = { total_requests: 0, last_request: null }; + if (!accountId?.trim()) { + return result; + } + if (platform !== 'twitter' && platform !== 'telegram') { + throw new Error('getApiKeyUsage: platform must be "twitter" or "telegram"'); + } + try { + await ensureRedisConnected(); + let key: string; + if (platform === 'twitter') { + key = `twitter_accounts:${accountId}`; + } else { + key = `telegram_accounts:${accountId}`; + } + + const data = await redisClient.hGetAll(key); + result.total_requests = data.total_requests ? parseInt(data.total_requests) : 0; + result.last_request = data.last_request ? data.last_request : null; + if (data.account_id) { + result.account_id = data.account_id; } + } catch (err) { + console.error('Redis operation failed:', err); } + return result; } + +