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