diff --git a/.env.example b/.env.example index 7b22933..240e3b8 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ BRANDING_NAME = "FixTweet" -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" +DIRECT_MEDIA_DOMAINS = "d.fxtwitter.com,dl.fxtwitter.com,d.pxtwitter.com,d.twittpr.com,dl.pxtwitter.com,dl.twittpr.com,d.fixupx.com,d.xfixup.com,dl.fixupx.com,dl.xfixup.com" +TEXT_ONLY_DOMAINS = "t.fxtwitter.com,t.twittpr.com,t.fixupx.com,t.xfixup.com" +INSTANT_VIEW_DOMAINS = "i.fxtwitter.com,i.twittpr.com,i.fixupx.com,i.xfixup.com" DEPRECATED_DOMAIN_LIST = "pxtwitter.com,www.pxtwitter.com" DEPRECATED_DOMAIN_EPOCH = "1559320000000000000" MOSAIC_DOMAIN_LIST = "mosaic.fxtwitter.com" diff --git a/jestconfig.json b/jestconfig.json index d743ecb..c8bfe14 100644 --- a/jestconfig.json +++ b/jestconfig.json @@ -6,6 +6,7 @@ "globals": { "BRANDING_NAME": "FixTweet", "TEXT_ONLY_DOMAINS": "t.fxtwitter.com,t.twittpr.com", + "INSTANT_VIEW_DOMAINS": "i.fxtwitter.com,i.twittpr.com", "DIRECT_MEDIA_DOMAINS": "d.fxtwitter.com,dl.fxtwitter.com", "MOSAIC_DOMAIN_LIST": "mosaic.fxtwitter.com", "DEPRECATED_DOMAIN_LIST": "pxtwitter.com,www.pxtwitter.com", diff --git a/src/api/status.ts b/src/api/status.ts index 4b549b1..869482d 100644 --- a/src/api/status.ts +++ b/src/api/status.ts @@ -210,22 +210,23 @@ export const statusAPI = async ( event: FetchEvent, flags?: InputFlags ): Promise => { - let wasMediaBlockedNSFW = false; - let res = await fetchConversation(status, event); + const res = await fetchConversation(status, event); const tweet = res.data?.tweetResult?.result; if (!tweet) { return { code: 404, message: 'NOT_FOUND' }; } - if (tweet.__typename === 'TweetUnavailable' && tweet.reason === 'NsfwLoggedOut') { - wasMediaBlockedNSFW = true; - res = await fetchConversation(status, event, true); - } + /* We're handling this in the actual fetch code now */ + + // if (tweet.__typename === 'TweetUnavailable' && tweet.reason === 'NsfwLoggedOut') { + // wasMediaBlockedNSFW = true; + // res = await fetchConversation(status, event, true); + // } // console.log(JSON.stringify(tweet)) if (tweet.__typename === 'TweetUnavailable') { - if (tweet.reason === 'Protected') { - writeDataPoint(event, language, wasMediaBlockedNSFW, 'PRIVATE_TWEET', flags); + if ((tweet as {reason: string})?.reason === 'Protected') { + writeDataPoint(event, language, false, 'PRIVATE_TWEET', flags); return { code: 401, message: 'PRIVATE_TWEET' }; // } else if (tweet.reason === 'NsfwLoggedOut') { // // API failure as elongator should have handled this @@ -233,14 +234,14 @@ export const statusAPI = async ( // return { code: 500, message: 'API_FAIL' }; } else { // Api failure at parsing status - writeDataPoint(event, language, wasMediaBlockedNSFW, 'API_FAIL', flags); + writeDataPoint(event, language, false, 'API_FAIL', flags); return { code: 500, message: 'API_FAIL' }; } } // If the tweet is not a graphQL tweet something went wrong if (!isGraphQLTweet(tweet)) { console.log('Tweet was not a valid tweet', tweet); - writeDataPoint(event, language, wasMediaBlockedNSFW, 'API_FAIL', flags); + writeDataPoint(event, language, false, 'API_FAIL', flags); return { code: 500, message: 'API_FAIL' }; } @@ -274,7 +275,7 @@ export const statusAPI = async ( /* Finally, staple the Tweet to the response and return it */ response.tweet = apiTweet; - writeDataPoint(event, language, wasMediaBlockedNSFW, 'OK', flags); + writeDataPoint(event, language, false, 'OK', flags); return response; }; diff --git a/src/constants.ts b/src/constants.ts index 9072ae1..c73c5e0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,6 +3,7 @@ export const Constants = { BRANDING_NAME: BRANDING_NAME, DIRECT_MEDIA_DOMAINS: DIRECT_MEDIA_DOMAINS.split(','), TEXT_ONLY_DOMAINS: TEXT_ONLY_DOMAINS.split(','), + INSTANT_VIEW_DOMAINS: INSTANT_VIEW_DOMAINS.split(','), DEPRECATED_DOMAIN_LIST: DEPRECATED_DOMAIN_LIST.split(','), DEPRECATED_DOMAIN_EPOCH: BigInt(DEPRECATED_DOMAIN_EPOCH), MOSAIC_DOMAIN_LIST: MOSAIC_DOMAIN_LIST.split(','), diff --git a/src/embed/status.ts b/src/embed/status.ts index fc4fb20..deb8730 100644 --- a/src/embed/status.ts +++ b/src/embed/status.ts @@ -42,7 +42,7 @@ export const handleStatus = async ( isTelegram /*&& !tweet.possibly_sensitive*/ && !flags?.direct && !flags?.api && - (tweet.media?.mosaic || tweet.is_note_tweet || tweet.quote); + (tweet.media?.mosaic || tweet.is_note_tweet || tweet.quote || flags?.forceInstantView); let ivbody = ''; diff --git a/src/fetch.ts b/src/fetch.ts index 048211d..55d50e3 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -128,11 +128,13 @@ export const twitterFetch = async ( method: 'GET', headers: headers }); + console.log('Elongator request successful'); } else { apiRequest = await fetch(url, { method: 'GET', headers: headers }); + console.log('Guest API request successful'); } response = await apiRequest?.json(); @@ -148,12 +150,19 @@ export const twitterFetch = async ( continue; } + // @ts-expect-error This is safe due to optional chaining + if ((response as TweetResultsByRestIdResult)?.data?.tweetResult?.result?.reason === 'NsfwLoggedOut') { + console.log(`nsfw tweet detected, it's elongator time`); + useElongator = true; + continue; + } + const remainingRateLimit = parseInt( apiRequest.headers.get('x-rate-limit-remaining') || '0' ); console.log(`Remaining rate limit: ${remainingRateLimit} requests`); /* Running out of requests within our rate limit, let's purge the cache */ - if (remainingRateLimit < 10) { + if (remainingRateLimit < 10 && !useElongator) { console.log(`Purging token on this edge due to low rate limit remaining`); event && event.waitUntil( @@ -180,6 +189,7 @@ export const twitterFetch = async ( // @ts-expect-error - We'll pin the guest token to whatever response we have response.guestToken = guestToken; + console.log('twitterFetch is all done here, see you soon!'); return response; } @@ -240,13 +250,17 @@ export const fetchConversation = async ( if (isGraphQLTweet(tweet)) { return true; } - if (tweet?.__typename === 'TweetUnavailable' && tweet.reason === 'Protected') { + console.log('invalid graphql tweet'); + if (tweet?.__typename === 'TweetUnavailable' && tweet.reason === 'NsfwLoggedOut') { + console.log('tweet is nsfw'); return true; } - if (tweet?.__typename === 'TweetUnavailable' && tweet.reason === 'NsfwLoggedOut') { + if (tweet?.__typename === 'TweetUnavailable' && tweet.reason === 'Protected') { + console.log('tweet is protected'); return true; } if (tweet?.__typename === 'TweetUnavailable') { + console.log('generic tweet unavailable error') return true; } // Final clause for checking if it's valid is if there's errors @@ -284,6 +298,7 @@ export const fetchUser = async ( const response = _res as GraphQLUserResponse; // If _res.data is an empty object, we have no user if (!Object.keys(response?.data).length) { + console.log(`response.data is empty, can't continue`); return false; } return !( diff --git a/src/server.ts b/src/server.ts index 53d6bc9..ccf5041 100644 --- a/src/server.ts +++ b/src/server.ts @@ -53,6 +53,9 @@ const statusRequest = async ( } else if (Constants.TEXT_ONLY_DOMAINS.includes(url.hostname)) { console.log('Text-only embed request'); flags.textOnly = true; + } else if (Constants.INSTANT_VIEW_DOMAINS.includes(url.hostname)) { + console.log('Forced instant view request'); + flags.forceInstantView = true; } else if (prefix === 'dl' || prefix === 'dir') { console.log('Direct media request by path prefix'); flags.direct = true; diff --git a/src/types/env.d.ts b/src/types/env.d.ts index 79ea257..369e8fb 100644 --- a/src/types/env.d.ts +++ b/src/types/env.d.ts @@ -1,6 +1,7 @@ declare const BRANDING_NAME: string; declare const DIRECT_MEDIA_DOMAINS: string; declare const TEXT_ONLY_DOMAINS: string; +declare const INSTANT_VIEW_DOMAINS: string; declare const DEPRECATED_DOMAIN_LIST: string; declare const DEPRECATED_DOMAIN_EPOCH: string; declare const HOST_URL: string; diff --git a/src/types/twitterTypes.d.ts b/src/types/twitterTypes.d.ts index c84dc2d..9aaf1d6 100644 --- a/src/types/twitterTypes.d.ts +++ b/src/types/twitterTypes.d.ts @@ -308,7 +308,7 @@ type GraphQLUser = { type GraphQLTweet = { // Workaround result: GraphQLTweet; - __typename: 'Tweet'; + __typename: 'Tweet' | 'TweetUnavailable'; rest_id: string; // "1674824189176590336", has_birdwatch_notes: false; core: { diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 69ffc15..bf62310 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -8,6 +8,7 @@ type InputFlags = { deprecated?: boolean; textOnly?: boolean; isXDomain?: boolean; + forceInstantView?: boolean; }; interface StatusResponse { diff --git a/test/index.test.ts b/test/index.test.ts index 655218a..257b1c1 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -6,6 +6,7 @@ const humanHeaders = { 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36' }; const githubUrl = 'https://github.com/FixTweet/FixTweet'; +const twitterBaseUrl = 'https://twitter.com'; test('Home page redirect', async () => { const result = await cacheWrapper( @@ -49,7 +50,7 @@ test('Twitter moment redirect', async () => { ); expect(result.status).toEqual(302); expect(result.headers.get('location')).toEqual( - 'https://twitter.com/i/events/1572638642127966214' + `${twitterBaseUrl}/i/events/1572638642127966214` ); }); @@ -78,7 +79,7 @@ test('API fetch basic Tweet', async () => { const tweet = response.tweet as APITweet; expect(tweet).toBeTruthy(); - expect(tweet.url).toEqual('https://twitter.com/jack/status/20'); + expect(tweet.url).toEqual(`${twitterBaseUrl}/jack/status/20`); expect(tweet.id).toEqual('20'); expect(tweet.text).toEqual('just setting up my twttr'); expect(tweet.author.screen_name?.toLowerCase()).toEqual('jack'); @@ -112,7 +113,7 @@ test('API fetch video Tweet', async () => { const tweet = response.tweet as APITweet; expect(tweet).toBeTruthy(); - expect(tweet.url).toEqual('https://twitter.com/X/status/854416760933556224'); + expect(tweet.url).toEqual(`${twitterBaseUrl}/X/status/854416760933556224`); expect(tweet.id).toEqual('854416760933556224'); expect(tweet.text).toEqual( 'Get the sauces ready, #NuggsForCarter has 3 million+ Retweets.' @@ -158,7 +159,7 @@ test('API fetch multi-photo Tweet', async () => { const tweet = response.tweet as APITweet; expect(tweet).toBeTruthy(); - expect(tweet.url).toEqual('https://twitter.com/X/status/1445094085593866246'); + expect(tweet.url).toEqual(`${twitterBaseUrl}/X/status/1445094085593866246`); expect(tweet.id).toEqual('1445094085593866246'); expect(tweet.text).toEqual('@netflix'); expect(tweet.author.screen_name?.toLowerCase()).toEqual('x'); @@ -206,7 +207,7 @@ test('API fetch poll Tweet', async () => { const tweet = response.tweet as APITweet; expect(tweet).toBeTruthy(); - expect(tweet.url).toEqual('https://twitter.com/X/status/1055475950543167488'); + expect(tweet.url).toEqual(`${twitterBaseUrl}/X/status/1055475950543167488`); expect(tweet.id).toEqual('1055475950543167488'); expect(tweet.text).toEqual('A poll:'); expect(tweet.author.screen_name?.toLowerCase()).toEqual('x'); @@ -256,7 +257,7 @@ test('API fetch user', async () => { const user = response.user as APIUser; expect(user).toBeTruthy(); - expect(user.url).toEqual('https://twitter.com/X'); + expect(user.url).toEqual(`${twitterBaseUrl}/X`); expect(user.id).toEqual('783214'); expect(user.screen_name).toEqual('X'); expect(user.followers).toEqual(expect.any(Number)); diff --git a/webpack.config.js b/webpack.config.js index ab41b68..dd272c3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,7 @@ let envVariables = [ 'BRANDING_NAME', 'DIRECT_MEDIA_DOMAINS', 'TEXT_ONLY_DOMAINS', + 'INSTANT_VIEW_DOMAINS', 'HOST_URL', 'REDIRECT_URL', 'EMBED_URL',