diff --git a/src/nitter.nim b/src/nitter.nim
index 442a8c0..ec2decf 100644
--- a/src/nitter.nim
+++ b/src/nitter.nim
@@ -71,10 +71,10 @@ routes:
applyUrlPrefs()
get "/":
- resp renderMain(renderSearch(), request, cfg, cookiePrefs())
+ resp renderMain(renderSearch(), request, cfg, requestPrefs())
get "/about":
- resp renderMain(renderAbout(), request, cfg, cookiePrefs())
+ resp renderMain(renderAbout(), request, cfg, requestPrefs())
get "/explore":
redirect("/about")
@@ -85,7 +85,7 @@ routes:
get "/i/redirect":
let url = decodeUrl(@"url")
if url.len == 0: resp Http404
- redirect(replaceUrls(url, cookiePrefs()))
+ redirect(replaceUrls(url, requestPrefs()))
error Http404:
resp Http404, showError("Page not found", cfg)
diff --git a/src/prefs.nim b/src/prefs.nim
index 573ccae..1a75f75 100644
--- a/src/prefs.nim
+++ b/src/prefs.nim
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only
-import tables, strutils, base64
+import tables, strutils
import types, prefs_impl
from config import get
from parsecfg import nil
@@ -11,17 +11,12 @@ var defaultPrefs*: Prefs
proc updateDefaultPrefs*(cfg: parsecfg.Config) =
genDefaultPrefs()
-proc getPrefs*(cookies: Table[string, string]): Prefs =
+proc getPrefs*(cookies, params: Table[string, string]): Prefs =
result = defaultPrefs
- genCookiePrefs(cookies)
-
-template getPref*(cookies: Table[string, string], pref): untyped =
- bind genCookiePref
- var res = defaultPrefs.`pref`
- genCookiePref(cookies, pref, res)
- res
+ genParsePrefs(cookies)
+ genParsePrefs(params)
proc encodePrefs*(prefs: Prefs): string =
var encPairs: seq[string]
genEncodePrefs(prefs)
- encode(encPairs.join("&"), safe=true)
+ encPairs.join(",")
diff --git a/src/prefs_impl.nim b/src/prefs_impl.nim
index 2faf8ef..149eadf 100644
--- a/src/prefs_impl.nim
+++ b/src/prefs_impl.nim
@@ -130,7 +130,7 @@ macro genDefaultPrefs*(): untyped =
result.add quote do:
defaultPrefs.`ident` = cfg.get("Preferences", `name`, `default`)
-macro genCookiePrefs*(cookies): untyped =
+macro genParsePrefs*(prefs): untyped =
result = nnkStmtList.newTree()
for pref in allPrefs():
let
@@ -140,37 +140,17 @@ macro genCookiePrefs*(cookies): untyped =
options = pref.options
result.add quote do:
- if `name` in `cookies`:
+ if `name` in `prefs`:
when `kind` == input or `name` == "theme":
- result.`ident` = `cookies`[`name`]
+ result.`ident` = `prefs`[`name`]
elif `kind` == checkbox:
- result.`ident` = `cookies`[`name`] == "on"
+ result.`ident` = `prefs`[`name`] == "on" or
+ `prefs`[`name`] == "true" or
+ `prefs`[`name`] == "1"
else:
- let value = `cookies`[`name`]
+ let value = `prefs`[`name`]
if value in `options`: result.`ident` = value
-macro genCookiePref*(cookies, prefName, res): untyped =
- result = nnkStmtList.newTree()
- for pref in allPrefs():
- let ident = ident(pref.name)
- if ident != prefName:
- continue
-
- let
- name = pref.name
- kind = newLit(pref.kind)
- options = pref.options
-
- result.add quote do:
- if `name` in `cookies`:
- when `kind` == input or `name` == "theme":
- `res` = `cookies`[`name`]
- elif `kind` == checkbox:
- `res` = `cookies`[`name`] == "on"
- else:
- let value = `cookies`[`name`]
- if value in `options`: `res` = value
-
macro genUpdatePrefs*(): untyped =
result = nnkStmtList.newTree()
let req = ident("request")
diff --git a/src/routes/embed.nim b/src/routes/embed.nim
index 994364b..0527d3d 100644
--- a/src/routes/embed.nim
+++ b/src/routes/embed.nim
@@ -19,7 +19,7 @@ proc createEmbedRouter*(cfg: Config) =
get "/@user/status/@id/embed":
let
tweet = await getGraphTweetResult(@"id")
- prefs = cookiePrefs()
+ prefs = requestPrefs()
path = getPath()
if tweet == nil:
diff --git a/src/routes/list.nim b/src/routes/list.nim
index ac3e97e..7dadc22 100644
--- a/src/routes/list.nim
+++ b/src/routes/list.nim
@@ -36,7 +36,7 @@ proc createListRouter*(cfg: Config) =
get "/i/lists/@id/?":
cond '.' notin @"id"
let
- prefs = cookiePrefs()
+ prefs = requestPrefs()
list = await getCachedList(id=(@"id"))
timeline = await getGraphListTweets(list.id, getCursor())
vnode = renderTimelineTweets(timeline, prefs, request.path)
@@ -45,7 +45,7 @@ proc createListRouter*(cfg: Config) =
get "/i/lists/@id/members":
cond '.' notin @"id"
let
- prefs = cookiePrefs()
+ prefs = requestPrefs()
list = await getCachedList(id=(@"id"))
members = await getGraphListMembers(list, getCursor())
respList(list, members, list.title, renderTimelineUsers(members, prefs, request.path))
diff --git a/src/routes/media.nim b/src/routes/media.nim
index 011d0f3..b3e5374 100644
--- a/src/routes/media.nim
+++ b/src/routes/media.nim
@@ -143,6 +143,6 @@ proc createMediaRouter*(cfg: Config) =
if ".m3u8" in url:
let vid = await safeFetch(url)
- content = proxifyVideo(vid, cookiePref(proxyVideos))
+ content = proxifyVideo(vid, requestPrefs().proxyVideos)
resp content, m3u8Mime
diff --git a/src/routes/preferences.nim b/src/routes/preferences.nim
index 345ff34..5886c0e 100644
--- a/src/routes/preferences.nim
+++ b/src/routes/preferences.nim
@@ -19,7 +19,7 @@ proc createPrefRouter*(cfg: Config) =
router preferences:
get "/settings":
let
- prefs = cookiePrefs()
+ prefs = requestPrefs()
prefsCode = encodePrefs(prefs)
prefsUrl = getUrlPrefix(cfg) & "/?prefs=" & prefsCode
html = renderPreferences(prefs, refPath(), findThemes(cfg.staticDir), prefsUrl)
diff --git a/src/routes/resolver.nim b/src/routes/resolver.nim
index 1baf873..5f074a5 100644
--- a/src/routes/resolver.nim
+++ b/src/routes/resolver.nim
@@ -18,8 +18,8 @@ proc createResolverRouter*(cfg: Config) =
router resolver:
get "/cards/@card/@id":
let url = "https://cards.twitter.com/cards/$1/$2" % [@"card", @"id"]
- respResolved(await resolve(url, cookiePrefs()), "card")
+ respResolved(await resolve(url, requestPrefs()), "card")
get "/t.co/@url":
let url = "https://t.co/" & @"url"
- respResolved(await resolve(url, cookiePrefs()), "t.co")
+ respResolved(await resolve(url, requestPrefs()), "t.co")
diff --git a/src/routes/router_utils.nim b/src/routes/router_utils.nim
index 2ef248a..379280c 100644
--- a/src/routes/router_utils.nim
+++ b/src/routes/router_utils.nim
@@ -1,24 +1,21 @@
# SPDX-License-Identifier: AGPL-3.0-only
-import strutils, sequtils, uri, tables, json, base64
+import strutils, sequtils, uri, tables, json
from jester import Request, cookies
import ../views/general
import ".."/[utils, prefs, types]
-export utils, prefs, types, uri, base64
+export utils, prefs, types, uri
template savePref*(pref, value: string; req: Request; expire=false) =
if not expire or pref in cookies(req):
setCookie(pref, value, daysForward(when expire: -10 else: 360),
httpOnly=true, secure=cfg.useHttps, sameSite=None, path="/")
-template cookiePrefs*(): untyped {.dirty.} =
- getPrefs(cookies(request))
-
-template cookiePref*(pref): untyped {.dirty.} =
- getPref(cookies(request), pref)
+template requestPrefs*(): untyped {.dirty.} =
+ getPrefs(cookies(request), params(request))
template showError*(error: string; cfg: Config): string =
- renderMain(renderError(error), request, cfg, cookiePrefs(), "Error")
+ renderMain(renderError(error), request, cfg, requestPrefs(), "Error")
template getPath*(): untyped {.dirty.} =
$(parseUri(request.path) ? filterParams(request.params))
@@ -40,17 +37,14 @@ proc getNames*(name: string): seq[string] =
template applyUrlPrefs*() {.dirty.} =
if @"prefs".len > 0:
- try:
- let decoded = decode(@"prefs")
- var params = initTable[string, string]()
- for pair in decoded.split('&'):
- let kv = pair.split('=', maxsplit=1)
- if kv.len == 2:
- params[kv[0]] = kv[1]
- elif kv.len == 1 and kv[0].len > 0:
- params[kv[0]] = ""
- genApplyPrefs(params, request)
- except: discard
+ var prefParams = initTable[string, string]()
+ for pair in @"prefs".split(','):
+ let kv = pair.split('=', maxsplit=1)
+ if kv.len == 2:
+ prefParams[kv[0]] = kv[1]
+ elif kv.len == 1 and kv[0].len > 0:
+ prefParams[kv[0]] = ""
+ genApplyPrefs(prefParams, request)
# Rebuild URL without prefs param
var params: seq[(string, string)]
diff --git a/src/routes/rss.nim b/src/routes/rss.nim
index b0e781d..6902001 100644
--- a/src/routes/rss.nim
+++ b/src/routes/rss.nim
@@ -15,7 +15,7 @@ proc redisKey*(page, name, cursor: string): string =
if cursor.len > 0:
result &= ":" & cursor
-proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.} =
+proc timelineRss*(req: Request; cfg: Config; query: Query; prefs: Prefs): Future[Rss] {.async.} =
var profile: Profile
let
name = req.params.getOrDefault("name")
@@ -39,7 +39,7 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
return Rss(feed: profile.user.username, cursor: "suspended")
if profile.user.fullname.len > 0:
- let rss = renderTimelineRss(profile, cfg, multi=(names.len > 1))
+ let rss = renderTimelineRss(profile, cfg, prefs, multi=(names.len > 1))
return Rss(feed: rss, cursor: profile.tweets.bottom)
template respRss*(rss, page) =
@@ -64,7 +64,9 @@ proc createRssRouter*(cfg: Config) =
if @"q".len > 200:
resp Http400, showError("Search input too long.", cfg)
- let query = initQuery(params(request))
+ let
+ prefs = requestPrefs()
+ query = initQuery(params(request))
if query.kind != tweets:
resp Http400, showError("Only Tweet searches are allowed for RSS feeds.", cfg)
@@ -78,7 +80,7 @@ proc createRssRouter*(cfg: Config) =
let tweets = await getGraphTweetSearch(query, cursor)
rss.cursor = tweets.bottom
- rss.feed = renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg)
+ rss.feed = renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg, prefs)
await cacheRss(key, rss)
respRss(rss, "Search")
@@ -87,6 +89,7 @@ proc createRssRouter*(cfg: Config) =
cond cfg.enableRss
cond '.' notin @"name"
let
+ prefs = requestPrefs()
name = @"name"
key = redisKey("twitter", name, getCursor())
@@ -94,7 +97,7 @@ proc createRssRouter*(cfg: Config) =
if rss.cursor.len > 0:
respRss(rss, "User")
- rss = await timelineRss(request, cfg, Query(fromUser: @[name]))
+ rss = await timelineRss(request, cfg, Query(fromUser: @[name]), prefs)
await cacheRss(key, rss)
respRss(rss, "User")
@@ -104,6 +107,7 @@ proc createRssRouter*(cfg: Config) =
cond '.' notin @"name"
cond @"tab" in ["with_replies", "media", "search"]
let
+ prefs = requestPrefs()
name = @"name"
tab = @"tab"
query =
@@ -122,7 +126,7 @@ proc createRssRouter*(cfg: Config) =
if rss.cursor.len > 0:
respRss(rss, "User")
- rss = await timelineRss(request, cfg, query)
+ rss = await timelineRss(request, cfg, query, prefs)
await cacheRss(key, rss)
respRss(rss, "User")
@@ -147,6 +151,7 @@ proc createRssRouter*(cfg: Config) =
get "/i/lists/@id/rss":
cond cfg.enableRss
let
+ prefs = requestPrefs()
id = @"id"
cursor = getCursor()
key = redisKey("lists", id, cursor)
@@ -159,7 +164,7 @@ proc createRssRouter*(cfg: Config) =
list = await getCachedList(id=id)
timeline = await getGraphListTweets(list.id, cursor)
rss.cursor = timeline.bottom
- rss.feed = renderListRss(timeline.content, list, cfg)
+ rss.feed = renderListRss(timeline.content, list, cfg, prefs)
await cacheRss(key, rss)
respRss(rss, "List")
diff --git a/src/routes/search.nim b/src/routes/search.nim
index e9f991d..4220427 100644
--- a/src/routes/search.nim
+++ b/src/routes/search.nim
@@ -19,7 +19,7 @@ proc createSearchRouter*(cfg: Config) =
resp Http400, showError("Search input too long.", cfg)
let
- prefs = cookiePrefs()
+ prefs = requestPrefs()
query = initQuery(params(request))
title = "Search" & (if q.len > 0: " (" & q & ")" else: "")
diff --git a/src/routes/status.nim b/src/routes/status.nim
index 0168dac..838b327 100644
--- a/src/routes/status.nim
+++ b/src/routes/status.nim
@@ -21,7 +21,7 @@ proc createStatusRouter*(cfg: Config) =
if id.len > 19 or id.any(c => not c.isDigit):
resp Http404, showError("Invalid tweet ID", cfg)
- let prefs = cookiePrefs()
+ let prefs = requestPrefs()
# used for the infinite scroll feature
if @"scroll".len > 0:
diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim
index 2ac87bb..d6d8c21 100644
--- a/src/routes/timeline.nim
+++ b/src/routes/timeline.nim
@@ -117,7 +117,7 @@ proc createTimelineRouter*(cfg: Config) =
cond @"name".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9', '_', ','})
cond @"tab" in ["with_replies", "media", "search", ""]
let
- prefs = cookiePrefs()
+ prefs = requestPrefs()
after = getCursor()
names = getNames(@"name")
diff --git a/src/routes/unsupported.nim b/src/routes/unsupported.nim
index e06a183..345dee7 100644
--- a/src/routes/unsupported.nim
+++ b/src/routes/unsupported.nim
@@ -10,7 +10,7 @@ export feature
proc createUnsupportedRouter*(cfg: Config) =
router unsupported:
template feature {.dirty.} =
- resp renderMain(renderFeature(), request, cfg, cookiePrefs())
+ resp renderMain(renderFeature(), request, cfg, requestPrefs())
get "/about/feature": feature()
get "/login/?@i?": feature()
diff --git a/src/sass/index.scss b/src/sass/index.scss
index 4ca4f3d..a85002e 100644
--- a/src/sass/index.scss
+++ b/src/sass/index.scss
@@ -110,6 +110,7 @@ legend {
.bookmark-note {
margin: 0;
+ margin-bottom: 10px;
}
}
diff --git a/src/sass/inputs.scss b/src/sass/inputs.scss
index 7ea2b0a..c69711a 100644
--- a/src/sass/inputs.scss
+++ b/src/sass/inputs.scss
@@ -205,7 +205,7 @@ input::-webkit-datetime-edit-year-field:focus {
background-color: var(--bg_elements);
border: 1px solid var(--accent_border);
color: var(--fg_color);
- font-size: 12px;
+ font-size: 13px;
padding: 6px 8px;
margin: 4px 0;
word-break: break-all;
diff --git a/src/views/general.nim b/src/views/general.nim
index 3bae9d3..74ddcb4 100644
--- a/src/views/general.nim
+++ b/src/views/general.nim
@@ -39,9 +39,7 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
video=""; images: seq[string] = @[]; banner=""; ogTitle="";
rss=""; alternate=""): VNode =
- var theme = prefs.theme.toTheme
- if "theme" in req.params:
- theme = req.params["theme"].toTheme
+ let theme = prefs.theme.toTheme
let ogType =
if video.len > 0: "video"
diff --git a/src/views/preferences.nim b/src/views/preferences.nim
index 40e9e11..b051a01 100644
--- a/src/views/preferences.nim
+++ b/src/views/preferences.nim
@@ -46,6 +46,8 @@ proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string];
text "Save this URL to restore your preferences (?prefs works on all pages)"
pre(class="prefs-code"):
text prefsUrl
+ p(class="bookmark-note"):
+ verbatim "You can override preferences with query parameters (e.g. ?hlsPlayback=on). These overrides aren't saved to cookies, and links won't retain the parameters. Intended for configuring RSS feeds and other cookieless environments. Hover over a preference to see its name."
h4(class="note"):
text "Preferences are stored client-side using cookies without any personal information."
diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim
index a5fe3b2..6753c5a 100644
--- a/src/views/renderutils.nim
+++ b/src/views/renderutils.nim
@@ -65,20 +65,20 @@ proc buttonReferer*(action, text, path: string; class=""; `method`="post"): VNod
text text
proc genCheckbox*(pref, label: string; state: bool): VNode =
- buildHtml(label(class="pref-group checkbox-container")):
+ buildHtml(label(class="pref-group checkbox-container", title=pref)):
text label
input(name=pref, `type`="checkbox", checked=state)
span(class="checkbox")
proc genInput*(pref, label, state, placeholder: string; class=""; autofocus=true): VNode =
let p = placeholder
- buildHtml(tdiv(class=("pref-group pref-input " & class))):
+ buildHtml(tdiv(class=("pref-group pref-input " & class), title=pref)):
if label.len > 0:
label(`for`=pref): text label
input(name=pref, `type`="text", placeholder=p, value=state, autofocus=(autofocus and state.len == 0))
proc genSelect*(pref, label, state: string; options: seq[string]): VNode =
- buildHtml(tdiv(class="pref-group pref-input")):
+ buildHtml(tdiv(class="pref-group pref-input", title=pref)):
label(`for`=pref): text label
select(name=pref):
for opt in options:
diff --git a/src/views/rss.nimf b/src/views/rss.nimf
index 717ad99..46d7eaf 100644
--- a/src/views/rss.nimf
+++ b/src/views/rss.nimf
@@ -49,10 +49,10 @@ Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)}
#end if
#end proc
#
-#proc renderRssTweet(tweet: Tweet; cfg: Config): string =
+#proc renderRssTweet(tweet: Tweet; cfg: Config; prefs: Prefs): string =
#let tweet = tweet.retweet.get(tweet)
#let urlPrefix = getUrlPrefix(cfg)
-#let text = replaceUrls(tweet.text, defaultPrefs, absolute=urlPrefix)
+#let text = replaceUrls(tweet.text, prefs, absolute=urlPrefix)
${text.replace("\n", "
\n")}
${quoteTweet.user.fullname} (@${quoteTweet.user.username})-${renderRssTweet(quoteTweet, cfg)} +${renderRssTweet(quoteTweet, cfg, prefs)}