Genuinely functional API

This commit is contained in:
dangered wolf 2022-07-25 16:32:50 -04:00
parent 0e20ea9db4
commit 65e0b775af
No known key found for this signature in database
GPG key ID: 41E4D37680ED8B58
6 changed files with 128 additions and 100 deletions

View file

@ -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<APITweet> => {
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;
}

View file

@ -1,9 +1,8 @@
import { Constants } from './constants';
export const handleMosaic = async (
mediaList: TweetMedia[],
userAgent: string
): Promise<TweetMedia> => {
mediaList: APIPhoto[]
): Promise<APIMosaicPhoto | null> => {
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;
}
};

View file

@ -168,15 +168,18 @@ const cacheWrapper = async (event: FetchEvent): Promise<Response> => {
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

View file

@ -162,70 +162,7 @@ export const handleStatus = async (
headers.push(`<meta content="${colorOverride}" property="theme-color"/>`);
/* 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(
`<meta name="twitter:image" content="${media.media_url_https}"/>`,
`<meta property="og:image" content="${media.media_url_https}"/>`
);
if (media.original_info?.width && media.original_info?.height) {
headers.push(
`<meta name="twitter:image:width" content="${media.original_info.width}"/>`,
`<meta name="twitter:image:height" content="${media.original_info.height}"/>`,
`<meta name="og:image:width" content="${media.original_info.width}"/>`,
`<meta name="og:image:height" content="${media.original_info.height}"/>`
);
}
if (!pushedCardType) {
headers.push(`<meta name="twitter:card" content="summary_large_image"/>`);
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(`<meta name="twitter:image" content="${media.media_url_https}"/>`);
/* 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(
`<meta name="twitter:card" content="player"/>`,
`<meta name="twitter:player:stream" content="${bestVariant?.url}"/>`,
`<meta name="twitter:player:stream:content_type" content="${bestVariant?.content_type}"/>`,
`<meta name="twitter:player:height" content="${media.original_info.height}"/>`,
`<meta name="twitter:player:width" content="${media.original_info.width}"/>`,
`<meta name="og:video" content="${bestVariant?.url}"/>`,
`<meta name="og:video:secure_url" content="${bestVariant?.url}"/>`,
`<meta name="og:video:height" content="${media.original_info.height}"/>`,
`<meta name="og:video:width" content="${media.original_info.width}"/>`,
`<meta name="og:video:type" content="${bestVariant?.content_type}"/>`
);
}
};
let actualMediaNumber = 0;
let renderedMosaic = false;

View file

@ -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];

34
src/types.d.ts vendored
View file

@ -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';
}