コミットを比較

...

28 コミット

作成者 SHA1 メッセージ 日付
テクニカル諏訪子 c5587a922e マージ 2021-12-31 03:32:18 +09:00
Zed 51c6605d3f Fix Twitter link replacements
Fixes #492
2021-12-30 05:11:05 +01:00
Zed d96550fcce Minor code improvements 2021-12-30 04:18:40 +01:00
Zed eed4d4033f Add canonical header to help search engines
Fixes #472
2021-12-30 04:17:58 +01:00
Zed 173dd8f016 Merge branch 'nicer-rss' 2021-12-30 02:22:39 +01:00
Zed dcac7e4a26 Simplify default preferences handling
Closes #441
2021-12-30 02:10:42 +01:00
Zed 7590dc1cda Remove hardcoded replaceYouTube config fallback 2021-12-30 01:55:55 +01:00
Zed 80f7bc0a02 Cleanup 2021-12-30 01:48:48 +01:00
Zed b0a5e38b3f Merge branch 'intent-userid' 2021-12-30 01:45:41 +01:00
Zed ddc2be8439 Make gzip handling more robust 2021-12-30 01:39:00 +01:00
Zed e3f6c72bf6 Skip list request if ID is empty 2021-12-29 08:03:00 +01:00
Zed 5e0eb02422 Improve withheld tweet rendering 2021-12-29 06:41:00 +01:00
Zed ab94d9eb7d Bump css version 2021-12-29 06:25:52 +01:00
Zed fb10bfc5e3 Revert breaking css change 2021-12-29 06:25:19 +01:00
jackyzy823 52af6b2746 Implement user_id to screen_name router 2021-12-28 09:30:55 +01:00
Zed ebffb6d251
Merge pull request #443 from jackyzy823/proxy
Add proxy for outgoing request
2021-12-28 08:20:54 +01:00
Zed c09a8d87d9 Attempt to fix occasional cursor error 2021-12-28 08:18:44 +01:00
jackyzy823 6aa913ad62 Add http proxy config 2021-12-28 07:49:49 +01:00
Zed aa2fed19d7 Skip search requests when query is empty 2021-12-28 07:35:35 +01:00
Zed 1e1e034237 Improve Twitter regex 2021-12-28 07:01:52 +01:00
Zed 0a8fd2fce2 Improve enableRSS logic 2021-12-28 06:21:22 +01:00
Zed 9b202e414b Merge branch 'config-rss-toggle' 2021-12-28 06:18:21 +01:00
Zed 2a2e9625e1 Minor config fix 2021-12-28 06:04:34 +01:00
Zed 99d3c46af5 Improve API error handling 2021-12-28 05:41:41 +01:00
Zed 6bcbe0ea9f Handle decompression errors 2021-12-28 05:13:47 +01:00
Timothy Bautista 2edf54d5b3 Add enableRSS setting in config file
Useful for instance owners who want to disable the RSS endpoint for
reasons such as abuse and not enough server resources to handle heavy
network traffic through that endpoint.

Resolves #437
2021-10-02 13:15:52 -06:00
Faye Duxovni 9c19e70a03 truncate tweet text for titles of rss feed items 2021-07-21 19:05:01 -04:00
Faye Duxovni 26842fa0bf render linebreaks in tweets properly in RSS 2021-07-21 19:05:01 -04:00
24個のファイルの変更190行の追加87行の削除

ファイルの表示

@ -12,9 +12,9 @@ listMinutes = 240 # how long to cache list info (not the tweets, so keep it hig
rssMinutes = 10 # how long to cache rss queries
redisHost = "localhost" # Change to "redis" if using docker-compose
redisPort = 6379
redisPassword = ""
redisConnections = 20 # connection pool size
redisMaxConnections = 30
redisPassword = ""
# max, new connections are opened when none are available, but if the pool size
# goes above this, they're closed when released. don't worry about this unless
# you receive tons of requests per second
@ -22,6 +22,9 @@ redisPassword = ""
[Config]
hmacKey = "secretkey" # random key for cryptographic signing of video urls
base64Media = false # use base64 encoding for proxied media urls
enableRSS = true # set this to false to disable RSS feeds
proxy = "" # proxy type http/https
proxyAuth = ""
tokenCount = 10
# minimum amount of usable tokens. tokens are used to authorize API requests,
# but they expire after ~1 hour, and have a limit of 187 requests.

