diff --git a/src/apiutils.nim b/src/apiutils.nim index b92b9d6..33454a7 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -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 diff --git a/src/routes/activityspoof.nim b/src/routes/activityspoof.nim index e40d8df..6675131 100644 --- a/src/routes/activityspoof.nim +++ b/src/routes/activityspoof.nim @@ -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 diff --git a/src/routes/status.nim b/src/routes/status.nim index 979edf9..a547d40 100644 --- a/src/routes/status.nim +++ b/src/routes/status.nim @@ -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": diff --git a/src/views/general.nim b/src/views/general.nim index c577fcd..95768b7 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -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) diff --git a/src/views/mastoapi.nim b/src/views/mastoapi.nim index 31e8dd2..18d0bef 100644 --- a/src/views/mastoapi.nim +++ b/src/views/mastoapi.nim @@ -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" diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 9072bc0..f990cb4 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -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: