apiutils.nim 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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, tokens, 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 genParams*(pars: openArray[(string, string)] = @[]; cursor="";
  11. count="20"; ext=true): seq[(string, string)] =
  12. result = timelineParams
  13. for p in pars:
  14. result &= p
  15. if ext:
  16. result &= ("include_ext_alt_text", "1")
  17. result &= ("include_ext_media_stats", "1")
  18. result &= ("include_ext_media_availability", "1")
  19. if count.len > 0:
  20. result &= ("count", count)
  21. if cursor.len > 0:
  22. # The raw cursor often has plus signs, which sometimes get turned into spaces,
  23. # so we need to turn them back into a plus
  24. if " " in cursor:
  25. result &= ("cursor", cursor.replace(" ", "+"))
  26. else:
  27. result &= ("cursor", cursor)
  28. proc getOauthHeader(url, oauthToken, oauthTokenSecret: string): string =
  29. let
  30. encodedUrl = url.replace(",", "%2C").replace("+", "%20")
  31. params = OAuth1Parameters(
  32. consumerKey: consumerKey,
  33. signatureMethod: "HMAC-SHA1",
  34. timestamp: $int(round(epochTime())),
  35. nonce: "0",
  36. isIncludeVersionToHeader: true,
  37. token: oauthToken
  38. )
  39. signature = getSignature(HttpGet, encodedUrl, "", params, consumerSecret, oauthTokenSecret)
  40. params.signature = percentEncode(signature)
  41. return getOauth1RequestHeader(params)["authorization"]
  42. proc genHeaders*(url, oauthToken, oauthTokenSecret: string): HttpHeaders =
  43. let header = getOauthHeader(url, oauthToken, oauthTokenSecret)
  44. result = newHttpHeaders({
  45. "connection": "keep-alive",
  46. "authorization": header,
  47. "content-type": "application/json",
  48. "x-twitter-active-user": "yes",
  49. "authority": "api.twitter.com",
  50. "accept-encoding": "gzip",
  51. "accept-language": "en-US,en;q=0.9",
  52. "accept": "*/*",
  53. "DNT": "1"
  54. })
  55. template fetchImpl(result, fetchBody) {.dirty.} =
  56. once:
  57. pool = HttpPool()
  58. var account = await getGuestAccount(api)
  59. if account.oauthToken.len == 0:
  60. echo "[accounts] Empty oauth token, account: ", account.id
  61. raise rateLimitError()
  62. try:
  63. var resp: AsyncResponse
  64. pool.use(genHeaders($url, account.oauthToken, account.oauthSecret)):
  65. template getContent =
  66. resp = await c.get($url)
  67. result = await resp.body
  68. getContent()
  69. if resp.status == $Http503:
  70. badClient = true
  71. raise newException(BadClientError, "Bad client")
  72. if resp.headers.hasKey(rlRemaining):
  73. let
  74. remaining = parseInt(resp.headers[rlRemaining])
  75. reset = parseInt(resp.headers[rlReset])
  76. account.setRateLimit(api, remaining, reset)
  77. if result.len > 0:
  78. if resp.headers.getOrDefault("content-encoding") == "gzip":
  79. result = uncompress(result, dfGzip)
  80. if result.startsWith("{\"errors"):
  81. let errors = result.fromJson(Errors)
  82. if errors in {expiredToken, badToken}:
  83. echo "fetch error: ", errors
  84. invalidate(account)
  85. raise rateLimitError()
  86. elif errors in {rateLimited}:
  87. # rate limit hit, resets after 24 hours
  88. setLimited(account, api)
  89. raise rateLimitError()
  90. elif result.startsWith("429 Too Many Requests"):
  91. echo "[accounts] 429 error, API: ", api, ", account: ", account.id
  92. account.apis[api].remaining = 0
  93. # rate limit hit, resets after the 15 minute window
  94. raise rateLimitError()
  95. fetchBody
  96. if resp.status == $Http400:
  97. raise newException(InternalError, $url)
  98. except InternalError as e:
  99. raise e
  100. except BadClientError as e:
  101. raise e
  102. except OSError as e:
  103. raise e
  104. except Exception as e:
  105. echo "error: ", e.name, ", msg: ", e.msg, ", accountId: ", account.id, ", url: ", url
  106. raise rateLimitError()
  107. finally:
  108. release(account)
  109. proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} =
  110. var body: string
  111. fetchImpl body:
  112. if body.startsWith('{') or body.startsWith('['):
  113. result = parseJson(body)
  114. else:
  115. echo resp.status, ": ", body, " --- url: ", url
  116. result = newJNull()
  117. let error = result.getError
  118. if error in {expiredToken, badToken}:
  119. echo "fetchBody error: ", error
  120. invalidate(account)
  121. raise rateLimitError()
  122. proc fetchRaw*(url: Uri; api: Api): Future[string] {.async.} =
  123. fetchImpl result:
  124. if not (result.startsWith('{') or result.startsWith('[')):
  125. echo resp.status, ": ", result, " --- url: ", url
  126. result.setLen(0)