diff --git a/README.md b/README.md index 2b68460..9e8231b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Inspired by the [invidio.us](https://github.com/omarroth/invidious) project. - AGPLv3 licensed, no proprietary instances permitted - Dark theme - Lightweight (for [@nim_lang](https://twitter.com/nim_lang), 36KB vs 580KB from twitter.com) +- Native RSS feeds ## Installation @@ -23,11 +24,12 @@ It is possible to install Nim system-wide or in the user directory you create be # su nitter $ git clone https://github.com/zedeus/nitter $ cd nitter -$ nimble build -d:release +$ nimble build -d:release -d:hostname="..." $ 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 or Apache for better security. diff --git a/public/css/fontello.css b/public/css/fontello.css index 3f45019..b58b3c7 100644 --- a/public/css/fontello.css +++ b/public/css/fontello.css @@ -1,11 +1,11 @@ @font-face { font-family: 'fontello'; - src: url('/fonts/fontello.eot?85902121'); - src: url('/fonts/fontello.eot?85902121#iefix') format('embedded-opentype'), - url('/fonts/fontello.woff2?85902121') format('woff2'), - url('/fonts/fontello.woff?85902121') format('woff'), - url('/fonts/fontello.ttf?85902121') format('truetype'), - url('/fonts/fontello.svg?85902121#fontello') format('svg'); + src: url('/fonts/fontello.eot?33844470'); + src: url('/fonts/fontello.eot?33844470#iefix') format('embedded-opentype'), + url('/fonts/fontello.woff2?33844470') format('woff2'), + url('/fonts/fontello.woff?33844470') format('woff'), + url('/fonts/fontello.ttf?33844470') format('truetype'), + url('/fonts/fontello.svg?33844470#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -50,4 +50,5 @@ .icon-search:before { content: '\e80e'; } /* '' */ .icon-pin:before { content: '\e80f'; } /* '' */ .icon-cog:before { content: '\e812'; } /* '' */ +.icon-rss:before { content: '\f143'; } /* '' */ .icon-thumbs-up:before { content: '\f164'; } /* '' */ diff --git a/public/fonts/fontello.eot b/public/fonts/fontello.eot index fc8567e..a3d11e8 100644 Binary files a/public/fonts/fontello.eot and b/public/fonts/fontello.eot differ diff --git a/public/fonts/fontello.svg b/public/fonts/fontello.svg index 8e5b56b..257ace5 100644 --- a/public/fonts/fontello.svg +++ b/public/fonts/fontello.svg @@ -40,6 +40,8 @@ + + diff --git a/public/fonts/fontello.ttf b/public/fonts/fontello.ttf index a9f94a7..847494e 100644 Binary files a/public/fonts/fontello.ttf and b/public/fonts/fontello.ttf differ diff --git a/public/fonts/fontello.woff b/public/fonts/fontello.woff index 81e1c15..a508a00 100644 Binary files a/public/fonts/fontello.woff and b/public/fonts/fontello.woff differ diff --git a/public/fonts/fontello.woff2 b/public/fonts/fontello.woff2 index 3cd5362..52dff8d 100644 Binary files a/public/fonts/fontello.woff2 and b/public/fonts/fontello.woff2 differ diff --git a/src/formatters.nim b/src/formatters.nim index a7e8bc1..42ce2dd 100644 --- a/src/formatters.nim +++ b/src/formatters.nim @@ -15,6 +15,8 @@ const twRegex = re"(www.|mobile.)?twitter.com" nbsp = $Rune(0x000A0) +const hostname {.strdefine.} = "nitter.net" + proc stripText*(text: string): string = text.replace(nbsp, " ").strip() @@ -23,12 +25,16 @@ proc shortLink*(text: string; length=28): string = if result.len > length: result = result[0 ..< length] & "…" -proc toLink*(url, text: string; class="timeline-link"): string = - a(text, class=class, href=url) +proc toLink*(url, text: string): string = + a(text, href=url) + +proc reUrlToShortLink*(m: RegexMatch; s: string): string = + let url = s[m.group(0)[0]] + toLink(url, shortLink(url)) proc reUrlToLink*(m: RegexMatch; s: string): string = let url = s[m.group(0)[0]] - toLink(url, shortLink(url)) + toLink(url, url.replace(re"https?://(www.)?", "")) proc reEmailToLink*(m: RegexMatch; s: string): string = let url = s[m.group(0)[0]] @@ -48,19 +54,9 @@ proc reUsernameToLink*(m: RegexMatch; s: string): string = pretext & toLink("/" & username, "@" & username) -proc linkifyText*(text: string; prefs: Prefs): string = - result = xmltree.escape(stripText(text)) - result = result.replace(ellipsisRegex, "") - result = result.replace(emailRegex, reEmailToLink) - result = result.replace(urlRegex, reUrlToLink) - result = result.replace(usernameRegex, reUsernameToLink) - result = result.replace(re"([^\s\(\n%])\s+([;.,!\)'%]|')", "$1") - result = result.replace(re"^\. 0: - result = result.replace(ytRegex, prefs.replaceYouTube) - if prefs.replaceTwitter.len > 0: - result = result.replace(twRegex, prefs.replaceTwitter) +proc reUsernameToFullLink*(m: RegexMatch; s: string): string = + result = reUsernameToLink(m, s) + result = result.replace("href=\"/", &"href=\"https://{hostname}/") proc replaceUrl*(url: string; prefs: Prefs): string = result = url @@ -69,6 +65,21 @@ proc replaceUrl*(url: string; prefs: Prefs): string = if prefs.replaceTwitter.len > 0: result = result.replace(twRegex, prefs.replaceTwitter) +proc linkifyText*(text: string; prefs: Prefs; rss=false): string = + result = xmltree.escape(stripText(text)) + result = result.replace(ellipsisRegex, "") + result = result.replace(emailRegex, reEmailToLink) + if rss: + result = result.replace(urlRegex, reUrlToLink) + result = result.replace(usernameRegex, reUsernameToFullLink) + else: + result = result.replace(urlRegex, reUrlToShortLink) + result = result.replace(usernameRegex, reUsernameToLink) + result = result.replace(re"([^\s\(\n%])\s+([;.,!\)'%]|')", "$1") + result = result.replace(re"^\. 0) if names.len == 1: - return await showSingleTimeline(names[0], after, agent, query, prefs, path, title) + let (p, t, r) = await fetchSingleTimeline(names[0], after, agent, query) + if p.username.len == 0: return + let pHtml = renderProfile(p, t, r, prefs, path) + return renderMain(pHtml, prefs, title, pageTitle(p), pageDesc(p), path, rss=rss) else: - return await showMultiTimeline(names, after, agent, query, prefs, path, title) + let + timeline = await fetchMultiTimeline(names, after, agent, query) + html = renderMulti(timeline, names.join(","), prefs, path) + return renderMain(html, prefs, title, "Multi") template respTimeline*(timeline: typed) = if timeline.len == 0: @@ -75,24 +78,27 @@ proc createTimelineRouter*(cfg: Config) = router timeline: get "/@name/?": cond '.' notin @"name" - respTimeline(await showTimeline(@"name", @"after", none(Query), - cookiePrefs(), getPath(), cfg.title)) + let rss = "/$1/rss" % @"name" + respTimeline(await showTimeline(@"name", @"after", none(Query), cookiePrefs(), + getPath(), cfg.title, rss)) get "/@name/search": cond '.' notin @"name" let query = initQuery(@"filter", @"include", @"not", @"sep", @"name") respTimeline(await showTimeline(@"name", @"after", some(query), - cookiePrefs(), getPath(), cfg.title)) + cookiePrefs(), getPath(), cfg.title, "")) get "/@name/replies": cond '.' notin @"name" + let rss = "/$1/replies/rss" % @"name" respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")), - cookiePrefs(), getPath(), cfg.title)) + cookiePrefs(), getPath(), cfg.title, rss)) get "/@name/media": cond '.' notin @"name" + let rss = "/$1/media/rss" % @"name" respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")), - cookiePrefs(), getPath(), cfg.title)) + cookiePrefs(), getPath(), cfg.title, rss)) get "/@name/status/@id": cond '.' notin @"name" @@ -121,7 +127,8 @@ proc createTimelineRouter*(cfg: Config) = resp renderMain(html, prefs, cfg.title, title, desc, path, images = @[thumb], `type`="video", video=vidUrl) else: - resp renderMain(html, prefs, cfg.title, title, desc, path, images=conversation.tweet.photos) + resp renderMain(html, prefs, cfg.title, title, desc, path, + images=conversation.tweet.photos, `type`="photo") get "/i/web/status/@id": redirect("/i/status/" & @"id") diff --git a/src/sass/tweet/thread.scss b/src/sass/tweet/thread.scss index 3d6c10d..1bbb6f4 100644 --- a/src/sass/tweet/thread.scss +++ b/src/sass/tweet/thread.scss @@ -50,7 +50,7 @@ width: 5px; top: 2px; margin-bottom: 0; - margin-left: -5px; + margin-left: -2.5px; } } diff --git a/src/views/general.nim b/src/views/general.nim index 08b492d..5df1269 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -5,7 +5,7 @@ import ../utils, ../types const doctype = "\n" -proc renderNavbar*(title, path: string): VNode = +proc renderNavbar*(title, path, rss: string): VNode = buildHtml(nav(id="nav", class="nav-bar container")): tdiv(class="inner-nav"): tdiv(class="item"): @@ -14,16 +14,21 @@ proc renderNavbar*(title, path: string): VNode = a(href="/"): img(class="site-logo", src="/logo.png") tdiv(class="item right"): + if rss.len > 0: + icon "rss", title="RSS Feed", href=rss icon "info-circled", title="About", href="/about" iconReferer "cog", "/settings", path, title="Preferences" -proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc=""; - path="/"; `type`="article"; video=""; images: seq[string] = @[]): string = +proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc=""; path="/"; + rss=""; `type`="article"; video=""; images: seq[string] = @[]): string = let node = buildHtml(html(lang="en")): head: link(rel="stylesheet", `type`="text/css", href="/css/style.css") link(rel="stylesheet", `type`="text/css", href="/css/fontello.css") + if rss.len > 0: + link(rel="alternate", `type`="application/rss+xml", href=rss, title="RSS feed") + if prefs.hlsPlayback: script(src="/js/hls.light.min.js") script(src="/js/hlsPlayback.js") @@ -38,7 +43,7 @@ proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc=" meta(property="og:type", content=`type`) meta(property="og:title", content=titleText) meta(property="og:description", content=desc) - meta(property="og:site_name", content="Twitter") + meta(property="og:site_name", content="Nitter") for url in images: meta(property="og:image", content=getPicUrl(url)) @@ -48,7 +53,7 @@ proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc=" meta(property="og:video:secure_url", content=video) body: - renderNavbar(title, path) + renderNavbar(title, path, rss) tdiv(id="content", class="container"): body diff --git a/src/views/rss.nimf b/src/views/rss.nimf new file mode 100644 index 0000000..f9879c7 --- /dev/null +++ b/src/views/rss.nimf @@ -0,0 +1,73 @@ +#? stdtmpl(subsChar = '$', metaChad = '#') +#import strutils, xmltree, strformat +#import ../types, ../utils, ../formatters +#const hostname {.strdefine.} = "nitter.net" +# +#proc renderRssTweet(tweet: Tweet; prefs: Prefs): string = +#let text = linkifyText(tweet.text, prefs, rss=true) +#if tweet.quote.isSome and get(tweet.quote).available: +#let quoteLink = hostname & getLink(get(tweet.quote)) +

${text}
${quoteLink}

+#else: +

${text}

+#end if +#if tweet.photos.len > 0: + +#elif tweet.video.isSome: + +#elif tweet.gif.isSome: +#let thumb = &"https://{hostname}{getPicUrl(get(tweet.gif).thumb)}" +#let url = &"https://{hostname}{getGifUrl(get(tweet.gif).url)}" + +#end if +#end proc +# +#proc getTitle(tweet: Tweet; prefs: Prefs): string = +#if tweet.pinned: result = "Pinned: " +#elif tweet.retweet.isSome: result = "RT: " +#end if +#result &= xmltree.escape(replaceUrl(tweet.text, prefs)) +#if result.len > 0: return +#end if +#if tweet.photos.len > 0: +# result &= "Image" +#elif tweet.video.isSome: +# result &= "Video" +#elif tweet.gif.isSome: +# result &= "Gif" +#end if +#end proc +# +#proc renderTimelineRss*(tweets: seq[Tweet]; profile: Profile): string = +#let prefs = Prefs(replaceTwitter: hostname) +#result = "" + + + + + ${profile.fullname} / @${profile.username} + https://${hostname}/${profile.username} + Twitter feed for: @${profile.username}. Generated by ${hostname} + en-us + 40 + + ${profile.fullname} / @${profile.username} + https://${hostname}/${profile.username} + https://${hostname}${getPicUrl(profile.getUserPic(style="_400x400"))} + 128 + 128 + + #for tweet in tweets: + + ${getTitle(tweet, prefs)} + @${tweet.profile.username} + + ${getRfc822Time(tweet)} + https://${hostname}${getLink(tweet)} + https://${hostname}${getLink(tweet)} + + #end for + + +#end proc