From 0e20ea9db462f3899dddbe72b074950069f3327b Mon Sep 17 00:00:00 2001 From: dangered wolf Date: Mon, 25 Jul 2022 15:11:13 -0400 Subject: [PATCH] Polls and external media in API --- src/api.ts | 102 +++++++++++++++++++++++++++++++++++-------------- src/card.ts | 68 ++++++++++++++------------------- src/server.ts | 1 + src/status.ts | 4 +- src/types.d.ts | 35 ++++++++++++++--- 5 files changed, 133 insertions(+), 77 deletions(-) diff --git a/src/api.ts b/src/api.ts index 44218d6..6bb8da3 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,66 @@ +import { renderCard } from './card'; import { fetchUsingGuest } from './fetch'; +import { linkFixer } from './linkFixer'; +import { colorFromPalette } from './palette'; import { translateTweet } from './translate'; -export const statueAPI = async ( +const populateTweetProperties = async ( + tweet: TweetPartial, + conversation: TimelineBlobPartial, + language: string = 'en' +): Promise => { + let apiTweet = {} as APITweet; + + const user = tweet.user; + const screenName = user?.screen_name || ''; + const name = user?.name || ''; + + apiTweet.text = linkFixer(tweet, tweet.full_text); + apiTweet.author = { + name: name, + screen_name: screenName, + avatar_url: user?.profile_image_url_https.replace('_normal', '_200x200') || '', + avatar_color: (apiTweet.palette = colorFromPalette( + tweet.user?.profile_image_extensions_media_color?.palette || [] + )), + banner_url: user?.profile_banner_url || '' + }; + apiTweet.replies = tweet.reply_count; + apiTweet.retweets = tweet.retweet_count; + apiTweet.likes = tweet.favorite_count; + apiTweet.palette = colorFromPalette( + tweet.user?.profile_image_extensions_media_color?.palette || [] + ); + + if (tweet.card) { + let card = await renderCard(tweet.card); + if (card.external_media) { + apiTweet.media = apiTweet.media || {}; + apiTweet.media.external = card.external_media; + } + if (card.poll) { + apiTweet.poll = card.poll; + } + } + + /* If a language is specified, let's try translating it! */ + if (typeof language === 'string' && language.length === 2 && language !== tweet.lang) { + let translateAPI = await translateTweet( + tweet, + conversation.guestToken || '', + language + ); + apiTweet.translation = { + text: translateAPI?.translation || '', + source_lang: translateAPI?.sourceLanguage || '', + target_lang: translateAPI?.destinationLanguage || '' + }; + } + + return apiTweet; +}; + +export const statusAPI = async ( event: FetchEvent, status: string, language: string @@ -37,35 +96,20 @@ export const statueAPI = async ( } let response: APIResponse = { code: 200, message: 'OK' } as APIResponse; - let apiTweet: APITweet = {} as APITweet; + let apiTweet: APITweet = (await populateTweetProperties( + tweet, + conversation, + language + )) as APITweet; - const user = tweet.user; - const screenName = user?.screen_name || ''; - const name = user?.name || ''; - - apiTweet.text = tweet.full_text; - apiTweet.author = { - name: name, - screen_name: screenName, - avatar_url: user?.profile_image_url_https || '', - banner_url: user?.profile_banner_url || '' - }; - apiTweet.replies = tweet.reply_count; - apiTweet.retweets = tweet.retweet_count; - apiTweet.likes = tweet.favorite_count; - - /* If a language is specified, let's try translating it! */ - if (typeof language === 'string' && language.length === 2 && language !== tweet.lang) { - let translateAPI = await translateTweet( - tweet, - conversation.guestToken || '', - language || 'en' - ); - apiTweet.translation = { - translated_text: translateAPI?.translation || '', - source_language: tweet.lang, - target_language: language - }; + let quoteTweet = + conversation.globalObjects?.tweets?.[tweet.quoted_status_id_str || '0'] || null; + if (quoteTweet) { + apiTweet.quote = (await populateTweetProperties( + quoteTweet, + conversation, + language + )) as APITweet; } response.tweet = apiTweet; diff --git a/src/card.ts b/src/card.ts index d5ffcda..f23994d 100644 --- a/src/card.ts +++ b/src/card.ts @@ -44,20 +44,14 @@ export const calculateTimeLeftString = (date: Date) => { }; export const renderCard = async ( - card: TweetCard, - headers: string[], - userAgent: string = '' -): Promise => { + card: TweetCard +): Promise<{ poll?: APIPoll; external_media?: APIExternalMedia }> => { let str = '\n\n'; const values = card.binding_values; console.log('rendering card on ', card); // Telegram's bars need to be a lot smaller to fit its bubbles - if (userAgent.indexOf('Telegram') > -1) { - barLength = 24; - } - let choices: { [label: string]: number } = {}; let totalVotes = 0; let timeLeft = ''; @@ -68,6 +62,9 @@ export const renderCard = async ( typeof values.choice1_count !== 'undefined' && typeof values.choice2_count !== 'undefined' ) { + let poll = {} as APIPoll; + poll.ends_at = values.end_datetime_utc?.string_value || ''; + if (typeof values.end_datetime_utc !== 'undefined') { const date = new Date(values.end_datetime_utc.string_value); timeLeft = calculateTimeLeftString(date); @@ -89,44 +86,35 @@ export const renderCard = async ( if (typeof values.choice4_count !== 'undefined') { choices[values.choice4_label?.string_value || ''] = parseInt( values.choice4_count.string_value - ); + ) || 0; totalVotes += parseInt(values.choice4_count.string_value); } - for (const [label, votes] of Object.entries(choices)) { - // render bar - const bar = 'โ–ˆ'.repeat(Math.round((votes / totalVotes || 0) * barLength)); - str += `${bar} -${label}โ€€โ€€(${Math.round((votes / totalVotes || 0) * 100)}%) -`; - } + poll.total_votes = totalVotes; + poll.choices = Object.keys(choices).map(label => { + return { + label: label, + count: choices[label], + percentage: ((Math.round((choices[label] / totalVotes) * 1000) || 0) / 10 || 0) + }; + }); - str += `\n${totalVotes} votes ยท ${timeLeft}`; + return { poll: poll }; /* Oh good, a non-Twitter video URL! This enables YouTube embeds and stuff to just work */ } else if (typeof values.player_url !== 'undefined') { - headers.push( - ``, - ``, - ``, - ``, - ``, - ``, - ``, - `` - ); - - /* A control sequence I made up to tell status.ts that external media is being embedded */ - str = 'EMBED_CARD'; + return { + external_media: { + type: 'video', + url: values.player_url.string_value, + width: parseInt( + (values.player_width?.string_value || '1280').replace('px', '') + ), // TODO: Replacing px might not be necessary, it's just there as a precaution + height: parseInt( + (values.player_height?.string_value || '720').replace('px', '') + ) + } + }; } } - - return str; + return {}; }; diff --git a/src/server.ts b/src/server.ts index 3fb1279..97bf775 100644 --- a/src/server.ts +++ b/src/server.ts @@ -99,6 +99,7 @@ router.get('/:prefix?/:handle/statuses/:id/video/:mediaNumber', statusRequest); router.get('/:prefix?/:handle/status/:id/:language', statusRequest); router.get('/:prefix?/:handle/statuses/:id/:language', statusRequest); router.get('/status/:id', statusRequest); +router.get('/status/:id/:language', statusRequest); router.get('/owoembed', async (request: Request) => { console.log('oembed hit!'); diff --git a/src/status.ts b/src/status.ts index f8b8aa6..baa90b3 100644 --- a/src/status.ts +++ b/src/status.ts @@ -9,7 +9,7 @@ import { Strings } from './strings'; import { handleMosaic } from './mosaic'; import { translateTweet } from './translate'; import { getAuthorText } from './author'; -import { statueAPI } from './api'; +import { statusAPI } from './api'; export const returnError = (error: string): StatusResponse => { return { @@ -33,7 +33,7 @@ export const handleStatus = async ( ): Promise => { console.log('Direct?', flags?.direct); - let api = await statueAPI(event, status, language || 'en'); + let api = await statusAPI(event, status, language || 'en'); if (flags?.api || true) { return { diff --git a/src/types.d.ts b/src/types.d.ts index 6436cc8..156f3c1 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -24,19 +24,37 @@ interface APIResponse { } interface APITranslate { - translated_text: string; - source_language: string; - target_language: string; + text: string; + source_lang: string; + target_lang: string; } interface APIAuthor { name?: string; screen_name?: string; avatar_url?: string; + avatar_color: string; banner_url?: string; } -interface APIPoll {} +interface APIExternalMedia { + type: 'video'; + url: string; + height: number; + width: number; +} + +interface APIPollChoice { + label: string; + count: number; + percentage: number; +} + +interface APIPoll { + choices: APIPollChoice[]; + total_votes: number; + ends_at: string; +} interface APITweet { id: string; @@ -48,9 +66,14 @@ interface APITweet { retweets: number; replies: number; - quote_tweet?: APITweet; + palette: string; + + quote?: APITweet; + poll?: APIPoll; translation?: APITranslate; author: APIAuthor; - thumbnail: string; + media: { + external?: APIExternalMedia; + }; }