diff --git a/src/api.nim b/src/api.nim index 5e6eee4..804ca70 100644 --- a/src/api.nim +++ b/src/api.nim @@ -3,7 +3,6 @@ import asyncdispatch, httpclient, uri, strutils, sequtils, sugar import packedjson import types, query, formatters, consts, apiutils, parser import experimental/parser as newParser -import config proc getGraphUser*(username: string): Future[User] {.async.} = if username.len == 0: return @@ -112,6 +111,24 @@ proc getGraphRetweeters*(id: string; after=""): Future[UsersTimeline] {.async.} js = await fetch(graphRetweeters ? params, Api.retweeters) result = parseGraphRetweetersTimeline(js, id) +proc getGraphFollowing*(id: string; after=""): Future[UsersTimeline] {.async.} = + if id.len == 0: return + let + cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: "" + variables = followVariables % [id, cursor] + params = {"variables": variables, "features": gqlFeatures} + js = await fetch(graphFollowing ? params, Api.following) + result = parseGraphFollowTimeline(js, id) + +proc getGraphFollowers*(id: string; after=""): Future[UsersTimeline] {.async.} = + if id.len == 0: return + let + cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: "" + variables = followVariables % [id, cursor] + params = {"variables": variables, "features": gqlFeatures} + js = await fetch(graphFollowers ? params, Api.followers) + result = parseGraphFollowTimeline(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/apiutils.nim b/src/apiutils.nim index 313005b..d757e45 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -71,8 +71,8 @@ template fetchImpl(result, additional_headers, fetchBody) {.dirty.} = getContent() - if resp.status == $Http429: - raise rateLimitError() + if resp.status == $Http429: + raise rateLimitError() if resp.status == $Http503: badClient = true diff --git a/src/consts.nim b/src/consts.nim index fa848c5..fef637e 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -28,6 +28,8 @@ const graphListTweets* = graphql / "jZntL0oVJSdjhmPcdbw_eA/ListLatestTweetsTimeline" graphFavoriters* = graphql / "mDc_nU8xGv0cLRWtTaIEug/Favoriters" graphRetweeters* = graphql / "RCR9gqwYD1NEgi9FWzA50A/Retweeters" + graphFollowers* = graphql / "EAqBhgcGr_qPOzhS4Q3scQ/Followers" + graphFollowing* = graphql / "JPZiqKjET7_M1r5Tlr8pyA/Following" timelineParams* = { "include_profile_interstitial_type": "0", @@ -129,3 +131,9 @@ const "count" : 20, "includePromotedContent": false }""" + + followVariables* = """{ + "userId" : "$1", $2 + "count" : 20, + "includePromotedContent": false +}""" diff --git a/src/parser.nim b/src/parser.nim index c128d2f..94ff889 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -498,10 +498,10 @@ 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 = +proc parseGraphUsersTimeline(timeline: JsonNode; after=""): UsersTimeline = result = UsersTimeline(beginning: after.len == 0) - let instructions = ? js{"data", key, "timeline", "instructions"} + let instructions = ? timeline{"instructions"} if instructions.len == 0: return @@ -520,10 +520,13 @@ proc parseGraphUsersTimeline(js: JsonNode; root: string; key: string; after=""): result.top = e{"content", "value"}.getStr proc parseGraphFavoritersTimeline*(js: JsonNode; root: string; after=""): UsersTimeline = - return parseGraphUsersTimeline(js, root, "favoriters_timeline", after) + return parseGraphUsersTimeline(js{"data", "favoriters_timeline", "timeline"}, after) proc parseGraphRetweetersTimeline*(js: JsonNode; root: string; after=""): UsersTimeline = - return parseGraphUsersTimeline(js, root, "retweeters_timeline", after) + return parseGraphUsersTimeline(js{"data", "retweeters_timeline", "timeline"}, after) + +proc parseGraphFollowTimeline*(js: JsonNode; root: string; after=""): UsersTimeline = + return parseGraphUsersTimeline(js{"data", "user", "result", "timeline", "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 d41f0d3..036eca0 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, timeline, search] +import ../views/[general, status, search] export uri, sequtils, options, sugar export router_utils diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index b4499ea..aa56b33 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -127,35 +127,41 @@ proc createTimelineRouter*(cfg: Config) = get "/@name/?@tab?/?": cond '.' notin @"name" cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"] - cond @"tab" in ["with_replies", "media", "search", "favorites", ""] + cond @"tab" in ["with_replies", "media", "search", "favorites", "following", "followers", ""] let prefs = cookiePrefs() after = getCursor() names = getNames(@"name") - var query = request.getQuery(@"tab", @"name") - if names.len != 1: - query.fromUser = names - - # used for the infinite scroll feature - if @"scroll".len > 0: - if query.fromUser.len != 1: - var timeline = await getGraphSearch(query, after) - if timeline.content.len == 0: resp Http404 - timeline.beginning = true - resp $renderTweetSearch(timeline, cfg, prefs, getPath()) + case @"tab": + of "followers": + resp renderMain(renderUserList(await getGraphFollowers(await getUserId(@"name"), getCursor()), prefs), request, cfg, prefs) + of "following": + resp renderMain(renderUserList(await getGraphFollowing(await getUserId(@"name"), getCursor()), prefs), request, cfg, prefs) else: - var profile = await fetchProfile(after, query, cfg, skipRail=true) - if profile.tweets.content.len == 0: resp Http404 - profile.tweets.beginning = true - resp $renderTimelineTweets(profile.tweets, prefs, getPath()) + var query = request.getQuery(@"tab", @"name") + if names.len != 1: + query.fromUser = names - let rss = - if @"tab".len == 0: - "/$1/rss" % @"name" - elif @"tab" == "search": - "/$1/search/rss?$2" % [@"name", genQueryUrl(query)] - else: - "/$1/$2/rss" % [@"name", @"tab"] + # used for the infinite scroll feature + if @"scroll".len > 0: + if query.fromUser.len != 1: + var timeline = await getGraphSearch(query, after) + if timeline.content.len == 0: resp Http404 + timeline.beginning = true + resp $renderTweetSearch(timeline, cfg, prefs, getPath()) + else: + var profile = await fetchProfile(after, query, cfg, skipRail=true) + if profile.tweets.content.len == 0: resp Http404 + profile.tweets.beginning = true + resp $renderTimelineTweets(profile.tweets, prefs, getPath()) - respTimeline(await showTimeline(request, query, cfg, prefs, rss, after)) + let rss = + if @"tab".len == 0: + "/$1/rss" % @"name" + elif @"tab" == "search": + "/$1/search/rss?$2" % [@"name", genQueryUrl(query)] + else: + "/$1/$2/rss" % [@"name", @"tab"] + + respTimeline(await showTimeline(request, query, cfg, prefs, rss, after)) diff --git a/src/types.nim b/src/types.nim index 98acaca..5a2b7e2 100644 --- a/src/types.nim +++ b/src/types.nim @@ -32,6 +32,8 @@ type userMedia favoriters retweeters + following + followers RateLimit* = object remaining*: int diff --git a/src/views/profile.nim b/src/views/profile.nim index 75cc169..db17c41 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -59,8 +59,10 @@ proc renderUserCard*(user: User; prefs: Prefs): VNode = tdiv(class="profile-card-extra-links"): ul(class="profile-statlist"): renderStat(user.tweets, "posts", text="Tweets") - renderStat(user.following, "following") - renderStat(user.followers, "followers") + a(href="/" & user.username & "/following"): + renderStat(user.following, "following") + a(href="/" & user.username & "/followers"): + renderStat(user.followers, "followers") renderStat(user.likes, "likes") proc renderPhotoRail(profile: Profile): VNode =