Merge remote-tracking branch 'upstream/master'

I really hope nothing breaks! :3
This commit is contained in:
2025-11-24 20:25:40 -03:00
14 changed files with 82 additions and 45 deletions

View File

@@ -6,7 +6,7 @@ import types, utils, query
const
cards = "cards.twitter.com/cards"
tco = "https://t.co"
twitter = parseUri("https://twitter.com")
twitter = parseUri("https://x.com")
let
twRegex = re"(?<=(?<!\S)https:\/\/|(?<=\s))(www\.|mobile\.)?twitter\.com"
@@ -64,18 +64,20 @@ proc replaceUrls*(body: string; prefs: Prefs; absolute=""): string =
result = body
if prefs.replaceYouTube.len > 0 and "youtu" in result:
result = result.replace(ytRegex, prefs.replaceYouTube)
let youtubeHost = strip(prefs.replaceYouTube, chars={'/'})
result = result.replace(ytRegex, youtubeHost)
if prefs.replaceTwitter.len > 0:
let twitterHost = strip(prefs.replaceTwitter, chars={'/'})
if tco in result:
result = result.replace(tco, https & prefs.replaceTwitter & "/t.co")
result = result.replace(tco, https & twitterHost & "/t.co")
if "x.com" in result:
result = result.replace(xRegex, prefs.replaceTwitter)
result = result.replace(xRegex, twitterHost)
result = result.replacef(xLinkRegex, a(
prefs.replaceTwitter & "$2", href = https & prefs.replaceTwitter & "$1"))
twitterHost & "$2", href = https & twitterHost & "$1"))
if "twitter.com" in result:
result = result.replace(cards, prefs.replaceTwitter & "/cards")
result = result.replace(twRegex, prefs.replaceTwitter)
result = result.replace(cards, twitterHost & "/cards")
result = result.replace(twRegex, twitterHost)
result = result.replacef(twLinkRegex, a(
prefs.replaceTwitter & "$2", href = https & prefs.replaceTwitter & "$1"))
if "imgur.com" in result:
@@ -96,9 +98,10 @@ proc replaceUrls*(body: string; prefs: Prefs; absolute=""): string =
prefs.replaceSoundCloud & "$4", href = https & prefs.replaceSoundCloud & "$2"))
if prefs.replaceReddit.len > 0 and ("reddit.com" in result or "redd.it" in result):
result = result.replace(rdShortRegex, prefs.replaceReddit & "/comments/")
result = result.replace(rdRegex, prefs.replaceReddit)
if prefs.replaceReddit in result and "/gallery/" in result:
let redditHost = strip(prefs.replaceReddit, chars={'/'})
result = result.replace(rdShortRegex, redditHost & "/comments/")
result = result.replace(rdRegex, redditHost)
if redditHost in result and "/gallery/" in result:
result = result.replace("/gallery/", "/comments/")
if absolute.len > 0 and "href" in result:

View File

@@ -487,6 +487,7 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
result.before.content.add tweet
elif entryId.startsWith("cursor-bottom"):
var cursorValue = select(
e{"content", "value"},
e{"content", "content", "value"},
e{"content", "itemContent", "value"}
)

View File

@@ -6,10 +6,9 @@ import types
const
validFilters* = @[
"media", "images", "twimg", "videos",
"native_video", "consumer_video", "pro_video",
"native_video", "consumer_video", "spaces",
"links", "news", "quote", "mentions",
"replies", "retweets", "nativeretweets",
"verified", "safe"
"replies", "retweets", "nativeretweets"
]
emptyQuery* = "include:nativeretweets"
@@ -18,6 +17,11 @@ template `@`(param: string): untyped =
if param in pms: pms[param]
else: ""
proc validateNumber(value: string): string =
if value.anyIt(not it.isDigit):
return ""
return value
proc initQuery*(pms: Table[string, string]; name=""): Query =
result = Query(
kind: parseEnum[QueryKind](@"f", tweets),
@@ -26,7 +30,7 @@ proc initQuery*(pms: Table[string, string]; name=""): Query =
excludes: validFilters.filterIt("e-" & it in pms),
since: @"since",
until: @"until",
near: @"near"
minLikes: validateNumber(@"min_faves")
)
if name.len > 0:
@@ -81,8 +85,8 @@ proc genQueryParam*(query: Query): string =
result &= " since:" & query.since
if query.until.len > 0:
result &= " until:" & query.until
if query.near.len > 0:
result &= &" near:\"{query.near}\" within:15mi"
if query.minLikes.len > 0:
result &= " min_faves:" & query.minLikes
if query.text.len > 0:
if result.len > 0:
result &= " " & query.text
@@ -106,8 +110,8 @@ proc genQueryUrl*(query: Query): string =
params.add "since=" & query.since
if query.until.len > 0:
params.add "until=" & query.until
if query.near.len > 0:
params.add "near=" & query.near
if query.minLikes.len > 0:
params.add "min_faves=" & query.minLikes
if params.len > 0:
result &= params.join("&")

View File

@@ -31,8 +31,6 @@ proc createStatusRouter*(cfg: Config) =
resp $renderReplies(replies, prefs, getPath())
let conv = await getTweet(id, getCursor())
if conv == nil:
warn "nil conv"
if conv == nil or conv.tweet == nil or conv.tweet.id == 0:
var error = "Tweet not found"
@@ -68,7 +66,7 @@ proc createStatusRouter*(cfg: Config) =
get "/@name/@s/@id/@m/?@i?":
cond @"s" in ["status", "statuses"]
cond @"m" in ["video", "photo"]
cond @"m" in ["video", "photo", "history"]
redirect("/$1/status/$2" % [@"name", @"id"])
get "/@name/statuses/@id/?":
@@ -76,6 +74,6 @@ proc createStatusRouter*(cfg: Config) =
get "/i/web/status/@id":
redirect("/i/status/" & @"id")
get "/@name/thread/@id/?":
redirect("/$1/status/$2" % [@"name", @"id"])

View File

@@ -105,6 +105,12 @@ proc createTimelineRouter*(cfg: Config) =
get "/intent/user":
respUserId()
get "/intent/follow/?":
let username = request.params.getOrDefault("screen_name")
if username.len == 0:
resp Http400, showError("Missing screen_name parameter", cfg)
redirect("/" & username)
get "/@name/?@tab?/?":
cond '.' notin @"name"
cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"]

View File

@@ -17,7 +17,7 @@ proc createUnsupportedRouter*(cfg: Config) =
get "/@name/lists/?": feature()
get "/intent/?@i?":
cond @"i" notin ["user"]
cond @"i" notin ["user", "follow"]
feature()
get "/i/@i?/?@j?":

View File

@@ -52,7 +52,7 @@ body {
background-color: var(--bg_color);
color: var(--fg_color);
font-family: $font_stack;
font-size: 14px;
font-size: 15px;
line-height: 1.3;
margin: 0;
}

View File

