Skip to content

Conversation

simon0x1800
Copy link
Collaborator

@simon0x1800 simon0x1800 commented Sep 19, 2025

Description:

  • Reverse engineered Twitter’s web client follow request using browser network inspection
  • Implemented followUser function to programmatically follow any Twitter account using the internal API
  • Credentials (bearer token, CSRF token, cookie) are securely stored in Redis and decrypted at runtime
  • Added utility to fetch and decrypt latest Twitter credentials from Redis
  • Provided a test script to verify follow functionality
  • Updated .env and moveEnvToRedis to support TWITTER_COOKIE
  • Ensured all runtime dependencies (node-fetch) are in the correct section of package.json

This enables automated, scalable following of Twitter accounts without manual browser interaction.

Summary by CodeRabbit

  • New Features

    • Added the ability to follow X (Twitter) accounts directly from the app.
    • Improved reliability with request timeouts and clearer error messages on failures.
  • Chores

    • Expanded stored credentials to include an additional token to improve success rates.
    • Updated credential retrieval to always use the latest available account from storage.
  • Tests

    • Added a manual test harness to verify following functionality and error handling.

Copy link

coderabbitai bot commented Sep 19, 2025

Walkthrough

Adds a Twitter follow feature using web API credentials from Redis, updates env-to-Redis to include TWITTER_COOKIE, and introduces a manual test script to exercise followUser. Implements credential decryption, HTTP POST to X.com friendships/create, timeout handling, and detailed error reporting.

Changes

Cohort / File(s) Summary of Changes
Twitter follow API
src/twitterApiFollow.ts
New module exporting TwitterAuthOptions, getLatestTwitterCredentialsFromRedis, and followUser. Retrieves and decrypts latest credentials from Redis, constructs POST to https://x.com/i/api/1.1/friendships/create.json with timeout and explicit headers, returns JSON or throws on HTTP errors.
Test harness
src/testTwitterFollow.ts
New manual test script invoking followUser with a hard-coded user ID, logging success or detailed error information.
Env → Redis updater
src/utils/moveEnvToRedis.ts
Adds TWITTER_COOKIE to stored/encrypted Twitter keys in the twitter-accounts object; no other logic changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Dev as Dev Script
  participant Test as testTwitterFollow.ts
  participant API as followUser()
  participant Cred as getLatestTwitterCredentialsFromRedis()
  participant Redis as Redis
  participant X as X.com API (friendships/create)

  Dev->>Test: run node src/testTwitterFollow.ts
  Test->>API: followUser(userId)
  alt options not provided
    API->>Cred: fetch latest credentials
    Cred->>Redis: GET "twitter-accounts"
    Redis-->>Cred: encrypted accounts JSON
    Cred->>Cred: parse + decrypt bearer/cookie/csrf
    Cred-->>API: TwitterAuthOptions
  end
  API->>X: POST /i/api/1.1/friendships/create.json<br/>headers: auth, cookie, x-csrf-token
  X-->>API: 200 JSON or error
  alt success
    API-->>Test: follow result JSON
    Test-->>Dev: "Follow successful!" + result
  else error
    API-->>Test: throw Error (status/text)
    Test-->>Dev: log message/status/response.data
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • tasin2610

Poem

I thump my paws—credentials in a row,
From Redis burrow, off to X we go!
A follow hop, a CSRF bop,
Bearer dreams in cookie wrap.
If errors loom, I won’t despair—
I log, I learn, I twitch an ear.
Success! Another friend to share. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title concisely and accurately describes the primary change: adding programmatic Twitter follow via a reverse-engineered web API. It is a single clear sentence, uses the conventional "feat:" prefix, and directly matches the followUser implementation, credential handling, and test script added in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/twitter-reverse-follow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (7)
src/utils/moveEnvToRedis.ts (1)

24-24: Adding TWITTER_COOKIE increases secret sensitivity; add TTL/rotation plan

Cookies are live session credentials. Consider storing with TTL and metadata to enable rotation/dedup, e.g., hset + expire, and include createdAt.

