tokens.nim 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. # SPDX-License-Identifier: AGPL-3.0-only
  2. import asyncdispatch, httpclient, times, sequtils, json, random
  3. import strutils, tables
  4. import types, consts
  5. const
  6. maxConcurrentReqs = 5 # max requests at a time per token, to avoid race conditions
  7. maxLastUse = 1.hours # if a token is unused for 60 minutes, it expires
  8. maxAge = 2.hours + 55.minutes # tokens expire after 3 hours
  9. failDelay = initDuration(minutes=30)
  10. var
  11. tokenPool: seq[Token]
  12. lastFailed: Time
  13. enableLogging = false
  14. let headers = newHttpHeaders({"authorization": auth})
  15. template log(str) =
  16. if enableLogging: echo "[tokens] ", str
  17. proc getPoolJson*(): JsonNode =
  18. var
  19. list = newJObject()
  20. totalReqs = 0
  21. totalPending = 0
  22. reqsPerApi: Table[string, int]
  23. for token in tokenPool:
  24. totalPending.inc(token.pending)
  25. list[token.tok] = %*{
  26. "apis": newJObject(),
  27. "pending": token.pending,
  28. "init": $token.init,
  29. "lastUse": $token.lastUse
  30. }
  31. for api in token.apis.keys:
  32. list[token.tok]["apis"][$api] = %token.apis[api]
  33. let
  34. maxReqs =
  35. case api
  36. of Api.timeline: 187
  37. of Api.listMembers, Api.listBySlug, Api.list, Api.listTweets,
  38. Api.userTweets, Api.userTweetsAndReplies, Api.userMedia,
  39. Api.userRestId, Api.userScreenName,
  40. Api.tweetDetail, Api.tweetResult, Api.search: 500
  41. of Api.userSearch: 900
  42. reqs = maxReqs - token.apis[api].remaining
  43. reqsPerApi[$api] = reqsPerApi.getOrDefault($api, 0) + reqs
  44. totalReqs.inc(reqs)
  45. return %*{
  46. "amount": tokenPool.len,
  47. "requests": totalReqs,
  48. "pending": totalPending,
  49. "apis": reqsPerApi,
  50. "tokens": list
  51. }
  52. proc rateLimitError*(): ref RateLimitError =
  53. newException(RateLimitError, "rate limited")
  54. proc fetchToken(): Future[Token] {.async.} =
  55. if getTime() - lastFailed < failDelay:
  56. raise rateLimitError()
  57. let client = newAsyncHttpClient(headers=headers)
  58. try:
  59. let
  60. resp = await client.postContent(activate)
  61. tokNode = parseJson(resp)["guest_token"]
  62. tok = tokNode.getStr($(tokNode.getInt))
  63. time = getTime()
  64. return Token(tok: tok, init: time, lastUse: time)
  65. except Exception as e:
  66. echo "[tokens] fetching token failed: ", e.msg
  67. if "Try again" notin e.msg:
  68. echo "[tokens] fetching tokens paused, resuming in 30 minutes"
  69. lastFailed = getTime()
  70. finally:
  71. client.close()
  72. proc expired(token: Token): bool =
  73. let time = getTime()
  74. token.init < time - maxAge or token.lastUse < time - maxLastUse
  75. proc isLimited(token: Token; api: Api): bool =
  76. if token.isNil or token.expired:
  77. return true
  78. if api in token.apis:
  79. let limit = token.apis[api]
  80. return (limit.remaining <= 10 and limit.reset > epochTime().int)
  81. else:
  82. return false
  83. proc isReady(token: Token; api: Api): bool =
  84. not (token.isNil or token.pending > maxConcurrentReqs or token.isLimited(api))
  85. proc release*(token: Token; used=false; invalid=false) =
  86. if token.isNil: return
  87. if invalid or token.expired:
  88. if invalid: log "discarding invalid token"
  89. elif token.expired: log "discarding expired token"
  90. let idx = tokenPool.find(token)
  91. if idx > -1: tokenPool.delete(idx)
  92. elif used:
  93. dec token.pending
  94. token.lastUse = getTime()
  95. proc getToken*(api: Api): Future[Token] {.async.} =
  96. for i in 0 ..< tokenPool.len:
  97. if result.isReady(api): break
  98. release(result)
  99. result = tokenPool.sample()
  100. if not result.isReady(api):
  101. release(result)
  102. result = await fetchToken()
  103. log "added new token to pool"
  104. tokenPool.add result
  105. if not result.isNil:
  106. inc result.pending
  107. else:
  108. raise rateLimitError()
  109. proc setRateLimit*(token: Token; api: Api; remaining, reset: int) =
  110. # avoid undefined behavior in race conditions
  111. if api in token.apis:
  112. let limit = token.apis[api]
  113. if limit.reset >= reset and limit.remaining < remaining:
  114. return
  115. token.apis[api] = RateLimit(remaining: remaining, reset: reset)
  116. proc poolTokens*(amount: int) {.async.} =
  117. var futs: seq[Future[Token]]
  118. for i in 0 ..< amount:
  119. futs.add fetchToken()
  120. for token in futs:
  121. var newToken: Token
  122. try: newToken = await token
  123. except: discard
  124. if not newToken.isNil:
  125. log "added new token to pool"
  126. tokenPool.add newToken
  127. proc initTokenPool*(cfg: Config) {.async.} =
  128. enableLogging = cfg.enableDebug
  129. while true:
  130. if tokenPool.countIt(not it.isLimited(Api.timeline)) < cfg.minTokens:
  131. await poolTokens(min(4, cfg.minTokens - tokenPool.len))
  132. await sleepAsync(2000)