diff --git a/README.md b/README.md index 59885d5..c4f7562 100644 --- a/README.md +++ b/README.md @@ -52,24 +52,27 @@ Twitter account. ## Installation -To compile Nitter you need a Nim installation, see [nim-lang.org](https://nim-lang.org/install.html) for details. It is possible to install it system-wide or in the user directory you create below. +To compile Nitter you need a Nim installation, see +[nim-lang.org](https://nim-lang.org/install.html) for details. It is possible to +install it system-wide or in the user directory you create below. -You also need to install `libsass` to compile the scss files. On Ubuntu and Debian, you can use `libsass-dev`. +You also need to install `libsass` to compile the scss files. On Ubuntu and +Debian, you can use `libsass-dev`. ```bash # useradd -m nitter # su nitter $ git clone https://github.com/zedeus/nitter $ cd nitter -$ nimble build -d:release -d:hostname="..." +$ nimble build -d:release $ nimble scss $ mkdir ./tmp ``` -Change `-d:hostname="..."` to your instance's domain, eg. `-d:hostname:"nitter.net"`. -Set your port and page title in `nitter.conf`, then run Nitter by executing `./nitter`. -You should run Nitter behind a reverse proxy such as -[Nginx](https://github.com/zedeus/nitter/wiki/Nginx) or Apache for better security. +Set your hostname, port and page title in `nitter.conf`, then run Nitter by +executing `./nitter`. You should run Nitter behind a reverse proxy such as +[Nginx](https://github.com/zedeus/nitter/wiki/Nginx) or Apache for better +security. To build and run Nitter in Docker: ```bash diff --git a/nitter.conf b/nitter.conf index 41b961c..857b9f0 100644 --- a/nitter.conf +++ b/nitter.conf @@ -2,8 +2,9 @@ address = "0.0.0.0" port = 8080 https = true # disable to enable cookies when not using https -title = "nitter" staticDir = "./public" +title = "nitter" +hostname = "nitter.net" [Cache] directory = "./tmp" diff --git a/src/config.nim b/src/config.nim index 2b2317f..ea62b1c 100644 --- a/src/config.nim +++ b/src/config.nim @@ -16,8 +16,9 @@ proc getConfig*(path: string): Config = address: cfg.get("Server", "address", "0.0.0.0"), port: cfg.get("Server", "port", 8080), useHttps: cfg.get("Server", "https", true), - title: cfg.get("Server", "title", "Nitter"), staticDir: cfg.get("Server", "staticDir", "./public"), + title: cfg.get("Server", "title", "Nitter"), + hostname: cfg.get("Server", "hostname", "nitter.net"), cacheDir: cfg.get("Cache", "directory", "/tmp/nitter"), profileCacheTime: cfg.get("Cache", "profileMinutes", 10) diff --git a/src/formatters.nim b/src/formatters.nim index b107943..af5f075 100644 --- a/src/formatters.nim +++ b/src/formatters.nim @@ -11,8 +11,6 @@ const twRegex = re"(www.|mobile.)?twitter.com" nbsp = $Rune(0x000A0) -const hostname {.strdefine.} = "nitter.net" - proc stripText*(text: string): string = text.replace(nbsp, " ").strip() @@ -29,14 +27,14 @@ proc shortLink*(text: string; length=28): string = if result.len > length: result = result[0 ..< length] & "…" -proc replaceUrl*(url: string; prefs: Prefs; rss=false): string = +proc replaceUrl*(url: string; prefs: Prefs; absolute=""): string = result = url if prefs.replaceYouTube.len > 0: result = result.replace(ytRegex, prefs.replaceYouTube) if prefs.replaceTwitter.len > 0: result = result.replace(twRegex, prefs.replaceTwitter) - if rss: - result = result.replace("href=\"/", "href=\"https://" & hostname & "/") + if absolute.len > 0: + result = result.replace("href=\"/", "href=\"https://" & absolute & "/") proc proxifyVideo*(manifest: string; proxy: bool): string = proc cb(m: RegexMatch; s: string): string = diff --git a/src/nitter.nim b/src/nitter.nim index db61aa5..aa0be9a 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -27,10 +27,10 @@ settings: routes: get "/": - resp renderMain(renderSearch(), request, cfg.title) + resp renderMain(renderSearch(), request, cfg) get "/about": - resp renderMain(renderAbout(), request, cfg.title) + resp renderMain(renderAbout(), request, cfg) get "/explore": redirect("/about") @@ -44,7 +44,7 @@ routes: redirect(replaceUrl(url, cookiePrefs())) error Http404: - resp showError("Page not found", cfg.title) + resp showError("Page not found", cfg) extend unsupported, "" extend preferences, "" diff --git a/src/prefs.nim b/src/prefs.nim index 5df560e..0319f7f 100644 --- a/src/prefs.nim +++ b/src/prefs.nim @@ -22,6 +22,10 @@ withDb: except DbError: discard +proc getDefaultPrefs(hostname: string): Prefs = + result = genDefaultPrefs() + result.replaceTwitter = hostname + proc cache*(prefs: var Prefs) = withDb: try: @@ -31,17 +35,18 @@ proc cache*(prefs: var Prefs) = except AssertionError, KeyError: prefs.insert() -proc getPrefs*(id: string): Prefs = - if id.len == 0: return genDefaultPrefs() +proc getPrefs*(id, hostname: string): Prefs = + if id.len == 0: + return getDefaultPrefs(hostname) withDb: try: result.getOne("id = ?", id) except KeyError: - result = genDefaultPrefs() + result = getDefaultPrefs(hostname) -proc resetPrefs*(prefs: var Prefs) = - var defPrefs = genDefaultPrefs() +proc resetPrefs*(prefs: var Prefs; hostname: string) = + var defPrefs = getDefaultPrefs(hostname) defPrefs.id = prefs.id cache(defPrefs) prefs = defPrefs diff --git a/src/prefs_impl.nim b/src/prefs_impl.nim index d77e8b9..96f8479 100644 --- a/src/prefs_impl.nim +++ b/src/prefs_impl.nim @@ -1,7 +1,5 @@ import macros, tables, strutils, xmltree -const hostname {.strdefine.} = "nitter.net" - type PrefKind* = enum checkbox, select, input @@ -24,7 +22,7 @@ const prefList*: OrderedTable[string, seq[Pref]] = { "Privacy": @[ Pref(kind: input, name: "replaceTwitter", label: "Replace Twitter links with Nitter (blank to disable)", - defaultInput: hostname, placeholder: "Nitter hostname"), + defaultInput: "nitter.net", placeholder: "Nitter hostname"), Pref(kind: input, name: "replaceYouTube", label: "Replace YouTube links with Invidious (blank to disable)", diff --git a/src/routes/list.nim b/src/routes/list.nim index b393e65..0a15755 100644 --- a/src/routes/list.nim +++ b/src/routes/list.nim @@ -8,10 +8,10 @@ import ../views/[general, timeline, list] template respList*(list, timeline: typed) = if list.minId.len == 0: - halt Http404, showError("List \"" & @"list" & "\" not found", cfg.title) + halt Http404, showError("List \"" & @"list" & "\" not found", cfg) let html = renderList(timeline, list.query, @"name", @"list") let rss = "/$1/lists/$2/rss" % [@"name", @"list"] - resp renderMain(html, request, cfg.title, rss=rss) + resp renderMain(html, request, cfg, rss=rss) proc createListRouter*(cfg: Config) = router list: diff --git a/src/routes/media.nim b/src/routes/media.nim index 0dc93dd..778610d 100644 --- a/src/routes/media.nim +++ b/src/routes/media.nim @@ -68,7 +68,7 @@ proc createMediaRouter*(cfg: Config) = let prefs = cookiePrefs() if getHmac(url) != @"sig": - resp showError("Failed to verify signature", cfg.title) + resp showError("Failed to verify signature", cfg) let client = newAsyncHttpClient() var content = await client.getContent(url) diff --git a/src/routes/preferences.nim b/src/routes/preferences.nim index 51266f6..47bd5c3 100644 --- a/src/routes/preferences.nim +++ b/src/routes/preferences.nim @@ -15,7 +15,7 @@ proc createPrefRouter*(cfg: Config) = get "/settings": let html = renderPreferences(cookiePrefs(), refPath()) - resp renderMain(html, request, cfg.title, "Preferences") + resp renderMain(html, request, cfg, "Preferences") get "/settings/@i?": redirect("/settings") @@ -28,7 +28,7 @@ proc createPrefRouter*(cfg: Config) = post "/resetprefs": var prefs = cookiePrefs() - resetPrefs(prefs) + resetPrefs(prefs, cfg.hostname) savePrefs() redirect($(parseUri("/settings") ? filterParams(request.params))) diff --git a/src/routes/router_utils.nim b/src/routes/router_utils.nim index 391ba6a..44ffba8 100644 --- a/src/routes/router_utils.nim +++ b/src/routes/router_utils.nim @@ -2,7 +2,7 @@ import ../utils, ../prefs export utils, prefs template cookiePrefs*(): untyped {.dirty.} = - getPrefs(request.cookies.getOrDefault("preferences")) + getPrefs(request.cookies.getOrDefault("preferences"), cfg.hostname) template getPath*(): untyped {.dirty.} = $(parseUri(request.path) ? filterParams(request.params)) diff --git a/src/routes/rss.nim b/src/routes/rss.nim index 7db5ec5..75fc433 100644 --- a/src/routes/rss.nim +++ b/src/routes/rss.nim @@ -8,46 +8,46 @@ import ../views/general include "../views/rss.nimf" -proc showRss*(name: string; query: Query): Future[string] {.async.} = +proc showRss*(name, hostname: string; query: Query): Future[string] {.async.} = let (profile, timeline, _) = await fetchSingleTimeline(name, "", getAgent(), query) if timeline != nil: - return renderTimelineRss(timeline, profile) + return renderTimelineRss(timeline, profile, hostname) template respRss*(rss: typed) = if rss.len == 0: - halt Http404, showError("User \"" & @"name" & "\" not found", cfg.title) + halt Http404, showError("User \"" & @"name" & "\" not found", cfg) resp rss, "application/rss+xml;charset=utf-8" proc createRssRouter*(cfg: Config) = router rss: get "/search/rss": if @"q".len > 200: - resp Http400, showError("Search input too long.", cfg.title) + resp Http400, showError("Search input too long.", cfg) let query = initQuery(params(request)) if query.kind != tweets: - resp Http400, showError("Only Tweet searches are allowed for RSS feeds.", cfg.title) + resp Http400, showError("Only Tweet searches are allowed for RSS feeds.", cfg) let tweets = await getSearch[Tweet](query, "", getAgent()) - respRss(renderSearchRss(tweets.content, query.text, genQueryUrl(query))) + respRss(renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg.hostname)) get "/@name/rss": cond '.' notin @"name" - respRss(await showRss(@"name", Query())) + respRss(await showRss(@"name", cfg.hostname, Query())) get "/@name/with_replies/rss": cond '.' notin @"name" - respRss(await showRss(@"name", getReplyQuery(@"name"))) + respRss(await showRss(@"name", cfg.hostname, getReplyQuery(@"name"))) get "/@name/media/rss": cond '.' notin @"name" - respRss(await showRss(@"name", getMediaQuery(@"name"))) + respRss(await showRss(@"name", cfg.hostname, getMediaQuery(@"name"))) get "/@name/search/rss": cond '.' notin @"name" - respRss(await showRss(@"name", initQuery(params(request), name=(@"name")))) + respRss(await showRss(@"name", cfg.hostname, initQuery(params(request), name=(@"name")))) get "/@name/lists/@list/rss": cond '.' notin @"name" let list = await getListTimeline(@"name", @"list", getAgent(), "") - respRss(renderListRss(list.content, @"name", @"list")) + respRss(renderListRss(list.content, @"name", @"list", cfg.hostname)) diff --git a/src/routes/search.nim b/src/routes/search.nim index 46cb255..f349a0d 100644 --- a/src/routes/search.nim +++ b/src/routes/search.nim @@ -12,7 +12,7 @@ proc createSearchRouter*(cfg: Config) = router search: get "/search/?": if @"q".len > 200: - resp Http400, showError("Search input too long.", cfg.title) + resp Http400, showError("Search input too long.", cfg) let prefs = cookiePrefs() let query = initQuery(params(request)) @@ -22,14 +22,14 @@ proc createSearchRouter*(cfg: Config) = if "," in @"q": redirect("/" & @"q") let users = await getSearch[Profile](query, @"max_position", getAgent()) - resp renderMain(renderUserSearch(users, prefs), request, cfg.title) + resp renderMain(renderUserSearch(users, prefs), request, cfg) of tweets: let tweets = await getSearch[Tweet](query, @"max_position", getAgent()) let rss = "/search/rss?" & genQueryUrl(query) - resp renderMain(renderTweetSearch(tweets, prefs, getPath()), request, - cfg.title, rss=rss) + resp renderMain(renderTweetSearch(tweets, prefs, getPath()), + request, cfg, rss=rss) else: - halt Http404, showError("Invalid search", cfg.title) + halt Http404, showError("Invalid search", cfg) get "/hashtag/@hash": redirect("/search?q=" & encodeUrl("#" & @"hash")) diff --git a/src/routes/status.nim b/src/routes/status.nim index ac028e4..4d942b8 100644 --- a/src/routes/status.nim +++ b/src/routes/status.nim @@ -22,7 +22,7 @@ proc createStatusRouter*(cfg: Config) = var error = "Tweet not found" if conversation != nil and conversation.tweet.tombstone.len > 0: error = conversation.tweet.tombstone - halt Http404, showError(error, cfg.title) + halt Http404, showError(error, cfg) let title = pageTitle(conversation.tweet.profile) @@ -32,15 +32,15 @@ proc createStatusRouter*(cfg: Config) = if conversation.tweet.video.isSome(): let thumb = get(conversation.tweet.video).thumb let vidUrl = getVideoEmbed(conversation.tweet.id) - resp renderMain(html, request, cfg.title, title, desc, images = @[thumb], + resp renderMain(html, request, cfg, title, desc, images = @[thumb], `type`="video", video=vidUrl) elif conversation.tweet.gif.isSome(): let thumb = get(conversation.tweet.gif).thumb let vidUrl = getVideoEmbed(conversation.tweet.id) - resp renderMain(html, request, cfg.title, title, desc, images = @[thumb], + resp renderMain(html, request, cfg, title, desc, images = @[thumb], `type`="video", video=vidUrl) else: - resp renderMain(html, request, cfg.title, title, desc, + resp renderMain(html, request, cfg, title, desc, images=conversation.tweet.photos, `type`="photo") get "/@name/status/@id/photo/@i": diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index fad8e66..16cdc32 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -51,7 +51,7 @@ proc get*(req: Request; key: string): string = if key in params(req): params(req)[key] else: "" -proc showTimeline*(request: Request; query: Query; title, rss: string): Future[string] {.async.} = +proc showTimeline*(request: Request; query: Query; cfg: Config; rss: string): Future[string] {.async.} = let agent = getAgent() prefs = cookiePrefs() @@ -63,16 +63,16 @@ proc showTimeline*(request: Request; query: Query; title, rss: string): Future[s let (p, t, r) = await fetchSingleTimeline(names[0], after, agent, query) if p.username.len == 0: return let pHtml = renderProfile(p, t, r, prefs, getPath()) - return renderMain(pHtml, request, title, pageTitle(p), pageDesc(p), rss=rss) + return renderMain(pHtml, request, cfg, pageTitle(p), pageDesc(p), rss=rss) else: let timeline = await fetchMultiTimeline(names, after, agent, query) html = renderTweetSearch(timeline, prefs, getPath()) - return renderMain(html, request, title, "Multi") + return renderMain(html, request, cfg, "Multi") template respTimeline*(timeline: typed) = if timeline.len == 0: - halt Http404, showError("User \"" & @"name" & "\" not found", cfg.title) + halt Http404, showError("User \"" & @"name" & "\" not found", cfg) resp timeline proc createTimelineRouter*(cfg: Config) = @@ -82,20 +82,20 @@ proc createTimelineRouter*(cfg: Config) = get "/@name/?": cond '.' notin @"name" let rss = "/$1/rss" % @"name" - respTimeline(await showTimeline(request, Query(), cfg.title, rss)) + respTimeline(await showTimeline(request, Query(), cfg, rss)) get "/@name/with_replies": cond '.' notin @"name" let rss = "/$1/with_replies/rss" % @"name" - respTimeline(await showTimeline(request, getReplyQuery(@"name"), cfg.title, rss)) + respTimeline(await showTimeline(request, getReplyQuery(@"name"), cfg, rss)) get "/@name/media": cond '.' notin @"name" let rss = "/$1/media/rss" % @"name" - respTimeline(await showTimeline(request, getMediaQuery(@"name"), cfg.title, rss)) + respTimeline(await showTimeline(request, getMediaQuery(@"name"), cfg, rss)) get "/@name/search": cond '.' notin @"name" let query = initQuery(params(request), name=(@"name")) let rss = "/$1/search/rss?$2" % [@"name", genQueryUrl(query)] - respTimeline(await showTimeline(request, query, cfg.title, rss)) + respTimeline(await showTimeline(request, query, cfg, rss)) diff --git a/src/routes/unsupported.nim b/src/routes/unsupported.nim index 9f092f7..56b37ab 100644 --- a/src/routes/unsupported.nim +++ b/src/routes/unsupported.nim @@ -7,14 +7,14 @@ import ../views/[general, about] proc createUnsupportedRouter*(cfg: Config) = router unsupported: get "/about/feature": - resp renderMain(renderFeature(), request, cfg.title) + resp renderMain(renderFeature(), request, cfg) get "/intent/?@i?": - resp renderMain(renderFeature(), request, cfg.title) + resp renderMain(renderFeature(), request, cfg) get "/login/?@i?": - resp renderMain(renderFeature(), request, cfg.title) + resp renderMain(renderFeature(), request, cfg) get "/i/@i?/?@j?": cond @"i" != "status" - resp renderMain(renderFeature(), request, cfg.title) + resp renderMain(renderFeature(), request, cfg) diff --git a/src/types.nim b/src/types.nim index 8ea7873..d4965fd 100644 --- a/src/types.nim +++ b/src/types.nim @@ -172,8 +172,9 @@ type address*: string port*: int useHttps*: bool - title*: string staticDir*: string + title*: string + hostname*: string cacheDir*: string profileCacheTime*: int diff --git a/src/views/general.nim b/src/views/general.nim index 0ff46b1..af650e7 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -27,9 +27,9 @@ proc renderNavbar*(title, rss: string; req: Request): VNode = icon "info-circled", title="About", href="/about" iconReferer "cog", "/settings", path, title="Preferences" -proc renderMain*(body: VNode; req: Request; title="Nitter"; titleText=""; desc=""; +proc renderMain*(body: VNode; req: Request; cfg: Config; titleText=""; desc=""; rss=""; `type`="article"; video=""; images: seq[string] = @[]): string = - let prefs = getPrefs(req.cookies.getOrDefault("preferences")) + let prefs = getPrefs(req.cookies.getOrDefault("preferences"), cfg.hostname) let node = buildHtml(html(lang="en")): head: link(rel="stylesheet", `type`="text/css", href="/css/style.css") @@ -50,9 +50,9 @@ proc renderMain*(body: VNode; req: Request; title="Nitter"; titleText=""; desc=" title: if titleText.len > 0: - text titleText & " | " & title + text titleText & " | " & cfg.title else: - text title + text cfg.title meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(property="og:type", content=`type`) @@ -68,7 +68,7 @@ proc renderMain*(body: VNode; req: Request; title="Nitter"; titleText=""; desc=" meta(property="og:video:secure_url", content=video) body: - renderNavbar(title, rss, req) + renderNavbar(cfg.title, rss, req) tdiv(class="container"): body @@ -80,5 +80,5 @@ proc renderError*(error: string): VNode = tdiv(class="error-panel"): span: text error -template showError*(error, title: string): string = - renderMain(renderError(error), request, title, "Error") +template showError*(error: string; cfg: Config): string = + renderMain(renderError(error), request, cfg, "Error") diff --git a/src/views/rss.nimf b/src/views/rss.nimf index 15a43e7..8dd2f5c 100644 --- a/src/views/rss.nimf +++ b/src/views/rss.nimf @@ -1,13 +1,12 @@ #? stdtmpl(subsChar = '$', metaChad = '#') #import strutils, xmltree, strformat #import ../types, ../utils, ../formatters -#const hostname {.strdefine.} = "nitter.net" # -#proc getTitle(tweet: Tweet; prefs: Prefs): string = +#proc getTitle(tweet: Tweet; prefs: Prefs; hostname: string): string = #if tweet.pinned: result = "Pinned: " #elif tweet.retweet.isSome: result = "RT: " #end if -#result &= xmltree.escape(replaceUrl(tweet.text, prefs, rss=true)) +#result &= xmltree.escape(replaceUrl(tweet.text, prefs, absolute=hostname)) #if result.len > 0: return #end if #if tweet.photos.len > 0: @@ -19,8 +18,8 @@ #end if #end proc # -#proc renderRssTweet(tweet: Tweet; prefs: Prefs): string = -#let text = replaceUrl(tweet.text, prefs, rss=true) +#proc renderRssTweet(tweet: Tweet; prefs: Prefs; hostname: string): string = +#let text = replaceUrl(tweet.text, prefs, absolute=hostname) #if tweet.quote.isSome and get(tweet.quote).available: #let quoteLink = hostname & getLink(get(tweet.quote))

