feat: added following system using cookies

This commit is contained in:
2025-11-19 18:09:37 -03:00
parent 4e05923cd8
commit 2fd13de8e1
9 changed files with 56 additions and 12 deletions

View File

@@ -10,7 +10,7 @@ import types, config, prefs, formatters, redis_cache, http_pool, auth
import views/[general, about] import views/[general, about]
import routes/[ import routes/[
preferences, timeline, status, media, search, rss, list, debug, 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 instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
const issuesUrl = "https://github.com/zedeus/nitter/issues" const issuesUrl = "https://github.com/zedeus/nitter/issues"
@@ -78,6 +78,7 @@ waitFor initRedisPool(cfg)
info &"Connected to Redis at {cfg.redisHost}:{cfg.redisPort}" info &"Connected to Redis at {cfg.redisHost}:{cfg.redisPort}"
createUnsupportedRouter(cfg) createUnsupportedRouter(cfg)
createFollowRouter(cfg)
createResolverRouter(cfg) createResolverRouter(cfg)
createPrefRouter(cfg) createPrefRouter(cfg)
createTimelineRouter(cfg) createTimelineRouter(cfg)
@@ -145,5 +146,6 @@ routes:
extend preferences, "" extend preferences, ""
extend resolver, "" extend resolver, ""
extend embed, "" extend embed, ""
extend follow, ""
extend debug, "" extend debug, ""
extend unsupported, "" extend unsupported, ""

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import tables import tables, strutils, sequtils
import types, prefs_impl import types, prefs_impl
from config import get from config import get
from parsecfg import nil from parsecfg import nil
@@ -13,6 +13,8 @@ proc updateDefaultPrefs*(cfg: parsecfg.Config) =
proc getPrefs*(cookies: Table[string, string]): Prefs = proc getPrefs*(cookies: Table[string, string]): Prefs =
result = defaultPrefs result = defaultPrefs
if "nitter_following" in cookies:
result.following = cookies["nitter_following"].split(',').filterIt(it.len > 0)
genCookiePrefs(cookies) genCookiePrefs(cookies)
template getPref*(cookies: Table[string, string], pref): untyped = template getPref*(cookies: Table[string, string], pref): untyped =

View File

@@ -225,6 +225,7 @@ macro genPrefsType*(): untyped =
let name = nnkPostfix.newTree(ident("*"), ident("Prefs")) let name = nnkPostfix.newTree(ident("*"), ident("Prefs"))
result = quote do: result = quote do:
type `name` = object type `name` = object
following*: seq[string]
discard discard
for pref in allPrefs(): for pref in allPrefs():

23
src/routes/follow.nim Normal file
View File

@@ -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())

View File

@@ -32,7 +32,7 @@ proc createSearchRouter*(cfg: Config) =
users = await getGraphUserSearch(query, getCursor()) users = await getGraphUserSearch(query, getCursor())
except InternalError: except InternalError:
users = Result[User](beginning: true, query: query) 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: of tweets:
let let
tweets = await getGraphTweetSearch(query, getCursor()) tweets = await getGraphTweetSearch(query, getCursor())

View File

@@ -12,7 +12,7 @@ proc renderStat(num: int; class: string; text=""): VNode =
span(class="profile-stat-num"): span(class="profile-stat-num"):
text insertSep($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" let class = if user.sensitive: "profile-card nsfw" else: "profile-card"
buildHtml(tdiv(class=class)): buildHtml(tdiv(class=class)):
tdiv(class="profile-card-info"): 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"): a(class="profile-card-avatar", href=url, target="_blank"):
genImg(user.getUserPic(size)) genImg(user.getUserPic(size))
tdiv(class="profile-card-tabs-name"): tdiv(class="profile-header"):
linkUser(user, class="profile-card-fullname") tdiv(class="profile-card-tabs-name"):
linkUser(user, class="profile-card-username") linkUser(user, class="profile-card-fullname")
linkUser(user, class="profile-card-username")
renderFollowButton(user, prefs, path)
tdiv(class="profile-card-extra"): tdiv(class="profile-card-extra"):
if user.bio.len > 0: 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: "" let sticky = if prefs.stickyProfile: " sticky" else: ""
tdiv(class=("profile-tab" & sticky)): tdiv(class=("profile-tab" & sticky)):
renderUserCard(profile.user, prefs) renderUserCard(profile.user, prefs, path)
if profile.photoRail.len > 0: if profile.photoRail.len > 0:
renderPhotoRail(profile) renderPhotoRail(profile)

View File

@@ -100,3 +100,15 @@ proc getTabClass*(query: Query; tab: QueryKind): string =
proc getAvatarClass*(prefs: Prefs): string = proc getAvatarClass*(prefs: Prefs): string =
if prefs.squareAvatars: "avatar" if prefs.squareAvatars: "avatar"
else: "avatar round" 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

View File

@@ -108,7 +108,7 @@ proc renderTweetSearch*(results: Timeline; prefs: Prefs; path: string;
renderTimelineTweets(results, prefs, path, pinned) 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")): buildHtml(tdiv(class="timeline-container")):
tdiv(class="timeline-header"): tdiv(class="timeline-header"):
form(`method`="get", action="/search", class="search-field", autocomplete="off"): 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" button(`type`="submit"): icon "search"
renderSearchTabs(results.query) renderSearchTabs(results.query)
renderTimelineUsers(results, prefs) renderTimelineUsers(results, prefs, path)

View File

@@ -55,7 +55,7 @@ proc renderThread(thread: Tweets; prefs: Prefs; path: string): VNode =
renderTweet(tweet, prefs, path, class=(header & "thread"), renderTweet(tweet, prefs, path, class=(header & "thread"),
index=i, last=(i == thread.high), showThread=show) 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" let class = if user.sensitive: "timeline-item nsfw" else: "timeline-item"
buildHtml(tdiv(class=class)): buildHtml(tdiv(class=class)):
a(class="tweet-link", href=("/" & user.username)) 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="tweet-name-row"):
tdiv(class="fullname-and-username"): tdiv(class="fullname-and-username"):
linkUser(user, class="fullname") linkUser(user, class="fullname")
renderFollowButton(user, prefs, path)
linkUser(user, class="username") linkUser(user, class="username")
tdiv(class="tweet-content media-body", dir="auto"): 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: if results.content.len > 0:
for user in results.content: for user in results.content:
renderUser(user, prefs) renderUser(user, prefs, path)
if results.bottom.len > 0: if results.bottom.len > 0:
renderMore(results.query, results.bottom) renderMore(results.query, results.bottom)
renderToTop() renderToTop()