# SPDX-License-Identifier: AGPL-3.0-only import httpclient, asyncdispatch, options, strutils, uri, times, tables import jsony, packedjson, zippy import types, tokens, consts, parserutils, http_pool import experimental/types/common import config const rlRemaining = "x-rate-limit-remaining" rlReset = "x-rate-limit-reset" var pool: HttpPool proc genParams*(pars: openArray[(string, string)] = @[]; cursor=""; count="20"; ext=true): seq[(string, string)] = result = timelineParams for p in pars: result &= p if ext: 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) if cursor.len > 0: # The raw cursor often has plus signs, which sometimes get turned into spaces, # so we need to turn them back into a plus if " " in cursor: result &= ("cursor", cursor.replace(" ", "+")) else: result &= ("cursor", cursor) #proc genHeaders*(token: Token = nil): HttpHeaders = proc genHeaders*(): HttpHeaders = result = newHttpHeaders({ "connection": "keep-alive", "authorization": auth, "content-type": "application/json", "accept-encoding": "gzip", "accept-language": "en-US,en;q=0.5", "accept": "*/*", "DNT": "1", "Host": "x.com", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Sec-GPC": "1", "TE": "trailers" }) #template updateToken() = # if resp.headers.hasKey(rlRemaining): # let # remaining = parseInt(resp.headers[rlRemaining]) # reset = parseInt(resp.headers[rlReset]) # token.setRateLimit(api, remaining, reset) template fetchImpl(result, additional_headers, fetchBody) {.dirty.} = once: pool = HttpPool() #var token = await getToken(api) #if token.tok.len == 0: # raise rateLimitError() if len(cfg.cookieHeader) != 0: additional_headers.add("Cookie", cfg.cookieHeader) additional_headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0") if len(cfg.xCsrfToken) != 0: additional_headers.add("x-csrf-token", cfg.xCsrfToken) additional_headers.add("x-twitter-active-user", "yes") additional_headers.add("x-twitter-auth-type", "OAuth2Session") additional_headers.add("x-twitter-client-language", "en") try: var resp: AsyncResponse #var headers = genHeaders(token) var headers = genHeaders() for key, value in additional_headers.pairs(): headers.add(key, value) pool.use(headers): template getContent = resp = await c.get($url) result = await resp.body getContent() if resp.status == $Http429: raise rateLimitError() if resp.status == $Http503: badClient = true raise newException(BadClientError, "Bad client") if resp.headers.hasKey(rlRemaining): let remaining = parseInt(resp.headers[rlRemaining]) reset = parseInt(resp.headers[rlReset]) #token.setRateLimit(api, remaining, reset) if result.len > 0: if resp.headers.getOrDefault("content-encoding") == "gzip": result = uncompress(result, dfGzip) if result.startsWith("{\"errors"): let errors = result.fromJson(Errors) if errors in {expiredToken, badToken, authorizationError}: echo "fetch error: ", errors #release(token, invalid=true) 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"): #echo "[accounts] 429 error, API: ", api, ", token: ", token[] #account.apis[api].remaining = 0 # rate limit hit, resets after the 15 minute window raise rateLimitError() fetchBody #release(token, used=true) if resp.status == $Http400: let errText = "body: '" & result & "' url: " & $url raise newException(InternalError, errText) except InternalError as e: raise e except BadClientError as e: #release(token, used=true) raise e except OSError as e: raise e except ProtocolError as e: raise e except Exception as e: echo "error: ", e.name, ", msg: ", e.msg, ", url: ", url #if "length" notin e.msg and "descriptor" notin e.msg: #release(token, invalid=true) raise rateLimitError() template retry(bod) = try: bod except ProtocolError: bod proc fetch*(url: Uri; api: Api; additional_headers: HttpHeaders = newHttpHeaders()): Future[JsonNode] {.async.} = retry: var body: string fetchImpl(body, additional_headers): if body.startsWith('{') or body.startsWith('['): result = parseJson(body) else: echo resp.status, ": ", body, " --- url: ", url result = newJNull() #updateToken() let error = result.getError if error in {expiredToken, badToken}: echo "fetch error: ", result.getError #release(token, invalid=true) raise rateLimitError() proc fetchRaw*(url: Uri; api: Api; additional_headers: HttpHeaders = newHttpHeaders()): Future[string] {.async.} = retry: fetchImpl(result, additional_headers): if not (result.startsWith('{') or result.startsWith('[')): echo resp.status, ": ", result, " --- url: ", url result.setLen(0) #updateToken() if result.startsWith("{\"errors"): let errors = result.fromJson(Errors) if errors in {expiredToken, badToken}: echo "fetch error: ", errors #release(token, invalid=true) raise rateLimitError()