From f4baef9a9bd018a1bccb12c85cd93acef3df2d5b Mon Sep 17 00:00:00 2001 From: PrivacyDev Date: Fri, 2 Jun 2023 23:47:05 -0400 Subject: [PATCH] added favoriters and retweeters endpoints --- src/api.nim | 18 ++++++++++++++++++ src/consts.nim | 8 ++++++++ src/parser.nim | 27 +++++++++++++++++++++++++++ src/routes/status.nim | 25 ++++++++++++++++++++++++- src/tokens.nim | 2 +- src/types.nim | 3 +++ src/views/search.nim | 5 +++++ 7 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/api.nim b/src/api.nim index c63a5b5..5e6eee4 100644 --- a/src/api.nim +++ b/src/api.nim @@ -94,6 +94,24 @@ proc getGraphTweet(id: string; after=""): Future[Conversation] {.async.} = js = await fetch(graphTweet ? params, Api.tweetDetail) result = parseGraphConversation(js, id) +proc getGraphFavoriters*(id: string; after=""): Future[UsersTimeline] {.async.} = + if id.len == 0: return + let + cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: "" + variables = reactorsVariables % [id, cursor] + params = {"variables": variables, "features": gqlFeatures} + js = await fetch(graphFavoriters ? params, Api.favoriters) + result = parseGraphFavoritersTimeline(js, id) + +proc getGraphRetweeters*(id: string; after=""): Future[UsersTimeline] {.async.} = + if id.len == 0: return + let + cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: "" + variables = reactorsVariables % [id, cursor] + params = {"variables": variables, "features": gqlFeatures} + js = await fetch(graphRetweeters ? params, Api.retweeters) + result = parseGraphRetweetersTimeline(js, id) + proc getReplies*(id, after: string): Future[Result[Chain]] {.async.} = result = (await getGraphTweet(id, after)).replies result.beginning = after.len == 0 diff --git a/src/consts.nim b/src/consts.nim index 4063876..ecd2e0b 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -26,6 +26,8 @@ const graphListBySlug* = graphql / "-kmqNvm5Y-cVrfvBy6docg/ListBySlug" graphListMembers* = graphql / "P4NpVZDqUD_7MEM84L-8nw/ListMembers" graphListTweets* = graphql / "jZntL0oVJSdjhmPcdbw_eA/ListLatestTweetsTimeline" + graphFavoriters* = graphql / "mDc_nU8xGv0cLRWtTaIEug/Favoriters" + graphRetweeters* = graphql / "RCR9gqwYD1NEgi9FWzA50A/Retweeters" timelineParams* = { "include_profile_interstitial_type": "0", @@ -122,3 +124,9 @@ const "withReactionsPerspective": false, "withVoice": false }""" + + reactorsVariables* = """{ + "tweetId" : "$1", $2 + "count" : 20, + "includePromotedContent": false +}""" diff --git a/src/parser.nim b/src/parser.nim index 5ec21e4..21f8561 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -493,6 +493,33 @@ proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Timeline = elif entryId.startsWith("cursor-bottom"): result.bottom = e{"content", "value"}.getStr +proc parseGraphUsersTimeline(js: JsonNode; root: string; key: string; after=""): UsersTimeline = + result = UsersTimeline(beginning: after.len == 0) + + let instructions = ? js{"data", key, "timeline", "instructions"} + + if instructions.len == 0: + return + + for i in instructions: + if i{"type"}.getStr == "TimelineAddEntries": + for e in i{"entries"}: + let entryId = e{"entryId"}.getStr + if entryId.startsWith("user"): + with graphUser, e{"content", "itemContent"}: + let user = parseGraphUser(graphUser) + result.content.add user + elif entryId.startsWith("cursor-bottom"): + result.bottom = e{"content", "value"}.getStr + elif entryId.startsWith("cursor-top"): + result.top = e{"content", "value"}.getStr + +proc parseGraphFavoritersTimeline*(js: JsonNode; root: string; after=""): UsersTimeline = + return parseGraphUsersTimeline(js, root, "favoriters_timeline", after) + +proc parseGraphRetweetersTimeline*(js: JsonNode; root: string; after=""): UsersTimeline = + return parseGraphUsersTimeline(js, root, "retweeters_timeline", after) + proc parseGraphSearch*(js: JsonNode; after=""): Timeline = result = Timeline(beginning: after.len == 0) diff --git a/src/routes/status.nim b/src/routes/status.nim index 7e89220..d41f0d3 100644 --- a/src/routes/status.nim +++ b/src/routes/status.nim @@ -5,7 +5,7 @@ import jester, karax/vdom import router_utils import ".."/[types, formatters, api] -import ../views/[general, status] +import ../views/[general, status, timeline, search] export uri, sequtils, options, sugar export router_utils @@ -14,6 +14,29 @@ export status proc createStatusRouter*(cfg: Config) = router status: + get "/@name/status/@id/@reactors": + cond '.' notin @"name" + let id = @"id" + + if id.len > 19 or id.any(c => not c.isDigit): + resp Http404, showError("Invalid tweet ID", cfg) + + let prefs = cookiePrefs() + + # used for the infinite scroll feature + if @"scroll".len > 0: + let replies = await getReplies(id, getCursor()) + if replies.content.len == 0: + resp Http404, "" + resp $renderReplies(replies, prefs, getPath()) + + if @"reactors" == "favoriters": + resp renderMain(renderUserList(await getGraphFavoriters(id, getCursor()), prefs), + request, cfg, prefs) + elif @"reactors" == "retweeters": + resp renderMain(renderUserList(await getGraphRetweeters(id, getCursor()), prefs), + request, cfg, prefs) + get "/@name/status/@id/?": cond '.' notin @"name" let id = @"id" diff --git a/src/tokens.nim b/src/tokens.nim index 6ef81f5..d3ea3a9 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -45,7 +45,7 @@ proc getPoolJson*(): JsonNode = of Api.listMembers, Api.listBySlug, Api.list, Api.listTweets, Api.userTweets, Api.userTweetsAndReplies, Api.userMedia, Api.userRestId, Api.userScreenName, - Api.tweetDetail, Api.tweetResult, Api.search: 500 + Api.tweetDetail, Api.tweetResult, Api.search, Api.retweeters, Api.favoriters: 500 of Api.userSearch: 900 reqs = maxReqs - token.apis[api].remaining diff --git a/src/types.nim b/src/types.nim index d080ee2..98acaca 100644 --- a/src/types.nim +++ b/src/types.nim @@ -30,6 +30,8 @@ type userTweets userTweetsAndReplies userMedia + favoriters + retweeters RateLimit* = object remaining*: int @@ -224,6 +226,7 @@ type replies*: Result[Chain] Timeline* = Result[Tweet] + UsersTimeline* = Result[User] Profile* = object user*: User diff --git a/src/views/search.nim b/src/views/search.nim index df210a5..86bebf4 100644 --- a/src/views/search.nim +++ b/src/views/search.nim @@ -121,3 +121,8 @@ proc renderUserSearch*(results: Result[User]; prefs: Prefs): VNode = renderSearchTabs(results.query) renderTimelineUsers(results, prefs) + +proc renderUserList*(results: Result[User]; prefs: Prefs): VNode = + buildHtml(tdiv(class="timeline-container")): + tdiv(class="timeline-header") + renderTimelineUsers(results, prefs)