# SPDX-License-Identifier: AGPL-3.0-only
import strutils, strformat, options, json, sequtils, times, math
import ".."/[types, formatters, utils]
proc formatTweetForMastoAPI*(tweet: Tweet, cfg: Config, prefs: Prefs): string =
var content = replaceUrls(tweet.text, prefs, absolute=getUrlPrefix(cfg))
if tweet.poll.isSome():
let poll = get(tweet.poll)
content &= "\n
"
for i in 0 ..< poll.options.len:
let
leader = if poll.leader == i: " leader" else: ""
val = poll.values[i]
perc = if val > 0: val / poll.votes * 100 else: 0
percStr = (&"{perc:>3.0f}").strip(chars={'.'}) & '%'
barLen = round((perc / 100) * 32).int
bar = repeat("ā", barLen)
notBar = repeat(" ", 32 - barLen)
content &= &"{poll.options[i]} ({insertSep($val, ',')}, {percStr})\n{bar}{notBar}\n"
content &= &"\n{insertSep($poll.votes, ',')} votes ⢠{poll.status}
"
if tweet.quote.isSome():
let
quote = get(tweet.quote)
quoteContent = replaceUrls(quote.text, prefs, absolute=getUrlPrefix(cfg))
quoteUrl = &"{getUrlPrefix(cfg)}/i/status/{quote.id}"
content &= &"\n\nā {quote.user.fullName} (@{quote.user.username})\n{quoteContent}"
if quote.video.isSome() or quote.gif.isSome():
content &= "\nš¹"
if quote.gif.isSome():
content &= " (GIF)"
elif quote.photos.len > 0:
content &= "\nš¼ļø"
if quote.photos.len > 1:
content &= &" ({quote.photos.len})"
content &= "
"
if tweet.birdwatch.isSome():
let
note = get(tweet.birdwatch)
noteContent = replaceUrls(note.text, prefs, absolute=getUrlPrefix(cfg))
content &= &"\n\nā {note.title}\n{noteContent}
"
result = content.replace("\n", "
")
proc getActivityStream*(tweet: Tweet, cfg: Config, prefs: Prefs): JsonNode =
let
tweetUrl = &"{getUrlPrefix(cfg)}/i/status/{tweet.id}"
tweetContent = formatTweetForMastoAPI(tweet, cfg, prefs)
var media: seq[JsonNode] = @[]
if tweet.photos.len > 0:
for imageObj in tweet.photos:
let
image = getUrlPrefix(cfg) & getPicUrl(imageObj.url)
splitUrl = imageObj.url.split('.')
var filetype = splitUrl[^1]
if filetype == "jpg":
filetype = "jpeg"
var mediaObj = newJObject()
mediaObj["type"] = %"Image"
mediaObj["mediaType"] = %("image/" & filetype)
mediaObj["url"] = %image
mediaObj["name"] = %imageObj.description
media.add(mediaObj)
if tweet.video.isSome():
let
videoObj = get(tweet.video)
vars = videoObj.variants.filterIt(it.contentType == mp4)
var description = videoObj.title
if videoObj.description.len > 0:
description = videoObj.description
let splitUrl = videoObj.thumb.split('.')
var filetype = splitUrl[^1]
if filetype == "jpg":
filetype = "jpeg"
var url: seq[JsonNode] = @[]
var thumb = newJObject()
thumb["type"] = %"Link"
thumb["mediaType"] = %("image/" & filetype)
thumb["href"] = %(getUrlPrefix(cfg) & getPicUrl(videoObj.thumb))
url.add(thumb)
var mediaObj = newJObject()
mediaObj["type"] = %"Link"
mediaObj["mediaType"] = %"video/mp4"
mediaObj["href"] = %(vars[^1].url.replace("https://video.twimg.com", getUrlPrefix(cfg) & "/tvid").replace(".mp4", ""))
url.add(mediaObj)
var wrapper = newJObject()
wrapper["type"] = %"Video"
wrapper["name"] = %description
wrapper["url"] = %url
media.add(wrapper)
elif tweet.gif.isSome():
let
gif = get(tweet.gif)
gifUrl = https & gif.url
let splitUrl = gif.thumb.split('.')
var filetype = splitUrl[^1]
if filetype == "jpg":
filetype = "jpeg"
var url: seq[JsonNode] = @[]
var thumb = newJObject()
thumb["type"] = %"Link"
thumb["mediaType"] = %("image/" & filetype)
thumb["href"] = %(getUrlPrefix(cfg) & getPicUrl(gif.thumb))
url.add(thumb)
var mediaObj = newJObject()
mediaObj["type"] = %"Link"
mediaObj["mediaType"] = %"video/mp4"
mediaObj["href"] = %(gifUrl.replace("https://video.twimg.com", getUrlPrefix(cfg) & "/tvid").replace(".mp4", ""))
url.add(mediaObj)
var wrapper = newJObject()
wrapper["type"] = %"Video"
wrapper["name"] = newJNull()
wrapper["url"] = %url
media.add(wrapper)
var context: seq[JsonNode] = @[]
let contextUrl: JsonNode = %"https://www.w3.org/ns/activitystreams"
context.add(contextUrl)
let asProps: JsonNode = %*{
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"sensitive": "as:sensitive",
}
context.add(asProps)
var postJson = newJObject()
postJson["@context"] = %context
postJson["id"] = %tweetUrl
postJson["type"] = %"Note"
postJson["summary"] = newJNull()
if tweet.replyId.len != 0:
let replyUrl = &"{getUrlPrefix(cfg)}/i/status/{tweet.replyId}"
postJson["inReplyTo"] = %replyUrl
postJson["inReplyToAtomUri"] = %replyUrl
else:
postJson["inReplyTo"] = newJNull()
postJson["inReplyToAtomUri"] = newJNull()
postJson["published"] = %($tweet.time)
postJson["url"] = %tweetUrl
postJson["attributedTo"] = %(&"{getUrlPrefix(cfg)}/users/{tweet.user.username}")
postJson["to"] = newJArray()
postJson["cc"] = %(@["https://www.w3.org/ns/activitystreams#Public"])
postJson["sensitive"] = %false # FIXME
postJson["atomUri"] = %tweetUrl
postJson["conversation"] = %""
postJson["content"] = %tweetContent
postJson["contentMap"] = %*{
"en": tweetContent
}
postJson["attachment"] = %media
postJson["tag"] = newJArray() # TODO: parse?
postJson["replies"] = newJObject()
result = postJson
proc getActivityStream*(user: User, cfg: Config, prefs: Prefs): JsonNode =
let userUrl = &"{getUrlPrefix(cfg)}/{user.username}"
var context: seq[JsonNode] = @[]
let contextUrl: JsonNode = %"https://www.w3.org/ns/activitystreams"
context.add(contextUrl)
let contextUrl2: JsonNode = %"https://w3id.org/security/v1"
context.add(contextUrl2)
let contextAka: JsonNode = %*{
"@id": "as:alsoKnownAs",
"@type": "@id"
}
let contextMovedTo = %*{
"@id": "as:movedTo",
"@type": "@id"
}
var asProps: JsonNode = %*{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
}
asProps["alsoKnownAs"] = contextAka
asProps["movedTo"] = contextMovedTo
context.add(asProps)
var fields: seq[JsonNode] = @[]
if user.location.len > 0:
var location = newJObject()
location["type"] = %"PropertyValue"
location["name"] = %"Location"
location["value"] = %user.location
fields.add(location)
if user.website.len > 0:
var website = newJObject()
website["type"] = %"PropertyValue"
website["name"] = %"Website"
website["value"] = %(&"{user.website}")
fields.add(website)
if user.botOwner.len > 0:
var botOwner = newJObject()
botOwner["type"] = %"PropertyValue"
botOwner["name"] = %"Automated by"
botOwner["value"] = %(&"{user.botOwner}")
fields.add(botOwner)
if user.pcf != "" and user.pcf != "None":
var pcf = newJObject()
pcf["type"] = %"PropertyValue"
pcf["name"] = %"PCF Label"
pcf["value"] = %user.pcf
fields.add(pcf)
if user.verifiedType != none:
var verified = newJObject()
verified["type"] = %"PropertyValue"
verified["name"] = %"Verified Type"
verified["value"] = %user.verifiedType
fields.add(verified)
var userJson = newJObject()
userJson["@context"] = %context
userJson["id"] = %userUrl
userJson["type"] = %"Person"
userJson["following"] = %(userUrl & "/following")
userJson["followers"] = %(userUrl & "/followers")
userJson["inbox"] = newJNull()
userJson["outbox"] = newJNull()
userJson["featured"] = newJNull()
userJson["featuredTags"] = newJNull()
userJson["preferredUsername"] = %user.username
userJson["name"] = %user.fullname
userJson["summary"] = %user.bio
userJson["url"] = %userUrl
userJson["manuallyApprovesFollowers"] = %user.protected
userJson["discoverable"] = %true
userJson["indexable"] = %false
userJson["published"] = %($user.joinDate)
userJson["memorial"] = %false
userJson["publicKey"] = newJNull()
userJson["tag"] = newJArray()
userJson["attachment"] = %fields
userJson["endpoints"] = newJObject()
userJson["icon"] = %*{
"type": "Image",
"mediaType": "image/jpeg",
"url": getUrlPrefix(cfg) & getPicUrl(user.userPic)
}
userJson["image"] = %*{
"type": "Image",
"mediaType": "image/jpeg",
"url": getUrlPrefix(cfg) & getPicUrl(user.banner)
}
result = userJson
proc getMastoAPIUser*(user: User, cfg: Config): JsonNode =
var fields: seq[JsonNode] = @[]
if user.location.len > 0:
var location = newJObject()
location["name"] = %"Location"
location["value"] = %user.location
location["verified_at"] = newJNull()
fields.add(location)
if user.website.len > 0:
var website = newJObject()
website["name"] = %"Website"
website["value"] = %(&"{user.website}")
website["verified_at"] = newJNull()
fields.add(website)
if user.botOwner.len > 0:
var botOwner = newJObject()
botOwner["name"] = %"Automated by"
botOwner["value"] = %(&"{user.botOwner}")
botOwner["verified_at"] = newJNull()
fields.add(botOwner)
if user.pcf != "" and user.pcf != "None":
var pcf = newJObject()
pcf["name"] = %"PCF Label"
pcf["value"] = %user.pcf
pcf["verified_at"] = newJNull()
fields.add(pcf)
if user.verifiedType != none:
var verified = newJObject()
verified["name"] = %"Verified Type"
verified["value"] = %user.verifiedType
verified["verified_at"] = newJNull()
fields.add(verified)
var userJson = newJObject()
userJson["id"] = %user.id
userJson["username"] = %user.username
userJson["acct"] = %user.username
userJson["display_name"] = %user.fullname
userJson["locked"] = %user.protected
userJson["bot"] = %user.bot
userJson["discoverable"] = %true
userJson["indexable"] = %false
userJson["group"] = %false
userJson["created_at"] = %($user.joinDate)
userJson["note"] = %user.bio
userJson["url"] = %(&"{getUrlPrefix(cfg)}/{user.username}")
userJson["uri"] = %(&"{getUrlPrefix(cfg)}/{user.username}")
userJson["avatar"] = %(getUrlPrefix(cfg) & getPicUrl(user.userPic))
userJson["avatar_static"] = %(getUrlPrefix(cfg) & getPicUrl(user.userPic))
userJson["header"] = %(getUrlPrefix(cfg) & getPicUrl(user.banner))
userJson["header_static"] = %(getUrlPrefix(cfg) & getPicUrl(user.banner))
userJson["followers_count"] = %user.followers
userJson["following_count"] = %user.following
userJson["statuses_count"] = %user.tweets
userJson["hide_collections"] = %false
userJson["noindex"] = %false
userJson["emojis"] = %(@[])
userJson["roles"] = %(@[])
userJson["fields"] = %fields
result = userJson