diff --git a/src/testTwitterFollow.ts b/src/testTwitterFollow.ts new file mode 100644 index 0000000..bbb2df6 --- /dev/null +++ b/src/testTwitterFollow.ts @@ -0,0 +1,34 @@ +import { followUser } from './twitterApiFollow'; +import 'dotenv/config'; + +async function testFollowUser() { + try { + // Replace with a real Twitter user ID you want to test following + // You can find user IDs by looking at Twitter URLs or using online tools + const testUserId = '44196397'; // This was the user ID from your earlier test + + console.log(`Attempting to follow user ID: ${testUserId}`); + + const result = await followUser(testUserId); + + console.log('Follow successful!'); + console.log('Response:', JSON.stringify(result, null, 2)); + + } catch (error: unknown) { + if (typeof error === 'object' && error !== null) { + // @ts-ignore + console.error('Follow failed:', (error as any).message || error); + // @ts-ignore + if ('response' in error && error.response) { + // @ts-ignore + console.error('Status:', error.response.status); + // @ts-ignore + console.error('Response:', error.response.data); + } + } else { + console.error('Follow failed:', error); + } + } +} + +testFollowUser(); \ No newline at end of file diff --git a/src/twitterApiFollow.ts b/src/twitterApiFollow.ts new file mode 100644 index 0000000..5478360 --- /dev/null +++ b/src/twitterApiFollow.ts @@ -0,0 +1,97 @@ +// Use global fetch and URLSearchParams (Node 18+) +import { createClient } from 'redis'; +import { decrypt } from './lib/encryption'; + +export interface TwitterAuthOptions { + bearerToken: string; + cookie: string; + csrfToken: string; +} + +/** + * Fetches and decrypts the latest Twitter credentials from Redis. + */ +export async function getLatestTwitterCredentialsFromRedis(redisUrl?: string): Promise { + const client = createClient({ url: redisUrl || process.env.REDIS_URL }); + let raw: string | null = null; + try { + await client.connect(); + raw = await client.get('twitter-accounts'); + } finally { + // Best-effort close + try { await client.quit(); } catch { /* ignore */ } + } + if (!raw) throw new Error('No twitter-accounts found in Redis'); + let arr: any[]; + try { + arr = JSON.parse(raw); + } catch { + throw new Error('Failed to parse twitter-accounts from Redis'); + } + if (!arr.length) throw new Error('No twitter accounts stored in Redis'); + const latest = arr[arr.length - 1]; + // Decrypt all fields + return { + bearerToken: decrypt(latest.TWITTER_BEARER), + cookie: decrypt(latest.TWITTER_COOKIE), + csrfToken: decrypt(latest.TWITTER_CSRF_TOKEN), + }; +} + +/** + * Follows a Twitter user by user_id using the web client API. + * @param userId - The Twitter user ID to follow. + * @param options - Auth and session info. If not provided, fetches from Redis. + * @returns The response from Twitter API. + */ +export async function followUser( + userId: string, + options?: TwitterAuthOptions +): Promise { + let creds: TwitterAuthOptions; + if (options) { + creds = options; + } else { + creds = await getLatestTwitterCredentialsFromRedis(); + } + const url = 'https://x.com/i/api/1.1/friendships/create.json'; + const params = new URLSearchParams({ + include_profile_interstitial_type: '1', + include_blocking: '1', + include_blocked_by: '1', + include_followed_by: '1', + include_want_retweets: '1', + include_mute_edge: '1', + include_can_dm: '1', + include_can_media_tag: '1', + include_ext_is_blue_verified: '1', + include_ext_verified_type: '1', + include_ext_profile_image_shape: '1', + skip_status: '1', + user_id: userId + }); + + const ac = new AbortController(); + const t = setTimeout(() => ac.abort(), 10_000); + const res = await fetch(url, { + method: 'POST', + headers: { + 'authorization': `Bearer ${creds.bearerToken}`, + 'cookie': creds.cookie, + 'x-csrf-token': creds.csrfToken, + 'content-type': 'application/x-www-form-urlencoded', + 'origin': 'https://x.com', + 'referer': 'https://x.com/', + 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36' + }, + body: params, + signal: ac.signal + }); + clearTimeout(t); + + if (!res.ok) { + const error = await res.text(); + throw new Error(`Twitter follow failed: ${res.status} ${error}`); + } + return res.json(); +} \ No newline at end of file