# SPDX-License-Identifier: AGPL-3.0-only import json, asyncdispatch, strutils, sequtils, uri, options, sugar, strformat, times import jester import router_utils import ".."/[types, formatters, api, redis_cache] import ../views/[mastoapi] export json, uri, sequtils, options, sugar, times export router_utils export api, formatters export mastoapi proc createActivityPubRouter*(cfg: Config) = router activityspoof: get "/api/v1/accounts/?": resp Http200, {"Content-Type": "application/json"}, """[]""" get "/api/v1/accounts/@id": let id = @"id" #if id.len > 19 or id.any(c => not c.isDigit): if not id.allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9', '_'}): resp Http404, {"Content-Type": "application/json"}, """{"error":"Invalid account ID"}""" #var username = await getCachedUsername(id) #if username.len == 0: #resp Http404, {"Content-Type": "application/json"}, """{"error":"User not found"}""" let user = await getCachedUser(id) if user.suspended or user.id.len == 0: resp Http404, {"Content-Type": "application/json"}, """{"error":"User not found"}""" resp Http200, {"Content-Type": "application/json"}, $getMastoAPIUser(user, cfg) get "/api/v1/statuses/@id": var id = @"id" query = "" # stupid hack to trick discord lmao if id.startsWith("422209040515"): query = "video" id.removePrefix("422209040515") elif id.startsWith("421608152015"): query = "photo:" id.removePrefix("421608152015") query &= id[0] id = id[1..^1] if id.len > 19 or id.any(c => not c.isDigit): resp Http404, {"Content-Type": "application/json"}, """{"error":"Invalid record ID"}""" let prefs = cookiePrefs() let conv = await getCachedTweet(id) if conv == nil: echo "nil conv" if conv == nil or conv.tweet == nil or conv.tweet.id.len == 0: var error = "Record not found" if conv != nil and conv.tweet != nil and conv.tweet.tombstone.len > 0: error = conv.tweet.tombstone var errJson = newJObject() errJson["error"] = %error 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) var mediaObj = newJObject() mediaObj["id"] = %"150745989836308480" # idk if discord even parses this snowflake, but its my user id why not mediaObj["type"] = %"image" mediaObj["url"] = %image mediaObj["preview_url"] = %image mediaObj["remote_url"] = %image mediaObj["preview_remote_url"] = %image mediaObj["text_url"] = newJNull() mediaObj["description"] = %imageObj.description # FIXME but this probably isnt used by discord mediaObj["meta"] = newJObject() media.add(mediaObj) if tweet.video.isSome(): let videoObj = get(tweet.video) vars = videoObj.variants.filterIt(it.contentType == mp4) videoUrl = vars[^1].url.replace("https://video.twimg.com", getUrlPrefix(cfg) & "/tvid").replace(".mp4", "") videoPreview = getUrlPrefix(cfg) & getPicUrl(videoObj.thumb) var mediaObj = newJObject() var description = videoObj.title if videoObj.description.len > 0: description = videoObj.description mediaObj["id"] = %"150745989836308480" mediaObj["type"] = %"video" mediaObj["url"] = %videoUrl mediaObj["preview_url"] = %videoPreview mediaObj["remote_url"] = %videoUrl mediaObj["preview_remote_url"] = %videoPreview mediaObj["text_url"] = newJNull() mediaObj["description"] = %description # FIXME but this probably isnt used by discord mediaObj["meta"] = newJObject() media.add(mediaObj) elif tweet.gif.isSome(): let gif = get(tweet.gif) gifUrl = (https & gif.url).replace("https://video.twimg.com", getUrlPrefix(cfg) & "/tvid").replace(".mp4", "") gifPreview = getUrlPrefix(cfg) & getPicUrl(gif.thumb) var mediaObj = newJObject() mediaObj["id"] = %"150745989836308480" mediaObj["type"] = %"video" mediaObj["url"] = %gifUrl mediaObj["preview_url"] = %gifPreview mediaObj["remote_url"] = %gifUrl mediaObj["preview_remote_url"] = %gifPreview mediaObj["text_url"] = newJNull() mediaObj["description"] = newJNull() # FIXME this requires refactoring gifs # FIXME but this probably isnt used by discord mediaObj["meta"] = newJObject() media.add(mediaObj) var fields: seq[JsonNode] = @[] if tweet.user.location.len > 0: var location = newJObject() location["name"] = %"Location" location["value"] = %tweet.user.location location["verified_at"] = newJNull() fields.add(location) if tweet.user.website.len > 0: var website = newJObject() website["name"] = %"Website" website["value"] = %(&"{tweet.user.website}") website["verified_at"] = newJNull() fields.add(website) var postJson = newJObject() postJson["id"] = %tweet.id postJson["url"] = %tweetUrl postJson["uri"] = %tweetUrl postJson["created_at"] = %($tweet.time) postJson["edited_at"] = newJNull() postJson["reblog"] = newJNull() if tweet.replyId.len != 0: postJson["in_reply_to_id"] = %(&"{tweet.replyId}") postJson["in_reply_to_account_id"] = %tweet.replyHandle else: postJson["in_reply_to_id"] = newJNull() postJson["in_reply_to_account_id"] = newJNull() postJson["language"] = %"en" # FIXME postJson["content"] = %formatTweetForMastoAPI(tweet, cfg, prefs) postJson["spoiler_text"] = %"" postJson["visibility"] = %"public" postJson["application"] = %*{ "name": "Nitter", "website": getUrlPrefix(cfg) } postJson["media_attachments"] = %media postJson["account"] = getMastoAPIUser(tweet.user, cfg) postJson["mentions"] = newJArray() # TODO: parse? postJson["tags"] = newJArray() # TODO: parse? postJson["emojis"] = newJArray() postJson["card"] = newJNull() postJson["poll"] = newJNull() # TODO: parse? resp Http200, {"Content-Type": "application/json"}, $postJson get "/users/@name/statuses/@id": var id = @"id" query = "" # stupid hack to trick discord lmao if id.startsWith("422209040515"): query = "video" id.removePrefix("422209040515") elif id.startsWith("421608152015"): query = "photo:" id.removePrefix("421608152015") query &= id[0] id = id[1..^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"}""" let prefs = cookiePrefs() let conv = await getCachedTweet(id) if conv == nil: echo "nil conv" if conv == nil or conv.tweet == nil or conv.tweet.id.len == 0: var error = "Record not found" if conv != nil and conv.tweet != nil and conv.tweet.tombstone.len > 0: error = conv.tweet.tombstone var errJson = newJObject() errJson["error"] = %error resp Http404, {"Content-Type": "application/json"}, $errJson 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 redirect("/$1/status/$2" % [@"name", @"id"]) get "/users/@name": if request.headers.hasKey("Accept") and request.headers["Accept"] == "application/activity+json": let user = await getCachedUser(@"name") if user.suspended or user.id.len == 0: resp Http404, {"Content-Type": "application/json"}, """{"error":"User not found"}""" let prefs = cookiePrefs() let userJson = getActivityStream(user, cfg, prefs) resp Http200, {"Content-Type": "application/json"}, $userJson redirect("/" & @"name") # might as well get "/.well-known/nodeinfo": var nodeinfo = newJObject() let link: JsonNode = %*{ "href": &"{getUrlPrefix(cfg)}/nodeinfo/2.1.json", "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1" } var links: seq[JsonNode] = @[] links.add(link) nodeinfo["links"] = %links resp Http200, {"Content-Type": "application/json"}, $nodeinfo get "/nodeinfo/2.1.json": var nodeinfo = newJObject() nodeinfo["version"] = %"2.1" nodeinfo["software"] = %*{ "name": "Nitter", "repository": "https://gitlab.com/Cynosphere/nitter" } var metadata = newJObject() metadata["features"] = newJArray() metadata["federation"] = newJObject() metadata["nodeDescription"] = %"Alternative Twitter front-end (ActivityPub support added for Discord)" metadata["nodeName"] = %"Nitter" metadata["private"] = %true metadata["maintainer"] = %*{ "name": "Cynthia", "email": "gamers@riseup.net" } nodeinfo["metadata"] = metadata nodeinfo["openRegistrations"] = %false nodeinfo["protocols"] = newJArray() var services = newJObject() services["inbound"] = newJArray() services["outbound"] = newJArray() nodeinfo["services"] = services nodeinfo["usage"] = newJObject() resp Http200, {"Content-Type": "application/json"}, $nodeinfo