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")}

#if tweet.photos.len > 0: # for photo in tweet.photos: @@ -81,7 +81,7 @@ Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)}
${quoteTweet.user.fullname} (@${quoteTweet.user.username})

-${renderRssTweet(quoteTweet, cfg)} +${renderRssTweet(quoteTweet, cfg, prefs)}