コミットを比較
共通のコミットはありません。 "8ce0a3ba04aebdff01e09afb85d93908487b82bd" と "a7a244a6dde310d42cbf91fcfdef8a21c06a273b" の履歴はすべて異なっています。
8ce0a3ba04
...
a7a244a6dd
|
@ -1,4 +1,4 @@
|
|||
FROM nimlang/nim:1.6.10-alpine-regular as nim
|
||||
FROM nimlang/nim:1.6.2-alpine-regular as nim
|
||||
LABEL maintainer="setenforce@protonmail.com"
|
||||
|
||||
RUN apk --no-cache add libsass-dev pcre
|
||||
|
|
|
@ -38,7 +38,7 @@ tokenCount = 10
|
|||
theme = "Loli"
|
||||
replaceTwitter = "twitter.076.ne.jp"
|
||||
replaceYouTube = "youtube.076.ne.jp"
|
||||
replaceReddit = "teddit.net"
|
||||
replaceReddit = ""
|
||||
replaceInstagram = ""
|
||||
replaceOdysee = "odysee.076.ne.jp"
|
||||
proxyVideos = true
|
||||
|
|
|
@ -11,17 +11,17 @@ bin = @["nitter"]
|
|||
# Dependencies
|
||||
|
||||
requires "nim >= 1.4.8"
|
||||
requires "jester#baca3f"
|
||||
requires "jester >= 0.5.0"
|
||||
requires "karax#6abcb77"
|
||||
requires "sass#e683aa1"
|
||||
requires "nimcrypto#b41129f"
|
||||
requires "nimcrypto#a5742a9"
|
||||
requires "markdown#a661c26"
|
||||
requires "packedjson#9e6fbb6"
|
||||
requires "supersnappy#6c94198"
|
||||
requires "supersnappy#2.1.1"
|
||||
requires "redpool#8b7c1db"
|
||||
requires "https://github.com/zedeus/redis#d0a0e6f"
|
||||
requires "zippy#61922b9"
|
||||
requires "flatty#9f885d7"
|
||||
requires "zippy#0.9.11"
|
||||
requires "flatty#0.2.3"
|
||||
requires "jsony#d0e69bd"
|
||||
|
||||
|
||||
|
|
|
@ -66,8 +66,6 @@ proc parseMedia(component: Component; card: UnifiedCard; result: var Card) =
|
|||
durationMs: videoInfo.durationMillis,
|
||||
variants: videoInfo.variants
|
||||
)
|
||||
of model3d:
|
||||
result.title = "Unsupported 3D model ad"
|
||||
|
||||
proc parseUnifiedCard*(json: string): Card =
|
||||
let card = json.fromJson(UnifiedCard)
|
||||
|
@ -84,8 +82,6 @@ proc parseUnifiedCard*(json: string): Card =
|
|||
component.parseMedia(card, result)
|
||||
of buttonGroup:
|
||||
discard
|
||||
of ComponentType.unknown:
|
||||
echo "ERROR: Unknown component type: ", json
|
||||
|
||||
case component.kind
|
||||
of twitterListDetails:
|
||||
|
|
|
@ -17,7 +17,6 @@ type
|
|||
twitterListDetails
|
||||
communityDetails
|
||||
mediaWithDetailsHorizontal
|
||||
unknown
|
||||
|
||||
Component* = object
|
||||
kind*: ComponentType
|
||||
|
@ -48,7 +47,7 @@ type
|
|||
vanity*: string
|
||||
|
||||
MediaType* = enum
|
||||
photo, video, model3d
|
||||
photo, video
|
||||
|
||||
MediaEntity* = object
|
||||
kind*: MediaType
|
||||
|
@ -78,29 +77,3 @@ converter fromText*(text: Text): string = text.content
|
|||
proc renameHook*(v: var HasTypeField; fieldName: var string) =
|
||||
if fieldName == "type":
|
||||
fieldName = "kind"
|
||||
|
||||
proc enumHook*(s: string; v: var ComponentType) =
|
||||
v = case s
|
||||
of "details": details
|
||||
of "media": media
|
||||
of "swipeable_media": swipeableMedia
|
||||
of "button_group": buttonGroup
|
||||
of "app_store_details": appStoreDetails
|
||||
of "twitter_list_details": twitterListDetails
|
||||
of "community_details": communityDetails
|
||||
of "media_with_details_horizontal": mediaWithDetailsHorizontal
|
||||
else: echo "ERROR: Unknown enum value (ComponentType): ", s; unknown
|
||||
|
||||
proc enumHook*(s: string; v: var AppType) =
|
||||
v = case s
|
||||
of "android_app": androidApp
|
||||
of "iphone_app": iPhoneApp
|
||||
of "ipad_app": iPadApp
|
||||
else: echo "ERROR: Unknown enum value (AppType): ", s; androidApp
|
||||
|
||||
proc enumHook*(s: string; v: var MediaType) =
|
||||
v = case s
|
||||
of "video": video
|
||||
of "photo": photo
|
||||
of "model3d": model3d
|
||||
else: echo "ERROR: Unknown enum value (MediaType): ", s; photo
|
||||
|
|
|
@ -12,7 +12,7 @@ let
|
|||
twRegex = re"(?<=(?<!\S)https:\/\/|(?<=\s))(www\.|mobile\.)?twitter\.com"
|
||||
twLinkRegex = re"""<a href="https:\/\/twitter.com([^"]+)">twitter\.com(\S+)</a>"""
|
||||
|
||||
ytRegex = re(r"([A-z.]+\.)?youtu(be\.com|\.be)", {reStudy, reIgnoreCase})
|
||||
ytRegex = re"([A-z.]+\.)?youtu(be\.com|\.be)"
|
||||
igRegex = re"(www\.)?instagram\.com"
|
||||
odRegex = re"(www\.)?(odysee\.com|lbry\.tv|open\.lbry\.com)"
|
||||
|
||||
|
@ -57,6 +57,8 @@ proc replaceUrls*(body: string; prefs: Prefs; absolute=""): string =
|
|||
|
||||
if prefs.replaceYouTube.len > 0 and "youtu" in result:
|
||||
result = result.replace(ytRegex, prefs.replaceYouTube)
|
||||
if prefs.replaceYouTube in result:
|
||||
result = result.replace("/c/", "/")
|
||||
|
||||
if prefs.replaceInstagram.len > 0:
|
||||
result = result.replace(igRegex, prefs.replaceInstagram)
|
||||
|
@ -65,11 +67,11 @@ proc replaceUrls*(body: string; prefs: Prefs; absolute=""): string =
|
|||
result = result.replace(odRegex, prefs.replaceOdysee)
|
||||
|
||||
if prefs.replaceTwitter.len > 0 and ("twitter.com" in body or tco in body):
|
||||
result = result.replace(tco, https & prefs.replaceTwitter & "/t.co")
|
||||
result = result.replace(tco, &"{https}{prefs.replaceTwitter}/t.co")
|
||||
result = result.replace(cards, prefs.replaceTwitter & "/cards")
|
||||
result = result.replace(twRegex, prefs.replaceTwitter)
|
||||
result = result.replacef(twLinkRegex, a(
|
||||
prefs.replaceTwitter & "$2", href = https & prefs.replaceTwitter & "$1"))
|
||||
prefs.replaceTwitter & "$2", href = &"{https}{prefs.replaceTwitter}$1"))
|
||||
|
||||
if prefs.replaceReddit.len > 0 and ("reddit.com" in result or "redd.it" in result):
|
||||
result = result.replace(rdShortRegex, prefs.replaceReddit & "/comments/")
|
||||
|
|
|
@ -45,6 +45,7 @@ proc parseGraphList*(js: JsonNode): List =
|
|||
banner: list{"custom_banner_media", "media_info", "url"}.getImageStr
|
||||
)
|
||||
|
||||
|
||||
proc parsePoll(js: JsonNode): Poll =
|
||||
let vals = js{"binding_values"}
|
||||
# name format is pollNchoice_*
|
||||
|
@ -205,10 +206,6 @@ proc parseTweet(js: JsonNode): Tweet =
|
|||
)
|
||||
)
|
||||
|
||||
# fix for pinned threads
|
||||
if result.hasThread and result.threadId == 0:
|
||||
result.threadId = js{"self_thread", "id_str"}.getId
|
||||
|
||||
result.expandTweetEntities(js)
|
||||
|
||||
if js{"is_quote_status"}.getBool:
|
||||
|
|
|
@ -28,13 +28,13 @@ template `?`*(js: JsonNode): untyped =
|
|||
if j.isNull: return
|
||||
j
|
||||
|
||||
template with*(ident, value, body): untyped =
|
||||
if true:
|
||||
template `with`*(ident, value, body): untyped =
|
||||
block:
|
||||
let ident {.inject.} = value
|
||||
if ident != nil: body
|
||||
|
||||
template with*(ident; value: JsonNode; body): untyped =
|
||||
if true:
|
||||
template `with`*(ident; value: JsonNode; body): untyped =
|
||||
block:
|
||||
let ident {.inject.} = value
|
||||
if value.notNull: body
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
import asyncdispatch, tables, times, hashes, uri
|
||||
import asyncdispatch, strutils, strformat, tables, times, hashes, uri
|
||||
|
||||
import jester
|
||||
|
||||
|
@ -10,11 +10,6 @@ include "../views/rss.nimf"
|
|||
|
||||
export times, hashes
|
||||
|
||||
proc redisKey*(page, name, cursor: string): string =
|
||||
result = page & ":" & name
|
||||
if cursor.len > 0:
|
||||
result &= ":" & cursor
|
||||
|
||||
proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.} =
|
||||
var profile: Profile
|
||||
let
|
||||
|
@ -47,8 +42,8 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
|
|||
template respRss*(rss, page) =
|
||||
if rss.cursor.len == 0:
|
||||
let info = case page
|
||||
of "User": " \"" & @"name" & "\" "
|
||||
of "List": " \"" & @"id" & "\" "
|
||||
of "User": &""" "{@"name"}" """
|
||||
of "List": &""" "{@"id"}" """
|
||||
else: " "
|
||||
|
||||
resp Http404, showError(page & info & "not found", cfg)
|
||||
|
@ -72,7 +67,7 @@ proc createRssRouter*(cfg: Config) =
|
|||
|
||||
let
|
||||
cursor = getCursor()
|
||||
key = redisKey("search", $hash(genQueryUrl(query)), cursor)
|
||||
key = &"search:{hash(genQueryUrl(query))}:cursor"
|
||||
|
||||
var rss = await getCachedRss(key)
|
||||
if rss.cursor.len > 0:
|
||||
|
@ -89,8 +84,9 @@ proc createRssRouter*(cfg: Config) =
|
|||
cond cfg.enableRss
|
||||
cond '.' notin @"name"
|
||||
let
|
||||
cursor = getCursor()
|
||||
name = @"name"
|
||||
key = redisKey("twitter", name, getCursor())
|
||||
key = &"twitter:{name}:{cursor}"
|
||||
|
||||
var rss = await getCachedRss(key)
|
||||
if rss.cursor.len > 0:
|
||||
|
@ -105,20 +101,18 @@ proc createRssRouter*(cfg: Config) =
|
|||
cond cfg.enableRss
|
||||
cond '.' notin @"name"
|
||||
cond @"tab" in ["with_replies", "media", "search"]
|
||||
let
|
||||
name = @"name"
|
||||
tab = @"tab"
|
||||
query =
|
||||
case tab
|
||||
let name = @"name"
|
||||
let query =
|
||||
case @"tab"
|
||||
of "with_replies": getReplyQuery(name)
|
||||
of "media": getMediaQuery(name)
|
||||
of "search": initQuery(params(request), name=name)
|
||||
else: Query(fromUser: @[name])
|
||||
|
||||
let searchKey = if tab != "search": ""
|
||||
else: ":" & $hash(genQueryUrl(query))
|
||||
|
||||
let key = redisKey(tab, name & searchKey, getCursor())
|
||||
var key = &"""{@"tab"}:{@"name"}:"""
|
||||
if @"tab" == "search":
|
||||
key &= $hash(genQueryUrl(query)) & ":"
|
||||
key &= getCursor()
|
||||
|
||||
var rss = await getCachedRss(key)
|
||||
if rss.cursor.len > 0:
|
||||
|
@ -138,27 +132,28 @@ proc createRssRouter*(cfg: Config) =
|
|||
cursor = getCursor()
|
||||
|
||||
if list.id.len == 0:
|
||||
resp Http404, showError("List \"" & @"slug" & "\" not found", cfg)
|
||||
resp Http404, showError(&"""List "{@"slug"}" not found""", cfg)
|
||||
|
||||
let url = "/i/lists/" & list.id & "/rss"
|
||||
let url = &"/i/lists/{list.id}/rss"
|
||||
if cursor.len > 0:
|
||||
redirect(url & "?cursor=" & encodeUrl(cursor, false))
|
||||
redirect(&"{url}?cursor={encodeUrl(cursor, false)}")
|
||||
else:
|
||||
redirect(url)
|
||||
|
||||
get "/i/lists/@id/rss":
|
||||
cond cfg.enableRss
|
||||
let
|
||||
id = @"id"
|
||||
cursor = getCursor()
|
||||
key = redisKey("lists", id, cursor)
|
||||
key =
|
||||
if cursor.len == 0: "lists:" & @"id"
|
||||
else: &"""lists:{@"id"}:{cursor}"""
|
||||
|
||||
var rss = await getCachedRss(key)
|
||||
if rss.cursor.len > 0:
|
||||
respRss(rss, "List")
|
||||
|
||||
let
|
||||
list = await getCachedList(id=id)
|
||||
list = await getCachedList(id=(@"id"))
|
||||
timeline = await getListTimeline(list.id, cursor)
|
||||
rss.cursor = timeline.bottom
|
||||
rss.feed = renderListRss(timeline.content, list, cfg)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
import strutils, uri
|
||||
import strutils, strformat, uri
|
||||
|
||||
import jester
|
||||
|
||||
|
@ -14,32 +14,30 @@ export search
|
|||
proc createSearchRouter*(cfg: Config) =
|
||||
router search:
|
||||
get "/search/?":
|
||||
let q = @"q"
|
||||
if q.len > 500:
|
||||
if @"q".len > 500:
|
||||
resp Http400, showError("Search input too long.", cfg)
|
||||
|
||||
let
|
||||
prefs = cookiePrefs()
|
||||
query = initQuery(params(request))
|
||||
title = "Search" & (if q.len > 0: " (" & q & ")" else: "")
|
||||
|
||||
case query.kind
|
||||
of users:
|
||||
if "," in q:
|
||||
redirect("/" & q)
|
||||
if "," in @"q":
|
||||
redirect("/" & @"q")
|
||||
let users = await getSearch[User](query, getCursor())
|
||||
resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs, title)
|
||||
resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs)
|
||||
of tweets:
|
||||
let
|
||||
tweets = await getSearch[Tweet](query, getCursor())
|
||||
rss = "/search/rss?" & genQueryUrl(query)
|
||||
resp renderMain(renderTweetSearch(tweets, prefs, getPath()),
|
||||
request, cfg, prefs, title, rss=rss)
|
||||
request, cfg, prefs, rss=rss)
|
||||
else:
|
||||
resp Http404, showError("Invalid search", cfg)
|
||||
|
||||
get "/hashtag/@hash":
|
||||
redirect("/search?q=" & encodeUrl("#" & @"hash"))
|
||||
redirect(&"""/search?q={encodeUrl("#" & @"hash")}""")
|
||||
|
||||
get "/opensearch":
|
||||
let url = getUrlPrefix(cfg) & "/search?q="
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
import asyncdispatch, strutils, sequtils, uri, options, times
|
||||
import asyncdispatch, strutils, strformat, sequtils, uri, options, times
|
||||
import jester, karax/vdom
|
||||
|
||||
import router_utils
|
||||
|
@ -102,7 +102,7 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
|
|||
template respTimeline*(timeline: typed) =
|
||||
let t = timeline
|
||||
if t.len == 0:
|
||||
resp Http404, showError("User \"" & @"name" & "\" not found", cfg)
|
||||
resp Http404, showError(&"""User "{@"name"}" not found""", cfg)
|
||||
resp t
|
||||
|
||||
template respUserId*() =
|
||||
|
|
|
@ -138,6 +138,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.attribution {
|
||||
display: flex;
|
||||
pointer-events: all;
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
||||
@media(max-width: 600px) {
|
||||
.main-tweet .tweet-content {
|
||||
font-size: 16px;
|
||||
|
|
|
@ -81,7 +81,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
|||
|
||||
title:
|
||||
if titleText.len > 0:
|
||||
text titleText & " | " & cfg.title
|
||||
text &"{titleText}|{cfg.title}"
|
||||
else:
|
||||
text cfg.title
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ proc renderUserCard*(user: User; prefs: Prefs): VNode =
|
|||
span:
|
||||
let url = replaceUrls(user.website, prefs)
|
||||
icon "link"
|
||||
a(href=url): text url.shortLink
|
||||
a(href=url): text shortLink(url)
|
||||
|
||||
tdiv(class="profile-joindate"):
|
||||
span(title=getJoinDateFull(user)):
|
||||
|
@ -108,7 +108,7 @@ proc renderProfile*(profile: var Profile; prefs: Prefs; path: string): VNode =
|
|||
renderBanner(profile.user.banner)
|
||||
|
||||
let sticky = if prefs.stickyProfile: " sticky" else: ""
|
||||
tdiv(class=("profile-tab" & sticky)):
|
||||
tdiv(class=(&"profile-tab{sticky}")):
|
||||
renderUserCard(profile.user, prefs)
|
||||
if profile.photoRail.len > 0:
|
||||
renderPhotoRail(profile)
|
||||
|
|
|
@ -55,12 +55,12 @@ proc genCheckbox*(pref, label: string; state: bool): VNode =
|
|||
else: input(name=pref, `type`="checkbox")
|
||||
span(class="checkbox")
|
||||
|
||||
proc genInput*(pref, label, state, placeholder: string; class=""; autofocus=true): VNode =
|
||||
proc genInput*(pref, label, state, placeholder: string; class=""): VNode =
|
||||
let p = placeholder
|
||||
buildHtml(tdiv(class=("pref-group pref-input " & class))):
|
||||
if label.len > 0:
|
||||
label(`for`=pref): text label
|
||||
if autofocus and state.len == 0:
|
||||
if state.len == 0:
|
||||
input(name=pref, `type`="text", placeholder=p, value=state, autofocus="")
|
||||
else:
|
||||
input(name=pref, `type`="text", placeholder=p, value=state)
|
||||
|
|
|
@ -87,7 +87,7 @@ proc renderSearchPanel*(query: Query): VNode =
|
|||
genDate("until", query.until)
|
||||
tdiv:
|
||||
span(class="search-title"): text "Near"
|
||||
genInput("near", "", query.near, "地域…", autofocus=false)
|
||||
genInput("near", "", query.near, placeholder="地域…")
|
||||
|
||||
proc renderTweetSearch*(results: Result[Tweet]; prefs: Prefs; path: string;
|
||||
pinned=none(Tweet)): VNode =
|
||||
|
|
|
@ -3,7 +3,7 @@ from parameterized import parameterized
|
|||
|
||||
text = [
|
||||
['elonmusk/status/1138136540096319488',
|
||||
'TREV PAGE', '@Model3Owners',
|
||||
'Trev Page', '@Model3Owners',
|
||||
"""As of March 58.4% of new car sales in Norway are electric.
|
||||
|
||||
What are we doing wrong? reuters.com/article/us-norwa…"""],
|
||||
|
|
新しいイシューから参照