chore: tighten lint rules and tests

This commit is contained in:
Peter Steinberger
2025-12-31 19:36:33 +01:00
parent 464b50b2fd
commit fc77190929
18 changed files with 1257 additions and 137 deletions
+112 -38
View File
@@ -4,6 +4,7 @@
import { randomBytes, randomUUID } from 'node:crypto';
import type { TwitterCookies } from './cookies.js';
// biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension.
import queryIds from './query-ids.json' with { type: 'json' };
import { runtimeQueryIds } from './runtime-query-ids.js';
@@ -12,6 +13,9 @@ const TWITTER_GRAPHQL_POST_URL = 'https://x.com/i/api/graphql';
const TWITTER_UPLOAD_URL = 'https://upload.twitter.com/i/media/upload.json';
const TWITTER_MEDIA_METADATA_URL = 'https://x.com/i/api/1.1/media/metadata/create.json';
const TWITTER_STATUS_UPDATE_URL = 'https://x.com/i/api/1.1/statuses/update.json';
const SETTINGS_SCREEN_NAME_REGEX = /"screen_name":"([^"]+)"/;
const SETTINGS_USER_ID_REGEX = /"user_id"\s*:\s*"(\d+)"/;
const SETTINGS_NAME_REGEX = /"name":"([^"\\]*(?:\\.[^"\\]*)*)"/;
// Query IDs rotate frequently; the values in query-ids.json are refreshed by
// scripts/update-query-ids.ts. The fallback values keep the client usable if
@@ -339,7 +343,9 @@ export class TwitterClient {
| undefined,
tweetId: string,
) {
if (!instructions) return undefined;
if (!instructions) {
return undefined;
}
for (const instruction of instructions) {
for (const entry of instruction.entries || []) {
@@ -401,10 +407,14 @@ export class TwitterClient {
private mediaCategoryForMime(mimeType: string): string | null {
if (mimeType.startsWith('image/')) {
if (mimeType === 'image/gif') return 'tweet_gif';
if (mimeType === 'image/gif') {
return 'tweet_gif';
}
return 'tweet_image';
}
if (mimeType.startsWith('video/')) return 'tweet_video';
if (mimeType.startsWith('video/')) {
return 'tweet_video';
}
return null;
}
@@ -414,7 +424,9 @@ export class TwitterClient {
async uploadMedia(input: { data: Uint8Array; mimeType: string; alt?: string }): Promise<UploadMediaResult> {
const category = this.mediaCategoryForMime(input.mimeType);
if (!category) return { success: false, error: `Unsupported media type: ${input.mimeType}` };
if (!category) {
return { success: false, error: `Unsupported media type: ${input.mimeType}` };
}
try {
const initParams = new URLSearchParams({
@@ -442,7 +454,9 @@ export class TwitterClient {
: initBody.media_id !== undefined
? String(initBody.media_id)
: undefined;
if (!mediaId) return { success: false, error: 'Media upload INIT did not return media_id' };
if (!mediaId) {
return { success: false, error: 'Media upload INIT did not return media_id' };
}
const chunkSize = 5 * 1024 * 1024;
let segmentIndex = 0;
@@ -516,11 +530,15 @@ export class TwitterClient {
error?: { message?: string; name?: string };
};
};
if (!statusBody.processing_info) break;
if (!statusBody.processing_info) {
break;
}
info.state = statusBody.processing_info.state;
info.check_after_secs = statusBody.processing_info.check_after_secs;
info.error = statusBody.processing_info.error;
if (info.state === 'succeeded') break;
if (info.state === 'succeeded') {
break;
}
attempts += 1;
}
}
@@ -547,15 +565,21 @@ export class TwitterClient {
for (const value of values) {
if (typeof value === 'string') {
const trimmed = value.trim();
if (trimmed) return trimmed;
if (trimmed) {
return trimmed;
}
}
}
return undefined;
}
private collectTextFields(value: unknown, keys: Set<string>, output: string[]): void {
if (!value) return;
if (typeof value === 'string') return;
if (!value) {
return;
}
if (typeof value === 'string') {
return;
}
if (Array.isArray(value)) {
for (const item of value) {
@@ -569,7 +593,9 @@ export class TwitterClient {
if (keys.has(key)) {
if (typeof nested === 'string') {
const trimmed = nested.trim();
if (trimmed) output.push(trimmed);
if (trimmed) {
output.push(trimmed);
}
continue;
}
}
@@ -582,7 +608,9 @@ export class TwitterClient {
const seen = new Set<string>();
const result: string[] = [];
for (const value of values) {
if (seen.has(value)) continue;
if (seen.has(value)) {
continue;
}
seen.add(value);
result.push(value);
}
@@ -591,7 +619,9 @@ export class TwitterClient {
private extractArticleText(result: GraphqlTweetResult | undefined): string | undefined {
const article = result?.article;
if (!article) return undefined;
if (!article) {
return undefined;
}
const articleResult = article.article_results?.result ?? article;
if (process.env.BIRD_DEBUG_ARTICLE === '1') {
@@ -656,7 +686,9 @@ export class TwitterClient {
private extractNoteTweetText(result: GraphqlTweetResult | undefined): string | undefined {
const note = result?.note_tweet?.note_tweet_results?.result;
if (!note) return undefined;
if (!note) {
return undefined;
}
return this.firstText(
note.text,
@@ -681,10 +713,14 @@ export class TwitterClient {
const username = userLegacy?.screen_name ?? userCore?.screen_name;
const name = userLegacy?.name ?? userCore?.name ?? username;
const userId = userResult?.rest_id;
if (!result?.rest_id || !username) return undefined;
if (!result?.rest_id || !username) {
return undefined;
}
const text = this.extractTweetText(result);
if (!text) return undefined;
if (!text) {
return undefined;
}
return {
id: result.rest_id,
@@ -742,7 +778,9 @@ export class TwitterClient {
}): GraphqlTweetResult[] {
const results: GraphqlTweetResult[] = [];
const pushResult = (result?: GraphqlTweetResult) => {
if (result?.rest_id) results.push(result);
if (result?.rest_id) {
results.push(result);
}
};
const content = entry.content;
@@ -809,7 +847,9 @@ export class TwitterClient {
const results = this.collectTweetResultsFromEntry(entry);
for (const result of results) {
const mapped = this.mapTweetResult(result);
if (!mapped || seen.has(mapped.id)) continue;
if (!mapped || seen.has(mapped.id)) {
continue;
}
seen.add(mapped.id);
tweets.push(mapped);
}
@@ -990,7 +1030,9 @@ export class TwitterClient {
for (const instruction of instructions) {
for (const entry of instruction.entries ?? []) {
const result = entry.content?.itemContent?.tweet_results?.result;
if (result?.rest_id !== tweetId) continue;
if (result?.rest_id !== tweetId) {
continue;
}
const articleResult = result.article?.article_results?.result;
const title = this.firstText(articleResult?.title, result.article?.title);
const plainText = this.firstText(articleResult?.plain_text, result.article?.plain_text);
@@ -1352,12 +1394,16 @@ export class TwitterClient {
if (data.errors && data.errors.length > 0) {
const fallback = await this.tryStatusUpdateFallback(data.errors, variables);
if (fallback) return fallback;
if (fallback) {
return fallback;
}
return { success: false, error: this.formatErrors(data.errors) };
}
const tweetId = data.data?.create_tweet?.tweet_results?.result?.rest_id;
if (tweetId) return { success: true, tweetId };
if (tweetId) {
return { success: true, tweetId };
}
return { success: false, error: 'Tweet created but no ID returned' };
}
@@ -1375,7 +1421,9 @@ export class TwitterClient {
if (data.errors && data.errors.length > 0) {
const fallback = await this.tryStatusUpdateFallback(data.errors, variables);
if (fallback) return fallback;
if (fallback) {
return fallback;
}
return {
success: false,
error: this.formatErrors(data.errors),
@@ -1414,7 +1462,9 @@ export class TwitterClient {
mediaIds?: string[];
} | null {
const text = typeof variables.tweet_text === 'string' ? variables.tweet_text : null;
if (!text) return null;
if (!text) {
return null;
}
const reply = variables.reply;
const inReplyToTweetId =
@@ -1486,7 +1536,9 @@ export class TwitterClient {
const tweetId =
typeof data.id_str === 'string' ? data.id_str : data.id !== undefined ? String(data.id) : undefined;
if (tweetId) return { success: true, tweetId };
if (tweetId) {
return { success: true, tweetId };
}
return { success: false, error: 'Tweet created but no ID returned' };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
@@ -1497,12 +1549,18 @@ export class TwitterClient {
errors: Array<{ message: string; code?: number }>,
variables: Record<string, unknown>,
): Promise<TweetResult | null> {
if (!errors.some((error) => error.code === 226)) return null;
if (!errors.some((error) => error.code === 226)) {
return null;
}
const input = this.statusUpdateInputFromCreateTweetVariables(variables);
if (!input) return null;
if (!input) {
return null;
}
const fallback = await this.postStatusUpdate(input);
if (fallback.success) return fallback;
if (fallback.success) {
return fallback;
}
return {
success: false,
@@ -1511,8 +1569,12 @@ export class TwitterClient {
}
private async ensureClientUserId(): Promise<void> {
if (process.env.NODE_ENV === 'test') return;
if (this.clientUserId) return;
if (process.env.NODE_ENV === 'test') {
return;
}
if (this.clientUserId) {
return;
}
const result = await this.getCurrentUser();
if (result.success && result.user?.id) {
this.clientUserId = result.user.id;
@@ -1622,12 +1684,16 @@ export class TwitterClient {
};
const firstAttempt = await tryOnce();
if (firstAttempt.success) return { success: true, tweets: firstAttempt.tweets };
if (firstAttempt.success) {
return { success: true, tweets: firstAttempt.tweets };
}
if (firstAttempt.had404) {
await this.refreshQueryIds();
const secondAttempt = await tryOnce();
if (secondAttempt.success) return { success: true, tweets: secondAttempt.tweets };
if (secondAttempt.success) {
return { success: true, tweets: secondAttempt.tweets };
}
return { success: false, error: secondAttempt.error };
}
@@ -1729,9 +1795,9 @@ export class TwitterClient {
}
const html = await response.text();
const usernameMatch = html.match(/"screen_name":"([^"]+)"/);
const idMatch = html.match(/"user_id"\s*:\s*"(\d+)"/);
const nameMatch = html.match(/"name":"([^"\\]*(?:\\.[^"\\]*)*)"/);
const usernameMatch = SETTINGS_SCREEN_NAME_REGEX.exec(html);
const idMatch = SETTINGS_USER_ID_REGEX.exec(html);
const nameMatch = SETTINGS_NAME_REGEX.exec(html);
const username = usernameMatch?.[1];
const userId = idMatch?.[1];
@@ -1765,7 +1831,9 @@ export class TwitterClient {
*/
async getReplies(tweetId: string): Promise<SearchResult> {
const response = await this.fetchTweetDetail(tweetId);
if (!response.success) return response;
if (!response.success) {
return response;
}
const instructions = response.data.threaded_conversation_with_injections_v2?.instructions;
const tweets = this.parseTweetsFromInstructions(instructions);
@@ -1779,7 +1847,9 @@ export class TwitterClient {
*/
async getThread(tweetId: string): Promise<SearchResult> {
const response = await this.fetchTweetDetail(tweetId);
if (!response.success) return response;
if (!response.success) {
return response;
}
const instructions = response.data.threaded_conversation_with_injections_v2?.instructions;
const tweets = this.parseTweetsFromInstructions(instructions);
@@ -1899,12 +1969,16 @@ export class TwitterClient {
};
const firstAttempt = await tryOnce();
if (firstAttempt.success) return { success: true, tweets: firstAttempt.tweets };
if (firstAttempt.success) {
return { success: true, tweets: firstAttempt.tweets };
}
if (firstAttempt.had404) {
await this.refreshQueryIds();
const secondAttempt = await tryOnce();
if (secondAttempt.success) return { success: true, tweets: secondAttempt.tweets };
if (secondAttempt.success) {
return { success: true, tweets: secondAttempt.tweets };
}
return { success: false, error: secondAttempt.error };
}