Skip to content
4 changes: 2 additions & 2 deletions src/fetchTelegramMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ export async function fetchTelegramMessages(
channel: string
): Promise<TelegramMessages[]> {
if (!channel) {
throw new Error("TG_CHANNEL environment variable is not set.");
throw new Error("TELEGRAM_TG_CHANNEL environment variable is not set.");
}
const apiId = process.env.API_ID;
const apiId = process.env.TELEGRAM_API_ID;

// Fetch channel entity to get the actual channel ID
let entity: Api.Channel;
Expand Down
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { fetchTelegramMessages } from './fetchTelegramMessages';
import { getApiKeyUsage } from './utils/redisUtils';

// Replace these with your values
const apiId = Number(process.env.API_ID);
const apiHash = process.env.API_HASH ?? "";
const apiId = Number(process.env.TELEGRAM_API_ID);
const apiHash = process.env.TELEGRAM_API_HASH ?? "";
if (!Number.isFinite(apiId)) {
throw new Error("API_ID environment variable is missing or not a valid number.");
}
Expand Down Expand Up @@ -41,11 +41,11 @@ async function startTelegramCron() {

// Run once at startup
try {
await fetchTelegramMessages(client, process.env.TG_CHANNEL!);
await fetchTelegramMessages(client, process.env.TELEGRAM_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'});
const usage = await getApiKeyUsage({ accountId, platform: 'telegram' });
console.log('Telegram API usage:', {
total_requests: usage.total_requests,
last_request: usage.last_request,
Expand All @@ -59,7 +59,7 @@ async function startTelegramCron() {
cron.schedule('*/5 * * * *', async () => {
console.log('Refetching Telegram messages...');
try {
await fetchTelegramMessages(client, process.env.TG_CHANNEL!);
await fetchTelegramMessages(client, process.env.TELEGRAM_TG_CHANNEL!);
// No duplicate print of Telegram API usage
} catch (err) {
console.error('Scheduled Telegram fetch failed:', err);
Expand Down
41 changes: 41 additions & 0 deletions src/lib/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as crypto from 'crypto';

const SCHEME = 'v1';
const ALGORITHM = 'aes-256-gcm';
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;
}


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')}`;
}


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');
}
10 changes: 10 additions & 0 deletions src/lib/utils/string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Utility string functions

/**
* Masks a string for display, showing only the first and last 4 characters.
* Example: abcd1234efgh5678 -> abcd…5678
*/
export function mask(v: string): string {
if (!v) return '';
return v.length <= 8 ? '********' : `${v.slice(0, 4)}…${v.slice(-4)}`;
}
18 changes: 9 additions & 9 deletions src/twitterApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ 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;
const AUTH_TOKEN = process.env.TWITTER_AUTH_TOKEN;
dotenv.config();

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}`,
"authorization": `Bearer ${process.env.TWITTER_BEARER}`,
"x-csrf-token": process.env.TWITTER_CSRF_TOKEN as string,
"cookie": `auth_token=${process.env.TWITTER_AUTH_TOKEN}; ct0=${process.env.TWITTER_CSRF_TOKEN}`,
};

const res = await fetch(url, { method: "GET", headers });
Expand All @@ -37,19 +37,19 @@ export async function fetchHomeTimeline(
const url = `https://x.com/i/api/graphql/${queryId}/HomeTimeline`;

// Check required environment variables
const requiredTokens = ['BEARER', 'CSRF_TOKEN', 'AUTH_TOKEN'];
const requiredTokens = ['TWITTER_BEARER', 'TWITTER_CSRF_TOKEN', 'TWITTER_AUTH_TOKEN'];
const missing = requiredTokens.filter(k => !process.env[k]);
if (missing.length) {
throw new Error(`Missing required tokens: ${missing.join(', ')}`);
}

// Setup headers with cookies
const cookie = `auth_token=${process.env.AUTH_TOKEN};ct0=${process.env.CSRF_TOKEN}`;
const cookie = `auth_token=${process.env.TWITTER_AUTH_TOKEN};ct0=${process.env.TWITTER_CSRF_TOKEN}`;

const headers = {
"authorization": `Bearer ${process.env.BEARER}`,
"authorization": `Bearer ${process.env.TWITTER_BEARER}`,
"content-type": "application/json",
"x-csrf-token": `${process.env.CSRF_TOKEN}`,
"x-csrf-token": `${process.env.TWITTER_CSRF_TOKEN}`,
"cookie": cookie,
};

Expand Down Expand Up @@ -172,7 +172,7 @@ async function main() {
const viewer = await fetchViewerAccount();
const accountId = viewer?.userId ?? process.env.TWITTER_ACCOUNT_ID;
if (!accountId) throw new Error('Missing TWITTER_ACCOUNT_ID and Viewer lookup failed.');
const usage = await getApiKeyUsage({accountId, platform:'twitter'});
const usage = await getApiKeyUsage({ accountId, platform: 'twitter' });
console.log('Twitter API usage:', {
total_requests: usage.total_requests,
last_request: usage.last_request,
Expand Down
69 changes: 69 additions & 0 deletions src/utils/moveEnvToRedis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { encrypt } from '../lib/encryption';
import { createClient } from 'redis';
import fs from 'fs';
import path from 'path';

const redisClient = createClient({ url: process.env.REDIS_URL });

async function ensureRedisConnected() {
if (!redisClient.isOpen) {
await redisClient.connect();
}
}

async function moveEnvToRedis() {
const envVars: Record<string, string> = {};
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<string, string> = {};
const telegramAccount: Record<string, string> = {};
const otherVars: Record<string, string> = {};

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(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));
55 changes: 55 additions & 0 deletions src/utils/showEnvVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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();
}

// Unified function to show both Twitter and Telegram accounts
type AccountRecord = Record<string, string> | { error: string };

async function showAccounts(
redisClient: ReturnType<typeof createClient>,
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)');
}
}
}

showEnvVariables();