From 7d2a558e89862072e204410836548f48a7212ac6 Mon Sep 17 00:00:00 2001 From: PrivacyDev Date: Tue, 4 Apr 2023 23:55:01 -0400 Subject: [PATCH] added favorites endpoint and added likes tab to profile pages --- nitter.example.conf | 3 +++ src/api.nim | 10 ++++++++++ src/apiutils.nim | 12 ++++++------ src/config.nim | 4 +++- src/consts.nim | 1 + src/query.nim | 7 +++++++ src/routes/rss.nim | 5 +++-- src/routes/search.nim | 2 +- src/routes/timeline.nim | 16 +++++++++------- src/types.nim | 6 +++++- src/views/profile.nim | 4 ++-- src/views/search.nim | 9 ++++++--- 12 files changed, 56 insertions(+), 23 deletions(-) diff --git a/nitter.example.conf b/nitter.example.conf index a7abea8..656e879 100644 --- a/nitter.example.conf +++ b/nitter.example.conf @@ -33,6 +33,9 @@ tokenCount = 10 # always at least $tokenCount usable tokens. again, only increase this if # you receive major bursts all the time +#cookieHeader = "ct0=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; auth_token=XXXXXXXXXXXXXXXXXXXXXXX" # authentication cookie of a logged in account, required for the likes tab +#xCsrfToken = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # required for the likes tab + # Change default preferences here, see src/prefs_impl.nim for a complete list [Preferences] theme = "Nitter" diff --git a/src/api.nim b/src/api.nim index dfcf413..8993e18 100644 --- a/src/api.nim +++ b/src/api.nim @@ -65,6 +65,16 @@ proc getTimeline*(id: string; after=""; replies=false): Future[Timeline] {.async url = timeline / (id & ".json") ? ps result = parseTimeline(await fetch(url, Api.timeline), after) +proc getFavorites*(id: string; cfg: Config; after=""): Future[Timeline] {.async.} = + if id.len == 0: return + let + ps = genParams({"userId": id}, after) + url = consts.favorites / (id & ".json") ? ps + headers = genHeaders() + headers.add("Cookie", cfg.cookieHeader) + headers.add("x-csrf-token", cfg.xCsrfToken) + result = parseTimeline(await fetch(url, Api.favorites, headers), after) + proc getMediaTimeline*(id: string; after=""): Future[Timeline] {.async.} = if id.len == 0: return let url = mediaTimeline / (id & ".json") ? genParams(cursor=after) diff --git a/src/apiutils.nim b/src/apiutils.nim index 917932a..78c8c45 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -50,7 +50,7 @@ template updateToken() = reset = parseInt(resp.headers[rlReset]) token.setRateLimit(api, remaining, reset) -template fetchImpl(result, fetchBody) {.dirty.} = +template fetchImpl(result, headers, fetchBody) {.dirty.} = once: pool = HttpPool() @@ -60,7 +60,7 @@ template fetchImpl(result, fetchBody) {.dirty.} = try: var resp: AsyncResponse - pool.use(genHeaders(token)): + pool.use(headers): template getContent = resp = await c.get($url) result = await resp.body @@ -96,9 +96,9 @@ template fetchImpl(result, fetchBody) {.dirty.} = release(token, invalid=true) raise rateLimitError() -proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} = +proc fetch*(url: Uri; api: Api; headers: HttpHeaders = genHeaders()): Future[JsonNode] {.async.} = var body: string - fetchImpl body: + fetchImpl(body, headers): if body.startsWith('{') or body.startsWith('['): result = parseJson(body) else: @@ -113,8 +113,8 @@ proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} = release(token, invalid=true) raise rateLimitError() -proc fetchRaw*(url: Uri; api: Api): Future[string] {.async.} = - fetchImpl result: +proc fetchRaw*(url: Uri; api: Api; headers: HttpHeaders = genHeaders()): Future[string] {.async.} = + fetchImpl(result, headers): if not (result.startsWith('{') or result.startsWith('[')): echo resp.status, ": ", result, " --- url: ", url result.setLen(0) diff --git a/src/config.nim b/src/config.nim index 1b05ffe..47f0fc3 100644 --- a/src/config.nim +++ b/src/config.nim @@ -40,7 +40,9 @@ proc getConfig*(path: string): (Config, parseCfg.Config) = enableRss: cfg.get("Config", "enableRSS", true), enableDebug: cfg.get("Config", "enableDebug", false), proxy: cfg.get("Config", "proxy", ""), - proxyAuth: cfg.get("Config", "proxyAuth", "") + proxyAuth: cfg.get("Config", "proxyAuth", ""), + cookieHeader: cfg.get("Config", "cookieHeader", ""), + xCsrfToken: cfg.get("Config", "xCsrfToken", "") ) return (conf, cfg) diff --git a/src/consts.nim b/src/consts.nim index bb4e1a3..c4e49d7 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -15,6 +15,7 @@ const timelineApi = api / "2/timeline" timeline* = timelineApi / "profile" mediaTimeline* = timelineApi / "media" + favorites* = timelineApi / "favorites" listTimeline* = timelineApi / "list.json" tweet* = timelineApi / "conversation" diff --git a/src/query.nim b/src/query.nim index d128f6f..49c5856 100644 --- a/src/query.nim +++ b/src/query.nim @@ -40,6 +40,13 @@ proc getMediaQuery*(name: string): Query = sep: "OR" ) + +proc getFavoritesQuery*(name: string): Query = + Query( + kind: favorites, + fromUser: @[name] + ) + proc getReplyQuery*(name: string): Query = Query( kind: replies, diff --git a/src/routes/rss.nim b/src/routes/rss.nim index 5da29b0..3b31671 100644 --- a/src/routes/rss.nim +++ b/src/routes/rss.nim @@ -23,7 +23,7 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async. names = getNames(name) if names.len == 1: - profile = await fetchProfile(after, query, skipRail=true, skipPinned=true) + profile = await fetchProfile(after, query, cfg, skipRail=true, skipPinned=true) else: var q = query q.fromUser = names @@ -104,7 +104,7 @@ proc createRssRouter*(cfg: Config) = get "/@name/@tab/rss": cond cfg.enableRss cond '.' notin @"name" - cond @"tab" in ["with_replies", "media", "search"] + cond @"tab" in ["with_replies", "media", "favorites", "search"] let name = @"name" tab = @"tab" @@ -112,6 +112,7 @@ proc createRssRouter*(cfg: Config) = case tab of "with_replies": getReplyQuery(name) of "media": getMediaQuery(name) + of "favorites": getFavoritesQuery(name) of "search": initQuery(params(request), name=name) else: Query(fromUser: @[name]) diff --git a/src/routes/search.nim b/src/routes/search.nim index b2fd718..70f5ca2 100644 --- a/src/routes/search.nim +++ b/src/routes/search.nim @@ -33,7 +33,7 @@ proc createSearchRouter*(cfg: Config) = let tweets = await getSearch[Tweet](query, getCursor()) rss = "/search/rss?" & genQueryUrl(query) - resp renderMain(renderTweetSearch(tweets, prefs, getPath()), + resp renderMain(renderTweetSearch(tweets, cfg, prefs, getPath()), request, cfg, prefs, title, rss=rss) else: resp Http404, showError("Invalid search", cfg) diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index a0a6e21..906c5d4 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -16,6 +16,7 @@ proc getQuery*(request: Request; tab, name: string): Query = case tab of "with_replies": getReplyQuery(name) of "media": getMediaQuery(name) + of "favorites": getFavoritesQuery(name) of "search": initQuery(params(request), name=name) else: Query(fromUser: @[name]) @@ -27,7 +28,7 @@ template skipIf[T](cond: bool; default; body: Future[T]): Future[T] = else: body -proc fetchProfile*(after: string; query: Query; skipRail=false; +proc fetchProfile*(after: string; query: Query; cfg: Config; skipRail=false; skipPinned=false): Future[Profile] {.async.} = let name = query.fromUser[0] @@ -50,6 +51,7 @@ proc fetchProfile*(after: string; query: Query; skipRail=false; of posts: getTimeline(userId, after) of replies: getTimeline(userId, after, replies=true) of media: getMediaTimeline(userId, after) + of favorites: getFavorites(userId, cfg, after) else: getSearch[Tweet](query, after) rail = @@ -83,10 +85,10 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs; if query.fromUser.len != 1: let timeline = await getSearch[Tweet](query, after) - html = renderTweetSearch(timeline, prefs, getPath()) + html = renderTweetSearch(timeline, cfg, prefs, getPath()) return renderMain(html, request, cfg, prefs, "Multi", rss=rss) - var profile = await fetchProfile(after, query, skipPinned=prefs.hidePins) + var profile = await fetchProfile(after, query, cfg, skipPinned=prefs.hidePins) template u: untyped = profile.user if u.suspended: @@ -94,7 +96,7 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs; if profile.user.id.len == 0: return - let pHtml = renderProfile(profile, prefs, getPath()) + let pHtml = renderProfile(profile, cfg, prefs, getPath()) result = renderMain(pHtml, request, cfg, prefs, pageTitle(u), pageDesc(u), rss=rss, images = @[u.getUserPic("_400x400")], banner=u.banner) @@ -124,7 +126,7 @@ proc createTimelineRouter*(cfg: Config) = get "/@name/?@tab?/?": cond '.' notin @"name" cond @"name" notin ["pic", "gif", "video"] - cond @"tab" in ["with_replies", "media", "search", ""] + cond @"tab" in ["with_replies", "media", "search", "favorites", ""] let prefs = cookiePrefs() after = getCursor() @@ -140,9 +142,9 @@ proc createTimelineRouter*(cfg: Config) = var timeline = await getSearch[Tweet](query, after) if timeline.content.len == 0: resp Http404 timeline.beginning = true - resp $renderTweetSearch(timeline, prefs, getPath()) + resp $renderTweetSearch(timeline, cfg, prefs, getPath()) else: - var profile = await fetchProfile(after, query, skipRail=true) + 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()) diff --git a/src/types.nim b/src/types.nim index 6f742d1..f5fa6ae 100644 --- a/src/types.nim +++ b/src/types.nim @@ -20,6 +20,7 @@ type userRestId userScreenName status + favorites RateLimit* = object remaining*: int @@ -95,7 +96,7 @@ type variants*: seq[VideoVariant] QueryKind* = enum - posts, replies, media, users, tweets, userList + posts, replies, media, users, tweets, userList, favorites Query* = object kind*: QueryKind @@ -257,6 +258,9 @@ type redisMaxConns*: int redisPassword*: string + cookieHeader*: string + xCsrfToken*: string + Rss* = object feed*, cursor*: string diff --git a/src/views/profile.nim b/src/views/profile.nim index 2b2e410..75cc169 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -99,7 +99,7 @@ proc renderProtected(username: string): VNode = h2: text "This account's tweets are protected." p: text &"Only confirmed followers have access to @{username}'s tweets." -proc renderProfile*(profile: var Profile; prefs: Prefs; path: string): VNode = +proc renderProfile*(profile: var Profile; cfg: Config; prefs: Prefs; path: string): VNode = profile.tweets.query.fromUser = @[profile.user.username] buildHtml(tdiv(class="profile-tabs")): @@ -116,4 +116,4 @@ proc renderProfile*(profile: var Profile; prefs: Prefs; path: string): VNode = if profile.user.protected: renderProtected(profile.user.username) else: - renderTweetSearch(profile.tweets, prefs, path, profile.pinned) + renderTweetSearch(profile.tweets, cfg, prefs, path, profile.pinned) diff --git a/src/views/search.nim b/src/views/search.nim index 77ba14f..cb37fdc 100644 --- a/src/views/search.nim +++ b/src/views/search.nim @@ -29,7 +29,7 @@ proc renderSearch*(): VNode = placeholder="Enter username...", dir="auto") button(`type`="submit"): icon "search" -proc renderProfileTabs*(query: Query; username: string): VNode = +proc renderProfileTabs*(query: Query; username: string; cfg: Config): VNode = let link = "/" & username buildHtml(ul(class="tab")): li(class=query.getTabClass(posts)): @@ -38,6 +38,9 @@ proc renderProfileTabs*(query: Query; username: string): VNode = a(href=(link & "/with_replies")): text "Tweets & Replies" li(class=query.getTabClass(media)): a(href=(link & "/media")): text "Media" + if len(cfg.xCsrfToken) != 0 and len(cfg.cookieHeader) != 0: + li(class=query.getTabClass(favorites)): + a(href=(link & "/favorites")): text "Likes" li(class=query.getTabClass(tweets)): a(href=(link & "/search")): text "Search" @@ -90,7 +93,7 @@ proc renderSearchPanel*(query: Query): VNode = span(class="search-title"): text "Near" genInput("near", "", query.near, "Location...", autofocus=false) -proc renderTweetSearch*(results: Result[Tweet]; prefs: Prefs; path: string; +proc renderTweetSearch*(results: Result[Tweet]; cfg: Config; prefs: Prefs; path: string; pinned=none(Tweet)): VNode = let query = results.query buildHtml(tdiv(class="timeline-container")): @@ -99,7 +102,7 @@ proc renderTweetSearch*(results: Result[Tweet]; prefs: Prefs; path: string; text query.fromUser.join(" | ") if query.fromUser.len > 0: - renderProfileTabs(query, query.fromUser.join(",")) + renderProfileTabs(query, query.fromUser.join(","), cfg) if query.fromUser.len == 0 or query.kind == tweets: tdiv(class="timeline-header"):