Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70487edc0b | ||
|
|
e9ef52b4e9 | ||
|
|
c1d3a7e2dd | ||
|
|
161cd4d4e2 | ||
|
|
6ac40e4a52 | ||
|
|
f06ef199bb | ||
|
|
662491f1b4 | ||
|
|
0e3d24ad55 | ||
|
|
fb1fdaf4b5 | ||
|
|
7c7a6e6a59 | ||
|
|
1a6f424e69 | ||
|
|
80a12a4b63 | ||
|
|
23d0564129 | ||
|
|
f7dddc42c1 | ||
|
|
f849d24779 | ||
|
|
656bcd40b9
|
||
|
|
061e7fb96e |
No files matched your search
@@ -71,3 +71,4 @@ jobs:
|
|||||||
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 }}
|
||||||
@@ -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
@@ -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>"
|
||||||
|
|||||||
+5
-5
@@ -2,12 +2,12 @@
|
|||||||
"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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -205,3 +205,19 @@ def test_embed_action():
|
|||||||
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
|
||||||
@@ -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
|
||||||
+25
-26
@@ -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)
|
|
||||||
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"]
|
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+67
-24
@@ -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):
|
||||||
|
lang = None
|
||||||
|
if activityseparator not in tweet_id: #old embeds
|
||||||
embedIndex = int(tweet_id[0])-1
|
embedIndex = int(tweet_id[0])-1
|
||||||
tweet_id = int(tweet_id[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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -92,3 +92,14 @@ def determineMediaToEmbed(tweetData,embedIndex = -1,convertGif = True):
|
|||||||
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
|
||||||
@@ -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())
|
||||||
|
|||||||
Reference in new issue
Block a user