feat: add following/followers query refresh + retry
This commit is contained in:
@@ -14,6 +14,8 @@ const TARGET_OPERATIONS = [
|
|||||||
'TweetDetail',
|
'TweetDetail',
|
||||||
'SearchTimeline',
|
'SearchTimeline',
|
||||||
'Bookmarks',
|
'Bookmarks',
|
||||||
|
'Following',
|
||||||
|
'Followers',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type OperationName = (typeof TARGET_OPERATIONS)[number];
|
type OperationName = (typeof TARGET_OPERATIONS)[number];
|
||||||
|
|||||||
@@ -349,6 +349,9 @@ program
|
|||||||
'SearchTimeline',
|
'SearchTimeline',
|
||||||
'UserArticlesTweets',
|
'UserArticlesTweets',
|
||||||
'Bookmarks',
|
'Bookmarks',
|
||||||
|
'Following',
|
||||||
|
'Followers',
|
||||||
|
'Likes',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (cmdOpts.fresh) {
|
if (cmdOpts.fresh) {
|
||||||
|
|||||||
+143
-61
@@ -2047,6 +2047,16 @@ export class TwitterClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getFollowingQueryIds(): Promise<string[]> {
|
||||||
|
const primary = await this.getQueryId('Following');
|
||||||
|
return Array.from(new Set([primary, 'BEkNpEt5pNETESoqMsTEGA']));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getFollowersQueryIds(): Promise<string[]> {
|
||||||
|
const primary = await this.getQueryId('Followers');
|
||||||
|
return Array.from(new Set([primary, 'kuFUYP9eV1FPoEy4N-pi7w']));
|
||||||
|
}
|
||||||
|
|
||||||
private parseUsersFromInstructions(
|
private parseUsersFromInstructions(
|
||||||
instructions: Array<{ type?: string; entries?: Array<unknown> }> | undefined,
|
instructions: Array<{ type?: string; entries?: Array<unknown> }> | undefined,
|
||||||
): TwitterUser[] {
|
): TwitterUser[] {
|
||||||
@@ -2057,17 +2067,18 @@ export class TwitterClient {
|
|||||||
const users: TwitterUser[] = [];
|
const users: TwitterUser[] = [];
|
||||||
|
|
||||||
for (const instruction of instructions) {
|
for (const instruction of instructions) {
|
||||||
if (instruction.type !== 'TimelineAddEntries' || !instruction.entries) {
|
if (!instruction.entries) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of instruction.entries) {
|
for (const entry of instruction.entries) {
|
||||||
const content = (entry as { content?: { itemContent?: { user_results?: { result?: unknown } } } })?.content;
|
const content = (entry as { content?: { itemContent?: { user_results?: { result?: unknown } } } })?.content;
|
||||||
const userResult = content?.itemContent?.user_results?.result as
|
const rawUserResult = content?.itemContent?.user_results?.result as
|
||||||
| {
|
| {
|
||||||
__typename?: string;
|
__typename?: string;
|
||||||
rest_id?: string;
|
rest_id?: string;
|
||||||
is_blue_verified?: boolean;
|
is_blue_verified?: boolean;
|
||||||
|
user?: unknown;
|
||||||
legacy?: {
|
legacy?: {
|
||||||
screen_name?: string;
|
screen_name?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -2088,17 +2099,26 @@ export class TwitterClient {
|
|||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
|
const userResult =
|
||||||
|
rawUserResult?.__typename === 'UserWithVisibilityResults' && rawUserResult.user
|
||||||
|
? (rawUserResult.user as typeof rawUserResult)
|
||||||
|
: rawUserResult;
|
||||||
|
|
||||||
if (!userResult || userResult.__typename !== 'User') {
|
if (!userResult || userResult.__typename !== 'User') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const legacy = userResult.legacy;
|
const legacy = userResult.legacy;
|
||||||
const core = userResult.core;
|
const core = userResult.core;
|
||||||
|
const username = legacy?.screen_name ?? core?.screen_name;
|
||||||
|
if (!userResult.rest_id || !username) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
users.push({
|
users.push({
|
||||||
id: userResult.rest_id ?? '',
|
id: userResult.rest_id,
|
||||||
username: legacy?.screen_name ?? core?.screen_name ?? '',
|
username,
|
||||||
name: legacy?.name ?? core?.name ?? '',
|
name: legacy?.name ?? core?.name ?? username,
|
||||||
description: legacy?.description,
|
description: legacy?.description,
|
||||||
followers_count: legacy?.followers_count,
|
followers_count: legacy?.followers_count,
|
||||||
following_count: legacy?.friends_count,
|
following_count: legacy?.friends_count,
|
||||||
@@ -2129,46 +2149,77 @@ export class TwitterClient {
|
|||||||
features: JSON.stringify(features),
|
features: JSON.stringify(features),
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryId = await this.getQueryId('Following');
|
const tryOnce = async () => {
|
||||||
const url = `${TWITTER_API_BASE}/${queryId}/Following?${params.toString()}`;
|
let lastError: string | undefined;
|
||||||
|
let had404 = false;
|
||||||
|
const queryIds = await this.getFollowingQueryIds();
|
||||||
|
|
||||||
try {
|
for (const queryId of queryIds) {
|
||||||
const response = await this.fetchWithTimeout(url, {
|
const url = `${TWITTER_API_BASE}/${queryId}/Following?${params.toString()}`;
|
||||||
method: 'GET',
|
|
||||||
headers: this.getHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
try {
|
||||||
const text = await response.text();
|
const response = await this.fetchWithTimeout(url, {
|
||||||
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` };
|
method: 'GET',
|
||||||
}
|
headers: this.getHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
const data = (await response.json()) as {
|
if (response.status === 404) {
|
||||||
data?: {
|
had404 = true;
|
||||||
user?: {
|
lastError = `HTTP ${response.status}`;
|
||||||
result?: {
|
continue;
|
||||||
timeline?: {
|
}
|
||||||
timeline?: {
|
|
||||||
instructions?: Array<{ type?: string; entries?: Array<unknown> }>;
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
return { success: false as const, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
data?: {
|
||||||
|
user?: {
|
||||||
|
result?: {
|
||||||
|
timeline?: {
|
||||||
|
timeline?: {
|
||||||
|
instructions?: Array<{ type?: string; entries?: Array<unknown> }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
errors?: Array<{ message: string }>;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
errors?: Array<{ message: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data.errors && data.errors.length > 0) {
|
if (data.errors && data.errors.length > 0) {
|
||||||
return { success: false, error: data.errors.map((e) => e.message).join(', ') };
|
return { success: false as const, error: data.errors.map((e) => e.message).join(', '), had404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const instructions = data.data?.user?.result?.timeline?.timeline?.instructions;
|
||||||
|
const users = this.parseUsersFromInstructions(instructions);
|
||||||
|
|
||||||
|
return { success: true as const, users, had404 };
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const instructions = data.data?.user?.result?.timeline?.timeline?.instructions;
|
return { success: false as const, error: lastError ?? 'Unknown error fetching following', had404 };
|
||||||
const users = this.parseUsersFromInstructions(instructions);
|
};
|
||||||
|
|
||||||
return { success: true, users };
|
const firstAttempt = await tryOnce();
|
||||||
} catch (error) {
|
if (firstAttempt.success) {
|
||||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
return { success: true, users: firstAttempt.users };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (firstAttempt.had404) {
|
||||||
|
await this.refreshQueryIds();
|
||||||
|
const secondAttempt = await tryOnce();
|
||||||
|
if (secondAttempt.success) {
|
||||||
|
return { success: true, users: secondAttempt.users };
|
||||||
|
}
|
||||||
|
return { success: false, error: secondAttempt.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: firstAttempt.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2188,45 +2239,76 @@ export class TwitterClient {
|
|||||||
features: JSON.stringify(features),
|
features: JSON.stringify(features),
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryId = await this.getQueryId('Followers');
|
const tryOnce = async () => {
|
||||||
const url = `${TWITTER_API_BASE}/${queryId}/Followers?${params.toString()}`;
|
let lastError: string | undefined;
|
||||||
|
let had404 = false;
|
||||||
|
const queryIds = await this.getFollowersQueryIds();
|
||||||
|
|
||||||
try {
|
for (const queryId of queryIds) {
|
||||||
const response = await this.fetchWithTimeout(url, {
|
const url = `${TWITTER_API_BASE}/${queryId}/Followers?${params.toString()}`;
|
||||||
method: 'GET',
|
|
||||||
headers: this.getHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
try {
|
||||||
const text = await response.text();
|
const response = await this.fetchWithTimeout(url, {
|
||||||
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` };
|
method: 'GET',
|
||||||
}
|
headers: this.getHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
const data = (await response.json()) as {
|
if (response.status === 404) {
|
||||||
data?: {
|
had404 = true;
|
||||||
user?: {
|
lastError = `HTTP ${response.status}`;
|
||||||
result?: {
|
continue;
|
||||||
timeline?: {
|
}
|
||||||
timeline?: {
|
|
||||||
instructions?: Array<{ type?: string; entries?: Array<unknown> }>;
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
return { success: false as const, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
data?: {
|
||||||
|
user?: {
|
||||||
|
result?: {
|
||||||
|
timeline?: {
|
||||||
|
timeline?: {
|
||||||
|
instructions?: Array<{ type?: string; entries?: Array<unknown> }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
errors?: Array<{ message: string }>;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
errors?: Array<{ message: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data.errors && data.errors.length > 0) {
|
if (data.errors && data.errors.length > 0) {
|
||||||
return { success: false, error: data.errors.map((e) => e.message).join(', ') };
|
return { success: false as const, error: data.errors.map((e) => e.message).join(', '), had404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const instructions = data.data?.user?.result?.timeline?.timeline?.instructions;
|
||||||
|
const users = this.parseUsersFromInstructions(instructions);
|
||||||
|
|
||||||
|
return { success: true as const, users, had404 };
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const instructions = data.data?.user?.result?.timeline?.timeline?.instructions;
|
return { success: false as const, error: lastError ?? 'Unknown error fetching followers', had404 };
|
||||||
const users = this.parseUsersFromInstructions(instructions);
|
};
|
||||||
|
|
||||||
return { success: true, users };
|
const firstAttempt = await tryOnce();
|
||||||
} catch (error) {
|
if (firstAttempt.success) {
|
||||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
return { success: true, users: firstAttempt.users };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (firstAttempt.had404) {
|
||||||
|
await this.refreshQueryIds();
|
||||||
|
const secondAttempt = await tryOnce();
|
||||||
|
if (secondAttempt.success) {
|
||||||
|
return { success: true, users: secondAttempt.users };
|
||||||
|
}
|
||||||
|
return { success: false, error: secondAttempt.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: firstAttempt.error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user