diff --git a/src/api.nim b/src/api.nim index ed19835..68e8eec 100644 --- a/src/api.nim +++ b/src/api.nim @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only import asyncdispatch, httpclient, strutils, sequtils, sugar import packedjson -import types, query, formatters, consts, apiutils, parser +import types, query, formatters, consts, apiutils, parser, utils import experimental/parser as newParser # Helper to generate params object for GraphQL requests @@ -146,7 +146,12 @@ proc getGraphEditHistory*(id: string): Future[EditHistory] {.async.} = result = parseGraphEditHistory(js, id) proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} = - let q = genQueryParam(query) + # workaround for #1372 + let maxId = + if not after.startsWith("maxid:"): "" + else: validateNumber(after[6..^1]) + + let q = genQueryParam(query, maxId) if q.len == 0 or q == emptyQuery: return Timeline(query: query, beginning: true) @@ -160,9 +165,9 @@ proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} = "withReactionsMetadata": false, "withReactionsPerspective": false } - if after.len > 0: + if after.len > 0 and maxId.len == 0: variables["cursor"] = % after - let + let url = apiReq(graphSearchTimeline, $variables) js = await fetch(url) result = parseGraphSearch[Tweets](js, after) @@ -170,7 +175,7 @@ proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} = # when no more items are available the API just returns the last page in # full. this detects that and clears the page instead. - if after.len > 0 and result.bottom.len > 0 and + if after.len > 0 and result.bottom.len > 0 and maxId.len == 0 and after[0..<64] == result.bottom[0..<64]: result.content.setLen(0) diff --git a/src/query.nim b/src/query.nim index 00bd08f..4d2ef78 100644 --- a/src/query.nim +++ b/src/query.nim @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only import strutils, strformat, sequtils, tables, uri -import types +import types, utils const validFilters* = @[ @@ -17,11 +17,6 @@ template `@`(param: string): untyped = if param in pms: pms[param] else: "" -proc validateNumber(value: string): string = - if value.anyIt(not it.isDigit): - return "" - return value - proc initQuery*(pms: Table[string, string]; name=""): Query = result = Query( kind: parseEnum[QueryKind](@"f", tweets), @@ -50,7 +45,7 @@ proc getReplyQuery*(name: string): Query = fromUser: @[name] ) -proc genQueryParam*(query: Query): string = +proc genQueryParam*(query: Query; maxId=""): string = var filters: seq[string] param: string @@ -58,12 +53,15 @@ proc genQueryParam*(query: Query): string = if query.kind == users: return query.text - param = "(" for i, user in query.fromUser: + if i == 0: + param = "(" + param &= &"from:{user}" if i < query.fromUser.high: param &= " OR " - param &= ")" + else: + param &= ")" if query.fromUser.len > 0 and query.kind in {posts, media}: param &= " (filter:self_threads OR -filter:replies)" @@ -86,7 +84,7 @@ proc genQueryParam*(query: Query): string = if query.since.len > 0: result &= " since:" & query.since - if query.until.len > 0: + if query.until.len > 0 and maxId.len == 0: result &= " until:" & query.until if query.minLikes.len > 0: result &= " min_faves:" & query.minLikes @@ -96,6 +94,9 @@ proc genQueryParam*(query: Query): string = else: result = query.text + if result.len > 0 and maxId.len > 0: + result &= " max_id:" & maxId + proc genQueryUrl*(query: Query): string = if query.kind notin {tweets, users}: return diff --git a/src/utils.nim b/src/utils.nim index 667299c..6bd977e 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -1,5 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat, uri, tables, base64 +import sequtils, strutils, strformat, uri, tables, base64 import nimcrypto var @@ -59,3 +59,8 @@ proc isTwitterUrl*(uri: Uri): bool = proc isTwitterUrl*(url: string): bool = isTwitterUrl(parseUri(url)) + +proc validateNumber*(value: string): string = + if value.anyIt(not it.isDigit): + return "" + return value diff --git a/src/views/timeline.nim b/src/views/timeline.nim index 317b7e3..2d7537f 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -11,6 +11,23 @@ proc getQuery(query: Query): string = if result.len > 0: result &= "&" +proc getSearchMaxId(results: Timeline; path: string): string = + if results.query.kind != tweets or results.content.len == 0 or + results.query.until.len == 0: + return + + let lastThread = results.content[^1] + if lastThread.len == 0 or lastThread[^1].id == 0: + return + + # 2000000 is the minimum decrement to guarantee no result overlap + var maxId = lastThread[^1].id - 2_000_000'i64 + if maxId <= 0: + maxId = lastThread[^1].id - 1 + + if maxId > 0: + return "maxid:" & $maxId + proc renderToTop*(focus="#"): VNode = buildHtml(tdiv(class="top-ref")): icon "down", href=focus @@ -122,6 +139,9 @@ proc renderTimelineTweets*(results: Timeline; prefs: Prefs; path: string; else: renderThread(thread, prefs, path) - if results.bottom.len > 0: + var cursor = getSearchMaxId(results, path) + if cursor.len > 0: + renderMore(results.query, cursor) + elif results.bottom.len > 0: renderMore(results.query, results.bottom) renderToTop()