# 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] 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/statuses/@id": let id = @"id" 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 getTweet(id) if conv == nil: echo "nil conv" if conv == nil or conv.tweet == nil or conv.tweet.id == 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 tweetUrl = &"{getUrlPrefix(cfg)}/i/status/{id}" var media: seq[JsonNode] = @[] if tweet.photos.len > 0: for url in tweet.photos: let image = getUrlPrefix(cfg) & getPicUrl(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"] = newJNull() mediaObj["preview_remote_url"] = newJNull() mediaObj["text_url"] = newJNull() mediaObj["description"] = newJNull() # FIXME (not used by discord, i like a11y) # 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) var mediaObj = newJObject() mediaObj["id"] = %"150745989836308480" mediaObj["type"] = %"video" mediaObj["url"] = %vars[^1].url mediaObj["preview_url"] = %(getUrlPrefix(cfg) & getPicUrl(videoObj.thumb)) mediaObj["remote_url"] = newJNull() mediaObj["preview_remote_url"] = newJNull() mediaObj["text_url"] = newJNull() mediaObj["description"] = newJNull() # FIXME (not used by discord, i like a11y) # FIXME but this probably isnt used by discord mediaObj["meta"] = newJObject() media.add(mediaObj) elif tweet.gif.isSome(): let gif = get(tweet.gif) var mediaObj = newJObject() mediaObj["id"] = %"150745989836308480" mediaObj["type"] = %"video" mediaObj["url"] = %(getUrlPrefix(cfg) & getPicUrl(gif.thumb)) mediaObj["preview_url"] = %(getUrlPrefix(cfg) & getPicUrl(gif.thumb)) mediaObj["remote_url"] = newJNull() mediaObj["preview_remote_url"] = newJNull() mediaObj["text_url"] = newJNull() mediaObj["description"] = newJNull() # FIXME (not used by discord, i like a11y) # FIXME but this probably isnt used by discord mediaObj["meta"] = newJObject() media.add(mediaObj) 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 != 0: postJson["in_reply_to_id"] = %(&"{tweet.replyId}") postJson["in_reply_to_account_id"] = %"" 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"] = %*{ "id": &"{tweet.user.id}", "display_name": tweet.user.fullname, "username": tweet.user.username, "acct": tweet.user.username, "url": &"{getUrlPrefix(cfg)}/{tweet.user.username}", "uri": &"{getUrlPrefix(cfg)}/{tweet.user.username}", "created_at": $tweet.user.joinDate, "locked": tweet.user.protected, "bot": false, # TODO? "discoverable": true, "indexable": false, "group": false, "avatar": getUrlPrefix(cfg) & getPicUrl(tweet.user.userPic), "avatar_static": getUrlPrefix(cfg) & getPicUrl(tweet.user.userPic), "header": getUrlPrefix(cfg) & getPicUrl(tweet.user.banner), "header_static": getUrlPrefix(cfg) & getPicUrl(tweet.user.banner), "followers_count": tweet.user.followers, "following_count": tweet.user.following, "statuses_count": tweet.user.tweets, "hide_collections": false, "noindex": false, "emojis": @[], "roles": @[], "fields": @[], } 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": let id = @"id" 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 getTweet(id) if conv == nil: echo "nil conv" if conv == nil or conv.tweet == nil or conv.tweet.id == 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 postJson = getActivityStream(conv.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 getGraphUser(@"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://gitdab.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