Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
baeaf68
Make maxConcurrentReqs configurable (#1341)
jackyzy823 Dec 8, 2025
a92e79e
Fix the checkmark position (#1347)
796176 Dec 24, 2025
a45227b
Add user-agent to guest_token request (#1359)
cmj Jan 29, 2026
33dd9b6
Fix /pic/ exploit
zedeus Feb 6, 2026
0a6e79e
Add bulk script create_sessions_browser.py
zedeus Feb 9, 2026
5d28bd1
Add preference for configuring sticky navbar
zedeus Feb 9, 2026
db36f75
Support restoring preferences via new prefs param
zedeus Feb 9, 2026
b85e8c5
Support preference overrides using URL params
zedeus Feb 9, 2026
40b1ba4
Bump css version
zedeus Feb 9, 2026
1c06a67
Support image alt text
zedeus Feb 10, 2026
dcec1eb
Fix invalid search link formatting
zedeus Feb 10, 2026
05b6dd2
Add config options to enable subset of RSS feeds
zedeus Feb 11, 2026
cbce620
Add dynamic-range-limit to prevent HDR jumpscares
zedeus Feb 12, 2026
90b664f
Make "Tweet unavailable" clickable and consistent
zedeus Feb 14, 2026
d45545c
Fix "Replying to" parsing
zedeus Feb 14, 2026
f257ce5
Bump style version
zedeus Feb 14, 2026
a15d1ce
Add full support for tweet edit history
zedeus Feb 15, 2026
2bd664a
Add community notes support
zedeus Feb 19, 2026
61b6748
Add community notes to RSS
zedeus Feb 19, 2026
95a9ee8
Update and speed up GitHub workflows (#1368)
zedeus Feb 19, 2026
d187b1c
Fix video thumbnails not loading
zedeus Feb 22, 2026
b0773dd
Fix incorrect multi-user search query
zedeus Mar 4, 2026
2898efa
Fix search repeating when the end has been reached
zedeus Mar 4, 2026
4bf3df9
Fix segfault
zedeus Mar 4, 2026
35a929c
Implement mixed-media tweet support
zedeus Mar 13, 2026
0fefcf9
Update gif class in tests
zedeus Mar 13, 2026
91ff936
Add workaround for broken "until" search filter
zedeus Mar 14, 2026
7ce29bd
Add new media grid and gallery views
zedeus Mar 15, 2026
33bf2c2
Support tweet content disclosures (AI and ads)
zedeus Mar 21, 2026
b726767
Bump css
zedeus Mar 21, 2026
b6ccea0
Add configurable retry logic
zedeus Mar 21, 2026
fea6f59
Fix mobile gallery and grid, add size preference
zedeus Mar 21, 2026
3429667
Fix null legacy tweet crash
zedeus Mar 21, 2026
741060c
Increase maxRetries in CI conf
zedeus Mar 21, 2026
e7e7050
Add support for viewing account info
zedeus Mar 29, 2026
0c75834
Increase CI test maxRetries
zedeus Mar 29, 2026
7d43178
Increase CI reruns
zedeus Mar 29, 2026
8114eef
Add support for broadcasts
zedeus Mar 30, 2026
4e38317
Fix verified type enum parsing error
zedeus Apr 16, 2026
74f5ff8
Fix thread test
zedeus Apr 16, 2026
82099de
Include session.kind in all debug output
zedeus Jun 2, 2026
5a4faa0
Fix OpenSearch response crash (#1400)
zestysoft Jun 2, 2026
d5ff410
Add same-origin referrer policy
zedeus Jun 2, 2026
d199481
Merge upstream zedeus/nitter master (d5ff410) into master
guanbinrui Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions src/apiutils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ template fetchImpl(result, fetchBody) {.dirty.} =
raise newException(BadClientError, "Bad client")

if resp.status == $Http404 and result.len == 0:
echo "[sessions] transient 404 (empty body), retrying: ", url.path
echo "[sessions] transient 404 (empty body), retrying: ", url.path, ", session: ", session.pretty
raise rateLimitError()

if resp.headers.hasKey(rlRemaining):
Expand All @@ -147,7 +147,7 @@ template fetchImpl(result, fetchBody) {.dirty.} =
if result.startsWith("{\"errors"):
let errors = result.fromJson(Errors)
if errors notin errorsToSkip:
echo "Fetch error, API: ", url.path, ", errors: ", errors
echo "Fetch error, API: ", url.path, ", errors: ", errors, ", session: ", session.pretty
if errors in {expiredToken, badToken, locked}:
invalidate(session)
raise rateLimitError()
Expand All @@ -162,7 +162,7 @@ template fetchImpl(result, fetchBody) {.dirty.} =
fetchBody

if resp.status == $Http400:
echo "ERROR 400, ", url.path, ": ", result
echo "ERROR 400, ", url.path, ": ", result, ", session: ", session.pretty
raise newException(InternalError, $url)
except InternalError as e:
raise e
Expand All @@ -177,45 +177,53 @@ template fetchImpl(result, fetchBody) {.dirty.} =
finally:
release(session)

template retry(bod) =
template retry(bod) {.dirty.} =
var session: Session
for i in 0 ..< maxRetries:
try:
session = nil
bod
break
except RateLimitError:
echo "[sessions] Rate limited, retrying ", req.cookie.endpoint,
" request (", i, "/", maxRetries, ")..."
let api = if session.isNil: req.cookie.endpoint
else: req.endpoint(session)
if session.isNil:
echo "[sessions] Rate limited, retrying ", api,
" request (", i, "/", maxRetries, ")..."
else:
echo "[sessions] Rate limited, retrying ", api,
" request (", i, "/", maxRetries, ")..., session: ", session.pretty
session = nil
if retryDelayMs > 0:
await sleepAsync(retryDelayMs)

proc fetch*(req: ApiReq): Future[JsonNode] {.async.} =
retry:
var
body: string
session = await getAndValidateSession(req)
var body: string
session = await getAndValidateSession(req)

let url = req.toUrl(session.kind)

fetchImpl body:
if body.startsWith('{') or body.startsWith('['):
result = parseJson(body)
else:
echo resp.status, ": ", body, " --- url: ", url
echo resp.status, ": ", body, " --- url: ", url, ", session: ", session.pretty
result = newJNull()

let error = result.getError
if error != null and error notin errorsToSkip:
echo "Fetch error, API: ", url.path, ", error: ", error
echo "Fetch error, API: ", url.path, ", error: ", error, ", session: ", session.pretty
if error in {expiredToken, badToken, locked}:
invalidate(session)
raise rateLimitError()

proc fetchRaw*(req: ApiReq): Future[string] {.async.} =
retry:
var session = await getAndValidateSession(req)
session = await getAndValidateSession(req)
let url = req.toUrl(session.kind)

fetchImpl result:
if not (result.startsWith('{') or result.startsWith('[')):
echo resp.status, ": ", result, " --- url: ", url
echo resp.status, ": ", result, " --- url: ", url, ", session: ", session.pretty
result.setLen(0)
19 changes: 17 additions & 2 deletions src/auth.nim
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ proc setMaxConcurrentReqs*(reqs: int) =
template log(str: varargs[string, `$`]) =
echo "[sessions] ", str.join("")

proc endpoint(req: ApiReq; session: Session): string =
proc endpoint*(req: ApiReq; session: Session): string =
case session.kind
of oauth: req.oauth.endpoint
of cookie: req.cookie.endpoint
Expand Down Expand Up @@ -50,6 +50,8 @@ proc getSessionPoolHealth*(): JsonNode =
oldest = now.int64
newest = 0'i64
average = 0'i64
oauthTotal, cookieTotal = 0
oauthLimited, cookieLimited = 0

for session in sessionPool:
let created = snowflakeToEpoch(session.id)
Expand All @@ -59,8 +61,15 @@ proc getSessionPoolHealth*(): JsonNode =
oldest = created
average += created

case session.kind
of oauth: inc oauthTotal
of cookie: inc cookieTotal

if session.limited:
limited.incl session.id
case session.kind
of oauth: inc oauthLimited
of cookie: inc cookieLimited

for api in session.apis.keys:
let
Expand All @@ -84,6 +93,8 @@ proc getSessionPoolHealth*(): JsonNode =
"sessions": %*{
"total": sessionPool.len,
"limited": limited.card,
"oauth": %*{"total": oauthTotal, "limited": oauthLimited},
"cookie": %*{"total": cookieTotal, "limited": cookieLimited},
"oldest": $fromUnix(oldest),
"newest": $fromUnix(newest),
"average": $fromUnix(average)
Expand All @@ -100,6 +111,7 @@ proc getSessionPoolDebug*(): JsonNode =

for session in sessionPool:
let sessionJson = %*{
"kind": $session.kind,
"apis": newJObject(),
"pending": session.pending,
}
Expand Down Expand Up @@ -173,7 +185,10 @@ proc getSession*(req: ApiReq): Future[Session] {.async.} =
if not result.isNil and result.isReady(req):
inc result.pending
else:
log "no sessions available for API: ", req.cookie.endpoint
if result.isNil:
log "no sessions available for API: ", req.cookie.endpoint
else:
log "no sessions available for API: ", req.endpoint(result), ", last tried: ", result.pretty
raise noSessionsError()

proc setLimited*(session: Session; req: ApiReq) =
Expand Down
7 changes: 4 additions & 3 deletions src/routes/search.nim
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ proc createSearchRouter*(cfg: Config) =
redirect("/search?f=tweets&q=" & encodeUrl("#" & @"hash"))

get "/opensearch":
let url = getUrlPrefix(cfg) & "/search?f=tweets&q="
resp Http200, {"Content-Type": "application/opensearchdescription+xml"},
generateOpenSearchXML(cfg.title, cfg.hostname, url)
let
url = getUrlPrefix(cfg) & "/search?f=tweets&q="
headers = {"Content-Type": "application/opensearchdescription+xml"}
resp Http200, headers, generateOpenSearchXML(cfg.title, cfg.hostname, url)
1 change: 1 addition & 0 deletions src/views/general.nim
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
text cfg.title

meta(name="viewport", content="width=device-width, initial-scale=1.0")
meta(name="referrer", content="same-origin")
meta(name="theme-color", content="#1F1F1F")
meta(property="og:type", content=ogType)
meta(property="og:title", content=(if ogTitle.len > 0: ogTitle else: titleText))
Expand Down