243 lines
8.5 KiB
Nim
243 lines
8.5 KiB
Nim
# 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
|