nitter/src/routes/activityspoof.nim
2025-03-22 22:50:31 -06:00

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