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