apiutils.nim 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  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
  5. import experimental/types/common
  6. const
  7. rlRemaining = "x-rate-limit-remaining"
  8. rlReset = "x-rate-limit-reset"
  9. var pool: HttpPool
  10. proc getOauthHeader(url, oauthToken, oauthTokenSecret: string): string =
  11. let
  12. encodedUrl = url.replace(",", "%2C").replace("+", "%20")
  13. params = OAuth1Parameters(
  14. consumerKey: consumerKey,
  15. signatureMethod: "HMAC-SHA1",
  16. timestamp: $int(round(epochTime())),
  17. nonce: "0",
  18. isIncludeVersionToHeader: true,
  19. token: oauthToken
  20. )
  21. signature = getSignature(HttpGet, encodedUrl, "", params, consumerSecret, oauthTokenSecret)
  22. params.signature = percentEncode(signature)
  23. return getOauth1RequestHeader(params)["authorization"]
  24. proc genHeaders*(url, oauthToken, oauthTokenSecret: string): HttpHeaders =
  25. let header = getOauthHeader(url, oauthToken, oauthTokenSecret)
  26. result = newHttpHeaders({
  27. "connection": "keep-alive",
  28. "authorization": header,
  29. "content-type": "application/json",
  30. "x-twitter-active-user": "yes",
  31. "authority": "api.x.com",
  32. "accept-encoding": "gzip",
  33. "accept-language": "en-US,en;q=0.9",
  34. "accept": "*/*",
  35. "DNT": "1"
  36. })
  37. template fetchImpl(result, fetchBody) {.dirty.} =
  38. once:
  39. pool = HttpPool()
  40. var session = await getSession(api)
  41. if session.oauthToken.len == 0:
  42. echo "[sessions] Empty oauth token, session: ", session.id
  43. raise rateLimitError()
  44. try:
  45. var resp: AsyncResponse
  46. pool.use(genHeaders($url, session.oauthToken, session.oauthSecret)):
  47. template getContent =
  48. resp = await c.get($url)
  49. result = await resp.body
  50. getContent()
  51. if resp.status == $Http503:
  52. badClient = true
  53. raise newException(BadClientError, "Bad client")
  54. if resp.headers.hasKey(rlRemaining):
  55. let
  56. remaining = parseInt(resp.headers[rlRemaining])
  57. reset = parseInt(resp.headers[rlReset])
  58. session.setRateLimit(api, remaining, reset)
  59. if result.len > 0:
  60. if resp.headers.getOrDefault("content-encoding") == "gzip":
  61. result = uncompress(result, dfGzip)
  62. if result.startsWith("{\"errors"):
  63. let errors = result.fromJson(Errors)
  64. echo "Fetch error, API: ", api, ", errors: ", errors
  65. if errors in {expiredToken, badToken, locked}:
  66. invalidate(session)
  67. raise rateLimitError()
  68. elif errors in {rateLimited}:
  69. # rate limit hit, resets after 24 hours
  70. setLimited(session, api)
  71. raise rateLimitError()
  72. elif result.startsWith("429 Too Many Requests"):
  73. echo "[sessions] 429 error, API: ", api, ", session: ", session.id
  74. session.apis[api].remaining = 0
  75. # rate limit hit, resets after the 15 minute window
  76. raise rateLimitError()
  77. fetchBody
  78. if resp.status == $Http400:
  79. echo "ERROR 400, ", api, ": ", result
  80. raise newException(InternalError, $url)
  81. except InternalError as e:
  82. raise e
  83. except BadClientError as e:
  84. raise e
  85. except OSError as e:
  86. raise e
  87. except Exception as e:
  88. let id = if session.isNil: "null" else: $session.id
  89. echo "error: ", e.name, ", msg: ", e.msg, ", sessionId: ", id, ", url: ", url
  90. raise rateLimitError()
  91. finally:
  92. release(session)
  93. template retry(bod) =
  94. try:
  95. bod
  96. except RateLimitError:
  97. echo "[sessions] Rate limited, retrying ", api, " request..."
  98. bod
  99. proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} =
  100. retry:
  101. var body: string
  102. fetchImpl body:
  103. if body.startsWith('{') or body.startsWith('['):
  104. result = parseJson(body)
  105. else:
  106. echo resp.status, ": ", body, " --- url: ", url
  107. result = newJNull()
  108. let error = result.getError
  109. if error != null:
  110. echo "Fetch error, API: ", api, ", error: ", error
  111. if error in {expiredToken, badToken, locked}:
  112. invalidate(session)
  113. raise rateLimitError()
  114. proc fetchRaw*(url: Uri; api: Api): Future[string] {.async.} =
  115. retry:
  116. fetchImpl result:
  117. if not (result.startsWith('{') or result.startsWith('[')):
  118. echo resp.status, ": ", result, " --- url: ", url
  119. result.setLen(0)