tokens.nim 4.1 KB

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