diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ea7af28 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 120, + "trailingComma": "none", + "singleQuote": true, + "semi": true, + "useTabs": false, + "tabWidth": 2, + "arrowParens": "always", + "jsxSingleQuote": true +} diff --git a/README.md b/README.md index 657f349..86e62bd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # ingestion-engine + Data ingestion service for fetching real-time content from social media APIs. diff --git a/package-lock.json b/package-lock.json index ee9b8fc..e17fe79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "@types/node": "^24.3.0", "esbuild": "^0.25.9", + "prettier": "^3.6.2", "tsx": "^4.20.5", "typescript": "^5.9.2" } @@ -1210,6 +1211,21 @@ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "license": "MIT" }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/readline2": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", diff --git a/package.json b/package.json index 8baa28b..2818606 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,12 @@ "redis": "^5.8.2", "telegram": "^2.26.22" }, + "scripts": { + "format": "prettier --write ." + }, "devDependencies": { "@types/node": "^24.3.0", + "prettier": "^3.6.2", "esbuild": "^0.25.9", "tsx": "^4.20.5", "typescript": "^5.9.2" diff --git a/src/fetchTelegramMessages.ts b/src/fetchTelegramMessages.ts index ab11bdb..86c4001 100644 --- a/src/fetchTelegramMessages.ts +++ b/src/fetchTelegramMessages.ts @@ -1,4 +1,4 @@ -import { Api, TelegramClient } from "telegram"; +import { Api, TelegramClient } from 'telegram'; import { trackApiKeyUsage } from './utils/redisUtils'; import { TelegramAccount } from './services/telegramAccountManager'; @@ -10,7 +10,7 @@ export async function fetchTelegramMessages( ): Promise { const channel = account.credentials.TELEGRAM_TG_CHANNEL; if (!channel) { - throw new Error("TELEGRAM_TG_CHANNEL is not set in account credentials."); + throw new Error('TELEGRAM_TG_CHANNEL is not set in account credentials.'); } if (process.env.DEBUG_TELEGRAM === '1') { @@ -38,13 +38,13 @@ export async function fetchTelegramMessages( const messages = await client.invoke( new Api.messages.GetHistory({ peer: entity, - limit: 10, + limit: 10 }) ); const out: TelegramMessages[] = []; - if ("messages" in messages) { + 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 : ''; @@ -54,9 +54,8 @@ export async function fetchTelegramMessages( console.log(formatted); } } else { - console.log("No messages property found in response:", messages); + console.log('No messages property found in response:', messages); } - return out; } diff --git a/src/index.ts b/src/index.ts index 2a018a0..0812353 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ import 'dotenv/config'; -import input from "input"; // interactive input for login +import input from 'input'; // interactive input for login import cron from 'node-cron'; -import { TelegramClient } from "telegram"; -import { StringSession } from "telegram/sessions"; +import { TelegramClient } from 'telegram'; +import { StringSession } from 'telegram/sessions'; import { fetchTelegramMessages } from './fetchTelegramMessages'; import { telegramAccountManager, TelegramAccount } from './services/telegramAccountManager'; @@ -34,24 +34,26 @@ async function createTelegramClient(account: TelegramAccount): Promise await input.text("Enter your phone number: "), - password: async () => await input.text("Enter 2FA password (if enabled): "), - phoneCode: async () => await input.text("Enter code you received: "), - onError: (err) => console.log(err), + phoneNumber: async () => await input.text('Enter your phone number: '), + password: async () => await input.text('Enter 2FA password (if enabled): '), + phoneCode: async () => await input.text('Enter code you received: '), + onError: (err) => console.log(err) }); console.log(`Logged in successfully for account: ${account.accountId}`); const saved = client.session.save(); - if (process.env.PRINT_TG_SESSION === "1" && isInteractive) { + if (process.env.PRINT_TG_SESSION === '1' && isInteractive) { // Emit an export-ready line deliberately, instead of dumping secrets in logs console.log(`export ${sessionKey}="${saved}"`); } @@ -63,7 +65,7 @@ async function createTelegramClient(account: TelegramAccount): Promise { - console.error("Failed to start Telegram cron:", err); + console.error('Failed to start Telegram cron:', err); }); - - - - diff --git a/src/lib/encryption.ts b/src/lib/encryption.ts index 7b723c8..e082236 100644 --- a/src/lib/encryption.ts +++ b/src/lib/encryption.ts @@ -7,35 +7,33 @@ const IV_LENGTH = 12; // per NIST recommendation for GCM // Use a strong key in production, ideally from a secure source let KEY: Buffer | null = null; function getKey(): Buffer { - const k = process.env.ENCRYPTION_KEY; - if (!k) throw new Error('ENCRYPTION_KEY environment variable must be set'); - if (!/^[0-9a-fA-F]{64}$/.test(k)) { - throw new Error('ENCRYPTION_KEY must be a 64-hex-char (256-bit) value'); - } - if (!KEY) KEY = Buffer.from(k, 'hex'); - return KEY; + const k = process.env.ENCRYPTION_KEY; + if (!k) throw new Error('ENCRYPTION_KEY environment variable must be set'); + if (!/^[0-9a-fA-F]{64}$/.test(k)) { + throw new Error('ENCRYPTION_KEY must be a 64-hex-char (256-bit) value'); + } + if (!KEY) KEY = Buffer.from(k, 'hex'); + return KEY; } - export function encrypt(text: string): string { - const iv = crypto.randomBytes(IV_LENGTH); - const cipher = crypto.createCipheriv(ALGORITHM, getKey(), iv); - const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); - const tag = cipher.getAuthTag(); - return `${SCHEME}:${iv.toString('hex')}:${tag.toString('hex')}:${ciphertext.toString('hex')}`; + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv(ALGORITHM, getKey(), iv); + const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); + const tag = cipher.getAuthTag(); + return `${SCHEME}:${iv.toString('hex')}:${tag.toString('hex')}:${ciphertext.toString('hex')}`; } - export function decrypt(text: string): string { - const parts = text.split(':'); - if (parts.length !== 4) throw new Error('Invalid payload format'); - const [scheme, ivHex, tagHex, dataHex] = parts; - if (scheme !== SCHEME) throw new Error(`Unsupported scheme: ${scheme}`); - const iv = Buffer.from(ivHex, 'hex'); - const tag = Buffer.from(tagHex, 'hex'); - const data = Buffer.from(dataHex, 'hex'); - const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), iv); - decipher.setAuthTag(tag); - const plaintext = Buffer.concat([decipher.update(data), decipher.final()]); - return plaintext.toString('utf8'); + const parts = text.split(':'); + if (parts.length !== 4) throw new Error('Invalid payload format'); + const [scheme, ivHex, tagHex, dataHex] = parts; + if (scheme !== SCHEME) throw new Error(`Unsupported scheme: ${scheme}`); + const iv = Buffer.from(ivHex, 'hex'); + const tag = Buffer.from(tagHex, 'hex'); + const data = Buffer.from(dataHex, 'hex'); + const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), iv); + decipher.setAuthTag(tag); + const plaintext = Buffer.concat([decipher.update(data), decipher.final()]); + return plaintext.toString('utf8'); } diff --git a/src/lib/utils/string.ts b/src/lib/utils/string.ts index 2a1e9a1..d3147cd 100644 --- a/src/lib/utils/string.ts +++ b/src/lib/utils/string.ts @@ -5,6 +5,6 @@ * Example: abcd1234efgh5678 -> abcd…5678 */ export function mask(v: string): string { - if (!v) return ''; - return v.length <= 8 ? '********' : `${v.slice(0, 4)}…${v.slice(-4)}`; + if (!v) return ''; + return v.length <= 8 ? '********' : `${v.slice(0, 4)}…${v.slice(-4)}`; } diff --git a/src/services/BaseAccountManager.ts b/src/services/BaseAccountManager.ts index 6705cdd..5aa95cd 100644 --- a/src/services/BaseAccountManager.ts +++ b/src/services/BaseAccountManager.ts @@ -1,69 +1,73 @@ import { createClient, RedisClientType } from 'redis'; export interface BaseAccount { - accountId: string; - credentials: Record; - lastUsed?: string; - totalRequests?: number; + accountId: string; + credentials: Record; + lastUsed?: string; + totalRequests?: number; } export abstract class BaseAccountManager { - protected redisClient: RedisClientType; - protected isConnected = false; - protected abstract platform: string; - protected abstract accountKey: string; - protected abstract usageKeyPrefix: string; + protected redisClient: RedisClientType; + protected isConnected = false; + protected abstract platform: string; + protected abstract accountKey: string; + protected abstract usageKeyPrefix: string; - constructor(redisUrl?: string) { - this.redisClient = createClient({ - url: redisUrl || process.env.REDIS_URL, - }); - this.redisClient.on('error', (err) => { - console.error(`Redis Client Error in ${this.platform}AccountManager:`, err); - }); - } + constructor(redisUrl?: string) { + this.redisClient = createClient({ + url: redisUrl || process.env.REDIS_URL + }); + this.redisClient.on('error', (err) => { + console.error(`Redis Client Error in ${this.platform}AccountManager:`, err); + }); + } - protected async ensureConnected(): Promise { - if (!this.isConnected) { - await this.redisClient.connect(); - this.isConnected = true; - } + protected async ensureConnected(): Promise { + if (!this.isConnected) { + await this.redisClient.connect(); + this.isConnected = true; } + } - protected abstract fetchAllAccounts(): Promise; + protected abstract fetchAllAccounts(): Promise; - async getEarliestUsedAccount(): Promise { - await this.ensureConnected(); - const accounts = await this.fetchAllAccounts(); - accounts.sort((a, b) => { - if (!a.lastUsed && !b.lastUsed) return 0; - if (!a.lastUsed) return -1; - if (!b.lastUsed) return 1; - return new Date(a.lastUsed).getTime() - new Date(b.lastUsed).getTime(); - }); - for (const acc of accounts) { - const lockKey = `lock:${this.platform}:${acc.accountId}`; - const ok = await this.redisClient.set(lockKey, '1', { NX: true, PX: 15000 }); - if (ok === 'OK') { - console.debug(`[${this.platform}AccountManager] Selected account=${acc.accountId} lastUsed=${acc.lastUsed ?? 'Never'} totalRequests=${acc.totalRequests ?? 0}`); - return acc; - } - } - throw new Error(`No available ${this.platform} accounts to claim (all locked).`); + async getEarliestUsedAccount(): Promise { + await this.ensureConnected(); + const accounts = await this.fetchAllAccounts(); + accounts.sort((a, b) => { + if (!a.lastUsed && !b.lastUsed) return 0; + if (!a.lastUsed) return -1; + if (!b.lastUsed) return 1; + return new Date(a.lastUsed).getTime() - new Date(b.lastUsed).getTime(); + }); + for (const acc of accounts) { + const lockKey = `lock:${this.platform}:${acc.accountId}`; + const ok = await this.redisClient.set(lockKey, '1', { NX: true, PX: 15000 }); + if (ok === 'OK') { + console.debug( + `[${this.platform}AccountManager] Selected account=${acc.accountId} lastUsed=${acc.lastUsed ?? 'Never'} totalRequests=${acc.totalRequests ?? 0}` + ); + return acc; + } } + throw new Error(`No available ${this.platform} accounts to claim (all locked).`); + } - async markAccountAsUsed(accountId: string): Promise { - await this.ensureConnected(); - await this.trackApiKeyUsageLocal(accountId); - try { await this.redisClient.del(`lock:${this.platform}:${accountId}`); } catch { } - } + async markAccountAsUsed(accountId: string): Promise { + await this.ensureConnected(); + await this.trackApiKeyUsageLocal(accountId); + try { + await this.redisClient.del(`lock:${this.platform}:${accountId}`); + } catch {} + } - protected abstract trackApiKeyUsageLocal(accountId: string): Promise; + protected abstract trackApiKeyUsageLocal(accountId: string): Promise; - async disconnect(): Promise { - if (this.isConnected) { - await this.redisClient.quit(); - this.isConnected = false; - } + async disconnect(): Promise { + if (this.isConnected) { + await this.redisClient.quit(); + this.isConnected = false; } + } } diff --git a/src/services/telegramAccountManager.ts b/src/services/telegramAccountManager.ts index 2e8567c..ef9058b 100644 --- a/src/services/telegramAccountManager.ts +++ b/src/services/telegramAccountManager.ts @@ -3,104 +3,103 @@ import { decrypt } from '../lib/encryption'; import { getApiKeyUsage, trackApiKeyUsage } from '../utils/redisUtils'; export interface TelegramAccount extends BaseAccount { - accountId: string; - credentials: { - TELEGRAM_API_ID: string; - TELEGRAM_API_HASH: string; - TELEGRAM_TG_CHANNEL: string; - }; - lastUsed?: string; - totalRequests?: number; + accountId: string; + credentials: { + TELEGRAM_API_ID: string; + TELEGRAM_API_HASH: string; + TELEGRAM_TG_CHANNEL: string; + }; + lastUsed?: string; + totalRequests?: number; } export class TelegramAccountManager extends BaseAccountManager { - protected platform = 'telegram'; - protected accountKey = 'telegram-accounts'; - protected usageKeyPrefix = 'telegram_accounts'; - - constructor(redisUrl?: string) { - super(redisUrl); + protected platform = 'telegram'; + protected accountKey = 'telegram-accounts'; + protected usageKeyPrefix = 'telegram_accounts'; + + constructor(redisUrl?: string) { + super(redisUrl); + } + + /** + * Fetch all Telegram accounts from Redis and decrypt their credentials + */ + protected async fetchAllAccounts(): Promise { + await this.ensureConnected(); + + const raw = await this.redisClient.get(this.accountKey); + if (!raw) { + throw new Error('No Telegram accounts found in Redis'); } - /** - * Fetch all Telegram accounts from Redis and decrypt their credentials - */ - protected async fetchAllAccounts(): Promise { - await this.ensureConnected(); - - const raw = await this.redisClient.get(this.accountKey); - if (!raw) { - throw new Error('No Telegram accounts found in Redis'); - } - - let encryptedAccounts: Record[]; - try { - encryptedAccounts = JSON.parse(raw); - } catch (e) { - throw new Error('Failed to parse Telegram accounts from Redis'); - } - - const accounts: TelegramAccount[] = []; - - for (let i = 0; i < encryptedAccounts.length; i++) { - const encryptedAccount = encryptedAccounts[i]; - - try { - // Decrypt credentials - const credentials = { - TELEGRAM_API_ID: decrypt(encryptedAccount.TELEGRAM_API_ID), - TELEGRAM_API_HASH: decrypt(encryptedAccount.TELEGRAM_API_HASH), - TELEGRAM_TG_CHANNEL: decrypt(encryptedAccount.TELEGRAM_TG_CHANNEL), - }; - - // Generate account ID from API ID (for uniqueness) - const accountId = `telegram_${credentials.TELEGRAM_API_ID}`; - - // Get usage statistics from Redis - const usage = await getApiKeyUsage({ accountId, platform: 'telegram' }); - - accounts.push({ - accountId, - credentials, - lastUsed: usage.last_request || undefined, - totalRequests: usage.total_requests - }); - } catch (e) { - console.warn(`Failed to decrypt Telegram account ${i + 1}:`, e); - continue; - } - } - - if (accounts.length === 0) { - throw new Error('No valid Telegram accounts could be decrypted'); - } - - return accounts; + let encryptedAccounts: Record[]; + try { + encryptedAccounts = JSON.parse(raw); + } catch (e) { + throw new Error('Failed to parse Telegram accounts from Redis'); } - - /** - * Local usage tracking for Telegram accounts - */ - protected async trackApiKeyUsageLocal(accountId: string): Promise { - await trackApiKeyUsage({ accountId, platform: 'telegram' }); + const accounts: TelegramAccount[] = []; + + for (let i = 0; i < encryptedAccounts.length; i++) { + const encryptedAccount = encryptedAccounts[i]; + + try { + // Decrypt credentials + const credentials = { + TELEGRAM_API_ID: decrypt(encryptedAccount.TELEGRAM_API_ID), + TELEGRAM_API_HASH: decrypt(encryptedAccount.TELEGRAM_API_HASH), + TELEGRAM_TG_CHANNEL: decrypt(encryptedAccount.TELEGRAM_TG_CHANNEL) + }; + + // Generate account ID from API ID (for uniqueness) + const accountId = `telegram_${credentials.TELEGRAM_API_ID}`; + + // Get usage statistics from Redis + const usage = await getApiKeyUsage({ accountId, platform: 'telegram' }); + + accounts.push({ + accountId, + credentials, + lastUsed: usage.last_request || undefined, + totalRequests: usage.total_requests + }); + } catch (e) { + console.warn(`Failed to decrypt Telegram account ${i + 1}:`, e); + continue; + } } - /** - * Get usage statistics for all accounts (no credentials) - */ - async getAllAccountsUsage(): Promise> { - const accounts = await this.fetchAllAccounts(); - return accounts.map(({ accountId, lastUsed, totalRequests }) => ({ accountId, lastUsed, totalRequests })); + if (accounts.length === 0) { + throw new Error('No valid Telegram accounts could be decrypted'); } - /** - * Get all accounts with credentials (full info) - */ - async getAllAccountsWithCredentials(): Promise { - return await this.fetchAllAccounts(); - } + return accounts; + } + + /** + * Local usage tracking for Telegram accounts + */ + protected async trackApiKeyUsageLocal(accountId: string): Promise { + await trackApiKeyUsage({ accountId, platform: 'telegram' }); + } + + /** + * Get usage statistics for all accounts (no credentials) + */ + async getAllAccountsUsage(): Promise> { + const accounts = await this.fetchAllAccounts(); + return accounts.map(({ accountId, lastUsed, totalRequests }) => ({ accountId, lastUsed, totalRequests })); + } + + /** + * Get all accounts with credentials (full info) + */ + async getAllAccountsWithCredentials(): Promise { + return await this.fetchAllAccounts(); + } } // Export singleton instance -export const telegramAccountManager = new TelegramAccountManager(); \ No newline at end of file +export const telegramAccountManager = new TelegramAccountManager(); diff --git a/src/services/twitterAccountManager.ts b/src/services/twitterAccountManager.ts index 980e7db..e07a8bd 100644 --- a/src/services/twitterAccountManager.ts +++ b/src/services/twitterAccountManager.ts @@ -3,114 +3,115 @@ import { decrypt } from '../lib/encryption'; import { createHash } from 'crypto'; export interface TwitterAccount extends BaseAccount { - accountId: string; - credentials: { - TWITTER_AUTH_TOKEN: string; - TWITTER_BEARER: string; - TWITTER_CSRF_TOKEN: string; - }; - lastUsed?: string; - totalRequests?: number; + accountId: string; + credentials: { + TWITTER_AUTH_TOKEN: string; + TWITTER_BEARER: string; + TWITTER_CSRF_TOKEN: string; + }; + lastUsed?: string; + totalRequests?: number; } export class TwitterAccountManager extends BaseAccountManager { - protected platform = 'twitter'; - protected accountKey = 'twitter-accounts'; - protected usageKeyPrefix = 'twitter_accounts'; - - constructor(redisUrl?: string) { - super(redisUrl); + protected platform = 'twitter'; + protected accountKey = 'twitter-accounts'; + protected usageKeyPrefix = 'twitter_accounts'; + + constructor(redisUrl?: string) { + super(redisUrl); + } + + /** + * Fetch all Twitter accounts from Redis and decrypt their credentials + */ + protected async fetchAllAccounts(): Promise { + await this.ensureConnected(); + + const raw = await this.redisClient.get(this.accountKey); + if (!raw) { + throw new Error('No Twitter accounts found in Redis'); } - /** - * Fetch all Twitter accounts from Redis and decrypt their credentials - */ - protected async fetchAllAccounts(): Promise { - await this.ensureConnected(); - - const raw = await this.redisClient.get(this.accountKey); - if (!raw) { - throw new Error('No Twitter accounts found in Redis'); - } - - let encryptedAccounts: Record[]; - try { - encryptedAccounts = JSON.parse(raw); - } catch (e) { - throw new Error('Failed to parse Twitter accounts from Redis'); - } - - const accounts: TwitterAccount[] = []; - - for (let i = 0; i < encryptedAccounts.length; i++) { - const encryptedAccount = encryptedAccounts[i]; - - try { - // Decrypt credentials - const credentials = { - TWITTER_AUTH_TOKEN: decrypt(encryptedAccount.TWITTER_AUTH_TOKEN), - TWITTER_BEARER: decrypt(encryptedAccount.TWITTER_BEARER), - TWITTER_CSRF_TOKEN: decrypt(encryptedAccount.TWITTER_CSRF_TOKEN), - }; - - // Generate stable, non-reversible account ID (SHA-256, 12 hex chars) - const token = credentials.TWITTER_AUTH_TOKEN; - const accountId = `twitter_${createHash('sha256').update(token).digest('hex').slice(0, 12)}`; - - // Get usage statistics from Redis (same client) - const usage = await this.getApiKeyUsageLocal(accountId); - - accounts.push({ - accountId, - credentials, - lastUsed: usage.last_request || undefined, - totalRequests: usage.total_requests - }); - } catch (e) { - console.warn(`Failed to decrypt account ${i + 1}:`, e); - continue; - } - } - - if (accounts.length === 0) { - throw new Error('No valid Twitter accounts could be decrypted'); - } - - return accounts; + let encryptedAccounts: Record[]; + try { + encryptedAccounts = JSON.parse(raw); + } catch (e) { + throw new Error('Failed to parse Twitter accounts from Redis'); } + const accounts: TwitterAccount[] = []; + + for (let i = 0; i < encryptedAccounts.length; i++) { + const encryptedAccount = encryptedAccounts[i]; - /** - * Local usage read for Twitter accounts (using the same Redis client) - */ - private async getApiKeyUsageLocal(accountId: string): Promise<{ total_requests: number; last_request: string | null }> { - await this.ensureConnected(); - const key = `twitter_accounts:${accountId}`; - const data = await this.redisClient.hGetAll(key); - return { - total_requests: data?.total_requests ? parseInt(data.total_requests, 10) : 0, - last_request: data?.last_request ?? null, + try { + // Decrypt credentials + const credentials = { + TWITTER_AUTH_TOKEN: decrypt(encryptedAccount.TWITTER_AUTH_TOKEN), + TWITTER_BEARER: decrypt(encryptedAccount.TWITTER_BEARER), + TWITTER_CSRF_TOKEN: decrypt(encryptedAccount.TWITTER_CSRF_TOKEN) }; - } - protected async trackApiKeyUsageLocal(accountId: string): Promise { - await this.ensureConnected(); - const key = `twitter_accounts:${accountId}`; - const now = new Date().toISOString(); - await this.redisClient - .multi() - .hIncrBy(key, 'total_requests', 1) - .hSet(key, { last_request: now, account_id: accountId }) - .exec(); + // Generate stable, non-reversible account ID (SHA-256, 12 hex chars) + const token = credentials.TWITTER_AUTH_TOKEN; + const accountId = `twitter_${createHash('sha256').update(token).digest('hex').slice(0, 12)}`; + + // Get usage statistics from Redis (same client) + const usage = await this.getApiKeyUsageLocal(accountId); + + accounts.push({ + accountId, + credentials, + lastUsed: usage.last_request || undefined, + totalRequests: usage.total_requests + }); + } catch (e) { + console.warn(`Failed to decrypt account ${i + 1}:`, e); + continue; + } } - /** - * Get usage statistics for all accounts - */ - async getAllAccountsUsage(): Promise { - return await this.fetchAllAccounts(); + if (accounts.length === 0) { + throw new Error('No valid Twitter accounts could be decrypted'); } + + return accounts; + } + + /** + * Local usage read for Twitter accounts (using the same Redis client) + */ + private async getApiKeyUsageLocal( + accountId: string + ): Promise<{ total_requests: number; last_request: string | null }> { + await this.ensureConnected(); + const key = `twitter_accounts:${accountId}`; + const data = await this.redisClient.hGetAll(key); + return { + total_requests: data?.total_requests ? parseInt(data.total_requests, 10) : 0, + last_request: data?.last_request ?? null + }; + } + + protected async trackApiKeyUsageLocal(accountId: string): Promise { + await this.ensureConnected(); + const key = `twitter_accounts:${accountId}`; + const now = new Date().toISOString(); + await this.redisClient + .multi() + .hIncrBy(key, 'total_requests', 1) + .hSet(key, { last_request: now, account_id: accountId }) + .exec(); + } + + /** + * Get usage statistics for all accounts + */ + async getAllAccountsUsage(): Promise { + return await this.fetchAllAccounts(); + } } // Export singleton instance -export const twitterAccountManager = new TwitterAccountManager(); \ No newline at end of file +export const twitterAccountManager = new TwitterAccountManager(); diff --git a/src/telegram.ts b/src/telegram.ts index 9870454..93f553f 100644 --- a/src/telegram.ts +++ b/src/telegram.ts @@ -1,51 +1,49 @@ -import { TelegramClient } from "telegram"; -import { StringSession } from "telegram/sessions"; -import input from "input"; // interactive input for login -import { Api } from "telegram"; +import { TelegramClient } from 'telegram'; +import { StringSession } from 'telegram/sessions'; +import input from 'input'; // interactive input for login +import { Api } from 'telegram'; // Replace these with your values const apiId = 26767039; -const apiHash = "5c9c82971de30b5e71030c27878b8115"; -const stringSession = new StringSession(""); // empty = new login +const apiHash = '5c9c82971de30b5e71030c27878b8115'; +const stringSession = new StringSession(''); // empty = new login (async () => { - console.log("Starting Telegram client..."); + console.log('Starting Telegram client...'); const client = new TelegramClient(stringSession, apiId, apiHash, { - connectionRetries: 5, + connectionRetries: 5 }); await client.start({ - phoneNumber: async () => await input.text("Enter your phone number: "), - password: async () => await input.text("Enter 2FA password (if enabled): "), - phoneCode: async () => await input.text("Enter code you received: "), - onError: (err) => console.log(err), + phoneNumber: async () => await input.text('Enter your phone number: '), + password: async () => await input.text('Enter 2FA password (if enabled): '), + phoneCode: async () => await input.text('Enter code you received: '), + onError: (err) => console.log(err) }); - console.log("Logged in successfully!"); - if (process.env.PRINT_TG_SESSION === "1") { - console.log("Your session string:", client.session.save()); + console.log('Logged in successfully!'); + if (process.env.PRINT_TG_SESSION === '1') { + console.log('Your session string:', client.session.save()); } else { - console.log( - "Session created. Set PRINT_TG_SESSION=1 to print it explicitly." - ); + console.log('Session created. Set PRINT_TG_SESSION=1 to print it explicitly.'); } // Example: fetch last 10 messages from a public channel - const channel = "@garden_btc"; // replace with any public channel username + const channel = '@garden_btc'; // replace with any public channel username const messages = await client.invoke( new Api.messages.GetHistory({ peer: channel, - limit: 10, + limit: 10 }) ); - if ("messages" in messages) { + if ('messages' in messages) { messages.messages.forEach((msg: any) => { console.log(msg.id, msg.message, msg.date); }); } else { - console.log("No messages property found in response:", messages); + console.log('No messages property found in response:', messages); } await client.disconnect(); diff --git a/src/tests/rotationDemo.ts b/src/tests/rotationDemo.ts index 4d56564..5593715 100644 --- a/src/tests/rotationDemo.ts +++ b/src/tests/rotationDemo.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * Demo script showing Twitter account rotation in action - * + * * This simulates the 5-minute rotation behavior by: * 1. Getting the earliest used account * 2. Using it for a "fetch operation" (simulated) @@ -13,84 +13,83 @@ import { twitterAccountManager } from '../services/twitterAccountManager'; import 'dotenv/config'; async function simulateTwitterFetch() { - console.log('🐦 Twitter Account Rotation Demo'); - console.log('================================\n'); - - try { - // Show initial state - console.log('📊 Initial account usage state:'); - const initialAccounts = await twitterAccountManager.getAllAccountsUsage(); - initialAccounts.forEach((account, index) => { - console.log(` ${index + 1}. ${account.accountId.slice(0, 20)}...`); - console.log(` Last used: ${account.lastUsed || 'Never'}`); - console.log(` Total requests: ${account.totalRequests || 0}`); - }); - - console.log('\n🔄 Simulating 5-minute rotation cycles...\n'); - - // Simulate 5 fetch cycles (representing 5-minute intervals) - for (let cycle = 1; cycle <= 5; cycle++) { - console.log(`--- Cycle ${cycle} (${cycle * 5} minutes) ---`); - - // Get the account that should be used (earliest used) - const selectedAccount = await twitterAccountManager.getEarliestUsedAccount(); - - console.log(`🎯 Selected: ${selectedAccount.accountId.slice(0, 20)}...`); - console.log(` Last used: ${selectedAccount.lastUsed || 'Never'}`); - console.log(` Total requests: ${selectedAccount.totalRequests || 0}`); - - // Simulate using the account for Twitter API calls - console.log(' 📡 Simulating Twitter API fetch...'); - - // Mark the account as used (this updates the timestamp) - await twitterAccountManager.markAccountAsUsed(selectedAccount.accountId); - - console.log(' ✅ Account marked as used\n'); - - // Wait a moment to ensure timestamp differences - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - console.log('📈 Final account usage state:'); - const finalAccounts = await twitterAccountManager.getAllAccountsUsage(); - - // Sort by last used to show the rotation order - finalAccounts.sort((a, b) => { - if (!a.lastUsed && !b.lastUsed) return 0; - if (!a.lastUsed) return -1; - if (!b.lastUsed) return 1; - return new Date(a.lastUsed).getTime() - new Date(b.lastUsed).getTime(); - }); - - finalAccounts.forEach((account, index) => { - const isNext = index === 0; - const prefix = isNext ? '👉' : ' '; - - console.log(`${prefix} ${account.accountId.slice(0, 20)}...`); - console.log(` Last used: ${account.lastUsed || 'Never'}`); - console.log(` Total requests: ${account.totalRequests || 0}`); - - if (isNext) { - console.log(` ⏭️ Will be used next`); - } - }); - - console.log('\n✨ Demo completed! The system will automatically rotate accounts every 5 minutes.'); - console.log(' The account with the earliest "last_request" timestamp gets selected next.'); - - } catch (error) { - console.error('❌ Demo failed:', error); - - if (error instanceof Error && error.message.includes('No Twitter accounts found')) { - console.log('\n💡 To set up accounts:'); - console.log(' 1. Add Twitter credentials to your .env file'); - console.log(' 2. Run: npm run move-env-to-redis'); - console.log(' 3. Run this demo again'); - } - } finally { - await twitterAccountManager.disconnect(); + console.log('🐦 Twitter Account Rotation Demo'); + console.log('================================\n'); + + try { + // Show initial state + console.log('📊 Initial account usage state:'); + const initialAccounts = await twitterAccountManager.getAllAccountsUsage(); + initialAccounts.forEach((account, index) => { + console.log(` ${index + 1}. ${account.accountId.slice(0, 20)}...`); + console.log(` Last used: ${account.lastUsed || 'Never'}`); + console.log(` Total requests: ${account.totalRequests || 0}`); + }); + + console.log('\n🔄 Simulating 5-minute rotation cycles...\n'); + + // Simulate 5 fetch cycles (representing 5-minute intervals) + for (let cycle = 1; cycle <= 5; cycle++) { + console.log(`--- Cycle ${cycle} (${cycle * 5} minutes) ---`); + + // Get the account that should be used (earliest used) + const selectedAccount = await twitterAccountManager.getEarliestUsedAccount(); + + console.log(`🎯 Selected: ${selectedAccount.accountId.slice(0, 20)}...`); + console.log(` Last used: ${selectedAccount.lastUsed || 'Never'}`); + console.log(` Total requests: ${selectedAccount.totalRequests || 0}`); + + // Simulate using the account for Twitter API calls + console.log(' 📡 Simulating Twitter API fetch...'); + + // Mark the account as used (this updates the timestamp) + await twitterAccountManager.markAccountAsUsed(selectedAccount.accountId); + + console.log(' ✅ Account marked as used\n'); + + // Wait a moment to ensure timestamp differences + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + console.log('📈 Final account usage state:'); + const finalAccounts = await twitterAccountManager.getAllAccountsUsage(); + + // Sort by last used to show the rotation order + finalAccounts.sort((a, b) => { + if (!a.lastUsed && !b.lastUsed) return 0; + if (!a.lastUsed) return -1; + if (!b.lastUsed) return 1; + return new Date(a.lastUsed).getTime() - new Date(b.lastUsed).getTime(); + }); + + finalAccounts.forEach((account, index) => { + const isNext = index === 0; + const prefix = isNext ? '👉' : ' '; + + console.log(`${prefix} ${account.accountId.slice(0, 20)}...`); + console.log(` Last used: ${account.lastUsed || 'Never'}`); + console.log(` Total requests: ${account.totalRequests || 0}`); + + if (isNext) { + console.log(` ⏭️ Will be used next`); + } + }); + + console.log('\n✨ Demo completed! The system will automatically rotate accounts every 5 minutes.'); + console.log(' The account with the earliest "last_request" timestamp gets selected next.'); + } catch (error) { + console.error('❌ Demo failed:', error); + + if (error instanceof Error && error.message.includes('No Twitter accounts found')) { + console.log('\n💡 To set up accounts:'); + console.log(' 1. Add Twitter credentials to your .env file'); + console.log(' 2. Run: npm run move-env-to-redis'); + console.log(' 3. Run this demo again'); } + } finally { + await twitterAccountManager.disconnect(); + } } // Run the demo -simulateTwitterFetch().catch(console.error); \ No newline at end of file +simulateTwitterFetch().catch(console.error); diff --git a/src/tests/telegramRotationDemo.ts b/src/tests/telegramRotationDemo.ts index 2bc79ef..ae65ba0 100644 --- a/src/tests/telegramRotationDemo.ts +++ b/src/tests/telegramRotationDemo.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * Demo script showing Telegram account rotation in action - * + * * This simulates the 5-minute rotation behavior by: * 1. Getting the earliest used Telegram account * 2. Using it for a "fetch operation" (simulated) @@ -13,87 +13,86 @@ import 'dotenv/config'; import { telegramAccountManager } from '../services/telegramAccountManager'; async function simulateTelegramFetch() { - console.log('📱 Telegram Account Rotation Demo'); - console.log('=================================\n'); - - try { - // Show initial state - console.log('📊 Initial account usage state:'); - const initialAccounts = await telegramAccountManager.getAllAccountsWithCredentials(); - initialAccounts.forEach((account, index) => { - console.log(` ${index + 1}. ${account.accountId}`); - console.log(` Last used: ${account.lastUsed || 'Never'}`); - console.log(` Total requests: ${account.totalRequests || 0}`); - console.log(` Channel: ${account.credentials.TELEGRAM_TG_CHANNEL}`); - }); - - console.log('\n🔄 Simulating 5-minute rotation cycles...\n'); - - // Simulate 5 fetch cycles (representing 5-minute intervals) - for (let cycle = 1; cycle <= 5; cycle++) { - console.log(`--- Cycle ${cycle} (${cycle * 5} minutes) ---`); - - // Get the account that should be used (earliest used) - const selectedAccount = await telegramAccountManager.getEarliestUsedAccount(); - - console.log(`🎯 Selected: ${selectedAccount.accountId}`); - console.log(` Last used: ${selectedAccount.lastUsed || 'Never'}`); - console.log(` Total requests: ${selectedAccount.totalRequests || 0}`); - console.log(` Channel: ${selectedAccount.credentials.TELEGRAM_TG_CHANNEL}`); - - // Simulate using the account for Telegram API calls - console.log(' 📡 Simulating Telegram API fetch...'); - - // Mark the account as used (this updates the timestamp) - await telegramAccountManager.markAccountAsUsed(selectedAccount.accountId); - - console.log(' ✅ Account marked as used\n'); - - // Wait a moment to ensure timestamp differences - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - console.log('📈 Final account usage state:'); - const finalAccounts = await telegramAccountManager.getAllAccountsWithCredentials(); - - // Sort by last used to show the rotation order - finalAccounts.sort((a, b) => { - if (!a.lastUsed && !b.lastUsed) return 0; - if (!a.lastUsed) return -1; - if (!b.lastUsed) return 1; - return new Date(a.lastUsed).getTime() - new Date(b.lastUsed).getTime(); - }); - - finalAccounts.forEach((account, index) => { - const isNext = index === 0; - const prefix = isNext ? '👉' : ' '; - - console.log(`${prefix} ${account.accountId}`); - console.log(` Last used: ${account.lastUsed || 'Never'}`); - console.log(` Total requests: ${account.totalRequests || 0}`); - console.log(` Channel: ${account.credentials.TELEGRAM_TG_CHANNEL}`); - - if (isNext) { - console.log(` ⏭️ Will be used next`); - } - }); - - console.log('\n✨ Demo completed! The system will automatically rotate Telegram accounts every 5 minutes.'); - console.log(' The account with the earliest "last_request" timestamp gets selected next.'); - - } catch (error) { - console.error('❌ Demo failed:', error); - - if (error instanceof Error && error.message.includes('No Telegram accounts found')) { - console.log('\n💡 To set up Telegram accounts:'); - console.log(' 1. Add Telegram credentials to your .env file'); - console.log(' 2. Run: npm run move-env-to-redis'); - console.log(' 3. Run this demo again'); - } - } finally { - await telegramAccountManager.disconnect(); + console.log('📱 Telegram Account Rotation Demo'); + console.log('=================================\n'); + + try { + // Show initial state + console.log('📊 Initial account usage state:'); + const initialAccounts = await telegramAccountManager.getAllAccountsWithCredentials(); + initialAccounts.forEach((account, index) => { + console.log(` ${index + 1}. ${account.accountId}`); + console.log(` Last used: ${account.lastUsed || 'Never'}`); + console.log(` Total requests: ${account.totalRequests || 0}`); + console.log(` Channel: ${account.credentials.TELEGRAM_TG_CHANNEL}`); + }); + + console.log('\n🔄 Simulating 5-minute rotation cycles...\n'); + + // Simulate 5 fetch cycles (representing 5-minute intervals) + for (let cycle = 1; cycle <= 5; cycle++) { + console.log(`--- Cycle ${cycle} (${cycle * 5} minutes) ---`); + + // Get the account that should be used (earliest used) + const selectedAccount = await telegramAccountManager.getEarliestUsedAccount(); + + console.log(`🎯 Selected: ${selectedAccount.accountId}`); + console.log(` Last used: ${selectedAccount.lastUsed || 'Never'}`); + console.log(` Total requests: ${selectedAccount.totalRequests || 0}`); + console.log(` Channel: ${selectedAccount.credentials.TELEGRAM_TG_CHANNEL}`); + + // Simulate using the account for Telegram API calls + console.log(' 📡 Simulating Telegram API fetch...'); + + // Mark the account as used (this updates the timestamp) + await telegramAccountManager.markAccountAsUsed(selectedAccount.accountId); + + console.log(' ✅ Account marked as used\n'); + + // Wait a moment to ensure timestamp differences + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + console.log('📈 Final account usage state:'); + const finalAccounts = await telegramAccountManager.getAllAccountsWithCredentials(); + + // Sort by last used to show the rotation order + finalAccounts.sort((a, b) => { + if (!a.lastUsed && !b.lastUsed) return 0; + if (!a.lastUsed) return -1; + if (!b.lastUsed) return 1; + return new Date(a.lastUsed).getTime() - new Date(b.lastUsed).getTime(); + }); + + finalAccounts.forEach((account, index) => { + const isNext = index === 0; + const prefix = isNext ? '👉' : ' '; + + console.log(`${prefix} ${account.accountId}`); + console.log(` Last used: ${account.lastUsed || 'Never'}`); + console.log(` Total requests: ${account.totalRequests || 0}`); + console.log(` Channel: ${account.credentials.TELEGRAM_TG_CHANNEL}`); + + if (isNext) { + console.log(` ⏭️ Will be used next`); + } + }); + + console.log('\n✨ Demo completed! The system will automatically rotate Telegram accounts every 5 minutes.'); + console.log(' The account with the earliest "last_request" timestamp gets selected next.'); + } catch (error) { + console.error('❌ Demo failed:', error); + + if (error instanceof Error && error.message.includes('No Telegram accounts found')) { + console.log('\n💡 To set up Telegram accounts:'); + console.log(' 1. Add Telegram credentials to your .env file'); + console.log(' 2. Run: npm run move-env-to-redis'); + console.log(' 3. Run this demo again'); } + } finally { + await telegramAccountManager.disconnect(); + } } // Run the demo -simulateTelegramFetch().catch(console.error); \ No newline at end of file +simulateTelegramFetch().catch(console.error); diff --git a/src/tests/testRotation.ts b/src/tests/testRotation.ts index b054a58..9b68286 100644 --- a/src/tests/testRotation.ts +++ b/src/tests/testRotation.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * Test script for Twitter account rotation system - * + * * This script tests: * 1. Fetching accounts from Redis * 2. Account rotation logic (earliest used first) @@ -14,126 +14,125 @@ import { createClient } from 'redis'; import 'dotenv/config'; async function testAccountRotation() { - console.log('🔍 Testing Twitter Account Rotation System\n'); - - try { - // Test 1: Get all accounts and show their usage - console.log('📊 Test 1: Fetching all Twitter accounts...'); - const allAccounts = await twitterAccountManager.getAllAccountsUsage(); - - if (allAccounts.length === 0) { - console.log('❌ No Twitter accounts found in Redis'); - console.log(' Make sure you have run the moveEnvToRedis script first'); - return; - } - - console.log(`✅ Found ${allAccounts.length} Twitter accounts:`); - allAccounts.forEach((account, index) => { - console.log(` Account ${index + 1}: ${account.accountId}`); - console.log(` Last used: ${account.lastUsed || 'Never'}`); - console.log(` Total requests: ${account.totalRequests || 0}`); - }); - - console.log('\n'); - - // Test 2: Get earliest used account multiple times - console.log('🔄 Test 2: Testing account rotation logic...'); - - for (let i = 1; i <= 3; i++) { - console.log(`\nIteration ${i}:`); - - const earliestAccount = await twitterAccountManager.getEarliestUsedAccount(); - console.log(` Selected account: ${earliestAccount.accountId}`); - console.log(` Last used: ${earliestAccount.lastUsed || 'Never'}`); - - // Mark the account as used - console.log(` Marking account as used...`); - await twitterAccountManager.markAccountAsUsed(earliestAccount.accountId); - - // Wait a moment for the timestamp to change - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - console.log('\n'); - - // Test 3: Show final state - console.log('📈 Test 3: Final usage state after rotation...'); - const finalAccounts = await twitterAccountManager.getAllAccountsUsage(); - finalAccounts.forEach((account, index) => { - console.log(` Account ${index + 1}: ${account.accountId}`); - console.log(` Last used: ${account.lastUsed || 'Never'}`); - console.log(` Total requests: ${account.totalRequests || 0}`); - }); - - console.log('\n'); - - // Test 4: Verify rotation order - console.log('🎯 Test 4: Verifying rotation order...'); - const nextAccount = await twitterAccountManager.getEarliestUsedAccount(); - console.log(` Next account to be used: ${nextAccount.accountId}`); - console.log(` Last used: ${nextAccount.lastUsed || 'Never'}`); - - console.log('\n✅ All tests completed successfully!'); - - } catch (error) { - console.error('❌ Test failed:', error); - } finally { - await twitterAccountManager.disconnect(); - } -} + console.log('🔍 Testing Twitter Account Rotation System\n'); -async function testRedisConnection() { - console.log('🔗 Testing Redis connection...'); + try { + // Test 1: Get all accounts and show their usage + console.log('📊 Test 1: Fetching all Twitter accounts...'); + const allAccounts = await twitterAccountManager.getAllAccountsUsage(); - const redisClient = createClient({ - url: process.env.REDIS_URL || 'redis://localhost:6379' + if (allAccounts.length === 0) { + console.log('❌ No Twitter accounts found in Redis'); + console.log(' Make sure you have run the moveEnvToRedis script first'); + return; + } + + console.log(`✅ Found ${allAccounts.length} Twitter accounts:`); + allAccounts.forEach((account, index) => { + console.log(` Account ${index + 1}: ${account.accountId}`); + console.log(` Last used: ${account.lastUsed || 'Never'}`); + console.log(` Total requests: ${account.totalRequests || 0}`); }); - try { - await redisClient.connect(); - - // Check if twitter-accounts key exists - const twitterAccounts = await redisClient.get('twitter-accounts'); - if (!twitterAccounts) { - console.log('⚠️ No twitter-accounts found in Redis'); - console.log(' Run this first: npm run move-env-to-redis'); - return false; - } - - // Try to parse the accounts - const accounts = JSON.parse(twitterAccounts); - console.log(`✅ Found ${accounts.length} encrypted Twitter accounts in Redis`); - - return true; - } catch (error) { - console.error('❌ Redis connection failed:', error); - return false; - } finally { - await redisClient.quit(); + console.log('\n'); + + // Test 2: Get earliest used account multiple times + console.log('🔄 Test 2: Testing account rotation logic...'); + + for (let i = 1; i <= 3; i++) { + console.log(`\nIteration ${i}:`); + + const earliestAccount = await twitterAccountManager.getEarliestUsedAccount(); + console.log(` Selected account: ${earliestAccount.accountId}`); + console.log(` Last used: ${earliestAccount.lastUsed || 'Never'}`); + + // Mark the account as used + console.log(` Marking account as used...`); + await twitterAccountManager.markAccountAsUsed(earliestAccount.accountId); + + // Wait a moment for the timestamp to change + await new Promise((resolve) => setTimeout(resolve, 1000)); } + + console.log('\n'); + + // Test 3: Show final state + console.log('📈 Test 3: Final usage state after rotation...'); + const finalAccounts = await twitterAccountManager.getAllAccountsUsage(); + finalAccounts.forEach((account, index) => { + console.log(` Account ${index + 1}: ${account.accountId}`); + console.log(` Last used: ${account.lastUsed || 'Never'}`); + console.log(` Total requests: ${account.totalRequests || 0}`); + }); + + console.log('\n'); + + // Test 4: Verify rotation order + console.log('🎯 Test 4: Verifying rotation order...'); + const nextAccount = await twitterAccountManager.getEarliestUsedAccount(); + console.log(` Next account to be used: ${nextAccount.accountId}`); + console.log(` Last used: ${nextAccount.lastUsed || 'Never'}`); + + console.log('\n✅ All tests completed successfully!'); + } catch (error) { + console.error('❌ Test failed:', error); + } finally { + await twitterAccountManager.disconnect(); + } } -async function main() { - console.log('Twitter Account Rotation Test Suite'); - console.log('====================================\n'); - - // Check Redis connection and data first - const redisOk = await testRedisConnection(); - if (!redisOk) { - console.log('\n❌ Please fix Redis issues before continuing'); - process.exit(1); - } +async function testRedisConnection() { + console.log('🔗 Testing Redis connection...'); - // Check encryption key - if (!process.env.ENCRYPTION_KEY) { - console.log('❌ ENCRYPTION_KEY environment variable not set'); - process.exit(1); + const redisClient = createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + + try { + await redisClient.connect(); + + // Check if twitter-accounts key exists + const twitterAccounts = await redisClient.get('twitter-accounts'); + if (!twitterAccounts) { + console.log('⚠️ No twitter-accounts found in Redis'); + console.log(' Run this first: npm run move-env-to-redis'); + return false; } - console.log('\n'); + // Try to parse the accounts + const accounts = JSON.parse(twitterAccounts); + console.log(`✅ Found ${accounts.length} encrypted Twitter accounts in Redis`); + + return true; + } catch (error) { + console.error('❌ Redis connection failed:', error); + return false; + } finally { + await redisClient.quit(); + } +} - // Run rotation tests - await testAccountRotation(); +async function main() { + console.log('Twitter Account Rotation Test Suite'); + console.log('====================================\n'); + + // Check Redis connection and data first + const redisOk = await testRedisConnection(); + if (!redisOk) { + console.log('\n❌ Please fix Redis issues before continuing'); + process.exit(1); + } + + // Check encryption key + if (!process.env.ENCRYPTION_KEY) { + console.log('❌ ENCRYPTION_KEY environment variable not set'); + process.exit(1); + } + + console.log('\n'); + + // Run rotation tests + await testAccountRotation(); } // Export for use in other scripts @@ -141,5 +140,5 @@ export { testAccountRotation, testRedisConnection }; // Run if executed directly if (require.main === module) { - main().catch(console.error); -} \ No newline at end of file + main().catch(console.error); +} diff --git a/src/tests/testTelegramRotation.ts b/src/tests/testTelegramRotation.ts index af8d4e6..839fa13 100644 --- a/src/tests/testTelegramRotation.ts +++ b/src/tests/testTelegramRotation.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * Test script for Telegram account rotation system - * + * * This script tests: * 1. Fetching Telegram accounts from Redis * 2. Account rotation logic (earliest used first) @@ -14,128 +14,127 @@ import { telegramAccountManager, TelegramAccount } from '../services/telegramAcc import { createClient } from 'redis'; async function testTelegramAccountRotation() { - console.log('📱 Testing Telegram Account Rotation System\n'); - - try { - // Test 1: Get all accounts and show their usage - console.log('📊 Test 1: Fetching all Telegram accounts...'); - const allAccounts = await telegramAccountManager.getAllAccountsWithCredentials(); - - if (allAccounts.length === 0) { - console.log('❌ No Telegram accounts found in Redis'); - console.log(' Make sure you have run the moveEnvToRedis script first'); - return; - } - - console.log(`✅ Found ${allAccounts.length} Telegram accounts:`); - allAccounts.forEach((account, index) => { - console.log(` Account ${index + 1}: ${account.accountId}`); - console.log(` Last used: ${account.lastUsed || 'Never'}`); - console.log(` Total requests: ${account.totalRequests || 0}`); - console.log(` Channel: ${account.credentials.TELEGRAM_TG_CHANNEL}`); - }); - - console.log('\n'); - - // Test 2: Get earliest used account multiple times - console.log('🔄 Test 2: Testing account rotation logic...'); - - for (let i = 1; i <= 3; i++) { - console.log(`\nIteration ${i}:`); - - const earliestAccount = await telegramAccountManager.getEarliestUsedAccount(); - console.log(` Selected account: ${earliestAccount.accountId}`); - console.log(` Last used: ${earliestAccount.lastUsed || 'Never'}`); - console.log(` Channel: ${earliestAccount.credentials.TELEGRAM_TG_CHANNEL}`); - - // Mark the account as used - console.log(` Marking account as used...`); - await telegramAccountManager.markAccountAsUsed(earliestAccount.accountId); - - // Wait a moment for the timestamp to change - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - console.log('\n'); - - // Test 3: Show final state - console.log('📈 Test 3: Final usage state after rotation...'); - const finalAccounts = await telegramAccountManager.getAllAccountsWithCredentials(); - finalAccounts.forEach((account, index) => { - console.log(` Account ${index + 1}: ${account.accountId}`); - console.log(` Last used: ${account.lastUsed || 'Never'}`); - console.log(` Total requests: ${account.totalRequests || 0}`); - }); - - console.log('\n'); - - // Test 4: Verify rotation order - console.log('🎯 Test 4: Verifying rotation order...'); - const nextAccount = await telegramAccountManager.getEarliestUsedAccount(); - console.log(` Next account to be used: ${nextAccount.accountId}`); - console.log(` Last used: ${nextAccount.lastUsed || 'Never'}`); - - console.log('\n✅ All Telegram tests completed successfully!'); - - } catch (error) { - console.error('❌ Test failed:', error); - } finally { - await telegramAccountManager.disconnect(); - } -} + console.log('📱 Testing Telegram Account Rotation System\n'); -async function testTelegramRedisConnection() { - console.log('🔗 Testing Redis connection for Telegram accounts...'); + try { + // Test 1: Get all accounts and show their usage + console.log('📊 Test 1: Fetching all Telegram accounts...'); + const allAccounts = await telegramAccountManager.getAllAccountsWithCredentials(); - const redisClient = createClient({ - url: process.env.REDIS_URL || 'redis://localhost:6379' + if (allAccounts.length === 0) { + console.log('❌ No Telegram accounts found in Redis'); + console.log(' Make sure you have run the moveEnvToRedis script first'); + return; + } + + console.log(`✅ Found ${allAccounts.length} Telegram accounts:`); + allAccounts.forEach((account, index) => { + console.log(` Account ${index + 1}: ${account.accountId}`); + console.log(` Last used: ${account.lastUsed || 'Never'}`); + console.log(` Total requests: ${account.totalRequests || 0}`); + console.log(` Channel: ${account.credentials.TELEGRAM_TG_CHANNEL}`); }); - try { - await redisClient.connect(); - - // Check if telegram-accounts key exists - const telegramAccounts = await redisClient.get('telegram-accounts'); - if (!telegramAccounts) { - console.log('⚠️ No telegram-accounts found in Redis'); - console.log(' Run this first: npm run move-env-to-redis'); - return false; - } - - // Try to parse the accounts - const accounts = JSON.parse(telegramAccounts); - console.log(`✅ Found ${accounts.length} encrypted Telegram accounts in Redis`); - - return true; - } catch (error) { - console.error('❌ Redis connection failed:', error); - return false; - } finally { - await redisClient.quit(); + console.log('\n'); + + // Test 2: Get earliest used account multiple times + console.log('🔄 Test 2: Testing account rotation logic...'); + + for (let i = 1; i <= 3; i++) { + console.log(`\nIteration ${i}:`); + + const earliestAccount = await telegramAccountManager.getEarliestUsedAccount(); + console.log(` Selected account: ${earliestAccount.accountId}`); + console.log(` Last used: ${earliestAccount.lastUsed || 'Never'}`); + console.log(` Channel: ${earliestAccount.credentials.TELEGRAM_TG_CHANNEL}`); + + // Mark the account as used + console.log(` Marking account as used...`); + await telegramAccountManager.markAccountAsUsed(earliestAccount.accountId); + + // Wait a moment for the timestamp to change + await new Promise((resolve) => setTimeout(resolve, 1000)); } + + console.log('\n'); + + // Test 3: Show final state + console.log('📈 Test 3: Final usage state after rotation...'); + const finalAccounts = await telegramAccountManager.getAllAccountsWithCredentials(); + finalAccounts.forEach((account, index) => { + console.log(` Account ${index + 1}: ${account.accountId}`); + console.log(` Last used: ${account.lastUsed || 'Never'}`); + console.log(` Total requests: ${account.totalRequests || 0}`); + }); + + console.log('\n'); + + // Test 4: Verify rotation order + console.log('🎯 Test 4: Verifying rotation order...'); + const nextAccount = await telegramAccountManager.getEarliestUsedAccount(); + console.log(` Next account to be used: ${nextAccount.accountId}`); + console.log(` Last used: ${nextAccount.lastUsed || 'Never'}`); + + console.log('\n✅ All Telegram tests completed successfully!'); + } catch (error) { + console.error('❌ Test failed:', error); + } finally { + await telegramAccountManager.disconnect(); + } } -async function main() { - console.log('Telegram Account Rotation Test Suite'); - console.log('=====================================\n'); - - // Check Redis connection and data first - const redisOk = await testTelegramRedisConnection(); - if (!redisOk) { - console.log('\n❌ Please fix Redis issues before continuing'); - process.exit(1); - } +async function testTelegramRedisConnection() { + console.log('🔗 Testing Redis connection for Telegram accounts...'); - // Check encryption key - if (!process.env.ENCRYPTION_KEY) { - console.log('❌ ENCRYPTION_KEY environment variable not set'); - process.exit(1); + const redisClient = createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + + try { + await redisClient.connect(); + + // Check if telegram-accounts key exists + const telegramAccounts = await redisClient.get('telegram-accounts'); + if (!telegramAccounts) { + console.log('⚠️ No telegram-accounts found in Redis'); + console.log(' Run this first: npm run move-env-to-redis'); + return false; } - console.log('\n'); + // Try to parse the accounts + const accounts = JSON.parse(telegramAccounts); + console.log(`✅ Found ${accounts.length} encrypted Telegram accounts in Redis`); + + return true; + } catch (error) { + console.error('❌ Redis connection failed:', error); + return false; + } finally { + await redisClient.quit(); + } +} - // Run rotation tests - await testTelegramAccountRotation(); +async function main() { + console.log('Telegram Account Rotation Test Suite'); + console.log('=====================================\n'); + + // Check Redis connection and data first + const redisOk = await testTelegramRedisConnection(); + if (!redisOk) { + console.log('\n❌ Please fix Redis issues before continuing'); + process.exit(1); + } + + // Check encryption key + if (!process.env.ENCRYPTION_KEY) { + console.log('❌ ENCRYPTION_KEY environment variable not set'); + process.exit(1); + } + + console.log('\n'); + + // Run rotation tests + await testTelegramAccountRotation(); } // Export for use in other scripts @@ -143,5 +142,5 @@ export { testTelegramAccountRotation, testTelegramRedisConnection }; // Run if executed directly if (require.main === module) { - main().catch(console.error); -} \ No newline at end of file + main().catch(console.error); +} diff --git a/src/twitterApi.ts b/src/twitterApi.ts index a8bd177..bf0affb 100644 --- a/src/twitterApi.ts +++ b/src/twitterApi.ts @@ -5,17 +5,17 @@ import { twitterAccountManager, TwitterAccount } from './services/twitterAccount dotenv.config(); async function fetchViewerAccount(account: TwitterAccount): Promise<{ screenName: string; userId: string } | null> { - const url = "https://x.com/i/api/graphql/jMaTSZ5dqXctUg5f97R6xw/Viewer"; + const url = 'https://x.com/i/api/graphql/jMaTSZ5dqXctUg5f97R6xw/Viewer'; const headers = { - "authorization": `Bearer ${account.credentials.TWITTER_BEARER}`, - "x-csrf-token": account.credentials.TWITTER_CSRF_TOKEN, - "cookie": `auth_token=${account.credentials.TWITTER_AUTH_TOKEN}; ct0=${account.credentials.TWITTER_CSRF_TOKEN}`, + authorization: `Bearer ${account.credentials.TWITTER_BEARER}`, + 'x-csrf-token': account.credentials.TWITTER_CSRF_TOKEN, + cookie: `auth_token=${account.credentials.TWITTER_AUTH_TOKEN}; ct0=${account.credentials.TWITTER_CSRF_TOKEN}` }; - const res = await fetch(url, { method: "GET", headers }); + const res = await fetch(url, { method: 'GET', headers }); if (!res.ok) { - console.error("Viewer API request failed:", res.status, res.statusText); + console.error('Viewer API request failed:', res.status, res.statusText); return null; } @@ -25,7 +25,7 @@ async function fetchViewerAccount(account: TwitterAccount): Promise<{ screenName return { screenName: user.legacy?.screen_name, - userId: user.rest_id, + userId: user.rest_id }; } @@ -33,11 +33,11 @@ export async function fetchHomeTimeline( seenTweetIds: string[] = [], providedAccount?: TwitterAccount ): Promise> { - const queryId = "wEpbv0WrfwV6y2Wlf0fxBQ"; + const queryId = 'wEpbv0WrfwV6y2Wlf0fxBQ'; const url = `https://x.com/i/api/graphql/${queryId}/HomeTimeline`; // Get the account to use (either provided or fetch the earliest used one) - const account = providedAccount || await twitterAccountManager.getEarliestUsedAccount(); + const account = providedAccount || (await twitterAccountManager.getEarliestUsedAccount()); console.log(`Using Twitter account: ${account.accountId} for timeline fetch`); @@ -45,10 +45,10 @@ export async function fetchHomeTimeline( const cookie = `auth_token=${account.credentials.TWITTER_AUTH_TOKEN};ct0=${account.credentials.TWITTER_CSRF_TOKEN}`; const headers = { - "authorization": `Bearer ${account.credentials.TWITTER_BEARER}`, - "content-type": "application/json", - "x-csrf-token": account.credentials.TWITTER_CSRF_TOKEN, - "cookie": cookie, + authorization: `Bearer ${account.credentials.TWITTER_BEARER}`, + 'content-type': 'application/json', + 'x-csrf-token': account.credentials.TWITTER_CSRF_TOKEN, + cookie: cookie }; // Prepare request body @@ -57,9 +57,9 @@ export async function fetchHomeTimeline( count: 20, includePromotedContent: true, latestControlAvailable: true, - requestContext: "launch", + requestContext: 'launch', withCommunity: true, - seenTweetIds: seenTweetIds || [], + seenTweetIds: seenTweetIds || [] }, features: { rweb_video_screen_enabled: false, @@ -96,16 +96,16 @@ export async function fetchHomeTimeline( responsive_web_grok_image_annotation_enabled: true, responsive_web_grok_imagine_annotation_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, - responsive_web_enhance_cards_enabled: false, + responsive_web_enhance_cards_enabled: false }, - queryId, + queryId }; // Make API request const response = await fetch(url, { - method: "POST", + method: 'POST', headers, - body: JSON.stringify(body), + body: JSON.stringify(body) }); // Check response status @@ -138,8 +138,7 @@ export async function fetchHomeTimeline( const base = result?.__typename === 'TweetWithVisibilityResults' ? result?.tweet : result; const restId: string | undefined = base?.rest_id; const fullText: string | undefined = - base?.legacy?.full_text ?? - base?.note_tweet?.note_tweet_results?.result?.text; + base?.legacy?.full_text ?? base?.note_tweet?.note_tweet_results?.result?.text; const authorId: string | undefined = base?.core?.user_results?.result?.rest_id; if (!restId || !fullText || !authorId) continue; if (seenTweetIdsSet.has(restId)) continue; @@ -148,7 +147,7 @@ export async function fetchHomeTimeline( } } } catch (e) { - console.error("Error parsing tweets:", e); + console.error('Error parsing tweets:', e); } // Track API usage after successful fetch diff --git a/src/types/input.d.ts b/src/types/input.d.ts index 80c448e..aa930c1 100644 --- a/src/types/input.d.ts +++ b/src/types/input.d.ts @@ -1,3 +1,3 @@ -declare module "input" { +declare module 'input' { export function text(prompt: string): Promise; } diff --git a/src/utils/moveEnvToRedis.ts b/src/utils/moveEnvToRedis.ts index c838b74..b717437 100644 --- a/src/utils/moveEnvToRedis.ts +++ b/src/utils/moveEnvToRedis.ts @@ -7,64 +7,66 @@ import 'dotenv/config'; const redisClient = createClient({ url: process.env.REDIS_URL }); async function ensureRedisConnected() { - if (!redisClient.isOpen) { - await redisClient.connect(); - } + if (!redisClient.isOpen) { + await redisClient.connect(); + } } async function moveEnvToRedis() { - const envVars: Record = {}; - const EXCLUDE = new Set(['ENCRYPTION_KEY']); - for (const [key, value] of Object.entries(process.env)) { - if (!value || EXCLUDE.has(key)) continue; - envVars[key] = value; - } - await ensureRedisConnected(); - // Define which keys belong to which service - const twitterKeys = ['TWITTER_AUTH_TOKEN', 'TWITTER_BEARER', 'TWITTER_CSRF_TOKEN']; - const telegramKeys = ['TELEGRAM_API_ID', 'TELEGRAM_API_HASH', 'TELEGRAM_TG_CHANNEL']; + const envVars: Record = {}; + const EXCLUDE = new Set(['ENCRYPTION_KEY']); + for (const [key, value] of Object.entries(process.env)) { + if (!value || EXCLUDE.has(key)) continue; + envVars[key] = value; + } + await ensureRedisConnected(); + // Define which keys belong to which service + const twitterKeys = ['TWITTER_AUTH_TOKEN', 'TWITTER_BEARER', 'TWITTER_CSRF_TOKEN']; + const telegramKeys = ['TELEGRAM_API_ID', 'TELEGRAM_API_HASH', 'TELEGRAM_TG_CHANNEL']; - // Encrypt each value individually and store as an object - const twitterAccount: Record = {}; - const telegramAccount: Record = {}; - const otherVars: Record = {}; + // Encrypt each value individually and store as an object + const twitterAccount: Record = {}; + const telegramAccount: Record = {}; + const otherVars: Record = {}; - for (const [key, value] of Object.entries(envVars)) { - if (twitterKeys.includes(key)) { - twitterAccount[key] = encrypt(value); - } else if (telegramKeys.includes(key)) { - telegramAccount[key] = encrypt(value); - } else { - otherVars[key] = encrypt(value); - } + for (const [key, value] of Object.entries(envVars)) { + if (twitterKeys.includes(key)) { + twitterAccount[key] = encrypt(value); + } else if (telegramKeys.includes(key)) { + telegramAccount[key] = encrypt(value); + } else { + otherVars[key] = encrypt(value); } + } - if (Object.keys(twitterAccount).length) { - let twitterArr: any[] = []; - const existing = await redisClient.get('twitter-accounts'); - if (existing) { - try { - twitterArr = JSON.parse(existing); - } catch { } - } - twitterArr.push(twitterAccount); - await redisClient.set('twitter-accounts', JSON.stringify(twitterArr)); - } - if (Object.keys(telegramAccount).length) { - let telegramArr: any[] = []; - const existing = await redisClient.get('telegram-accounts'); - if (existing) { - try { - telegramArr = JSON.parse(existing); - } catch { } - } - telegramArr.push(telegramAccount); - await redisClient.set('telegram-accounts', JSON.stringify(telegramArr)); + if (Object.keys(twitterAccount).length) { + let twitterArr: any[] = []; + const existing = await redisClient.get('twitter-accounts'); + if (existing) { + try { + twitterArr = JSON.parse(existing); + } catch {} } - if (Object.keys(otherVars).length) { - await redisClient.set('env-variables', JSON.stringify(otherVars)); + twitterArr.push(twitterAccount); + await redisClient.set('twitter-accounts', JSON.stringify(twitterArr)); + } + if (Object.keys(telegramAccount).length) { + let telegramArr: any[] = []; + const existing = await redisClient.get('telegram-accounts'); + if (existing) { + try { + telegramArr = JSON.parse(existing); + } catch {} } - console.log('Moved and individually encrypted env variables to Redis (twitter-accounts, telegram-accounts, env-variables as objects).'); + telegramArr.push(telegramAccount); + await redisClient.set('telegram-accounts', JSON.stringify(telegramArr)); + } + if (Object.keys(otherVars).length) { + await redisClient.set('env-variables', JSON.stringify(otherVars)); + } + console.log( + 'Moved and individually encrypted env variables to Redis (twitter-accounts, telegram-accounts, env-variables as objects).' + ); } moveEnvToRedis().then(() => process.exit(0)); diff --git a/src/utils/redisUtils.ts b/src/utils/redisUtils.ts index 5b51f37..32f257e 100644 --- a/src/utils/redisUtils.ts +++ b/src/utils/redisUtils.ts @@ -15,7 +15,13 @@ async function ensureRedisConnected() { redisConnected = true; } } -export async function trackApiKeyUsage({ accountId, platform }: { accountId: string, platform: 'telegram' | 'twitter' }): Promise { +export async function trackApiKeyUsage({ + accountId, + platform +}: { + accountId: string; + platform: 'telegram' | 'twitter'; +}): Promise { if (!accountId?.trim()) { console.warn('trackApiKeyUsage: empty accountId; skipping'); return; @@ -37,7 +43,7 @@ export async function trackApiKeyUsage({ accountId, platform }: { accountId: str .hIncrBy(key, 'total_requests', 1) .hSet(key, { last_request: now, - account_id: accountId, + account_id: accountId }) .exec(); } catch (err) { @@ -53,13 +59,18 @@ export async function trackApiKeyUsage({ accountId, platform }: { accountId: str */ interface dataType { - accountId: string, - platform: 'telegram' | 'twitter' + accountId: string; + platform: 'telegram' | 'twitter'; } -export async function getApiKeyUsage(data: dataType): Promise<{ total_requests: number; last_request: string | null; account_id?: string }> { +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 }; + let result: { total_requests: number; last_request: string | null; account_id?: string } = { + total_requests: 0, + last_request: null + }; if (!accountId?.trim()) { return result; } @@ -120,7 +131,7 @@ export async function getBatchApiKeyUsage( results.push({ accountId, total_requests: data.total_requests ? parseInt(data.total_requests) : 0, - last_request: data.last_request ? data.last_request : null, + last_request: data.last_request ? data.last_request : null }); } } catch (err) { @@ -129,5 +140,3 @@ export async function getBatchApiKeyUsage( return results; } - - diff --git a/src/utils/showEnvVariables.ts b/src/utils/showEnvVariables.ts index 4bb7152..cec9add 100644 --- a/src/utils/showEnvVariables.ts +++ b/src/utils/showEnvVariables.ts @@ -2,54 +2,51 @@ import { createClient } from 'redis'; import { mask } from '../lib/utils/string'; import { decrypt } from '../lib/encryption'; - async function showEnvVariables() { - const redisClient = createClient({ url: process.env.REDIS_URL }); - const decryptFlag = process.argv.includes('--decrypt'); - let decryptFn: ((v: string) => string) | null = null; - if (decryptFlag) { - decryptFn = decrypt; - } - await redisClient.connect(); - await showAccounts(redisClient, decryptFlag, decryptFn); - await redisClient.quit(); + const redisClient = createClient({ url: process.env.REDIS_URL }); + const decryptFlag = process.argv.includes('--decrypt'); + let decryptFn: ((v: string) => string) | null = null; + if (decryptFlag) { + decryptFn = decrypt; + } + await redisClient.connect(); + await showAccounts(redisClient, decryptFlag, decryptFn); + await redisClient.quit(); } // Unified function to show both Twitter and Telegram accounts type AccountRecord = Record | { error: string }; async function showAccounts( - redisClient: ReturnType, - decryptFlag: boolean, - decryptFn: ((v: string) => string) | null + redisClient: ReturnType, + decryptFlag: boolean, + decryptFn: ((v: string) => string) | null ) { - const services: { name: string; key: string }[] = [ - { name: 'Twitter', key: 'twitter-accounts' }, - { name: 'Telegram', key: 'telegram-accounts' } - ]; - for (const service of services) { - const raw = await redisClient.get(service.key); - console.log(`\n${service.name} Accounts:`); - if (raw) { - let accounts: AccountRecord[]; - try { - accounts = JSON.parse(raw) as AccountRecord[]; - } catch (e) { - accounts = [{ error: 'Failed to parse' }]; - } - accounts.forEach((acc, idx) => { - console.log(`Account ${idx + 1}:`); - Object.entries(acc).forEach(([k, v]) => { - const shown = decryptFlag && decryptFn ? decryptFn(v as string) : mask(v as string); - console.log(` ${k}: ${shown}`); - }); - }); - } else { - console.log(' (none)'); - } + const services: { name: string; key: string }[] = [ + { name: 'Twitter', key: 'twitter-accounts' }, + { name: 'Telegram', key: 'telegram-accounts' } + ]; + for (const service of services) { + const raw = await redisClient.get(service.key); + console.log(`\n${service.name} Accounts:`); + if (raw) { + let accounts: AccountRecord[]; + try { + accounts = JSON.parse(raw) as AccountRecord[]; + } catch (e) { + accounts = [{ error: 'Failed to parse' }]; + } + accounts.forEach((acc, idx) => { + console.log(`Account ${idx + 1}:`); + Object.entries(acc).forEach(([k, v]) => { + const shown = decryptFlag && decryptFn ? decryptFn(v as string) : mask(v as string); + console.log(` ${k}: ${shown}`); + }); + }); + } else { + console.log(' (none)'); } + } } showEnvVariables(); - -