From 2fd13de8e16575f4f1a89d1f92b3824831dceb2c Mon Sep 17 00:00:00 2001 From: kuu7o Date: Wed, 19 Nov 2025 18:09:37 -0300 Subject: [PATCH] feat: added following system using cookies --- src/nitter.nim | 4 +++- src/prefs.nim | 4 +++- src/prefs_impl.nim | 1 + src/routes/follow.nim | 23 +++++++++++++++++++++++ src/routes/search.nim | 2 +- src/views/profile.nim | 13 ++++++++----- src/views/renderutils.nim | 12 ++++++++++++ src/views/search.nim | 4 ++-- src/views/timeline.nim | 5 +++-- 9 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 src/routes/follow.nim diff --git a/src/nitter.nim b/src/nitter.nim index 5625924..24439d3 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -10,7 +10,7 @@ import types, config, prefs, formatters, redis_cache, http_pool, auth import views/[general, about] import routes/[ preferences, timeline, status, media, search, rss, list, debug, - unsupported, embed, resolver, router_utils] + unsupported, embed, resolver, router_utils, follow] const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances" const issuesUrl = "https://github.com/zedeus/nitter/issues" @@ -78,6 +78,7 @@ waitFor initRedisPool(cfg) info &"Connected to Redis at {cfg.redisHost}:{cfg.redisPort}" createUnsupportedRouter(cfg) +createFollowRouter(cfg) createResolverRouter(cfg) createPrefRouter(cfg) createTimelineRouter(cfg) @@ -145,5 +146,6 @@ routes: extend preferences, "" extend resolver, "" extend embed, "" + extend follow, "" extend debug, "" extend unsupported, "" diff --git a/src/prefs.nim b/src/prefs.nim index fa40a6d..2ba081b 100644 --- a/src/prefs.nim +++ b/src/prefs.nim @@ -1,5 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-only -import tables +import tables, strutils, sequtils import types, prefs_impl from config import get from parsecfg import nil @@ -13,6 +13,8 @@ proc updateDefaultPrefs*(cfg: parsecfg.Config) = proc getPrefs*(cookies: Table[string, string]): Prefs = result = defaultPrefs + if "nitter_following" in cookies: + result.following = cookies["nitter_following"].split(',').filterIt(it.len > 0) genCookiePrefs(cookies) template getPref*(cookies: Table[string, string], pref): untyped = diff --git a/src/prefs_impl.nim b/src/prefs_impl.nim index a3ed888..25273e8 100644 --- a/src/prefs_impl.nim +++ b/src/prefs_impl.nim @@ -225,6 +225,7 @@ macro genPrefsType*(): untyped = let name = nnkPostfix.newTree(ident("*"), ident("Prefs")) result = quote do: type `name` = object + following*: seq[string] discard for pref in allPrefs(): diff --git a/src/routes/follow.nim b/src/routes/follow.nim new file mode 100644 index 0000000..fe6bee8 --- /dev/null +++ b/src/routes/follow.nim @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: AGPL-3.0-only +import strutils, sequtils, uri +import jester +import router_utils +import ".."/[types] + +proc createFollowRouter*(cfg: Config) = + router follow: + post "/follow": + let user = @"user" + var prefs = cookiePrefs() + if user.len > 0 and user notin prefs.following: + prefs.following.add(user) + setCookie("nitter_following", prefs.following.join(","), daysForward(360), path="/", httpOnly=true, secure=cfg.useHttps, sameSite=None) + redirect(refPath()) + + post "/unfollow": + let user = @"user" + var prefs = cookiePrefs() + if user.len > 0 and user in prefs.following: + prefs.following.keepItIf(it != user) + setCookie("nitter_following", prefs.following.join(","), daysForward(360), path="/", httpOnly=true, secure=cfg.useHttps, sameSite=None) + redirect(refPath()) diff --git a/src/routes/search.nim b/src/routes/search.nim index e9f991d..5719cc0 100644 --- a/src/routes/search.nim +++ b/src/routes/search.nim @@ -32,7 +32,7 @@ proc createSearchRouter*(cfg: Config) = users = await getGraphUserSearch(query, getCursor()) except InternalError: users = Result[User](beginning: true, query: query) - resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs, title) + resp renderMain(renderUserSearch(users, prefs, getPath()), request, cfg, prefs, title) of tweets: let tweets = await getGraphTweetSearch(query, getCursor()) diff --git a/src/views/profile.nim b/src/views/profile.nim index 72f56f3..fd13fea 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -12,7 +12,7 @@ proc renderStat(num: int; class: string; text=""): VNode = span(class="profile-stat-num"): text insertSep($num, ',') -proc renderUserCard*(user: User; prefs: Prefs): VNode = +proc renderUserCard*(user: User; prefs: Prefs; path: string): VNode = let class = if user.sensitive: "profile-card nsfw" else: "profile-card" buildHtml(tdiv(class=class)): tdiv(class="profile-card-info"): @@ -25,9 +25,12 @@ proc renderUserCard*(user: User; prefs: Prefs): VNode = a(class="profile-card-avatar", href=url, target="_blank"): genImg(user.getUserPic(size)) - tdiv(class="profile-card-tabs-name"): - linkUser(user, class="profile-card-fullname") - linkUser(user, class="profile-card-username") + tdiv(class="profile-header"): + tdiv(class="profile-card-tabs-name"): + linkUser(user, class="profile-card-fullname") + linkUser(user, class="profile-card-username") + + renderFollowButton(user, prefs, path) tdiv(class="profile-card-extra"): if user.bio.len > 0: @@ -110,7 +113,7 @@ proc renderProfile*(profile: var Profile; prefs: Prefs; path: string): VNode = let sticky = if prefs.stickyProfile: " sticky" else: "" tdiv(class=("profile-tab" & sticky)): - renderUserCard(profile.user, prefs) + renderUserCard(profile.user, prefs, path) if profile.photoRail.len > 0: renderPhotoRail(profile) diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim index 0039eee..ee9bed4 100644 --- a/src/views/renderutils.nim +++ b/src/views/renderutils.nim @@ -100,3 +100,15 @@ proc getTabClass*(query: Query; tab: QueryKind): string = proc getAvatarClass*(prefs: Prefs): string = if prefs.squareAvatars: "avatar" else: "avatar round" + +proc renderFollowButton*(user: User; prefs: Prefs; path: string): VNode = + let + isFollowing = user.username in prefs.following + action = if isFollowing: "/unfollow" else: "/follow" + text = if isFollowing: "Unfollow" else: "Follow" + class = if isFollowing: "follow-button unfollow" else: "follow-button" + + buildHtml(form(action=action, `method`="post", class="follow-form")): + refererField path + hiddenField("user", user.username) + button(class=class, `type`="submit"): text text diff --git a/src/views/search.nim b/src/views/search.nim index 9f7fc95..e6c22c0 100644 --- a/src/views/search.nim +++ b/src/views/search.nim @@ -108,7 +108,7 @@ proc renderTweetSearch*(results: Timeline; prefs: Prefs; path: string; renderTimelineTweets(results, prefs, path, pinned) -proc renderUserSearch*(results: Result[User]; prefs: Prefs): VNode = +proc renderUserSearch*(results: Result[User]; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="timeline-container")): tdiv(class="timeline-header"): form(`method`="get", action="/search", class="search-field", autocomplete="off"): @@ -117,4 +117,4 @@ proc renderUserSearch*(results: Result[User]; prefs: Prefs): VNode = button(`type`="submit"): icon "search" renderSearchTabs(results.query) - renderTimelineUsers(results, prefs) + renderTimelineUsers(results, prefs, path) diff --git a/src/views/timeline.nim b/src/views/timeline.nim index d3a5b87..019916e 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -55,7 +55,7 @@ proc renderThread(thread: Tweets; prefs: Prefs; path: string): VNode = renderTweet(tweet, prefs, path, class=(header & "thread"), index=i, last=(i == thread.high), showThread=show) -proc renderUser(user: User; prefs: Prefs): VNode = +proc renderUser(user: User; prefs: Prefs; path: string): VNode = let class = if user.sensitive: "timeline-item nsfw" else: "timeline-item" buildHtml(tdiv(class=class)): a(class="tweet-link", href=("/" & user.username)) @@ -67,6 +67,7 @@ proc renderUser(user: User; prefs: Prefs): VNode = tdiv(class="tweet-name-row"): tdiv(class="fullname-and-username"): linkUser(user, class="fullname") + renderFollowButton(user, prefs, path) linkUser(user, class="username") tdiv(class="tweet-content media-body", dir="auto"): @@ -79,7 +80,7 @@ proc renderTimelineUsers*(results: Result[User]; prefs: Prefs; path=""): VNode = if results.content.len > 0: for user in results.content: - renderUser(user, prefs) + renderUser(user, prefs, path) if results.bottom.len > 0: renderMore(results.query, results.bottom) renderToTop()