commit
da0ad25c2c
14 changed files with 192 additions and 54 deletions
|
@ -10,6 +10,7 @@ Inspired by the [invidio.us](https://github.com/omarroth/invidious) project.
|
||||||
- AGPLv3 licensed, no proprietary instances permitted
|
- AGPLv3 licensed, no proprietary instances permitted
|
||||||
- Dark theme
|
- Dark theme
|
||||||
- Lightweight (for [@nim_lang](https://twitter.com/nim_lang), 36KB vs 580KB from twitter.com)
|
- Lightweight (for [@nim_lang](https://twitter.com/nim_lang), 36KB vs 580KB from twitter.com)
|
||||||
|
- Native RSS feeds
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -23,11 +24,12 @@ It is possible to install Nim system-wide or in the user directory you create be
|
||||||
# su nitter
|
# su nitter
|
||||||
$ git clone https://github.com/zedeus/nitter
|
$ git clone https://github.com/zedeus/nitter
|
||||||
$ cd nitter
|
$ cd nitter
|
||||||
$ nimble build -d:release
|
$ nimble build -d:release -d:hostname="..."
|
||||||
$ nimble scss
|
$ nimble scss
|
||||||
$ mkdir ./tmp
|
$ 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`.
|
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
|
You should run Nitter behind a reverse proxy such as nginx or Apache for better
|
||||||
security.
|
security.
|
||||||
|
|
13
public/css/fontello.css
vendored
13
public/css/fontello.css
vendored
|
@ -1,11 +1,11 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'fontello';
|
font-family: 'fontello';
|
||||||
src: url('/fonts/fontello.eot?85902121');
|
src: url('/fonts/fontello.eot?33844470');
|
||||||
src: url('/fonts/fontello.eot?85902121#iefix') format('embedded-opentype'),
|
src: url('/fonts/fontello.eot?33844470#iefix') format('embedded-opentype'),
|
||||||
url('/fonts/fontello.woff2?85902121') format('woff2'),
|
url('/fonts/fontello.woff2?33844470') format('woff2'),
|
||||||
url('/fonts/fontello.woff?85902121') format('woff'),
|
url('/fonts/fontello.woff?33844470') format('woff'),
|
||||||
url('/fonts/fontello.ttf?85902121') format('truetype'),
|
url('/fonts/fontello.ttf?33844470') format('truetype'),
|
||||||
url('/fonts/fontello.svg?85902121#fontello') format('svg');
|
url('/fonts/fontello.svg?33844470#fontello') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
@ -50,4 +50,5 @@
|
||||||
.icon-search:before { content: '\e80e'; } /* '' */
|
.icon-search:before { content: '\e80e'; } /* '' */
|
||||||
.icon-pin:before { content: '\e80f'; } /* '' */
|
.icon-pin:before { content: '\e80f'; } /* '' */
|
||||||
.icon-cog:before { content: '\e812'; } /* '' */
|
.icon-cog:before { content: '\e812'; } /* '' */
|
||||||
|
.icon-rss:before { content: '\f143'; } /* '' */
|
||||||
.icon-thumbs-up:before { content: '\f164'; } /* '' */
|
.icon-thumbs-up:before { content: '\f164'; } /* '' */
|
||||||
|
|
Binary file not shown.
|
@ -40,6 +40,8 @@
|
||||||
|
|
||||||
<glyph glyph-name="cog" unicode="" d="M0 272l0 156 150 16q14 45 38 88l-96 117 109 109 117-95q41 23 88 37l16 150 156 0 16-150q45-14 88-37l117 95 109-109-96-117q24-43 38-88l150-16 0-156-150-16q-14-47-38-88l96-117-109-109-117 96q-43-24-88-38l-16-150-156 0-16 150q-47 14-88 38l-117-96-109 109 96 117q-24 41-38 88z m355 78q0-60 42-102t103-42 103 42 42 102-42 103-103 42-103-42-42-103z" horiz-adv-x="1000" />
|
<glyph glyph-name="cog" unicode="" d="M0 272l0 156 150 16q14 45 38 88l-96 117 109 109 117-95q41 23 88 37l16 150 156 0 16-150q45-14 88-37l117 95 109-109-96-117q24-43 38-88l150-16 0-156-150-16q-14-47-38-88l96-117-109-109-117 96q-43-24-88-38l-16-150-156 0-16 150q-47 14-88 38l-117-96-109 109 96 117q-24 41-38 88z m355 78q0-60 42-102t103-42 103 42 42 102-42 103-103 42-103-42-42-103z" horiz-adv-x="1000" />
|
||||||
|
|
||||||
|
<glyph glyph-name="rss-squared" unicode="" d="M286 136q0 29-21 50t-51 21-50-21-21-50 21-51 50-21 51 21 21 51z m196-53q-8 130-99 222t-221 98q-8 1-14-5t-5-13v-71q0-7 5-12t12-6q86-6 147-68t67-147q1-7 6-12t12-5h72q7 0 13 6t5 13z m214 0q-3 86-31 166t-78 145-115 114-145 78-166 31q-7 1-13-5-5-5-5-13v-71q0-7 5-12t12-6q114-4 211-62t156-155 62-211q0-8 5-13t13-5h71q7 0 13 6 6 5 5 13z m161 535v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
|
||||||
|
|
||||||
<glyph glyph-name="thumbs-up" unicode="" d="M143 100q0 15-11 25t-25 11q-15 0-25-11t-11-25q0-15 11-25t25-11q15 0 25 11t11 25z m89 286v-357q0-15-10-25t-26-11h-160q-15 0-25 11t-11 25v357q0 14 11 25t25 10h160q15 0 26-10t10-25z m661 0q0-48-31-83 9-25 9-43 1-42-24-76 9-31 0-66-9-31-31-52 5-62-27-101-36-43-110-44h-72q-37 0-80 9t-68 16-67 22q-69 24-88 25-15 0-25 11t-11 25v357q0 14 10 25t24 11q13 1 42 33t57 67q38 49 56 67 10 10 17 27t10 27 8 34q4 22 7 34t11 29 19 28q10 11 25 11 25 0 46-6t33-15 22-22 14-25 7-28 2-25 1-22q0-21-6-43t-10-33-16-31q-1-4-5-10t-6-13-5-13h155q43 0 75-32t32-75z" horiz-adv-x="928.6" />
|
<glyph glyph-name="thumbs-up" unicode="" d="M143 100q0 15-11 25t-25 11q-15 0-25-11t-11-25q0-15 11-25t25-11q15 0 25 11t11 25z m89 286v-357q0-15-10-25t-26-11h-160q-15 0-25 11t-11 25v357q0 14 11 25t25 10h160q15 0 26-10t10-25z m661 0q0-48-31-83 9-25 9-43 1-42-24-76 9-31 0-66-9-31-31-52 5-62-27-101-36-43-110-44h-72q-37 0-80 9t-68 16-67 22q-69 24-88 25-15 0-25 11t-11 25v357q0 14 10 25t24 11q13 1 42 33t57 67q38 49 56 67 10 10 17 27t10 27 8 34q4 22 7 34t11 29 19 28q10 11 25 11 25 0 46-6t33-15 22-22 14-25 7-28 2-25 1-22q0-21-6-43t-10-33-16-31q-1-4-5-10t-6-13-5-13h155q43 0 75-32t32-75z" horiz-adv-x="928.6" />
|
||||||
</font>
|
</font>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 7.1 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -15,6 +15,8 @@ const
|
||||||
twRegex = re"(www.|mobile.)?twitter.com"
|
twRegex = re"(www.|mobile.)?twitter.com"
|
||||||
nbsp = $Rune(0x000A0)
|
nbsp = $Rune(0x000A0)
|
||||||
|
|
||||||
|
const hostname {.strdefine.} = "nitter.net"
|
||||||
|
|
||||||
proc stripText*(text: string): string =
|
proc stripText*(text: string): string =
|
||||||
text.replace(nbsp, " ").strip()
|
text.replace(nbsp, " ").strip()
|
||||||
|
|
||||||
|
@ -23,12 +25,16 @@ proc shortLink*(text: string; length=28): string =
|
||||||
if result.len > length:
|
if result.len > length:
|
||||||
result = result[0 ..< length] & "…"
|
result = result[0 ..< length] & "…"
|
||||||
|
|
||||||
proc toLink*(url, text: string; class="timeline-link"): string =
|
proc toLink*(url, text: string): string =
|
||||||
a(text, class=class, href=url)
|
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 =
|
proc reUrlToLink*(m: RegexMatch; s: string): string =
|
||||||
let url = s[m.group(0)[0]]
|
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 =
|
proc reEmailToLink*(m: RegexMatch; s: string): string =
|
||||||
let url = s[m.group(0)[0]]
|
let url = s[m.group(0)[0]]
|
||||||
|
@ -48,19 +54,9 @@ proc reUsernameToLink*(m: RegexMatch; s: string): string =
|
||||||
|
|
||||||
pretext & toLink("/" & username, "@" & username)
|
pretext & toLink("/" & username, "@" & username)
|
||||||
|
|
||||||
proc linkifyText*(text: string; prefs: Prefs): string =
|
proc reUsernameToFullLink*(m: RegexMatch; s: string): string =
|
||||||
result = xmltree.escape(stripText(text))
|
result = reUsernameToLink(m, s)
|
||||||
result = result.replace(ellipsisRegex, "")
|
result = result.replace("href=\"/", &"href=\"https://{hostname}/")
|
||||||
result = result.replace(emailRegex, reEmailToLink)
|
|
||||||
result = result.replace(urlRegex, reUrlToLink)
|
|
||||||
result = result.replace(usernameRegex, reUsernameToLink)
|
|
||||||
result = result.replace(re"([^\s\(\n%])<a", "$1 <a")
|
|
||||||
result = result.replace(re"</a>\s+([;.,!\)'%]|')", "</a>$1")
|
|
||||||
result = result.replace(re"^\. <a", ".<a")
|
|
||||||
if prefs.replaceYouTube.len > 0:
|
|
||||||
result = result.replace(ytRegex, prefs.replaceYouTube)
|
|
||||||
if prefs.replaceTwitter.len > 0:
|
|
||||||
result = result.replace(twRegex, prefs.replaceTwitter)
|
|
||||||
|
|
||||||
proc replaceUrl*(url: string; prefs: Prefs): string =
|
proc replaceUrl*(url: string; prefs: Prefs): string =
|
||||||
result = url
|
result = url
|
||||||
|
@ -69,6 +65,21 @@ proc replaceUrl*(url: string; prefs: Prefs): string =
|
||||||
if prefs.replaceTwitter.len > 0:
|
if prefs.replaceTwitter.len > 0:
|
||||||
result = result.replace(twRegex, prefs.replaceTwitter)
|
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%])<a", "$1 <a")
|
||||||
|
result = result.replace(re"</a>\s+([;.,!\)'%]|')", "</a>$1")
|
||||||
|
result = result.replace(re"^\. <a", ".<a")
|
||||||
|
result = result.replaceUrl(prefs)
|
||||||
|
|
||||||
proc stripTwitterUrls*(text: string): string =
|
proc stripTwitterUrls*(text: string): string =
|
||||||
result = text
|
result = text
|
||||||
result = result.replace(picRegex, "")
|
result = result.replace(picRegex, "")
|
||||||
|
@ -103,7 +114,10 @@ proc getJoinDateFull*(profile: Profile): string =
|
||||||
profile.joinDate.format("h:mm tt - d MMM YYYY")
|
profile.joinDate.format("h:mm tt - d MMM YYYY")
|
||||||
|
|
||||||
proc getTime*(tweet: Tweet): string =
|
proc getTime*(tweet: Tweet): string =
|
||||||
tweet.time.format("d/M/yyyy', ' HH:mm:ss")
|
tweet.time.format("d/M/yyyy', 'HH:mm:ss")
|
||||||
|
|
||||||
|
proc getRfc822Time*(tweet: Tweet): string =
|
||||||
|
tweet.time.format("ddd', 'd MMM yyyy HH:mm:ss 'GMT'")
|
||||||
|
|
||||||
proc getLink*(tweet: Tweet | Quote): string =
|
proc getLink*(tweet: Tweet | Quote): string =
|
||||||
&"/{tweet.profile.username}/status/{tweet.id}"
|
&"/{tweet.profile.username}/status/{tweet.id}"
|
||||||
|
|
|
@ -5,7 +5,7 @@ import jester
|
||||||
|
|
||||||
import types, config, prefs
|
import types, config, prefs
|
||||||
import views/[general, about]
|
import views/[general, about]
|
||||||
import routes/[preferences, timeline, media]
|
import routes/[preferences, timeline, media, rss]
|
||||||
|
|
||||||
const configPath {.strdefine.} = "./nitter.conf"
|
const configPath {.strdefine.} = "./nitter.conf"
|
||||||
let cfg = getConfig(configPath)
|
let cfg = getConfig(configPath)
|
||||||
|
@ -13,6 +13,7 @@ let cfg = getConfig(configPath)
|
||||||
createPrefRouter(cfg)
|
createPrefRouter(cfg)
|
||||||
createTimelineRouter(cfg)
|
createTimelineRouter(cfg)
|
||||||
createMediaRouter(cfg)
|
createMediaRouter(cfg)
|
||||||
|
createRssRouter(cfg)
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
port = Port(cfg.port)
|
port = Port(cfg.port)
|
||||||
|
@ -32,6 +33,7 @@ routes:
|
||||||
redirect("/" & @"query")
|
redirect("/" & @"query")
|
||||||
|
|
||||||
extend preferences, ""
|
extend preferences, ""
|
||||||
|
extend rss, ""
|
||||||
extend timeline, ""
|
extend timeline, ""
|
||||||
extend media, ""
|
extend media, ""
|
||||||
|
|
||||||
|
|
32
src/routes/rss.nim
Normal file
32
src/routes/rss.nim
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import asyncdispatch, strutils
|
||||||
|
|
||||||
|
import jester
|
||||||
|
|
||||||
|
import router_utils, timeline
|
||||||
|
import ".."/[cache, agents, search]
|
||||||
|
import ../views/general
|
||||||
|
|
||||||
|
include "../views/rss.nimf"
|
||||||
|
|
||||||
|
proc showRss*(name: string; query: Option[Query]): Future[string] {.async.} =
|
||||||
|
let (profile, timeline, _) = await fetchSingleTimeline(name, "", getAgent(), query)
|
||||||
|
return renderTimelineRss(timeline.content, profile)
|
||||||
|
|
||||||
|
template respRss*(rss: typed) =
|
||||||
|
if rss.len == 0:
|
||||||
|
resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title)
|
||||||
|
resp rss, "application/rss+xml;charset=utf-8"
|
||||||
|
|
||||||
|
proc createRssRouter*(cfg: Config) =
|
||||||
|
router rss:
|
||||||
|
get "/@name/rss":
|
||||||
|
cond '.' notin @"name"
|
||||||
|
respRss(await showRss(@"name", none(Query)))
|
||||||
|
|
||||||
|
get "/@name/replies/rss":
|
||||||
|
cond '.' notin @"name"
|
||||||
|
respRss(await showRss(@"name", some(getReplyQuery(@"name"))))
|
||||||
|
|
||||||
|
get "/@name/media/rss":
|
||||||
|
cond '.' notin @"name"
|
||||||
|
respRss(await showRss(@"name", some(getMediaQuery(@"name"))))
|
|
@ -6,13 +6,17 @@ import router_utils
|
||||||
import ".."/[api, prefs, types, utils, cache, formatters, agents, search]
|
import ".."/[api, prefs, types, utils, cache, formatters, agents, search]
|
||||||
import ../views/[general, profile, timeline, status]
|
import ../views/[general, profile, timeline, status]
|
||||||
|
|
||||||
|
include "../views/rss.nimf"
|
||||||
|
|
||||||
export uri, sequtils
|
export uri, sequtils
|
||||||
export router_utils
|
export router_utils
|
||||||
export api, cache, formatters, search, agents
|
export api, cache, formatters, search, agents
|
||||||
export profile, timeline, status
|
export profile, timeline, status
|
||||||
|
|
||||||
proc showSingleTimeline(name, after, agent: string; query: Option[Query];
|
type ProfileTimeline = (Profile, Timeline, seq[GalleryPhoto])
|
||||||
prefs: Prefs; path, title: string): Future[string] {.async.} =
|
|
||||||
|
proc fetchSingleTimeline*(name, after, agent: string;
|
||||||
|
query: Option[Query]): Future[ProfileTimeline] {.async.} =
|
||||||
let railFut = getPhotoRail(name, agent)
|
let railFut = getPhotoRail(name, agent)
|
||||||
|
|
||||||
var timeline: Timeline
|
var timeline: Timeline
|
||||||
|
@ -34,35 +38,34 @@ proc showSingleTimeline(name, after, agent: string; query: Option[Query];
|
||||||
profile = await getCachedProfile(name, agent)
|
profile = await getCachedProfile(name, agent)
|
||||||
timeline = await timelineFut
|
timeline = await timelineFut
|
||||||
|
|
||||||
if profile.username.len == 0:
|
if profile.username.len == 0: return
|
||||||
return ""
|
return (profile, timeline, await railFut)
|
||||||
|
|
||||||
let profileHtml = renderProfile(profile, timeline, await railFut, prefs, path)
|
proc fetchMultiTimeline*(names: seq[string]; after, agent: string;
|
||||||
return renderMain(profileHtml, prefs, title, pageTitle(profile),
|
query: Option[Query]): Future[Timeline] {.async.} =
|
||||||
pageDesc(profile), path)
|
|
||||||
|
|
||||||
proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query];
|
|
||||||
prefs: Prefs; path, title: string): Future[string] {.async.} =
|
|
||||||
var q = query
|
var q = query
|
||||||
if q.isSome:
|
if q.isSome:
|
||||||
get(q).fromUser = names
|
get(q).fromUser = names
|
||||||
else:
|
else:
|
||||||
q = some(Query(kind: multi, fromUser: names, excludes: @["replies"]))
|
q = some(Query(kind: multi, fromUser: names, excludes: @["replies"]))
|
||||||
|
|
||||||
var timeline = renderMulti(await getTimelineSearch(get(q), after, agent),
|
return await getTimelineSearch(get(q), after, agent)
|
||||||
names.join(","), prefs, path)
|
|
||||||
|
|
||||||
return renderMain(timeline, prefs, title, "Multi")
|
|
||||||
|
|
||||||
proc showTimeline*(name, after: string; query: Option[Query];
|
proc showTimeline*(name, after: string; query: Option[Query];
|
||||||
prefs: Prefs; path, title: string): Future[string] {.async.} =
|
prefs: Prefs; path, title, rss: string): Future[string] {.async.} =
|
||||||
let agent = getAgent()
|
let agent = getAgent()
|
||||||
let names = name.strip(chars={'/'}).split(",").filterIt(it.len > 0)
|
let names = name.strip(chars={'/'}).split(",").filterIt(it.len > 0)
|
||||||
|
|
||||||
if names.len == 1:
|
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:
|
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) =
|
template respTimeline*(timeline: typed) =
|
||||||
if timeline.len == 0:
|
if timeline.len == 0:
|
||||||
|
@ -75,24 +78,27 @@ proc createTimelineRouter*(cfg: Config) =
|
||||||
router timeline:
|
router timeline:
|
||||||
get "/@name/?":
|
get "/@name/?":
|
||||||
cond '.' notin @"name"
|
cond '.' notin @"name"
|
||||||
respTimeline(await showTimeline(@"name", @"after", none(Query),
|
let rss = "/$1/rss" % @"name"
|
||||||
cookiePrefs(), getPath(), cfg.title))
|
respTimeline(await showTimeline(@"name", @"after", none(Query), cookiePrefs(),
|
||||||
|
getPath(), cfg.title, rss))
|
||||||
|
|
||||||
get "/@name/search":
|
get "/@name/search":
|
||||||
cond '.' notin @"name"
|
cond '.' notin @"name"
|
||||||
let query = initQuery(@"filter", @"include", @"not", @"sep", @"name")
|
let query = initQuery(@"filter", @"include", @"not", @"sep", @"name")
|
||||||
respTimeline(await showTimeline(@"name", @"after", some(query),
|
respTimeline(await showTimeline(@"name", @"after", some(query),
|
||||||
cookiePrefs(), getPath(), cfg.title))
|
cookiePrefs(), getPath(), cfg.title, ""))
|
||||||
|
|
||||||
get "/@name/replies":
|
get "/@name/replies":
|
||||||
cond '.' notin @"name"
|
cond '.' notin @"name"
|
||||||
|
let rss = "/$1/replies/rss" % @"name"
|
||||||
respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")),
|
respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")),
|
||||||
cookiePrefs(), getPath(), cfg.title))
|
cookiePrefs(), getPath(), cfg.title, rss))
|
||||||
|
|
||||||
get "/@name/media":
|
get "/@name/media":
|
||||||
cond '.' notin @"name"
|
cond '.' notin @"name"
|
||||||
|
let rss = "/$1/media/rss" % @"name"
|
||||||
respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")),
|
respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")),
|
||||||
cookiePrefs(), getPath(), cfg.title))
|
cookiePrefs(), getPath(), cfg.title, rss))
|
||||||
|
|
||||||
get "/@name/status/@id":
|
get "/@name/status/@id":
|
||||||
cond '.' notin @"name"
|
cond '.' notin @"name"
|
||||||
|
@ -121,7 +127,8 @@ proc createTimelineRouter*(cfg: Config) =
|
||||||
resp renderMain(html, prefs, cfg.title, title, desc, path, images = @[thumb],
|
resp renderMain(html, prefs, cfg.title, title, desc, path, images = @[thumb],
|
||||||
`type`="video", video=vidUrl)
|
`type`="video", video=vidUrl)
|
||||||
else:
|
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":
|
get "/i/web/status/@id":
|
||||||
redirect("/i/status/" & @"id")
|
redirect("/i/status/" & @"id")
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
width: 5px;
|
width: 5px;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-left: -5px;
|
margin-left: -2.5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import ../utils, ../types
|
||||||
|
|
||||||
const doctype = "<!DOCTYPE html>\n"
|
const doctype = "<!DOCTYPE html>\n"
|
||||||
|
|
||||||
proc renderNavbar*(title, path: string): VNode =
|
proc renderNavbar*(title, path, rss: string): VNode =
|
||||||
buildHtml(nav(id="nav", class="nav-bar container")):
|
buildHtml(nav(id="nav", class="nav-bar container")):
|
||||||
tdiv(class="inner-nav"):
|
tdiv(class="inner-nav"):
|
||||||
tdiv(class="item"):
|
tdiv(class="item"):
|
||||||
|
@ -14,16 +14,21 @@ proc renderNavbar*(title, path: string): VNode =
|
||||||
a(href="/"): img(class="site-logo", src="/logo.png")
|
a(href="/"): img(class="site-logo", src="/logo.png")
|
||||||
|
|
||||||
tdiv(class="item right"):
|
tdiv(class="item right"):
|
||||||
|
if rss.len > 0:
|
||||||
|
icon "rss", title="RSS Feed", href=rss
|
||||||
icon "info-circled", title="About", href="/about"
|
icon "info-circled", title="About", href="/about"
|
||||||
iconReferer "cog", "/settings", path, title="Preferences"
|
iconReferer "cog", "/settings", path, title="Preferences"
|
||||||
|
|
||||||
proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc="";
|
proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc=""; path="/";
|
||||||
path="/"; `type`="article"; video=""; images: seq[string] = @[]): string =
|
rss=""; `type`="article"; video=""; images: seq[string] = @[]): string =
|
||||||
let node = buildHtml(html(lang="en")):
|
let node = buildHtml(html(lang="en")):
|
||||||
head:
|
head:
|
||||||
link(rel="stylesheet", `type`="text/css", href="/css/style.css")
|
link(rel="stylesheet", `type`="text/css", href="/css/style.css")
|
||||||
link(rel="stylesheet", `type`="text/css", href="/css/fontello.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:
|
if prefs.hlsPlayback:
|
||||||
script(src="/js/hls.light.min.js")
|
script(src="/js/hls.light.min.js")
|
||||||
script(src="/js/hlsPlayback.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:type", content=`type`)
|
||||||
meta(property="og:title", content=titleText)
|
meta(property="og:title", content=titleText)
|
||||||
meta(property="og:description", content=desc)
|
meta(property="og:description", content=desc)
|
||||||
meta(property="og:site_name", content="Twitter")
|
meta(property="og:site_name", content="Nitter")
|
||||||
|
|
||||||
for url in images:
|
for url in images:
|
||||||
meta(property="og:image", content=getPicUrl(url))
|
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)
|
meta(property="og:video:secure_url", content=video)
|
||||||
|
|
||||||
body:
|
body:
|
||||||
renderNavbar(title, path)
|
renderNavbar(title, path, rss)
|
||||||
|
|
||||||
tdiv(id="content", class="container"):
|
tdiv(id="content", class="container"):
|
||||||
body
|
body
|
||||||
|
|
73
src/views/rss.nimf
Normal file
73
src/views/rss.nimf
Normal file
|
@ -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))
|
||||||
|
<p>${text}<br><a href="https://${quoteLink}">${quoteLink}</a></p>
|
||||||
|
#else:
|
||||||
|
<p>${text}</p>
|
||||||
|
#end if
|
||||||
|
#if tweet.photos.len > 0:
|
||||||
|
<img src="https://${hostname}${getPicUrl(tweet.photos[0])}" width="250" />
|
||||||
|
#elif tweet.video.isSome:
|
||||||
|
<img src="https://${hostname}${getPicUrl(get(tweet.video).thumb)}" width="250" />
|
||||||
|
#elif tweet.gif.isSome:
|
||||||
|
#let thumb = &"https://{hostname}{getPicUrl(get(tweet.gif).thumb)}"
|
||||||
|
#let url = &"https://{hostname}{getGifUrl(get(tweet.gif).url)}"
|
||||||
|
<video poster="${thumb}" autoplay muted loop width="250">
|
||||||
|
<source src="${url}" type="video/mp4"</source></video>
|
||||||
|
#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 = ""
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<atom:link href="https://${hostname}/${profile.username}/rss" rel="self" type="application/rss+xml" />
|
||||||
|
<title>${profile.fullname} / @${profile.username}</title>
|
||||||
|
<link>https://${hostname}/${profile.username}</link>
|
||||||
|
<description>Twitter feed for: @${profile.username}. Generated by ${hostname}</description>
|
||||||
|
<language>en-us</language>
|
||||||
|
<ttl>40</ttl>
|
||||||
|
<image>
|
||||||
|
<title>${profile.fullname} / @${profile.username}</title>
|
||||||
|
<link>https://${hostname}/${profile.username}</link>
|
||||||
|
<url>https://${hostname}${getPicUrl(profile.getUserPic(style="_400x400"))}</url>
|
||||||
|
<width>128</width>
|
||||||
|
<height>128</height>
|
||||||
|
</image>
|
||||||
|
#for tweet in tweets:
|
||||||
|
<item>
|
||||||
|
<title>${getTitle(tweet, prefs)}</title>
|
||||||
|
<dc:creator>@${tweet.profile.username}</dc:creator>
|
||||||
|
<description><![CDATA[${renderRssTweet(tweet, prefs).strip(chars={'\n'})}]]></description>
|
||||||
|
<pubDate>${getRfc822Time(tweet)}</pubDate>
|
||||||
|
<guid>https://${hostname}${getLink(tweet)}</guid>
|
||||||
|
<link>https://${hostname}${getLink(tweet)}</link>
|
||||||
|
</item>
|
||||||
|
#end for
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
#end proc
|
Loading…
Reference in a new issue