Example (outside current hunk):

  • Use setEx for 'twitter-accounts:' and maintain a capped list index.
  • Add { createdAt, label } fields to each account object.
src/testTwitterFollow.ts (3)

6-9: Avoid hard‑coded live user ID in repo

Source-controlled ID '44196397' can cause unintended follows. Read from env and fail clearly if missing.

-        const testUserId = '44196397'; // This was the user ID from your earlier test
+        const testUserId = process.env.TEST_TWITTER_USER_ID;
+        if (!testUserId) throw new Error('Set TEST_TWITTER_USER_ID to run this test');

17-30: Simplify error handling; remove axios‑style branches and ts‑ignores

followUser throws a plain Error with message; error.response is never set here. Also set non‑zero exit code on failure.

-    } 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);
-        }
-    }
+    } catch (error: unknown) {
+        const msg = error instanceof Error ? error.message : String(error);
+        console.error('Follow failed:', msg);
+        process.exitCode = 1;
+    }

34-34: Gate manual test execution

Prevent accidental runs in non‑dev contexts.

-testFollowUser();
+if (process.env.RUN_TWITTER_FOLLOW_TEST === '1') {
+  // eslint-disable-next-line @typescript-eslint/no-floating-promises
+  testFollowUser();
+}
src/twitterApiFollow.ts (3)

2-5: Use global fetch and URLSearchParams; drop node-fetch import

Avoid ESM/CJS pitfalls and extra dependency.

-
-import fetch from 'node-fetch';
-import { URLSearchParams } from 'url';
 import { createClient } from 'redis';
 import { decrypt } from './lib/encryption';
+// Node 18+: global fetch and URLSearchParams are available

44-53: Input validation and explicit options precedence

Reject empty userId early and simplify options branching.

 export async function followUser(
     userId: string,
     options?: TwitterAuthOptions
 ): Promise<any> {
-    let creds: TwitterAuthOptions;
-    if (options) {
-        creds = options;
-    } else {
-        creds = await getLatestTwitterCredentialsFromRedis();
-    }
+    if (!userId || !userId.trim()) throw new Error('userId is required');
+    const creds = options ?? await getLatestTwitterCredentialsFromRedis();

82-86: Return typed JSON and include response text on JSON parse failure

Improve debuggability by parsing conditionally and typing the success case.

-    if (!res.ok) {
-        const error = await res.text();
-        throw new Error(`Twitter follow failed: ${res.status} ${error}`);
-    }
-    return res.json();
+    if (!res.ok) {
+        const text = await res.text();
+        throw new Error(`Twitter follow failed: ${res.status} ${text}`);
+    }
+    const contentType = res.headers.get('content-type') || '';
+    return contentType.includes('application/json') ? res.json() : res.text();

Add a lightweight type to the module (outside this hunk) for callers:

export interface FollowResponse {
  // minimal shape; extend as needed
  id_str?: string;
  following?: boolean;
  // ...
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4817c91 and 19fbec8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • package.json (1 hunks)
  • src/testTwitterFollow.ts (1 hunks)
  • src/twitterApiFollow.ts (1 hunks)
  • src/utils/moveEnvToRedis.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/testTwitterFollow.ts (1)
src/twitterApiFollow.ts (1)
  • followUser (44-87)
src/twitterApiFollow.ts (1)
src/lib/encryption.ts (1)
  • decrypt (29-41)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/twitterApiFollow.ts (3)

32-38: Missing credential validation and CSRF token verification.

The current implementation doesn't validate that decrypted credentials are non-empty or verify that the CSRF token matches the ct0 cookie value, which can lead to authentication failures.

Apply this fix to validate credentials:

     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),
-    };
+    const bearerToken = decrypt(latest.TWITTER_BEARER);
+    const cookie = decrypt(latest.TWITTER_COOKIE);
+    const csrfToken = decrypt(latest.TWITTER_CSRF_TOKEN);
+    
+    if (!bearerToken || !cookie || !csrfToken) {
+        throw new Error('Incomplete Twitter credentials in Redis (bearerToken/cookie/csrfToken)');
+    }
+    
+    // Verify ct0 cookie matches CSRF token
+    const ct0 = /(?:^|;\s*)ct0=([^;]+)/.exec(cookie)?.[1];
+    if (ct0 && ct0 !== csrfToken) {
+        throw new Error('CSRF token mismatch: x-csrf-token does not match ct0 cookie');
+    }
+    
+    return { bearerToken, cookie, csrfToken };

