mixed media in tweets, allow discord embeds to select shown media, ...more
- direct image linking (buggy if you try and do /photo or /video with no index or slash) - fix activitypub images not being images - gross hack to tell discord to fetch a single image for fedi (broken for videos lol, discord's media proxy is not activitystream spec compliant)
This commit is contained in:
parent
71c772d6c9
commit
a6412968fe
@ -125,6 +125,8 @@ template fetchImpl(result, additional_headers, fetchBody) {.dirty.} =
|
||||
raise e
|
||||
except OSError as e:
|
||||
raise e
|
||||
except ProtocolError as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
echo "error: ", e.name, ", msg: ", e.msg, ", url: ", url
|
||||
#if "length" notin e.msg and "descriptor" notin e.msg:
|
||||
@ -134,12 +136,11 @@ template fetchImpl(result, additional_headers, fetchBody) {.dirty.} =
|
||||
template retry(bod) =
|
||||
try:
|
||||
bod
|
||||
except RateLimitError:
|
||||
echo "[accounts] Rate limited, retrying ", api, " request..."
|
||||
except ProtocolError:
|
||||
bod
|
||||
|
||||
proc fetch*(url: Uri; api: Api; additional_headers: HttpHeaders = newHttpHeaders()): Future[JsonNode] {.async.} =
|
||||
#retry:
|
||||
retry:
|
||||
var body: string
|
||||
fetchImpl(body, additional_headers):
|
||||
if body.startsWith('{') or body.startsWith('['):
|
||||
@ -157,7 +158,7 @@ proc fetch*(url: Uri; api: Api; additional_headers: HttpHeaders = newHttpHeaders
|
||||
raise rateLimitError()
|
||||
|
||||
proc fetchRaw*(url: Uri; api: Api; additional_headers: HttpHeaders = newHttpHeaders()): Future[string] {.async.} =
|
||||
#retry:
|
||||
retry:
|
||||
fetchImpl(result, additional_headers):
|
||||
if not (result.startsWith('{') or result.startsWith('[')):
|
||||
echo resp.status, ": ", result, " --- url: ", url
|
||||
|
||||
@ -33,7 +33,15 @@ proc createActivityPubRouter*(cfg: Config) =
|
||||
resp Http200, {"Content-Type": "application/json"}, $getMastoAPIUser(user, cfg)
|
||||
|
||||
get "/api/v1/statuses/@id":
|
||||
let id = @"id"
|
||||
var
|
||||
id = @"id"
|
||||
query = ""
|
||||
|
||||
# stupid hack to trick discord lmao
|
||||
if id.find("_") != -1:
|
||||
let parts = id.split("_")
|
||||
id = parts[0]
|
||||
query = parts[1]
|
||||
|
||||
if id.len > 19 or id.any(c => not c.isDigit):
|
||||
resp Http404, {"Content-Type": "application/json"}, """{"error":"Invalid record ID"}"""
|
||||
@ -54,11 +62,44 @@ proc createActivityPubRouter*(cfg: Config) =
|
||||
|
||||
resp Http404, {"Content-Type": "application/json"}, $errJson
|
||||
|
||||
var
|
||||
mediaType = ""
|
||||
mediaIndex = ""
|
||||
if query.len > 0:
|
||||
let parts = query.split(":")
|
||||
mediaType = parts[0]
|
||||
if parts.len == 2:
|
||||
mediaIndex = parts[1]
|
||||
|
||||
let
|
||||
tweet = conv.tweet
|
||||
tweetUrl = &"{getUrlPrefix(cfg)}/i/status/{id}"
|
||||
var media: seq[JsonNode] = @[]
|
||||
|
||||
if mediaType.len > 0:
|
||||
if mediaType == "video" and tweet.video.isSome or tweet.gif.isSome:
|
||||
tweet.photos = @[]
|
||||
elif mediaType == "photo" and tweet.photos.len > 0:
|
||||
if mediaIndex.len > 0:
|
||||
var index = parseInt(mediaIndex)
|
||||
var useVideo = false
|
||||
if index > tweet.photos.len:
|
||||
if tweet.video.isSome or tweet.gif.isSome:
|
||||
useVideo = true
|
||||
else:
|
||||
index = tweet.photos.len
|
||||
elif index < 1:
|
||||
index = 1
|
||||
|
||||
if useVideo:
|
||||
tweet.photos = @[]
|
||||
else:
|
||||
index -= 1
|
||||
tweet.video = none(Video)
|
||||
let image = tweet.photos[index]
|
||||
tweet.photos = @[]
|
||||
tweet.photos.add(image)
|
||||
|
||||
if tweet.photos.len > 0:
|
||||
for imageObj in tweet.photos:
|
||||
let image = getUrlPrefix(cfg) & getPicUrl(imageObj.url)
|
||||
@ -169,7 +210,25 @@ proc createActivityPubRouter*(cfg: Config) =
|
||||
resp Http200, {"Content-Type": "application/json"}, $postJson
|
||||
|
||||
get "/users/@name/statuses/@id":
|
||||
let id = @"id"
|
||||
var
|
||||
id = @"id"
|
||||
query = ""
|
||||
|
||||
# stupid hack to trick discord lmao
|
||||
if id.find("_") != -1:
|
||||
let parts = id.split("_")
|
||||
id = parts[0]
|
||||
query = parts[1]
|
||||
|
||||
var
|
||||
mediaType = ""
|
||||
mediaIndex = ""
|
||||
if query.len > 0:
|
||||
let parts = query.split(":")
|
||||
mediaType = parts[0]
|
||||
if parts.len == 2:
|
||||
mediaIndex = parts[1]
|
||||
|
||||
if request.headers.hasKey("Accept") and request.headers["Accept"] == "application/activity+json":
|
||||
if id.len > 19 or id.any(c => not c.isDigit):
|
||||
resp Http404, {"Content-Type": "application/json"}, """{"error":"Invalid record ID"}"""
|
||||
@ -190,7 +249,33 @@ proc createActivityPubRouter*(cfg: Config) =
|
||||
|
||||
resp Http404, {"Content-Type": "application/json"}, $errJson
|
||||
|
||||
let postJson = getActivityStream(conv.tweet, cfg, prefs)
|
||||
let tweet = conv.tweet
|
||||
|
||||
if mediaType.len > 0:
|
||||
if mediaType == "video" and tweet.video.isSome or tweet.gif.isSome:
|
||||
tweet.photos = @[]
|
||||
elif mediaType == "photo" and tweet.photos.len > 0:
|
||||
if mediaIndex.len > 0:
|
||||
var index = parseInt(mediaIndex)
|
||||
var useVideo = false
|
||||
if index > tweet.photos.len:
|
||||
if tweet.video.isSome or tweet.gif.isSome:
|
||||
useVideo = true
|
||||
else:
|
||||
index = tweet.photos.len
|
||||
elif index < 1:
|
||||
index = 1
|
||||
|
||||
if useVideo:
|
||||
tweet.photos = @[]
|
||||
else:
|
||||
index -= 1
|
||||
tweet.video = none(Video)
|
||||
let image = tweet.photos[index]
|
||||
tweet.photos = @[]
|
||||
tweet.photos.add(image)
|
||||
|
||||
let postJson = getActivityStream(tweet, cfg, prefs)
|
||||
|
||||
resp Http200, {"Content-Type": "application/json"}, $postJson
|
||||
|
||||
|
||||
@ -37,13 +37,29 @@ proc createStatusRouter*(cfg: Config) =
|
||||
resp renderMain(renderUserList(await getGraphRetweeters(id, getCursor()), prefs),
|
||||
request, cfg, prefs)
|
||||
|
||||
get "/@name/status/@id/?":
|
||||
get "/@name/status/@id/?@m?/?@i?/?":
|
||||
cond '.' notin @"name"
|
||||
var id = @"id"
|
||||
var rawFile = false
|
||||
if id.endsWith(".mp4"):
|
||||
rawFile = true
|
||||
id.removeSuffix(".mp4")
|
||||
var
|
||||
id = @"id"
|
||||
media = @"m"
|
||||
mediaIndex = @"i"
|
||||
|
||||
let url = $request.getNativeReq().url
|
||||
var
|
||||
rawVideo = false
|
||||
rawImage = false
|
||||
if url.endsWith(".mp4") or url.endsWith(".gif"):
|
||||
rawVideo = true
|
||||
elif url.endsWith(".png") or url.endsWith(".jpg"):
|
||||
rawImage = true
|
||||
|
||||
for ext in @[".mp4", ".gif", ".png", ".jpg"]:
|
||||
if id.endsWith(ext):
|
||||
id.removeSuffix(ext)
|
||||
if media.endsWith(ext):
|
||||
media.removeSuffix(ext)
|
||||
if mediaIndex.endsWith(ext):
|
||||
mediaIndex.removeSuffix(ext)
|
||||
|
||||
if request.headers.hasKey("Accept") and request.headers["Accept"] == "application/activity+json":
|
||||
if id.len > 19 or id.any(c => not c.isDigit):
|
||||
@ -65,7 +81,33 @@ proc createStatusRouter*(cfg: Config) =
|
||||
|
||||
resp Http404, {"Content-Type": "application/json"}, $errJson
|
||||
|
||||
let postJson = getActivityStream(conv.tweet, cfg, prefs)
|
||||
let tweet = conv.tweet
|
||||
|
||||
if media.len > 0:
|
||||
if media == "video" and tweet.video.isSome or tweet.gif.isSome:
|
||||
tweet.photos = @[]
|
||||
elif media == "photo" and tweet.photos.len > 0:
|
||||
if mediaIndex.len > 0:
|
||||
var index = parseInt(mediaIndex)
|
||||
var useVideo = false
|
||||
if index > tweet.photos.len:
|
||||
if tweet.video.isSome or tweet.gif.isSome:
|
||||
useVideo = true
|
||||
else:
|
||||
index = tweet.photos.len
|
||||
elif index < 1:
|
||||
index = 1
|
||||
|
||||
if useVideo:
|
||||
tweet.photos = @[]
|
||||
else:
|
||||
index -= 1
|
||||
tweet.video = none(Video)
|
||||
let image = tweet.photos[index]
|
||||
tweet.photos = @[]
|
||||
tweet.photos.add(image)
|
||||
|
||||
let postJson = getActivityStream(tweet, cfg, prefs)
|
||||
|
||||
resp Http200, {"Content-Type": "application/json"}, $postJson
|
||||
|
||||
@ -99,6 +141,36 @@ proc createStatusRouter*(cfg: Config) =
|
||||
avatar = tweet.user.userPic
|
||||
time = some(tweet.time)
|
||||
|
||||
let isDiscord = request.headers.getOrDefault("User-Agent").toString().contains("Discordbot")
|
||||
var
|
||||
realMediaIndex = mediaIndex
|
||||
realUseVideo = false
|
||||
if isDiscord and media.len > 0:
|
||||
if media == "video" and tweet.video.isSome or tweet.gif.isSome:
|
||||
tweet.photos = @[]
|
||||
elif media == "photo" and tweet.photos.len > 0:
|
||||
if mediaIndex.len > 0:
|
||||
var index = parseInt(mediaIndex)
|
||||
var useVideo = false
|
||||
if index > tweet.photos.len:
|
||||
if tweet.video.isSome or tweet.gif.isSome:
|
||||
useVideo = true
|
||||
realUseVideo = true
|
||||
else:
|
||||
index = tweet.photos.len
|
||||
elif index < 1:
|
||||
index = 1
|
||||
|
||||
if useVideo:
|
||||
tweet.photos = @[]
|
||||
else:
|
||||
realMediaIndex = $index
|
||||
index -= 1
|
||||
tweet.video = none(Video)
|
||||
let image = tweet.photos[index]
|
||||
tweet.photos = @[]
|
||||
tweet.photos.add(image)
|
||||
|
||||
var
|
||||
images = tweet.photos
|
||||
video = ""
|
||||
@ -123,7 +195,7 @@ proc createStatusRouter*(cfg: Config) =
|
||||
|
||||
if tweet.video.isSome():
|
||||
let videoObj = get(tweet.video)
|
||||
images = @[Image(url:videoObj.thumb)]
|
||||
images.add(Image(url:videoObj.thumb))
|
||||
|
||||
let vars = videoObj.variants.filterIt(it.contentType == mp4)
|
||||
# idk why this wont sort when it sorts everywhere else
|
||||
@ -131,7 +203,7 @@ proc createStatusRouter*(cfg: Config) =
|
||||
video = vars[^1].url
|
||||
elif tweet.gif.isSome():
|
||||
let gif = get(tweet.gif)
|
||||
images = @[Image(url:gif.thumb)]
|
||||
images.add(Image(url:gif.thumb))
|
||||
video = getUrlPrefix(cfg) & getPicUrl(gif.url)
|
||||
#elif tweet.card.isSome():
|
||||
# let card = tweet.card.get()
|
||||
@ -140,20 +212,44 @@ proc createStatusRouter*(cfg: Config) =
|
||||
# elif card.video.isSome():
|
||||
# images = @[card.video.get().thumb]
|
||||
|
||||
if rawFile and video != "":
|
||||
if rawVideo and video != "":
|
||||
redirect(video)
|
||||
elif rawImage and images.len > 0:
|
||||
if media == "photo" and mediaIndex.len > 0:
|
||||
var index = parseInt(mediaIndex)
|
||||
var useVideo = false
|
||||
if index > tweet.photos.len:
|
||||
if video != "":
|
||||
useVideo = true
|
||||
else:
|
||||
index = tweet.photos.len
|
||||
elif index < 1:
|
||||
index = 1
|
||||
|
||||
if useVideo:
|
||||
redirect(video)
|
||||
else:
|
||||
index -= 1
|
||||
redirect(getPicUrl(images[index].url))
|
||||
else:
|
||||
redirect(getPicUrl(images[0].url))
|
||||
|
||||
var query = ""
|
||||
if media == "video":
|
||||
query = "video"
|
||||
elif media == "photo" and mediaIndex.len > 0:
|
||||
if realUseVideo and video != "":
|
||||
query = "video"
|
||||
else:
|
||||
query = &"photo:{realMediaIndex}"
|
||||
|
||||
let html = renderConversation(conv, prefs, getPath() & "#m")
|
||||
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle,
|
||||
images=images, video=video, avatar=avatar, time=time,
|
||||
context=context, contextUrl=contextUrl, id=id)
|
||||
context=context, contextUrl=contextUrl, id=id, media=query
|
||||
)
|
||||
|
||||
get "/@name/@s/@id/@m/?@i?":
|
||||
cond @"s" in ["status", "statuses"]
|
||||
cond @"m" in ["video", "photo"]
|
||||
redirect("/$1/status/$2" % [@"name", @"id"])
|
||||
|
||||
get "/@name/statuses/@id/?":
|
||||
get "/@name/statuses/@id/?@m?/?@i?":
|
||||
redirect("/$1/status/$2" % [@"name", @"id"])
|
||||
|
||||
get "/i/web/status/@id":
|
||||
|
||||
@ -39,7 +39,8 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
|
||||
proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||
video=""; images: seq[Image] = @[]; banner=""; ogTitle="";
|
||||
rss=""; canonical=""; avatar=""; context=""; contextUrl="";
|
||||
id=""; time: Option[DateTime] = none(DateTime)): VNode =
|
||||
id=""; time: Option[DateTime] = none(DateTime); media=""
|
||||
): VNode =
|
||||
var theme = prefs.theme.toTheme
|
||||
if "theme" in req.params:
|
||||
theme = req.params["theme"].toTheme
|
||||
@ -166,7 +167,10 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||
author = encodeUrl(context)
|
||||
link(rel="alternate", href=(&"{getUrlPrefix(cfg)}/oembed.json?type=video&provider={encodeUrl(siteName)}&title={title}&user={author}&url={encodeUrl(contextUrl)}"), type="application/json+oembed")
|
||||
|
||||
link(rel="alternate", href=(&"{getUrlPrefix(cfg)}/users/i/statuses/{id}"), type="application/activity+json")
|
||||
var fediUrl = &"{getUrlPrefix(cfg)}/users/i/statuses/{id}"
|
||||
if media.len > 0:
|
||||
fediUrl &= "_" & media
|
||||
link(rel="alternate", href=fediUrl, type="application/activity+json")
|
||||
|
||||
# this is last so images are also preloaded
|
||||
# if this is done earlier, Chrome only preloads one image for some reason
|
||||
@ -176,14 +180,14 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||
proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs;
|
||||
titleText=""; desc=""; ogTitle=""; rss=""; video="";
|
||||
images: seq[Image] = @[]; banner=""; avatar=""; context="";
|
||||
contextUrl=""; id=""; time: Option[DateTime] = none(DateTime)
|
||||
): string =
|
||||
contextUrl=""; id=""; time: Option[DateTime] = none(DateTime);
|
||||
media=""): string =
|
||||
|
||||
let canonical = getTwitterLink(req.path, req.params)
|
||||
|
||||
let node = buildHtml(html(lang="en")):
|
||||
renderHead(prefs, cfg, req, titleText, desc, video, images, banner, ogTitle,
|
||||
rss, canonical, avatar, context, contextUrl, id, time)
|
||||
rss, canonical, avatar, context, contextUrl, id, time, media)
|
||||
|
||||
body:
|
||||
renderNavbar(cfg, req, rss, canonical)
|
||||
|
||||
@ -64,11 +64,10 @@ proc getActivityStream*(tweet: Tweet, cfg: Config, prefs: Prefs): JsonNode =
|
||||
filetype = "jpeg"
|
||||
|
||||
var mediaObj = newJObject()
|
||||
mediaObj["type"] = %"Document"
|
||||
mediaObj["type"] = %"Image"
|
||||
mediaObj["mediaType"] = %("image/" & filetype)
|
||||
mediaObj["url"] = %image
|
||||
mediaObj["name"] = %imageObj.description
|
||||
|
||||
media.add(mediaObj)
|
||||
|
||||
if tweet.video.isSome():
|
||||
@ -79,25 +78,59 @@ proc getActivityStream*(tweet: Tweet, cfg: Config, prefs: Prefs): JsonNode =
|
||||
if videoObj.description.len > 0:
|
||||
description = videoObj.description
|
||||
|
||||
var mediaObj = newJObject()
|
||||
mediaObj["type"] = %"Document"
|
||||
mediaObj["mediaType"] = %"video/mp4"
|
||||
mediaObj["url"] = %vars[^1].url
|
||||
mediaObj["name"] = %description
|
||||
let splitUrl = videoObj.thumb.split('.')
|
||||
var filetype = splitUrl[^1]
|
||||
if filetype == "jpg":
|
||||
filetype = "jpeg"
|
||||
|
||||
media.add(mediaObj)
|
||||
var url: seq[JsonNode] = @[]
|
||||
|
||||
var thumb = newJObject()
|
||||
thumb["type"] = %"Link"
|
||||
thumb["mediaType"] = %("image/" & filetype)
|
||||
thumb["href"] = %(getUrlPrefix(cfg) & getPicUrl(videoObj.thumb))
|
||||
url.add(thumb)
|
||||
|
||||
var mediaObj = newJObject()
|
||||
mediaObj["type"] = %"Link"
|
||||
mediaObj["mediaType"] = %"video/mp4"
|
||||
mediaObj["href"] = %vars[^1].url
|
||||
url.add(mediaObj)
|
||||
|
||||
var wrapper = newJObject()
|
||||
wrapper["type"] = %"Video"
|
||||
wrapper["name"] = %description
|
||||
wrapper["url"] = %url
|
||||
media.add(wrapper)
|
||||
elif tweet.gif.isSome():
|
||||
let
|
||||
gif = get(tweet.gif)
|
||||
gifUrl = getUrlPrefix(cfg) & getPicUrl(gif.url)
|
||||
|
||||
var mediaObj = newJObject()
|
||||
mediaObj["type"] = %"Document"
|
||||
mediaObj["mediaType"] = %"video/mp4"
|
||||
mediaObj["url"] = %gifUrl
|
||||
mediaObj["name"] = newJNull() # FIXME a11y
|
||||
let splitUrl = gif.thumb.split('.')
|
||||
var filetype = splitUrl[^1]
|
||||
if filetype == "jpg":
|
||||
filetype = "jpeg"
|
||||
|
||||
media.add(mediaObj)
|
||||
var url: seq[JsonNode] = @[]
|
||||
|
||||
var thumb = newJObject()
|
||||
thumb["type"] = %"Link"
|
||||
thumb["mediaType"] = %("image/" & filetype)
|
||||
thumb["href"] = %(getUrlPrefix(cfg) & getPicUrl(gif.thumb))
|
||||
url.add(thumb)
|
||||
|
||||
var mediaObj = newJObject()
|
||||
mediaObj["type"] = %"Link"
|
||||
mediaObj["mediaType"] = %"video/mp4"
|
||||
mediaObj["href"] = %gifUrl
|
||||
url.add(mediaObj)
|
||||
|
||||
var wrapper = newJObject()
|
||||
wrapper["type"] = %"Video"
|
||||
wrapper["name"] = newJNull()
|
||||
wrapper["url"] = %url
|
||||
media.add(wrapper)
|
||||
|
||||
var context: seq[JsonNode] = @[]
|
||||
let contextUrl: JsonNode = %"https://www.w3.org/ns/activitystreams"
|
||||
|
||||
@ -225,7 +225,8 @@ proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||||
buildHtml(tdiv(class="quote-media-container")):
|
||||
if quote.photos.len > 0:
|
||||
renderAlbum(quote)
|
||||
elif quote.video.isSome:
|
||||
|
||||
if quote.video.isSome:
|
||||
renderVideo(quote.video.get(), prefs, path)
|
||||
elif quote.gif.isSome:
|
||||
renderGif(quote.gif.get(), prefs)
|
||||
@ -343,7 +344,8 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
|
||||
|
||||
if tweet.photos.len > 0:
|
||||
renderAlbum(tweet)
|
||||
elif tweet.video.isSome:
|
||||
|
||||
if tweet.video.isSome:
|
||||
renderVideo(tweet.video.get(), prefs, path)
|
||||
views = tweet.video.get().views
|
||||
elif tweet.gif.isSome:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user