commit
						da0ad25c2c
					
				| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'; } /* '' */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											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="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" />
 | 
			
		||||
</font>
 | 
			
		||||
</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"
 | 
			
		||||
  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%])<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 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%])<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 =
 | 
			
		||||
  result = text
 | 
			
		||||
  result = result.replace(picRegex, "")
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +114,10 @@ proc getJoinDateFull*(profile: Profile): string =
 | 
			
		|||
  profile.joinDate.format("h:mm tt - d MMM YYYY")
 | 
			
		||||
 | 
			
		||||
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 =
 | 
			
		||||
  &"/{tweet.profile.username}/status/{tweet.id}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import jester
 | 
			
		|||
 | 
			
		||||
import types, config, prefs
 | 
			
		||||
import views/[general, about]
 | 
			
		||||
import routes/[preferences, timeline, media]
 | 
			
		||||
import routes/[preferences, timeline, media, rss]
 | 
			
		||||
 | 
			
		||||
const configPath {.strdefine.} = "./nitter.conf"
 | 
			
		||||
let cfg = getConfig(configPath)
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ let cfg = getConfig(configPath)
 | 
			
		|||
createPrefRouter(cfg)
 | 
			
		||||
createTimelineRouter(cfg)
 | 
			
		||||
createMediaRouter(cfg)
 | 
			
		||||
createRssRouter(cfg)
 | 
			
		||||
 | 
			
		||||
settings:
 | 
			
		||||
  port = Port(cfg.port)
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +33,7 @@ routes:
 | 
			
		|||
    redirect("/" & @"query")
 | 
			
		||||
 | 
			
		||||
  extend preferences, ""
 | 
			
		||||
  extend rss, ""
 | 
			
		||||
  extend timeline, ""
 | 
			
		||||
  extend media, ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 ../views/[general, profile, timeline, status]
 | 
			
		||||
 | 
			
		||||
include "../views/rss.nimf"
 | 
			
		||||
 | 
			
		||||
export uri, sequtils
 | 
			
		||||
export router_utils
 | 
			
		||||
export api, cache, formatters, search, agents
 | 
			
		||||
export profile, timeline, status
 | 
			
		||||
 | 
			
		||||
proc showSingleTimeline(name, after, agent: string; query: Option[Query];
 | 
			
		||||
                        prefs: Prefs; path, title: string): Future[string] {.async.} =
 | 
			
		||||
type ProfileTimeline = (Profile, Timeline, seq[GalleryPhoto])
 | 
			
		||||
 | 
			
		||||
proc fetchSingleTimeline*(name, after, agent: string;
 | 
			
		||||
                          query: Option[Query]): Future[ProfileTimeline] {.async.} =
 | 
			
		||||
  let railFut = getPhotoRail(name, agent)
 | 
			
		||||
 | 
			
		||||
  var timeline: Timeline
 | 
			
		||||
| 
						 | 
				
			
			@ -34,35 +38,34 @@ proc showSingleTimeline(name, after, agent: string; query: Option[Query];
 | 
			
		|||
      profile = await getCachedProfile(name, agent)
 | 
			
		||||
    timeline = await timelineFut
 | 
			
		||||
 | 
			
		||||
  if profile.username.len == 0:
 | 
			
		||||
    return ""
 | 
			
		||||
  if profile.username.len == 0: return
 | 
			
		||||
  return (profile, timeline, await railFut)
 | 
			
		||||
 | 
			
		||||
  let profileHtml = renderProfile(profile, timeline, await railFut, prefs, path)
 | 
			
		||||
  return renderMain(profileHtml, prefs, title, pageTitle(profile),
 | 
			
		||||
                    pageDesc(profile), path)
 | 
			
		||||
 | 
			
		||||
proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query];
 | 
			
		||||
                       prefs: Prefs; path, title: string): Future[string] {.async.} =
 | 
			
		||||
proc fetchMultiTimeline*(names: seq[string]; after, agent: string;
 | 
			
		||||
                         query: Option[Query]): Future[Timeline] {.async.} =
 | 
			
		||||
  var q = query
 | 
			
		||||
  if q.isSome:
 | 
			
		||||
    get(q).fromUser = names
 | 
			
		||||
  else:
 | 
			
		||||
    q = some(Query(kind: multi, fromUser: names, excludes: @["replies"]))
 | 
			
		||||
 | 
			
		||||
  var timeline = renderMulti(await getTimelineSearch(get(q), after, agent),
 | 
			
		||||
                             names.join(","), prefs, path)
 | 
			
		||||
 | 
			
		||||
  return renderMain(timeline, prefs, title, "Multi")
 | 
			
		||||
  return await getTimelineSearch(get(q), after, agent)
 | 
			
		||||
 | 
			
		||||
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 names = name.strip(chars={'/'}).split(",").filterIt(it.len > 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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,7 +50,7 @@
 | 
			
		|||
        width: 5px;
 | 
			
		||||
        top: 2px;
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
        margin-left: -5px;
 | 
			
		||||
        margin-left: -2.5px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import ../utils, ../types
 | 
			
		|||
 | 
			
		||||
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")):
 | 
			
		||||
    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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 New Issue