47-56: Add safety controls for reverse-engineered web API usage.

This function uses a reverse-engineered Twitter web endpoint with real session cookies. The official Twitter API documentation shows that HTTP 403 may be returned for various reasons, and operations are asynchronous with eventual consistency. Without proper safeguards, this could violate Twitter's Terms of Service or cause account suspensions.

Add the following safety controls:

 export async function followUser(
     userId: string,
     options?: TwitterAuthOptions
 ): Promise<any> {
+    // Gate behind explicit feature flag
+    if (process.env.ALLOW_TWITTER_WEB_AUTOMATION !== '1') {
+        throw new Error('Twitter web automation is disabled. Set ALLOW_TWITTER_WEB_AUTOMATION=1 to enable.');
+    }
+
     let creds: TwitterAuthOptions;

Additionally, implement rate limiting and retry logic with exponential backoff for 429/420 responses.


92-96: Add retry logic for rate limiting and improve error handling.

The current error handling doesn't account for Twitter's rate limiting responses (429, 420) or implement retry logic. Additionally, error messages could expose sensitive response data.

Implement retry logic with exponential backoff:

-    if (!res.ok) {
-        const error = await res.text();
-        throw new Error(`Twitter follow failed: ${res.status} ${error}`);
-    }
-    return res.json();
+    if (!res.ok) {
+        if (res.status === 429 || res.status === 420) {
+            // Rate limited - implement exponential backoff
+            const retryAfter = res.headers.get('retry-after');
+            const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.min(1000 * Math.pow(2, attempt), 30000);
+            throw new Error(`Twitter API rate limited: ${res.status}. Retry after ${delay}ms`);
+        }
+        
+        // Log error details internally, return sanitized message
+        const errorText = await res.text();
+        console.error(`Twitter follow failed for user ${userId}: ${res.status} ${errorText}`);
+        throw new Error(`Twitter follow failed: ${res.status}`);
+    }
+    return res.json();

Consider implementing a full retry wrapper function with exponential backoff and jitter.

🧹 Nitpick comments (1)
src/twitterApiFollow.ts (1)

1-1: Consider runtime dependency implications.

The comment indicates reliance on global fetch and URLSearchParams in Node 18+. While Node 18+ includes global fetch, it's marked as experimental in some versions. For production use, consider explicitly importing a fetch implementation for better stability.

-// Use global fetch and URLSearchParams (Node 18+)
+import fetch from 'node-fetch';
 import { createClient } from 'redis';
 import { decrypt } from './lib/encryption';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19fbec8 and 0383f57.

📒 Files selected for processing (1)
  • src/twitterApiFollow.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/twitterApiFollow.ts (1)
src/lib/encryption.ts (1)
  • decrypt (29-41)
🔇 Additional comments (3)
src/twitterApiFollow.ts (3)

14-23: Good improvement implementing Redis client cleanup.

The implementation properly handles Redis client cleanup in a finally block as suggested in the previous review. This ensures the client is closed even if connection or data retrieval fails.


74-90: Good implementation of timeout and browser headers.

The code properly implements the AbortController timeout mechanism and includes appropriate browser headers (origin, referer, user-agent) that make the request resemble a legitimate web client. This addresses the previous review feedback.


57-72: Verify follow request parameters against current Twitter/X web client (src/twitterApiFollow.ts:57-72)
Capture the actual network request sent by X.com in your browser’s DevTools when following a user and confirm that the URLSearchParams keys exactly match those sent by the web client to avoid automated-request detection.

@tasin2610 tasin2610 merged commit 6e3363d into main Oct 2, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants