Compare commits

..

10 Commits

Author SHA1 Message Date
061e7fb96e alterware: our changes
Some checks failed
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
22828a1fc8 fix DB cache 2025-04-22 22:04:17 +02:00
84a19e9baa Add old.vxtwitter as a valid subdomain for legacy embeds 2025-04-16 15:10:43 +01:00
68717be147 Add temp fix for legacy embeds 2025-04-16 14:34:58 +01:00
740a300d1e Fix several small bugs & dead code 2025-03-31 21:33:49 +01:00
dde6f889cb Fix qrt issue 2025-03-31 21:14:21 +01:00
24812acd32 Refactor more; fix Activity url for videos 2025-03-31 20:29:43 +01:00
9452e7be8c Update tests 2025-03-31 01:48:12 +01:00
a851fe587b Misc refactoring; Icons based on media type 2025-03-31 01:36:23 +01:00
6aad6aee7f Use new embed format for easier readability 2025-03-31 01:11:03 +01:00
12 changed files with 327 additions and 83 deletions

77
activity.py Normal file
View File

@ -0,0 +1,77 @@
import datetime
import msgs
from utils import determineEmbedTweet, determineMediaToEmbed
from copy import deepcopy
def tweetDataToActivity(tweetData,embedIndex = -1):
content=""
if tweetData['replyingTo'] is not None:
content += f"<blockquote>↪️ <i>Replying to @{tweetData['replyingTo']}</i></blockquote>"
content+=f"<p>{tweetData['text']}</p>"
attachments=[]
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>"
if tweetData['pollData'] is not None:
content += f"<p>{msgs.genPollDisplay(tweetData['pollData'])}</p>"
content += "</p>"
content = content.replace("\n","<br>")
#if media is not None:
# attachments.append({"type":mediatype,"url":media})
likes = tweetData['likes']
retweets = tweetData['retweets']
# convert date epoch to iso format
date = tweetData['date_epoch']
date = datetime.datetime.fromtimestamp(date).isoformat() + "Z"
embedTweetData = determineEmbedTweet(tweetData)
embeddingMedia = embedTweetData['hasMedia']
media = None
if embeddingMedia:
media = determineMediaToEmbed(embedTweetData,embedIndex)
if media is not None:
media = deepcopy(media)
if media['type'] == "gif":
media['type'] = "gifv"
if 'thumbnail_url' not in media:
media['thumbnail_url'] = media['url']
attachments.append({
"id": "114163769487684704",
"type": media['type'],
"url": media['url'],
"preview_url": media['thumbnail_url'],
})
# https://docs.joinmastodon.org/methods/statuses/
return {
"id": tweetData['tweetID'],
"url": f"https://x.com/{tweetData['user_screen_name']}/status/{tweetData['tweetID']}",
"uri": f"https://x.com/{tweetData['user_screen_name']}/status/{tweetData['tweetID']}",
"created_at": date,
"edited_at": None,
"reblog": None,
"in_reply_to_account_id": None,
"language": "en",
"content": content,
"spoiler_text": "",
"visibility": "public",
"application": {
"website": None
},
"media_attachments": attachments,
"account": {
"display_name": tweetData['user_name'],
"username": tweetData['user_screen_name'],
"acct": tweetData['user_screen_name'],
"url": f"https://x.com/{tweetData['user_screen_name']}/status/{tweetData['tweetID']}",
"uri": f"https://x.com/{tweetData['user_screen_name']}/status/{tweetData['tweetID']}",
"locked": False,
"avatar": tweetData['user_profile_image_url'],
"avatar_static": tweetData['user_profile_image_url'],
"hide_collections": False,
"noindex": False,
},
}

View File

@ -49,7 +49,9 @@ def addVnfToTweetIdCache(tweet_id, vnf):
global link_cache global link_cache
try: try:
if link_cache_system == "db": if link_cache_system == "db":
out = db.linkCache.update_one(vnf) filter_query = {'tweet': tweet_id}
update_operation = {'$set': vnf}
out = db.linkCache.update_one(filter_query, update_operation, upsert=True)
log.debug("Link added to DB cache ") log.debug("Link added to DB cache ")
return True return True
elif link_cache_system == "json": elif link_cache_system == "json":
@ -86,7 +88,7 @@ def getVnfFromTweetIdCache(tweet_id):
collection = db.linkCache collection = db.linkCache
vnf = collection.find_one({'tweet': tweet_id}) vnf = collection.find_one({'tweet': tweet_id})
if vnf != None: if vnf != None:
hits = ( vnf['hits'] + 1 ) hits = ( vnf.get('hits', 0) + 1 )
log.debug("Link located in DB cache.") log.debug("Link located in DB cache.")
query = { 'tweet': tweet_id } query = { 'tweet': tweet_id }
change = { "$set" : { "hits" : hits } } change = { "$set" : { "hits" : hits } }

View File

@ -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

BIN
static/richEmbed/gif.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
static/richEmbed/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
static/richEmbed/text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/richEmbed/video.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

View File

@ -3,6 +3,20 @@
<meta property="og:site_name" content="{{ appname }}"> <meta property="og:site_name" content="{{ appname }}">
<meta name="twitter:title" content="{{ tweet['user_name'] }} (@{{ tweet['user_screen_name'] }})" /> <meta name="twitter:title" content="{{ tweet['user_name'] }} (@{{ tweet['user_screen_name'] }})" />
{% if activityLink %}
<link type="application/activity+json" href="{{ activityLink|safe }}" />
{% endif %}
{% if sicon %}
<!-- Video icon created by Rizki Ahmad Fauzi - Flaticon -->
<!-- Picture and Text icon created by Freepik - Flaticon -->
<!-- Gif file icons created by Grand Iconic - Flaticon -->
<link rel="apple-touch-icon" sizes="128x128" href="/{{ sicon }}.png">
{% else %}
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
{% endif %}
<script src="/openInApp.js"></script> <script src="/openInApp.js"></script>
<script>document.addEventListener('DOMContentLoaded',function(){openTweet('{{ tweet["tweetID"] }}')})</script> <script>document.addEventListener('DOMContentLoaded',function(){openTweet('{{ tweet["tweetID"] }}')})</script>
<style> <style>

View File

@ -194,3 +194,14 @@ def test_embed_rtf():
resp = client.get(testTextTweet.replace("https://twitter.com","")+".rtf",headers={"User-Agent":"test"}) resp = client.get(testTextTweet.replace("https://twitter.com","")+".rtf",headers={"User-Agent":"test"})
assert resp.status_code==200 assert resp.status_code==200
assert testTextTweet_compare["text"] in str(resp.data) assert testTextTweet_compare["text"] in str(resp.data)
def test_embed_action():
cache.clearCache()
resp = client.get(testTextTweet.replace("https://twitter.com",""),headers={"User-Agent":"test"})
assert resp.status_code==200
assert "application/activity+json" in str(resp.data)
assert "%F0%9F%92%96" in str(resp.data) # 💖
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 "application/activity+json" in str(resp.data)
assert "%F0%9F%92%96" not in str(resp.data) # 💖

View File

@ -1,5 +1,4 @@
from weakref import finalize from flask import Flask, render_template, request, redirect, abort, Response, send_from_directory, send_file
from flask import Flask, render_template, request, redirect, abort, Response, send_from_directory, url_for, send_file, make_response, jsonify
from configHandler import config from configHandler import config
remoteCombine='combination_method' in config['config'] and config['config']['combination_method'] != "local" remoteCombine='combination_method' in config['config'] and config['config']['combination_method'] != "local"
@ -15,14 +14,16 @@ 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 from utils import getTweetIdFromUrl, pathregex, determineMediaToEmbed, determineEmbedTweet, BytesIOWrapper, fixMedia
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
from PyRTF.document.section import Section from PyRTF.document.section import Section
from PyRTF.document.paragraph import Paragraph from PyRTF.document.paragraph import Paragraph
from utils import BytesIOWrapper
from copy import deepcopy from copy import deepcopy
import json
import datetime
import activity as activitymg
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
user_agent="" user_agent=""
@ -34,6 +35,10 @@ staticFiles = { # TODO: Use flask static files instead of this
"preferences": {"mime": "text/html","path": "preferences.html"}, "preferences": {"mime": "text/html","path": "preferences.html"},
"style.css": {"mime": "text/css","path": "style.css"}, "style.css": {"mime": "text/css","path": "style.css"},
"Roboto-Regular.ttf": {"mime": "font/ttf","path": "Roboto-Regular.ttf"}, "Roboto-Regular.ttf": {"mime": "font/ttf","path": "Roboto-Regular.ttf"},
"gif.png": {"mime": "image/png","path": "richEmbed/gif.png"},
"video.png": {"mime": "image/png","path": "richEmbed/video.png"},
"image.png": {"mime": "image/png","path": "richEmbed/image.png"},
"text.png": {"mime": "image/png","path": "richEmbed/text.png"},
} }
generate_embed_user_agents = [ generate_embed_user_agents = [
@ -61,13 +66,6 @@ def isValidUserAgent(user_agent):
return True return True
return False return False
def fixMedia(mediaInfo):
# This is for the iOS Discord app, which has issues when serving URLs ending in .mp4 (https://github.com/dylanpdx/BetterTwitFix/issues/210)
if 'video.twimg.com' not in mediaInfo['url'] or 'convert?url=' in mediaInfo['url']:
return mediaInfo
mediaInfo['url'] = mediaInfo['url'].replace("https://video.twimg.com",f"{config['config']['url']}/tvid").replace(".mp4","")
return mediaInfo
def message(text): def message(text):
return render_template( return render_template(
'default.html', 'default.html',
@ -77,7 +75,24 @@ def message(text):
repo = config['config']['repo'], repo = config['config']['repo'],
url = config['config']['url'] ) url = config['config']['url'] )
def renderImageTweetEmbed(tweetData,image,appnameSuffix=""): def generateActivityLink(tweetData,media=None,mediatype=None,embedIndex=-1):
global user_agent
if 'LegacyEmbed' in user_agent: # TODO: Clean up; This is a hacky fix to make the new activity embed not trigger
return None
try:
embedIndex = embedIndex+1
return f"{config['config']['url']}/users/{tweetData['user_screen_name']}/statuses/{str(embedIndex)}{tweetData['tweetID']}"
except Exception as e:
log.error("Error generating activity link: "+str(e))
return None
def getAppName(tweetData,appnameSuffix=""):
appName = config['config']['appname']+appnameSuffix
if 'Discord' not in user_agent:
appName = msgs.formatProvider(config['config']['appname']+appnameSuffix,tweetData)
return appName
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'])
@ -91,15 +106,22 @@ def renderImageTweetEmbed(tweetData,image,appnameSuffix=""):
desc=embedDesc, desc=embedDesc,
urlEncodedDesc=urllib.parse.quote(embedDesc), urlEncodedDesc=urllib.parse.quote(embedDesc),
tweetLink=f'https://twitter.com/{tweetData["user_screen_name"]}/status/{tweetData["tweetID"]}', tweetLink=f'https://twitter.com/{tweetData["user_screen_name"]}/status/{tweetData["tweetID"]}',
appname=msgs.formatProvider(config['config']['appname']+appnameSuffix,tweetData), appname=getAppName(tweetData,appnameSuffix),
color=config['config']['color'] color=config['config']['color'],
sicon="image",
activityLink=generateActivityLink(tweetData,image,"image/png",embedIndex)
) )
def renderVideoTweetEmbed(tweetData,mediaInfo,appnameSuffix=""): 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'])
mediaInfo=fixMedia(mediaInfo) mediaInfo=fixMedia(mediaInfo)
appName = config['config']['appname']+appnameSuffix
if 'Discord' not in user_agent:
appName = msgs.formatProvider(config['config']['appname']+appnameSuffix,tweetData)
return render_template("video.html", return render_template("video.html",
tweet=tweetData, tweet=tweetData,
media=mediaInfo, media=mediaInfo,
@ -107,26 +129,32 @@ def renderVideoTweetEmbed(tweetData,mediaInfo,appnameSuffix=""):
desc=embedDesc, desc=embedDesc,
urlEncodedDesc=urllib.parse.quote(embedDesc), urlEncodedDesc=urllib.parse.quote(embedDesc),
tweetLink=f'https://twitter.com/{tweetData["user_screen_name"]}/status/{tweetData["tweetID"]}', tweetLink=f'https://twitter.com/{tweetData["user_screen_name"]}/status/{tweetData["tweetID"]}',
appname=msgs.formatProvider(config['config']['appname']+appnameSuffix,tweetData), appname=appName,
color=config['config']['color'] color=config['config']['color'],
sicon="video",
activityLink=generateActivityLink(tweetData,mediaInfo['url'],"video/mp4",embedIndex)
) )
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'])
return render_template("text.html", return render_template("text.html",
tweet=tweetData, tweet=tweetData,
host=config['config']['url'], host=config['config']['url'],
desc=embedDesc, desc=embedDesc,
urlEncodedDesc=urllib.parse.quote(embedDesc), urlEncodedDesc=urllib.parse.quote(embedDesc),
tweetLink=f'https://twitter.com/{tweetData["user_screen_name"]}/status/{tweetData["tweetID"]}', tweetLink=f'https://twitter.com/{tweetData["user_screen_name"]}/status/{tweetData["tweetID"]}',
appname=msgs.formatProvider(config['config']['appname']+appnameSuffix,tweetData), appname=getAppName(tweetData,appnameSuffix),
color=config['config']['color'] color=config['config']['color'],
activityLink=generateActivityLink(tweetData),
sicon="text"
) )
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)
return render_template("image.html", return render_template("image.html",
tweet=tweetData, tweet=tweetData,
pic=[tweetData['article']["image"]], pic=[tweetData['article']["image"]],
@ -134,8 +162,9 @@ def renderArticleTweetEmbed(tweetData,appnameSuffix=""):
desc=embedDesc, desc=embedDesc,
urlEncodedDesc=urllib.parse.quote(embedDesc), urlEncodedDesc=urllib.parse.quote(embedDesc),
tweetLink=f'https://twitter.com/{tweetData["user_screen_name"]}/status/{tweetData["tweetID"]}', tweetLink=f'https://twitter.com/{tweetData["user_screen_name"]}/status/{tweetData["tweetID"]}',
appname=msgs.formatProvider(config['config']['appname']+appnameSuffix,tweetData), appname=getAppName(tweetData,appnameSuffix),
color=config['config']['color'] color=config['config']['color'],
sicon="image"
) )
def renderUserEmbed(userData,appnameSuffix=""): def renderUserEmbed(userData,appnameSuffix=""):
@ -166,6 +195,70 @@ def oembedend():
provName = request.args.get("provider",None) provName = request.args.get("provider",None)
return oEmbedGen(desc, user, link, ttype,providerName=provName) return oEmbedGen(desc, user, link, ttype,providerName=provName)
@app.route('/activity.json')
def activity():
tweetId = request.args.get("id", None)
publishedDate = request.args.get("published", None)
likes = request.args.get("likes", None)
retweets = request.args.get("retweets", None)
userAttrTo = request.args.get("user", None)
content = request.args.get("content", None)
attachments = json.loads(request.args.get("attachments", "[]"))
##
attachmentsRaw = []
for attachment in attachments:
attachmentsRaw.append({
"type": "Document",
"mediaType": attachment["type"],
"url": attachment["url"],
"preview_url": "https://pbs.twimg.com/ext_tw_video_thumb/1906073839441735680/pu/img/2xqg6tlK9mK0mSOR.jpg",
})
return {
"id": "https://x.com/i/status/"+tweetId,
"type": "Note",
"summary": None,
"inReplyTo": None,
"published": publishedDate,
"url": "https://x.com/i/status/"+tweetId,
"attributedTo": userAttrTo,
"content": content,
"attachment": attachmentsRaw,
"likes": {
"type": "Collection",
"totalItems": int(likes)
},
"shares": {
"type": "Collection",
"totalItems": int(retweets)
},
}
@app.route('/user.json')
def userJson():
screen_name = request.args.get("screen_name", None)
name = request.args.get("name", None)
pfp = request.args.get("pfp", None)
return {
"id": screen_name,
"type": "Person",
"preferredUsername": screen_name,
"name": name,
"summary": "",
"url": "https://x.com/"+screen_name,
"tag": [],
"attachment": [],
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": pfp
},
}
def getTweetData(twitter_url,include_txt="false",include_rtf="false"): def getTweetData(twitter_url,include_txt="false",include_rtf="false"):
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":
@ -202,22 +295,16 @@ def getUserData(twitter_url):
userData = getApiUserResponse(rawUserData) userData = getApiUserResponse(rawUserData)
return userData return userData
def determineEmbedTweet(tweetData):
# Determine which tweet, i.e main or QRT, to embed the media from.
# if there is no QRT, return the main tweet => default behavior
# if both don't have media, return the main tweet => embedding qrt text will be handled in the embed description
# if both have media, return the main tweet => priority is given to the main tweet's media
# if only the QRT has media, return the QRT => show the QRT's media, not the main tweet's
# if only the main tweet has media, return the main tweet => show the main tweet's media, embedding QRT text will be handled in the embed description
if tweetData['qrt'] is None:
return tweetData
if tweetData['qrt']['hasMedia'] and not tweetData['hasMedia']:
return tweetData['qrt']
return tweetData
@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):
global user_agent
user_agent = request.headers.get('User-Agent', None)
if user_agent is None:
user_agent = "unknown"
isApiRequest=request.url.startswith("https://api.vx") or request.url.startswith("http://api.vx") isApiRequest=request.url.startswith("https://api.vx") or request.url.startswith("http://api.vx")
if not isApiRequest and (request.url.startswith("https://l.vx") or request.url.startswith("https://old.vx")) 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
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:
staticFiles[sub_path]["path"] = sub_path staticFiles[sub_path]["path"] = sub_path
@ -300,48 +387,36 @@ def twitfix(sub_path):
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
return tweetData return tweetData
elif directEmbed: # direct embed elif directEmbed: # direct embed
embeddingMedia = tweetData['hasMedia']
renderMedia = None
if embeddingMedia:
renderMedia = determineMediaToEmbed(tweetData,embedIndex)
# direct embeds should always prioritize the main tweet, so don't check for qrt # direct embeds should always prioritize the main tweet, so don't check for qrt
# determine what type of media we're dealing with # determine what type of media we're dealing with
if not tweetData['hasMedia'] and qrt is None: if not embeddingMedia and qrt is None:
return renderTextTweetEmbed(tweetData) return renderTextTweetEmbed(tweetData)
elif tweetData['allSameType'] and tweetData['media_extended'][0]['type'] == "image" and embedIndex == -1 and tweetData['combinedMediaUrl'] != None:
return render_template("rawimage.html",media={"url":tweetData['combinedMediaUrl']})
else: else:
# this means we have mixed media or video, and we're only going to embed one if renderMedia['type'] == "image":
if embedIndex == -1: # if the user didn't specify an index, we'll just use the first one return render_template("rawimage.html",media=renderMedia)
embedIndex = 0 elif renderMedia['type'] == "video" or renderMedia['type'] == "gif":
media = tweetData['media_extended'][embedIndex] return render_template("rawvideo.html",media=renderMedia)
media=fixMedia(media)
if media['type'] == "image":
return render_template("rawimage.html",media=media)
elif media['type'] == "video" or media['type'] == "gif":
return render_template("rawvideo.html",media=media)
else: # full embed else: # full embed
embedTweetData = determineEmbedTweet(tweetData) embedTweetData = determineEmbedTweet(tweetData)
embeddingMedia = embedTweetData['hasMedia']
if "article" in embedTweetData and embedTweetData["article"] is not None: if "article" in embedTweetData and embedTweetData["article"] is not None:
return renderArticleTweetEmbed(tweetData," - See original tweet for full article") return renderArticleTweetEmbed(tweetData," See original tweet for full article")
elif not embedTweetData['hasMedia']: elif not embeddingMedia:
return renderTextTweetEmbed(tweetData) return renderTextTweetEmbed(tweetData)
elif embedTweetData['allSameType'] and embedTweetData['media_extended'][0]['type'] == "image" and embedIndex == -1 and embedTweetData['combinedMediaUrl'] != None:
return renderImageTweetEmbed(tweetData,embedTweetData['combinedMediaUrl'],appnameSuffix=" - See original tweet for full quality")
else: else:
# this means we have mixed media or video, and we're only going to embed one media = determineMediaToEmbed(embedTweetData,embedIndex)
if embedIndex == -1: # if the user didn't specify an index, we'll just use the first one suffix=""
embedIndex = 0 if "suffix" in media:
media = embedTweetData['media_extended'][embedIndex] suffix = media["suffix"]
if len(embedTweetData["media_extended"]) > 1:
suffix = f' - Media {embedIndex+1}/{len(embedTweetData["media_extended"])}'
else:
suffix = ''
if media['type'] == "image": if media['type'] == "image":
return renderImageTweetEmbed(tweetData,media['url'] , appnameSuffix=suffix) return renderImageTweetEmbed(tweetData,media['url'] , appnameSuffix=suffix,embedIndex=embedIndex)
elif media['type'] == "video" or media['type'] == "gif": elif media['type'] == "video" or media['type'] == "gif":
if media['type'] == "gif": return renderVideoTweetEmbed(tweetData,media,appnameSuffix=suffix,embedIndex=embedIndex)
if config['config']['gifConvertAPI'] != "" and config['config']['gifConvertAPI'] != "none":
vurl=media['originalUrl'] if 'originalUrl' in media else media['url']
media['url'] = config['config']['gifConvertAPI'] + "/convert?url=" + vurl
suffix += " - GIF"
return renderVideoTweetEmbed(tweetData,media,appnameSuffix=suffix)
return message(msgs.failedToScan) return message(msgs.failedToScan)
@ -377,6 +452,23 @@ def rendercombined():
imgIo.seek(0) imgIo.seek(0)
return send_file(imgIo, mimetype='image/jpeg',max_age=86400) return send_file(imgIo, mimetype='image/jpeg',max_age=86400)
@app.route("/api/v1/statuses/<string:tweet_id>")
def api_v1_status(tweet_id):
embedIndex = int(tweet_id[0])-1
tweet_id = int(tweet_id[1:])
twitter_url=f"https://twitter.com/i/status/{tweet_id}"
tweetData = getTweetData(twitter_url)
if tweetData is None:
log.error("Tweet Data Get failed for "+twitter_url)
return message(msgs.failedToScan)
qrt = None
if 'qrtURL' in tweetData and tweetData['qrtURL'] is not None:
qrt = getTweetData(tweetData['qrtURL'])
tweetData['qrt'] = qrt
if tweetData is None:
abort(500) # this should cause Discord to fall back to the default embed
return activitymg.tweetDataToActivity(tweetData,embedIndex)
def oEmbedGen(description, user, video_link, ttype,providerName=None): def oEmbedGen(description, user, video_link, ttype,providerName=None):
if providerName == None: if providerName == None:
providerName = config['config']['appname'] providerName = config['config']['appname']
@ -394,4 +486,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)

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

View File

@ -1,5 +1,6 @@
import re import re
import io import io
from configHandler import config
pathregex = re.compile("\\w{1,15}\\/(status|statuses)\\/(\\d{2,20})") pathregex = re.compile("\\w{1,15}\\/(status|statuses)\\/(\\d{2,20})")
endTCOregex = re.compile("(^.*?) +https:\/\/t.co\/.*?$") endTCOregex = re.compile("(^.*?) +https:\/\/t.co\/.*?$")
@ -41,3 +42,50 @@ class BytesIOWrapper(io.BufferedReader):
def peek(self, size=-1): def peek(self, size=-1):
return self._encoding_call('peek', size) return self._encoding_call('peek', size)
def fixMedia(mediaInfo):
# This is for the iOS Discord app, which has issues when serving URLs ending in .mp4 (https://github.com/dylanpdx/BetterTwitFix/issues/210)
if 'video.twimg.com' not in mediaInfo['url'] or 'convert?url=' in mediaInfo['url'] or 'originalUrl' in mediaInfo:
return mediaInfo
mediaInfo["originalUrl"] = mediaInfo['url']
mediaInfo['url'] = mediaInfo['url'].replace("https://video.twimg.com",f"{config['config']['url']}/tvid").replace(".mp4","")
return mediaInfo
def determineEmbedTweet(tweetData):
# Determine which tweet, i.e main or QRT, to embed the media from.
# if there is no QRT, return the main tweet => default behavior
# if both don't have media, return the main tweet => embedding qrt text will be handled in the embed description
# if both have media, return the main tweet => priority is given to the main tweet's media
# if only the QRT has media, return the QRT => show the QRT's media, not the main tweet's
# if only the main tweet has media, return the main tweet => show the main tweet's media, embedding QRT text will be handled in the embed description
if tweetData['qrt'] is None:
return tweetData
if tweetData['qrt']['hasMedia'] and not tweetData['hasMedia']:
return tweetData['qrt']
return tweetData
def determineMediaToEmbed(tweetData,embedIndex = -1):
if tweetData['allSameType'] and tweetData['media_extended'][0]['type'] == "image" and embedIndex == -1 and tweetData['combinedMediaUrl'] != None:
return {"url":tweetData['combinedMediaUrl'],"type":"image"}
else:
# this means we have mixed media or video, and we're only going to embed one
if embedIndex == -1: # if the user didn't specify an index, we'll just use the first one
embedIndex = 0
media = tweetData['media_extended'][embedIndex]
media=fixMedia(media)
suffix=""
if len(tweetData["media_extended"]) > 1:
suffix = f' • Media {embedIndex+1}/{len(tweetData["media_extended"])}'
else:
suffix = ''
media["suffix"] = suffix
if media['type'] == "image":
return media
elif media['type'] == "video" or media['type'] == "gif":
if media['type'] == "gif":
if config['config']['gifConvertAPI'] != "" and config['config']['gifConvertAPI'] != "none":
vurl=media['originalUrl'] if 'originalUrl' in media else media['url']
media['url'] = config['config']['gifConvertAPI'] + "/convert?url=" + vurl
suffix += " • GIF"
media["suffix"] = suffix
return media