ファイルの表示

@ -22,6 +22,7 @@ proc getGraphListById*(id: string): Future[List] {.async.} =
result = parseGraphList(js)
proc getListTimeline*(id: string; after=""): Future[Timeline] {.async.} =
if id.len == 0: return
let
ps = genParams({"list_id": id, "ranking_mode": "reverse_chronological"}, after)
url = listTimeline ? ps
@ -40,6 +41,12 @@ proc getProfile*(username: string): Future[Profile] {.async.} =
url = userShow ? ps
result = parseUserShow(await fetch(url, oldApi=true), username)
proc getProfileById*(userId: string): Future[Profile] {.async.} =
let
ps = genParams({"user_id": userId})
url = userShow ? ps
result = parseUserShowId(await fetch(url, oldApi=true), userId)
proc getTimeline*(id: string; after=""; replies=false): Future[Timeline] {.async.} =
let
ps = genParams({"userId": id, "include_tweet_replies": $replies}, after)
@ -67,11 +74,16 @@ proc getSearch*[T](query: Query; after=""): Future[Result[T]] {.async.} =
searchMode = ("tweet_search_mode", "live")
parse = parseTimeline
let
q = genQueryParam(query)
url = search ? genParams(searchParams & @[("q", q), searchMode], after)
result = parse(await fetch(url), after)
result.query = query
let q = genQueryParam(query)
if q.len == 0 or q == emptyQuery:
return Result[T](beginning: true, query: query)
let url = search ? genParams(searchParams & @[("q", q), searchMode], after)
try:
result = parse(await fetch(url), after)
result.query = query
except InternalError:
return Result[T](beginning: true, query: query)
proc getTweetImpl(id: string; after=""): Future[Conversation] {.async.} =
let url = tweet / (id & ".json") ? genParams(cursor=after)

ファイルの表示

@ -14,10 +14,15 @@ proc genParams*(pars: openarray[(string, string)] = @[]; cursor="";
result &= p
if ext:
result &= ("ext", "mediaStats")
if cursor.len > 0:
result &= ("cursor", cursor)
if count.len > 0:
result &= ("count", count)
if cursor.len > 0:
# The raw cursor often has plus signs, which sometimes get turned into spaces,
# so we need to them back into a plus
if " " in cursor:
result &= ("cursor", cursor.replace(" ", "+"))
else:
result &= ("cursor", cursor)
proc genHeaders*(token: Token = nil): HttpHeaders =
result = newHttpHeaders({
@ -44,9 +49,15 @@ proc fetch*(url: Uri; oldApi=false): Future[JsonNode] {.async.} =
let headers = genHeaders(token)
try:
var resp: AsyncResponse
let body = pool.use(headers):
var body = pool.use(headers):
resp = await c.get($url)
uncompress(await resp.body)
await resp.body
if body.len > 0:
if resp.headers.getOrDefault("content-encoding") == "gzip":
body = uncompress(body, dfGzip)
else:
echo "non-gzip body, url: ", url, ", body: ", body
if body.startsWith('{') or body.startsWith('['):
result = parseJson(body)
@ -64,8 +75,13 @@ proc fetch*(url: Uri; oldApi=false): Future[JsonNode] {.async.} =
echo "fetch error: ", result.getError
release(token, true)
raise rateLimitError()
if resp.status == $Http400:
raise newException(InternalError, $url)
except InternalError as e:
raise e
except Exception as e:
echo "error: ", e.msg, ", token: ", token[], ", url: ", url
echo "error: ", e.name, ", msg: ", e.msg, ", token: ", token[], ", url: ", url
if "length" notin e.msg and "descriptor" notin e.msg:
release(token, true)
raise rateLimitError()

ファイルの表示

@ -2,8 +2,8 @@
import parsecfg except Config
import types, strutils
proc get*[T](config: parseCfg.Config; s, v: string; default: T): T =
let val = config.getSectionValue(s, v)
proc get*[T](config: parseCfg.Config; section, key: string; default: T): T =
let val = config.getSectionValue(section, key)
if val.len == 0: return default
when T is int: parseInt(val)
@ -19,13 +19,16 @@ proc getConfig*(path: string): (Config, parseCfg.Config) =
useHttps: cfg.get("Server", "https", true),
httpMaxConns: cfg.get("Server", "httpMaxConnections", 100),
title: cfg.get("Server", "title", "Nitter"),
hostname: cfg.get("Server", "hostname", "nitter.net"),
title: cfg.get("Server", "title", "076ツイッター"),
hostname: cfg.get("Server", "hostname", "twitter.076.ne.jp"),
staticDir: cfg.get("Server", "staticDir", "./public"),
hmacKey: cfg.get("Config", "hmacKey", "secretkey"),
base64Media: cfg.get("Config", "base64Media", false),
minTokens: cfg.get("Config", "tokenCount", 10),
enableRss: cfg.get("Config", "enableRSS", true),
proxy: cfg.get("Config", "proxy", ""),
proxyAuth: cfg.get("Config", "proxyAuth", ""),
listCacheTime: cfg.get("Cache", "listMinutes", 120),
rssCacheTime: cfg.get("Cache", "rssMinutes", 10),

ファイルの表示

@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only
import strutils, strformat, times, uri, tables, xmltree, htmlparser
import strutils, strformat, times, uri, tables, xmltree, htmlparser, htmlgen
import regex
import types, utils, query
@ -15,7 +15,9 @@ const
# Images aren't supported due to errors from Teddit when the image
# wasn't first displayed via a post on the Teddit instance.
twRegex = re"(?<![^\/> ])(?<![^\/]\/)(www\.|mobile\.)?twitter\.com"
twRegex = re"(?<=(?<!\S)https:\/\/|(?<=\s))(www\.|mobile\.)?twitter\.com"
twLinkRegex = re"""<a href="https:\/\/twitter.com([^"]+)">twitter\.com(\S+)</a>"""
cards = "cards.twitter.com/cards"
tco = "https://t.co"
@ -29,7 +31,7 @@ const
twitter = parseUri("https://twitter.com")
proc getUrlPrefix*(cfg: Config): string =
if cfg.useHttps: "https://" & cfg.hostname
if cfg.useHttps: https & cfg.hostname
else: "http://" & cfg.hostname
proc stripHtml*(text: string): string =
@ -64,10 +66,12 @@ proc replaceUrls*(body: string; prefs: Prefs; absolute=""): string =
result = result.replace(odRegex, prefs.replaceOdysee)
if prefs.replaceTwitter.len > 0 and
(twRegex in result or tco in result):
result = result.replace(tco, "https://" & prefs.replaceTwitter & "/t.co")
(twRegex in result or twLinkRegex in result or tco in result):
result = result.replace(tco, https & prefs.replaceTwitter & "/t.co")
result = result.replace(cards, prefs.replaceTwitter & "/cards")
result = result.replace(twRegex, prefs.replaceTwitter)
result = result.replace(twLinkRegex, a(
prefs.replaceTwitter & "$2", href = https & prefs.replaceTwitter & "$1"))
if prefs.replaceReddit.len > 0 and (rdRegex in result or "redd.it" in result):
result = result.replace(rdShortRegex, prefs.replaceReddit & "/comments/")
@ -165,7 +169,7 @@ proc getTwitterLink*(path: string; params: Table[string, string]): string =
path = "/search"
if "/search" notin path and query.fromUser.len < 2:
return $(twitter / path ? filterParams(params))
return $(twitter / path)
let p = {
"f": if query.kind == users: "user" else: "live",

ファイルの表示

@ -6,10 +6,17 @@ type
conns*: seq[AsyncHttpClient]
var maxConns {.threadvar.}: int
var proxy {.threadvar.}: Proxy
proc setMaxHttpConns*(n: int) =
maxConns = n
proc setHttpProxy*(url: string; auth: string) =
if url.len > 0:
proxy = newProxy(url, auth)
else:
proxy = nil
proc release*(pool: HttpPool; client: AsyncHttpClient) =
if pool.conns.len >= maxConns:
client.close()
@ -20,7 +27,7 @@ template use*(pool: HttpPool; heads: HttpHeaders; body: untyped): untyped =
var c {.inject.}: AsyncHttpClient
if pool.conns.len == 0:
c = newAsyncHttpClient(headers=heads)
c = newAsyncHttpClient(headers=heads, proxy=proxy)
else:
c = pool.conns.pop()
c.headers = heads

ファイルの表示

@ -13,6 +13,7 @@ import routes/[
unsupported, embed, resolver, router_utils]
const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
const issuesUrl = "https://github.com/zedeus/nitter/issues"
let configPath = getEnv("NITTER_CONF_FILE", "./nitter.conf")
let (cfg, fullCfg) = getConfig(configPath)
@ -31,6 +32,7 @@ setCacheTimes(cfg)
setHmacKey(cfg.hmacKey)
setProxyEncoding(cfg.base64Media)
setMaxHttpConns(cfg.httpMaxConns)
setHttpProxy(cfg.proxy, cfg.proxyAuth)
waitFor initRedisPool(cfg)
stdout.write &"Connected to Redis at {cfg.redisHost}:{cfg.redisPort}\n"
@ -75,11 +77,17 @@ routes:
error Http404:
resp Http404, showError("Page not found", cfg)
error InternalError:
echo error.exc.name, ": ", error.exc.msg
const link = a("open a GitHub issue", href = issuesUrl)
resp Http500, showError(
&"An error occurred, please {link} with the URL you tried to visit.", cfg)
error RateLimitError:
echo error.exc.msg
resp Http429, showError("Instance has been rate limited.<br>Use " &
a("another instance", href = instancesUrl) &
" or try again later.", cfg)
echo error.exc.name, ": ", error.exc.msg
const link = a("another instance", href = instancesUrl)
resp Http429, showError(
&"Instance has been rate limited.<br>Use {link} or try again later.", cfg)
extend unsupported, ""
extend preferences, ""

ファイルの表示

@ -38,6 +38,18 @@ proc parseUserShow*(js: JsonNode; username: string): Profile =
result = parseProfile(js)
proc parseUserShowId*(js: JsonNode; userId: string): Profile =
if js.isNull:
return Profile(id: userId)
with error, js{"errors"}:
result = Profile(id: userId)
if error.getError == suspended:
result.suspended = true
return
result = parseProfile(js)
proc parseGraphProfile*(js: JsonNode; username: string): Profile =
if js.isNull: return
with error, js{"errors"}:
@ -271,10 +283,9 @@ proc parseTweet(js: JsonNode): Tweet =
else: discard
with jsWithheld, js{"withheld_in_countries"}:
var withheldInCountries: seq[string]
if jsWithheld.kind == JArray:
withheldInCountries = jsWithheld.to(seq[string])
let withheldInCountries: seq[string] =
if jsWithheld.kind != JArray: @[]
else: jsWithheld.to(seq[string])
# XX - Content is withheld in all countries
# XY - Content is withheld due to a DMCA request.
@ -282,6 +293,7 @@ proc parseTweet(js: JsonNode): Tweet =
withheldInCountries.len > 0 and ("XX" in withheldInCountries or
"XY" in withheldInCountries or
"withheld" in result.text):
result.text.removeSuffix(" Learn more.")
result.available = false
proc finalizeTweet(global: GlobalObjects; id: string): Tweet =

ファイルの表示

@ -12,6 +12,8 @@ const
"verified", "safe"
]
emptyQuery* = "include:nativeretweets"
template `@`(param: string): untyped =
if param in pms: pms[param]
else: ""

ファイルの表示

@ -78,6 +78,7 @@ proc cache*(data: Profile) {.async.} =
pool.withAcquire(r):
r.startPipelining()
discard await r.setex(name.profileKey, baseCacheTime, compress(toFlatty(data)))
discard await r.setex("i:" & data.id , baseCacheTime, data.username)
discard await r.hset(name.pidKey, name, data.id)
discard await r.flushPipeline()
@ -110,6 +111,15 @@ proc getCachedProfile*(username: string; fetch=true): Future[Profile] {.async.}
elif fetch:
result = await getProfile(username)
proc getCachedProfileUsername*(userId: string): Future[string] {.async.} =
let username = await get("i:" & userId)
if username != redisNil:
result = username
else:
let profile = await getProfileById(userId)
result = profile.username
await cache(profile)
proc getCachedPhotoRail*(name: string): Future[PhotoRail] {.async.} =
if name.len == 0: return
let rail = await get("pr:" & toLower(name))

ファイルの表示

@ -52,6 +52,7 @@ template respRss*(rss) =
proc createRssRouter*(cfg: Config) =
router rss:
get "/search/rss":
cond cfg.enableRss
if @"q".len > 200:
resp Http400, showError("Search input too long.", cfg)
@ -76,6 +77,7 @@ proc createRssRouter*(cfg: Config) =
respRss(rss)
get "/@name/rss":
cond cfg.enableRss
cond '.' notin @"name"
let
cursor = getCursor()
@ -92,6 +94,7 @@ proc createRssRouter*(cfg: Config) =
respRss(rss)
get "/@name/@tab/rss":
cond cfg.enableRss
cond '.' notin @"name"
cond @"tab" in ["with_replies", "media", "search"]
let name = @"name"
@ -117,6 +120,7 @@ proc createRssRouter*(cfg: Config) =
respRss(rss)
get "/@name/lists/@list/rss":
cond cfg.enableRss
cond '.' notin @"name"
let
cursor = getCursor()

ファイルの表示

@ -4,7 +4,7 @@ import strutils, uri
import jester
import router_utils
import ".."/[query, types, api]
import ".."/[query, types, api, formatters]
import ../views/[general, search]
include "../views/opensearch.nimf"
@ -40,7 +40,6 @@ proc createSearchRouter*(cfg: Config) =
redirect("/search?q=" & encodeUrl("#" & @"hash"))
get "/opensearch":
var url = if cfg.useHttps: "https://" else: "http://"
url &= cfg.hostname & "/search?q="
let url = getUrlPrefix(cfg) & "/search?q="
resp Http200, {"Content-Type": "application/opensearchdescription+xml"},
generateOpenSearchXML(cfg.title, cfg.hostname, url)

ファイルの表示

@ -18,6 +18,7 @@ proc createStatusRouter*(cfg: Config) =
cond '.' notin @"name"
let prefs = cookiePrefs()
# used for the infinite scroll feature
if @"scroll".len > 0:
let replies = await getReplies(@"id", getCursor())
if replies.content.len == 0:
@ -34,10 +35,12 @@ proc createStatusRouter*(cfg: Config) =
error = conv.tweet.tombstone
resp Http404, showError(error, cfg)
var
let
title = pageTitle(conv.tweet)
ogTitle = pageTitle(conv.tweet.profile)
desc = conv.tweet.text
var
images = conv.tweet.photos
video = ""

ファイルの表示

@ -78,9 +78,6 @@ proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
return (profile, timeline, await rail)
proc get*(req: Request; key: string): string =
params(req).getOrDefault(key)
proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
rss, after: string): Future[string] {.async.} =
if query.fromUser.len != 1:
@ -105,8 +102,22 @@ template respTimeline*(timeline: typed) =
resp Http404, showError("User \"" & @"name" & "\" not found", cfg)
resp t
template respUserId*() =
cond @"user_id".len > 0
let username = await getCachedProfileUsername(@"user_id")
if username.len > 0:
redirect("/" & username)
else:
resp Http404, showError("User not found", cfg)
proc createTimelineRouter*(cfg: Config) =
router timeline:
get "/i/user/@user_id":
respUserId()
get "/intent/user":
respUserId()
get "/@name/?@tab?/?":
cond '.' notin @"name"
cond @"name" notin ["pic", "gif", "video"]
@ -120,6 +131,7 @@ proc createTimelineRouter*(cfg: Config) =
if names.len != 1:
query.fromUser = names
# used for the infinite scroll feature
if @"scroll".len > 0:
if query.fromUser.len != 1:
var timeline = await getSearch[Tweet](query, after)
@ -132,10 +144,12 @@ proc createTimelineRouter*(cfg: Config) =
timeline.beginning = true
resp $renderTimelineTweets(timeline, prefs, getPath())
var rss = "/$1/$2/rss" % [@"name", @"tab"]
if @"tab".len == 0:
rss = "/$1/rss" % @"name"
elif @"tab" == "search":
rss &= "?" & genQueryUrl(query)
let rss =
if @"tab".len == 0:
"/$1/rss" % @"name"
elif @"tab" == "search":
"/$1/search/rss?$2" % [@"name", genQueryUrl(query)]
else:
"/$1/$2/rss" % [@"name", @"tab"]
respTimeline(await showTimeline(request, query, cfg, prefs, rss, after))

ファイルの表示

@ -11,10 +11,13 @@ proc createUnsupportedRouter*(cfg: Config) =
resp renderMain(renderFeature(), request, cfg, themePrefs())
get "/about/feature": feature()
get "/intent/?@i?": feature()
get "/login/?@i?": feature()
get "/@name/lists/?": feature()
get "/i/@i?/?@j?":
cond @"i" notin ["status", "lists"]
get "/intent/?@i?":
cond @"i" notin ["user"]
feature()
get "/i/@i?/?@j?":
cond @"i" notin ["status", "lists" , "user"]
feature()

ファイルの表示

@ -158,8 +158,4 @@
padding: .75em;
display: flex;
position: relative;
&.unavailable {
flex-direction: column;
}
}

ファイルの表示

@ -191,6 +191,7 @@
box-sizing: border-box;
border-radius: 10px;
background-color: var(--bg_color);
z-index: 2;
}
.tweet-link {

ファイルの表示

@ -6,6 +6,7 @@ genPrefsType()
type
RateLimitError* = object of CatchableError
InternalError* = object of CatchableError
Token* = ref object
tok*: string
@ -215,6 +216,9 @@ type
hmacKey*: string
base64Media*: bool
minTokens*: int
enableRss*: bool
proxy*: string
proxyAuth*: string
rssCacheTime*: int
listCacheTime*: int

ファイルの表示

@ -52,8 +52,10 @@ proc cleanFilename*(filename: string): string =
result &= ".png"
proc filterParams*(params: Table): seq[(string, string)] =
const filter = ["name", "id", "list", "referer", "scroll"]
toSeq(params.pairs()).filterIt(it[0] notin filter and it[1].len > 0)
const filter = ["name", "tab", "id", "list", "referer", "scroll"]
for p in params.pairs():
if p[1].len > 0 and p[0] notin filter:
result.add p
proc isTwitterUrl*(uri: Uri): bool =
uri.hostname in twitterDomains

ファイルの表示

@ -11,30 +11,29 @@ const
doctype = "<!DOCTYPE html>\n"
lp = readFile("public/lp.svg")
proc renderNavbar*(title, rss: string; req: Request): VNode =
let twitterPath = getTwitterLink(req.path, req.params)
proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
var path = $(parseUri(req.path) ? filterParams(req.params))
if "/status" in path: path.add "#m"
buildHtml(nav):
tdiv(class="inner-nav"):
tdiv(class="nav-item"):
a(class="site-name", href="/"): text title
a(class="site-name", href="/"): text cfg.title
a(href="/"): img(class="site-logo", src="/logo.png", alt="Logo")
tdiv(class="nav-item right"):
icon "search", title="Search", href="/search"
if rss.len > 0:
if cfg.enableRss and rss.len > 0:
icon "rss-feed", title="RSS Feed", href=rss
icon "bird", title="Open in Twitter", href=twitterPath
icon "bird", title="Open in Twitter", href=canonical
a(href="https://liberapay.com/zedeus"): verbatim lp
icon "info", title="About", href="/about"
iconReferer "cog", "/settings", path, title="Preferences"
proc renderHead*(prefs: Prefs; cfg: Config; titleText=""; desc=""; video="";
images: seq[string] = @[]; banner=""; ogTitle=""; theme="";
rss=""): VNode =
rss=""; canonical=""): VNode =
let ogType =
if video.len > 0: "video"
elif rss.len > 0: "object"
@ -44,7 +43,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; titleText=""; desc=""; video="";
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
buildHtml(head):
link(rel="stylesheet", type="text/css", href="/css/style.css?v=4")
link(rel="stylesheet", type="text/css", href="/css/style.css?v=6")
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=2")
if theme.len > 0:
@ -58,7 +57,10 @@ proc renderHead*(prefs: Prefs; cfg: Config; titleText=""; desc=""; video="";
link(rel="search", type="application/opensearchdescription+xml", title=cfg.title,
href=opensearchUrl)
if rss.len > 0:
if canonical.len > 0:
link(rel="canonical", href=canonical)
if cfg.enableRss and rss.len > 0:
link(rel="alternate", type="application/rss+xml", href=rss, title="RSS feed")
if prefs.hlsPlayback:
@ -117,11 +119,14 @@ proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs;
if "theme" in req.params:
theme = toLowerAscii(req.params["theme"]).replace(" ", "_")
let canonical = getTwitterLink(req.path, req.params)
let node = buildHtml(html(lang="en")):
renderHead(prefs, cfg, titleText, desc, video, images, banner, ogTitle, theme, rss)
renderHead(prefs, cfg, titleText, desc, video, images, banner, ogTitle,
theme, rss, canonical)
body:
renderNavbar(cfg.title, rss, req)
renderNavbar(cfg, req, rss, canonical)
tdiv(class="container"):
body

ファイルの表示

@ -31,7 +31,7 @@ proc linkUser*(profile: Profile, class=""): VNode =
icon "lock", title="Protected account"
proc linkText*(text: string; class=""): VNode =
let url = if "http" notin text: "http://" & text else: text
let url = if "http" notin text: https & text else: text
buildHtml():
a(href=url, class=class): text text

ファイルの表示

@ -1,21 +1,18 @@
#? stdtmpl(subsChar = '$', metaChar = '#')
## SPDX-License-Identifier: AGPL-3.0-only
#import strutils, xmltree, strformat, options
#import ../types, ../utils, ../formatters
#import strutils, xmltree, strformat, options, unicode
#import ../types, ../utils, ../formatters, ../prefs
#
#proc getPrefs(cfg: Config): Prefs =
#result.replaceTwitter = cfg.replaceTwitter
#result.replaceYouTube = cfg.replaceYouTube
#result.replaceReddit = cfg.replaceReddit
#result.replaceInstagram = cfg.replaceInstagram
#end proc
#
#proc getTitle(tweet: Tweet; prefs: Prefs; retweet: string): string =
#proc getTitle(tweet: Tweet; retweet: string): string =
#if tweet.pinned: result = "Pinned: "
#elif retweet.len > 0: result = &"RT by @{retweet}: "
#elif tweet.reply.len > 0: result = &"R to @{tweet.reply[0]}: "
#end if
#result &= xmltree.escape(stripHtml(tweet.text))
#var text = stripHtml(tweet.text)
#if unicode.runeLen(text) > 32:
# text = unicode.runeSubStr(text, 0, 32) & "..."
#end if
#result &= xmltree.escape(text)
#if result.len > 0: return
#end if
#if tweet.photos.len > 0:
@ -31,15 +28,14 @@
Twitter feed for: ${desc}. Generated by ${cfg.hostname}
#end proc
#
#proc renderRssTweet(tweet: Tweet; prefs: Prefs; cfg: Config): string =
#proc renderRssTweet(tweet: Tweet; cfg: Config): string =
#let tweet = tweet.retweet.get(tweet)
#let urlPrefix = getUrlPrefix(cfg)
#let text = replaceUrls(tweet.text, prefs, absolute=urlPrefix)
#let text = replaceUrls(tweet.text, defaultPrefs, absolute=urlPrefix)
<p>${text.replace("\n", "<br>\n")}</p>
#if tweet.quote.isSome and get(tweet.quote).available:
# let quoteLink = getLink(get(tweet.quote))
<p>${text}<br><a href="${urlPrefix}${quoteLink}">${cfg.hostname}${quoteLink}</a></p>
#else:
<p>${text}</p>
<p><a href="${urlPrefix}${quoteLink}">${cfg.hostname}${quoteLink}</a></p>
#end if
#if tweet.photos.len > 0:
# for photo in tweet.photos:
@ -60,7 +56,7 @@ Twitter feed for: ${desc}. Generated by ${cfg.hostname}
#end if
#end proc
#
#proc renderRssTweets(tweets: seq[Tweet]; prefs: Prefs; cfg: Config): string =
#proc renderRssTweets(tweets: seq[Tweet]; cfg: Config): string =
#let urlPrefix = getUrlPrefix(cfg)
#var links: seq[string]
#for t in tweets:
@ -71,9 +67,9 @@ Twitter feed for: ${desc}. Generated by ${cfg.hostname}
# end if
# links.add link
<item>
<title>${getTitle(tweet, prefs, retweet)}</title>
<title>${getTitle(tweet, retweet)}</title>
<dc:creator>@${tweet.profile.username}</dc:creator>
<description><![CDATA[${renderRssTweet(tweet, prefs, cfg).strip(chars={'\n'})}]]></description>
<description><![CDATA[${renderRssTweet(tweet, cfg).strip(chars={'\n'})}]]></description>
<pubDate>${getRfc822Time(tweet)}</pubDate>
<guid>${urlPrefix & link}</guid>
<link>${urlPrefix & link}</link>
@ -107,7 +103,7 @@ Twitter feed for: ${desc}. Generated by ${cfg.hostname}
<height>128</height>
</image>
#if timeline.content.len > 0:
${renderRssTweets(timeline.content, getPrefs(cfg), cfg)}
${renderRssTweets(timeline.content, cfg)}
#end if
</channel>
</rss>
@ -126,7 +122,7 @@ ${renderRssTweets(timeline.content, getPrefs(cfg), cfg)}
<description>${getDescription(list.name & " by @" & list.username, cfg)}</description>
<language>en-us</language>
<ttl>40</ttl>
${renderRssTweets(tweets, getPrefs(cfg), cfg)}
${renderRssTweets(tweets, cfg)}
</channel>
</rss>
#end proc
@ -145,7 +141,7 @@ ${renderRssTweets(tweets, getPrefs(cfg), cfg)}
<description>${getDescription("Search \"" & escName & "\"", cfg)}</description>
<language>en-us</language>
<ttl>40</ttl>
${renderRssTweets(tweets, getPrefs(cfg), cfg)}
${renderRssTweets(tweets, cfg)}
</channel>
</rss>
#end proc

ファイルの表示

@ -1,5 +1,4 @@
# SPDX-License-Identifier: AGPL-3.0-only
import uri
import karax/[karaxdsl, vdom]
import ".."/[types, formatters]
@ -38,7 +37,7 @@ proc renderReplies*(replies: Result[Chain]; prefs: Prefs; path: string): VNode =
renderReplyThread(thread, prefs, path)
if replies.bottom.len > 0:
renderMore(Query(), encodeUrl(replies.bottom), focus="#r")
renderMore(Query(), replies.bottom, focus="#r")
proc renderConversation*(conv: Conversation; prefs: Prefs; path: string): VNode =
let hasAfter = conv.after.content.len > 0

ファイルの表示

@ -26,7 +26,7 @@ proc renderNewer*(query: Query; path: string; focus=""): VNode =
proc renderMore*(query: Query; cursor: string; focus=""): VNode =
buildHtml(tdiv(class="show-more")):
a(href=(&"?{getQuery(query)}cursor={encodeUrl(cursor)}{focus}")):
a(href=(&"?{getQuery(query)}cursor={encodeUrl(cursor, usePlus=false)}{focus}")):
text "もっと"
proc renderNoMore(): VNode =