Use legacy timeline/user endpoint for Tweets tab
This commit is contained in:
parent
5725780c99
commit
624394430c
8 changed files with 81 additions and 38 deletions
10
src/api.nim
10
src/api.nim
|
@ -40,6 +40,16 @@ proc getGraphUserTweets*(id: string; kind: TimelineKind; after=""): Future[Profi
|
|||
# url = oldUserTweets / (id & ".json") ? ps
|
||||
# result = parseTimeline(await fetch(url, Api.timeline), after)
|
||||
|
||||
proc getUserTimeline*(id: string; after=""): Future[Profile] {.async.} =
|
||||
var ps = genParams({"id": id})
|
||||
if after.len > 0:
|
||||
ps.add ("down_cursor", after)
|
||||
|
||||
let
|
||||
url = legacyUserTweets ? ps
|
||||
js = await fetch(url, Api.userTimeline)
|
||||
result = parseUserTimeline(js, after)
|
||||
|
||||
proc getGraphListTweets*(id: string; after=""): Future[Timeline] {.async.} =
|
||||
if id.len == 0: return
|
||||
let
|
||||
|
|
|
@ -16,8 +16,8 @@ proc genParams*(pars: openArray[(string, string)] = @[]; cursor="";
|
|||
for p in pars:
|
||||
result &= p
|
||||
if ext:
|
||||
result &= ("ext", "mediaStats,isBlueVerified,isVerified,blue,blueVerified")
|
||||
result &= ("include_ext_alt_text", "1")
|
||||
result &= ("include_ext_media_stats", "1")
|
||||
result &= ("include_ext_media_availability", "1")
|
||||
if count.len > 0:
|
||||
result &= ("count", count)
|
||||
|
|
|
@ -7,6 +7,7 @@ const
|
|||
api = parseUri("https://api.twitter.com")
|
||||
activate* = $(api / "1.1/guest/activate.json")
|
||||
|
||||
legacyUserTweets* = api / "1.1/timeline/user.json"
|
||||
photoRail* = api / "1.1/statuses/media_timeline.json"
|
||||
userSearch* = api / "1.1/users/search.json"
|
||||
tweetSearch* = api / "1.1/search/universal.json"
|
||||
|
@ -28,28 +29,20 @@ const
|
|||
graphListTweets* = graphql / "BbGLL1ZfMibdFNWlk7a0Pw/ListTimeline"
|
||||
|
||||
timelineParams* = {
|
||||
"cards_platform": "Web-13",
|
||||
"tweet_mode": "extended",
|
||||
"ui_lang": "en-US",
|
||||
"send_error_codes": "1",
|
||||
"simple_quoted_tweet": "1",
|
||||
"skip_status": "1",
|
||||
"include_blocked_by": "0",
|
||||
"include_blocking": "0",
|
||||
"include_can_dm": "0",
|
||||
"include_can_media_tag": "1",
|
||||
"include_cards": "1",
|
||||
"include_composer_source": "0",
|
||||
"include_entities": "1",
|
||||
"include_ext_is_blue_verified": "1",
|
||||
"include_ext_media_color": "0",
|
||||
"include_followed_by": "0",
|
||||
"include_mute_edge": "0",
|
||||
"include_profile_interstitial_type": "0",
|
||||
"include_quote_count": "1",
|
||||
"include_reply_count": "1",
|
||||
"include_user_entities": "1",
|
||||
"include_want_retweets": "0",
|
||||
"include_ext_reply_count": "1",
|
||||
"include_ext_is_blue_verified": "1",
|
||||
"include_ext_media_color": "0",
|
||||
"cards_platform": "Web-13",
|
||||
"tweet_mode": "extended",
|
||||
"send_error_codes": "1",
|
||||
"simple_quoted_tweet": "1"
|
||||
}.toSeq
|
||||
|
||||
gqlFeatures* = """{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
import strutils, options, times, math
|
||||
import strutils, options, times, math, tables
|
||||
import packedjson, packedjson/deserialiser
|
||||
import types, parserutils, utils
|
||||
import experimental/parser/unifiedcard
|
||||
|
@ -81,7 +81,7 @@ proc parseGif(js: JsonNode): Gif =
|
|||
proc parseVideo(js: JsonNode): Video =
|
||||
result = Video(
|
||||
thumb: js{"media_url_https"}.getImageStr,
|
||||
views: js{"ext", "mediaStats", "r", "ok", "viewCount"}.getStr($js{"mediaStats", "viewCount"}.getInt),
|
||||
views: getVideoViewCount(js),
|
||||
available: true,
|
||||
title: js{"ext_alt_text"}.getStr,
|
||||
durationMs: js{"video_info", "duration_millis"}.getInt
|
||||
|
@ -313,6 +313,54 @@ proc parseTweetSearch*(js: JsonNode; after=""): Timeline =
|
|||
if result.content.len > 0:
|
||||
result.bottom = $(result.content[^1][0].id - 1)
|
||||
|
||||
proc parseUserTimelineTweet(tweet: JsonNode; users: TableRef[string, User]): Tweet =
|
||||
result = parseTweet(tweet, tweet{"card"})
|
||||
|
||||
if result.isNil or not result.available:
|
||||
return
|
||||
|
||||
with user, tweet{"user"}:
|
||||
let userId = user{"id_str"}.getStr
|
||||
if user{"ext_is_blue_verified"}.getBool(false):
|
||||
users[userId].verified = users[userId].verified or true
|
||||
result.user = users[userId]
|
||||
|
||||
proc parseUserTimeline*(js: JsonNode; after=""): Profile =
|
||||
result = Profile(tweets: Timeline(beginning: after.len == 0))
|
||||
|
||||
if js.kind == JNull or "response" notin js or "twitter_objects" notin js:
|
||||
return
|
||||
|
||||
var users = newTable[string, User]()
|
||||
for userId, user in js{"twitter_objects", "users"}:
|
||||
users[userId] = parseUser(user)
|
||||
|
||||
for entity in js{"response", "timeline"}:
|
||||
let
|
||||
tweetId = entity{"tweet", "id"}.getId
|
||||
isPinned = entity{"tweet", "is_pinned"}.getBool(false)
|
||||
|
||||
with tweet, js{"twitter_objects", "tweets", $tweetId}:
|
||||
var parsed = parseUserTimelineTweet(tweet, users)
|
||||
|
||||
if not parsed.isNil and parsed.available:
|
||||
if parsed.quote.isSome:
|
||||
parsed.quote = some parseUserTimelineTweet(tweet{"quoted_status"}, users)
|
||||
|
||||
if parsed.retweet.isSome:
|
||||
let retweet = parseUserTimelineTweet(tweet{"retweeted_status"}, users)
|
||||
if retweet.quote.isSome:
|
||||
retweet.quote = some parseUserTimelineTweet(tweet{"retweeted_status", "quoted_status"}, users)
|
||||
parsed.retweet = some retweet
|
||||
|
||||
if isPinned:
|
||||
parsed.pinned = true
|
||||
result.pinned = some parsed
|
||||
else:
|
||||
result.tweets.content.add parsed
|
||||
|
||||
result.tweets.bottom = js{"response", "cursor", "bottom"}.getStr
|
||||
|
||||
# proc finalizeTweet(global: GlobalObjects; id: string): Tweet =
|
||||
# let intId = if id.len > 0: parseBiggestInt(id) else: 0
|
||||
# result = global.tweets.getOrDefault(id, Tweet(id: intId))
|
||||
|
|
|
@ -148,6 +148,12 @@ proc getMp4Resolution*(url: string): int =
|
|||
# cannot determine resolution (e.g. m3u8/non-mp4 video)
|
||||
return 0
|
||||
|
||||
proc getVideoViewCount*(js: JsonNode): string =
|
||||
with stats, js{"ext_media_stats"}:
|
||||
return stats{"view_count"}.getStr($stats{"viewCount"}.getInt)
|
||||
|
||||
return $js{"mediaStats", "viewCount"}.getInt(0)
|
||||
|
||||
proc extractSlice(js: JsonNode): Slice[int] =
|
||||
result = js["indices"][0].getInt ..< js["indices"][1].getInt
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ proc fetchProfile*(after: string; query: Query; skipRail=false;
|
|||
|
||||
result =
|
||||
case query.kind
|
||||
# of posts: await getTimeline(userId, after)
|
||||
of posts: await getUserTimeline(userId, after)
|
||||
of replies: await getGraphUserTweets(userId, TimelineKind.replies, after)
|
||||
of media: await getGraphUserTweets(userId, TimelineKind.media, after)
|
||||
else: Profile(tweets: await getTweetSearch(query, after))
|
||||
|
@ -63,21 +63,6 @@ proc fetchProfile*(after: string; query: Query; skipRail=false;
|
|||
|
||||
result.tweets.query = query
|
||||
|
||||
if result.user.protected or result.user.suspended:
|
||||
return
|
||||
|
||||
if query.kind == posts:
|
||||
if result.user.verified:
|
||||
for chain in result.tweets.content:
|
||||
if chain[0].user.id == result.user.id:
|
||||
chain[0].user.verified = true
|
||||
if not skipPinned and result.user.pinnedTweet > 0 and after.len == 0:
|
||||
let tweet = await getCachedTweet(result.user.pinnedTweet)
|
||||
if not tweet.isNil:
|
||||
tweet.pinned = true
|
||||
tweet.user = result.user
|
||||
result.pinned = some tweet
|
||||
|
||||
proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
|
||||
rss, after: string): Future[string] {.async.} =
|
||||
if query.fromUser.len != 1:
|
||||
|
|
|
@ -44,10 +44,10 @@ proc getPoolJson*(): JsonNode =
|
|||
of Api.search: 100000
|
||||
of Api.photoRail: 180
|
||||
of Api.timeline: 187
|
||||
of Api.userTweets: 300
|
||||
of Api.userTweets, Api.userTimeline: 300
|
||||
of Api.userTweetsAndReplies, Api.userRestId,
|
||||
Api.userScreenName, Api.tweetDetail, Api.tweetResult: 500
|
||||
of Api.list, Api.listTweets, Api.listMembers, Api.listBySlug, Api.userMedia: 500
|
||||
Api.userScreenName, Api.tweetDetail, Api.tweetResult,
|
||||
Api.list, Api.listTweets, Api.listMembers, Api.listBySlug, Api.userMedia: 500
|
||||
of Api.userSearch: 900
|
||||
reqs = maxReqs - token.apis[api].remaining
|
||||
|
||||
|
@ -161,6 +161,6 @@ proc initTokenPool*(cfg: Config) {.async.} =
|
|||
enableLogging = cfg.enableDebug
|
||||
|
||||
while true:
|
||||
if tokenPool.countIt(not it.isLimited(Api.timeline)) < cfg.minTokens:
|
||||
if tokenPool.countIt(not it.isLimited(Api.userTimeline)) < cfg.minTokens:
|
||||
await poolTokens(min(4, cfg.minTokens - tokenPool.len))
|
||||
await sleepAsync(2000)
|
||||
|
|
|
@ -18,6 +18,7 @@ type
|
|||
tweetDetail
|
||||
tweetResult
|
||||
timeline
|
||||
userTimeline
|
||||
photoRail
|
||||
search
|
||||
userSearch
|
||||
|
|
Loading…
Reference in a new issue