Files
last30days-skill/vendor/package/dist/lib/twitter-client-follow.js
T
Matt Van Horn 32992834ee Merge PR #48: feat: add Xiaohongshu source + Reddit public fallback
- Xiaohongshu search via local MCP service (opt-in, zero impact if service not running)
- Reddit public JSON fallback (works with zero API keys)
- Reddit priority: ScrapeCreators -> OpenAI -> public fallback
- Updated env.py: Reddit always available via public fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:11:35 -08:00

178 lines
8.0 KiB
JavaScript

import { TWITTER_API_BASE } from './twitter-client-constants.js';
export function withFollow(Base) {
class TwitterClientFollow extends Base {
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
constructor(...args) {
super(...args);
}
/**
* Follow a user by their user ID
*/
async follow(userId) {
await this.ensureClientUserId();
// Try REST API first (more reliable for follow/unfollow)
const restResult = await this.followViaRest(userId, 'create');
if (restResult.success) {
return restResult;
}
// Fallback to GraphQL
return this.followViaGraphQL(userId, true);
}
/**
* Unfollow a user by their user ID
*/
async unfollow(userId) {
await this.ensureClientUserId();
// Try REST API first (more reliable for follow/unfollow)
const restResult = await this.followViaRest(userId, 'destroy');
if (restResult.success) {
return restResult;
}
// Fallback to GraphQL
return this.followViaGraphQL(userId, false);
}
async followViaRest(userId, action) {
const urls = [
`https://x.com/i/api/1.1/friendships/${action}.json`,
`https://api.twitter.com/1.1/friendships/${action}.json`,
];
const params = new URLSearchParams({
user_id: userId,
skip_status: 'true',
});
let lastError;
for (const url of urls) {
try {
const response = await this.fetchWithTimeout(url, {
method: 'POST',
headers: {
...this.getBaseHeaders(),
'content-type': 'application/x-www-form-urlencoded',
},
body: params.toString(),
});
if (!response.ok) {
const text = await response.text();
// Parse error response
try {
const errorData = JSON.parse(text);
if (errorData.errors && errorData.errors.length > 0) {
const error = errorData.errors[0];
// Code 160: Already following / already not following
if (error.code === 160) {
return { success: true };
}
// Code 162: Blocked from following
if (error.code === 162) {
return { success: false, error: 'You have been blocked from following this account' };
}
// Code 108: Cannot find specified user
if (error.code === 108) {
return { success: false, error: 'User not found' };
}
lastError = `${error.message} (code ${error.code})`;
continue;
}
}
catch {
// Not JSON, continue with generic error
}
lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`;
continue;
}
const data = (await response.json());
if (data.errors && data.errors.length > 0) {
lastError = data.errors.map((e) => e.message).join(', ');
continue;
}
// Success - user data returned
if (data.id_str || data.screen_name) {
return {
success: true,
userId: data.id_str,
username: data.screen_name,
};
}
return { success: true };
}
catch (error) {
lastError = error instanceof Error ? error.message : String(error);
}
}
return { success: false, error: lastError ?? `Unknown error during ${action}` };
}
async followViaGraphQL(userId, follow) {
const operationName = follow ? 'CreateFriendship' : 'DestroyFriendship';
const variables = {
user_id: userId,
};
const tryOnce = async () => {
let lastError;
let had404 = false;
const queryIds = await this.getFollowQueryIds(follow);
for (const queryId of queryIds) {
const url = `${TWITTER_API_BASE}/${queryId}/${operationName}`;
try {
const response = await this.fetchWithTimeout(url, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify({ variables, queryId }),
});
if (response.status === 404) {
had404 = true;
lastError = 'HTTP 404';
continue;
}
if (!response.ok) {
const text = await response.text();
lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`;
continue;
}
const data = (await response.json());
if (data.errors && data.errors.length > 0) {
lastError = data.errors.map((e) => e.message).join(', ');
continue;
}
const result = data.data?.user?.result;
return {
success: true,
userId: result?.rest_id,
username: result?.legacy?.screen_name,
had404,
};
}
catch (error) {
lastError = error instanceof Error ? error.message : String(error);
}
}
return {
success: false,
error: lastError ?? `Unknown error during ${operationName}`,
had404,
};
};
const firstAttempt = await tryOnce();
if (firstAttempt.success) {
return { success: true, userId: firstAttempt.userId, username: firstAttempt.username };
}
if (firstAttempt.had404) {
await this.refreshQueryIds();
const secondAttempt = await tryOnce();
if (secondAttempt.success) {
return { success: true, userId: secondAttempt.userId, username: secondAttempt.username };
}
return { success: false, error: secondAttempt.error };
}
return { success: false, error: firstAttempt.error };
}
async getFollowQueryIds(follow) {
const primary = await this.getQueryId(follow ? 'CreateFriendship' : 'DestroyFriendship');
const fallbacks = follow
? ['8h9JVdV8dlSyqyRDJEPCsA', 'OPwKc1HXnBT_bWXfAlo-9g']
: ['ppXWuagMNXgvzx6WoXBW0Q', '8h9JVdV8dlSyqyRDJEPCsA'];
return Array.from(new Set([primary, ...fallbacks]));
}
}
return TwitterClientFollow;
}
//# sourceMappingURL=twitter-client-follow.js.map