${text}
${quoteLink}

@@ -39,7 +38,7 @@ #end if #end proc # -#proc renderRssTweets(tweets: seq[Tweet]; prefs: Prefs): string = +#proc renderRssTweets(tweets: seq[Tweet]; prefs: Prefs; hostname: string): string = #var links: seq[string] #for tweet in tweets: #let link = getLink(tweet) @@ -47,9 +46,9 @@ #end if #links.add link - ${getTitle(tweet, prefs)} + ${getTitle(tweet, prefs, hostname)} @${tweet.profile.username} - + ${getRfc822Time(tweet)} https://${hostname & link} https://${hostname & link} @@ -57,7 +56,7 @@ #end for #end proc # -#proc renderTimelineRss*(timeline: Timeline; profile: Profile): string = +#proc renderTimelineRss*(timeline: Timeline; profile: Profile; hostname: string): string = #let prefs = Prefs(replaceTwitter: hostname, replaceYoutube: "invidio.us") #result = "" @@ -77,13 +76,13 @@ 128 #if timeline != nil: - ${renderRssTweets(timeline.content, prefs)} + ${renderRssTweets(timeline.content, prefs, hostname)} #end if #end proc # -#proc renderListRss*(tweets: seq[Tweet]; name, list: string): string = +#proc renderListRss*(tweets: seq[Tweet]; name, list, hostname: string): string = #let prefs = Prefs(replaceTwitter: hostname, replaceYoutube: "invidio.us") #let link = &"https://{hostname}/{name}/lists/{list}" #result = "" @@ -96,12 +95,12 @@ Twitter feed for: ${list} by @${name}. Generated by ${hostname} en-us 40 - ${renderRssTweets(tweets, prefs)} + ${renderRssTweets(tweets, prefs, hostname)} #end proc # -#proc renderSearchRss*(tweets: seq[Tweet]; name, param: string): string = +#proc renderSearchRss*(tweets: seq[Tweet]; name, param, hostname: string): string = #let prefs = Prefs(replaceTwitter: hostname, replaceYoutube: "invidio.us") #let link = &"https://{hostname}/search" #result = "" @@ -114,7 +113,7 @@ Twitter feed for search "${name}". Generated by ${hostname} en-us 40 - ${renderRssTweets(tweets, prefs)} + ${renderRssTweets(tweets, prefs, hostname)} #end proc