From 51714b5ad2f8992972f43fd9cb2178aba2729f39 Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 21 Aug 2023 11:25:27 +0200 Subject: [PATCH 01/21] Add guest accounts variable to GitHub action --- .github/workflows/run-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 140b6bf..37979cb 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -40,6 +40,9 @@ jobs: nimble md nimble scss - name: Run tests + env: + GUEST_ACCOUNTS: ${{ secrets.GUEST_ACCOUNTS }} run: | + echo $GUEST_ACCOUNTS > ./guest_accounts.json ./nitter & pytest -n4 tests From c3d9441370e9740a02834dbf1da849263a9075c3 Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 21 Aug 2023 14:49:50 +0200 Subject: [PATCH 02/21] Unify some guest account logs --- src/apiutils.nim | 2 +- src/tokens.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apiutils.nim b/src/apiutils.nim index 6e333e7..453b36a 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -138,7 +138,7 @@ proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} = elif errors in {rateLimited}: account.apis[api].limited = true account.apis[api].limitedAt = epochTime().int - echo "rate limited, api: ", $api, ", reqs left: ", account.apis[api].remaining, ", id: ", account.id + echo "[accounts] rate limited, api: ", api, ", reqs left: ", account.apis[api].remaining, ", id: ", account.id proc fetchRaw*(url: Uri; api: Api): Future[string] {.async.} = fetchImpl result: diff --git a/src/tokens.nim b/src/tokens.nim index 45aa895..a44866f 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -77,7 +77,7 @@ proc isLimited(account: GuestAccount; api: Api): bool = if limit.limited and (epochTime().int - limit.limitedAt) > dayInSeconds: account.apis[api].limited = false - echo "account limit reset, api: ", api, ", id: ", account.id + log "resetting limit, api: ", api, ", id: ", account.id return limit.limited or (limit.remaining <= 10 and limit.reset > epochTime().int) else: From 12504bcffed59efe626b90a574b27857f7450515 Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 21 Aug 2023 18:12:06 +0200 Subject: [PATCH 03/21] Fix compilation error --- src/tokens.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tokens.nim b/src/tokens.nim index a44866f..d1f1e68 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -77,7 +77,7 @@ proc isLimited(account: GuestAccount; api: Api): bool = if limit.limited and (epochTime().int - limit.limitedAt) > dayInSeconds: account.apis[api].limited = false - log "resetting limit, api: ", api, ", id: ", account.id + log "resetting limit, api: " & $api & ", id: " & $account.id return limit.limited or (limit.remaining <= 10 and limit.reset > epochTime().int) else: From 30bdf3a14e900c653f1c277c5e0221cab846e1de Mon Sep 17 00:00:00 2001 From: Zed Date: Tue, 22 Aug 2023 01:32:09 +0200 Subject: [PATCH 04/21] Reduce max concurrent pending requests per account --- src/tokens.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tokens.nim b/src/tokens.nim index d1f1e68..f16a816 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -1,10 +1,10 @@ -# SPDX-License-Identifier: AGPL-3.0-only +#i hate begging for this too em SPDX-License-Identifier: AGPL-3.0-only import asyncdispatch, times, json, random, strutils, tables import types # max requests at a time per account to avoid race conditions const - maxConcurrentReqs = 5 + maxConcurrentReqs = 2 dayInSeconds = 24 * 60 * 60 var From 5c08e6a774f428b4193a33e0400729c279ded07c Mon Sep 17 00:00:00 2001 From: Zed Date: Tue, 22 Aug 2023 02:27:44 +0200 Subject: [PATCH 05/21] Fix compilation on older versions of Nim --- src/parserutils.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parserutils.nim b/src/parserutils.nim index c65052e..7cf696e 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -36,7 +36,8 @@ template with*(ident, value, body): untyped = template with*(ident; value: JsonNode; body): untyped = if true: let ident {.inject.} = value - if value.notNull: body + # value.notNull causes a compilation error for versions < 1.6.14 + if notNull(value): body template getCursor*(js: JsonNode): string = js{"content", "operation", "cursor", "value"}.getStr From 6e8744943f9c47cbc7150785a86fa05a18d7b98b Mon Sep 17 00:00:00 2001 From: Zed Date: Tue, 22 Aug 2023 03:43:18 +0200 Subject: [PATCH 06/21] Tweak /.tokens, add amount of limited accounts --- src/tokens.nim | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/tokens.nim b/src/tokens.nim index f16a816..5582a51 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -19,6 +19,7 @@ proc getPoolJson*(): JsonNode = list = newJObject() totalReqs = 0 totalPending = 0 + totalLimited = 0 reqsPerApi: Table[string, int] let now = epochTime().int @@ -31,34 +32,40 @@ proc getPoolJson*(): JsonNode = } for api in account.apis.keys: - let obj = %*{} - if account.apis[api].limited: - obj["limited"] = %true + let + apiStatus = account.apis[api] + obj = %*{} - if account.apis[api].reset > now.int: - obj["remaining"] = %account.apis[api].remaining + if apiStatus.limited: + obj["limited"] = %true + inc totalLimited + + if apiStatus.reset > now.int: + obj["remaining"] = %apiStatus.remaining + + if "remaining" notin obj and not apiStatus.limited: + continue list[account.id]["apis"][$api] = obj - if "remaining" notin obj: - continue - let maxReqs = case api of Api.search: 50 + of Api.tweetDetail: 150 of Api.photoRail: 180 of Api.userTweets, Api.userTweetsAndReplies, Api.userMedia, - Api.userRestId, Api.userScreenName, - Api.tweetDetail, Api.tweetResult, + Api.userRestId, Api.userScreenName, Api.tweetResult, Api.list, Api.listTweets, Api.listMembers, Api.listBySlug: 500 - reqs = maxReqs - account.apis[api].remaining + of Api.userSearch: 900 + reqs = maxReqs - apiStatus.remaining reqsPerApi[$api] = reqsPerApi.getOrDefault($api, 0) + reqs totalReqs.inc(reqs) return %*{ "amount": accountPool.len, + "limited": totalLimited, "requests": totalReqs, "pending": totalPending, "apis": reqsPerApi, From 8df5256c1dd3970bcbb71cfa7753c3b50d59287e Mon Sep 17 00:00:00 2001 From: Zed Date: Tue, 22 Aug 2023 03:44:11 +0200 Subject: [PATCH 07/21] Switch back to old user search endpoint --- src/api.nim | 28 ++++++++++++---------------- src/consts.nim | 2 ++ src/experimental/parser/user.nim | 2 +- src/experimental/types/user.nim | 1 + src/parser.nim | 26 ++++++++++---------------- src/routes/search.nim | 2 +- src/types.nim | 1 + 7 files changed, 28 insertions(+), 34 deletions(-) diff --git a/src/api.nim b/src/api.nim index d6a4564..4ac999c 100644 --- a/src/api.nim +++ b/src/api.nim @@ -112,29 +112,25 @@ proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} = if after.len > 0: variables["cursor"] = % after let url = graphSearchTimeline ? {"variables": $variables, "features": gqlFeatures} - result = parseGraphSearch[Tweets](await fetch(url, Api.search), after) + result = parseGraphSearch(await fetch(url, Api.search), after) result.query = query -proc getGraphUserSearch*(query: Query; after=""): Future[Result[User]] {.async.} = +proc getUserSearch*(query: Query; page="1"): Future[Result[User]] {.async.} = if query.text.len == 0: return Result[User](query: query, beginning: true) - var - variables = %*{ - "rawQuery": query.text, - "count": 20, - "product": "People", - "withDownvotePerspective": false, - "withReactionsMetadata": false, - "withReactionsPerspective": false - } - if after.len > 0: - variables["cursor"] = % after - result.beginning = false + let + page = if page.len == 0: "1" else: page + url = userSearch ? genParams({"q": query.text, "skip_status": "1", "page": page}) + js = await fetchRaw(url, Api.userSearch) + + result = parseUsers(js) - let url = graphSearchTimeline ? {"variables": $variables, "features": gqlFeatures} - result = parseGraphSearch[User](await fetch(url, Api.search), after) result.query = query + if page.len == 0: + result.bottom = "2" + elif page.allCharsInSet(Digits): + result.bottom = $(parseInt(page) + 1) proc getPhotoRail*(name: string): Future[PhotoRail] {.async.} = if name.len == 0: return diff --git a/src/consts.nim b/src/consts.nim index 2cfd1ed..8bf6422 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -9,6 +9,7 @@ const activate* = $(api / "1.1/guest/activate.json") photoRail* = api / "1.1/statuses/media_timeline.json" + userSearch* = api / "1.1/users/search.json" graphql = api / "graphql" graphUser* = graphql / "u7wQyGi6oExe8_TRWGMq4Q/UserResultByScreenNameQuery" @@ -34,6 +35,7 @@ const "include_user_entities": "1", "include_ext_reply_count": "1", "include_ext_is_blue_verified": "1", + #"include_ext_verified_type": "1", "include_ext_media_color": "0", "cards_platform": "Web-13", "tweet_mode": "extended", diff --git a/src/experimental/parser/user.nim b/src/experimental/parser/user.nim index b4d710f..5962a87 100644 --- a/src/experimental/parser/user.nim +++ b/src/experimental/parser/user.nim @@ -56,7 +56,7 @@ proc toUser*(raw: RawUser): User = tweets: raw.statusesCount, likes: raw.favouritesCount, media: raw.mediaCount, - verified: raw.verified, + verified: raw.verified or raw.extIsBlueVerified, protected: raw.protected, joinDate: parseTwitterDate(raw.createdAt), banner: getBanner(raw), diff --git a/src/experimental/types/user.nim b/src/experimental/types/user.nim index 1c8a5c3..39331a0 100644 --- a/src/experimental/types/user.nim +++ b/src/experimental/types/user.nim @@ -16,6 +16,7 @@ type statusesCount*: int mediaCount*: int verified*: bool + extIsBlueVerified*: bool protected*: bool profileLinkColor*: string profileBannerUrl*: string diff --git a/src/parser.nim b/src/parser.nim index 9262b28..03242c1 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -1,5 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, options, times, math, tables +import strutils, options, times, math import packedjson, packedjson/deserialiser import types, parserutils, utils import experimental/parser/unifiedcard @@ -436,8 +436,8 @@ proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile = tweet.id = parseBiggestInt(entryId) result.pinned = some tweet -proc parseGraphSearch*[T: User | Tweets](js: JsonNode; after=""): Result[T] = - result = Result[T](beginning: after.len == 0) +proc parseGraphSearch*(js: JsonNode; after=""): Timeline = + result = Timeline(beginning: after.len == 0) let instructions = js{"data", "search_by_raw_query", "search_timeline", "timeline", "instructions"} if instructions.len == 0: @@ -448,19 +448,13 @@ proc parseGraphSearch*[T: User | Tweets](js: JsonNode; after=""): Result[T] = if typ == "TimelineAddEntries": for e in instruction{"entries"}: let entryId = e{"entryId"}.getStr - when T is Tweets: - if entryId.startsWith("tweet"): - with tweetRes, e{"content", "itemContent", "tweet_results", "result"}: - let tweet = parseGraphTweet(tweetRes) - if not tweet.available: - tweet.id = parseBiggestInt(entryId.getId()) - result.content.add tweet - elif T is User: - if entryId.startsWith("user"): - with userRes, e{"content", "itemContent"}: - result.content.add parseGraphUser(userRes) - - if entryId.startsWith("cursor-bottom"): + if entryId.startsWith("tweet"): + with tweetRes, e{"content", "itemContent", "tweet_results", "result"}: + let tweet = parseGraphTweet(tweetRes) + if not tweet.available: + tweet.id = parseBiggestInt(entryId.getId()) + result.content.add tweet + elif entryId.startsWith("cursor-bottom"): result.bottom = e{"content", "value"}.getStr elif typ == "TimelineReplaceEntry": if instruction{"entry_id_to_replace"}.getStr.startsWith("cursor-bottom"): diff --git a/src/routes/search.nim b/src/routes/search.nim index e9f991d..676229e 100644 --- a/src/routes/search.nim +++ b/src/routes/search.nim @@ -29,7 +29,7 @@ proc createSearchRouter*(cfg: Config) = redirect("/" & q) var users: Result[User] try: - users = await getGraphUserSearch(query, getCursor()) + users = await getUserSearch(query, getCursor()) except InternalError: users = Result[User](beginning: true, query: query) resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs, title) diff --git a/src/types.nim b/src/types.nim index 33d0cda..fcb24c0 100644 --- a/src/types.nim +++ b/src/types.nim @@ -19,6 +19,7 @@ type tweetResult photoRail search + userSearch list listBySlug listMembers From 45808361af63e848ce322a27190d3ed7aba0723a Mon Sep 17 00:00:00 2001 From: Zed Date: Tue, 22 Aug 2023 04:45:49 +0200 Subject: [PATCH 08/21] Fix tweetDetail stats --- src/tokens.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tokens.nim b/src/tokens.nim index 5582a51..a3ed78a 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -52,10 +52,10 @@ proc getPoolJson*(): JsonNode = maxReqs = case api of Api.search: 50 - of Api.tweetDetail: 150 of Api.photoRail: 180 of Api.userTweets, Api.userTweetsAndReplies, Api.userMedia, - Api.userRestId, Api.userScreenName, Api.tweetResult, + Api.userRestId, Api.userScreenName, + Api.tweetResult, Api.tweetDetail, Api.list, Api.listTweets, Api.listMembers, Api.listBySlug: 500 of Api.userSearch: 900 reqs = maxReqs - apiStatus.remaining From a3e11e3272ebdd6118cd818591530ac8be5ae491 Mon Sep 17 00:00:00 2001 From: Zed Date: Wed, 23 Aug 2023 10:14:44 +0200 Subject: [PATCH 09/21] Switch to using typeahead for user search --- src/api.nim | 12 +++--------- src/consts.nim | 2 +- src/experimental/parser/user.nim | 7 +++++++ src/experimental/types/user.nim | 3 +++ src/routes/search.nim | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/api.nim b/src/api.nim index 4ac999c..a7efbd0 100644 --- a/src/api.nim +++ b/src/api.nim @@ -115,22 +115,16 @@ proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} = result = parseGraphSearch(await fetch(url, Api.search), after) result.query = query -proc getUserSearch*(query: Query; page="1"): Future[Result[User]] {.async.} = +proc getUserSearch*(query: Query): Future[Result[User]] {.async.} = if query.text.len == 0: return Result[User](query: query, beginning: true) let - page = if page.len == 0: "1" else: page - url = userSearch ? genParams({"q": query.text, "skip_status": "1", "page": page}) + url = userSearch ? genParams({"q": query.text, "result_type": "users"}) js = await fetchRaw(url, Api.userSearch) - result = parseUsers(js) - + result = parseTypeahead(js) result.query = query - if page.len == 0: - result.bottom = "2" - elif page.allCharsInSet(Digits): - result.bottom = $(parseInt(page) + 1) proc getPhotoRail*(name: string): Future[PhotoRail] {.async.} = if name.len == 0: return diff --git a/src/consts.nim b/src/consts.nim index 8bf6422..1bb950e 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -9,7 +9,7 @@ const activate* = $(api / "1.1/guest/activate.json") photoRail* = api / "1.1/statuses/media_timeline.json" - userSearch* = api / "1.1/users/search.json" + userSearch* = api / "1.1/search/typeahead.json" graphql = api / "graphql" graphUser* = graphql / "u7wQyGi6oExe8_TRWGMq4Q/UserResultByScreenNameQuery" diff --git a/src/experimental/parser/user.nim b/src/experimental/parser/user.nim index 5962a87..400e740 100644 --- a/src/experimental/parser/user.nim +++ b/src/experimental/parser/user.nim @@ -85,3 +85,10 @@ proc parseUsers*(json: string; after=""): Result[User] = let raw = json.fromJson(seq[RawUser]) for user in raw: result.content.add user.toUser + +proc parseTypeahead*(json: string): Result[User] = + result = Result[User](beginning: true) + + let raw = json.fromJson(Typeahead) + for user in raw.users: + result.content.add user.toUser diff --git a/src/experimental/types/user.nim b/src/experimental/types/user.nim index 39331a0..7d34f7b 100644 --- a/src/experimental/types/user.nim +++ b/src/experimental/types/user.nim @@ -42,3 +42,6 @@ type Color* = object red*, green*, blue*: int + + Typeahead* = object + users*: seq[RawUser] diff --git a/src/routes/search.nim b/src/routes/search.nim index 676229e..e0a888c 100644 --- a/src/routes/search.nim +++ b/src/routes/search.nim @@ -29,7 +29,7 @@ proc createSearchRouter*(cfg: Config) = redirect("/" & q) var users: Result[User] try: - users = await getUserSearch(query, getCursor()) + users = await getUserSearch(query) except InternalError: users = Result[User](beginning: true, query: query) resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs, title) From 88b005c9da726af3f0858339da60949b42278744 Mon Sep 17 00:00:00 2001 From: Zed Date: Wed, 23 Aug 2023 19:31:40 +0200 Subject: [PATCH 10/21] Revert "Switch to using typeahead for user search" This reverts commit a3e11e3272ebdd6118cd818591530ac8be5ae491. --- src/api.nim | 12 +++++++++--- src/consts.nim | 2 +- src/experimental/parser/user.nim | 7 ------- src/experimental/types/user.nim | 3 --- src/routes/search.nim | 2 +- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/api.nim b/src/api.nim index a7efbd0..4ac999c 100644 --- a/src/api.nim +++ b/src/api.nim @@ -115,16 +115,22 @@ proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} = result = parseGraphSearch(await fetch(url, Api.search), after) result.query = query -proc getUserSearch*(query: Query): Future[Result[User]] {.async.} = +proc getUserSearch*(query: Query; page="1"): Future[Result[User]] {.async.} = if query.text.len == 0: return Result[User](query: query, beginning: true) let - url = userSearch ? genParams({"q": query.text, "result_type": "users"}) + page = if page.len == 0: "1" else: page + url = userSearch ? genParams({"q": query.text, "skip_status": "1", "page": page}) js = await fetchRaw(url, Api.userSearch) - result = parseTypeahead(js) + result = parseUsers(js) + result.query = query + if page.len == 0: + result.bottom = "2" + elif page.allCharsInSet(Digits): + result.bottom = $(parseInt(page) + 1) proc getPhotoRail*(name: string): Future[PhotoRail] {.async.} = if name.len == 0: return diff --git a/src/consts.nim b/src/consts.nim index 1bb950e..8bf6422 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -9,7 +9,7 @@ const activate* = $(api / "1.1/guest/activate.json") photoRail* = api / "1.1/statuses/media_timeline.json" - userSearch* = api / "1.1/search/typeahead.json" + userSearch* = api / "1.1/users/search.json" graphql = api / "graphql" graphUser* = graphql / "u7wQyGi6oExe8_TRWGMq4Q/UserResultByScreenNameQuery" diff --git a/src/experimental/parser/user.nim b/src/experimental/parser/user.nim index 400e740..5962a87 100644 --- a/src/experimental/parser/user.nim +++ b/src/experimental/parser/user.nim @@ -85,10 +85,3 @@ proc parseUsers*(json: string; after=""): Result[User] = let raw = json.fromJson(seq[RawUser]) for user in raw: result.content.add user.toUser - -proc parseTypeahead*(json: string): Result[User] = - result = Result[User](beginning: true) - - let raw = json.fromJson(Typeahead) - for user in raw.users: - result.content.add user.toUser diff --git a/src/experimental/types/user.nim b/src/experimental/types/user.nim index 7d34f7b..39331a0 100644 --- a/src/experimental/types/user.nim +++ b/src/experimental/types/user.nim @@ -42,6 +42,3 @@ type Color* = object red*, green*, blue*: int - - Typeahead* = object - users*: seq[RawUser] diff --git a/src/routes/search.nim b/src/routes/search.nim index e0a888c..676229e 100644 --- a/src/routes/search.nim +++ b/src/routes/search.nim @@ -29,7 +29,7 @@ proc createSearchRouter*(cfg: Config) = redirect("/" & q) var users: Result[User] try: - users = await getUserSearch(query) + users = await getUserSearch(query, getCursor()) except InternalError: users = Result[User](beginning: true, query: query) resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs, title) From ae9fa02bf5f8459e59f0f18120fee9f06404aef5 Mon Sep 17 00:00:00 2001 From: Zed Date: Fri, 25 Aug 2023 16:28:30 +0200 Subject: [PATCH 11/21] Switch to TweetDetail for tweets --- src/consts.nim | 10 +++++++--- src/parser.nim | 25 +++++++++++++++---------- src/tokens.nim | 3 ++- tests/test_card.py | 10 +++++----- tests/test_timeline.py | 2 +- tests/test_tweet_media.py | 2 +- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/consts.nim b/src/consts.nim index 8bf6422..96cea47 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -17,7 +17,7 @@ const graphUserTweets* = graphql / "3JNH4e9dq1BifLxAa3UMWg/UserWithProfileTweetsQueryV2" graphUserTweetsAndReplies* = graphql / "8IS8MaO-2EN6GZZZb8jF0g/UserWithProfileTweetsAndRepliesQueryV2" graphUserMedia* = graphql / "PDfFf8hGeJvUCiTyWtw4wQ/MediaTimelineV2" - graphTweet* = graphql / "83h5UyHZ9wEKBVzALX8R_g/ConversationTimelineV2" + graphTweet* = graphql / "q94uRCEn65LZThakYcPT6g/TweetDetail" graphTweetResult* = graphql / "sITyJdhRPpvpEjg4waUmTA/TweetResultByIdQuery" graphSearchTimeline* = graphql / "gkjsKepM6gl_HmFWoWKfgg/SearchTimeline" graphListById* = graphql / "iTpgCtbdxrsJfyx0cFjHqg/ListByRestId" @@ -89,8 +89,12 @@ const tweetVariables* = """{ "focalTweetId": "$1", $2 - "includeHasBirdwatchNotes": false -}""" + "includeHasBirdwatchNotes": false, + "includePromotedContent": false, + "withBirdwatchNotes": false, + "withVoice": false, + "withV2Timeline": true +}""".replace(" ", "").replace("\n", "") # oldUserTweetsVariables* = """{ # "userId": "$1", $2 diff --git a/src/parser.nim b/src/parser.nim index 03242c1..d5190a3 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -324,7 +324,7 @@ proc parseGraphTweet(js: JsonNode): Tweet = of "TweetWithVisibilityResults": return parseGraphTweet(js{"tweet"}) - var jsCard = copy(js{"tweet_card", "legacy"}) + var jsCard = copy(js{"card", "legacy"}) if jsCard.kind != JNull: var values = newJObject() for val in jsCard["binding_values"]: @@ -342,7 +342,6 @@ proc parseGraphTweet(js: JsonNode): Tweet = result.quote = some(parseGraphTweet(js{"quoted_status_result", "result"})) proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] = - let thread = js{"content", "items"} for t in js{"content", "items"}: let entryId = t{"entryId"}.getStr if "cursor-showmore" in entryId: @@ -350,11 +349,16 @@ proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] = result.thread.cursor = cursor.getStr result.thread.hasMore = true elif "tweet" in entryId: - let tweet = parseGraphTweet(t{"item", "content", "tweetResult", "result"}) - result.thread.content.add tweet + let + isLegacy = t{"item"}.hasKey("itemContent") + (contentKey, resultKey) = if isLegacy: ("itemContent", "tweet_results") + else: ("content", "tweetResult") - if t{"item", "content", "tweetDisplayType"}.getStr == "SelfThread": - result.self = true + with content, t{"item", contentKey}: + result.thread.content.add parseGraphTweet(content{resultKey, "result"}) + + if content{"tweetDisplayType"}.getStr == "SelfThread": + result.self = true proc parseGraphTweetResult*(js: JsonNode): Tweet = with tweet, js{"data", "tweet_result", "result"}: @@ -363,14 +367,14 @@ proc parseGraphTweetResult*(js: JsonNode): Tweet = proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation = result = Conversation(replies: Result[Chain](beginning: true)) - let instructions = ? js{"data", "timeline_response", "instructions"} + let instructions = ? js{"data", "threaded_conversation_with_injections_v2", "instructions"} if instructions.len == 0: return for e in instructions[0]{"entries"}: let entryId = e{"entryId"}.getStr if entryId.startsWith("tweet"): - with tweetResult, e{"content", "content", "tweetResult", "result"}: + with tweetResult, e{"content", "itemContent", "tweet_results", "result"}: let tweet = parseGraphTweet(tweetResult) if not tweet.available: @@ -385,7 +389,7 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation = let tweet = Tweet( id: parseBiggestInt(id), available: false, - text: e{"content", "content", "tombstoneInfo", "richText"}.getTombstone + text: e{"content", "itemContent", "tombstoneInfo", "richText"}.getTombstone ) if id == tweetId: @@ -397,9 +401,10 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation = if self: result.after = thread else: + echo "adding thread: ", thread.content.len result.replies.content.add thread elif entryId.startsWith("cursor-bottom"): - result.replies.bottom = e{"content", "content", "value"}.getStr + result.replies.bottom = e{"content", "itemContent", "value"}.getStr proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile = result = Profile(tweets: Timeline(beginning: after.len == 0)) diff --git a/src/tokens.nim b/src/tokens.nim index a3ed78a..bb9696c 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -52,10 +52,11 @@ proc getPoolJson*(): JsonNode = maxReqs = case api of Api.search: 50 + of Api.tweetDetail: 150 of Api.photoRail: 180 of Api.userTweets, Api.userTweetsAndReplies, Api.userMedia, Api.userRestId, Api.userScreenName, - Api.tweetResult, Api.tweetDetail, + Api.tweetResult, Api.list, Api.listTweets, Api.listMembers, Api.listBySlug: 500 of Api.userSearch: 900 reqs = maxReqs - apiStatus.remaining diff --git a/tests/test_card.py b/tests/test_card.py index f84ddca..733bd40 100644 --- a/tests/test_card.py +++ b/tests/test_card.py @@ -13,11 +13,6 @@ card = [ 'Basic OBS Studio plugin, written in nim, supporting C++ (C fine too) - obsplugin.nim', 'gist.github.com', True], - ['FluentAI/status/1116417904831029248', - 'Amazon’s Alexa isn’t just AI — thousands of humans are listening', - 'One of the only ways to improve Alexa is to have human beings check it for errors', - 'theverge.com', True], - ['nim_lang/status/1082989146040340480', 'Nim in 2018: A short recap', 'There were several big news in the Nim world in 2018 – two new major releases, partnership with Status, and much more. But let us go chronologically.', @@ -25,6 +20,11 @@ card = [ ] no_thumb = [ + ['FluentAI/status/1116417904831029248', + 'Amazon’s Alexa isn’t just AI — thousands of humans are listening', + 'One of the only ways to improve Alexa is to have human beings check it for errors', + 'theverge.com'], + ['Thom_Wolf/status/1122466524860702729', 'facebookresearch/fairseq', 'Facebook AI Research Sequence-to-Sequence Toolkit written in Python. - GitHub - facebookresearch/fairseq: Facebook AI Research Sequence-to-Sequence Toolkit written in Python.', diff --git a/tests/test_timeline.py b/tests/test_timeline.py index b56d6ad..919aa70 100644 --- a/tests/test_timeline.py +++ b/tests/test_timeline.py @@ -6,7 +6,7 @@ normal = [['jack'], ['elonmusk']] after = [['jack', '1681686036294803456'], ['elonmusk', '1681686036294803456']] -no_more = [['mobile_test_8?cursor=1000']] +no_more = [['mobile_test_8?cursor=DAABCgABF4YVAqN___kKAAICNn_4msIQAAgAAwAAAAIAAA']] empty = [['emptyuser'], ['mobile_test_10']] diff --git a/tests/test_tweet_media.py b/tests/test_tweet_media.py index 7a00983..f54cea7 100644 --- a/tests/test_tweet_media.py +++ b/tests/test_tweet_media.py @@ -14,7 +14,7 @@ poll = [ image = [ ['mobile_test/status/519364660823207936', 'BzUnaDFCUAAmrjs'], - ['mobile_test_2/status/324619691039543297', 'BIFH45vCUAAQecj'] + #['mobile_test_2/status/324619691039543297', 'BIFH45vCUAAQecj'] ] gif = [ From 03794a8d4a0eb13fccdf88d5f6633956daaf6564 Mon Sep 17 00:00:00 2001 From: Zed Date: Fri, 25 Aug 2023 16:32:33 +0200 Subject: [PATCH 12/21] Cleanup --- src/parser.nim | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parser.nim b/src/parser.nim index d5190a3..4087a79 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -401,7 +401,6 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation = if self: result.after = thread else: - echo "adding thread: ", thread.content.len result.replies.content.add thread elif entryId.startsWith("cursor-bottom"): result.replies.bottom = e{"content", "itemContent", "value"}.getStr From 7630f57f17246ffb60d4f5472d17af5fc3c6fa9f Mon Sep 17 00:00:00 2001 From: Zed Date: Sat, 26 Aug 2023 05:16:38 +0200 Subject: [PATCH 13/21] Fix cards not being displayed --- src/parser.nim | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/parser.nim b/src/parser.nim index 4087a79..914c038 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -4,7 +4,7 @@ import packedjson, packedjson/deserialiser import types, parserutils, utils import experimental/parser/unifiedcard -proc parseGraphTweet(js: JsonNode): Tweet +proc parseGraphTweet(js: JsonNode; isLegacy=false): Tweet proc parseUser(js: JsonNode; id=""): User = if js.isNull: return @@ -306,7 +306,7 @@ proc parsePhotoRail*(js: JsonNode): PhotoRail = if url.len == 0: continue result.add GalleryPhoto(url: url, tweetId: $t.id) -proc parseGraphTweet(js: JsonNode): Tweet = +proc parseGraphTweet(js: JsonNode; isLegacy=false): Tweet = if js.kind == JNull: return Tweet() @@ -322,9 +322,9 @@ proc parseGraphTweet(js: JsonNode): Tweet = of "TweetPreviewDisplay": return Tweet(text: "You're unable to view this Tweet because it's only available to the Subscribers of the account owner.") of "TweetWithVisibilityResults": - return parseGraphTweet(js{"tweet"}) + return parseGraphTweet(js{"tweet"}, isLegacy) - var jsCard = copy(js{"card", "legacy"}) + var jsCard = copy(js{if isLegacy: "card" else: "tweet_card", "legacy"}) if jsCard.kind != JNull: var values = newJObject() for val in jsCard["binding_values"]: @@ -339,7 +339,7 @@ proc parseGraphTweet(js: JsonNode): Tweet = result.expandNoteTweetEntities(noteTweet) if result.quote.isSome: - result.quote = some(parseGraphTweet(js{"quoted_status_result", "result"})) + result.quote = some(parseGraphTweet(js{"quoted_status_result", "result"}, isLegacy)) proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] = for t in js{"content", "items"}: @@ -355,14 +355,14 @@ proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] = else: ("content", "tweetResult") with content, t{"item", contentKey}: - result.thread.content.add parseGraphTweet(content{resultKey, "result"}) + result.thread.content.add parseGraphTweet(content{resultKey, "result"}, isLegacy) if content{"tweetDisplayType"}.getStr == "SelfThread": result.self = true proc parseGraphTweetResult*(js: JsonNode): Tweet = with tweet, js{"data", "tweet_result", "result"}: - result = parseGraphTweet(tweet) + result = parseGraphTweet(tweet, false) proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation = result = Conversation(replies: Result[Chain](beginning: true)) @@ -375,7 +375,7 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation = let entryId = e{"entryId"}.getStr if entryId.startsWith("tweet"): with tweetResult, e{"content", "itemContent", "tweet_results", "result"}: - let tweet = parseGraphTweet(tweetResult) + let tweet = parseGraphTweet(tweetResult, true) if not tweet.available: tweet.id = parseBiggestInt(entryId.getId()) @@ -421,7 +421,7 @@ proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile = let entryId = e{"entryId"}.getStr if entryId.startsWith("tweet"): with tweetResult, e{"content", "content", "tweetResult", "result"}: - let tweet = parseGraphTweet(tweetResult) + let tweet = parseGraphTweet(tweetResult, false) if not tweet.available: tweet.id = parseBiggestInt(entryId.getId()) result.tweets.content.add tweet @@ -432,7 +432,7 @@ proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile = result.tweets.bottom = e{"content", "value"}.getStr if after.len == 0 and i{"__typename"}.getStr == "TimelinePinEntry": with tweetResult, i{"entry", "content", "content", "tweetResult", "result"}: - let tweet = parseGraphTweet(tweetResult) + let tweet = parseGraphTweet(tweetResult, false) tweet.pinned = true if not tweet.available and tweet.tombstone.len == 0: let entryId = i{"entry", "entryId"}.getEntryId @@ -454,7 +454,7 @@ proc parseGraphSearch*(js: JsonNode; after=""): Timeline = let entryId = e{"entryId"}.getStr if entryId.startsWith("tweet"): with tweetRes, e{"content", "itemContent", "tweet_results", "result"}: - let tweet = parseGraphTweet(tweetRes) + let tweet = parseGraphTweet(tweetRes, true) if not tweet.available: tweet.id = parseBiggestInt(entryId.getId()) result.content.add tweet From 4ccf350dc74416173cad16eb0a7dd4b49239fd2c Mon Sep 17 00:00:00 2001 From: Zed Date: Tue, 29 Aug 2023 23:45:18 +0200 Subject: [PATCH 14/21] Improve .tokens output --- src/tokens.nim | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/tokens.nim b/src/tokens.nim index bb9696c..a3af9bf 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -1,5 +1,5 @@ -#i hate begging for this too em SPDX-License-Identifier: AGPL-3.0-only -import asyncdispatch, times, json, random, strutils, tables +#SPDX-License-Identifier: AGPL-3.0-only +import asyncdispatch, times, json, random, strutils, tables, sets import types # max requests at a time per account to avoid race conditions @@ -19,14 +19,16 @@ proc getPoolJson*(): JsonNode = list = newJObject() totalReqs = 0 totalPending = 0 - totalLimited = 0 + limited: HashSet[string] reqsPerApi: Table[string, int] let now = epochTime().int for account in accountPool: totalPending.inc(account.pending) - list[account.id] = %*{ + + var includeAccount = false + let accountJson = %*{ "apis": newJObject(), "pending": account.pending, } @@ -36,17 +38,18 @@ proc getPoolJson*(): JsonNode = apiStatus = account.apis[api] obj = %*{} - if apiStatus.limited: - obj["limited"] = %true - inc totalLimited - if apiStatus.reset > now.int: obj["remaining"] = %apiStatus.remaining if "remaining" notin obj and not apiStatus.limited: continue - list[account.id]["apis"][$api] = obj + if apiStatus.limited: + obj["limited"] = %true + limited.incl account.id + + accountJson{"apis", $api} = obj + includeAccount = true let maxReqs = @@ -64,9 +67,12 @@ proc getPoolJson*(): JsonNode = reqsPerApi[$api] = reqsPerApi.getOrDefault($api, 0) + reqs totalReqs.inc(reqs) + if includeAccount: + list[account.id] = accountJson + return %*{ "amount": accountPool.len, - "limited": totalLimited, + "limited": limited.card, "requests": totalReqs, "pending": totalPending, "apis": reqsPerApi, From 986b91ac733b36949bd9ac13c78409c4a98f83a0 Mon Sep 17 00:00:00 2001 From: Zed Date: Tue, 29 Aug 2023 23:58:03 +0200 Subject: [PATCH 15/21] Handle ProtocolError and BadClientError equally --- src/http_pool.nim | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/http_pool.nim b/src/http_pool.nim index b4e3cee..664e9a6 100644 --- a/src/http_pool.nim +++ b/src/http_pool.nim @@ -39,11 +39,8 @@ template use*(pool: HttpPool; heads: HttpHeaders; body: untyped): untyped = try: body - except ProtocolError: - # Twitter closed the connection, retry - body - except BadClientError: - # Twitter returned 503, we need a new client + except BadClientError, ProtocolError: + # Twitter returned 503 or closed the connection, we need a new client pool.release(c, true) badClient = false c = pool.acquire(heads) From 898b19b92f3121b2185ba3a59381be2ea8299f44 Mon Sep 17 00:00:00 2001 From: Zed Date: Wed, 30 Aug 2023 03:04:22 +0200 Subject: [PATCH 16/21] Improve rate limit handling, minor refactor --- src/apiutils.nim | 67 ++++++++++++++++++++---------------------------- src/tokens.nim | 28 ++++++++++++-------- src/types.nim | 2 +- 3 files changed, 47 insertions(+), 50 deletions(-) diff --git a/src/apiutils.nim b/src/apiutils.nim index 453b36a..37afded 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -61,13 +61,6 @@ proc genHeaders*(url, oauthToken, oauthTokenSecret: string): HttpHeaders = "DNT": "1" }) -template updateAccount() = - if resp.headers.hasKey(rlRemaining): - let - remaining = parseInt(resp.headers[rlRemaining]) - reset = parseInt(resp.headers[rlReset]) - account.setRateLimit(api, remaining, reset) - template fetchImpl(result, fetchBody) {.dirty.} = once: pool = HttpPool() @@ -89,28 +82,46 @@ template fetchImpl(result, fetchBody) {.dirty.} = badClient = true raise newException(BadClientError, "Bad client") + if resp.headers.hasKey(rlRemaining): + let + remaining = parseInt(resp.headers[rlRemaining]) + reset = parseInt(resp.headers[rlReset]) + account.setRateLimit(api, remaining, reset) + if result.len > 0: if resp.headers.getOrDefault("content-encoding") == "gzip": result = uncompress(result, dfGzip) - else: - echo "non-gzip body, url: ", url, ", body: ", result + + if result.startsWith("{\"errors"): + let errors = result.fromJson(Errors) + if errors in {expiredToken, badToken}: + echo "fetch error: ", errors + invalidate(account) + raise rateLimitError() + elif errors in {rateLimited}: + # rate limit hit, resets after 24 hours + setLimited(account, api) + raise rateLimitError() + elif result.startsWith("429 Too Many Requests"): + account.apis[api].remaining = 0 + # rate limit hit, resets after the 15 minute window + raise rateLimitError() fetchBody - release(account, used=true) - if resp.status == $Http400: raise newException(InternalError, $url) except InternalError as e: raise e except BadClientError as e: - release(account, used=true) + raise e + except OSError as e: raise e except Exception as e: echo "error: ", e.name, ", msg: ", e.msg, ", accountId: ", account.id, ", url: ", url - if "length" notin e.msg and "descriptor" notin e.msg: - release(account, invalid=true) raise rateLimitError() + finally: + release(account) proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} = var body: string @@ -121,36 +132,14 @@ proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} = echo resp.status, ": ", body, " --- url: ", url result = newJNull() - updateAccount() - let error = result.getError - if error in {invalidToken, badToken}: - echo "fetch error: ", result.getError - release(account, invalid=true) + if error in {expiredToken, badToken}: + echo "fetchBody error: ", error + invalidate(account) raise rateLimitError() - if body.startsWith("{\"errors"): - let errors = body.fromJson(Errors) - if errors in {invalidToken, badToken}: - echo "fetch error: ", errors - release(account, invalid=true) - raise rateLimitError() - elif errors in {rateLimited}: - account.apis[api].limited = true - account.apis[api].limitedAt = epochTime().int - echo "[accounts] rate limited, api: ", api, ", reqs left: ", account.apis[api].remaining, ", id: ", account.id - proc fetchRaw*(url: Uri; api: Api): Future[string] {.async.} = fetchImpl result: if not (result.startsWith('{') or result.startsWith('[')): echo resp.status, ": ", result, " --- url: ", url result.setLen(0) - - updateAccount() - - if result.startsWith("{\"errors"): - let errors = result.fromJson(Errors) - if errors in {invalidToken, badToken}: - echo "fetch error: ", errors - release(account, invalid=true) - raise rateLimitError() diff --git a/src/tokens.nim b/src/tokens.nim index a3af9bf..c620bc7 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -11,7 +11,7 @@ var accountPool: seq[GuestAccount] enableLogging = false -template log(str) = +template log(str: varargs[string, `$`]) = if enableLogging: echo "[accounts] ", str proc getPoolJson*(): JsonNode = @@ -91,7 +91,7 @@ proc isLimited(account: GuestAccount; api: Api): bool = if limit.limited and (epochTime().int - limit.limitedAt) > dayInSeconds: account.apis[api].limited = false - log "resetting limit, api: " & $api & ", id: " & $account.id + log "resetting limit, api: ", api, ", id: ", account.id return limit.limited or (limit.remaining <= 10 and limit.reset > epochTime().int) else: @@ -100,15 +100,18 @@ proc isLimited(account: GuestAccount; api: Api): bool = proc isReady(account: GuestAccount; api: Api): bool = not (account.isNil or account.pending > maxConcurrentReqs or account.isLimited(api)) -proc release*(account: GuestAccount; used=false; invalid=false) = +proc invalidate*(account: var GuestAccount) = if account.isNil: return - if invalid: - log "discarding invalid account: " & account.id + log "invalidating expired account: ", account.id - let idx = accountPool.find(account) - if idx > -1: accountPool.delete(idx) - elif used: - dec account.pending + # TODO: This isn't sufficient, but it works for now + let idx = accountPool.find(account) + if idx > -1: accountPool.delete(idx) + account = nil + +proc release*(account: GuestAccount; invalid=false) = + if account.isNil: return + dec account.pending proc getGuestAccount*(api: Api): Future[GuestAccount] {.async.} = for i in 0 ..< accountPool.len: @@ -119,9 +122,14 @@ proc getGuestAccount*(api: Api): Future[GuestAccount] {.async.} = if not result.isNil and result.isReady(api): inc result.pending else: - log "no accounts available for API: " & $api + log "no accounts available for API: ", api raise rateLimitError() +proc setLimited*(account: GuestAccount; api: Api) = + account.apis[api].limited = true + account.apis[api].limitedAt = epochTime().int + log "rate limited, api: ", api, ", reqs left: ", account.apis[api].remaining, ", id: ", account.id + proc setRateLimit*(account: GuestAccount; api: Api; remaining, reset: int) = # avoid undefined behavior in race conditions if api in account.apis: diff --git a/src/types.nim b/src/types.nim index fcb24c0..8a7a66e 100644 --- a/src/types.nim +++ b/src/types.nim @@ -56,7 +56,7 @@ type userNotFound = 50 suspended = 63 rateLimited = 88 - invalidToken = 89 + expiredToken = 89 listIdOrSlug = 112 tweetNotFound = 144 tweetNotAuthorized = 179 From 37b58a5a7e52b897c565677016c8d7bf56494bc4 Mon Sep 17 00:00:00 2001 From: Zed Date: Wed, 30 Aug 2023 03:43:49 +0200 Subject: [PATCH 17/21] Fix accounts logging --- src/tokens.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tokens.nim b/src/tokens.nim index c620bc7..3628ef6 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -12,7 +12,7 @@ var enableLogging = false template log(str: varargs[string, `$`]) = - if enableLogging: echo "[accounts] ", str + if enableLogging: echo "[accounts] ", str.join("") proc getPoolJson*(): JsonNode = var From 282ce8b0e9088a0a215116ce234c0b46f1e0d55f Mon Sep 17 00:00:00 2001 From: Zed Date: Thu, 31 Aug 2023 01:29:54 +0200 Subject: [PATCH 18/21] Add 429 logging --- src/apiutils.nim | 1 + src/tokens.nim | 2 +- src/types.nim | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/apiutils.nim b/src/apiutils.nim index 37afded..e5b9be2 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -103,6 +103,7 @@ template fetchImpl(result, fetchBody) {.dirty.} = setLimited(account, api) raise rateLimitError() elif result.startsWith("429 Too Many Requests"): + echo "[accounts] 429 error, API: ", api, ", account: ", account.id account.apis[api].remaining = 0 # rate limit hit, resets after the 15 minute window raise rateLimitError() diff --git a/src/tokens.nim b/src/tokens.nim index 3628ef6..a4ebe7f 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -109,7 +109,7 @@ proc invalidate*(account: var GuestAccount) = if idx > -1: accountPool.delete(idx) account = nil -proc release*(account: GuestAccount; invalid=false) = +proc release*(account: GuestAccount) = if account.isNil: return dec account.pending diff --git a/src/types.nim b/src/types.nim index 8a7a66e..4cacc4b 100644 --- a/src/types.nim +++ b/src/types.nim @@ -40,8 +40,6 @@ type id*: string oauthToken*: string oauthSecret*: string - # init*: Time - lastUse*: Time pending*: int apis*: Table[Api, RateLimit] From 82beb5da8c60a981f0ce61fc99c8f23dff3b3865 Mon Sep 17 00:00:00 2001 From: Zed Date: Thu, 31 Aug 2023 01:31:27 +0200 Subject: [PATCH 19/21] Add empty oauth token logging --- src/apiutils.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apiutils.nim b/src/apiutils.nim index e5b9be2..0b1db26 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -67,6 +67,7 @@ template fetchImpl(result, fetchBody) {.dirty.} = var account = await getGuestAccount(api) if account.oauthToken.len == 0: + echo "[accounts] Empty oauth token, account: ", account.id raise rateLimitError() try: From 84dcf4907907fc59a60e7bf2a7db738789e3e868 Mon Sep 17 00:00:00 2001 From: Zed Date: Thu, 31 Aug 2023 05:06:47 +0200 Subject: [PATCH 20/21] Fix negative pending requests bug --- src/tokens.nim | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tokens.nim b/src/tokens.nim index a4ebe7f..3e20597 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -116,7 +116,6 @@ proc release*(account: GuestAccount) = proc getGuestAccount*(api: Api): Future[GuestAccount] {.async.} = for i in 0 ..< accountPool.len: if result.isReady(api): break - release(result) result = accountPool.sample() if not result.isNil and result.isReady(api): From b8fe212e941ebb27a39b0900cfe271d700738f22 Mon Sep 17 00:00:00 2001 From: Zed Date: Fri, 1 Sep 2023 21:37:34 +0200 Subject: [PATCH 21/21] Add media proxying error logging --- src/routes/media.nim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/media.nim b/src/routes/media.nim index e63a0f8..d335c97 100644 --- a/src/routes/media.nim +++ b/src/routes/media.nim @@ -37,6 +37,7 @@ proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} = try: let res = await client.get(url) if res.status != "200 OK": + echo "[media] Proxying media failed, status: $1, url: $2, body: $3" % [res.status, url, await res.body] return Http404 let hashed = $hash(url) @@ -65,6 +66,7 @@ proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} = await request.client.send(data) data.setLen 0 except HttpRequestError, ProtocolError, OSError: + echo "[media] Proxying media exception, error: $1, url: $2" % [getCurrentExceptionMsg(), url] result = Http404 finally: client.close()