From 57620d300478f5eb6496c69a85c7e63503c2cc1b Mon Sep 17 00:00:00 2001
From: dangered wolf <d@ngeredwolf.me>
Date: Thu, 6 Apr 2023 17:49:41 -0400
Subject: [PATCH] Attempt to implement API fallback proxy

---
 .env.example       |  3 ++-
 src/api/status.ts  | 36 +++++++++++++++++++++++++-----------
 src/constants.ts   |  1 +
 src/fetch.ts       | 10 +++++-----
 src/types/env.d.ts |  1 +
 webpack.config.js  |  3 ++-
 6 files changed, 36 insertions(+), 18 deletions(-)

diff --git a/.env.example b/.env.example
index fb2d5f8..e0f9351 100644
--- a/.env.example
+++ b/.env.example
@@ -12,4 +12,5 @@ EMBED_URL = "https://github.com/FixTweet/FixTweet"
 SENTRY_DSN = ""
 SENTRY_AUTH_TOKEN = ""
 SENTRY_ORG = ""
-SENTRY_PROJECT = ""
\ No newline at end of file
+SENTRY_PROJECT = ""
+API_FALLBACK_DOMAIN = ""
\ No newline at end of file
diff --git a/src/api/status.ts b/src/api/status.ts
index 71a3f93..32a56fb 100644
--- a/src/api/status.ts
+++ b/src/api/status.ts
@@ -154,21 +154,35 @@ export const statusAPI = async (
 
     /* We've got timeline instructions, so the Tweet is probably private */
     if (conversation.timeline?.instructions?.length > 0) {
-      return { code: 401, message: 'PRIVATE_TWEET' };
-    }
+      // Try request again with fallback this time
+      console.log('No Tweet was found, trying fallback in case of geo-restriction');
+      const conversationFallback = await fetchConversation(status, event, true);
+      tweet = conversationFallback?.globalObjects?.tweets?.[status] || {};
 
-    /* {"errors":[{"code":34,"message":"Sorry, that page does not exist."}]} */
-    if (conversation.errors?.[0]?.code === 34) {
-      return { code: 404, message: 'NOT_FOUND' };
-    }
+      if (typeof tweet.full_text !== 'undefined') {
+        console.log('Successfully loaded Tweet, requiring fallback');
+      }
 
-    /* Tweets object is completely missing, smells like API failure */
-    if (typeof conversation?.globalObjects?.tweets === 'undefined') {
+      if (
+        typeof tweet.full_text === 'undefined' &&
+        conversation.timeline?.instructions?.length > 0
+      ) {
+        return { code: 401, message: 'PRIVATE_TWEET' };
+      }
+    } else {
+      /* {"errors":[{"code":34,"message":"Sorry, that page does not exist."}]} */
+      if (conversation.errors?.[0]?.code === 34) {
+        return { code: 404, message: 'NOT_FOUND' };
+      }
+
+      /* Tweets object is completely missing, smells like API failure */
+      if (typeof conversation?.globalObjects?.tweets === 'undefined') {
+        return { code: 500, message: 'API_FAIL' };
+      }
+
+      /* If we have no idea what happened then just return API error */
       return { code: 500, message: 'API_FAIL' };
     }
-
-    /* If we have no idea what happened then just return API error */
-    return { code: 500, message: 'API_FAIL' };
   }
 
   /* Creating the response objects */
diff --git a/src/constants.ts b/src/constants.ts
index 306be9d..5e89eb3 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -15,6 +15,7 @@ export const Constants = {
   API_DOCS_URL: `https://github.com/dangeredwolf/FixTweet/wiki/API-Home`,
   TWITTER_ROOT: 'https://twitter.com',
   TWITTER_API_ROOT: 'https://api.twitter.com',
+  API_FALLBACK_DOMAIN: API_FALLBACK_DOMAIN,
   BOT_UA_REGEX:
     /bot|facebook|embed|got|firefox\/92|firefox\/38|curl|wget|go-http|yahoo|generator|whatsapp|preview|link|proxy|vkshare|images|analyzer|index|crawl|spider|python|cfnetwork|node/gi,
   /* 3 hours */
diff --git a/src/fetch.ts b/src/fetch.ts
index 65b20a9..e5eda06 100644
--- a/src/fetch.ts
+++ b/src/fetch.ts
@@ -113,9 +113,6 @@ export const twitterFetch = async (
     headers['x-twitter-active-user'] = 'yes';
     headers['x-guest-token'] = guestToken;
 
-    /* We pretend to be the Twitter Web App as closely as possible,
-      so we use twitter.com/i/api/2 instead of api.twitter.com/2.
-      We probably don't have to do this at all. But hey, better to be consistent with Twitter Web App. */
     let response: unknown;
     let apiRequest;
 
@@ -179,10 +176,13 @@ export const twitterFetch = async (
 
 export const fetchConversation = async (
   status: string,
-  event: FetchEvent
+  event: FetchEvent,
+  fallback = false
 ): Promise<TimelineBlobPartial> => {
   return (await twitterFetch(
-    `${Constants.TWITTER_API_ROOT}/2/timeline/conversation/${status}.json?${Constants.GUEST_FETCH_PARAMETERS}`,
+    `${
+      fallback ? Constants.API_FALLBACK_DOMAIN : Constants.TWITTER_API_ROOT
+    }/2/timeline/conversation/${status}.json?${Constants.GUEST_FETCH_PARAMETERS}`,
     event,
     (_conversation: unknown) => {
       const conversation = _conversation as TimelineBlobPartial;
diff --git a/src/types/env.d.ts b/src/types/env.d.ts
index 3f6de37..0d008bf 100644
--- a/src/types/env.d.ts
+++ b/src/types/env.d.ts
@@ -9,6 +9,7 @@ declare const EMBED_URL: string;
 declare const REDIRECT_URL: string;
 declare const MOSAIC_DOMAIN_LIST: string;
 declare const API_HOST_LIST: string;
+declare const API_FALLBACK_DOMAIN: string;
 
 declare const SENTRY_DSN: string;
 declare const RELEASE_NAME: string;
diff --git a/webpack.config.js b/webpack.config.js
index b3a9c1c..40b7321 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -29,7 +29,8 @@ let envVariables = [
   'API_HOST_LIST',
   'SENTRY_DSN',
   'DEPRECATED_DOMAIN_LIST',
-  'DEPRECATED_DOMAIN_EPOCH'
+  'DEPRECATED_DOMAIN_EPOCH',
+  'API_FALLBACK_DOMAIN'
 ];
 
 let plugins = [