@@ -14,6 +14,7 @@ button {
input[type="text"],
input[type="date"],
input[type="number"],
select {
@include input-colors;
background-color: var(--bg_elements);
@@ -24,7 +25,12 @@ select {
font-size: 14px;
}
input[type="text"] {
input[type="number"] {
-moz-appearance: textfield;
}
input[type="text"],
input[type="number"] {
height: 16px;
}
@@ -38,6 +44,17 @@ input[type="date"]::-webkit-inner-spin-button {
display: none;
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
display: none;
-webkit-appearance: none;
margin: 0;
}
input[type="date"]::-webkit-clear-button {
margin-left: 17px;
filter: grayscale(100%);
@@ -176,7 +193,8 @@ input::-webkit-datetime-edit-year-field:focus {
color: var(--fg_color);
}
input[type="text"] {
input[type="text"],
input[type="number"] {
position: absolute;
right: 0;
max-width: 140px;

View File

@@ -27,7 +27,8 @@
min-width: 0;
}
input[type="text"] {
input[type="text"],
input[type="number"] {
height: calc(100% - 4px);
width: calc(100% - 8px);
}
@@ -155,15 +156,14 @@
.profile-tabs {
@include search-resize(820px, 5);
@include search-resize(725px, 4);
@include search-resize(600px, 6);
@include search-resize(560px, 5);
@include search-resize(480px, 4);
@include search-resize(715px, 4);
@include search-resize(700px, 5);
@include search-resize(485px, 4);
@include search-resize(410px, 3);
}
@include search-resize(560px, 5);
@include search-resize(480px, 4);
@include search-resize(700px, 5);
@include search-resize(485px, 4);
@include search-resize(410px, 3);
@media(max-width: 600px) {

View File

@@ -141,7 +141,7 @@ type
fromUser*: seq[string]
since*: string
until*: string
near*: string
minLikes*: string
sep*: string
Gif* = object

View File

@@ -33,14 +33,14 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
tdiv(class="nav-item right"):
if cfg.enableRss and rss.len > 0:
icon "rss", title="RSS Feed", href=rss
icon "bird", title="Open in Twitter", href=canonical
icon "bird", title="Open in X", href=canonical
a(href="https://buymeacoffee.com/kuu7o"): verbatim lp
icon "info", title="About", href="/about"
icon "cog", title="Preferences", href=("/settings?referer=" & encodeUrl(path))
proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
video=""; images: seq[string] = @[]; banner=""; ogTitle="";
rss=""; canonical=""): VNode =
rss=""; alternate=""): VNode =
var theme = prefs.theme.toTheme
if "theme" in req.params:
theme = req.params["theme"].toTheme
@@ -54,7 +54,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
buildHtml(head):
link(rel="stylesheet", type="text/css", href="/css/style.css?v=20")
link(rel="stylesheet", type="text/css", href="/css/style.css?v=21")
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=3")
if theme.len > 0:
@@ -68,8 +68,8 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
link(rel="search", type="application/opensearchdescription+xml", title=cfg.title,
href=opensearchUrl)
if canonical.len > 0:
link(rel="canonical", href=canonical)
if alternate.len > 0:
link(rel="alternate", href=alternate, title="View on X")
if cfg.enableRss and rss.len > 0:
link(rel="alternate", type="application/rss+xml", href=rss, title="RSS feed")
@@ -131,16 +131,16 @@ proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs;
titleText=""; desc=""; ogTitle=""; rss=""; video="";
images: seq[string] = @[]; banner=""): string =
let canonical = getTwitterLink(req.path, req.params)
let twitterLink = getTwitterLink(req.path, req.params)
let node = buildHtml(html(lang="en")):
renderHead(prefs, cfg, req, titleText, desc, video, images, banner, ogTitle,
rss, canonical)
rss, twitterLink)
body(class=if prefs.verifiedBadge == "Hide all": "hide-verified-all"
elif prefs.verifiedBadge == "Show official only": "hide-verified-blue"
else: ""):
renderNavbar(cfg, req, rss, canonical)
renderNavbar(cfg, req, rss, twitterLink)
tdiv(class="container"):
body

View File

@@ -89,6 +89,13 @@ proc genDate*(pref, state: string): VNode =
input(name=pref, `type`="date", value=state)
icon "calendar"
proc genNumberInput*(pref, label, state, placeholder: string; class=""; autofocus=true; min="0"): VNode =
let p = placeholder
buildHtml(tdiv(class=("pref-group pref-input " & class))):
if label.len > 0:
label(`for`=pref): text label
input(name=pref, `type`="number", placeholder=p, value=state, autofocus=(autofocus and state.len == 0), min=min, step="1")
proc genImg*(url: string; class=""): VNode =
buildHtml():
img(src=getPicUrl(url), class=class, alt="", loading="lazy")

View File

@@ -57,7 +57,7 @@ proc renderThread(thread: Tweets; prefs: Prefs; path: string): VNode =
proc renderUser*(user: User; prefs: Prefs; path: string): VNode =
let class = if user.sensitive: "timeline-item nsfw" else: "timeline-item"
buildHtml(tdiv(class=class)):
buildHtml(tdiv(class=class, data-username=user.username)):
a(class="tweet-link", href=("/" & user.username))
tdiv(class="tweet-body profile-result"):
tdiv(class="tweet-header"):

View File

@@ -275,7 +275,7 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
divClass.add " nsfw"
if not tweet.available:
return buildHtml(tdiv(class=divClass & "unavailable timeline-item")):
return buildHtml(tdiv(class=divClass & "unavailable timeline-item", data-username=tweet.user.username)):
tdiv(class="unavailable-box"):
if tweet.tombstone.len > 0:
text tweet.tombstone
@@ -297,7 +297,7 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
tweet = tweet.retweet.get
retweet = fullTweet.user.fullname
buildHtml(tdiv(class=("timeline-item " & divClass))):
buildHtml(tdiv(class=("timeline-item " & divClass), data-username=tweet.user.username)):
if not mainTweet:
a(class="tweet-link", href=getLink(tweet))