mirror of
https://github.com/CompeyDev/fxtwitter-docker.git
synced 2025-04-05 10:30:55 +01:00
Merge pull request #296 from FixTweet/addressable-media-numbers
Rewritten, modular photo/video embed renderer
This commit is contained in:
commit
e2ee22789b
14 changed files with 282 additions and 154 deletions
|
@ -1,5 +1,4 @@
|
|||
BRANDING_NAME = "FixTweet"
|
||||
BRANDING_NAME_DISCORD = "FixTweet - Embed videos, polls & more!"
|
||||
DIRECT_MEDIA_DOMAINS = "d.fxtwitter.com,dl.fxtwitter.com,d.pxtwitter.com,d.twittpr.com,dl.pxtwitter.com,dl.twittpr.com"
|
||||
TEXT_ONLY_DOMAINS = "t.fxtwitter.com,t.twittpr.com"
|
||||
DEPRECATED_DOMAIN_LIST = "pxtwitter.com,www.pxtwitter.com"
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
},
|
||||
"globals": {
|
||||
"BRANDING_NAME": "FixTweet",
|
||||
"BRANDING_NAME_DISCORD": "FixTweetBrandingDiscord",
|
||||
"TEXT_ONLY_DOMAINS": "t.fxtwitter.com,t.twittpr.com",
|
||||
"DIRECT_MEDIA_DOMAINS": "d.fxtwitter.com,dl.fxtwitter.com",
|
||||
"MOSAIC_DOMAIN_LIST": "mosaic.fxtwitter.com",
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
"main": "dist/worker.js",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"publish": "wrangler publish",
|
||||
"publish": "wrangler deploy",
|
||||
"deploy": "wrangler deploy",
|
||||
"log": "wrangler tail",
|
||||
"reload": "wrangler publish && wrangler tail",
|
||||
"reload": "wrangler deploy && wrangler tail",
|
||||
"prettier": "prettier --write .",
|
||||
"lint:eslint": "eslint --max-warnings=0 src",
|
||||
"test": "jest --config jestconfig.json --verbose"
|
||||
|
|
|
@ -71,18 +71,22 @@ const populateTweetProperties = async (
|
|||
tweet.extended_entities?.media || tweet.entities?.media || []
|
||||
);
|
||||
|
||||
// console.log('tweet', JSON.stringify(tweet));
|
||||
|
||||
/* Populate this Tweet's media */
|
||||
mediaList.forEach(media => {
|
||||
const mediaObject = processMedia(media);
|
||||
if (mediaObject) {
|
||||
apiTweet.media = apiTweet.media || {};
|
||||
apiTweet.media.all = apiTweet.media?.all || [];
|
||||
apiTweet.media.all.push(mediaObject);
|
||||
|
||||
if (mediaObject.type === 'photo') {
|
||||
apiTweet.twitter_card = 'summary_large_image';
|
||||
apiTweet.media = apiTweet.media || {};
|
||||
apiTweet.media.photos = apiTweet.media.photos || [];
|
||||
apiTweet.media.photos.push(mediaObject);
|
||||
} else if (mediaObject.type === 'video' || mediaObject.type === 'gif') {
|
||||
apiTweet.twitter_card = 'player';
|
||||
apiTweet.media = apiTweet.media || {};
|
||||
apiTweet.media.videos = apiTweet.media.videos || [];
|
||||
apiTweet.media.videos.push(mediaObject);
|
||||
}
|
||||
|
@ -207,7 +211,7 @@ export const statusAPI = async (
|
|||
conversation.timeline?.instructions?.length > 0
|
||||
) {
|
||||
console.log(
|
||||
'Tweet could not be accessed with elongator, must be private/suspende, got tweet ',
|
||||
'Tweet could not be accessed with elongator, must be private/suspended, got tweet ',
|
||||
tweet,
|
||||
' conversation ',
|
||||
conversation
|
||||
|
@ -223,11 +227,13 @@ export const statusAPI = async (
|
|||
return { code: 404, message: 'NOT_FOUND' };
|
||||
}
|
||||
|
||||
/* Commented this the part below out for now since it seems like atm this check doesn't actually do anything */
|
||||
|
||||
/* Tweets object is completely missing, smells like API failure */
|
||||
if (typeof conversation?.globalObjects?.tweets === 'undefined') {
|
||||
writeDataPoint(event, language, wasMediaBlockedNSFW, 'API_FAIL', flags);
|
||||
return { code: 500, message: 'API_FAIL' };
|
||||
}
|
||||
// if (typeof conversation?.globalObjects?.tweets === 'undefined') {
|
||||
// writeDataPoint(event, language, wasMediaBlockedNSFW, 'API_FAIL', flags);
|
||||
// return { code: 500, message: 'API_FAIL' };
|
||||
// }
|
||||
|
||||
/* If we have no idea what happened then just return API error */
|
||||
writeDataPoint(event, language, wasMediaBlockedNSFW, 'API_FAIL', flags);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export const Constants = {
|
||||
/* These constants are populated by variables in .env, then set by Webpack */
|
||||
BRANDING_NAME: BRANDING_NAME,
|
||||
BRANDING_NAME_DISCORD: BRANDING_NAME_DISCORD,
|
||||
DIRECT_MEDIA_DOMAINS: DIRECT_MEDIA_DOMAINS.split(','),
|
||||
TEXT_ONLY_DOMAINS: TEXT_ONLY_DOMAINS.split(','),
|
||||
DEPRECATED_DOMAIN_LIST: DEPRECATED_DOMAIN_LIST.split(','),
|
||||
|
@ -30,10 +29,13 @@ export const Constants = {
|
|||
'include_quote_count=true',
|
||||
'include_reply_count=1',
|
||||
'tweet_mode=extended',
|
||||
'include_entities=true',
|
||||
'include_ext_media_color=true',
|
||||
'include_ext_media_availability=true',
|
||||
'include_ext_sensitive_media_warning=true',
|
||||
'simple_quoted_tweet=true'
|
||||
'include_ext_has_birdwatch_notes=true',
|
||||
'simple_quoted_tweet=true',
|
||||
'ext=mediaStats%2ChighlightedLabel'
|
||||
].join('&'),
|
||||
BASE_HEADERS: {
|
||||
'DNT': `1`,
|
||||
|
|
|
@ -4,6 +4,8 @@ import { formatNumber, sanitizeText } from '../helpers/utils';
|
|||
import { Strings } from '../strings';
|
||||
import { getAuthorText } from '../helpers/author';
|
||||
import { statusAPI } from '../api/status';
|
||||
import { renderPhoto } from '../render/photo';
|
||||
import { renderVideo } from '../render/video';
|
||||
|
||||
export const returnError = (error: string): StatusResponse => {
|
||||
return {
|
||||
|
@ -43,6 +45,13 @@ 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];
|
||||
}
|
||||
|
||||
/* If there was any errors fetching the Tweet, we'll return it */
|
||||
switch (api.code) {
|
||||
case 401:
|
||||
|
@ -56,13 +65,22 @@ export const handleStatus = async (
|
|||
/* Catch direct media request (d.fxtwitter.com, or .mp4 / .jpg) */
|
||||
if (flags?.direct && tweet.media) {
|
||||
let redirectUrl: string | null = null;
|
||||
if (tweet.media.videos) {
|
||||
const { videos } = tweet.media;
|
||||
redirectUrl = (videos[(mediaNumber || 1) - 1] || videos[0]).url;
|
||||
} else if (tweet.media.photos) {
|
||||
const { photos } = tweet.media;
|
||||
redirectUrl = (photos[(mediaNumber || 1) - 1] || photos[0]).url;
|
||||
const all = tweet.media.all || [];
|
||||
// if (tweet.media.videos) {
|
||||
// const { videos } = tweet.media;
|
||||
// redirectUrl = (videos[(mediaNumber || 1) - 1] || videos[0]).url;
|
||||
// } else if (tweet.media.photos) {
|
||||
// const { photos } = tweet.media;
|
||||
// redirectUrl = (photos[(mediaNumber || 1) - 1] || photos[0]).url;
|
||||
// }
|
||||
|
||||
const selectedMedia = all[(mediaNumber || 1) - 1];
|
||||
if (selectedMedia) {
|
||||
redirectUrl = selectedMedia.url;
|
||||
} else if (all.length > 0) {
|
||||
redirectUrl = all[0].url;
|
||||
}
|
||||
|
||||
if (redirectUrl) {
|
||||
return { response: Response.redirect(redirectUrl, 302) };
|
||||
}
|
||||
|
@ -124,127 +142,82 @@ export const handleStatus = async (
|
|||
newText = `${formatText}\n\n` + `${translation.text}\n\n`;
|
||||
}
|
||||
|
||||
/* This Tweet has a video to render.
|
||||
console.log('overrideMedia', JSON.stringify(overrideMedia));
|
||||
|
||||
Twitter supports multiple videos in a Tweet now. But we have no mechanism to embed more than one.
|
||||
You can still use /video/:number to get a specific video. Otherwise, it'll pick the first. */
|
||||
if (tweet.media?.videos) {
|
||||
authorText = newText || '';
|
||||
if (overrideMedia) {
|
||||
let instructions: ResponseInstructions;
|
||||
|
||||
if (tweet?.translation) {
|
||||
authorText = tweet.translation?.text || '';
|
||||
switch (overrideMedia.type) {
|
||||
case 'photo':
|
||||
/* This Tweet has a photo to render. */
|
||||
instructions = renderPhoto(
|
||||
{
|
||||
tweet: tweet,
|
||||
authorText: authorText,
|
||||
engagementText: engagementText,
|
||||
userAgent: userAgent,
|
||||
isOverrideMedia: true
|
||||
},
|
||||
overrideMedia as APIPhoto
|
||||
);
|
||||
headers.push(...instructions.addHeaders);
|
||||
if (instructions.authorText) {
|
||||
authorText = instructions.authorText;
|
||||
}
|
||||
if (instructions.siteName) {
|
||||
siteName = instructions.siteName;
|
||||
}
|
||||
break;
|
||||
case 'video':
|
||||
instructions = renderVideo(
|
||||
{ tweet: tweet, userAgent: userAgent, text: newText, isOverrideMedia: true },
|
||||
overrideMedia as APIVideo
|
||||
);
|
||||
headers.push(...instructions.addHeaders);
|
||||
if (instructions.authorText) {
|
||||
authorText = instructions.authorText;
|
||||
}
|
||||
if (instructions.siteName) {
|
||||
siteName = instructions.siteName;
|
||||
}
|
||||
/* This Tweet has a video to render. */
|
||||
break;
|
||||
}
|
||||
|
||||
const { videos } = tweet.media;
|
||||
const video = videos[(mediaNumber || 1) - 1];
|
||||
|
||||
/* This fix is specific to Discord not wanting to render videos that are too large,
|
||||
or rendering low quality videos too small.
|
||||
|
||||
Basically, our solution is to cut the dimensions in half if the video is too big (> 1080p),
|
||||
or double them if it's too small. (<400p)
|
||||
|
||||
We check both height and width so we can apply this to both horizontal and vertical videos equally*/
|
||||
|
||||
let sizeMultiplier = 1;
|
||||
|
||||
if (video.width > 1920 || video.height > 1920) {
|
||||
sizeMultiplier = 0.5;
|
||||
}
|
||||
if (video.width < 400 && video.height < 400) {
|
||||
sizeMultiplier = 2;
|
||||
}
|
||||
|
||||
/* Like photos when picking a specific one (not using mosaic),
|
||||
we'll put an indicator if there are more than one video */
|
||||
if (videos.length > 1) {
|
||||
const videoCounter = Strings.VIDEO_COUNT.format({
|
||||
number: String(videos.indexOf(video) + 1),
|
||||
total: String(videos.length)
|
||||
});
|
||||
|
||||
authorText =
|
||||
authorText === Strings.DEFAULT_AUTHOR_TEXT
|
||||
? videoCounter
|
||||
: `${authorText}${authorText ? ' ― ' : ''}${videoCounter}`;
|
||||
|
||||
siteName = `${Constants.BRANDING_NAME} - ${videoCounter}`;
|
||||
|
||||
if (engagementText) {
|
||||
siteName = `${Constants.BRANDING_NAME} - ${engagementText} - ${videoCounter}`;
|
||||
}
|
||||
}
|
||||
|
||||
/* Push the raw video-related headers */
|
||||
headers.push(
|
||||
`<meta property="twitter:player:stream:content_type" content="${video.format}"/>`,
|
||||
`<meta property="twitter:player:height" content="${
|
||||
video.height * sizeMultiplier
|
||||
}"/>`,
|
||||
`<meta property="twitter:player:width" content="${video.width * sizeMultiplier}"/>`,
|
||||
`<meta property="og:video" content="${video.url}"/>`,
|
||||
`<meta property="og:video:secure_url" content="${video.url}"/>`,
|
||||
`<meta property="og:video:height" content="${video.height * sizeMultiplier}"/>`,
|
||||
`<meta property="og:video:width" content="${video.width * sizeMultiplier}"/>`,
|
||||
`<meta property="og:video:type" content="${video.format}"/>`,
|
||||
`<meta property="twitter:image" content="0"/>`
|
||||
} else if (tweet.media?.mosaic) {
|
||||
const instructions = renderPhoto(
|
||||
{
|
||||
tweet: tweet,
|
||||
authorText: authorText,
|
||||
engagementText: engagementText,
|
||||
userAgent: userAgent
|
||||
},
|
||||
tweet.media?.mosaic
|
||||
);
|
||||
}
|
||||
|
||||
/* This Tweet has one or more photos to render */
|
||||
if (tweet.media?.photos) {
|
||||
const { photos } = tweet.media;
|
||||
let photo: APIPhoto | APIMosaicPhoto = photos[(mediaNumber || 1) - 1];
|
||||
|
||||
/* If there isn't a specified media number and we have a
|
||||
mosaic response, we'll render it using mosaic */
|
||||
if (typeof mediaNumber !== 'number' && tweet.media.mosaic) {
|
||||
photo = {
|
||||
/* Include dummy height/width for TypeScript reasons. We have a check to make sure we don't use these later. */
|
||||
height: 0,
|
||||
width: 0,
|
||||
url: tweet.media.mosaic.formats.jpeg,
|
||||
type: 'photo',
|
||||
altText: ''
|
||||
};
|
||||
/* If mosaic isn't available or the link calls for a specific photo,
|
||||
we'll indicate which photo it is out of the total */
|
||||
} else if (photos.length > 1) {
|
||||
const photoCounter = Strings.PHOTO_COUNT.format({
|
||||
number: String(photos.indexOf(photo) + 1),
|
||||
total: String(photos.length)
|
||||
});
|
||||
|
||||
authorText =
|
||||
authorText === Strings.DEFAULT_AUTHOR_TEXT
|
||||
? photoCounter
|
||||
: `${authorText}${authorText ? ' ― ' : ''}${photoCounter}`;
|
||||
|
||||
siteName = `${Constants.BRANDING_NAME} - ${photoCounter}`;
|
||||
|
||||
if (engagementText) {
|
||||
siteName = `${Constants.BRANDING_NAME} - ${engagementText} - ${photoCounter}`;
|
||||
}
|
||||
}
|
||||
|
||||
/* Push the raw photo-related headers */
|
||||
headers.push(
|
||||
`<meta property="twitter:image" content="${photo.url}"/>`,
|
||||
`<meta property="og:image" content="${photo.url}"/>`
|
||||
headers.push(...instructions.addHeaders);
|
||||
} else if (tweet.media?.videos) {
|
||||
const instructions = renderVideo(
|
||||
{ tweet: tweet, userAgent: userAgent, text: newText },
|
||||
tweet.media?.videos[0]
|
||||
);
|
||||
|
||||
if (!tweet.media.mosaic) {
|
||||
headers.push(
|
||||
`<meta property="twitter:image:width" content="${photo.width}"/>`,
|
||||
`<meta property="twitter:image:height" content="${photo.height}"/>`,
|
||||
`<meta property="og:image:width" content="${photo.width}"/>`,
|
||||
`<meta property="og:image:height" content="${photo.height}"/>`
|
||||
);
|
||||
headers.push(...instructions.addHeaders);
|
||||
if (instructions.authorText) {
|
||||
authorText = instructions.authorText;
|
||||
}
|
||||
}
|
||||
|
||||
/* We have external media available to us (i.e. YouTube videos) */
|
||||
if (tweet.media?.external) {
|
||||
if (instructions.siteName) {
|
||||
siteName = instructions.siteName;
|
||||
}
|
||||
} else if (tweet.media?.photos) {
|
||||
const instructions = renderPhoto(
|
||||
{
|
||||
tweet: tweet,
|
||||
authorText: authorText,
|
||||
engagementText: engagementText,
|
||||
userAgent: userAgent
|
||||
},
|
||||
tweet.media?.photos[0]
|
||||
);
|
||||
headers.push(...instructions.addHeaders);
|
||||
} else if (tweet.media?.external) {
|
||||
const { external } = tweet.media;
|
||||
authorText = newText || '';
|
||||
headers.push(
|
||||
|
|
|
@ -2,7 +2,12 @@
|
|||
export const linkFixer = (tweet: TweetPartial, text: string): string => {
|
||||
if (typeof tweet.entities?.urls !== 'undefined') {
|
||||
tweet.entities?.urls.forEach((url: TcoExpansion) => {
|
||||
text = text.replace(url.url, url.expanded_url);
|
||||
let newURL = url.expanded_url;
|
||||
|
||||
if (newURL.match(/^https:\/\/twitter\.com\/i\/web\/status\/\w+/g) !== null) {
|
||||
newURL = '';
|
||||
}
|
||||
text = text.replace(url.url, newURL);
|
||||
});
|
||||
|
||||
/* Remove any link with unavailable original.
|
||||
|
|
|
@ -29,10 +29,11 @@ export const handleMosaic = async (
|
|||
}
|
||||
|
||||
return {
|
||||
type: 'mosaic_photo',
|
||||
formats: {
|
||||
jpeg: `${baseUrl}jpeg/${id}${path}`,
|
||||
webp: `${baseUrl}webp/${id}${path}`
|
||||
}
|
||||
} as unknown as APIMosaicPhoto;
|
||||
} as APIMosaicPhoto;
|
||||
}
|
||||
};
|
||||
|
|
62
src/render/photo.ts
Normal file
62
src/render/photo.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { Constants } from '../constants';
|
||||
import { Strings } from '../strings';
|
||||
|
||||
export const renderPhoto = (
|
||||
properties: RenderProperties,
|
||||
photo: APIPhoto | APIMosaicPhoto
|
||||
): ResponseInstructions => {
|
||||
const { tweet, engagementText, authorText, isOverrideMedia, userAgent } = properties;
|
||||
const instructions: ResponseInstructions = { addHeaders: [] };
|
||||
|
||||
if (
|
||||
(tweet.media?.photos?.length || 0) > 1 &&
|
||||
(!tweet.media?.mosaic || isOverrideMedia)
|
||||
) {
|
||||
photo = photo as APIPhoto;
|
||||
|
||||
const all = tweet.media?.all as APIMedia[];
|
||||
const baseString =
|
||||
all.length === tweet.media?.photos?.length
|
||||
? Strings.PHOTO_COUNT
|
||||
: Strings.MEDIA_COUNT;
|
||||
|
||||
const photoCounter = baseString.format({
|
||||
number: String(all.indexOf(photo) + 1),
|
||||
total: String(all.length)
|
||||
});
|
||||
|
||||
const isTelegram = (userAgent?.indexOf('Telegram') ?? 0) > -1;
|
||||
|
||||
if (authorText === Strings.DEFAULT_AUTHOR_TEXT || isTelegram) {
|
||||
instructions.authorText = photoCounter;
|
||||
} else {
|
||||
instructions.authorText = `${authorText}${
|
||||
authorText ? ' ― ' : ''
|
||||
}${photoCounter}`;
|
||||
}
|
||||
|
||||
if (engagementText && !isTelegram) {
|
||||
instructions.siteName = `${Constants.BRANDING_NAME} - ${engagementText} - ${photoCounter}`;
|
||||
} else {
|
||||
instructions.siteName = `${Constants.BRANDING_NAME} - ${photoCounter}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (photo.type === 'mosaic_photo' && !isOverrideMedia) {
|
||||
instructions.addHeaders = [
|
||||
`<meta property="twitter:image" content="${tweet.media?.mosaic?.formats.jpeg}"/>`,
|
||||
`<meta property="og:image" content="${tweet.media?.mosaic?.formats.jpeg}"/>`
|
||||
];
|
||||
} else {
|
||||
instructions.addHeaders = [
|
||||
`<meta property="twitter:image" content="${photo.url}"/>`,
|
||||
`<meta property="og:image" content="${photo.url}"/>`,
|
||||
`<meta property="twitter:image:width" content="${photo.width}"/>`,
|
||||
`<meta property="twitter:image:height" content="${photo.height}"/>`,
|
||||
`<meta property="og:image:width" content="${photo.width}"/>`,
|
||||
`<meta property="og:image:height" content="${photo.height}"/>`
|
||||
];
|
||||
}
|
||||
|
||||
return instructions;
|
||||
};
|
61
src/render/video.ts
Normal file
61
src/render/video.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { Constants } from '../constants';
|
||||
import { Strings } from '../strings';
|
||||
|
||||
export const renderVideo = (
|
||||
properties: RenderProperties,
|
||||
video: APIVideo
|
||||
): ResponseInstructions => {
|
||||
const { tweet, userAgent, text } = properties;
|
||||
const instructions: ResponseInstructions = { addHeaders: [] };
|
||||
|
||||
const all = tweet.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.
|
||||
|
||||
Basically, our solution is to cut the dimensions in half if the video is too big (> 1080p),
|
||||
or double them if it's too small. (<400p)
|
||||
|
||||
We check both height and width so we can apply this to both horizontal and vertical videos equally*/
|
||||
|
||||
let sizeMultiplier = 1;
|
||||
|
||||
if (video.width > 1920 || video.height > 1920) {
|
||||
sizeMultiplier = 0.5;
|
||||
}
|
||||
if (video.width < 400 && video.height < 400) {
|
||||
sizeMultiplier = 2;
|
||||
}
|
||||
|
||||
/* Like photos when picking a specific one (not using mosaic),
|
||||
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;
|
||||
const videoCounter = baseString.format({
|
||||
number: String(all.indexOf(video) + 1),
|
||||
total: String(all.length)
|
||||
});
|
||||
|
||||
instructions.siteName = `${Constants.BRANDING_NAME} - ${videoCounter}`;
|
||||
}
|
||||
|
||||
instructions.authorText = tweet.translation?.text || text || '';
|
||||
|
||||
/* Push the raw video-related headers */
|
||||
instructions.addHeaders = [
|
||||
`<meta property="twitter:player:stream:content_type" content="${video.format}"/>`,
|
||||
`<meta property="twitter:player:height" content="${video.height * sizeMultiplier}"/>`,
|
||||
`<meta property="twitter:player:width" content="${video.width * sizeMultiplier}"/>`,
|
||||
`<meta property="og:video" content="${video.url}"/>`,
|
||||
`<meta property="og:video:secure_url" content="${video.url}"/>`,
|
||||
`<meta property="og:video:height" content="${video.height * sizeMultiplier}"/>`,
|
||||
`<meta property="og:video:width" content="${video.width * sizeMultiplier}"/>`,
|
||||
`<meta property="og:video:type" content="${video.format}"/>`,
|
||||
`<meta property="twitter:image" content="0"/>`
|
||||
];
|
||||
|
||||
return instructions;
|
||||
};
|
|
@ -131,8 +131,9 @@ This is caused by Twitter API downtime or a new bug. Try again in a little while
|
|||
QUOTE_TEXT: `↘️ Quoting {name} (@{screen_name})`,
|
||||
TRANSLATE_TEXT: `↘️ Translated from {language}`,
|
||||
TRANSLATE_TEXT_INTL: `↘️ {source} ➡️ {destination}`,
|
||||
PHOTO_COUNT: `Photo {number} of {total}`,
|
||||
VIDEO_COUNT: `Video {number} of {total}`,
|
||||
PHOTO_COUNT: `Photo {number} / {total}`,
|
||||
VIDEO_COUNT: `Video {number} / {total}`,
|
||||
MEDIA_COUNT: `Media {number} / {total}`,
|
||||
|
||||
SINGULAR_DAY_LEFT: 'day left',
|
||||
PLURAL_DAYS_LEFT: 'days left',
|
||||
|
|
1
src/types/env.d.ts
vendored
1
src/types/env.d.ts
vendored
|
@ -1,5 +1,4 @@
|
|||
declare const BRANDING_NAME: string;
|
||||
declare const BRANDING_NAME_DISCORD: string;
|
||||
declare const DIRECT_MEDIA_DOMAINS: string;
|
||||
declare const TEXT_ONLY_DOMAINS: string;
|
||||
declare const DEPRECATED_DOMAIN_LIST: string;
|
||||
|
|
48
src/types/types.d.ts
vendored
48
src/types/types.d.ts
vendored
|
@ -15,6 +15,24 @@ interface StatusResponse {
|
|||
cacheControl?: string | null;
|
||||
}
|
||||
|
||||
interface ResponseInstructions {
|
||||
addHeaders: string[];
|
||||
authorText?: string;
|
||||
siteName?: string;
|
||||
engagementText?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface RenderProperties {
|
||||
tweet: APITweet;
|
||||
siteText?: string;
|
||||
authorText?: string;
|
||||
engagementText?: string;
|
||||
isOverrideMedia?: boolean;
|
||||
userAgent?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface Request {
|
||||
params: {
|
||||
[param: string]: string;
|
||||
|
@ -91,15 +109,26 @@ interface APIPoll {
|
|||
time_left_en: string;
|
||||
}
|
||||
|
||||
interface APIPhoto {
|
||||
type: 'photo';
|
||||
interface APIMedia {
|
||||
type: string;
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface APIPhoto extends APIMedia {
|
||||
type: 'photo';
|
||||
altText: string;
|
||||
}
|
||||
|
||||
interface APIMosaicPhoto {
|
||||
interface APIVideo extends APIMedia {
|
||||
type: 'video' | 'gif';
|
||||
thumbnail_url: string;
|
||||
format: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface APIMosaicPhoto extends APIMedia {
|
||||
type: 'mosaic_photo';
|
||||
formats: {
|
||||
webp: string;
|
||||
|
@ -107,16 +136,6 @@ interface APIMosaicPhoto {
|
|||
};
|
||||
}
|
||||
|
||||
interface APIVideo {
|
||||
type: 'video' | 'gif';
|
||||
url: string;
|
||||
thumbnail_url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
format: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface APITweet {
|
||||
id: string;
|
||||
url: string;
|
||||
|
@ -140,12 +159,13 @@ interface APITweet {
|
|||
external?: APIExternalMedia;
|
||||
photos?: APIPhoto[];
|
||||
videos?: APIVideo[];
|
||||
all?: APIMedia[];
|
||||
mosaic?: APIMosaicPhoto;
|
||||
};
|
||||
|
||||
lang: string | null;
|
||||
possibly_sensitive: boolean;
|
||||
|
||||
|
||||
replying_to: string | null;
|
||||
replying_to_status: string | null;
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ require('dotenv').config();
|
|||
|
||||
let envVariables = [
|
||||
'BRANDING_NAME',
|
||||
'BRANDING_NAME_DISCORD',
|
||||
'DIRECT_MEDIA_DOMAINS',
|
||||
'TEXT_ONLY_DOMAINS',
|
||||
'HOST_URL',
|
||||
|
|
Loading…
Add table
Reference in a new issue