Compare commits
No commits in common. "7a6548cb2b18001e0443f20ef93f76e57ef8acb8" and "6e86e086c3221b991181c7391930d25a32d937b8" have entirely different histories.
7a6548cb2b
...
6e86e086c3
17 changed files with 81 additions and 559 deletions
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -5,7 +5,7 @@ privacy and performance. The source is available on GitHub at
|
||||||
<https://github.com/zedeus/nitter>
|
<https://github.com/zedeus/nitter>
|
||||||
|
|
||||||
**This instance is running a fork, whose source can be found at**
|
**This instance is running a fork, whose source can be found at**
|
||||||
<https://gitlab.com/Cynosphere/nitter>
|
<https://gitdab.com/Cynosphere/nitter>
|
||||||
|
|
||||||
* No JavaScript or ads
|
* No JavaScript or ads
|
||||||
* All requests go through the backend, client never talks to Twitter
|
* All requests go through the backend, client never talks to Twitter
|
||||||
|
|
|
@ -100,7 +100,7 @@ proc getGraphTweet(id: string; after=""): Future[Conversation] {.async.} =
|
||||||
let
|
let
|
||||||
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
||||||
variables = tweetVariables % [id, cursor]
|
variables = tweetVariables % [id, cursor]
|
||||||
params = {"variables": variables, "features": gqlFeatures, "fieldToggles": tweetFieldToggles}
|
params = {"variables": variables, "features": gqlFeatures}
|
||||||
js = await fetch(graphTweet ? params, Api.tweetDetail)
|
js = await fetch(graphTweet ? params, Api.tweetDetail)
|
||||||
result = parseGraphConversation(js, id)
|
result = parseGraphConversation(js, id)
|
||||||
|
|
||||||
|
|
|
@ -116,8 +116,7 @@ template fetchImpl(result, additional_headers, fetchBody) {.dirty.} =
|
||||||
#release(token, used=true)
|
#release(token, used=true)
|
||||||
|
|
||||||
if resp.status == $Http400:
|
if resp.status == $Http400:
|
||||||
let errText = "body: '" & result & "' url: " & $url
|
raise newException(InternalError, $url)
|
||||||
raise newException(InternalError, errText)
|
|
||||||
except InternalError as e:
|
except InternalError as e:
|
||||||
raise e
|
raise e
|
||||||
except BadClientError as e:
|
except BadClientError as e:
|
||||||
|
|
|
@ -20,7 +20,7 @@ const
|
||||||
graphUserTweets* = graphql / "3JNH4e9dq1BifLxAa3UMWg/UserWithProfileTweetsQueryV2"
|
graphUserTweets* = graphql / "3JNH4e9dq1BifLxAa3UMWg/UserWithProfileTweetsQueryV2"
|
||||||
graphUserTweetsAndReplies* = graphql / "8IS8MaO-2EN6GZZZb8jF0g/UserWithProfileTweetsAndRepliesQueryV2"
|
graphUserTweetsAndReplies* = graphql / "8IS8MaO-2EN6GZZZb8jF0g/UserWithProfileTweetsAndRepliesQueryV2"
|
||||||
graphUserMedia* = graphql / "dexO_2tohK86JDudXXG3Yw/UserMedia"
|
graphUserMedia* = graphql / "dexO_2tohK86JDudXXG3Yw/UserMedia"
|
||||||
graphTweet* = graphql / "y90SwUGBZ3yz0yNUmCHgTw/TweetDetail"
|
graphTweet* = graphql / "q94uRCEn65LZThakYcPT6g/TweetDetail"
|
||||||
graphTweetResult* = graphql / "sITyJdhRPpvpEjg4waUmTA/TweetResultByIdQuery"
|
graphTweetResult* = graphql / "sITyJdhRPpvpEjg4waUmTA/TweetResultByIdQuery"
|
||||||
graphSearchTimeline* = graphql / "gkjsKepM6gl_HmFWoWKfgg/SearchTimeline"
|
graphSearchTimeline* = graphql / "gkjsKepM6gl_HmFWoWKfgg/SearchTimeline"
|
||||||
graphListById* = graphql / "iTpgCtbdxrsJfyx0cFjHqg/ListByRestId"
|
graphListById* = graphql / "iTpgCtbdxrsJfyx0cFjHqg/ListByRestId"
|
||||||
|
@ -95,36 +95,30 @@ const
|
||||||
"unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false,
|
"unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false,
|
||||||
"verified_phone_label_enabled": false,
|
"verified_phone_label_enabled": false,
|
||||||
"vibe_api_enabled": false,
|
"vibe_api_enabled": false,
|
||||||
"view_counts_everywhere_api_enabled": false,
|
"view_counts_everywhere_api_enabled": false
|
||||||
"premium_content_api_read_enabled": false,
|
|
||||||
"responsive_web_grok_analyze_button_fetch_trends_enabled": false,
|
|
||||||
"responsive_web_grok_analysis_button_from_backend": false,
|
|
||||||
"responsive_web_grok_analyze_post_followups_enabled": false,
|
|
||||||
"responsive_web_jetfuel_frame": false,
|
|
||||||
"profile_label_improvements_pcf_label_in_post_enabled": true,
|
|
||||||
"responsive_web_grok_image_annotation_enabled": false,
|
|
||||||
"responsive_web_grok_share_attachment_enabled": false,
|
|
||||||
"rweb_video_screen_enabled": false
|
|
||||||
}""".replace(" ", "").replace("\n", "")
|
}""".replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
tweetVariables* = """{
|
tweetVariables* = """{
|
||||||
"focalTweetId": "$1",
|
"focalTweetId": "$1",
|
||||||
$2
|
$2
|
||||||
"with_rux_injections": false,
|
"includeHasBirdwatchNotes": false,
|
||||||
"rankingMode": "Relevance",
|
|
||||||
"includePromotedContent": false,
|
"includePromotedContent": false,
|
||||||
"withCommunity": true,
|
"withBirdwatchNotes": false,
|
||||||
"withQuickPromoteEligibilityTweetFields": false,
|
"withVoice": false,
|
||||||
"withBirdwatchNotes": true,
|
"withV2Timeline": true
|
||||||
"withVoice": true
|
|
||||||
}""".replace(" ", "").replace("\n", "")
|
}""".replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
tweetFieldToggles* = """{
|
# oldUserTweetsVariables* = """{
|
||||||
"withArticleRichContentState": false,
|
# "userId": "$1", $2
|
||||||
"withArticlePlainText": true,
|
# "count": 20,
|
||||||
"withGrokAnalyze": false,
|
# "includePromotedContent": false,
|
||||||
"withDisallowedReplyControls": false
|
# "withDownvotePerspective": false,
|
||||||
}""".replace(" ", "").replace("\n", "")
|
# "withReactionsMetadata": false,
|
||||||
|
# "withReactionsPerspective": false,
|
||||||
|
# "withVoice": false,
|
||||||
|
# "withV2Timeline": true
|
||||||
|
# }
|
||||||
|
# """.replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
userTweetsVariables* = """{
|
userTweetsVariables* = """{
|
||||||
"rest_id": "$1",
|
"rest_id": "$1",
|
||||||
|
|
|
@ -31,7 +31,8 @@ let
|
||||||
illegalXmlRegex = re"(*UTF8)[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]"
|
illegalXmlRegex = re"(*UTF8)[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]"
|
||||||
|
|
||||||
proc getUrlPrefix*(cfg: Config): string =
|
proc getUrlPrefix*(cfg: Config): string =
|
||||||
"https://" & cfg.hostname
|
if cfg.useHttps: https & cfg.hostname
|
||||||
|
else: "http://" & cfg.hostname
|
||||||
|
|
||||||
proc shortLink*(text: string; length=28): string =
|
proc shortLink*(text: string; length=28): string =
|
||||||
result = text.replace(wwwRegex, "")
|
result = text.replace(wwwRegex, "")
|
||||||
|
|
|
@ -10,8 +10,7 @@ import types, config, prefs, formatters, redis_cache, http_pool
|
||||||
import views/[general, about]
|
import views/[general, about]
|
||||||
import routes/[
|
import routes/[
|
||||||
preferences, timeline, status, media, search, list, #rss, debug,
|
preferences, timeline, status, media, search, list, #rss, debug,
|
||||||
unsupported, embed, resolver, router_utils, home, follow, twitter_api,
|
unsupported, embed, resolver, router_utils, home, follow, twitter_api]
|
||||||
activityspoof]
|
|
||||||
|
|
||||||
const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
|
const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
|
||||||
const issuesUrl = "https://github.com/zedeus/nitter/issues"
|
const issuesUrl = "https://github.com/zedeus/nitter/issues"
|
||||||
|
@ -52,7 +51,6 @@ createEmbedRouter(cfg)
|
||||||
#createRssRouter(cfg)
|
#createRssRouter(cfg)
|
||||||
#createDebugRouter(cfg)
|
#createDebugRouter(cfg)
|
||||||
createTwitterApiRouter(cfg)
|
createTwitterApiRouter(cfg)
|
||||||
createActivityPubRouter(cfg)
|
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
port = Port(cfg.port)
|
port = Port(cfg.port)
|
||||||
|
@ -105,6 +103,5 @@ routes:
|
||||||
extend resolver, ""
|
extend resolver, ""
|
||||||
extend embed, ""
|
extend embed, ""
|
||||||
#extend debug, ""
|
#extend debug, ""
|
||||||
extend activityspoof, ""
|
|
||||||
extend api, ""
|
extend api, ""
|
||||||
extend unsupported, ""
|
extend unsupported, ""
|
||||||
|
|
|
@ -505,9 +505,7 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
|
||||||
if instructions.len == 0:
|
if instructions.len == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
for i in instructions:
|
for e in instructions[0]{"entries"}:
|
||||||
if i{"type"}.getStr == "TimelineAddEntries":
|
|
||||||
for e in i{"entries"}:
|
|
||||||
let entryId = e{"entryId"}.getStr
|
let entryId = e{"entryId"}.getStr
|
||||||
if entryId.startsWith("tweet"):
|
if entryId.startsWith("tweet"):
|
||||||
with tweetResult, e{"content", "itemContent", "tweet_results", "result"}:
|
with tweetResult, e{"content", "itemContent", "tweet_results", "result"}:
|
||||||
|
@ -539,7 +537,7 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
|
||||||
else:
|
else:
|
||||||
result.replies.content.add thread
|
result.replies.content.add thread
|
||||||
elif entryId.startsWith("cursor-bottom"):
|
elif entryId.startsWith("cursor-bottom"):
|
||||||
result.replies.bottom = e{"content", "value"}.getStr
|
result.replies.bottom = e{"content", "itemContent", "value"}.getStr
|
||||||
|
|
||||||
proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile =
|
proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile =
|
||||||
result = Profile(tweets: Timeline(beginning: after.len == 0))
|
result = Profile(tweets: Timeline(beginning: after.len == 0))
|
||||||
|
|
|
@ -1,242 +0,0 @@
|
||||||
# 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"] = %(&"https://{gif.url}")
|
|
||||||
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://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
|
|
|
@ -1,16 +1,16 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import json, asyncdispatch, strutils, sequtils, uri, options, sugar, strformat, times
|
import asyncdispatch, strutils, sequtils, uri, options, sugar, strformat
|
||||||
|
|
||||||
import jester, karax/vdom
|
import jester, karax/vdom
|
||||||
|
|
||||||
import router_utils
|
import router_utils
|
||||||
import ".."/[types, formatters, api]
|
import ".."/[types, formatters, api]
|
||||||
import ../views/[general, status, search, mastoapi]
|
import ../views/[general, status, search]
|
||||||
|
|
||||||
export json, uri, sequtils, options, sugar, times
|
export uri, sequtils, options, sugar
|
||||||
export router_utils
|
export router_utils
|
||||||
export api, formatters
|
export api, formatters
|
||||||
export status, mastoapi
|
export status
|
||||||
|
|
||||||
proc createStatusRouter*(cfg: Config) =
|
proc createStatusRouter*(cfg: Config) =
|
||||||
router status:
|
router status:
|
||||||
|
@ -39,35 +39,7 @@ proc createStatusRouter*(cfg: Config) =
|
||||||
|
|
||||||
get "/@name/status/@id/?":
|
get "/@name/status/@id/?":
|
||||||
cond '.' notin @"name"
|
cond '.' notin @"name"
|
||||||
var id = @"id"
|
let id = @"id"
|
||||||
var rawFile = false
|
|
||||||
if id.endsWith(".mp4"):
|
|
||||||
rawFile = true
|
|
||||||
id.removeSuffix(".mp4")
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if id.len > 19 or id.any(c => not c.isDigit):
|
if id.len > 19 or id.any(c => not c.isDigit):
|
||||||
resp Http404, showError("Invalid tweet ID", cfg)
|
resp Http404, showError("Invalid tweet ID", cfg)
|
||||||
|
@ -107,7 +79,7 @@ proc createStatusRouter*(cfg: Config) =
|
||||||
|
|
||||||
if tweet.quote.isSome():
|
if tweet.quote.isSome():
|
||||||
let
|
let
|
||||||
quote = get(tweet.quote)
|
quote = tweet.quote.get()
|
||||||
quoteUser = quote.user
|
quoteUser = quote.user
|
||||||
if tweet.replyId != 0:
|
if tweet.replyId != 0:
|
||||||
context = &"↩ Replying to: @{tweet.replyHandle}\n↘ Quoting: {quoteUser.fullname} (@{quoteUser.username})"
|
context = &"↩ Replying to: @{tweet.replyHandle}\n↘ Quoting: {quoteUser.fullname} (@{quoteUser.username})"
|
||||||
|
@ -130,7 +102,7 @@ proc createStatusRouter*(cfg: Config) =
|
||||||
elif tweet.gif.isSome():
|
elif tweet.gif.isSome():
|
||||||
let gif = get(tweet.gif)
|
let gif = get(tweet.gif)
|
||||||
images = @[gif.thumb]
|
images = @[gif.thumb]
|
||||||
video = getUrlPrefix(cfg) & getPicUrl(gif.url)
|
video = getPicUrl(gif.url)
|
||||||
#elif tweet.card.isSome():
|
#elif tweet.card.isSome():
|
||||||
# let card = tweet.card.get()
|
# let card = tweet.card.get()
|
||||||
# if card.image.len > 0:
|
# if card.image.len > 0:
|
||||||
|
@ -138,13 +110,10 @@ proc createStatusRouter*(cfg: Config) =
|
||||||
# elif card.video.isSome():
|
# elif card.video.isSome():
|
||||||
# images = @[card.video.get().thumb]
|
# images = @[card.video.get().thumb]
|
||||||
|
|
||||||
if rawFile and video != "":
|
|
||||||
redirect(video)
|
|
||||||
|
|
||||||
let html = renderConversation(conv, prefs, getPath() & "#m")
|
let html = renderConversation(conv, prefs, getPath() & "#m")
|
||||||
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle,
|
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle,
|
||||||
images=images, video=video, avatar=avatar, time=time,
|
images=images, video=video, avatar=avatar, time=time,
|
||||||
context=context, contextUrl=contextUrl, id=id)
|
context=context, contextUrl=contextUrl)
|
||||||
|
|
||||||
get "/@name/@s/@id/@m/?@i?":
|
get "/@name/@s/@id/@m/?@i?":
|
||||||
cond @"s" in ["status", "statuses"]
|
cond @"s" in ["status", "statuses"]
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import asyncdispatch, strutils, sequtils, uri, options, times, json
|
import asyncdispatch, strutils, sequtils, uri, options, times
|
||||||
import jester, karax/vdom
|
import jester, karax/vdom
|
||||||
|
|
||||||
import router_utils
|
import router_utils
|
||||||
import ".."/[types, formatters, query, api]
|
import ".."/[types, formatters, query, api]
|
||||||
import ../views/[general, profile, timeline, status, search, mastoapi]
|
import ../views/[general, profile, timeline, status, search]
|
||||||
|
|
||||||
export vdom
|
export vdom
|
||||||
export uri, sequtils, json
|
export uri, sequtils
|
||||||
export router_utils
|
export router_utils
|
||||||
export formatters, query, api
|
export formatters, query, api
|
||||||
export profile, timeline, status, mastoapi
|
export profile, timeline, status
|
||||||
|
|
||||||
proc getQuery*(request: Request; tab, name: string): Query =
|
proc getQuery*(request: Request; tab, name: string): Query =
|
||||||
case tab
|
case tab
|
||||||
|
@ -137,18 +137,6 @@ proc createTimelineRouter*(cfg: Config) =
|
||||||
of "following":
|
of "following":
|
||||||
resp renderMain(renderUserList(await getGraphFollowing(await getUserId(@"name"), getCursor()), prefs), request, cfg, prefs)
|
resp renderMain(renderUserList(await getGraphFollowing(await getUserId(@"name"), getCursor()), prefs), request, cfg, prefs)
|
||||||
else:
|
else:
|
||||||
if request.headers.hasKey("Accept") and request.headers["Accept"] == "application/activity+json":
|
|
||||||
let userId = await getUserId(@"name")
|
|
||||||
|
|
||||||
if userId == "suspended" or userId.len == 0:
|
|
||||||
resp Http404, {"Content-Type": "application/json"}, """{"error":"User not found"}"""
|
|
||||||
|
|
||||||
let user = await getGraphUser(@"name")
|
|
||||||
|
|
||||||
let userJson = getActivityStream(user, cfg, prefs)
|
|
||||||
|
|
||||||
resp Http200, {"Content-Type": "application/json"}, $userJson
|
|
||||||
|
|
||||||
var query = request.getQuery(@"tab", @"name")
|
var query = request.getQuery(@"tab", @"name")
|
||||||
if names.len != 1:
|
if names.len != 1:
|
||||||
query.fromUser = names
|
query.fromUser = names
|
||||||
|
|
|
@ -138,7 +138,7 @@ proc createTwitterApiRouter*(cfg: Config) =
|
||||||
let response = await getUserProfileJson(username)
|
let response = await getUserProfileJson(username)
|
||||||
respJson response
|
respJson response
|
||||||
|
|
||||||
#get "/api/user/@id/tweets":
|
# get "/api/user/@id/tweets":
|
||||||
# let id = @"id"
|
# let id = @"id"
|
||||||
# let response = await getUserTweetsJson(id)
|
# let response = await getUserTweetsJson(id)
|
||||||
# respJson response
|
# respJson response
|
||||||
|
|
|
@ -5,7 +5,7 @@ import karax/[karaxdsl, vdom]
|
||||||
const
|
const
|
||||||
date = staticExec("git show -s --format=\"%cd\" --date=format:\"%Y.%m.%d\"")
|
date = staticExec("git show -s --format=\"%cd\" --date=format:\"%Y.%m.%d\"")
|
||||||
hash = staticExec("git show -s --format=\"%h\"")
|
hash = staticExec("git show -s --format=\"%h\"")
|
||||||
link = "https://gitlab.com/Cynosphere/nitter/commit/" & hash
|
link = "https://gitdab.com/Cynosphere/nitter/commit/" & hash
|
||||||
version = &"{date}-{hash}"
|
version = &"{date}-{hash}"
|
||||||
|
|
||||||
var aboutHtml: string
|
var aboutHtml: string
|
||||||
|
|
|
@ -3,7 +3,7 @@ import uri, strutils, strformat, times, options
|
||||||
import karax/[karaxdsl, vdom]
|
import karax/[karaxdsl, vdom]
|
||||||
|
|
||||||
import renderutils
|
import renderutils
|
||||||
import ".."/[utils, types, prefs, formatters]
|
import ../utils, ../types, ../prefs, ../formatters
|
||||||
|
|
||||||
import jester
|
import jester
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
|
||||||
proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||||
video=""; images: seq[string] = @[]; banner=""; ogTitle="";
|
video=""; images: seq[string] = @[]; banner=""; ogTitle="";
|
||||||
rss=""; canonical=""; avatar=""; context=""; contextUrl="";
|
rss=""; canonical=""; avatar=""; context=""; contextUrl="";
|
||||||
id=""; time: Option[DateTime] = none(DateTime)): VNode =
|
time: Option[DateTime] = none(DateTime)): VNode =
|
||||||
var theme = prefs.theme.toTheme
|
var theme = prefs.theme.toTheme
|
||||||
if "theme" in req.params:
|
if "theme" in req.params:
|
||||||
theme = req.params["theme"].toTheme
|
theme = req.params["theme"].toTheme
|
||||||
|
@ -62,9 +62,9 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||||
if theme.len > 0:
|
if theme.len > 0:
|
||||||
link(rel="stylesheet", type="text/css", href=(&"/css/themes/{theme}.css"))
|
link(rel="stylesheet", type="text/css", href=(&"/css/themes/{theme}.css"))
|
||||||
|
|
||||||
link(rel="icon", type="image/png", sizes="32x32", href=(&"{getUrlPrefix(cfg)}/favicon-32x32.png"))
|
|
||||||
link(rel="icon", type="image/png", sizes="16x16", href=(&"{getUrlPrefix(cfg)}/favicon-16x16.png"))
|
|
||||||
link(rel="apple-touch-icon", sizes="180x180", href="/apple-touch-icon.png")
|
link(rel="apple-touch-icon", sizes="180x180", href="/apple-touch-icon.png")
|
||||||
|
link(rel="icon", type="image/png", sizes="32x32", href="/favicon-32x32.png")
|
||||||
|
link(rel="icon", type="image/png", sizes="16x16", href="/favicon-16x16.png")
|
||||||
link(rel="manifest", href="/site.webmanifest")
|
link(rel="manifest", href="/site.webmanifest")
|
||||||
link(rel="mask-icon", href="/safari-pinned-tab.svg", color="#ff6c60")
|
link(rel="mask-icon", href="/safari-pinned-tab.svg", color="#ff6c60")
|
||||||
link(rel="search", type="application/opensearchdescription+xml", title=cfg.title,
|
link(rel="search", type="application/opensearchdescription+xml", title=cfg.title,
|
||||||
|
@ -104,9 +104,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||||
|
|
||||||
var siteName = "Nitter"
|
var siteName = "Nitter"
|
||||||
|
|
||||||
let isDiscord = req.headers.getOrDefault("User-Agent").toString().contains("Discordbot")
|
if time.isSome:
|
||||||
|
|
||||||
if time.isSome and not isDiscord:
|
|
||||||
let timeObj = time.get
|
let timeObj = time.get
|
||||||
let timeStr = $timeObj
|
let timeStr = $timeObj
|
||||||
meta(property="og:article:published_time", content=timeStr)
|
meta(property="og:article:published_time", content=timeStr)
|
||||||
|
@ -157,14 +155,12 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||||
if contextUrl != "":
|
if contextUrl != "":
|
||||||
url = contextUrl
|
url = contextUrl
|
||||||
|
|
||||||
link(rel="alternate", href=(&"{getUrlPrefix(cfg)}/oembed.json?type=video&provider={encodeUrl(siteName)}&title={title}&user={author}&url={encodeUrl(url)}"), type="application/json+oembed")
|
verbatim &"<link rel=\"alternate\" href=\"{getUrlPrefix(cfg)}/oembed.json?type=video&provider={encodeUrl(siteName)}&title={title}&user={author}&url={encodeUrl(url)}\" type=\"application/json+oembed\" />"
|
||||||
elif context != "" and contextUrl != "":
|
elif context != "" and contextUrl != "":
|
||||||
var
|
var
|
||||||
title = encodeUrl(finalizedTitleText)
|
title = encodeUrl(finalizedTitleText)
|
||||||
author = encodeUrl(context)
|
author = encodeUrl(context)
|
||||||
link(rel="alternate", href=(&"{getUrlPrefix(cfg)}/oembed.json?type=video&provider={encodeUrl(siteName)}&title={title}&user={author}&url={encodeUrl(contextUrl)}"), type="application/json+oembed")
|
verbatim &"<link rel=\"alternate\" href=\"{getUrlPrefix(cfg)}/oembed.json?type=video&provider={encodeUrl(siteName)}&title={title}&user={author}&url={encodeUrl(contextUrl)}\" type=\"application/json+oembed\" />"
|
||||||
|
|
||||||
link(rel="alternate", href=(&"{getUrlPrefix(cfg)}/users/i/statuses/{id}"), type="application/activity+json")
|
|
||||||
|
|
||||||
# this is last so images are also preloaded
|
# this is last so images are also preloaded
|
||||||
# if this is done earlier, Chrome only preloads one image for some reason
|
# if this is done earlier, Chrome only preloads one image for some reason
|
||||||
|
@ -174,14 +170,14 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||||
proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs;
|
proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs;
|
||||||
titleText=""; desc=""; ogTitle=""; rss=""; video="";
|
titleText=""; desc=""; ogTitle=""; rss=""; video="";
|
||||||
images: seq[string] = @[]; banner=""; avatar=""; context="";
|
images: seq[string] = @[]; banner=""; avatar=""; context="";
|
||||||
contextUrl=""; id=""; time: Option[DateTime] = none(DateTime)
|
contextUrl = ""; time: Option[DateTime] = none(DateTime)
|
||||||
): string =
|
): string =
|
||||||
|
|
||||||
let canonical = getTwitterLink(req.path, req.params)
|
let canonical = getTwitterLink(req.path, req.params)
|
||||||
|
|
||||||
let node = buildHtml(html(lang="en")):
|
let node = buildHtml(html(lang="en")):
|
||||||
renderHead(prefs, cfg, req, titleText, desc, video, images, banner, ogTitle,
|
renderHead(prefs, cfg, req, titleText, desc, video, images, banner, ogTitle,
|
||||||
rss, canonical, avatar, context, contextUrl, id, time)
|
rss, canonical, avatar, context, contextUrl, time)
|
||||||
|
|
||||||
body:
|
body:
|
||||||
renderNavbar(cfg, req, rss, canonical)
|
renderNavbar(cfg, req, rss, canonical)
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
import strutils, strformat, options, json, sequtils, times
|
|
||||||
import ".."/[types, formatters, utils]
|
|
||||||
|
|
||||||
proc formatTweetForMastoAPI*(tweet: Tweet, cfg: Config, prefs: Prefs): string =
|
|
||||||
var content = replaceUrls(tweet.text, prefs)
|
|
||||||
|
|
||||||
if tweet.quote.isSome():
|
|
||||||
let
|
|
||||||
quote = get(tweet.quote)
|
|
||||||
quoteContent = replaceUrls(quote.text, prefs)
|
|
||||||
quoteUrl = &"{getUrlPrefix(cfg)}/i/status/{quote.id}"
|
|
||||||
content &= &"\n\n<blockquote><b>↘ <a href=\"{quoteUrl}\">{quote.user.fullName} (@{quote.user.username})</a></b>\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 &= "</blockquote>"
|
|
||||||
|
|
||||||
if tweet.birdwatch.isSome():
|
|
||||||
let
|
|
||||||
note = get(tweet.birdwatch)
|
|
||||||
noteContent = replaceUrls(note.text, prefs)
|
|
||||||
content &= &"\n<blockquote><b>ⓘ {note.title}</b>\n{noteContent}</blockquote>"
|
|
||||||
|
|
||||||
result = content.replace("\n", "<br>")
|
|
||||||
|
|
||||||
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 url in tweet.photos:
|
|
||||||
let image = getUrlPrefix(cfg) & getPicUrl(url)
|
|
||||||
var mediaObj = newJObject()
|
|
||||||
|
|
||||||
mediaObj["type"] = %"Document"
|
|
||||||
mediaObj["mediaType"] = %"image/png"
|
|
||||||
mediaObj["url"] = %image
|
|
||||||
mediaObj["name"] = newJNull() # FIXME a11y
|
|
||||||
|
|
||||||
media.add(mediaObj)
|
|
||||||
|
|
||||||
if tweet.video.isSome():
|
|
||||||
let
|
|
||||||
videoObj = get(tweet.video)
|
|
||||||
vars = videoObj.variants.filterIt(it.contentType == mp4)
|
|
||||||
var mediaObj = newJObject()
|
|
||||||
|
|
||||||
mediaObj["type"] = %"Document"
|
|
||||||
mediaObj["mediaType"] = %"video/mp4"
|
|
||||||
mediaObj["url"] = %vars[^1].url
|
|
||||||
mediaObj["name"] = newJNull() # FIXME a11y
|
|
||||||
|
|
||||||
media.add(mediaObj)
|
|
||||||
elif tweet.gif.isSome():
|
|
||||||
let gif = get(tweet.gif)
|
|
||||||
var mediaObj = newJObject()
|
|
||||||
|
|
||||||
mediaObj["type"] = %"Document"
|
|
||||||
mediaObj["mediaType"] = %"video/mp4"
|
|
||||||
mediaObj["url"] = %(&"https://{gif.url}")
|
|
||||||
mediaObj["name"] = newJNull() # FIXME a11y
|
|
||||||
|
|
||||||
media.add(mediaObj)
|
|
||||||
|
|
||||||
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 != 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 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"] = newJArray()
|
|
||||||
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
|
|
|
@ -24,9 +24,9 @@ proc renderSearch*(): VNode =
|
||||||
buildHtml(tdiv(class="panel-container")):
|
buildHtml(tdiv(class="panel-container")):
|
||||||
tdiv(class="search-bar"):
|
tdiv(class="search-bar"):
|
||||||
form(`method`="get", action="/search", autocomplete="off"):
|
form(`method`="get", action="/search", autocomplete="off"):
|
||||||
hiddenField("f", "tweets")
|
hiddenField("f", "users")
|
||||||
input(`type`="text", name="q", autofocus="",
|
input(`type`="text", name="q", autofocus="",
|
||||||
placeholder="Search...", dir="auto")
|
placeholder="Enter username...", dir="auto")
|
||||||
button(`type`="submit"): icon "search"
|
button(`type`="submit"): icon "search"
|
||||||
|
|
||||||
proc renderProfileTabs*(query: Query; username: string; cfg: Config): VNode =
|
proc renderProfileTabs*(query: Query; username: string; cfg: Config): VNode =
|
||||||
|
|
|
@ -351,7 +351,7 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
|
||||||
if tweet.quote.isSome:
|
if tweet.quote.isSome:
|
||||||
renderQuote(tweet.quote.get(), prefs, path)
|
renderQuote(tweet.quote.get(), prefs, path)
|
||||||
|
|
||||||
if tweet.birdwatch.isSome:
|
if mainTweet and tweet.birdwatch.isSome:
|
||||||
renderCommunityNote(tweet.birdwatch.get(), prefs)
|
renderCommunityNote(tweet.birdwatch.get(), prefs)
|
||||||
|
|
||||||
if mainTweet:
|
if mainTweet:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue