mirror of
https://github.com/zedeus/nitter
synced 2026-09-05 14:49:32 +00:00
parent
a68b57629a
commit
8142bab1a9
13 changed files with 103 additions and 39 deletions
|
|
@ -26,6 +26,7 @@ enableRSS = true # master switch, set to false to disable all RSS fe
|
||||||
enableRSSUserTweets = true # /@user/rss
|
enableRSSUserTweets = true # /@user/rss
|
||||||
enableRSSUserReplies = true # /@user/with_replies/rss
|
enableRSSUserReplies = true # /@user/with_replies/rss
|
||||||
enableRSSUserMedia = true # /@user/media/rss
|
enableRSSUserMedia = true # /@user/media/rss
|
||||||
|
enableRSSUserArticles = true # /@user/articles/rss
|
||||||
enableRSSSearch = true # /search/rss and /@user/search/rss
|
enableRSSSearch = true # /search/rss and /@user/search/rss
|
||||||
enableRSSList = true # list RSS feeds
|
enableRSSList = true # list RSS feeds
|
||||||
enableDebug = false # enable request logs and debug endpoints (/.sessions)
|
enableDebug = false # enable request logs and debug endpoints (/.sessions)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,12 @@ proc userTweetsAndRepliesUrl(id: string; cursor: string): ApiReq =
|
||||||
oauth: apiUrl(graphUserTweetsAndRepliesV2, restIdVars % [id, cursor, "20"], userTweetsFieldToggles, skipTid=true)
|
oauth: apiUrl(graphUserTweetsAndRepliesV2, restIdVars % [id, cursor, "20"], userTweetsFieldToggles, skipTid=true)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
proc userArticlesUrl(id: string; cursor: string): ApiReq =
|
||||||
|
result = ApiReq(
|
||||||
|
cookie: apiUrl(graphUserArticles, userArticlesVars % [id, cursor], userTweetsFieldToggles),
|
||||||
|
oauth: apiUrl(graphUserArticlesV2, restIdVars % [id, cursor, "20"], userTweetsFieldToggles)
|
||||||
|
)
|
||||||
|
|
||||||
proc tweetDetailUrl(id, cursor: string; mode = Relevance): ApiReq =
|
proc tweetDetailUrl(id, cursor: string; mode = Relevance): ApiReq =
|
||||||
return apiReq(graphTweet, tweetVars % [id, cursor, $mode])
|
return apiReq(graphTweet, tweetVars % [id, cursor, $mode])
|
||||||
# let cookieVars = tweetDetailVars % [id, cursor]
|
# let cookieVars = tweetDetailVars % [id, cursor]
|
||||||
|
|
@ -112,6 +118,7 @@ proc getGraphUserTweets*(id: string; kind: TimelineKind; after=""): Future[Profi
|
||||||
of TimelineKind.tweets: userTweetsUrl(id, cursor)
|
of TimelineKind.tweets: userTweetsUrl(id, cursor)
|
||||||
of TimelineKind.replies: userTweetsAndRepliesUrl(id, cursor)
|
of TimelineKind.replies: userTweetsAndRepliesUrl(id, cursor)
|
||||||
of TimelineKind.media: mediaUrl(id, cursor)
|
of TimelineKind.media: mediaUrl(id, cursor)
|
||||||
|
of TimelineKind.articles: userArticlesUrl(id, cursor)
|
||||||
js = await fetch(url)
|
js = await fetch(url)
|
||||||
result = parseGraphTimeline(js, after)
|
result = parseGraphTimeline(js, after)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ proc getConfig*(path: string): (Config, parseCfg.Config) =
|
||||||
enableRSSUserTweets: masterRss and cfg.get("Config", "enableRSSUserTweets", true),
|
enableRSSUserTweets: masterRss and cfg.get("Config", "enableRSSUserTweets", true),
|
||||||
enableRSSUserReplies: masterRss and cfg.get("Config", "enableRSSUserReplies", true),
|
enableRSSUserReplies: masterRss and cfg.get("Config", "enableRSSUserReplies", true),
|
||||||
enableRSSUserMedia: masterRss and cfg.get("Config", "enableRSSUserMedia", true),
|
enableRSSUserMedia: masterRss and cfg.get("Config", "enableRSSUserMedia", true),
|
||||||
|
enableRSSUserArticles: masterRss and cfg.get("Config", "enableRSSUserArticles", true),
|
||||||
enableRSSSearch: masterRss and cfg.get("Config", "enableRSSSearch", true),
|
enableRSSSearch: masterRss and cfg.get("Config", "enableRSSSearch", true),
|
||||||
enableRSSList: masterRss and cfg.get("Config", "enableRSSList", true),
|
enableRSSList: masterRss and cfg.get("Config", "enableRSSList", true),
|
||||||
enableDebug: cfg.get("Config", "enableDebug", false),
|
enableDebug: cfg.get("Config", "enableDebug", false),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ const
|
||||||
graphUserTweetsAndReplies* = "qUpkZU6eN8MbtQb7rC_pYg/UserTweetsAndReplies"
|
graphUserTweetsAndReplies* = "qUpkZU6eN8MbtQb7rC_pYg/UserTweetsAndReplies"
|
||||||
graphUserMedia* = "VyudDWQnr9vJNw7GasFz2g/UserMedia"
|
graphUserMedia* = "VyudDWQnr9vJNw7GasFz2g/UserMedia"
|
||||||
graphUserMediaV2* = "WK111rbR0vM0ZX4lyZCYjw/MediaTimelineV2"
|
graphUserMediaV2* = "WK111rbR0vM0ZX4lyZCYjw/MediaTimelineV2"
|
||||||
|
graphUserArticles* = "ZmMjUyrTpwYfTGAdylEyMw/UserArticlesTweets"
|
||||||
|
graphUserArticlesV2* = "PsGixN38UZz2RheyayNB5Q/UserProfileArticlesTimelineQuery"
|
||||||
graphTweet* = "OZMbEnEa96AN8Pq6HyTWdw/ConversationTimeline"
|
graphTweet* = "OZMbEnEa96AN8Pq6HyTWdw/ConversationTimeline"
|
||||||
graphTweetDetail* = "XMOz5h24KAZ86qKffKTLdQ/TweetDetail"
|
graphTweetDetail* = "XMOz5h24KAZ86qKffKTLdQ/TweetDetail"
|
||||||
graphTweetResult* = "xYOrBQoTlfKJJPsX76MZEw/TweetResultByIdQuery"
|
graphTweetResult* = "xYOrBQoTlfKJJPsX76MZEw/TweetResultByIdQuery"
|
||||||
|
|
@ -148,6 +150,13 @@ const
|
||||||
"withVoice": true
|
"withVoice": true
|
||||||
}""".replace(" ", "").replace("\n", "")
|
}""".replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
|
userArticlesVars* = """{
|
||||||
|
"userId": "$1", $2
|
||||||
|
"count": 20,
|
||||||
|
"includePromotedContent": false,
|
||||||
|
"withVoice": true
|
||||||
|
}""".replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
articleVars* = """{
|
articleVars* = """{
|
||||||
"tweetId": "$1",
|
"tweetId": "$1",
|
||||||
"includePromotedContent": false,
|
"includePromotedContent": false,
|
||||||
|
|
|
||||||
|
|
@ -479,7 +479,7 @@ proc parseCard(js: JsonNode; urls: JsonNode): Card =
|
||||||
result.url = getPicUrl(result.image)
|
result.url = getPicUrl(result.image)
|
||||||
|
|
||||||
proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull();
|
proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull();
|
||||||
replyId: int64 = 0): Tweet =
|
replyId: int64 = 0; hasArticle = false): Tweet =
|
||||||
if js.isNull: return Tweet()
|
if js.isNull: return Tweet()
|
||||||
|
|
||||||
let time =
|
let time =
|
||||||
|
|
@ -547,7 +547,7 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull();
|
||||||
elif name.len > 0 and jsCard{"binding_values"}.notNull:
|
elif name.len > 0 and jsCard{"binding_values"}.notNull:
|
||||||
result.card = some parseCard(jsCard, js{"entities", "urls"})
|
result.card = some parseCard(jsCard, js{"entities", "urls"})
|
||||||
|
|
||||||
result.expandTweetEntities(js)
|
result.expandTweetEntities(js, hasArticle)
|
||||||
parseLegacyMediaEntities(js, result)
|
parseLegacyMediaEntities(js, result)
|
||||||
|
|
||||||
with jsWithheld, js{"withheld_in_countries"}:
|
with jsWithheld, js{"withheld_in_countries"}:
|
||||||
|
|
@ -605,6 +605,8 @@ proc parseGraphTweet*(js: JsonNode): Tweet =
|
||||||
with restId, js{"reply_to_results", "rest_id"}:
|
with restId, js{"reply_to_results", "rest_id"}:
|
||||||
replyId = restId.getId
|
replyId = restId.getId
|
||||||
|
|
||||||
|
let hasArticle = js{"article", "article_results", "result", "title"}.getStr.len > 0
|
||||||
|
|
||||||
if "details" in js:
|
if "details" in js:
|
||||||
result = Tweet(
|
result = Tweet(
|
||||||
id: js{"rest_id"}.getId,
|
id: js{"rest_id"}.getId,
|
||||||
|
|
@ -639,7 +641,6 @@ proc parseGraphTweet*(js: JsonNode): Tweet =
|
||||||
if result.attribution.isNone:
|
if result.attribution.isNone:
|
||||||
parseLegacyMediaEntities(js{"legacy"}, result)
|
parseLegacyMediaEntities(js{"legacy"}, result)
|
||||||
|
|
||||||
let hasArticle = js{"article", "article_results", "result", "title"}.getStr.len > 0
|
|
||||||
result.expandTweetEntitiesV2(js, hasArticle)
|
result.expandTweetEntitiesV2(js, hasArticle)
|
||||||
|
|
||||||
# Strip video source URL from text (for videos from other tweets)
|
# Strip video source URL from text (for videos from other tweets)
|
||||||
|
|
@ -653,7 +654,7 @@ proc parseGraphTweet*(js: JsonNode): Tweet =
|
||||||
result.text = result.text[0 ..< idx].strip()
|
result.text = result.text[0 ..< idx].strip()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
result = parseTweet(js{"legacy"}, jsCard, replyId)
|
result = parseTweet(js{"legacy"}, jsCard, replyId, hasArticle)
|
||||||
result.id = js{"rest_id"}.getId
|
result.id = js{"rest_id"}.getId
|
||||||
|
|
||||||
with artNode, js{"article", "article_results", "result"}:
|
with artNode, js{"article", "article_results", "result"}:
|
||||||
|
|
@ -662,7 +663,10 @@ proc parseGraphTweet*(js: JsonNode): Tweet =
|
||||||
result.articlePreview = some ArticlePreview(
|
result.articlePreview = some ArticlePreview(
|
||||||
title: artTitle,
|
title: artTitle,
|
||||||
previewText: artNode{"preview_text"}.getStr,
|
previewText: artNode{"preview_text"}.getStr,
|
||||||
coverImage: artNode{"cover_media_results", "result", "media_info", "original_img_url"}.getImageStr,
|
coverImage: select(
|
||||||
|
artNode{"cover_media_results", "result", "media_info", "original_img_url"},
|
||||||
|
artNode{"cover_media", "media_info", "original_img_url"}
|
||||||
|
).getImageStr,
|
||||||
tweetId: result.id
|
tweetId: result.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -281,7 +281,7 @@ proc expandUserEntities*(user: var User; js: JsonNode) =
|
||||||
.replacef(htRegex, htReplace)
|
.replacef(htRegex, htReplace)
|
||||||
|
|
||||||
proc expandTextEntities(tweet: Tweet; entities: JsonNode; text: string; textSlice: Slice[int];
|
proc expandTextEntities(tweet: Tweet; entities: JsonNode; text: string; textSlice: Slice[int];
|
||||||
replyTo=""; hasRedundantLink=false) =
|
replyTo=""; hasRedundantLink=false; hasArticle=false) =
|
||||||
let hasCard = tweet.card.isSome
|
let hasCard = tweet.card.isSome
|
||||||
|
|
||||||
var replacements = newSeq[ReplaceSlice]()
|
var replacements = newSeq[ReplaceSlice]()
|
||||||
|
|
@ -292,7 +292,8 @@ proc expandTextEntities(tweet: Tweet; entities: JsonNode; text: string; textSlic
|
||||||
if urlStr.len == 0 or urlStr notin text:
|
if urlStr.len == 0 or urlStr notin text:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
replacements.extractUrls(u, textSlice.b, hideTwitter = hasRedundantLink)
|
replacements.extractUrls(u, textSlice.b, hideTwitter = hasRedundantLink,
|
||||||
|
hideArticle = hasArticle)
|
||||||
|
|
||||||
if hasCard and u{"url"}.getStr == get(tweet.card).url:
|
if hasCard and u{"url"}.getStr == get(tweet.card).url:
|
||||||
get(tweet.card).url = u.getExpandedUrl
|
get(tweet.card).url = u.getExpandedUrl
|
||||||
|
|
@ -329,7 +330,7 @@ proc expandTextEntities(tweet: Tweet; entities: JsonNode; text: string; textSlic
|
||||||
|
|
||||||
tweet.text = text.toRunes.replacedWith(replacements, textSlice).strip(leading=false)
|
tweet.text = text.toRunes.replacedWith(replacements, textSlice).strip(leading=false)
|
||||||
|
|
||||||
proc expandTweetEntities*(tweet: Tweet; js: JsonNode) =
|
proc expandTweetEntities*(tweet: Tweet; js: JsonNode; hasArticle=false) =
|
||||||
let
|
let
|
||||||
entities = ? js{"entities"}
|
entities = ? js{"entities"}
|
||||||
textRange = js{"display_text_range"}
|
textRange = js{"display_text_range"}
|
||||||
|
|
@ -344,7 +345,7 @@ proc expandTweetEntities*(tweet: Tweet; js: JsonNode) =
|
||||||
tweet.reply.add replyTo
|
tweet.reply.add replyTo
|
||||||
|
|
||||||
tweet.expandTextEntities(entities, tweet.text, textSlice, replyTo,
|
tweet.expandTextEntities(entities, tweet.text, textSlice, replyTo,
|
||||||
hasQuote or hasJobCard)
|
hasQuote or hasJobCard, hasArticle)
|
||||||
|
|
||||||
proc expandTextEntitiesV2(tweet: Tweet; js: JsonNode; text: string; textSlice: Slice[int];
|
proc expandTextEntitiesV2(tweet: Tweet; js: JsonNode; text: string; textSlice: Slice[int];
|
||||||
hasRedundantLink=false; hasArticle=false) =
|
hasRedundantLink=false; hasArticle=false) =
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ proc initQuery*(pms: Table[string, string]; name=""): Query =
|
||||||
minLikes: validateNumber(@"min_faves")
|
minLikes: validateNumber(@"min_faves")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# articles is an internal tab kind, not a valid search filter
|
||||||
|
if result.kind == QueryKind.articles:
|
||||||
|
result.kind = tweets
|
||||||
|
|
||||||
if name.len > 0:
|
if name.len > 0:
|
||||||
result.fromUser = name.split(",")
|
result.fromUser = name.split(",")
|
||||||
|
|
||||||
|
|
@ -40,6 +44,12 @@ proc getMediaQuery*(name: string): Query =
|
||||||
sep: "OR"
|
sep: "OR"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
proc getArticlesQuery*(name: string): Query =
|
||||||
|
Query(
|
||||||
|
kind: QueryKind.articles,
|
||||||
|
fromUser: @[name]
|
||||||
|
)
|
||||||
|
|
||||||
proc getReplyQuery*(name: string): Query =
|
proc getReplyQuery*(name: string): Query =
|
||||||
Query(
|
Query(
|
||||||
kind: replies,
|
kind: replies,
|
||||||
|
|
|
||||||
|
|
@ -106,24 +106,16 @@ proc createRssRouter*(cfg: Config) =
|
||||||
|
|
||||||
get "/@name/@tab/rss":
|
get "/@name/@tab/rss":
|
||||||
cond '.' notin @"name"
|
cond '.' notin @"name"
|
||||||
cond @"tab" in ["with_replies", "media", "search"]
|
cond @"tab" in ["with_replies", "media", "search", "articles"]
|
||||||
let rssEnabled = case @"tab"
|
# articles can't be approximated by search, so multi-user is unsupported
|
||||||
of "with_replies": cfg.enableRSSUserReplies
|
cond not (@"tab" == "articles" and ',' in @"name")
|
||||||
of "media": cfg.enableRSSUserMedia
|
if not cfg.tabRssEnabled(@"tab"):
|
||||||
of "search": cfg.enableRSSSearch
|
|
||||||
else: false
|
|
||||||
if not rssEnabled:
|
|
||||||
resp Http403, showError("RSS feed is disabled", cfg)
|
resp Http403, showError("RSS feed is disabled", cfg)
|
||||||
let
|
let
|
||||||
prefs = requestPrefs()
|
prefs = requestPrefs()
|
||||||
name = @"name"
|
name = @"name"
|
||||||
tab = @"tab"
|
tab = @"tab"
|
||||||
query =
|
query = request.getQuery(tab, name, prefs)
|
||||||
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": ""
|
let searchKey = if tab != "search": ""
|
||||||
else: ":" & $hash(genQueryUrl(query))
|
else: ":" & $hash(genQueryUrl(query))
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,22 @@ export router_utils
|
||||||
export redis_cache, formatters, query, api
|
export redis_cache, formatters, query, api
|
||||||
export profile, timeline, status, about_account
|
export profile, timeline, status, about_account
|
||||||
|
|
||||||
|
proc tabRssEnabled*(cfg: Config; tab: string): bool =
|
||||||
|
case tab
|
||||||
|
of "": cfg.enableRSSUserTweets
|
||||||
|
of "with_replies": cfg.enableRSSUserReplies
|
||||||
|
of "media": cfg.enableRSSUserMedia
|
||||||
|
of "articles": cfg.enableRSSUserArticles
|
||||||
|
of "search": cfg.enableRSSSearch
|
||||||
|
else: false
|
||||||
|
|
||||||
proc getQuery*(request: Request; tab, name: string; prefs: Prefs): Query =
|
proc getQuery*(request: Request; tab, name: string; prefs: Prefs): Query =
|
||||||
let view = request.params.getOrDefault("view")
|
let view = request.params.getOrDefault("view")
|
||||||
case tab
|
case tab
|
||||||
of "with_replies":
|
of "with_replies":
|
||||||
result = getReplyQuery(name)
|
result = getReplyQuery(name)
|
||||||
|
of "articles":
|
||||||
|
result = getArticlesQuery(name)
|
||||||
of "media":
|
of "media":
|
||||||
result = getMediaQuery(name)
|
result = getMediaQuery(name)
|
||||||
result.view =
|
result.view =
|
||||||
|
|
@ -64,6 +75,7 @@ proc fetchProfile*(after: string; query: Query; skipRail=false): Future[Profile]
|
||||||
of posts: await getGraphUserTweets(userId, TimelineKind.tweets, after)
|
of posts: await getGraphUserTweets(userId, TimelineKind.tweets, after)
|
||||||
of replies: await getGraphUserTweets(userId, TimelineKind.replies, after)
|
of replies: await getGraphUserTweets(userId, TimelineKind.replies, after)
|
||||||
of media: await getGraphUserTweets(userId, TimelineKind.media, after)
|
of media: await getGraphUserTweets(userId, TimelineKind.media, after)
|
||||||
|
of QueryKind.articles: await getGraphUserTweets(userId, TimelineKind.articles, after)
|
||||||
else: Profile(tweets: await getGraphTweetSearch(query, after))
|
else: Profile(tweets: await getGraphTweetSearch(query, after))
|
||||||
|
|
||||||
result.user = await user
|
result.user = await user
|
||||||
|
|
@ -172,7 +184,9 @@ proc createTimelineRouter*(cfg: Config) =
|
||||||
cond '.' notin @"name"
|
cond '.' notin @"name"
|
||||||
cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"]
|
cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"]
|
||||||
cond @"name".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9', '_', ','})
|
cond @"name".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9', '_', ','})
|
||||||
cond @"tab" in ["with_replies", "media", "search", ""]
|
cond @"tab" in ["with_replies", "media", "search", "articles", ""]
|
||||||
|
# articles can't be approximated by search, so multi-user is unsupported
|
||||||
|
cond not (@"tab" == "articles" and ',' in @"name")
|
||||||
let
|
let
|
||||||
prefs = requestPrefs()
|
prefs = requestPrefs()
|
||||||
after = getCursor()
|
after = getCursor()
|
||||||
|
|
@ -196,15 +210,8 @@ proc createTimelineRouter*(cfg: Config) =
|
||||||
profile.tweets.beginning = true
|
profile.tweets.beginning = true
|
||||||
resp $renderTimelineTweets(profile.tweets, prefs, getPath())
|
resp $renderTimelineTweets(profile.tweets, prefs, getPath())
|
||||||
|
|
||||||
let rssEnabled =
|
|
||||||
if @"tab".len == 0: cfg.enableRSSUserTweets
|
|
||||||
elif @"tab" == "with_replies": cfg.enableRSSUserReplies
|
|
||||||
elif @"tab" == "media": cfg.enableRSSUserMedia
|
|
||||||
elif @"tab" == "search": cfg.enableRSSSearch
|
|
||||||
else: false
|
|
||||||
|
|
||||||
let rss =
|
let rss =
|
||||||
if not rssEnabled:
|
if not cfg.tabRssEnabled(@"tab"):
|
||||||
""
|
""
|
||||||
elif @"tab".len == 0:
|
elif @"tab".len == 0:
|
||||||
"/$1/rss" % @"name"
|
"/$1/rss" % @"name"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ type
|
||||||
BadClientError* = object of CatchableError
|
BadClientError* = object of CatchableError
|
||||||
|
|
||||||
TimelineKind* {.pure.} = enum
|
TimelineKind* {.pure.} = enum
|
||||||
tweets, replies, media
|
tweets, replies, media, articles
|
||||||
|
|
||||||
ApiUrl* = object
|
ApiUrl* = object
|
||||||
endpoint*: string
|
endpoint*: string
|
||||||
|
|
@ -174,7 +174,8 @@ type
|
||||||
variants*: seq[VideoVariant]
|
variants*: seq[VideoVariant]
|
||||||
|
|
||||||
QueryKind* = enum
|
QueryKind* = enum
|
||||||
posts, replies, media, users, tweets, userList, followers, following, lists, top
|
posts, replies, media, users, tweets, userList, followers, following, lists, top,
|
||||||
|
articles
|
||||||
|
|
||||||
RankingMode* = enum
|
RankingMode* = enum
|
||||||
Relevance, Recency, Likes
|
Relevance, Recency, Likes
|
||||||
|
|
@ -431,6 +432,7 @@ type
|
||||||
enableRSSUserTweets*: bool
|
enableRSSUserTweets*: bool
|
||||||
enableRSSUserReplies*: bool
|
enableRSSUserReplies*: bool
|
||||||
enableRSSUserMedia*: bool
|
enableRSSUserMedia*: bool
|
||||||
|
enableRSSUserArticles*: bool
|
||||||
enableRSSSearch*: bool
|
enableRSSSearch*: bool
|
||||||
enableRSSList*: bool
|
enableRSSList*: bool
|
||||||
enableDebug*: bool
|
enableDebug*: bool
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@
|
||||||
## text = unicode.runeSubStr(text, 0, 32) & "..."
|
## text = unicode.runeSubStr(text, 0, 32) & "..."
|
||||||
##end if
|
##end if
|
||||||
#text = xmltree.escape(text)
|
#text = xmltree.escape(text)
|
||||||
|
## article tweets' text is just the article link; the title says more
|
||||||
|
#if tweet.articlePreview.isSome and tweet.articlePreview.get().title.len > 0:
|
||||||
|
# result = prefix & xmltree.escape(tweet.articlePreview.get().title)
|
||||||
|
# return
|
||||||
|
#end if
|
||||||
#if text.len > 0:
|
#if text.len > 0:
|
||||||
# result = prefix & text
|
# result = prefix & text
|
||||||
# return
|
# return
|
||||||
|
|
@ -34,12 +39,6 @@
|
||||||
# end case
|
# end case
|
||||||
# end if
|
# end if
|
||||||
#end if
|
#end if
|
||||||
#if result.len == 0 and tweet.articlePreview.isSome:
|
|
||||||
# let art = tweet.articlePreview.get()
|
|
||||||
# if art.title.len > 0:
|
|
||||||
# result = prefix & xmltree.escape(art.title)
|
|
||||||
# end if
|
|
||||||
#end if
|
|
||||||
#if result.len == 0 and tweet.card.isSome:
|
#if result.len == 0 and tweet.card.isSome:
|
||||||
# let card = tweet.card.get()
|
# let card = tweet.card.get()
|
||||||
# if card.kind notin {hidden, unknown} and card.title.len > 0:
|
# if card.kind notin {hidden, unknown} and card.title.len > 0:
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ proc renderProfileTabs*(query: Query; username: string): VNode =
|
||||||
a(href=(link & "/with_replies")): text "Tweets & Replies"
|
a(href=(link & "/with_replies")): text "Tweets & Replies"
|
||||||
li(class=query.getTabClass(media)):
|
li(class=query.getTabClass(media)):
|
||||||
a(href=(link & "/media")): text "Media"
|
a(href=(link & "/media")): text "Media"
|
||||||
|
if query.fromUser.len == 1:
|
||||||
|
li(class=query.getTabClass(QueryKind.articles)):
|
||||||
|
a(href=(link & "/articles")): text "Articles"
|
||||||
li(class=query.getTabClass(tweets)):
|
li(class=query.getTabClass(tweets)):
|
||||||
a(href=(link & "/search")): text "Search"
|
a(href=(link & "/search")): text "Search"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,3 +84,31 @@ class TweetTest(BaseTestCase):
|
||||||
#for i, url in enumerate(images):
|
#for i, url in enumerate(images):
|
||||||
#img = self.get_attribute(Timeline.photo_rail + f' a:nth-child({i + 1}) img', 'src')
|
#img = self.get_attribute(Timeline.photo_rail + f' a:nth-child({i + 1}) img', 'src')
|
||||||
#self.assertIn(url, img)
|
#self.assertIn(url, img)
|
||||||
|
|
||||||
|
|
||||||
|
class ArticlesTabTest(BaseTestCase):
|
||||||
|
def test_articles_tab_on_profile(self):
|
||||||
|
self.open_nitter('satyanadella')
|
||||||
|
self.assert_element_present('.tab .tab-item a[href="/satyanadella/articles"]')
|
||||||
|
|
||||||
|
def test_articles_timeline(self):
|
||||||
|
self.open_nitter('satyanadella/articles')
|
||||||
|
self.assert_text('Articles', '.tab .tab-item.active a')
|
||||||
|
self.assert_element_present('.timeline .article-card')
|
||||||
|
self.assert_element_present('.timeline .article-card a[href^="/i/article/"]')
|
||||||
|
|
||||||
|
def test_articles_card_cover_and_no_raw_link(self):
|
||||||
|
self.open_nitter('jack/articles')
|
||||||
|
self.assert_element_present('.timeline .article-card .card-image img')
|
||||||
|
# the article link tweet text is redundant with the card and is stripped
|
||||||
|
self.assert_element_absent('.timeline .tweet-content a[href*="x.com/i/article"]')
|
||||||
|
|
||||||
|
def test_articles_empty(self):
|
||||||
|
self.open_nitter('mobile_test/articles')
|
||||||
|
self.assert_text('No items found', Timeline.none)
|
||||||
|
|
||||||
|
def test_articles_multi_user_unsupported(self):
|
||||||
|
self.open_nitter('jack,satyanadella')
|
||||||
|
self.assert_element_absent('.tab .tab-item a[href$="/articles"]')
|
||||||
|
self.open_nitter('jack,satyanadella/articles')
|
||||||
|
self.assert_text('Page not found')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue