351 lines
12 KiB
Nim
351 lines
12 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, 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"] = %(&"<a href=\"{tweet.user.website}\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\">{tweet.user.website}</a>")
|
|
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
|