# 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