From 1a6f424e6993b9f188dbe815831837c42da9b85a Mon Sep 17 00:00:00 2001
From: Dylan
Date: Sat, 18 Apr 2026 20:11:03 +0100
Subject: [PATCH] Initial translation implementation #273
---
activity.py | 13 ++++++--
msgs.py | 15 +++++++--
requirements.txt | 2 +-
translation.py | 7 ++++
twExtract/__init__.py | 40 ++++++++++++----------
twitfix.py | 77 ++++++++++++++++++++++++++++++-------------
utils.py | 13 +++++++-
vxApi.py | 10 ++++++
8 files changed, 130 insertions(+), 47 deletions(-)
create mode 100644 translation.py
diff --git a/activity.py b/activity.py
index bd6264f..6c52caa 100644
--- a/activity.py
+++ b/activity.py
@@ -8,11 +8,20 @@ def tweetDataToActivity(tweetData,embedIndex = -1):
if tweetData['replyingTo'] is not None:
content += f"↪️ Replying to @{tweetData['replyingTo']}
"
- content+=f"{tweetData['text']}
"
+
+ mainText = tweetData['text']
+ if tweetData["translation"] is not None:
+ content += f"🌐 {tweetData['translation']['source_language'].upper()}→{tweetData['translation']['destination_language'].upper()}
"
+ mainText=tweetData['translation']["text"]
+
+ content+=f"{mainText}
"
attachments=[]
if tweetData['qrt'] is not None:
- content += f"QRT: {tweetData['qrt']['user_screen_name']}
{tweetData['qrt']['text']}
"
+ qrtText = tweetData['qrt']['text']
+ if "translation" in tweetData['qrt'] and tweetData['qrt']["translation"] is not None:
+ qrtText = tweetData['qrt']["translation"]["text"]
+ content += f"QRT: {tweetData['qrt']['user_screen_name']}
{qrtText}
"
if tweetData['pollData'] is not None:
content += f"{msgs.genPollDisplay(tweetData['pollData'])}
"
content += "
"
diff --git a/msgs.py b/msgs.py
index 8bcd17c..07d3d02 100644
--- a/msgs.py
+++ b/msgs.py
@@ -17,8 +17,11 @@ def genLikesDisplay(vnf):
return ("💖 " + numerize.numerize(vnf['likes']))
def genQrtDisplay(qrt):
+ text = qrt['text']
+ if "translation" in qrt:
+ text = qrt["translation"]["text"]
verifiedCheck = "☑️" if ('verified' in qrt and qrt['verified']) else ""
- return ("\n\n【QRT of " + qrt['user_name'] + " (@" + qrt['user_screen_name'] + ")"+ verifiedCheck+":】\n\n'" + qrt['text'] + "'")
+ return ("\n\n【QRT of " + qrt['user_name'] + " (@" + qrt['user_screen_name'] + ")"+ verifiedCheck+":】\n\n'" + text + "'")
def genPollDisplay(poll):
pctSplit=10
@@ -27,6 +30,9 @@ def genPollDisplay(poll):
output+=choice["name"]+"\n"+("█"*int(choice["percent"]/pctSplit)) +" "+str(choice["percent"])+"%\n"
return output
+def genTranslationDisplay(translation):
+ return f"【TL "+translation["source_language"]+"→"+translation["destination_language"]+"】\n\n"+translation["text"]
+
# formats the top text of the embed
def formatProvider(base,vnf):
finalText = base
@@ -38,13 +44,16 @@ def formatProvider(base,vnf):
finalText = base
return finalText
-def formatEmbedDesc(type,body,qrt,pollData):
+def formatEmbedDesc(type,body,qrt,pollData,translation):
# Trim the embed description to 248 characters, prioritizing poll and likes
qrtType=None
if qrt!=None:
qrtType="Text"
+ if translation is not None:
+ body = genTranslationDisplay(translation)
+
limit = videoDescLimit if type=="Video" or (qrt!=None and (qrtType=="Video")) else tweetDescLimit
output = ""
@@ -73,6 +82,6 @@ def formatEmbedDesc(type,body,qrt,pollData):
diff = len(output)-limit
# remove the characters from body, add ellipsis
body = body[:-(diff+1)]+"…"
- return formatEmbedDesc(type,body,qrt,pollData)
+ return formatEmbedDesc(type,body,qrt,pollData,None)
else:
return output
diff --git a/requirements.txt b/requirements.txt
index ef9f74d..209347e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,4 +8,4 @@ Werkzeug==2.3.7
numerize==0.12
oauthlib==3.2.2
PyRTF3==0.47.5
-XClientTransaction==0.0.2
\ No newline at end of file
+XClientTransaction==1.0.2
\ No newline at end of file
diff --git a/translation.py b/translation.py
new file mode 100644
index 0000000..7e74ef4
--- /dev/null
+++ b/translation.py
@@ -0,0 +1,7 @@
+validLanguages = ['AF', 'AN', 'AR', 'AS', 'AY', 'AZ', 'BA', 'BE', 'BG', 'BN', 'BR', 'BS', 'CA', 'CS', 'CY', 'DA', 'DE', 'DE-DE', 'EL', 'EN', 'EO', 'ES', 'ES-419', 'ET', 'EU', 'FA', 'FI', 'FR', 'FR-FR', 'GA', 'GL', 'GN', 'GU', 'HA', 'HE', 'HI', 'HR', 'HT', 'HU', 'HY', 'ID', 'IG', 'IS', 'IT', 'JA', 'JV', 'KA', 'KK', 'KO', 'KY', 'LA', 'LB', 'LN', 'LT', 'LV', 'MG', 'MI', 'MK', 'ML', 'MN', 'MR', 'MS', 'MT', 'MY', 'NB', 'NE', 'NL', 'OC', 'OM', 'PA', 'PL', 'PS', 'PT-BR', 'PT-PT', 'QU', 'RO', 'RU', 'SA', 'SK', 'SL', 'SQ', 'SR', 'ST', 'SU', 'SV', 'SW', 'TA', 'TE', 'TG', 'TH', 'TK', 'TL', 'TN', 'TR', 'TS', 'TT', 'UK', 'UR', 'UZ', 'VI', 'WO', 'XH', 'YI', 'ZH', 'ZH-HANS', 'ZH-HANT', 'ZU']
+languageAliases = {
+ #"EN":"EN-US",
+ "JP":"JA",
+}
+
+mergedLangs=(validLanguages+list(languageAliases.keys()))
\ No newline at end of file
diff --git a/twExtract/__init__.py b/twExtract/__init__.py
index 364f56e..793c9af 100644
--- a/twExtract/__init__.py
+++ b/twExtract/__init__.py
@@ -113,7 +113,7 @@ def cycleBearerTokenGet(url,headers):
return tweet
raise TwExtractError(400, "Extract error")
-def twitterApiGet(url,btoken=None,authToken=None,guestToken=None,userAgent=None):
+def twitterApiGet(url,btoken=None,authToken=None,guestToken=None,userAgent=None,language=None):
if authToken != None and authToken.startswith("oa|"):
url = url.replace("https://x.com/i/api/graphql/","https://api.twitter.com/graphql/")
@@ -122,7 +122,7 @@ def twitterApiGet(url,btoken=None,authToken=None,guestToken=None,userAgent=None)
secret = authToken.split("|")[1]
twt = oauth1.Client(client_key='3nVuSoBZnx6U4vzUxf5w',client_secret='Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys',resource_owner_key=key,resource_owner_secret=secret)
- hdr = getAuthHeaders(androidBearer)
+ hdr = getAuthHeaders(androidBearer,language=language)
del hdr["Authorization"]
hdr["X-Twitter-Client"] = "TwitterAndroid"
if userAgent is not None:
@@ -135,12 +135,12 @@ def twitterApiGet(url,btoken=None,authToken=None,guestToken=None,userAgent=None)
if btoken is None:
btoken = v2bearer
#return cycleBearerTokenGet(url,getAuthHeaders(bearer,authToken=authToken,guestToken=guestToken))
- headers = getAuthHeaders(btoken,authToken=authToken,guestToken=guestToken)
+ headers = getAuthHeaders(btoken,authToken=authToken,guestToken=guestToken,language=language)
response = requests.get(url, headers=headers)
return response
-def getAuthHeaders(btoken,authToken=None,guestToken=None):
+def getAuthHeaders(btoken,authToken=None,guestToken=None,language=None):
csrfToken=str(uuid.uuid4()).replace('-', '')
headers = {"x-twitter-active-user":"yes","x-twitter-client-language":"en","x-csrf-token":csrfToken,"User-Agent":requestUserAgent}
@@ -152,6 +152,9 @@ def getAuthHeaders(btoken,authToken=None,guestToken=None):
if guestToken is not None:
headers["x-guest-token"] = guestToken
+ if language is not None:
+ headers["x-twitter-client-language"] = language
+
return headers
def getGuestToken():
@@ -268,7 +271,7 @@ def extractStatus_twExtractProxy(url,workaroundTokens=None):
continue
return output
-def extractStatusV2(url,workaroundTokens):
+def extractStatusV2(url,workaroundTokens,tlLanguage=None):
# get tweet ID
m = re.search(pathregex, url)
if m is None:
@@ -282,7 +285,7 @@ def extractStatusV2(url,workaroundTokens):
def request_with_token(twid, authToken):
vars = json.loads('{"includeTweetImpression":true,"includeHasBirdwatchNotes":false,"includeEditPerspective":false,"rest_ids":["x"],"includeEditControl":true,"includeCommunityTweetRelationship":true,"includeTweetVisibilityNudge":true}')
vars['rest_ids'][0] = str(twid)
- tweet = twitterApiGet(f"https://x.com/i/api/graphql/{v2graphql_api}/TweetResultsByIdsQuery?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(v2Features)}",authToken=authToken)
+ tweet = twitterApiGet(f"https://x.com/i/api/graphql/{v2graphql_api}/TweetResultsByIdsQuery?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(v2Features)}",authToken=authToken,language=tlLanguage)
try:
rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining")
print(f"Twitter Token Rate limit remaining: {rateLimitRemaining}")
@@ -321,7 +324,7 @@ def extractStatusV2(url,workaroundTokens):
return tweet
return parallel_token_request(twid, tokens, request_with_token)
-def extractStatusV2Android(url,workaroundTokens):
+def extractStatusV2Android(url,workaroundTokens,tlLanguage=None):
# get tweet ID
m = re.search(pathregex, url)
if m is None:
@@ -335,7 +338,7 @@ def extractStatusV2Android(url,workaroundTokens):
try:
vars = json.loads('{"referrer":"home","includeTweetImpression":true,"includeHasBirdwatchNotes":false,"isReaderMode":false,"includeEditPerspective":false,"includeEditControl":true,"focalTweetId":0,"includeCommunityTweetRelationship":true,"includeTweetVisibilityNudge":true}')
vars['focalTweetId'] = int(twid)
- tweet = twitterApiGet(f"https://x.com/i/api/graphql/{androidGraphql_api}/ConversationTimelineV2?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(androidGraphqlFeatures)}", authToken=authToken,btoken=androidBearer,userAgent="TwitterAndroid/11.61.0-release.0 (311610000-r-0) G011A/9 (google;G011A;google;G011A;0;;1;2016)")
+ tweet = twitterApiGet(f"https://x.com/i/api/graphql/{androidGraphql_api}/ConversationTimelineV2?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(androidGraphqlFeatures)}", authToken=authToken,btoken=androidBearer,userAgent="TwitterAndroid/11.61.0-release.0 (311610000-r-0) G011A/9 (google;G011A;google;G011A;0;;1;2016)",language=tlLanguage)
try:
rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining")
print(f"Twitter Android Token Rate limit remaining: {rateLimitRemaining}")
@@ -380,7 +383,7 @@ def extractStatusV2Android(url,workaroundTokens):
return tweet
return parallel_token_request(twid, tokens, request_with_token)
-def extractStatusV2TweetDetail(url,workaroundTokens):
+def extractStatusV2TweetDetail(url,workaroundTokens,tlLanguage=None):
# get tweet ID
m = re.search(pathregex, url)
if m is None:
@@ -395,7 +398,7 @@ def extractStatusV2TweetDetail(url,workaroundTokens):
try:
vars = json.loads('{"focalTweetId":"0","with_rux_injections":false,"includePromotedContent":true,"withCommunity":true,"withQuickPromoteEligibilityTweetFields":true,"withBirdwatchNotes":true,"withVoice":true,"withV2Timeline":true}')
vars['focalTweetId'] = str(twid)
- tweet = twitterApiGet(f"https://x.com/i/api/graphql/{tweetDetailGraphql_api}/TweetDetail?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(tweetDetailGraphqlFeatures)}", authToken=authToken,btoken=v2bearer)
+ tweet = twitterApiGet(f"https://x.com/i/api/graphql/{tweetDetailGraphql_api}/TweetDetail?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(tweetDetailGraphqlFeatures)}", authToken=authToken,btoken=v2bearer,language=tlLanguage)
try:
rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining")
print(f"Twitter Token Rate limit remaining: {rateLimitRemaining}")
@@ -440,10 +443,10 @@ def extractStatusV2TweetDetail(url,workaroundTokens):
return tweet
return parallel_token_request(twid, tokens, request_with_token)
-def extractStatusV2Rest_Anon(url,workaroundTokens):
- return extractStatusV2Rest(url,None)
+def extractStatusV2Rest_Anon(url,workaroundTokens,tlLanguage=None):
+ return extractStatusV2Rest(url,None,tlLanguage=tlLanguage)
-def extractStatusV2Rest(url,workaroundTokens):
+def extractStatusV2Rest(url,workaroundTokens,tlLanguage=None):
# get tweet ID
m = re.search(pathregex, url)
if m is None:
@@ -461,11 +464,11 @@ def extractStatusV2Rest(url,workaroundTokens):
random.shuffle(tokens)
for authToken in tokens:
try:
- tweet = twitterApiGet(f"https://x.com/i/api/graphql/{v2AnonGraphql_api}/TweetResultByRestId?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(v2AnonFeatures)}", btoken=v2bearer,authToken=authToken,guestToken=guestToken)
+ tweet = twitterApiGet(f"https://x.com/i/api/graphql/{v2AnonGraphql_api}/TweetResultByRestId?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(v2AnonFeatures)}", btoken=v2bearer,authToken=authToken,guestToken=guestToken,language=tlLanguage)
except Exception as e:
continue
else:
- tweet = twitterApiGet(f"https://x.com/i/api/graphql/{v2AnonGraphql_api}/TweetResultByRestId?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(v2AnonFeatures)}", btoken=v2bearer,guestToken=guestToken)
+ tweet = twitterApiGet(f"https://x.com/i/api/graphql/{v2AnonGraphql_api}/TweetResultByRestId?variables={urllib.parse.quote(json.dumps(vars))}&features={urllib.parse.quote(v2AnonFeatures)}", btoken=v2bearer,guestToken=guestToken,language=tlLanguage)
try:
rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining")
@@ -512,12 +515,13 @@ def fixTweetData(tweet):
pass
return tweet
-def extractStatus(url,workaroundTokens=None):
- # TODO: commented out methods are too slow/unreliable at the moment
+def extractStatus(url,workaroundTokens=None,tlLanguage=None):
methods=[extractStatusV2Rest_Anon,extractStatusV2,extractStatusV2Rest,extractStatusV2Android]#,extractStatusV2TweetDetail]
+ if tlLanguage is not None: # prioritize endpoints that return translations
+ methods = [extractStatusV2Rest,extractStatusV2TweetDetail,extractStatusV2Android,extractStatusV2,extractStatusV2Rest_Anon]
for method in methods:
try:
- result = method(url,workaroundTokens)
+ result = method(url,workaroundTokens,tlLanguage=tlLanguage)
if 'legacy' not in result:
print(f"{method.__name__} method failed: Legacy not found for {url}")
# try another method
diff --git a/twitfix.py b/twitfix.py
index f6860c9..13f7a9a 100644
--- a/twitfix.py
+++ b/twitfix.py
@@ -14,7 +14,7 @@ import msgs
import twExtract as twExtract
from cache import addVnfToLinkCache,getVnfFromLinkCache
import vxlogging as log
-from utils import getTweetIdFromUrl, pathregex, determineMediaToEmbed, determineEmbedTweet, BytesIOWrapper, fixMedia
+from utils import getTweetIdFromUrl, pathregex, determineMediaToEmbed, determineEmbedTweet, BytesIOWrapper, fixMedia, indexOfAny
from vxApi import getApiResponse, getApiUserResponse
from urllib.parse import urlparse
from PyRTF.Elements import Document
@@ -22,12 +22,14 @@ from PyRTF.document.section import Section
from PyRTF.document.paragraph import Paragraph
from copy import deepcopy
import json
-import datetime
+import translation
import activity as activitymg
app = Flask(__name__)
CORS(app)
user_agent=""
+activityseparator="zqj"
+
staticFiles = { # TODO: Use flask static files instead of this
"favicon.ico": {"mime": "image/vnd.microsoft.icon","path": "favicon.ico"},
"apple-touch-icon.png": {"mime": "image/png","path": "apple-touch-icon.png"},
@@ -82,7 +84,11 @@ def generateActivityLink(tweetData,media=None,mediatype=None,embedIndex=-1):
return None
try:
embedIndex = embedIndex+1
- return f"{config['config']['url']}/users/{tweetData['user_screen_name']}/statuses/{str(embedIndex)}{tweetData['tweetID']}"
+
+ extras=""
+ if tweetData["translation"] is not None:
+ extras+=activityseparator+tweetData["translation"]["destination_language"]
+ return f"{config['config']['url']}/users/{tweetData['user_screen_name']}/statuses/{str(embedIndex)}{activityseparator}{tweetData['tweetID']}{extras}"
except Exception as e:
log.error("Error generating activity link: "+str(e))
return None
@@ -95,7 +101,7 @@ def getAppName(tweetData,appnameSuffix=""):
def renderImageTweetEmbed(tweetData,image,appnameSuffix="",embedIndex=-1):
qrt = tweetData['qrt']
- embedDesc = msgs.formatEmbedDesc("Image",tweetData['text'],qrt,tweetData['pollData'])
+ embedDesc = msgs.formatEmbedDesc("Image",tweetData['text'],qrt,tweetData['pollData'],tweetData["translation"])
if image.startswith("https://pbs.twimg.com") and "?" not in image:
image = f"{image}?name=orig"
@@ -115,7 +121,7 @@ def renderImageTweetEmbed(tweetData,image,appnameSuffix="",embedIndex=-1):
def renderVideoTweetEmbed(tweetData,mediaInfo,appnameSuffix="",embedIndex=-1):
qrt = tweetData['qrt']
- embedDesc = msgs.formatEmbedDesc("Video",tweetData['text'],qrt,tweetData['pollData'])
+ embedDesc = msgs.formatEmbedDesc("Video",tweetData['text'],qrt,tweetData['pollData'],tweetData["translation"])
mediaInfo=fixMedia(mediaInfo)
@@ -138,7 +144,7 @@ def renderVideoTweetEmbed(tweetData,mediaInfo,appnameSuffix="",embedIndex=-1):
def renderTextTweetEmbed(tweetData,appnameSuffix=""):
qrt = tweetData['qrt']
- embedDesc = msgs.formatEmbedDesc("Text",tweetData['text'],qrt,tweetData['pollData'])
+ embedDesc = msgs.formatEmbedDesc("Text",tweetData['text'],qrt,tweetData['pollData'],tweetData["translation"])
return render_template("text.html",
tweet=tweetData,
@@ -154,7 +160,7 @@ def renderTextTweetEmbed(tweetData,appnameSuffix=""):
def renderArticleTweetEmbed(tweetData,appnameSuffix=""):
articlePreview=tweetData['article']["title"]+"\n\n"+tweetData['article']["preview_text"]+"…"
- embedDesc = msgs.formatEmbedDesc("Image",articlePreview,None,None)
+ embedDesc = msgs.formatEmbedDesc("Image",articlePreview,None,None,None)
return render_template("image.html",
tweet=tweetData,
@@ -260,9 +266,9 @@ def userJson():
},
}
-def getTweetData(twitter_url,include_txt="false",include_rtf="false"):
+def getTweetData(twitter_url,include_txt="false",include_rtf="false",tlLanguage=None):
cachedVNF = getVnfFromLinkCache(twitter_url)
- if cachedVNF is not None and include_txt == "false" and include_rtf == "false":
+ if cachedVNF is not None and include_txt == "false" and include_rtf == "false" and tlLanguage is None:
return cachedVNF
try:
@@ -270,7 +276,7 @@ def getTweetData(twitter_url,include_txt="false",include_rtf="false"):
workaroundTokens = config['config']['workaroundTokens'].split(",")
else:
workaroundTokens = None
- rawTweetData = twExtract.extractStatus(twitter_url,workaroundTokens=workaroundTokens)
+ rawTweetData = twExtract.extractStatus(twitter_url,workaroundTokens=workaroundTokens,tlLanguage=tlLanguage)
except:
rawTweetData = None
if rawTweetData == None or 'error' in rawTweetData:
@@ -281,7 +287,15 @@ def getTweetData(twitter_url,include_txt="false",include_rtf="false"):
tweetData = getApiResponse(rawTweetData,include_txt,include_rtf)
if tweetData is None:
return None
- if include_txt == "false" and include_rtf == "false":
+
+ if tweetData["translation"] is None and tlLanguage is not None and tlLanguage != tweetData["lang"]:
+ # translation probably failed on twitter's end
+ pass # TODO: implement DeepL api
+
+ if tweetData["translation"] is not None and tlLanguage is None:
+ tweetData["translation"] = None # didn't ask for TL
+
+ if include_txt == "false" and include_rtf == "false" and tlLanguage is None:
addVnfToLinkCache(twitter_url,tweetData)
return tweetData
@@ -307,6 +321,7 @@ def getUserData(twitter_url,includeFeed=False):
@app.route('/') # Default endpoint used by everything
def twitfix(sub_path):
+ splitPath=sub_path.split("/")
global user_agent
user_agent = request.headers.get('User-Agent', None)
if user_agent is None:
@@ -355,24 +370,33 @@ def twitfix(sub_path):
include_txt="false"
include_rtf="false"
+
+ translationIndex=indexOfAny(splitPath,translation.mergedLangs,caseInsensitive=True)
+ translationLang = None
+ if translationIndex != -1:
+ if splitPath[translationIndex].upper() in translation.languageAliases:
+ translationLang = translation.languageAliases[splitPath[translationIndex].upper()]
+ else:
+ translationLang = splitPath[translationIndex].lower()
+
if isApiRequest:
if "include_txt" in request.args:
include_txt = request.args.get("include_txt")
if "include_rtf" in request.args:
include_rtf = request.args.get("include_rtf")
- tweetData = getTweetData(twitter_url,include_txt,include_rtf)
+ tweetData = getTweetData(twitter_url,include_txt,include_rtf,tlLanguage=translationLang)
if tweetData is None:
log.error("Tweet Data Get failed for "+twitter_url)
return message(msgs.failedToScan)
qrt = None
if 'qrtURL' in tweetData and tweetData['qrtURL'] is not None:
- qrt = getTweetData(tweetData['qrtURL'])
+ qrt = getTweetData(tweetData['qrtURL'],tlLanguage=translationLang)
tweetData['qrt'] = qrt
retweet = None
if 'retweetURL' in tweetData and tweetData['retweetURL'] is not None:
- retweet = getTweetData(tweetData['retweetURL'])
+ retweet = getTweetData(tweetData['retweetURL'],tlLanguage=translationLang)
tweetData['retweet'] = retweet
tweetData = deepcopy(tweetData)
@@ -403,9 +427,10 @@ def twitfix(sub_path):
return send_file(BytesIOWrapper(rtf), mimetype='application/rtf', as_attachment=True, download_name=f'{tweetData["user_screen_name"]}_{tweetData["tweetID"]}.rtf')
embedIndex = -1
- # if url ends with /1, /2, /3, or /4, we'll use that as the index
- if sub_path[-2:] in ["/1","/2","/3","/4"]:
- embedIndex = int(sub_path[-1])-1
+ # if url has /1, /2, /3, or /4, we'll use that as the index
+ numIdx=indexOfAny(splitPath,["1","2","3","4"]);
+ if numIdx != -1:
+ embedIndex = int(splitPath[numIdx])-1
sub_path = sub_path[:-2]
if isApiRequest: # Directly return the API response if the request is from the API
@@ -484,21 +509,29 @@ def rendercombined():
@app.route("/api/v1/statuses/")
def api_v1_status(tweet_id):
- embedIndex = int(tweet_id[0])-1
- tweet_id = int(tweet_id[1:])
+ lang = None
+ if activityseparator not in tweet_id: #old embeds
+ embedIndex = int(tweet_id[0])-1
+ tweet_id = int(tweet_id[1:])
+ else:
+ args = tweet_id.split(activityseparator)
+ embedIndex = int(args[0])
+ tweet_id = int(args[1])
+ if len(args)>2:
+ lang=args[2]
twitter_url=f"https://twitter.com/i/status/{tweet_id}"
- tweetData = getTweetData(twitter_url)
+ tweetData = getTweetData(twitter_url,tlLanguage=lang)
if tweetData is None:
log.error("Tweet Data Get failed for "+twitter_url)
return message(msgs.failedToScan)
qrt = None
if 'qrtURL' in tweetData and tweetData['qrtURL'] is not None:
- qrt = getTweetData(tweetData['qrtURL'])
+ qrt = getTweetData(tweetData['qrtURL'],tlLanguage=lang)
tweetData['qrt'] = qrt
retweet = None
if 'retweetURL' in tweetData and tweetData['retweetURL'] is not None:
- retweet = getTweetData(tweetData['retweetURL'])
+ retweet = getTweetData(tweetData['retweetURL'],tlLanguage=lang)
tweetData['retweet'] = retweet
if tweetData is None:
diff --git a/utils.py b/utils.py
index 037bc04..7dd66b7 100644
--- a/utils.py
+++ b/utils.py
@@ -91,4 +91,15 @@ def determineMediaToEmbed(tweetData,embedIndex = -1,convertGif = True):
media['url'] = gcApi + "/convert.avif?url=" + vurl
suffix += " • GIF"
media["suffix"] = suffix
- return media
\ No newline at end of file
+ return media
+
+def indexOfAny(list,items,caseInsensitive=False):
+ if caseInsensitive:
+ list = [item.upper() for item in list]
+ items = [item.upper() for item in items]
+ for item in items:
+ try:
+ return list.index(item)
+ except:
+ continue
+ return -1
\ No newline at end of file
diff --git a/vxApi.py b/vxApi.py
index 69d7ce3..6b2eaad 100644
--- a/vxApi.py
+++ b/vxApi.py
@@ -77,6 +77,7 @@ def getApiResponse(tweet,include_txt=False,include_rtf=False):
oldTweetVersion = False
tweetArticle=None
lang=None
+ translation=None
if "screen_name" not in userL:
userL["screen_name"] = user["core"]["screen_name"]
@@ -85,6 +86,14 @@ def getApiResponse(tweet,include_txt=False,include_rtf=False):
if "profile_image_url_https" not in userL:
userL["profile_image_url_https"] = user["avatar"]["image_url"]
+ if "grok_translated_post_with_availability" in tweet and tweet["grok_translated_post_with_availability"]["is_available"] and "data" in tweet["grok_translated_post_with_availability"]:
+ tlData = tweet["grok_translated_post_with_availability"]["data"]
+ translation = {
+ "source_language":tlData["source_language"],
+ "destination_language":tlData["destination_language"],
+ "text":tlData["translation"]
+ }
+
#editedTweet=False
try:
if "birdwatch_pivot" in tweet:
@@ -305,6 +314,7 @@ def getApiResponse(tweet,include_txt=False,include_rtf=False):
"replyingToID": replyingToID,
"fetched_on": int(datetime.now().timestamp()),
"retweetURL":retweetURL,
+ "translation":translation
}
try:
apiObject["date_epoch"] = int(datetime.strptime(tweetL["created_at"], "%a %b %d %H:%M:%S %z %Y").timestamp())