Compare commits

...
16 Commits
Author SHA1 Message Date
Future e9ef52b4e9 Merge branch 'main' of https://github.com/dylanpdx/BetterTwitFix into update
Run Tests / build (push) Failing after 1m5s
2026-09-01 13:52:40 +02:00
Dylan c1d3a7e2dd Fix HTML escape issues 2026-07-15 14:12:05 +01:00
Dylan 161cd4d4e2 Fix feed endpoints 2026-06-24 00:23:52 +01:00
Dylan 6ac40e4a52 Fix guest token fetch and restore original method 2026-06-23 22:28:55 +01:00
Dylan f06ef199bb Switch to syndication for anon extract 2026-06-23 18:19:17 +01:00
Dylan 662491f1b4 Add subdomains as input for translation #273 2026-04-19 15:27:34 +01:00
Dylan 0e3d24ad55 Fix for #305 2026-04-19 13:40:26 +01:00
Dylan fb1fdaf4b5 Fix reverted change 2026-04-19 01:39:52 +01:00
Dylan 7c7a6e6a59 Finish translation implementation 2026-04-18 23:50:09 +01:00
Dylan 1a6f424e69 Initial translation implementation #273 2026-04-18 20:11:03 +01:00
Future 80a12a4b63 Merge pull request 'November update' (#2) from github/origin into alterware
Run Tests / build (push) Failing after 1m13s
Reviewed-on: #2
2025-11-10 07:43:07 +00:00
Future 23d0564129 Merge branch 'main' of https://github.com/dylanpdx/BetterTwitFix into github/origin
Run Tests / build (push) Failing after 2m11s
2025-11-10 08:40:46 +01:00
Future f7dddc42c1 Merge pull request 'chore: update my branch' (#1) from update/alterware into alterware
Run Tests / build (push) Failing after 1m31s
Reviewed-on: #1
2025-06-02 09:29:43 +00:00
alterware f849d24779 Merge branch 'main' into alterware
Run Tests / build (push) Failing after 2m47s
2025-06-02 09:25:02 +00:00
Future 656bcd40b9 build: test CI here
Run Tests / build (push) Failing after 1m28s
2025-05-20 22:05:26 +02:00
alterware 061e7fb96e alterware: our changes
Run Tests / build (3.10) (push) Failing after 1m8s
Run Tests / build (3.11) (push) Failing after 9s
Run Tests / build (3.12) (push) Failing after 8s
2025-05-20 19:37:44 +00:00
16 changed files with 224 additions and 87 deletions

No files matched your search

+2 -1
View File
@@ -70,4 +70,5 @@ jobs:
VXTWITTER_GIF_CONVERT_API: ${{ secrets.VXTWITTER_GIF_CONVERT_API }} VXTWITTER_GIF_CONVERT_API: ${{ secrets.VXTWITTER_GIF_CONVERT_API }}
VXTWITTER_WORKAROUND_TOKENS: ${{ secrets.VXTWITTER_WORKAROUND_TOKENS }} VXTWITTER_WORKAROUND_TOKENS: ${{ secrets.VXTWITTER_WORKAROUND_TOKENS }}
VXTWITTER_PROXIES: ${{ secrets.VXTWITTER_PROXIES }} VXTWITTER_PROXIES: ${{ secrets.VXTWITTER_PROXIES }}
SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }} SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }}
DEEPL_API_KEY: ${{ secrets.DEEPL_API_KEY }}
+17 -10
View File
@@ -6,23 +6,30 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: test environment: test
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps: steps:
- uses: actions/checkout@v3 - name: Check out files
- name: Set up Python ${{ matrix.python-version }} uses: actions/checkout@v4
uses: actions/setup-python@v4
with: - name: Install Python
python-version: ${{ matrix.python-version }} run: |
apt-get update
apt-get install -y python3 python3-pip python3-venv
- name: Set up virtual environment
run: |
python3 -m venv venv
. venv/bin/activate
python -m pip install --upgrade pip
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip . venv/bin/activate
pip install pytest pytest-cov pytest-env pip install pytest pytest-cov pytest-env
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with pytest - name: Test with pytest
env: env:
VXTWITTER_WORKAROUND_TOKENS: ${{ secrets.VXTWITTER_WORKAROUND_TOKENS }} VXTWITTER_WORKAROUND_TOKENS: ${{ secrets.VXTWITTER_WORKAROUND_TOKENS }}
run: | run: |
. venv/bin/activate
pytest --cov-config=.coveragerc --cov=. pytest --cov-config=.coveragerc --cov=.
+12 -2
View File
@@ -2,17 +2,27 @@ import datetime
import msgs import msgs
from utils import determineEmbedTweet, determineMediaToEmbed from utils import determineEmbedTweet, determineMediaToEmbed
from copy import deepcopy from copy import deepcopy
import html
def tweetDataToActivity(tweetData,embedIndex = -1): def tweetDataToActivity(tweetData,embedIndex = -1):
content="" content=""
if tweetData['replyingTo'] is not None: if tweetData['replyingTo'] is not None:
content += f"<blockquote>↪️ <i>Replying to @{tweetData['replyingTo']}</i></blockquote>" content += f"<blockquote>↪️ <i>Replying to @{tweetData['replyingTo']}</i></blockquote>"
content+=f"<p>{tweetData['text']}</p>"
mainText = html.escape(tweetData['text'])
if tweetData["translation"] is not None:
content += f"<blockquote>🌐 <i>{tweetData['translation']['source_language'].upper()}→{tweetData['translation']['destination_language'].upper()}</i></blockquote>"
mainText=html.escape(tweetData['translation']["text"])
content+=f"<p>{mainText}</p>"
attachments=[] attachments=[]
if tweetData['qrt'] is not None: if tweetData['qrt'] is not None:
content += f"<blockquote><b>QRT: <a href=\"{tweetData['qrtURL']}\">{tweetData['qrt']['user_screen_name']}</a></b><br>{tweetData['qrt']['text']}</blockquote>" qrtText = tweetData['qrt']['text']
if "translation" in tweetData['qrt'] and tweetData['qrt']["translation"] is not None:
qrtText = tweetData['qrt']["translation"]["text"]
content += f"<blockquote><b>QRT: <a href=\"{tweetData['qrtURL']}\">{tweetData['qrt']['user_screen_name']}</a></b><br>{qrtText}</blockquote>"
if tweetData['pollData'] is not None: if tweetData['pollData'] is not None:
content += f"<p>{msgs.genPollDisplay(tweetData['pollData'])}</p>" content += f"<p>{msgs.genPollDisplay(tweetData['pollData'])}</p>"
content += "</p>" content += "</p>"
+7 -7
View File
@@ -2,14 +2,14 @@
"config": { "config": {
"appname": "vxTwitter", "appname": "vxTwitter",
"color": "#1DA1F2", "color": "#1DA1F2",
"database": "[url to mongo database goes here]", "database": "mongodb://localhost:27017/bettervxtwitter",
"link_cache": "ram", "link_cache": "db",
"method": "hybrid", "method": "hybrid",
"repo": "https://github.com/dylanpdx/BetterTwitFix", "repo": "https://git.alterware.dev/alterware/BetterTwitFix",
"table": "[database table here]", "table": "links",
"url": "https://vxtwitter.com", "url": "https://girlcock.alterware.dev",
"combination_method": "local", "combination_method": "local",
"gifConvertAPI": "local", "gifConvertAPI": "local",
"workaroundTokens":null "workaroundTokens": null
} }
} }
+4 -2
View File
@@ -13,7 +13,8 @@ if ('RUNNING_SERVERLESS' in os.environ and os.environ['RUNNING_SERVERLESS'] == '
"url": os.getenv("VXTWITTER_URL","https://vxtwitter.com"), "url": os.getenv("VXTWITTER_URL","https://vxtwitter.com"),
"combination_method": os.getenv("VXTWITTER_COMBINATION_METHOD","local"), # can either be 'local' or a URL to a server handling requests in the same format "combination_method": os.getenv("VXTWITTER_COMBINATION_METHOD","local"), # can either be 'local' or a URL to a server handling requests in the same format
"gifConvertAPI":os.getenv("VXTWITTER_GIF_CONVERT_API",""), "gifConvertAPI":os.getenv("VXTWITTER_GIF_CONVERT_API",""),
"workaroundTokens":os.getenv("VXTWITTER_WORKAROUND_TOKENS",None) "workaroundTokens":os.getenv("VXTWITTER_WORKAROUND_TOKENS",None),
"deeplKey":os.getenv("DEEPL_API_KEY",None),
} }
} }
else: else:
@@ -31,7 +32,8 @@ else:
"url": "https://vxtwitter.com", "url": "https://vxtwitter.com",
"combination_method": "local", # can either be 'local' or a URL to a server handling requests in the same format "combination_method": "local", # can either be 'local' or a URL to a server handling requests in the same format
"gifConvertAPI":"", "gifConvertAPI":"",
"workaroundTokens":None "workaroundTokens":None,
"deeplKey":None
} }
} }
+12 -3
View File
@@ -17,8 +17,11 @@ def genLikesDisplay(vnf):
return ("💖 " + numerize.numerize(vnf['likes'])) return ("💖 " + numerize.numerize(vnf['likes']))
def genQrtDisplay(qrt): def genQrtDisplay(qrt):
text = qrt['text']
if "translation" in qrt and qrt["translation"] is not None:
text = qrt["translation"]["text"]
verifiedCheck = "☑️" if ('verified' in qrt and qrt['verified']) else "" 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): def genPollDisplay(poll):
pctSplit=10 pctSplit=10
@@ -27,6 +30,9 @@ def genPollDisplay(poll):
output+=choice["name"]+"\n"+("█"*int(choice["percent"]/pctSplit)) +" "+str(choice["percent"])+"%\n" output+=choice["name"]+"\n"+("█"*int(choice["percent"]/pctSplit)) +" "+str(choice["percent"])+"%\n"
return output 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 # formats the top text of the embed
def formatProvider(base,vnf): def formatProvider(base,vnf):
finalText = base finalText = base
@@ -38,13 +44,16 @@ def formatProvider(base,vnf):
finalText = base finalText = base
return finalText 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 # Trim the embed description to 248 characters, prioritizing poll and likes
qrtType=None qrtType=None
if qrt!=None: if qrt!=None:
qrtType="Text" qrtType="Text"
if translation is not None:
body = genTranslationDisplay(translation)
limit = videoDescLimit if type=="Video" or (qrt!=None and (qrtType=="Video")) else tweetDescLimit limit = videoDescLimit if type=="Video" or (qrt!=None and (qrtType=="Video")) else tweetDescLimit
output = "" output = ""
@@ -73,6 +82,6 @@ def formatEmbedDesc(type,body,qrt,pollData):
diff = len(output)-limit diff = len(output)-limit
# remove the characters from body, add ellipsis # remove the characters from body, add ellipsis
body = body[:-(diff+1)]+"…" body = body[:-(diff+1)]+"…"
return formatEmbedDesc(type,body,qrt,pollData) return formatEmbedDesc(type,body,qrt,pollData,None)
else: else:
return output return output
+2 -1
View File
@@ -8,4 +8,5 @@ Werkzeug==2.3.7
numerize==0.12 numerize==0.12
oauthlib==3.2.2 oauthlib==3.2.2
PyRTF3==0.47.5 PyRTF3==0.47.5
XClientTransaction==0.0.2 XClientTransaction==1.0.2
deepl==1.3.0
+1
View File
@@ -29,6 +29,7 @@ provider:
VXTWITTER_COMBINATION_METHOD: ${env:VXTWITTER_COMBINATION_METHOD, 'local'} VXTWITTER_COMBINATION_METHOD: ${env:VXTWITTER_COMBINATION_METHOD, 'local'}
VXTWITTER_GIF_CONVERT_API: ${env:VXTWITTER_GIF_CONVERT_API, ''} VXTWITTER_GIF_CONVERT_API: ${env:VXTWITTER_GIF_CONVERT_API, ''}
VXTWITTER_WORKAROUND_TOKENS: ${env:VXTWITTER_WORKAROUND_TOKENS, ''} VXTWITTER_WORKAROUND_TOKENS: ${env:VXTWITTER_WORKAROUND_TOKENS, ''}
DEEPL_API_KEY: ${env:DEEPL_API_KEY, ''}
#VXTWITTER_PROXIES: ${env:VXTWITTER_PROXIES, ''} #VXTWITTER_PROXIES: ${env:VXTWITTER_PROXIES, ''}
package: package:
+17 -1
View File
@@ -204,4 +204,20 @@ def test_embed_action():
resp = client.get(testTextTweet.replace("https://twitter.com",""),headers={"User-Agent":"Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)"}) resp = client.get(testTextTweet.replace("https://twitter.com",""),headers={"User-Agent":"Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)"})
assert resp.status_code==200 assert resp.status_code==200
assert "application/activity+json" in str(resp.data) assert "application/activity+json" in str(resp.data)
assert "%F0%9F%92%96" not in str(resp.data) # 💖 assert "%F0%9F%92%96" not in str(resp.data) # 💖
def test_embed_translated():
resp = client.get(testTextTweet.replace("https://twitter.com","")+"/jp",headers={"User-Agent":"test"})
assert resp.status_code==200
assert "→" in resp.text
resp = client.get(testTextTweet.replace("https://twitter.com","")+"/ja",headers={"User-Agent":"test"})
assert resp.status_code==200
assert "→" in resp.text
def test_embed_translated_subdomain():
resp = client.get(testTextTweet.replace("https://twitter.com","https://jp.vxtwitter.com"),headers={"User-Agent":"test"})
assert resp.status_code==200
assert "→" in resp.text
resp = client.get(testTextTweet.replace("https://twitter.com","https://ja.vxtwitter.com"),headers={"User-Agent":"test"})
assert resp.status_code==200
assert "→" in resp.text
+27
View File
@@ -0,0 +1,27 @@
import deepl
from configHandler import config
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()))
def getDeeplTranslation(text,target):
target = target.upper()
if target == "EN":
target = "EN-US"
try:
if 'deeplKey' not in config["config"] or config["config"]["deeplKey"] == None:
return None
deepl_client = deepl.Translator(config["config"]["deeplKey"])
result = deepl_client.translate_text(text, target_lang=target)
return {
"text":result.text,
"source_language":result.detected_source_lang,
"destination_language":target
}
except Exception as e:
return None
+26 -27
View File
@@ -14,7 +14,7 @@ bearer="Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DT
v2bearer="Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA" v2bearer="Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
androidBearer="Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F" androidBearer="Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F"
requestUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0" requestUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0"
bearerTokens=[bearer,v2bearer,androidBearer] bearerTokens=[bearer,v2bearer,androidBearer]
@@ -113,7 +113,7 @@ def cycleBearerTokenGet(url,headers):
return tweet return tweet
raise TwExtractError(400, "Extract error") 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|"): if authToken != None and authToken.startswith("oa|"):
url = url.replace("https://x.com/i/api/graphql/","https://api.twitter.com/graphql/") 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] secret = authToken.split("|")[1]
twt = oauth1.Client(client_key='3nVuSoBZnx6U4vzUxf5w',client_secret='Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys',resource_owner_key=key,resource_owner_secret=secret) 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"] del hdr["Authorization"]
hdr["X-Twitter-Client"] = "TwitterAndroid" hdr["X-Twitter-Client"] = "TwitterAndroid"
if userAgent is not None: if userAgent is not None:
@@ -135,12 +135,12 @@ def twitterApiGet(url,btoken=None,authToken=None,guestToken=None,userAgent=None)
if btoken is None: if btoken is None:
btoken = v2bearer btoken = v2bearer
#return cycleBearerTokenGet(url,getAuthHeaders(bearer,authToken=authToken,guestToken=guestToken)) #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) response = requests.get(url, headers=headers)
return response return response
def getAuthHeaders(btoken,authToken=None,guestToken=None): def getAuthHeaders(btoken,authToken=None,guestToken=None,language=None):
csrfToken=str(uuid.uuid4()).replace('-', '') csrfToken=str(uuid.uuid4()).replace('-', '')
headers = {"x-twitter-active-user":"yes","x-twitter-client-language":"en","x-csrf-token":csrfToken,"User-Agent":requestUserAgent} headers = {"x-twitter-active-user":"yes","x-twitter-client-language":"en","x-csrf-token":csrfToken,"User-Agent":requestUserAgent}
@@ -152,19 +152,17 @@ def getAuthHeaders(btoken,authToken=None,guestToken=None):
if guestToken is not None: if guestToken is not None:
headers["x-guest-token"] = guestToken headers["x-guest-token"] = guestToken
if language is not None:
headers["x-twitter-client-language"] = language
return headers return headers
def getGuestToken(): def getGuestToken():
global guestToken global guestToken
global guestTokenUses global guestTokenUses
if guestToken is None: if guestToken is None:
r = requests.get(f"https://{twitterUrl}",headers={"User-Agent":requestUserAgent,"Cookie":"night_mode=2"},allow_redirects=False) r = requests.post(f"https://api.{twitterUrl}/1.1/guest/activate.json", headers={"Authorization":v2bearer,"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0"})
m = re.search(gt_pattern, r.text) guestToken = json.loads(r.text)["guest_token"]
if m is None:
r = requests.post(f"https://api.{twitterUrl}/1.1/guest/activate.json", headers={"Authorization":bearer})
guestToken = json.loads(r.text)["guest_token"]
else:
guestToken = m.group(1)
guestTokenUses = 0 guestTokenUses = 0
else: else:
guestTokenUses+=1 guestTokenUses+=1
@@ -216,7 +214,7 @@ def extractStatus_guestToken(url):
raise TwExtractError(error["code"], error["message"]) raise TwExtractError(error["code"], error["message"])
return output return output
def extractStatus_syndication(url,workaroundTokens=None): def extractStatus_syndication(url,workaroundTokens=None,tlLanguage=None):
# https://github.com/mikf/gallery-dl/blob/46cae04aa3a113c7b6bbee1bb468669564b14ae8/gallery_dl/extractor/twitter.py#L1784 # https://github.com/mikf/gallery-dl/blob/46cae04aa3a113c7b6bbee1bb468669564b14ae8/gallery_dl/extractor/twitter.py#L1784
m = re.search(pathregex, url) m = re.search(pathregex, url)
if m is None: if m is None:
@@ -268,7 +266,7 @@ def extractStatus_twExtractProxy(url,workaroundTokens=None):
continue continue
return output return output
def extractStatusV2(url,workaroundTokens): def extractStatusV2(url,workaroundTokens,tlLanguage=None):
# get tweet ID # get tweet ID
m = re.search(pathregex, url) m = re.search(pathregex, url)
if m is None: if m is None:
@@ -282,7 +280,7 @@ def extractStatusV2(url,workaroundTokens):
def request_with_token(twid, authToken): 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 = json.loads('{"includeTweetImpression":true,"includeHasBirdwatchNotes":false,"includeEditPerspective":false,"rest_ids":["x"],"includeEditControl":true,"includeCommunityTweetRelationship":true,"includeTweetVisibilityNudge":true}')
vars['rest_ids'][0] = str(twid) 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: try:
rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining") rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining")
print(f"Twitter Token Rate limit remaining: {rateLimitRemaining}") print(f"Twitter Token Rate limit remaining: {rateLimitRemaining}")
@@ -321,7 +319,7 @@ def extractStatusV2(url,workaroundTokens):
return tweet return tweet
return parallel_token_request(twid, tokens, request_with_token) return parallel_token_request(twid, tokens, request_with_token)
def extractStatusV2Android(url,workaroundTokens): def extractStatusV2Android(url,workaroundTokens,tlLanguage=None):
# get tweet ID # get tweet ID
m = re.search(pathregex, url) m = re.search(pathregex, url)
if m is None: if m is None:
@@ -335,7 +333,7 @@ def extractStatusV2Android(url,workaroundTokens):
try: try:
vars = json.loads('{"referrer":"home","includeTweetImpression":true,"includeHasBirdwatchNotes":false,"isReaderMode":false,"includeEditPerspective":false,"includeEditControl":true,"focalTweetId":0,"includeCommunityTweetRelationship":true,"includeTweetVisibilityNudge":true}') 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) 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: try:
rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining") rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining")
print(f"Twitter Android Token Rate limit remaining: {rateLimitRemaining}") print(f"Twitter Android Token Rate limit remaining: {rateLimitRemaining}")
@@ -380,7 +378,7 @@ def extractStatusV2Android(url,workaroundTokens):
return tweet return tweet
return parallel_token_request(twid, tokens, request_with_token) return parallel_token_request(twid, tokens, request_with_token)
def extractStatusV2TweetDetail(url,workaroundTokens): def extractStatusV2TweetDetail(url,workaroundTokens,tlLanguage=None):
# get tweet ID # get tweet ID
m = re.search(pathregex, url) m = re.search(pathregex, url)
if m is None: if m is None:
@@ -395,7 +393,7 @@ def extractStatusV2TweetDetail(url,workaroundTokens):
try: try:
vars = json.loads('{"focalTweetId":"0","with_rux_injections":false,"includePromotedContent":true,"withCommunity":true,"withQuickPromoteEligibilityTweetFields":true,"withBirdwatchNotes":true,"withVoice":true,"withV2Timeline":true}') 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) 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: try:
rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining") rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining")
print(f"Twitter Token Rate limit remaining: {rateLimitRemaining}") print(f"Twitter Token Rate limit remaining: {rateLimitRemaining}")
@@ -440,10 +438,10 @@ def extractStatusV2TweetDetail(url,workaroundTokens):
return tweet return tweet
return parallel_token_request(twid, tokens, request_with_token) return parallel_token_request(twid, tokens, request_with_token)
def extractStatusV2Rest_Anon(url,workaroundTokens): def extractStatusV2Rest_Anon(url,workaroundTokens,tlLanguage=None):
return extractStatusV2Rest(url,None) return extractStatusV2Rest(url,None,tlLanguage=tlLanguage)
def extractStatusV2Rest(url,workaroundTokens): def extractStatusV2Rest(url,workaroundTokens,tlLanguage=None):
# get tweet ID # get tweet ID
m = re.search(pathregex, url) m = re.search(pathregex, url)
if m is None: if m is None:
@@ -461,11 +459,11 @@ def extractStatusV2Rest(url,workaroundTokens):
random.shuffle(tokens) random.shuffle(tokens)
for authToken in tokens: for authToken in tokens:
try: 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: except Exception as e:
continue continue
else: 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: try:
rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining") rateLimitRemaining = tweet.headers.get("x-rate-limit-remaining")
@@ -512,12 +510,13 @@ def fixTweetData(tweet):
pass pass
return tweet return tweet
def extractStatus(url,workaroundTokens=None): def extractStatus(url,workaroundTokens=None,tlLanguage=None):
# TODO: commented out methods are too slow/unreliable at the moment
methods=[extractStatusV2Rest_Anon,extractStatusV2,extractStatusV2Rest,extractStatusV2Android]#,extractStatusV2TweetDetail] 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: for method in methods:
try: try:
result = method(url,workaroundTokens) result = method(url,workaroundTokens,tlLanguage=tlLanguage)
if 'legacy' not in result: if 'legacy' not in result:
print(f"{method.__name__} method failed: Legacy not found for {url}") print(f"{method.__name__} method failed: Legacy not found for {url}")
# try another method # try another method
+1 -1
View File
@@ -52,7 +52,7 @@ def get_twitter_homepage(headers=None):
def generate_transaction_id(method: str, path: str,headers=None) -> str: def generate_transaction_id(method: str, path: str,headers=None) -> str:
session = requests.Session() session = requests.Session()
session.headers = generate_headers() session.headers = generate_headers()
home_page = session.get(url="https://x.com") home_page = session.get(url="https://x.com/home")
home_page_response = bs4.BeautifulSoup(home_page.content, 'html.parser') home_page_response = bs4.BeautifulSoup(home_page.content, 'html.parser')
ondemand_file_url = get_ondemand_file_url(response=home_page_response) ondemand_file_url = get_ondemand_file_url(response=home_page_response)
ondemand_file = session.get(url=ondemand_file_url) ondemand_file = session.get(url=ondemand_file_url)
+69 -26
View File
@@ -14,7 +14,7 @@ import msgs
import twExtract as twExtract import twExtract as twExtract
from cache import addVnfToLinkCache,getVnfFromLinkCache from cache import addVnfToLinkCache,getVnfFromLinkCache
import vxlogging as log 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 vxApi import getApiResponse, getApiUserResponse
from urllib.parse import urlparse from urllib.parse import urlparse
from PyRTF.Elements import Document from PyRTF.Elements import Document
@@ -22,12 +22,14 @@ from PyRTF.document.section import Section
from PyRTF.document.paragraph import Paragraph from PyRTF.document.paragraph import Paragraph
from copy import deepcopy from copy import deepcopy
import json import json
import datetime import translation
import activity as activitymg import activity as activitymg
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
user_agent="" user_agent=""
activityseparator="zqj"
staticFiles = { # TODO: Use flask static files instead of this staticFiles = { # TODO: Use flask static files instead of this
"favicon.ico": {"mime": "image/vnd.microsoft.icon","path": "favicon.ico"}, "favicon.ico": {"mime": "image/vnd.microsoft.icon","path": "favicon.ico"},
"apple-touch-icon.png": {"mime": "image/png","path": "apple-touch-icon.png"}, "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 return None
try: try:
embedIndex = embedIndex+1 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: except Exception as e:
log.error("Error generating activity link: "+str(e)) log.error("Error generating activity link: "+str(e))
return None return None
@@ -95,7 +101,7 @@ def getAppName(tweetData,appnameSuffix=""):
def renderImageTweetEmbed(tweetData,image,appnameSuffix="",embedIndex=-1): def renderImageTweetEmbed(tweetData,image,appnameSuffix="",embedIndex=-1):
qrt = tweetData['qrt'] 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: if image.startswith("https://pbs.twimg.com") and "?" not in image:
image = f"{image}?name=orig" image = f"{image}?name=orig"
@@ -115,7 +121,7 @@ def renderImageTweetEmbed(tweetData,image,appnameSuffix="",embedIndex=-1):
def renderVideoTweetEmbed(tweetData,mediaInfo,appnameSuffix="",embedIndex=-1): def renderVideoTweetEmbed(tweetData,mediaInfo,appnameSuffix="",embedIndex=-1):
qrt = tweetData['qrt'] 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) mediaInfo=fixMedia(mediaInfo)
@@ -138,7 +144,7 @@ def renderVideoTweetEmbed(tweetData,mediaInfo,appnameSuffix="",embedIndex=-1):
def renderTextTweetEmbed(tweetData,appnameSuffix=""): def renderTextTweetEmbed(tweetData,appnameSuffix=""):
qrt = tweetData['qrt'] 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", return render_template("text.html",
tweet=tweetData, tweet=tweetData,
@@ -154,7 +160,7 @@ def renderTextTweetEmbed(tweetData,appnameSuffix=""):
def renderArticleTweetEmbed(tweetData,appnameSuffix=""): def renderArticleTweetEmbed(tweetData,appnameSuffix=""):
articlePreview=tweetData['article']["title"]+"\n\n"+tweetData['article']["preview_text"]+"…" 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", return render_template("image.html",
tweet=tweetData, 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) 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 return cachedVNF
try: try:
@@ -270,7 +276,7 @@ def getTweetData(twitter_url,include_txt="false",include_rtf="false"):
workaroundTokens = config['config']['workaroundTokens'].split(",") workaroundTokens = config['config']['workaroundTokens'].split(",")
else: else:
workaroundTokens = None workaroundTokens = None
rawTweetData = twExtract.extractStatus(twitter_url,workaroundTokens=workaroundTokens) rawTweetData = twExtract.extractStatus(twitter_url,workaroundTokens=workaroundTokens,tlLanguage=tlLanguage)
except: except:
rawTweetData = None rawTweetData = None
if rawTweetData == None or 'error' in rawTweetData: 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) tweetData = getApiResponse(rawTweetData,include_txt,include_rtf)
if tweetData is None: if tweetData is None:
return 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
tweetData["translation"] = translation.getDeeplTranslation(tweetData["text"],tlLanguage)
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) addVnfToLinkCache(twitter_url,tweetData)
return tweetData return tweetData
@@ -307,13 +321,19 @@ def getUserData(twitter_url,includeFeed=False):
@app.route('/<path:sub_path>') # Default endpoint used by everything @app.route('/<path:sub_path>') # Default endpoint used by everything
def twitfix(sub_path): def twitfix(sub_path):
splitPath=sub_path.split("/")
if "|" in splitPath[-1]: # discord doesn't strip this when a link is spoilered
splitPath[-1] = splitPath[-1].replace("|","")
subdomain = request.host.rsplit(".",2)[0]
global user_agent global user_agent
user_agent = request.headers.get('User-Agent', None) user_agent = request.headers.get('User-Agent', None)
if user_agent is None: if user_agent is None:
user_agent = "unknown" user_agent = "unknown"
isApiRequest=request.url.startswith("https://api.vx") or request.url.startswith("http://api.vx") isApiRequest=subdomain=="api"
if not isApiRequest and (request.url.startswith("https://l.vx") or request.url.startswith("https://old.vx")) and "Discord" in user_agent: if not isApiRequest and (subdomain=="l" or subdomain=="old") and "Discord" in user_agent:
user_agent = user_agent.replace("Discord","LegacyEmbed") # TODO: Clean up; This is a hacky fix to make the new activity embed not trigger user_agent = user_agent.replace("Discord","LegacyEmbed") # TODO: Clean up; This is a hacky fix to make the new activity embed not trigger
if sub_path in staticFiles: if sub_path in staticFiles:
if 'path' not in staticFiles[sub_path] or staticFiles[sub_path]["path"] == None: if 'path' not in staticFiles[sub_path] or staticFiles[sub_path]["path"] == None:
@@ -355,24 +375,38 @@ def twitfix(sub_path):
include_txt="false" include_txt="false"
include_rtf="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()
elif subdomain.upper() in translation.mergedLangs:
if subdomain.upper() in translation.languageAliases:
translationLang = translation.languageAliases[subdomain.upper()]
else:
translationLang = subdomain.lower()
if isApiRequest: if isApiRequest:
if "include_txt" in request.args: if "include_txt" in request.args:
include_txt = request.args.get("include_txt") include_txt = request.args.get("include_txt")
if "include_rtf" in request.args: if "include_rtf" in request.args:
include_rtf = request.args.get("include_rtf") 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: if tweetData is None:
log.error("Tweet Data Get failed for "+twitter_url) log.error("Tweet Data Get failed for "+twitter_url)
return message(msgs.failedToScan) return message(msgs.failedToScan)
qrt = None qrt = None
if 'qrtURL' in tweetData and tweetData['qrtURL'] is not None: if 'qrtURL' in tweetData and tweetData['qrtURL'] is not None:
qrt = getTweetData(tweetData['qrtURL']) qrt = getTweetData(tweetData['qrtURL'],tlLanguage=translationLang)
tweetData['qrt'] = qrt tweetData['qrt'] = qrt
retweet = None retweet = None
if 'retweetURL' in tweetData and tweetData['retweetURL'] is not 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['retweet'] = retweet
tweetData = deepcopy(tweetData) tweetData = deepcopy(tweetData)
@@ -383,7 +417,7 @@ def twitfix(sub_path):
requestUrlWithoutQuery = request.url requestUrlWithoutQuery = request.url
directEmbed=False directEmbed=False
if requestUrlWithoutQuery.startswith("https://d.vx") or requestUrlWithoutQuery.endswith(".mp4") or requestUrlWithoutQuery.endswith(".png"): if subdomain=="d" or requestUrlWithoutQuery.endswith(".mp4") or requestUrlWithoutQuery.endswith(".png"):
directEmbed = True directEmbed = True
# remove the .mp4 from the end of the URL # remove the .mp4 from the end of the URL
if requestUrlWithoutQuery.endswith(".mp4") or requestUrlWithoutQuery.endswith(".png"): if requestUrlWithoutQuery.endswith(".mp4") or requestUrlWithoutQuery.endswith(".png"):
@@ -403,9 +437,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') return send_file(BytesIOWrapper(rtf), mimetype='application/rtf', as_attachment=True, download_name=f'{tweetData["user_screen_name"]}_{tweetData["tweetID"]}.rtf')
embedIndex = -1 embedIndex = -1
# if url ends with /1, /2, /3, or /4, we'll use that as the index # if url has /1, /2, /3, or /4, we'll use that as the index
if sub_path[-2:] in ["/1","/2","/3","/4"]: numIdx=indexOfAny(splitPath,["1","2","3","4"]);
embedIndex = int(sub_path[-1])-1 if numIdx != -1:
embedIndex = int(splitPath[numIdx])-1
sub_path = sub_path[:-2] sub_path = sub_path[:-2]
if isApiRequest: # Directly return the API response if the request is from the API if isApiRequest: # Directly return the API response if the request is from the API
@@ -484,21 +519,29 @@ def rendercombined():
@app.route("/api/v1/statuses/<string:tweet_id>") @app.route("/api/v1/statuses/<string:tweet_id>")
def api_v1_status(tweet_id): def api_v1_status(tweet_id):
embedIndex = int(tweet_id[0])-1 lang = None
tweet_id = int(tweet_id[1:]) 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])-1
tweet_id = int(args[1])
if len(args)>2:
lang=args[2]
twitter_url=f"https://twitter.com/i/status/{tweet_id}" twitter_url=f"https://twitter.com/i/status/{tweet_id}"
tweetData = getTweetData(twitter_url) tweetData = getTweetData(twitter_url,tlLanguage=lang)
if tweetData is None: if tweetData is None:
log.error("Tweet Data Get failed for "+twitter_url) log.error("Tweet Data Get failed for "+twitter_url)
return message(msgs.failedToScan) return message(msgs.failedToScan)
qrt = None qrt = None
if 'qrtURL' in tweetData and tweetData['qrtURL'] is not None: if 'qrtURL' in tweetData and tweetData['qrtURL'] is not None:
qrt = getTweetData(tweetData['qrtURL']) qrt = getTweetData(tweetData['qrtURL'],tlLanguage=lang)
tweetData['qrt'] = qrt tweetData['qrt'] = qrt
retweet = None retweet = None
if 'retweetURL' in tweetData and tweetData['retweetURL'] is not None: if 'retweetURL' in tweetData and tweetData['retweetURL'] is not None:
retweet = getTweetData(tweetData['retweetURL']) retweet = getTweetData(tweetData['retweetURL'],tlLanguage=lang)
tweetData['retweet'] = retweet tweetData['retweet'] = retweet
if tweetData is None: if tweetData is None:
@@ -522,4 +565,4 @@ def oEmbedGen(description, user, video_link, ttype,providerName=None):
if __name__ == "__main__": if __name__ == "__main__":
app.config['SERVER_NAME']='localhost:80' app.config['SERVER_NAME']='localhost:80'
app.run(host='0.0.0.0') app.run(host='0.0.0.0', port=8080)
+5 -5
View File
@@ -3,11 +3,11 @@ Description=Init file for twitfix uwsgi instance
After=network.target After=network.target
[Service] [Service]
User=dylan User=carbonara
Group=dylan Group=carbonara
WorkingDirectory=/home/dylan/BetterTwitFix WorkingDirectory=/home/carbonara/twitter/BetterTwitFix
Environment="PATH=/home/dylan/BetterTwitFix/venv/bin" Environment="PATH=/home/carbonara/twitter/BetterTwitFix/venv/bin"
ExecStart=/home/dylan/BetterTwitFix/venv/bin/uwsgi --ini twitfix.ini ExecStart=/home/carbonara/twitter/BetterTwitFix/venv/bin/uwsgi --ini twitfix.ini
Restart=always Restart=always
RestartSec=3 RestartSec=3
+12 -1
View File
@@ -91,4 +91,15 @@ def determineMediaToEmbed(tweetData,embedIndex = -1,convertGif = True):
media['url'] = gcApi + "/convert.avif?url=" + vurl media['url'] = gcApi + "/convert.avif?url=" + vurl
suffix += " • GIF" suffix += " • GIF"
media["suffix"] = suffix media["suffix"] = suffix
return media 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
+10
View File
@@ -77,6 +77,7 @@ def getApiResponse(tweet,include_txt=False,include_rtf=False):
oldTweetVersion = False oldTweetVersion = False
tweetArticle=None tweetArticle=None
lang=None lang=None
translation=None
if "screen_name" not in userL: if "screen_name" not in userL:
userL["screen_name"] = user["core"]["screen_name"] 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: if "profile_image_url_https" not in userL:
userL["profile_image_url_https"] = user["avatar"]["image_url"] 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 #editedTweet=False
try: try:
if "birdwatch_pivot" in tweet: if "birdwatch_pivot" in tweet:
@@ -305,6 +314,7 @@ def getApiResponse(tweet,include_txt=False,include_rtf=False):
"replyingToID": replyingToID, "replyingToID": replyingToID,
"fetched_on": int(datetime.now().timestamp()), "fetched_on": int(datetime.now().timestamp()),
"retweetURL":retweetURL, "retweetURL":retweetURL,
"translation":translation
} }
try: try:
apiObject["date_epoch"] = int(datetime.strptime(tweetL["created_at"], "%a %b %d %H:%M:%S %z %Y").timestamp()) apiObject["date_epoch"] = int(datetime.strptime(tweetL["created_at"], "%a %b %d %H:%M:%S %z %Y").timestamp())