From 65e0b775af29544802468ad629814a1107348641 Mon Sep 17 00:00:00 2001 From: dangered wolf Date: Mon, 25 Jul 2022 16:32:50 -0400 Subject: [PATCH] Genuinely functional API --- src/api.ts | 71 +++++++++++++++++++++++++++++++++++++++++++---- src/mosaic.ts | 39 ++++++++++++-------------- src/server.ts | 17 +++++++----- src/status.ts | 65 +------------------------------------------ src/tweetTypes.ts | 2 +- src/types.d.ts | 34 ++++++++++++++++++++++- 6 files changed, 128 insertions(+), 100 deletions(-) diff --git a/src/api.ts b/src/api.ts index 6bb8da3..e4dca30 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,9 +1,36 @@ import { renderCard } from './card'; +import { Constants } from './constants'; import { fetchUsingGuest } from './fetch'; import { linkFixer } from './linkFixer'; +import { handleMosaic } from './mosaic'; import { colorFromPalette } from './palette'; import { translateTweet } from './translate'; +const processMedia = (media: TweetMedia): APIPhoto | APIVideo | null => { + if (media.type === 'photo') { + return { + type: 'photo', + url: media.media_url_https, + width: media.original_info.width, + height: media.original_info.height + } + } else if (media.type === 'video' || media.type === 'animated_gif') { + // Find the variant with the highest bitrate + let bestVariant = media.video_info?.variants?.reduce?.((a, b) => + (a.bitrate ?? 0) > (b.bitrate ?? 0) ? a : b + ); + return { + url: bestVariant?.url || '', + thumbnail_url: media.media_url_https, + width: media.original_info.width, + height: media.original_info.height, + format: bestVariant?.content_type || '', + type: media.type === 'animated_gif' ? 'gif' : 'video' + } + } + return null; +}; + const populateTweetProperties = async ( tweet: TweetPartial, conversation: TimelineBlobPartial, @@ -11,30 +38,62 @@ const populateTweetProperties = async ( ): Promise => { let apiTweet = {} as APITweet; - const user = tweet.user; + const user = tweet.user as UserPartial; const screenName = user?.screen_name || ''; const name = user?.name || ''; + apiTweet.url = `${Constants.TWITTER_ROOT}/${screenName}/status/${tweet.id_str}`; 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 || [] - )), + avatar_color: 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 || [] + apiTweet.color = apiTweet.author.avatar_color; + apiTweet.twitter_card = 'tweet'; + + let mediaList = Array.from( + tweet.extended_entities?.media || tweet.entities?.media || [] ); + mediaList.forEach(media => { + let mediaObject = processMedia(media); + console.log('mediaObject', JSON.stringify(mediaObject)) + if (mediaObject) { + apiTweet.twitter_card = 'summary_large_image'; + if (mediaObject.type === 'photo') { + apiTweet.media = apiTweet.media || {}; + apiTweet.media.photos = apiTweet.media.photos || []; + apiTweet.media.photos.push(mediaObject); + + console.log('media',apiTweet.media); + } else if (mediaObject.type === 'video' || mediaObject.type === 'gif') { + apiTweet.media = apiTweet.media || {}; + apiTweet.media.video = mediaObject as APIVideo; + } + } + }) + + if (mediaList[0]?.ext_media_color?.palette) { + apiTweet.color = colorFromPalette(mediaList[0].ext_media_color.palette); + } + + if (apiTweet.media?.photos?.length || 0 > 1) { + let mosaic = await handleMosaic(apiTweet.media.photos || []); + if (mosaic !== null) { + apiTweet.media.mosaic = mosaic; + } + } + if (tweet.card) { let card = await renderCard(tweet.card); if (card.external_media) { + apiTweet.twitter_card = 'summary_large_image'; apiTweet.media = apiTweet.media || {}; apiTweet.media.external = card.external_media; } diff --git a/src/mosaic.ts b/src/mosaic.ts index 5a32b58..7f7799b 100644 --- a/src/mosaic.ts +++ b/src/mosaic.ts @@ -1,9 +1,8 @@ import { Constants } from './constants'; export const handleMosaic = async ( - mediaList: TweetMedia[], - userAgent: string -): Promise => { + mediaList: APIPhoto[] +): Promise => { let mosaicDomains = Constants.MOSAIC_DOMAIN_LIST; let selectedDomain: string | null = null; while (selectedDomain === null && mosaicDomains.length > 0) { @@ -20,41 +19,39 @@ export const handleMosaic = async ( // Fallback if all Mosaic servers are down if (selectedDomain === null) { - return mediaList[0]; + return null; } else { // console.log('mediaList', mediaList); let mosaicMedia = mediaList.map( media => - media.media_url_https?.match(/(?<=\/media\/)[a-zA-Z0-9_\-]+(?=[\.\?])/g)?.[0] || + media.url?.match(/(?<=\/media\/)[a-zA-Z0-9_\-]+(?=[\.\?])/g)?.[0] || '' ); // console.log('mosaicMedia', mosaicMedia); // TODO: use a better system for this, 0 gets png 1 gets webp, usually - let constructUrl = `https://${selectedDomain}/${ - userAgent.indexOf('Telegram') > -1 ? '0' : '1' - }`; + let baseUrl = `https://${selectedDomain}/`; + let path = ''; + if (mosaicMedia[0]) { - constructUrl += `/${mosaicMedia[0]}`; + path += `/${mosaicMedia[0]}`; } if (mosaicMedia[1]) { - constructUrl += `/${mosaicMedia[1]}`; + path += `/${mosaicMedia[1]}`; } if (mosaicMedia[2]) { - constructUrl += `/${mosaicMedia[2]}`; + path += `/${mosaicMedia[2]}`; } if (mosaicMedia[3]) { - constructUrl += `/${mosaicMedia[3]}`; + path += `/${mosaicMedia[3]}`; } - console.log(`Mosaic URL: ${constructUrl}`); - return { - media_url_https: constructUrl, - original_info: { - height: mediaList.reduce((acc, media) => acc + media.original_info?.height, 0), - width: mediaList.reduce((acc, media) => acc + media.original_info?.width, 0) - }, - type: 'photo' - } as TweetMedia; + height: mediaList.reduce((acc, media) => acc + media.height, 0), + width: mediaList.reduce((acc, media) => acc + media.width, 0), + formats: { + jpeg: `${baseUrl}jpeg${path}`, + webp: `${baseUrl}webp${path}`, + } + } as APIMosaicPhoto; } }; diff --git a/src/server.ts b/src/server.ts index 97bf775..60c197b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -168,15 +168,18 @@ const cacheWrapper = async (event: FetchEvent): Promise => { switch (request.method) { case 'GET': - let cachedResponse = await cache.match(cacheKey); + if (cacheUrl.hostname !== Constants.API_HOST) { - if (cachedResponse) { - console.log('Cache hit'); - return cachedResponse; + let cachedResponse = await cache.match(cacheKey); + + if (cachedResponse) { + console.log('Cache hit'); + return cachedResponse; + } + + console.log('Cache miss'); } - - console.log('Cache miss'); - + let response = await router.handle(event.request, event); // Store the fetched response as cacheKey diff --git a/src/status.ts b/src/status.ts index baa90b3..14ff0e4 100644 --- a/src/status.ts +++ b/src/status.ts @@ -162,70 +162,7 @@ export const handleStatus = async ( headers.push(``); /* Inline helper function for handling media */ - const processMedia = (media: TweetMedia) => { - if (media.type === 'photo') { - if (flags?.direct && typeof media.media_url_https === 'string') { - redirectMedia = media.media_url_https; - return; - } - - headers.push( - ``, - `` - ); - - if (media.original_info?.width && media.original_info?.height) { - headers.push( - ``, - ``, - ``, - `` - ); - } - - if (!pushedCardType) { - headers.push(``); - pushedCardType = true; - } - } else if (media.type === 'video' || media.type === 'animated_gif') { - // Find the variant with the highest bitrate - let bestVariant = media.video_info?.variants?.reduce?.((a, b) => - (a.bitrate ?? 0) > (b.bitrate ?? 0) ? a : b - ); - - if (flags?.direct && bestVariant?.url) { - console.log(`Redirecting to ${bestVariant.url}`); - redirectMedia = bestVariant.url; - return; - } - - /* This is for the video thumbnail */ - headers.push(``); - - /* On Discord we have to use the author field in order to get the tweet text - to display on videos. This length is limited, however, and if there is too - much text Discord will refuse to display it at all, so we trim down as much - as the client will display. */ - if (userAgent && userAgent?.indexOf?.('Discord') > -1) { - text = text.substr(0, 179); - } - - authorText = encodeURIComponent(text); - - headers.push( - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - `` - ); - } - }; + let actualMediaNumber = 0; let renderedMosaic = false; diff --git a/src/tweetTypes.ts b/src/tweetTypes.ts index cf9012b..673cf25 100644 --- a/src/tweetTypes.ts +++ b/src/tweetTypes.ts @@ -85,7 +85,7 @@ type TweetMedia = { medium: TweetMediaSize; small: TweetMediaSize; }; - type: 'photo' | 'video'; + type: 'photo' | 'video' | 'animated_gif'; url: string; video_info?: { aspect_ratio: [number, number]; diff --git a/src/types.d.ts b/src/types.d.ts index 156f3c1..415dcd3 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -56,8 +56,35 @@ interface APIPoll { ends_at: string; } +interface APIPhoto { + type: 'photo'; + url: string; + width: number; + height: number; +} + +interface APIMosaicPhoto { + type: 'mosaic_photo'; + width: number; + height: number; + formats: { + webp: string; + jpeg: string; + } +} + +interface APIVideo { + type: 'video' | 'gif'; + url: string; + thumbnail_url: string; + width: number; + height: number; + format: string; +} + interface APITweet { id: string; + url: string; tweet: string; text?: string; created_at: string; @@ -66,7 +93,7 @@ interface APITweet { retweets: number; replies: number; - palette: string; + color: string; quote?: APITweet; poll?: APIPoll; @@ -75,5 +102,10 @@ interface APITweet { media: { external?: APIExternalMedia; + photos?: APIPhoto[]; + video?: APIVideo; + mosaic?: APIMosaicPhoto; }; + + twitter_card: 'tweet' | 'summary' | 'summary_large_image' | 'player'; }