From 2ff6e15f150163f5c67dfc0f18acb40646f3f7be Mon Sep 17 00:00:00 2001 From: dangered wolf Date: Fri, 8 Dec 2023 14:59:45 -0500 Subject: [PATCH] Synchronize terminology (status/tweet/post -> status) --- src/embed/status.ts | 142 ++++++++--------- src/helpers/author.ts | 50 +++--- src/helpers/card.ts | 2 +- src/helpers/graphql.ts | 8 +- src/helpers/translate.ts | 6 +- src/providers/twitter/conversation.ts | 110 ++++++------- src/providers/twitter/processor.ts | 219 +++++++++++++------------- src/realms/twitter/router.ts | 42 ++--- src/realms/twitter/routes/oembed.ts | 2 +- src/realms/twitter/routes/status.ts | 14 +- src/render/instantview.ts | 80 +++++----- src/render/photo.ts | 8 +- src/render/video.ts | 12 +- src/types/types.d.ts | 16 +- src/types/vendor/twitter.d.ts | 20 +-- test/worker.test.ts | 162 +++++++++---------- 16 files changed, 446 insertions(+), 447 deletions(-) diff --git a/src/embed/status.ts b/src/embed/status.ts index 3f0a2ea..220c652 100644 --- a/src/embed/status.ts +++ b/src/embed/status.ts @@ -24,7 +24,7 @@ export const returnError = (c: Context, error: string): Response => { Like Twitter, we use the terminologies interchangably. */ export const handleStatus = async ( c: Context, - status: string, + statusId: string, mediaNumber: number | undefined, userAgent: string, flags: InputFlags, @@ -41,19 +41,19 @@ export const handleStatus = async ( } const thread = await constructTwitterThread( - status, + statusId, fetchWithThreads, c, language, flags?.api ?? false ); - const tweet = thread?.status as APITweet; + const status = thread?.status as APITwitterStatus; const api = { code: thread.code, message: '', - tweet: tweet + tweet: status }; switch (api.code) { @@ -82,7 +82,7 @@ export const handleStatus = async ( return c.json(api); } - if (tweet === null) { + if (status === null) { return returnError(c, Strings.ERROR_TWEET_NOT_FOUND); } @@ -98,17 +98,17 @@ export const handleStatus = async ( } const isTelegram = (userAgent || '').indexOf('Telegram') > -1; - /* Should sensitive posts be allowed Instant View? */ + /* Should sensitive statuses be allowed Instant View? */ let useIV = - isTelegram /*&& !tweet.possibly_sensitive*/ && + isTelegram /*&& !status.possibly_sensitive*/ && !flags?.direct && !flags?.gallery && !flags?.api && - (tweet.media?.photos?.[0] || // Force instant view for photos for now https://bugs.telegram.org/c/33679 - tweet.media?.mosaic || - tweet.is_note_tweet || - tweet.quote || - tweet.translation || + (status.media?.photos?.[0] || // Force instant view for photos for now https://bugs.telegram.org/c/33679 + status.media?.mosaic || + status.is_note_tweet || + status.quote || + status.translation || flags?.forceInstantView); /* Force enable IV for archivers */ @@ -120,20 +120,20 @@ export const handleStatus = async ( let overrideMedia: APIMedia | undefined; - // Check if mediaNumber exists, and if that media exists in tweet.media.all. If it does, we'll store overrideMedia variable - if (mediaNumber && tweet.media && tweet.media.all && tweet.media.all[mediaNumber - 1]) { - overrideMedia = tweet.media.all[mediaNumber - 1]; + // Check if mediaNumber exists, and if that media exists in status.media.all. If it does, we'll store overrideMedia variable + if (mediaNumber && status.media && status.media.all && status.media.all[mediaNumber - 1]) { + overrideMedia = status.media.all[mediaNumber - 1]; } /* Catch direct media request (d.fxtwitter.com, or .mp4 / .jpg) */ - if (flags?.direct && !flags?.textOnly && tweet.media) { + if (flags?.direct && !flags?.textOnly && status.media) { let redirectUrl: string | null = null; - const all = tweet.media.all || []; - // if (tweet.media.videos) { - // const { videos } = tweet.media; + const all = status.media.all || []; + // if (status.media.videos) { + // const { videos } = status.media; // redirectUrl = (videos[(mediaNumber || 1) - 1] || videos[0]).url; - // } else if (tweet.media.photos) { - // const { photos } = tweet.media; + // } else if (status.media.photos) { + // const { photos } = status.media; // redirectUrl = (photos[(mediaNumber || 1) - 1] || photos[0]).url; // } @@ -149,48 +149,48 @@ export const handleStatus = async ( } } - /* User requested gallery view, but this isn't a post with media */ - if (flags.gallery && (tweet.media?.all?.length ?? 0) < 1) { + /* User requested gallery view, but this isn't a status with media */ + if (flags.gallery && (status.media?.all?.length ?? 0) < 1) { flags.gallery = false; } /* At this point, we know we're going to have to create a regular embed because it's not an API or direct media request */ - let authorText = getAuthorText(tweet) || Strings.DEFAULT_AUTHOR_TEXT; + let authorText = getAuthorText(status) || Strings.DEFAULT_AUTHOR_TEXT; const engagementText = authorText.replace(/ {4}/g, ' '); let siteName = Constants.BRANDING_NAME; - let newText = tweet.text; + let newText = status.text; /* Base headers included in all responses */ const headers = [ - ``, - ``, - ``, - ``, + ``, + ``, + ``, + ``, ]; if (!flags.gallery) { headers.push( ``, - `` + `` ); } - /* This little thing ensures if by some miracle a FixTweet embed is loaded in a browser, + /* This little thing ensures if by some miracle a Fixstatus embed is loaded in a browser, it will gracefully redirect to the destination instead of just seeing a blank screen. Telegram is dumb and it just gets stuck if this is included, so we never include it for Telegram UAs. */ if (!isTelegram) { headers.push( - `` + `` ); } if (useIV) { try { const instructions = renderInstantView({ - tweet: tweet, + status: status, text: newText, flags: flags }); @@ -205,11 +205,11 @@ export const handleStatus = async ( } } - console.log('translation', tweet.translation) + console.log('translation', status.translation) - /* This Tweet has a translation attached to it, so we'll render it. */ - if (tweet.translation) { - const { translation } = tweet; + /* This status has a translation attached to it, so we'll render it. */ + if (status.translation) { + const { translation } = status; const formatText = language === 'en' @@ -228,16 +228,16 @@ export const handleStatus = async ( if (!flags?.textOnly) { const media = - tweet.media?.all && tweet.media?.all.length > 0 ? tweet.media : tweet.quote?.media || {}; + status.media?.all && status.media?.all.length > 0 ? status.media : status.quote?.media || {}; if (overrideMedia) { let instructions: ResponseInstructions; switch (overrideMedia.type) { case 'photo': - /* This Tweet has a photo to render. */ + /* This status has a photo to render. */ instructions = renderPhoto( { - tweet: tweet, + status: status, authorText: authorText, engagementText: engagementText, userAgent: userAgent, @@ -253,13 +253,13 @@ export const handleStatus = async ( siteName = instructions.siteName; } /* Overwrite our Twitter Card if overriding media, so it displays correctly in Discord */ - if (tweet.embed_card === 'player') { - tweet.embed_card = 'summary_large_image'; + if (status.embed_card === 'player') { + status.embed_card = 'summary_large_image'; } break; case 'video': instructions = renderVideo( - { tweet: tweet, userAgent: userAgent, text: newText, isOverrideMedia: true }, + { status: status, userAgent: userAgent, text: newText, isOverrideMedia: true }, overrideMedia as APIVideo ); headers.push(...instructions.addHeaders); @@ -270,15 +270,15 @@ export const handleStatus = async ( siteName = instructions.siteName; } /* Overwrite our Twitter Card if overriding media, so it displays correctly in Discord */ - if (tweet.embed_card !== 'player') { - tweet.embed_card = 'player'; + if (status.embed_card !== 'player') { + status.embed_card = 'player'; } - /* This Tweet has a video to render. */ + /* This status has a video to render. */ break; } } else if (media?.videos) { const instructions = renderVideo( - { tweet: tweet, userAgent: userAgent, text: newText }, + { status: status, userAgent: userAgent, text: newText }, media.videos[0] ); headers.push(...instructions.addHeaders); @@ -291,7 +291,7 @@ export const handleStatus = async ( } else if (media?.mosaic) { const instructions = renderPhoto( { - tweet: tweet, + status: status, authorText: authorText, engagementText: engagementText, userAgent: userAgent @@ -302,7 +302,7 @@ export const handleStatus = async ( } else if (media?.photos) { const instructions = renderPhoto( { - tweet: tweet, + status: status, authorText: authorText, engagementText: engagementText, userAgent: userAgent @@ -326,9 +326,9 @@ export const handleStatus = async ( } } - /* This Tweet contains a poll, so we'll render it */ - if (tweet.poll) { - const { poll } = tweet; + /* This status contains a poll, so we'll render it */ + if (status.poll) { + const { poll } = status; let barLength = 32; let str = ''; @@ -338,7 +338,7 @@ export const handleStatus = async ( } /* Render each poll choice */ - tweet.poll.choices.forEach(choice => { + status.poll.choices.forEach(choice => { const bar = '█'.repeat((choice.percentage / 100) * barLength); // eslint-disable-next-line no-irregular-whitespace str += `${bar}\n${choice.label}  (${choice.percentage}%)\n`; @@ -366,14 +366,14 @@ export const handleStatus = async ( /* If we have no media to display, instead we'll display the user profile picture in the embed */ if ( - !tweet.media?.videos && - !tweet.media?.photos && - !tweet.quote?.media?.photos && - !tweet.quote?.media?.videos && + !status.media?.videos && + !status.media?.photos && + !status.quote?.media?.photos && + !status.quote?.media?.videos && !flags?.textOnly ) { /* Use a slightly higher resolution image for profile pics */ - const avatar = tweet.author.avatar_url; + const avatar = status.author.avatar_url; if (!useIV) { headers.push( ``, @@ -398,7 +398,7 @@ export const handleStatus = async ( and we have to pretend to be Medium in order to get working IV, but haven't figured if the template is causing issues. */ const text = useIV ? sanitizeText(newText).replace(/\n/g, '
') : sanitizeText(newText); - const useCard = tweet.embed_card === 'tweet' ? tweet.quote?.embed_card : tweet.embed_card; + const useCard = status.embed_card === 'tweet' ? status.quote?.embed_card : status.embed_card; /* Push basic headers relating to author, Tweet text, and site name */ @@ -408,31 +408,31 @@ export const handleStatus = async ( if (!flags.gallery) { headers.push( - ``, + ``, ``, ``, ); } else { if (isTelegram) { headers.push( - `` + `` ) } else { headers.push( - `` + `` ) } } /* Special reply handling if authorText is not overriden */ - if (tweet.replying_to && authorText === Strings.DEFAULT_AUTHOR_TEXT) { - authorText = `↪ Replying to @${tweet.replying_to.screen_name}`; + if (status.replying_to && authorText === Strings.DEFAULT_AUTHOR_TEXT) { + authorText = `↪ Replying to @${status.replying_to.screen_name}`; /* We'll assume it's a thread if it's a reply to themselves */ } else if ( - tweet.replying_to?.screen_name === tweet.author.screen_name && + status.replying_to?.screen_name === status.author.screen_name && authorText === Strings.DEFAULT_AUTHOR_TEXT ) { - authorText = `↪ A part of @${tweet.author.screen_name}'s thread`; + authorText = `↪ A part of @${status.author.screen_name}'s thread`; } if (!flags.gallery) { @@ -442,18 +442,18 @@ export const handleStatus = async ( ``.format( { base: Constants.HOST_URL, - text: flags.gallery ? tweet.author.name : encodeURIComponent(truncateWithEllipsis(authorText, 255)), + text: flags.gallery ? status.author.name : encodeURIComponent(truncateWithEllipsis(authorText, 255)), deprecatedFlag: flags?.deprecated ? '&deprecated=true' : '', - status: encodeURIComponent(status), - author: encodeURIComponent(tweet.author.screen_name || ''), - name: tweet.author.name || '' + status: encodeURIComponent(statusId), + author: encodeURIComponent(status.author.screen_name || ''), + name: status.author.name || '' } ) ); } /* When dealing with a Tweet of unknown lang, fall back to en */ - const lang = tweet.lang === null ? 'en' : tweet.lang || 'en'; + const lang = status.lang === null ? 'en' : status.lang || 'en'; /* Finally, after all that work we return the response HTML! */ return c.html( diff --git a/src/helpers/author.ts b/src/helpers/author.ts index 03a5de3..e9d1249 100644 --- a/src/helpers/author.ts +++ b/src/helpers/author.ts @@ -1,26 +1,26 @@ import { formatNumber } from './utils'; /* The embed "author" text we populate with replies, retweets, and likes unless it's a video */ -export const getAuthorText = (tweet: APITweet): string | null => { +export const getAuthorText = (status: APITwitterStatus): string | null => { /* Build out reply, retweet, like counts */ if ( - tweet.likes > 0 || - tweet.reposts > 0 || - tweet.replies > 0 || - (tweet.views ? tweet.views > 0 : false) + status.likes > 0 || + status.reposts > 0 || + status.replies > 0 || + (status.views ? status.views > 0 : false) ) { let authorText = ''; - if (tweet.replies > 0) { - authorText += `${formatNumber(tweet.replies)} 💬 `; + if (status.replies > 0) { + authorText += `${formatNumber(status.replies)} 💬 `; } - if (tweet.reposts > 0) { - authorText += `${formatNumber(tweet.reposts)} 🔁 `; + if (status.reposts > 0) { + authorText += `${formatNumber(status.reposts)} 🔁 `; } - if (tweet.likes > 0) { - authorText += `${formatNumber(tweet.likes)} ❤️ `; + if (status.likes > 0) { + authorText += `${formatNumber(status.likes)} ❤️ `; } - if (tweet.views && tweet.views > 0) { - authorText += `${formatNumber(tweet.views)} 👁️ `; + if (status.views && status.views > 0) { + authorText += `${formatNumber(status.views)} 👁️ `; } authorText = authorText.trim(); @@ -30,22 +30,22 @@ export const getAuthorText = (tweet: APITweet): string | null => { return null; }; -/* The embed "author" text we populate with replies, retweets, and likes unless it's a video */ -export const getSocialTextIV = (tweet: APITweet): string | null => { - /* Build out reply, retweet, like counts */ - if (tweet.likes > 0 || tweet.reposts > 0 || tweet.replies > 0) { +/* The embed "author" text we populate with replies, reposts, and likes unless it's a video */ +export const getSocialTextIV = (status: APITwitterStatus): string | null => { + /* Build out reply, repost, like counts */ + if (status.likes > 0 || status.reposts > 0 || status.replies > 0) { let authorText = ''; - if (tweet.replies > 0) { - authorText += `💬 ${formatNumber(tweet.replies)} `; + if (status.replies > 0) { + authorText += `💬 ${formatNumber(status.replies)} `; } - if (tweet.reposts > 0) { - authorText += `🔁 ${formatNumber(tweet.reposts)} `; + if (status.reposts > 0) { + authorText += `🔁 ${formatNumber(status.reposts)} `; } - if (tweet.likes > 0) { - authorText += `❤️ ${formatNumber(tweet.likes)} `; + if (status.likes > 0) { + authorText += `❤️ ${formatNumber(status.likes)} `; } - if (tweet.views && tweet.views > 0) { - authorText += `👁️ ${formatNumber(tweet.views)} `; + if (status.views && status.views > 0) { + authorText += `👁️ ${formatNumber(status.views)} `; } authorText = authorText.trim(); diff --git a/src/helpers/card.ts b/src/helpers/card.ts index 34e3f19..19424f8 100644 --- a/src/helpers/card.ts +++ b/src/helpers/card.ts @@ -2,7 +2,7 @@ import { calculateTimeLeftString } from './pollTime'; /* Renders card for polls and non-Twitter video embeds (i.e. YouTube) */ export const renderCard = ( - card: GraphQLTweet['card'] + card: GraphQLTwitterStatus['card'] ): { poll?: APIPoll; external_media?: APIExternalMedia } => { if (!Array.isArray(card.legacy.binding_values)) { return {}; diff --git a/src/helpers/graphql.ts b/src/helpers/graphql.ts index 6d4049a..210056f 100644 --- a/src/helpers/graphql.ts +++ b/src/helpers/graphql.ts @@ -1,6 +1,6 @@ -export const isGraphQLTweetNotFoundResponse = ( +export const isGraphQLTwitterStatusNotFoundResponse = ( response: unknown -): response is GraphQLTweetNotFoundResponse => { +): response is GraphQLTwitterStatusNotFoundResponse => { return ( typeof response === 'object' && response !== null && @@ -12,12 +12,12 @@ export const isGraphQLTweetNotFoundResponse = ( ); }; -export const isGraphQLTweet = (response: unknown): response is GraphQLTweet => { +export const isGraphQLTwitterStatus = (response: unknown): response is GraphQLTwitterStatus => { return ( typeof response === 'object' && response !== null && (('__typename' in response && (response.__typename === 'Tweet' || response.__typename === 'TweetWithVisibilityResults')) || - typeof (response as GraphQLTweet).legacy?.full_text === 'string') + typeof (response as GraphQLTwitterStatus).legacy?.full_text === 'string') ); }; diff --git a/src/helpers/translate.ts b/src/helpers/translate.ts index 9438e6b..eca3845 100644 --- a/src/helpers/translate.ts +++ b/src/helpers/translate.ts @@ -2,9 +2,9 @@ import { Context } from 'hono'; import { Constants } from '../constants'; import { withTimeout } from './utils'; -/* Handles translating Tweets when asked! */ -export const translateTweet = async ( - tweet: GraphQLTweet, +/* Handles translating statuses when asked! */ +export const translateStatus = async ( + tweet: GraphQLTwitterStatus, guestToken: string, language: string, c: Context diff --git a/src/providers/twitter/conversation.ts b/src/providers/twitter/conversation.ts index 955f602..4d174cf 100644 --- a/src/providers/twitter/conversation.ts +++ b/src/providers/twitter/conversation.ts @@ -1,8 +1,8 @@ import { Constants } from '../../constants'; import { twitterFetch } from '../../fetch'; -import { buildAPITweet } from './processor'; +import { buildAPITwitterStatus } from './processor'; import { Experiment, experimentCheck } from '../../experiments'; -import { isGraphQLTweet } from '../../helpers/graphql'; +import { isGraphQLTwitterStatus } from '../../helpers/graphql'; import { Context } from 'hono'; export const fetchTweetDetail = async ( @@ -60,11 +60,11 @@ export const fetchTweetDetail = async ( useElongator, (_conversation: unknown) => { const conversation = _conversation as TweetDetailResult; - const tweet = findTweetInBucket( + const tweet = findStatusInBucket( status, processResponse(conversation?.data?.threaded_conversation_with_injections_v2?.instructions) ); - if (tweet && isGraphQLTweet(tweet)) { + if (tweet && isGraphQLTwitterStatus(tweet)) { return true; } console.log('invalid graphql tweet', conversation); @@ -126,7 +126,7 @@ export const fetchByRestId = async ( const conversation = _conversation as TweetResultsByRestIdResult; // If we get a not found error it's still a valid response const tweet = conversation.data?.tweetResult?.result; - if (isGraphQLTweet(tweet)) { + if (isGraphQLTwitterStatus(tweet)) { return true; } console.log('invalid graphql tweet'); @@ -159,7 +159,7 @@ export const fetchByRestId = async ( const processResponse = (instructions: ThreadInstruction[]): GraphQLProcessBucket => { const bucket: GraphQLProcessBucket = { - tweets: [], + statuses: [], cursors: [] }; instructions?.forEach?.(instruction => { @@ -181,10 +181,10 @@ const processResponse = (instructions: ThreadInstruction[]): GraphQLProcessBucke if (itemContentType === 'TimelineTweet') { const entryType = content.itemContent.tweet_results.result.__typename; if (entryType === 'Tweet') { - bucket.tweets.push(content.itemContent.tweet_results.result as GraphQLTweet); + bucket.statuses.push(content.itemContent.tweet_results.result as GraphQLTwitterStatus); } if (entryType === 'TweetWithVisibilityResults') { - bucket.tweets.push(content.itemContent.tweet_results.result.tweet as GraphQLTweet); + bucket.statuses.push(content.itemContent.tweet_results.result.tweet as GraphQLTwitterStatus); } } else if (itemContentType === 'TimelineTimelineCursor') { bucket.cursors.push(content.itemContent as GraphQLTimelineCursor); @@ -197,11 +197,11 @@ const processResponse = (instructions: ThreadInstruction[]): GraphQLProcessBucke if (itemContentType === 'TimelineTweet') { const entryType = item.item.itemContent.tweet_results?.result?.__typename; if (entryType === 'Tweet') { - bucket.tweets.push(item.item.itemContent.tweet_results.result as GraphQLTweet); + bucket.statuses.push(item.item.itemContent.tweet_results.result as GraphQLTwitterStatus); } if (entryType === 'TweetWithVisibilityResults') { - bucket.tweets.push( - item.item.itemContent.tweet_results.result.tweet as GraphQLTweet + bucket.statuses.push( + item.item.itemContent.tweet_results.result.tweet as GraphQLTwitterStatus ); } } else if (itemContentType === 'TimelineTimelineCursor') { @@ -216,22 +216,22 @@ const processResponse = (instructions: ThreadInstruction[]): GraphQLProcessBucke return bucket; }; -const findTweetInBucket = (id: string, bucket: GraphQLProcessBucket): GraphQLTweet | null => { - return bucket.tweets.find(tweet => (tweet.rest_id ?? tweet.legacy?.id_str) === id) ?? null; +const findStatusInBucket = (id: string, bucket: GraphQLProcessBucket): GraphQLTwitterStatus | null => { + return bucket.statuses.find(status => (status.rest_id ?? status.legacy?.id_str) === id) ?? null; }; -const findNextTweet = (id: string, bucket: GraphQLProcessBucket): number => { - return bucket.tweets.findIndex(tweet => tweet.legacy?.in_reply_to_status_id_str === id); +const findNextStatus = (id: string, bucket: GraphQLProcessBucket): number => { + return bucket.statuses.findIndex(status => status.legacy?.in_reply_to_status_id_str === id); }; -const findPreviousTweet = (id: string, bucket: GraphQLProcessBucket): number => { - const tweet = bucket.tweets.find(tweet => (tweet.rest_id ?? tweet.legacy?.id_str) === id); - if (!tweet) { +const findPreviousStatus = (id: string, bucket: GraphQLProcessBucket): number => { + const status = bucket.statuses.find(status => (status.rest_id ?? status.legacy?.id_str) === id); + if (!status) { console.log('uhhh, we could not even find that tweet, dunno how that happened'); return -1; } - return bucket.tweets.findIndex( - _tweet => (_tweet.rest_id ?? _tweet.legacy?.id_str) === tweet.legacy?.in_reply_to_status_id_str + return bucket.statuses.findIndex( + _status => (_status.rest_id ?? _status.legacy?.id_str) === status.legacy?.in_reply_to_status_id_str ); }; @@ -249,7 +249,7 @@ const consolidateCursors = ( }); }; -const filterBucketTweets = (tweets: GraphQLTweet[], original: GraphQLTweet) => { +const filterBucketStatuses = (tweets: GraphQLTwitterStatus[], original: GraphQLTwitterStatus) => { return tweets.filter( tweet => tweet.core?.user_results?.result?.rest_id === original.core?.user_results?.result?.rest_id @@ -267,7 +267,7 @@ export const constructTwitterThread = async ( console.log('language', language); let response: TweetDetailResult | TweetResultsByRestIdResult | null = null; - let status: APITweet; + let status: APITwitterStatus; /* We can use TweetDetail on elongator accounts to increase per-account rate limit. We also use TweetDetail to process threads (WIP) @@ -293,13 +293,13 @@ export const constructTwitterThread = async ( console.log('Using TweetResultsByRestId for request...'); response = (await fetchByRestId(id, c)) as TweetResultsByRestIdResult; - const result = response?.data?.tweetResult?.result as GraphQLTweet; + const result = response?.data?.tweetResult?.result as GraphQLTwitterStatus; if (typeof result === 'undefined') { return { status: null, thread: null, author: null, code: 404 }; } - const buildStatus = await buildAPITweet(c, result, language, false, legacyAPI); + const buildStatus = await buildAPITwitterStatus(c, result, language, false, legacyAPI); if ((buildStatus as FetchResults)?.status === 401) { return { status: null, thread: null, author: null, code: 401 }; @@ -307,7 +307,7 @@ export const constructTwitterThread = async ( return { status: null, thread: null, author: null, code: 404 }; } - status = buildStatus as APITweet; + status = buildStatus as APITwitterStatus; return { status: status, thread: null, author: status.author, code: 200 }; } @@ -315,14 +315,14 @@ export const constructTwitterThread = async ( const bucket = processResponse( response?.data?.threaded_conversation_with_injections_v2?.instructions ?? [] ); - const originalTweet = findTweetInBucket(id, bucket); + const originalStatus = findStatusInBucket(id, bucket); /* Don't bother processing thread on a null tweet */ - if (originalTweet === null) { + if (originalStatus === null) { return { status: null, thread: null, author: null, code: 404 }; } - status = (await buildAPITweet(c, originalTweet, undefined, false, legacyAPI)) as APITweet; + status = (await buildAPITwitterStatus(c, originalStatus, undefined, false, legacyAPI)) as APITwitterStatus; if (status === null) { return { status: null, thread: null, author: null, code: 404 }; @@ -335,15 +335,15 @@ export const constructTwitterThread = async ( return { status: status, thread: null, author: author, code: 200 }; } - const threadTweets = [originalTweet]; - bucket.tweets = filterBucketTweets(bucket.tweets, originalTweet); + const threadStatuses = [originalStatus]; + bucket.statuses = filterBucketStatuses(bucket.statuses, originalStatus); let currentId = id; /* Process tweets that are following the current one in the thread */ - while (findNextTweet(currentId, bucket) !== -1) { - const index = findNextTweet(currentId, bucket); - const tweet = bucket.tweets[index]; + while (findNextStatus(currentId, bucket) !== -1) { + const index = findNextStatus(currentId, bucket); + const tweet = bucket.statuses[index]; const newCurrentId = tweet.rest_id ?? tweet.legacy?.id_str; @@ -357,15 +357,15 @@ export const constructTwitterThread = async ( 'in bucket' ); - threadTweets.push(tweet); + threadStatuses.push(tweet); currentId = newCurrentId; - console.log('Current index', index, 'of', bucket.tweets.length); + console.log('Current index', index, 'of', bucket.statuses.length); - /* Reached the end of the current list of tweets in thread) */ - if (index >= bucket.tweets.length - 1) { - /* See if we have a cursor to fetch more tweets */ + /* Reached the end of the current list of statuses in thread) */ + if (index >= bucket.statuses.length - 1) { + /* See if we have a cursor to fetch more statuses */ const cursor = bucket.cursors.find( cursor => cursor.cursorType === 'Bottom' || cursor.cursorType === 'ShowMore' ); @@ -396,26 +396,26 @@ export const constructTwitterThread = async ( const cursorResponse = processResponse( loadCursor?.data?.threaded_conversation_with_injections_v2?.instructions ?? [] ); - bucket.tweets = bucket.tweets.concat( - filterBucketTweets(cursorResponse.tweets, originalTweet) + bucket.statuses = bucket.statuses.concat( + filterBucketStatuses(cursorResponse.statuses, originalStatus) ); /* Remove old cursor and add new bottom cursor if necessary */ consolidateCursors(bucket.cursors, cursorResponse.cursors); console.log('updated bucket of cursors', bucket.cursors); } - console.log('Preview of next tweet:', findNextTweet(currentId, bucket)); + console.log('Preview of next status:', findNextStatus(currentId, bucket)); } currentId = id; - while (findPreviousTweet(currentId, bucket) !== -1) { - const index = findPreviousTweet(currentId, bucket); - const tweet = bucket.tweets[index]; - const newCurrentId = tweet.rest_id ?? tweet.legacy?.id_str; + while (findPreviousStatus(currentId, bucket) !== -1) { + const index = findPreviousStatus(currentId, bucket); + const status = bucket.statuses[index]; + const newCurrentId = status.rest_id ?? status.legacy?.id_str; console.log( - 'adding previous tweet to thread', + 'adding previous status to thread', newCurrentId, 'from', currentId, @@ -424,12 +424,12 @@ export const constructTwitterThread = async ( 'in bucket' ); - threadTweets.unshift(tweet); + threadStatuses.unshift(status); currentId = newCurrentId; if (index === 0) { - /* See if we have a cursor to fetch more tweets */ + /* See if we have a cursor to fetch more statuses */ const cursor = bucket.cursors.find( cursor => cursor.cursorType === 'Top' || cursor.cursorType === 'ShowMore' ); @@ -438,7 +438,7 @@ export const constructTwitterThread = async ( console.log('No cursor present, stopping pagination up'); break; } - console.log('Cursor present, fetching more tweets up'); + console.log('Cursor present, fetching more statuses up'); let loadCursor: TweetDetailResult; @@ -459,17 +459,17 @@ export const constructTwitterThread = async ( const cursorResponse = processResponse( loadCursor?.data?.threaded_conversation_with_injections_v2?.instructions ?? [] ); - bucket.tweets = cursorResponse.tweets.concat( - filterBucketTweets(bucket.tweets, originalTweet) + bucket.statuses = cursorResponse.statuses.concat( + filterBucketStatuses(bucket.statuses, originalStatus) ); /* Remove old cursor and add new top cursor if necessary */ consolidateCursors(bucket.cursors, cursorResponse.cursors); - // console.log('updated bucket of tweets', bucket.tweets); + // console.log('updated bucket of statuses', bucket.statuses); console.log('updated bucket of cursors', bucket.cursors); } - console.log('Preview of previous tweet:', findPreviousTweet(currentId, bucket)); + console.log('Preview of previous status:', findPreviousStatus(currentId, bucket)); } const socialThread: SocialThread = { @@ -479,8 +479,8 @@ export const constructTwitterThread = async ( code: 200 }; - threadTweets.forEach(async tweet => { - socialThread.thread?.push((await buildAPITweet(c, tweet, undefined, true, false)) as APITweet); + threadStatuses.forEach(async status => { + socialThread.thread?.push((await buildAPITwitterStatus(c, status, undefined, true, false)) as APITwitterStatus); }); return socialThread; diff --git a/src/providers/twitter/processor.ts b/src/providers/twitter/processor.ts index f0d070d..d0abf1a 100644 --- a/src/providers/twitter/processor.ts +++ b/src/providers/twitter/processor.ts @@ -2,65 +2,64 @@ import { renderCard } from '../../helpers/card'; import { Constants } from '../../constants'; import { linkFixer } from '../../helpers/linkFixer'; import { handleMosaic } from '../../helpers/mosaic'; -// import { translateTweet } from '../../helpers/translate'; import { unescapeText } from '../../helpers/utils'; import { processMedia } from '../../helpers/media'; import { convertToApiUser } from './profile'; -import { translateTweet } from '../../helpers/translate'; +import { translateStatus } from '../../helpers/translate'; import { Context } from 'hono'; -export const buildAPITweet = async ( +export const buildAPITwitterStatus = async ( c: Context, - tweet: GraphQLTweet, + status: GraphQLTwitterStatus, language: string | undefined, threadPiece = false, legacyAPI = false // eslint-disable-next-line sonarjs/cognitive-complexity -): Promise => { - const apiTweet = {} as APITweet; +): Promise => { + const apiStatus = {} as APITwitterStatus; - /* Sometimes, Twitter returns a different kind of Tweet type called 'TweetWithVisibilityResults'. + /* Sometimes, Twitter returns a different kind of type called 'TweetWithVisibilityResults'. It has slightly different attributes from the regular 'Tweet' type. We fix that up here. */ - if (typeof tweet.core === 'undefined' && typeof tweet.result !== 'undefined') { - tweet = tweet.result; + if (typeof status.core === 'undefined' && typeof status.result !== 'undefined') { + status = status.result; } - if (typeof tweet.core === 'undefined' && typeof tweet.tweet?.core !== 'undefined') { - tweet.core = tweet.tweet.core; + if (typeof status.core === 'undefined' && typeof status.tweet?.core !== 'undefined') { + status.core = status.tweet.core; } - if (typeof tweet.legacy === 'undefined' && typeof tweet.tweet?.legacy !== 'undefined') { - tweet.legacy = tweet.tweet?.legacy; + if (typeof status.legacy === 'undefined' && typeof status.tweet?.legacy !== 'undefined') { + status.legacy = status.tweet?.legacy; } - if (typeof tweet.views === 'undefined' && typeof tweet?.tweet?.views !== 'undefined') { - tweet.views = tweet?.tweet?.views; + if (typeof status.views === 'undefined' && typeof status?.tweet?.views !== 'undefined') { + status.views = status?.tweet?.views; } - if (typeof tweet.core === 'undefined') { - console.log('Tweet still not valid', tweet); - if (tweet.__typename === 'TweetUnavailable' && tweet.reason === 'Protected') { + if (typeof status.core === 'undefined') { + console.log('Status still not valid', status); + if (status.__typename === 'TweetUnavailable' && status.reason === 'Protected') { return { status: 401 }; } else { return { status: 404 }; } } - const graphQLUser = tweet.core.user_results.result; + const graphQLUser = status.core.user_results.result; const apiUser = convertToApiUser(graphQLUser); /* Sometimes, `rest_id` is undefined for some reason. Inconsistent behavior. See: https://github.com/FixTweet/FixTweet/issues/416 */ - const id = tweet.rest_id ?? tweet.legacy.id_str; + const id = status.rest_id ?? status.legacy.id_str; /* Populating a lot of the basics */ - apiTweet.url = `${Constants.TWITTER_ROOT}/${apiUser.screen_name}/status/${id}`; - apiTweet.id = id; - apiTweet.text = unescapeText( - linkFixer(tweet.legacy.entities?.urls, tweet.legacy.full_text || '') + apiStatus.url = `${Constants.TWITTER_ROOT}/${apiUser.screen_name}/status/${id}`; + apiStatus.id = id; + apiStatus.text = unescapeText( + linkFixer(status.legacy.entities?.urls, status.legacy.full_text || '') ); if (!threadPiece) { - apiTweet.author = { + apiStatus.author = { id: apiUser.id, name: apiUser.name, screen_name: apiUser.screen_name, @@ -79,111 +78,111 @@ export const buildAPITweet = async ( website: apiUser.website }; } - apiTweet.replies = tweet.legacy.reply_count; + apiStatus.replies = status.legacy.reply_count; if (legacyAPI) { // @ts-expect-error Use retweets for legacy API - apiTweet.retweets = tweet.legacy.retweet_count; + apiStatus.retweets = status.legacy.retweet_count; // @ts-expect-error `tweets` is only part of legacy API - apiTweet.author.tweets = apiTweet.author.statuses; + apiStatus.author.tweets = apiStatus.author.statuses; // @ts-expect-error Part of legacy API that we no longer are able to track - apiTweet.author.avatar_color = null; + apiStatus.author.avatar_color = null; // @ts-expect-error Use retweets for legacy API - delete apiTweet.reposts; + delete apiStatus.reposts; // @ts-expect-error Use tweets and not posts for legacy API - delete apiTweet.author.statuses; - delete apiTweet.author.global_screen_name; + delete apiStatus.author.statuses; + delete apiStatus.author.global_screen_name; } else { - apiTweet.reposts = tweet.legacy.retweet_count; - apiTweet.author.global_screen_name = apiUser.global_screen_name; + apiStatus.reposts = status.legacy.retweet_count; + apiStatus.author.global_screen_name = apiUser.global_screen_name; } - apiTweet.likes = tweet.legacy.favorite_count; - apiTweet.embed_card = 'tweet'; - apiTweet.created_at = tweet.legacy.created_at; - apiTweet.created_timestamp = new Date(tweet.legacy.created_at).getTime() / 1000; + apiStatus.likes = status.legacy.favorite_count; + apiStatus.embed_card = 'tweet'; + apiStatus.created_at = status.legacy.created_at; + apiStatus.created_timestamp = new Date(status.legacy.created_at).getTime() / 1000; - apiTweet.possibly_sensitive = tweet.legacy.possibly_sensitive; + apiStatus.possibly_sensitive = status.legacy.possibly_sensitive; - if (tweet.views.state === 'EnabledWithCount') { - apiTweet.views = parseInt(tweet.views.count || '0') ?? null; + if (status.views.state === 'EnabledWithCount') { + apiStatus.views = parseInt(status.views.count || '0') ?? null; } else { - apiTweet.views = null; + apiStatus.views = null; } - console.log('note_tweet', JSON.stringify(tweet.note_tweet)); - const noteTweetText = tweet.note_tweet?.note_tweet_results?.result?.text; + console.log('note_tweet', JSON.stringify(status.note_tweet)); + const noteTweetText = status.note_tweet?.note_tweet_results?.result?.text; if (noteTweetText) { - tweet.legacy.entities.urls = tweet.note_tweet?.note_tweet_results?.result?.entity_set.urls; - tweet.legacy.entities.hashtags = - tweet.note_tweet?.note_tweet_results?.result?.entity_set.hashtags; - tweet.legacy.entities.symbols = - tweet.note_tweet?.note_tweet_results?.result?.entity_set.symbols; + status.legacy.entities.urls = status.note_tweet?.note_tweet_results?.result?.entity_set.urls; + status.legacy.entities.hashtags = + status.note_tweet?.note_tweet_results?.result?.entity_set.hashtags; + status.legacy.entities.symbols = + status.note_tweet?.note_tweet_results?.result?.entity_set.symbols; console.log('We meet the conditions to use new note tweets'); - apiTweet.text = unescapeText(linkFixer(tweet.legacy.entities.urls, noteTweetText)); - apiTweet.is_note_tweet = true; + apiStatus.text = unescapeText(linkFixer(status.legacy.entities.urls, noteTweetText)); + apiStatus.is_note_tweet = true; } else { - apiTweet.is_note_tweet = false; + apiStatus.is_note_tweet = false; } - if (tweet.legacy.lang !== 'unk') { - apiTweet.lang = tweet.legacy.lang; + if (status.legacy.lang !== 'unk') { + apiStatus.lang = status.legacy.lang; } else { - apiTweet.lang = null; + apiStatus.lang = null; } if (legacyAPI) { // @ts-expect-error Use replying_to string for legacy API - apiTweet.replying_to = tweet.legacy?.in_reply_to_screen_name || null; + apiStatus.replying_to = status.legacy?.in_reply_to_screen_name || null; // @ts-expect-error Use replying_to_status string for legacy API - apiTweet.replying_to_status = tweet.legacy?.in_reply_to_status_id_str || null; - } else if (tweet.legacy.in_reply_to_screen_name) { - apiTweet.replying_to = { - screen_name: tweet.legacy.in_reply_to_screen_name || null, - post: tweet.legacy.in_reply_to_status_id_str || null + apiStatus.replying_to_status = status.legacy?.in_reply_to_status_id_str || null; + } else if (status.legacy.in_reply_to_screen_name) { + apiStatus.replying_to = { + screen_name: status.legacy.in_reply_to_screen_name || null, + post: status.legacy.in_reply_to_status_id_str || null }; } else { - apiTweet.replying_to = null; + apiStatus.replying_to = null; } - apiTweet.media = {}; + apiStatus.media = {}; - /* We found a quote tweet, let's process that too */ - const quoteTweet = tweet.quoted_status_result; - if (quoteTweet) { - const buildQuote = await buildAPITweet(c, quoteTweet, language, threadPiece, legacyAPI); + /* We found a quote, let's process that too */ + const quote = status.quoted_status_result; + if (quote) { + const buildQuote = await buildAPITwitterStatus(c, quote, language, threadPiece, legacyAPI); if ((buildQuote as FetchResults).status) { - apiTweet.quote = undefined; + apiStatus.quote = undefined; } else { - apiTweet.quote = buildQuote as APITweet; + apiStatus.quote = buildQuote as APITwitterStatus; } - /* Only override the embed_card if it's a basic tweet, since media always takes precedence */ - if (apiTweet.embed_card === 'tweet' && typeof apiTweet.quote !== 'undefined') { - apiTweet.embed_card = apiTweet.quote.embed_card; + /* Only override the embed_card if it's a basic status, since media always takes precedence */ + if (apiStatus.embed_card === 'tweet' && typeof apiStatus.quote !== 'undefined') { + apiStatus.embed_card = apiStatus.quote.embed_card; } } const mediaList = Array.from( - tweet.legacy.extended_entities?.media || tweet.legacy.entities?.media || [] + status.legacy.extended_entities?.media || status.legacy.entities?.media || [] ); - // console.log('tweet', JSON.stringify(tweet)); + // console.log('status', JSON.stringify(status)); - /* Populate this Tweet's media */ + /* Populate status media */ mediaList.forEach(media => { const mediaObject = processMedia(media); if (mediaObject) { - apiTweet.media.all = apiTweet.media?.all ?? []; - apiTweet.media?.all?.push(mediaObject); + apiStatus.media.all = apiStatus.media?.all ?? []; + apiStatus.media?.all?.push(mediaObject); if (mediaObject.type === 'photo') { - apiTweet.embed_card = 'summary_large_image'; - apiTweet.media.photos = apiTweet.media?.photos ?? []; - apiTweet.media.photos?.push(mediaObject); + apiStatus.embed_card = 'summary_large_image'; + apiStatus.media.photos = apiStatus.media?.photos ?? []; + apiStatus.media.photos?.push(mediaObject); } else if (mediaObject.type === 'video' || mediaObject.type === 'gif') { - apiTweet.embed_card = 'player'; - apiTweet.media.videos = apiTweet.media?.videos ?? []; - apiTweet.media.videos?.push(mediaObject); + apiStatus.embed_card = 'player'; + apiStatus.media.videos = apiStatus.media?.videos ?? []; + apiStatus.media.videos?.push(mediaObject); } else { console.log('Unknown media type', mediaObject.type); } @@ -193,21 +192,21 @@ export const buildAPITweet = async ( /* Grab color palette data */ /* if (mediaList[0]?.ext_media_color?.palette) { - apiTweet.color = colorFromPalette(mediaList[0].ext_media_color.palette); + apiStatus.color = colorFromPalette(mediaList[0].ext_media_color.palette); } */ /* Handle photos and mosaic if available */ - if ((apiTweet?.media.photos?.length || 0) > 1 && !threadPiece) { - const mosaic = await handleMosaic(apiTweet.media?.photos || [], id); - if (typeof apiTweet.media !== 'undefined' && mosaic !== null) { - apiTweet.media.mosaic = mosaic; + if ((apiStatus?.media.photos?.length || 0) > 1 && !threadPiece) { + const mosaic = await handleMosaic(apiStatus.media?.photos || [], id); + if (typeof apiStatus.media !== 'undefined' && mosaic !== null) { + apiStatus.media.mosaic = mosaic; } } - // Add Tweet source but remove the link HTML tag - if (tweet.source) { - apiTweet.source = (tweet.source || '').replace( + // Add source but remove the link HTML tag + if (status.source) { + apiStatus.source = (status.source || '').replace( /(.+?)<\/a>/, '$2' ); @@ -215,34 +214,34 @@ export const buildAPITweet = async ( /* Populate a Twitter card */ - if (tweet.card) { - const card = renderCard(tweet.card); + if (status.card) { + const card = renderCard(status.card); if (card.external_media) { - apiTweet.media.external = card.external_media; + apiStatus.media.external = card.external_media; } if (card.poll) { - apiTweet.poll = card.poll; + apiStatus.poll = card.poll; } } if ( - apiTweet.media?.videos && - apiTweet.media?.videos.length > 0 && - apiTweet.embed_card !== 'player' + apiStatus.media?.videos && + apiStatus.media?.videos.length > 0 && + apiStatus.embed_card !== 'player' ) { - apiTweet.embed_card = 'player'; + apiStatus.embed_card = 'player'; } console.log('language?', language); /* If a language is specified in API or by user, let's try translating it! */ - if (typeof language === 'string' && language.length === 2 && language !== tweet.legacy.lang) { - console.log(`Attempting to translate Tweet to ${language}...`); - const translateAPI = await translateTweet(tweet, '', language, c); + if (typeof language === 'string' && language.length === 2 && language !== status.legacy.lang) { + console.log(`Attempting to translate status to ${language}...`); + const translateAPI = await translateStatus(status, '', language, c); if (translateAPI !== null && translateAPI?.translation) { - apiTweet.translation = { + apiStatus.translation = { text: unescapeText( - linkFixer(tweet.legacy?.entities?.urls, translateAPI?.translation || '') + linkFixer(status.legacy?.entities?.urls, translateAPI?.translation || '') ), source_lang: translateAPI?.sourceLanguage || '', target_lang: translateAPI?.destinationLanguage || '', @@ -253,16 +252,16 @@ export const buildAPITweet = async ( if (legacyAPI) { // @ts-expect-error Use twitter_card for legacy API - apiTweet.twitter_card = apiTweet.embed_card; + apiStatus.twitter_card = apiStatus.embed_card; // @ts-expect-error Part of legacy API that we no longer are able to track - apiTweet.color = null; + apiStatus.color = null; // @ts-expect-error Use twitter_card for legacy API - delete apiTweet.embed_card; - if ((apiTweet.media.all?.length ?? 0) < 1 && !apiTweet.media.external) { + delete apiStatus.embed_card; + if ((apiStatus.media.all?.length ?? 0) < 1 && !apiStatus.media.external) { // @ts-expect-error media is not required in legacy API if empty - delete apiTweet.media; + delete apiStatus.media; } } - return apiTweet; + return apiStatus; }; diff --git a/src/realms/twitter/router.ts b/src/realms/twitter/router.ts index a4014aa..19512c7 100644 --- a/src/realms/twitter/router.ts +++ b/src/realms/twitter/router.ts @@ -27,72 +27,72 @@ export const getBaseRedirectUrl = (c: Context) => { }; /* Workaround for some dumb maybe-build time issue where statusRequest isn't ready or something because none of these trigger*/ -const tweetRequest = async (c: Context) => await statusRequest(c); +const twitterStatusRequest = async (c: Context) => await statusRequest(c); const _profileRequest = async (c: Context) => await profileRequest(c); /* How can hono not handle trailing slashes? This is so stupid, serious TODO: Figure out how to make this not stupid. */ -twitter.get('/:endpoint{status(es)?}/:id', tweetRequest); -twitter.get('/:endpoint{status(es)?}/:id/', tweetRequest); -twitter.get('/:endpoint{status(es)?}/:id/:language/', tweetRequest); -twitter.get('/:endpoint{status(es)?}/:id/:language', tweetRequest); +twitter.get('/:endpoint{status(es)?}/:id', twitterStatusRequest); +twitter.get('/:endpoint{status(es)?}/:id/', twitterStatusRequest); +twitter.get('/:endpoint{status(es)?}/:id/:language/', twitterStatusRequest); +twitter.get('/:endpoint{status(es)?}/:id/:language', twitterStatusRequest); twitter.get( '/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:language', - tweetRequest + twitterStatusRequest ); twitter.get( '/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:language/', - tweetRequest + twitterStatusRequest ); -twitter.get('/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id', tweetRequest); -twitter.get('/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/', tweetRequest); +twitter.get('/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id', twitterStatusRequest); +twitter.get('/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/', twitterStatusRequest); twitter.get( '/:prefix{(dir|dl)}/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:language', - tweetRequest + twitterStatusRequest ); twitter.get( '/:prefix{(dir|dl)}/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:language/', - tweetRequest + twitterStatusRequest ); twitter.get( '/:prefix{(dir|dl)}/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id', - tweetRequest + twitterStatusRequest ); twitter.get( '/:prefix{(dir|dl)}/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/', - tweetRequest + twitterStatusRequest ); twitter.get( '/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:mediaType{(photos?|videos?)}/:mediaNumber{[1-4]}', - tweetRequest + twitterStatusRequest ); twitter.get( '/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:mediaType{(photos?|videos?)}/:mediaNumber{[1-4]}/', - tweetRequest + twitterStatusRequest ); twitter.get( '/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:mediaType{(photos?|videos?)}/:mediaNumber{[1-4]}/:language', - tweetRequest + twitterStatusRequest ); twitter.get( '/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:mediaType{(photos?|videos?)}/:mediaNumber{[1-4]}/:language/', - tweetRequest + twitterStatusRequest ); twitter.get( '/:prefix{(dir|dl)}/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:mediaType{(photos?|videos?)}/:mediaNumber{[1-4]}', - tweetRequest + twitterStatusRequest ); twitter.get( '/:prefix{(dir|dl)}/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:mediaType{(photos?|videos?)}/:mediaNumber{[1-4]}/', - tweetRequest + twitterStatusRequest ); twitter.get( '/:prefix{(dir|dl)}/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:mediaType{(photos?|videos?)}/:mediaNumber{[1-4]}/:language', - tweetRequest + twitterStatusRequest ); twitter.get( '/:prefix{(dir|dl)}/:handle{[0-9a-zA-Z_]+}/:endpoint{status(es)?}/:id/:mediaType{(photos?|videos?)}/:mediaNumber{[1-4]}/:language/', - tweetRequest + twitterStatusRequest ); twitter.get('/version/', versionRoute); diff --git a/src/realms/twitter/routes/oembed.ts b/src/realms/twitter/routes/oembed.ts index fe3aa9b..de80759 100644 --- a/src/realms/twitter/routes/oembed.ts +++ b/src/realms/twitter/routes/oembed.ts @@ -18,7 +18,7 @@ export const oembed = async (c: Context) => { const data: OEmbed = { author_name: text, author_url: `${Constants.TWITTER_ROOT}/${encodeURIComponent(author)}/status/${status}`, - /* Change provider name if tweet is on deprecated domain. */ + /* Change provider name if status is on deprecated domain. */ provider_name: searchParams.get('deprecated') === 'true' ? Strings.DEPRECATED_DOMAIN_NOTICE_DISCORD : name, provider_url: url, diff --git a/src/realms/twitter/routes/status.ts b/src/realms/twitter/routes/status.ts index d3560a5..8b5d4c8 100644 --- a/src/realms/twitter/routes/status.ts +++ b/src/realms/twitter/routes/status.ts @@ -4,7 +4,7 @@ import { getBaseRedirectUrl } from '../router'; import { handleStatus } from '../../../embed/status'; import { Strings } from '../../../strings'; -/* Handler for status (Tweet) request */ +/* Handler for status request */ export const statusRequest = async (c: Context) => { const { prefix, handle, id, mediaNumber, language } = c.req.param(); const url = new URL(c.req.url); @@ -35,8 +35,8 @@ export const statusRequest = async (c: Context) => { const isBotUA = userAgent.match(Constants.BOT_UA_REGEX) !== null || flags?.archive; /* Check if domain is a direct media domain (i.e. d.fxtwitter.com), - the tweet is prefixed with /dl/ or /dir/ (for TwitFix interop), or the - tweet ends in .mp4, .jpg, .jpeg, or .png + the status is prefixed with /dl/ or /dir/ (for TwitFix interop), or the + status ends in .mp4, .jpg, .jpeg, or .png Note that .png is not documented because images always redirect to a jpg, but it will help someone who does it mistakenly on something like Discord @@ -63,7 +63,7 @@ export const statusRequest = async (c: Context) => { flags.direct = true; } - /* The pxtwitter.com domain is deprecated and Tweets posted after deprecation + /* The pxtwitter.com domain is deprecated and statuses posted after deprecation date will have a notice saying we've moved to fxtwitter.com! */ if ( Constants.DEPRECATED_DOMAIN_LIST.includes(url.hostname) && @@ -115,9 +115,9 @@ export const statusRequest = async (c: Context) => { if (statusResponse) { /* We're checking if the User Agent is a bot again specifically in case they requested - direct media (d.fxtwitter.com, .mp4/.jpg, etc) but the Tweet contains no media. + direct media (d.fxtwitter.com, .mp4/.jpg, etc) but the status contains no media. - Since we obviously have no media to give the user, we'll just redirect to the Tweet. + Since we obviously have no media to give the user, we'll just redirect to the status. Embeds will return as usual to bots as if direct media was never specified. */ if (!isBotUA && !flags.api && !flags.direct) { const baseUrl = getBaseRedirectUrl(c); @@ -135,7 +135,7 @@ export const statusRequest = async (c: Context) => { } } else { /* A human has clicked a fxtwitter.com/:screen_name/status/:id link! - Obviously we just need to redirect to the Tweet directly.*/ + Obviously we just need to redirect to the status directly.*/ console.log('Matched human UA', userAgent); return c.redirect(`${baseUrl}/${handle || 'i'}/status/${id?.match(/\d{2,20}/)?.[0]}`, 302); diff --git a/src/render/instantview.ts b/src/render/instantview.ts index 40a5fa9..3dddc85 100644 --- a/src/render/instantview.ts +++ b/src/render/instantview.ts @@ -4,7 +4,7 @@ import { getSocialTextIV } from '../helpers/author'; import { sanitizeText } from '../helpers/utils'; import { Strings } from '../strings'; -const populateUserLinks = (tweet: APIStatus, text: string): string => { +const populateUserLinks = (status: APIStatus, text: string): string => { /* TODO: Maybe we can add username splices to our API so only genuinely valid users are linked? */ text.match(/@(\w{1,15})/g)?.forEach(match => { const username = match.replace('@', ''); @@ -16,10 +16,10 @@ const populateUserLinks = (tweet: APIStatus, text: string): string => { return text; }; -const generateTweetMedia = (tweet: APIStatus): string => { +const generateStatusMedia = (status: APIStatus): string => { let media = ''; - if (tweet.media?.all?.length) { - tweet.media.all.forEach(mediaItem => { + if (status.media?.all?.length) { + status.media.all.forEach(mediaItem => { switch (mediaItem.type) { case 'photo': // eslint-disable-next-line no-case-declarations @@ -30,10 +30,10 @@ const generateTweetMedia = (tweet: APIStatus): string => { }); break; case 'video': - media += `View original post` : notApplicableComment, + socialText: getSocialTextIV(status as APITwitterStatus) || '', + viewOriginal: !isQuote ? `View original post` : notApplicableComment, aboutSection: isQuote ? '' : `

About author

@@ -144,7 +144,7 @@ const generateTweetFooter = (tweet: APIStatus, isQuote = false): string => {

{following} Following  {followers} Followers  - {tweets} Posts + {statuses} Posts

`.format({ pfp: `${
             author.name
@@ -156,44 +156,44 @@ const generateTweetFooter = (tweet: APIStatus, isQuote = false): string => {
           joined: author.joined ? `📆 ${formatDate(new Date(author.joined))}` : '',
           following: truncateSocialCount(author.following),
           followers: truncateSocialCount(author.followers),
-          tweets: truncateSocialCount(author.statuses)
+          statuses: truncateSocialCount(author.statuses)
         })
   });
 };
 
-const generateTweet = (tweet: APIStatus, isQuote = false): string => {
-  let text = paragraphify(sanitizeText(tweet.text), isQuote);
+const generateStatus = (status: APIStatus, isQuote = false): string => {
+  let text = paragraphify(sanitizeText(status.text), isQuote);
   text = htmlifyLinks(text);
   text = htmlifyHashtags(text);
-  text = populateUserLinks(tweet, text);
+  text = populateUserLinks(status, text);
 
-  const translatedText = getTranslatedText(tweet as APITweet, isQuote);
+  const translatedText = getTranslatedText(status as APITwitterStatus, isQuote);
 
   return `<!-- Telegram Instant View -->
   {quoteHeader}
-  <!-- Embed Tweet media -->
-  ${generateTweetMedia(tweet)} 
+  <!-- Embed media -->
+  ${generateStatusMedia(status)} 
   <!-- Translated text (if applicable) -->
   ${translatedText ? translatedText : notApplicableComment}
-  <!-- Embed Tweet text -->
+  <!-- Embed Status text -->
   ${text}
-  <!-- Embedded quote tweet -->
-  ${!isQuote && tweet.quote ? generateTweet(tweet.quote, true) : notApplicableComment}
-  ${!isQuote ? generateTweetFooter(tweet) : ''}
-  <br>${!isQuote ? `<a href=View original post` : notApplicableComment} + + ${!isQuote && status.quote ? generateStatus(status.quote, true) : notApplicableComment} + ${!isQuote ? generateStatusFooter(status) : ''} +
${!isQuote ? `View original post` : notApplicableComment} `.format({ quoteHeader: isQuote - ? `

Quoting ${tweet.author.name} (@${tweet.author.screen_name})

` + ? `

Quoting ${status.author.name} (@${status.author.screen_name})

` : '' }); }; export const renderInstantView = (properties: RenderProperties): ResponseInstructions => { console.log('Generating Instant View...'); - const { tweet, flags } = properties; + const { status, flags } = properties; const instructions: ResponseInstructions = { addHeaders: [] }; /* Use ISO date for Medium template */ - const postDate = new Date(tweet.created_at).toISOString(); + const statusDate = new Date(status.created_at).toISOString(); /* Pretend to be Medium to allow Instant View to work. Thanks to https://nikstar.me/post/instant-view/ for the help! @@ -202,7 +202,7 @@ export const renderInstantView = (properties: RenderProperties): ResponseInstruc contact me https://t.me/dangeredwolf */ instructions.addHeaders = [ ``, - ``, + ``, flags?.archive ? `` : `` @@ -216,13 +216,13 @@ export const renderInstantView = (properties: RenderProperties): ResponseInstruc flags?.archive ? `${Constants.BRANDING_NAME} archive` : 'If you can see this, your browser is doing something weird with your user agent.' - } View original post + } View original post
- View original -

${tweet.author.name} (@${tweet.author.screen_name})

+ View original +

${status.author.name} (@${status.author.screen_name})

- ${generateTweet(tweet)} + ${generateStatus(status)}
`; return instructions; diff --git a/src/render/photo.ts b/src/render/photo.ts index bba2871..62852a2 100644 --- a/src/render/photo.ts +++ b/src/render/photo.ts @@ -5,15 +5,15 @@ export const renderPhoto = ( properties: RenderProperties, photo: APIPhoto | APIMosaicPhoto ): ResponseInstructions => { - const { tweet, engagementText, authorText, isOverrideMedia, userAgent } = properties; + const { status, engagementText, authorText, isOverrideMedia, userAgent } = properties; const instructions: ResponseInstructions = { addHeaders: [] }; - if ((tweet.media?.photos?.length || 0) > 1 && (!tweet.media?.mosaic || isOverrideMedia)) { + if ((status.media?.photos?.length || 0) > 1 && (!status.media?.mosaic || isOverrideMedia)) { photo = photo as APIPhoto; - const all = tweet.media?.all as APIMedia[]; + const all = status.media?.all as APIMedia[]; const baseString = - all.length === tweet.media?.photos?.length ? Strings.PHOTO_COUNT : Strings.MEDIA_COUNT; + all.length === status.media?.photos?.length ? Strings.PHOTO_COUNT : Strings.MEDIA_COUNT; const photoCounter = baseString.format({ number: String(all.indexOf(photo) + 1), diff --git a/src/render/video.ts b/src/render/video.ts index 994a8ec..88bfaec 100644 --- a/src/render/video.ts +++ b/src/render/video.ts @@ -6,10 +6,10 @@ export const renderVideo = ( properties: RenderProperties, video: APIVideo ): ResponseInstructions => { - const { tweet, userAgent, text } = properties; + const { status, userAgent, text } = properties; const instructions: ResponseInstructions = { addHeaders: [] }; - const all = tweet.media?.all as APIMedia[]; + const all = status.media?.all as APIMedia[]; /* This fix is specific to Discord not wanting to render videos that are too large, or rendering low quality videos too small. @@ -32,7 +32,7 @@ export const renderVideo = ( we'll put an indicator if there are more than one video */ if (all && all.length > 1 && (userAgent?.indexOf('Telegram') ?? 0) > -1) { const baseString = - all.length === tweet.media?.videos?.length ? Strings.VIDEO_COUNT : Strings.MEDIA_COUNT; + all.length === status.media?.videos?.length ? Strings.VIDEO_COUNT : Strings.MEDIA_COUNT; const videoCounter = baseString.format({ number: String(all.indexOf(video) + 1), total: String(all.length) @@ -41,10 +41,10 @@ export const renderVideo = ( instructions.siteName = `${Constants.BRANDING_NAME} - ${videoCounter}`; } - instructions.authorText = tweet.translation?.text || text || ''; + instructions.authorText = status.translation?.text || text || ''; - if (instructions.authorText.length < 40 && tweet.quote) { - instructions.authorText += `\n${handleQuote(tweet.quote)}`; + if (instructions.authorText.length < 40 && status.quote) { + instructions.authorText += `\n${handleQuote(status.quote)}`; } /* Push the raw video-related headers */ diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 48d360e..bfd048a 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -28,7 +28,7 @@ interface ResponseInstructions { } interface RenderProperties { - tweet: APITweet; + status: APITwitterStatus; siteText?: string; authorText?: string; engagementText?: string; @@ -41,13 +41,13 @@ interface RenderProperties { interface TweetAPIResponse { code: number; message: string; - tweet?: APITweet; + tweet?: APITwitterStatus; } -interface SocialPostAPIResponse { +interface StatusAPIResponse { code: number; message: string; - post?: APITweet; + status?: APITwitterStatus; } interface UserAPIResponse { @@ -146,7 +146,7 @@ interface APIStatus { embed_card: 'tweet' | 'summary' | 'summary_large_image' | 'player'; } -interface APITweet extends APIStatus { +interface APITwitterStatus extends APIStatus { views?: number | null; translation?: APITranslate; @@ -183,13 +183,13 @@ interface APIUser { } interface SocialPost { - status: APIStatus | APITweet | null; + status: APIStatus | APITwitterStatus | null; author: APIUser | null; } interface SocialThread { - status: APIStatus | APITweet | null; - thread: (APIStatus | APITweet)[] | null; + status: APIStatus | APITwitterStatus | null; + thread: (APIStatus | APITwitterStatus)[] | null; author: APIUser | null; code: number; } diff --git a/src/types/vendor/twitter.d.ts b/src/types/vendor/twitter.d.ts index e8e693d..b2bcd97 100644 --- a/src/types/vendor/twitter.d.ts +++ b/src/types/vendor/twitter.d.ts @@ -308,7 +308,7 @@ type GraphQLUser = { }; }; -type GraphQLTweetLegacy = { +type GraphQLTwitterStatusLegacy = { id_str: string; // "1674824189176590336" created_at: string; // "Tue Sep 14 20:00:00 +0000 2021" conversation_id_str: string; // "1674824189176590336" @@ -351,9 +351,9 @@ type GraphQLTweetLegacy = { }; }; -type GraphQLTweet = { +type GraphQLTwitterStatus = { // Workaround - result: GraphQLTweet; + result: GraphQLTwitterStatus; __typename: 'Tweet' | 'TweetWithVisibilityResults' | 'TweetUnavailable'; reason: string; // used for errors rest_id: string; // "1674824189176590336", @@ -364,7 +364,7 @@ type GraphQLTweet = { }; }; tweet?: { - legacy: GraphQLTweetLegacy; + legacy: GraphQLTwitterStatusLegacy; views: { count: string; // "562" state: string; // "EnabledWithCount" @@ -383,8 +383,8 @@ type GraphQLTweet = { state: string; // "EnabledWithCount" }; source: string; // "Twitter Web App" - quoted_status_result?: GraphQLTweet; - legacy: GraphQLTweetLegacy; + quoted_status_result?: GraphQLTwitterStatus; + legacy: GraphQLTwitterStatusLegacy; note_tweet: { is_expandable: boolean; note_tweet_results: { @@ -450,7 +450,7 @@ type GraphQLTimelineTweet = { item: 'TimelineTweet'; __typename: 'TimelineTweet'; tweet_results: { - result: GraphQLTweet | TweetTombstone; + result: GraphQLTwitterStatus | TweetTombstone; }; }; @@ -521,7 +521,7 @@ type TimelineTerminateTimelineInstruction = { type: 'TimelineTerminateTimeline'; direction: 'Top'; }; -type GraphQLTweetNotFoundResponse = { +type GraphQLTwitterStatusNotFoundResponse = { errors: [ { message: string; // "_Missing: No status found with that ID" @@ -560,7 +560,7 @@ type TweetResultsByRestIdResult = { errors?: unknown[]; data?: { tweetResult?: { - result?: TweetStub | GraphQLTweet; + result?: TweetStub | GraphQLTwitterStatus; }; }; }; @@ -571,6 +571,6 @@ type TweetStub = { }; interface GraphQLProcessBucket { - tweets: GraphQLTweet[]; + statuses: GraphQLTwitterStatus[]; cursors: GraphQLTimelineCursor[]; } diff --git a/test/worker.test.ts b/test/worker.test.ts index a726c7b..df527c2 100644 --- a/test/worker.test.ts +++ b/test/worker.test.ts @@ -47,7 +47,7 @@ test('Home page redirect', async () => { expect(resultHuman.headers.get('location')).toEqual(githubUrl); }); -test('Tweet redirect human', async () => { +test('Status redirect human', async () => { const result = await app.request( new Request('https://fxtwitter.com/jack/status/20', { method: 'GET', @@ -58,7 +58,7 @@ test('Tweet redirect human', async () => { expect(result.headers.get('location')).toEqual('https://twitter.com/jack/status/20'); }); -test('Tweet redirect human trailing slash', async () => { +test('Status redirect human trailing slash', async () => { const result = await app.request( new Request('https://fxtwitter.com/jack/status/20/', { method: 'GET', @@ -69,7 +69,7 @@ test('Tweet redirect human trailing slash', async () => { expect(result.headers.get('location')).toEqual('https://twitter.com/jack/status/20'); }); -test('Tweet redirect human custom base redirect', async () => { +test('Status redirect human custom base redirect', async () => { const result = await app.request( new Request('https://fxtwitter.com/jack/status/20', { method: 'GET', @@ -97,7 +97,7 @@ test('Twitter moment redirect', async () => { expect(result.headers.get('location')).toEqual(`${twitterBaseUrl}/i/events/1572638642127966214`); }); -test('Tweet response robot', async () => { +test('Status response robot', async () => { const result = await app.request( new Request('https://fxtwitter.com/jack/status/20', { method: 'GET', @@ -107,7 +107,7 @@ test('Tweet response robot', async () => { expect(result.status).toEqual(200); }); -test('Tweet response robot (trailing slash/query string and extra characters)', async () => { +test('Status response robot (trailing slash/query string and extra characters)', async () => { const result = await app.request( new Request('https://fxtwitter.com/jack/status/20||/?asdf=ghjk&klop;', { method: 'GET', @@ -117,7 +117,7 @@ test('Tweet response robot (trailing slash/query string and extra characters)', expect(result.status).toEqual(200); }); -test('API fetch basic Tweet', async () => { +test('API fetch basic Status', async () => { const result = await app.request( new Request('https://api.fxtwitter.com/status/20', { method: 'GET', @@ -130,29 +130,29 @@ test('API fetch basic Tweet', async () => { expect(response.code).toEqual(200); expect(response.message).toEqual('OK'); - const tweet = response.tweet as APITweet; - expect(tweet).toBeTruthy(); - expect(tweet.url).toEqual(`${twitterBaseUrl}/jack/status/20`); - expect(tweet.id).toEqual('20'); - expect(tweet.text).toEqual('just setting up my twttr'); - expect(tweet.author.screen_name?.toLowerCase()).toEqual('jack'); - expect(tweet.author.id).toEqual('12'); - expect(tweet.author.name).toBeTruthy(); - expect(tweet.author.avatar_url).toBeTruthy(); - expect(tweet.author.banner_url).toBeTruthy(); - expect(tweet.replies).toBeGreaterThan(0); + const status = response.tweet as APITwitterStatus; + expect(status).toBeTruthy(); + expect(status.url).toEqual(`${twitterBaseUrl}/jack/status/20`); + expect(status.id).toEqual('20'); + expect(status.text).toEqual('just setting up my twttr'); + expect(status.author.screen_name?.toLowerCase()).toEqual('jack'); + expect(status.author.id).toEqual('12'); + expect(status.author.name).toBeTruthy(); + expect(status.author.avatar_url).toBeTruthy(); + expect(status.author.banner_url).toBeTruthy(); + expect(status.replies).toBeGreaterThan(0); // @ts-expect-error retweets only in legacy API - expect(tweet.retweets).toBeGreaterThan(0); - expect(tweet.likes).toBeGreaterThan(0); + expect(status.retweets).toBeGreaterThan(0); + expect(status.likes).toBeGreaterThan(0); // @ts-expect-error twitter_card only in legacy API - expect(tweet.twitter_card).toEqual('tweet'); - expect(tweet.created_at).toEqual('Tue Mar 21 20:50:14 +0000 2006'); - expect(tweet.created_timestamp).toEqual(1142974214); - expect(tweet.lang).toEqual('en'); - expect(tweet.replying_to).toBeNull(); + expect(status.twitter_card).toEqual('tweet'); + expect(status.created_at).toEqual('Tue Mar 21 20:50:14 +0000 2006'); + expect(status.created_timestamp).toEqual(1142974214); + expect(status.lang).toEqual('en'); + expect(status.replying_to).toBeNull(); }); -// test('API fetch video Tweet', async () => { +// test('API fetch video Status', async () => { // const result = await app.request( // new Request('https://api.fxtwitter.com/X/status/854416760933556224', { // method: 'GET', @@ -165,27 +165,27 @@ test('API fetch basic Tweet', async () => { // expect(response.code).toEqual(200); // expect(response.message).toEqual('OK'); -// const tweet = response.tweet as APITweet; -// expect(tweet).toBeTruthy(); -// expect(tweet.url).toEqual(`${twitterBaseUrl}/X/status/854416760933556224`); -// expect(tweet.id).toEqual('854416760933556224'); -// expect(tweet.text).toEqual( +// const status = response.tweet as APITwitterStatus; +// expect(status).toBeTruthy(); +// expect(status.url).toEqual(`${twitterBaseUrl}/X/status/854416760933556224`); +// expect(status.id).toEqual('854416760933556224'); +// expect(status.text).toEqual( // 'Get the sauces ready, #NuggsForCarter has 3 million+ Retweets.' // ); -// expect(tweet.author.screen_name?.toLowerCase()).toEqual('x'); -// expect(tweet.author.id).toEqual('783214'); -// expect(tweet.author.name).toBeTruthy(); -// expect(tweet.author.avatar_url).toBeTruthy(); -// expect(tweet.author.banner_url).toBeTruthy(); -// expect(tweet.replies).toBeGreaterThan(0); -// expect(tweet.retweets).toBeGreaterThan(0); -// expect(tweet.likes).toBeGreaterThan(0); -// expect(tweet.twitter_card).toEqual('player'); -// expect(tweet.created_at).toEqual('Tue Apr 18 19:30:04 +0000 2017'); -// expect(tweet.created_timestamp).toEqual(1492543804); -// expect(tweet.lang).toEqual('en'); -// expect(tweet.replying_to).toBeNull(); -// const video = tweet.media?.videos?.[0] as APIVideo; +// expect(status.author.screen_name?.toLowerCase()).toEqual('x'); +// expect(status.author.id).toEqual('783214'); +// expect(status.author.name).toBeTruthy(); +// expect(status.author.avatar_url).toBeTruthy(); +// expect(status.author.banner_url).toBeTruthy(); +// expect(status.replies).toBeGreaterThan(0); +// expect(status.retweets).toBeGreaterThan(0); +// expect(status.likes).toBeGreaterThan(0); +// expect(status.twitter_card).toEqual('player'); +// expect(status.created_at).toEqual('Tue Apr 18 19:30:04 +0000 2017'); +// expect(status.created_timestamp).toEqual(1492543804); +// expect(status.lang).toEqual('en'); +// expect(status.replying_to).toBeNull(); +// const video = status.media?.videos?.[0] as APIVideo; // expect(video.url).toEqual( // 'https://video.twimg.com/amplify_video/854415175776059393/vid/720x720/dNEi0crU-jA4mTtr.mp4' // ); @@ -197,7 +197,7 @@ test('API fetch basic Tweet', async () => { // expect(video.type).toEqual('video'); // }); -// test('API fetch multi-photo Tweet', async () => { +// test('API fetch multi-photo status', async () => { // const result = await app.request( // new Request('https://api.fxtwitter.com/Twitter/status/1445094085593866246', { // method: 'GET', @@ -210,22 +210,22 @@ test('API fetch basic Tweet', async () => { // expect(response.code).toEqual(200); // expect(response.message).toEqual('OK'); -// const tweet = response.tweet as APITweet; -// expect(tweet).toBeTruthy(); -// expect(tweet.url).toEqual(`${twitterBaseUrl}/X/status/1445094085593866246`); -// expect(tweet.id).toEqual('1445094085593866246'); -// expect(tweet.text).toEqual('@netflix'); -// expect(tweet.author.screen_name?.toLowerCase()).toEqual('x'); -// expect(tweet.author.id).toEqual('783214'); -// expect(tweet.author.name).toBeTruthy(); -// expect(tweet.author.avatar_url).toBeTruthy(); -// expect(tweet.author.banner_url).toBeTruthy(); -// expect(tweet.twitter_card).toEqual('summary_large_image'); -// expect(tweet.created_at).toEqual('Mon Oct 04 18:30:53 +0000 2021'); -// expect(tweet.created_timestamp).toEqual(1633372253); -// expect(tweet.replying_to?.toLowerCase()).toEqual('netflix'); -// expect(tweet.media?.photos).toBeTruthy(); -// const photos = tweet.media?.photos as APIPhoto[]; +// const status = response.tweet as APITwitterStatus; +// expect(status).toBeTruthy(); +// expect(status.url).toEqual(`${twitterBaseUrl}/X/status/1445094085593866246`); +// expect(status.id).toEqual('1445094085593866246'); +// expect(status.text).toEqual('@netflix'); +// expect(status.author.screen_name?.toLowerCase()).toEqual('x'); +// expect(status.author.id).toEqual('783214'); +// expect(status.author.name).toBeTruthy(); +// expect(status.author.avatar_url).toBeTruthy(); +// expect(status.author.banner_url).toBeTruthy(); +// expect(status.twitter_card).toEqual('summary_large_image'); +// expect(status.created_at).toEqual('Mon Oct 04 18:30:53 +0000 2021'); +// expect(status.created_timestamp).toEqual(1633372253); +// expect(status.replying_to?.toLowerCase()).toEqual('netflix'); +// expect(status.media?.photos).toBeTruthy(); +// const photos = status.media?.photos as APIPhoto[]; // expect(photos[0].url).toEqual('https://pbs.twimg.com/media/FA4BaFaXoBUV3di.jpg'); // expect(photos[0].width).toEqual(950); // expect(photos[0].height).toEqual(620); @@ -234,8 +234,8 @@ test('API fetch basic Tweet', async () => { // expect(photos[1].width).toEqual(1386); // expect(photos[1].height).toEqual(706); // expect(photos[1].altText).toBeTruthy(); -// expect(tweet.media?.mosaic).toBeTruthy(); -// const mosaic = tweet.media?.mosaic as APIMosaicPhoto; +// expect(status.media?.mosaic).toBeTruthy(); +// const mosaic = status.media?.mosaic as APIMosaicPhoto; // expect(mosaic.formats?.jpeg).toEqual( // 'https://mosaic.fxtwitter.com/jpeg/1445094085593866246/FA4BaFaXoBUV3di/FA4BaUyXEAcAHvK' // ); @@ -244,7 +244,7 @@ test('API fetch basic Tweet', async () => { // ); // }); -// test('API fetch poll Tweet', async () => { +// test('API fetch poll status', async () => { // const result = await app.request( // new Request('https://api.fxtwitter.com/status/1055475950543167488', { // method: 'GET', @@ -257,23 +257,23 @@ test('API fetch basic Tweet', async () => { // expect(response.code).toEqual(200); // expect(response.message).toEqual('OK'); -// const tweet = response.tweet as APITweet; -// expect(tweet).toBeTruthy(); -// expect(tweet.url).toEqual(`${twitterBaseUrl}/X/status/1055475950543167488`); -// expect(tweet.id).toEqual('1055475950543167488'); -// expect(tweet.text).toEqual('A poll:'); -// expect(tweet.author.screen_name?.toLowerCase()).toEqual('x'); -// expect(tweet.author.id).toEqual('783214'); -// expect(tweet.author.name).toBeTruthy(); -// expect(tweet.author.avatar_url).toBeTruthy(); -// expect(tweet.author.banner_url).toBeTruthy(); -// expect(tweet.twitter_card).toEqual('tweet'); -// expect(tweet.created_at).toEqual('Thu Oct 25 15:07:31 +0000 2018'); -// expect(tweet.created_timestamp).toEqual(1540480051); -// expect(tweet.lang).toEqual('en'); -// expect(tweet.replying_to).toBeNull(); -// expect(tweet.poll).toBeTruthy(); -// const poll = tweet.poll as APIPoll; +// const status = response.tweet as APITwitterStatus; +// expect(status).toBeTruthy(); +// expect(status.url).toEqual(`${twitterBaseUrl}/X/status/1055475950543167488`); +// expect(status.id).toEqual('1055475950543167488'); +// expect(status.text).toEqual('A poll:'); +// expect(status.author.screen_name?.toLowerCase()).toEqual('x'); +// expect(status.author.id).toEqual('783214'); +// expect(status.author.name).toBeTruthy(); +// expect(status.author.avatar_url).toBeTruthy(); +// expect(status.author.banner_url).toBeTruthy(); +// expect(status.twitter_card).toEqual('tweet'); +// expect(status.created_at).toEqual('Thu Oct 25 15:07:31 +0000 2018'); +// expect(status.created_timestamp).toEqual(1540480051); +// expect(status.lang).toEqual('en'); +// expect(status.replying_to).toBeNull(); +// expect(status.poll).toBeTruthy(); +// const poll = status.poll as APIPoll; // expect(poll.ends_at).toEqual('2018-10-26T03:07:30Z'); // expect(poll.time_left_en).toEqual('Final results'); // expect(poll.total_votes).toEqual(54703);