diff --git a/.env.example b/.env.example
index fb2d5f8..7b22933 100644
--- a/.env.example
+++ b/.env.example
@@ -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"
diff --git a/jestconfig.json b/jestconfig.json
index 863ec8b..d743ecb 100644
--- a/jestconfig.json
+++ b/jestconfig.json
@@ -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",
diff --git a/package.json b/package.json
index ff58381..38e23c9 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/src/api/status.ts b/src/api/status.ts
index d554f1b..545f8fb 100644
--- a/src/api/status.ts
+++ b/src/api/status.ts
@@ -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);
diff --git a/src/constants.ts b/src/constants.ts
index ba3367e..45be395 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -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`,
diff --git a/src/embed/status.ts b/src/embed/status.ts
index f3c56e2..16caa1c 100644
--- a/src/embed/status.ts
+++ b/src/embed/status.ts
@@ -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(
- ``,
- ``,
- ``,
- ``,
- ``,
- ``,
- ``,
- ``,
- ``
+ } 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(
- ``,
- ``
+ 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(
- ``,
- ``,
- ``,
- ``
- );
+ 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(
diff --git a/src/helpers/linkFixer.ts b/src/helpers/linkFixer.ts
index 8dfb8e7..d28bcd9 100644
--- a/src/helpers/linkFixer.ts
+++ b/src/helpers/linkFixer.ts
@@ -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.
diff --git a/src/helpers/mosaic.ts b/src/helpers/mosaic.ts
index 2ff2d4f..44e5c60 100644
--- a/src/helpers/mosaic.ts
+++ b/src/helpers/mosaic.ts
@@ -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;
}
};
diff --git a/src/render/photo.ts b/src/render/photo.ts
new file mode 100644
index 0000000..67b871e
--- /dev/null
+++ b/src/render/photo.ts
@@ -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 = [
+ ``,
+ ``
+ ];
+ } else {
+ instructions.addHeaders = [
+ ``,
+ ``,
+ ``,
+ ``,
+ ``,
+ ``
+ ];
+ }
+
+ return instructions;
+};
diff --git a/src/render/video.ts b/src/render/video.ts
new file mode 100644
index 0000000..a1d751e
--- /dev/null
+++ b/src/render/video.ts
@@ -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 = [
+ ``,
+ ``,
+ ``,
+ ``,
+ ``,
+ ``,
+ ``,
+ ``,
+ ``
+ ];
+
+ return instructions;
+};
diff --git a/src/strings.ts b/src/strings.ts
index ec613fa..96ffcc0 100644
--- a/src/strings.ts
+++ b/src/strings.ts
@@ -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',
diff --git a/src/types/env.d.ts b/src/types/env.d.ts
index 3656c89..79ea257 100644
--- a/src/types/env.d.ts
+++ b/src/types/env.d.ts
@@ -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;
diff --git a/src/types/types.d.ts b/src/types/types.d.ts
index 476e3bd..fc868f8 100644
--- a/src/types/types.d.ts
+++ b/src/types/types.d.ts
@@ -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;
diff --git a/webpack.config.js b/webpack.config.js
index 8496e52..ab41b68 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -19,7 +19,6 @@ require('dotenv').config();
let envVariables = [
'BRANDING_NAME',
- 'BRANDING_NAME_DISCORD',
'DIRECT_MEDIA_DOMAINS',
'TEXT_ONLY_DOMAINS',
'HOST_URL',