apiutils.nim 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. # SPDX-License-Identifier: AGPL-3.0-only
  2. import httpclient, asyncdispatch, options, strutils, uri, times, math, tables
  3. import jsony, packedjson, zippy, oauth1
  4. import types, auth, consts, parserutils, http_pool, tid
  5. import experimental/types/common
  6. const
  7. rlRemaining = "x-rate-limit-remaining"
  8. rlReset = "x-rate-limit-reset"
  9. rlLimit = "x-rate-limit-limit"
  10. errorsToSkip = {null, doesntExist, tweetNotFound, timeout, unauthorized, badRequest}
  11. var
  12. pool: HttpPool
  13. disableTid: bool
  14. proc setDisableTid*(disable: bool) =
  15. disableTid = disable
  16. proc toUrl(req: ApiReq; sessionKind: SessionKind): Uri =
  17. case sessionKind
  18. of oauth:
  19. let o = req.oauth
  20. parseUri("https://api.x.com/graphql") / o.endpoint ? o.params
  21. of cookie:
  22. let c = req.cookie
  23. parseUri("https://x.com/i/api/graphql") / c.endpoint ? c.params
  24. proc getOauthHeader(url, oauthToken, oauthTokenSecret: string): string =
  25. let
  26. encodedUrl = url.replace(",", "%2C").replace("+", "%20")
  27. params = OAuth1Parameters(
  28. consumerKey: consumerKey,
  29. signatureMethod: "HMAC-SHA1",
  30. timestamp: $int(round(epochTime())),
  31. nonce: "0",
  32. isIncludeVersionToHeader: true,
  33. token: oauthToken
  34. )
  35. signature = getSignature(HttpGet, encodedUrl, "", params, consumerSecret, oauthTokenSecret)
  36. params.signature = percentEncode(signature)
  37. return getOauth1RequestHeader(params)["authorization"]
  38. proc getCookieHeader(authToken, ct0: string): string =
  39. "auth_token=" & authToken & "; ct0=" & ct0
  40. proc genHeaders*(session: Session, url: Uri): Future[HttpHeaders] {.async.} =
  41. result = newHttpHeaders({
  42. "accept": "*/*",
  43. "accept-encoding": "gzip",
  44. "accept-language": "en-US,en;q=0.9",
  45. "connection": "keep-alive",
  46. "content-type": "application/json",
  47. "origin": "https://x.com",
  48. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
  49. "x-twitter-active-user": "yes",
  50. "x-twitter-client-language": "en"
  51. })
  52. case session.kind
  53. of SessionKind.oauth:
  54. result["authorization"] = getOauthHeader($url, session.oauthToken, session.oauthSecret)
  55. of SessionKind.cookie:
  56. result["x-twitter-auth-type"] = "OAuth2Session"
  57. result["x-csrf-token"] = session.ct0
  58. result["cookie"] = getCookieHeader(session.authToken, session.ct0)
  59. if disableTid:
  60. result["authorization"] = bearerToken2
  61. else:
  62. result["authorization"] = bearerToken
  63. result["x-client-transaction-id"] = await genTid(url.path)
  64. proc getAndValidateSession*(req: ApiReq): Future[Session] {.async.} =
  65. result = await getSession(req)
  66. case result.kind
  67. of SessionKind.oauth:
  68. if result.oauthToken.len == 0:
  69. echo "[sessions] Empty oauth token, session: ", result.pretty
  70. raise rateLimitError()
  71. of SessionKind.cookie:
  72. if result.authToken.len == 0 or result.ct0.len == 0:
  73. echo "[sessions] Empty cookie credentials, session: ", result.pretty
  74. raise rateLimitError()
  75. template fetchImpl(result, fetchBody) {.dirty.} =
  76. once:
  77. pool = HttpPool()
  78. try:
  79. var resp: AsyncResponse
  80. pool.use(await genHeaders(session, url)):
  81. template getContent =
  82. resp = await c.get($url)
  83. result = await resp.body
  84. getContent()
  85. if resp.status == $Http503:
  86. badClient = true
  87. raise newException(BadClientError, "Bad client")
  88. if resp.headers.hasKey(rlRemaining):
  89. let
  90. remaining = parseInt(resp.headers[rlRemaining])
  91. reset = parseInt(resp.headers[rlReset])
  92. limit = parseInt(resp.headers[rlLimit])
  93. session.setRateLimit(req, remaining, reset, limit)
  94. if result.len > 0:
  95. if resp.headers.getOrDefault("content-encoding") == "gzip":
  96. result = uncompress(result, dfGzip)
  97. if result.startsWith("{\"errors"):
  98. let errors = result.fromJson(Errors)
  99. if errors notin errorsToSkip:
  100. echo "Fetch error, API: ", url.path, ", errors: ", errors
  101. if errors in {expiredToken, badToken, locked}:
  102. invalidate(session)
  103. raise rateLimitError()
  104. elif errors in {rateLimited}:
  105. # rate limit hit, resets after 24 hours
  106. setLimited(session, req)
  107. raise rateLimitError()
  108. elif result.startsWith("429 Too Many Requests"):
  109. echo "[sessions] 429 error, API: ", url.path, ", session: ", session.pretty
  110. raise rateLimitError()
  111. fetchBody
  112. if resp.status == $Http400:
  113. echo "ERROR 400, ", url.path, ": ", result
  114. raise newException(InternalError, $url)
  115. except InternalError as e:
  116. raise e
  117. except BadClientError as e:
  118. raise e
  119. except OSError as e:
  120. raise e
  121. except Exception as e:
  122. let s = session.pretty
  123. echo "error: ", e.name, ", msg: ", e.msg, ", session: ", s, ", url: ", url
  124. raise rateLimitError()
  125. finally:
  126. release(session)
  127. template retry(bod) =
  128. try:
  129. bod
  130. except RateLimitError:
  131. echo "[sessions] Rate limited, retrying ", req.cookie.endpoint, " request..."
  132. bod
  133. proc fetch*(req: ApiReq): Future[JsonNode] {.async.} =
  134. retry:
  135. var
  136. body: string
  137. session = await getAndValidateSession(req)
  138. let url = req.toUrl(session.kind)
  139. fetchImpl body:
  140. if body.startsWith('{') or body.startsWith('['):
  141. result = parseJson(body)
  142. else:
  143. echo resp.status, ": ", body, " --- url: ", url
  144. result = newJNull()
  145. let error = result.getError
  146. if error != null and error notin errorsToSkip:
  147. echo "Fetch error, API: ", url.path, ", error: ", error
  148. if error in {expiredToken, badToken, locked}:
  149. invalidate(session)
  150. raise rateLimitError()
  151. proc fetchRaw*(req: ApiReq): Future[string] {.async.} =
  152. retry:
  153. var session = await getAndValidateSession(req)
  154. let url = req.toUrl(session.kind)
  155. fetchImpl result:
  156. if not (result.startsWith('{') or result.startsWith('[')):
  157. echo resp.status, ": ", result, " --- url: ", url
  158. result.setLen(0)