import type { GraphqlTweetResult, TweetData, TwitterUser } from './twitter-client-types.js'; export function normalizeQuoteDepth(value?: number): number { if (value === undefined || value === null) { return 1; } if (!Number.isFinite(value)) { return 1; } return Math.max(0, Math.floor(value)); } export function firstText(...values: Array): string | undefined { for (const value of values) { if (typeof value === 'string') { const trimmed = value.trim(); if (trimmed) { return trimmed; } } } return undefined; } export function collectTextFields(value: unknown, keys: Set, output: string[]): void { if (!value) { return; } if (typeof value === 'string') { return; } if (Array.isArray(value)) { for (const item of value) { collectTextFields(item, keys, output); } return; } if (typeof value === 'object') { for (const [key, nested] of Object.entries(value as Record)) { if (keys.has(key)) { if (typeof nested === 'string') { const trimmed = nested.trim(); if (trimmed) { output.push(trimmed); } continue; } } collectTextFields(nested, keys, output); } } } export function uniqueOrdered(values: string[]): string[] { const seen = new Set(); const result: string[] = []; for (const value of values) { if (seen.has(value)) { continue; } seen.add(value); result.push(value); } return result; } export function extractArticleText(result: GraphqlTweetResult | undefined): string | undefined { const article = result?.article; if (!article) { return undefined; } const articleResult = article.article_results?.result ?? article; if (process.env.BIRD_DEBUG_ARTICLE === '1') { console.error( '[bird][debug][article] payload:', JSON.stringify( { rest_id: result?.rest_id, article: articleResult, note_tweet: result?.note_tweet?.note_tweet_results?.result ?? null, }, null, 2, ), ); } const title = firstText(articleResult.title, article.title); let body = firstText( articleResult.plain_text, article.plain_text, articleResult.body?.text, articleResult.body?.richtext?.text, articleResult.body?.rich_text?.text, articleResult.content?.text, articleResult.content?.richtext?.text, articleResult.content?.rich_text?.text, articleResult.text, articleResult.richtext?.text, articleResult.rich_text?.text, article.body?.text, article.body?.richtext?.text, article.body?.rich_text?.text, article.content?.text, article.content?.richtext?.text, article.content?.rich_text?.text, article.text, article.richtext?.text, article.rich_text?.text, ); if (body && title && body.trim() === title.trim()) { body = undefined; } if (!body) { const collected: string[] = []; collectTextFields(articleResult, new Set(['text', 'title']), collected); collectTextFields(article, new Set(['text', 'title']), collected); const unique = uniqueOrdered(collected); const filtered = title ? unique.filter((value) => value !== title) : unique; if (filtered.length > 0) { body = filtered.join('\n\n'); } } if (title && body && !body.startsWith(title)) { return `${title}\n\n${body}`; } return body ?? title; } export function extractNoteTweetText(result: GraphqlTweetResult | undefined): string | undefined { const note = result?.note_tweet?.note_tweet_results?.result; if (!note) { return undefined; } return firstText( note.text, note.richtext?.text, note.rich_text?.text, note.content?.text, note.content?.richtext?.text, note.content?.rich_text?.text, ); } export function extractTweetText(result: GraphqlTweetResult | undefined): string | undefined { return extractArticleText(result) ?? extractNoteTweetText(result) ?? firstText(result?.legacy?.full_text); } export function unwrapTweetResult(result: GraphqlTweetResult | undefined): GraphqlTweetResult | undefined { if (!result) { return undefined; } if (result.tweet) { return result.tweet; } return result; } export interface MapTweetResultOptions { quoteDepth: number; includeRaw?: boolean; } export function mapTweetResult( result: GraphqlTweetResult | undefined, quoteDepthOrOptions: number | MapTweetResultOptions, ): TweetData | undefined { const options: MapTweetResultOptions = typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions; const { quoteDepth, includeRaw = false } = options; const userResult = result?.core?.user_results?.result; const userLegacy = userResult?.legacy; const userCore = userResult?.core; 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; } const text = extractTweetText(result); if (!text) { return undefined; } let quotedTweet: TweetData | undefined; if (quoteDepth > 0) { const quotedResult = unwrapTweetResult(result.quoted_status_result?.result); if (quotedResult) { quotedTweet = mapTweetResult(quotedResult, { quoteDepth: quoteDepth - 1, includeRaw }); } } const tweetData: TweetData = { id: result.rest_id, text, createdAt: result.legacy?.created_at, replyCount: result.legacy?.reply_count, retweetCount: result.legacy?.retweet_count, likeCount: result.legacy?.favorite_count, conversationId: result.legacy?.conversation_id_str, inReplyToStatusId: result.legacy?.in_reply_to_status_id_str ?? undefined, author: { username, name: name || username, }, authorId: userId, quotedTweet, }; if (includeRaw) { tweetData._raw = result; } return tweetData; } export function findTweetInInstructions( instructions: | Array<{ entries?: Array<{ content?: { itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; }; }>; }> | undefined, tweetId: string, ) { if (!instructions) { return undefined; } for (const instruction of instructions) { for (const entry of instruction.entries || []) { const result = entry.content?.itemContent?.tweet_results?.result; if (result?.rest_id === tweetId) { return result; } } } return undefined; } export function collectTweetResultsFromEntry(entry: { content?: { itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; item?: { itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; }; items?: Array<{ item?: { itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; }; itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; content?: { itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; }; }>; }; }): GraphqlTweetResult[] { const results: GraphqlTweetResult[] = []; const pushResult = (result?: GraphqlTweetResult) => { if (result?.rest_id) { results.push(result); } }; const content = entry.content; pushResult(content?.itemContent?.tweet_results?.result); pushResult(content?.item?.itemContent?.tweet_results?.result); for (const item of content?.items ?? []) { pushResult(item?.item?.itemContent?.tweet_results?.result); pushResult(item?.itemContent?.tweet_results?.result); pushResult(item?.content?.itemContent?.tweet_results?.result); } return results; } export interface ParseTweetsOptions { quoteDepth: number; includeRaw?: boolean; } export function parseTweetsFromInstructions( instructions: | Array<{ entries?: Array<{ content?: { itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; item?: { itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; }; items?: Array<{ item?: { itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; }; itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; content?: { itemContent?: { tweet_results?: { result?: GraphqlTweetResult; }; }; }; }>; }; }>; }> | undefined, quoteDepthOrOptions: number | ParseTweetsOptions, ): TweetData[] { const options: ParseTweetsOptions = typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions; const { quoteDepth, includeRaw = false } = options; const tweets: TweetData[] = []; const seen = new Set(); for (const instruction of instructions ?? []) { for (const entry of instruction.entries ?? []) { const results = collectTweetResultsFromEntry(entry); for (const result of results) { const mapped = mapTweetResult(result, { quoteDepth, includeRaw }); if (!mapped || seen.has(mapped.id)) { continue; } seen.add(mapped.id); tweets.push(mapped); } } } return tweets; } export function extractCursorFromInstructions( instructions: | Array<{ entries?: Array<{ content?: unknown; }>; }> | undefined, cursorType = 'Bottom', ): string | undefined { for (const instruction of instructions ?? []) { for (const entry of instruction.entries ?? []) { const content = entry.content as { cursorType?: unknown; value?: unknown } | undefined; if (content?.cursorType === cursorType && typeof content.value === 'string' && content.value.length > 0) { return content.value; } } } return undefined; } export function parseUsersFromInstructions( instructions: Array<{ type?: string; entries?: Array }> | undefined, ): TwitterUser[] { if (!instructions) { return []; } const users: TwitterUser[] = []; for (const instruction of instructions) { if (!instruction.entries) { continue; } for (const entry of instruction.entries) { const content = (entry as { content?: { itemContent?: { user_results?: { result?: unknown } } } })?.content; const rawUserResult = content?.itemContent?.user_results?.result as | { __typename?: string; rest_id?: string; is_blue_verified?: boolean; user?: unknown; legacy?: { screen_name?: string; name?: string; description?: string; followers_count?: number; friends_count?: number; profile_image_url_https?: string; created_at?: string; }; core?: { screen_name?: string; name?: string; created_at?: string; }; avatar?: { image_url?: string; }; } | undefined; const userResult = rawUserResult?.__typename === 'UserWithVisibilityResults' && rawUserResult.user ? (rawUserResult.user as typeof rawUserResult) : rawUserResult; if (!userResult || userResult.__typename !== 'User') { continue; } const legacy = userResult.legacy; const core = userResult.core; const username = legacy?.screen_name ?? core?.screen_name; if (!userResult.rest_id || !username) { continue; } users.push({ id: userResult.rest_id, username, name: legacy?.name ?? core?.name ?? username, description: legacy?.description, followersCount: legacy?.followers_count, followingCount: legacy?.friends_count, isBlueVerified: userResult.is_blue_verified, profileImageUrl: legacy?.profile_image_url_https ?? userResult.avatar?.image_url, createdAt: legacy?.created_at ?? core?.created_at, }); } } return users; }