mirror of
https://github.com/zedeus/nitter.git
synced 2025-12-05 19:45:36 -05:00
Compare commits
5 Commits
71e65c84d7
...
693a189462
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
693a189462 | ||
|
|
7734d976f7 | ||
|
|
a62ec9cbb4 | ||
|
|
4b9aec6fde | ||
|
|
064ec88080 |
@@ -42,7 +42,8 @@ proc userTweetsAndRepliesUrl(id: string; cursor: string): ApiReq =
|
||||
proc tweetDetailUrl(id: string; cursor: string): ApiReq =
|
||||
let cookieVars = tweetDetailVars % [id, cursor]
|
||||
result = ApiReq(
|
||||
cookie: apiUrl(graphTweetDetail, cookieVars, tweetDetailFieldToggles),
|
||||
# cookie: apiUrl(graphTweetDetail, cookieVars, tweetDetailFieldToggles),
|
||||
cookie: apiUrl(graphTweet, tweetVars % [id, cursor]),
|
||||
oauth: apiUrl(graphTweet, tweetVars % [id, cursor])
|
||||
)
|
||||
|
||||
|
||||
@@ -48,21 +48,19 @@ proc getCookieHeader(authToken, ct0: string): string =
|
||||
|
||||
proc genHeaders*(session: Session, url: Uri): Future[HttpHeaders] {.async.} =
|
||||
result = newHttpHeaders({
|
||||
"accept": "*/*",
|
||||
"accept-encoding": "gzip",
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
"connection": "keep-alive",
|
||||
"content-type": "application/json",
|
||||
"x-twitter-active-user": "yes",
|
||||
"x-twitter-client-language": "en",
|
||||
"origin": "https://x.com",
|
||||
"accept-encoding": "gzip",
|
||||
"accept-language": "en-US,en;q=0.5",
|
||||
"accept": "*/*",
|
||||
"DNT": "1",
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
||||
"x-twitter-active-user": "yes",
|
||||
"x-twitter-client-language": "en"
|
||||
})
|
||||
|
||||
case session.kind
|
||||
of SessionKind.oauth:
|
||||
result["authority"] = "api.x.com"
|
||||
result["authorization"] = getOauthHeader($url, session.oauthToken, session.oauthSecret)
|
||||
of SessionKind.cookie:
|
||||
result["x-twitter-auth-type"] = "OAuth2Session"
|
||||
|
||||
@@ -52,10 +52,10 @@ proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
|
||||
""
|
||||
|
||||
let headers = newHttpHeaders({
|
||||
"Content-Type": res.headers["content-type", 0],
|
||||
"Content-Length": contentLength,
|
||||
"Cache-Control": maxAge,
|
||||
"ETag": hashed
|
||||
"content-type": res.headers["content-type", 0],
|
||||
"content-length": contentLength,
|
||||
"cache-control": maxAge,
|
||||
"etag": hashed
|
||||
})
|
||||
|
||||
respond(request, headers)
|
||||
|
||||
@@ -114,6 +114,7 @@ proc createTimelineRouter*(cfg: Config) =
|
||||
get "/@name/?@tab?/?":
|
||||
cond '.' notin @"name"
|
||||
cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"]
|
||||
cond @"name".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9', '_'})
|
||||
cond @"tab" in ["with_replies", "media", "search", ""]
|
||||
let
|
||||
prefs = cookiePrefs()
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
## SPDX-License-Identifier: AGPL-3.0-only
|
||||
#import strutils, xmltree, strformat, options, unicode
|
||||
#import ../types, ../utils, ../formatters, ../prefs
|
||||
## Snowflake ID cutoff for RSS GUID format transition
|
||||
## Corresponds to approximately December 14, 2025 UTC
|
||||
#const guidCutoff = 2000000000000000000'i64
|
||||
#
|
||||
#proc getTitle(tweet: Tweet; retweet: string): string =
|
||||
#if tweet.pinned: result = "Pinned: "
|
||||
@@ -101,12 +104,17 @@ ${renderRssTweet(quoteTweet, cfg)}
|
||||
# if link in links: continue
|
||||
# end if
|
||||
# links.add link
|
||||
# let useGlobalGuid = tweet.id >= guidCutoff
|
||||
<item>
|
||||
<title>${getTitle(tweet, retweet)}</title>
|
||||
<dc:creator>@${tweet.user.username}</dc:creator>
|
||||
<description><![CDATA[${renderRssTweet(tweet, cfg).strip(chars={'\n'})}]]></description>
|
||||
<pubDate>${getRfc822Time(tweet)}</pubDate>
|
||||
#if useGlobalGuid:
|
||||
<guid isPermaLink="false">${tweet.id}</guid>
|
||||
#else:
|
||||
<guid>${urlPrefix & link}</guid>
|
||||
#end if
|
||||
<link>${urlPrefix & link}</link>
|
||||
</item>
|
||||
# end for
|
||||
|
||||
@@ -28,14 +28,19 @@ proc renderReplyThread(thread: Chain; prefs: Prefs; path: string): VNode =
|
||||
if thread.hasMore:
|
||||
renderMoreReplies(thread)
|
||||
|
||||
proc renderReplies*(replies: Result[Chain]; prefs: Prefs; path: string): VNode =
|
||||
proc renderReplies*(replies: Result[Chain]; prefs: Prefs; path: string; tweet: Tweet = nil): VNode =
|
||||
buildHtml(tdiv(class="replies", id="r")):
|
||||
var hasReplies = false
|
||||
var replyCount = 0
|
||||
for thread in replies.content:
|
||||
if thread.content.len == 0: continue
|
||||
hasReplies = true
|
||||
replyCount += thread.content.len
|
||||
renderReplyThread(thread, prefs, path)
|
||||
|
||||
if replies.bottom.len > 0:
|
||||
renderMore(Query(), replies.bottom, focus="#r")
|
||||
if hasReplies and replies.bottom.len > 0:
|
||||
if tweet == nil or not replies.beginning or replyCount < tweet.stats.replies:
|
||||
renderMore(Query(), replies.bottom, focus="#r")
|
||||
|
||||
proc renderConversation*(conv: Conversation; prefs: Prefs; path: string): VNode =
|
||||
let hasAfter = conv.after.content.len > 0
|
||||
@@ -70,6 +75,6 @@ proc renderConversation*(conv: Conversation; prefs: Prefs; path: string): VNode
|
||||
if not conv.replies.beginning:
|
||||
renderNewer(Query(), getLink(conv.tweet), focus="#r")
|
||||
if conv.replies.content.len > 0 or conv.replies.bottom.len > 0:
|
||||
renderReplies(conv.replies, prefs, path)
|
||||
renderReplies(conv.replies, prefs, path, conv.tweet)
|
||||
|
||||
renderToTop(focus="#m")
|
||||
|
||||
@@ -15,7 +15,19 @@ protected = [
|
||||
['Poop', 'Randy', 'Social media fanatic.']
|
||||
]
|
||||
|
||||
invalid = [['thisprofiledoesntexist'], ['%']]
|
||||
invalid = [['thisprofiledoesntexist']]
|
||||
|
||||
malformed = [
|
||||
['${userId}'],
|
||||
['$%7BuserId%7D'], # URL encoded version
|
||||
['%'], # Percent sign is invalid
|
||||
['user@name'],
|
||||
['user.name'],
|
||||
['user-name'],
|
||||
['user$name'],
|
||||
['user{name}'],
|
||||
['user name'], # space
|
||||
]
|
||||
|
||||
banner_image = [
|
||||
['mobile_test', 'profile_banners%2F82135242%2F1384108037%2F1500x500']
|
||||
@@ -65,6 +77,13 @@ class ProfileTest(BaseTestCase):
|
||||
self.open_nitter(username)
|
||||
self.assert_text(f'User "{username}" not found')
|
||||
|
||||
@parameterized.expand(malformed)
|
||||
def test_malformed_username(self, username):
|
||||
"""Test that malformed usernames (with invalid characters) return 404"""
|
||||
self.open_nitter(username)
|
||||
# Malformed usernames should return 404 page not found, not try to fetch from Twitter
|
||||
self.assert_text('Page not found')
|
||||
|
||||
def test_suspended(self):
|
||||
self.open_nitter('suspendme')
|
||||
self.assert_text('User "suspendme" has been suspended')
|
||||
|
||||
Reference in New Issue
Block a user