Pārlūkot izejas kodu

Support preference overrides using URL params

Fixes #186
Zed 7 mēneši atpakaļ
vecāks
revīzija
b85e8c5d7d

+ 3 - 3
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)

+ 5 - 10
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(",")

+ 7 - 27
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")

+ 1 - 1
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:

+ 2 - 2
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))

+ 1 - 1
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

+ 1 - 1
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)

+ 2 - 2
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")

+ 13 - 19
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)]

+ 12 - 7
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")

+ 1 - 1
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: "")
 

+ 1 - 1
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:

+ 1 - 1
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")
 

+ 1 - 1
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()

+ 1 - 0
src/sass/index.scss

@@ -110,6 +110,7 @@ legend {
 
   .bookmark-note {
     margin: 0;
+    margin-bottom: 10px;
   }
 }
 

+ 1 - 1
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;

+ 1 - 3
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"

+ 2 - 0
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. <code>?hlsPlayback=on</code>). 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."

+ 3 - 3
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:

+ 11 - 11
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)
 <p>${text.replace("\n", "<br>\n")}</p>
 #if tweet.photos.len > 0:
 #  for photo in tweet.photos:
@@ -81,7 +81,7 @@ Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)}
 <blockquote>
 <b>${quoteTweet.user.fullname} (@${quoteTweet.user.username})</b>
 <p>
-${renderRssTweet(quoteTweet, cfg)}
+${renderRssTweet(quoteTweet, cfg, prefs)}
 </p>
 <footer>
 — <cite><a href="${quoteLink}">${quoteLink}</a>
@@ -90,7 +90,7 @@ ${renderRssTweet(quoteTweet, cfg)}
 #end if
 #end proc
 #
-#proc renderRssTweets(tweets: seq[Tweets]; cfg: Config; userId=""): string =
+#proc renderRssTweets(tweets: seq[Tweets]; cfg: Config; prefs: Prefs; userId=""): string =
 #let urlPrefix = getUrlPrefix(cfg)
 #var links: seq[string]
 #for thread in tweets:
@@ -108,7 +108,7 @@ ${renderRssTweet(quoteTweet, cfg)}
       <item>
         <title>${getTitle(tweet, retweet)}</title>
         <dc:creator>@${tweet.user.username}</dc:creator>
-        <description><![CDATA[${renderRssTweet(tweet, cfg).strip(chars={'\n'})}]]></description>
+        <description><![CDATA[${renderRssTweet(tweet, cfg, prefs).strip(chars={'\n'})}]]></description>
         <pubDate>${getRfc822Time(tweet)}</pubDate>
 #if useGlobalGuid:
         <guid isPermaLink="false">${tweet.id}</guid>
@@ -121,7 +121,7 @@ ${renderRssTweet(quoteTweet, cfg)}
 #end for
 #end proc
 #
-#proc renderTimelineRss*(profile: Profile; cfg: Config; multi=false): string =
+#proc renderTimelineRss*(profile: Profile; cfg: Config; prefs: Prefs; multi=false): string =
 #let urlPrefix = getUrlPrefix(cfg)
 #result = ""
 #let handle = (if multi: "" else: "@") & profile.user.username
@@ -147,13 +147,13 @@ ${renderRssTweet(quoteTweet, cfg)}
     </image>
 #let tweetsList = getTweetsWithPinned(profile)
 #if tweetsList.len > 0:
-${renderRssTweets(tweetsList, cfg, userId=profile.user.id)}
+${renderRssTweets(tweetsList, cfg, prefs, userId=profile.user.id)}
 #end if
   </channel>
 </rss>
 #end proc
 #
-#proc renderListRss*(tweets: seq[Tweets]; list: List; cfg: Config): string =
+#proc renderListRss*(tweets: seq[Tweets]; list: List; cfg: Config; prefs: Prefs): string =
 #let link = &"{getUrlPrefix(cfg)}/i/lists/{list.id}"
 #result = ""
 <?xml version="1.0" encoding="UTF-8"?>
@@ -165,12 +165,12 @@ ${renderRssTweets(tweetsList, cfg, userId=profile.user.id)}
     <description>${getDescription(&"{list.name} by @{list.username}", cfg)}</description>
     <language>en-us</language>
     <ttl>40</ttl>
-${renderRssTweets(tweets, cfg)}
+${renderRssTweets(tweets, cfg, prefs)}
  </channel>
 </rss>
 #end proc
 #
-#proc renderSearchRss*(tweets: seq[Tweets]; name, param: string; cfg: Config): string =
+#proc renderSearchRss*(tweets: seq[Tweets]; name, param: string; cfg: Config; prefs: Prefs): string =
 #let link = &"{getUrlPrefix(cfg)}/search"
 #let escName = xmltree.escape(name)
 #result = ""
@@ -183,7 +183,7 @@ ${renderRssTweets(tweets, cfg)}
     <description>${getDescription(&"Search \"{escName}\"", cfg)}</description>
     <language>en-us</language>
     <ttl>40</ttl>
-${renderRssTweets(tweets, cfg)}
+${renderRssTweets(tweets, cfg, prefs)}
   </channel>
 </rss>
 #end proc