tokens.nim 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. # SPDX-License-Identifier: AGPL-3.0-only
  2. import asyncdispatch, times, json, random, strutils, tables
  3. import types
  4. # max requests at a time per account to avoid race conditions
  5. const
  6. maxConcurrentReqs = 5
  7. dayInSeconds = 24 * 60 * 60
  8. var
  9. accountPool: seq[GuestAccount]
  10. enableLogging = false
  11. template log(str) =
  12. if enableLogging: echo "[accounts] ", str
  13. proc getPoolJson*(): JsonNode =
  14. var
  15. list = newJObject()
  16. totalReqs = 0
  17. totalPending = 0
  18. reqsPerApi: Table[string, int]
  19. let now = epochTime().int
  20. for account in accountPool:
  21. totalPending.inc(account.pending)
  22. list[account.id] = %*{
  23. "apis": newJObject(),
  24. "pending": account.pending,
  25. }
  26. for api in account.apis.keys:
  27. let obj = %*{}
  28. if account.apis[api].limited:
  29. obj["limited"] = %true
  30. if account.apis[api].reset > now.int:
  31. obj["remaining"] = %account.apis[api].remaining
  32. list[account.id]["apis"][$api] = obj
  33. if "remaining" notin obj:
  34. continue
  35. let
  36. maxReqs =
  37. case api
  38. of Api.search: 50
  39. of Api.photoRail: 180
  40. of Api.userTweets, Api.userTweetsAndReplies, Api.userMedia,
  41. Api.userRestId, Api.userScreenName,
  42. Api.tweetDetail, Api.tweetResult,
  43. Api.list, Api.listTweets, Api.listMembers, Api.listBySlug: 500
  44. reqs = maxReqs - account.apis[api].remaining
  45. reqsPerApi[$api] = reqsPerApi.getOrDefault($api, 0) + reqs
  46. totalReqs.inc(reqs)
  47. return %*{
  48. "amount": accountPool.len,
  49. "requests": totalReqs,
  50. "pending": totalPending,
  51. "apis": reqsPerApi,
  52. "accounts": list
  53. }
  54. proc rateLimitError*(): ref RateLimitError =
  55. newException(RateLimitError, "rate limited")
  56. proc isLimited(account: GuestAccount; api: Api): bool =
  57. if account.isNil:
  58. return true
  59. if api in account.apis:
  60. let limit = account.apis[api]
  61. if limit.limited and (epochTime().int - limit.limitedAt) > dayInSeconds:
  62. account.apis[api].limited = false
  63. log "resetting limit, api: " & $api & ", id: " & $account.id
  64. return limit.limited or (limit.remaining <= 10 and limit.reset > epochTime().int)
  65. else:
  66. return false
  67. proc isReady(account: GuestAccount; api: Api): bool =
  68. not (account.isNil or account.pending > maxConcurrentReqs or account.isLimited(api))
  69. proc release*(account: GuestAccount; used=false; invalid=false) =
  70. if account.isNil: return
  71. if invalid:
  72. log "discarding invalid account: " & account.id
  73. let idx = accountPool.find(account)
  74. if idx > -1: accountPool.delete(idx)
  75. elif used:
  76. dec account.pending
  77. proc getGuestAccount*(api: Api): Future[GuestAccount] {.async.} =
  78. for i in 0 ..< accountPool.len:
  79. if result.isReady(api): break
  80. release(result)
  81. result = accountPool.sample()
  82. if not result.isNil and result.isReady(api):
  83. inc result.pending
  84. else:
  85. log "no accounts available for API: " & $api
  86. raise rateLimitError()
  87. proc setRateLimit*(account: GuestAccount; api: Api; remaining, reset: int) =
  88. # avoid undefined behavior in race conditions
  89. if api in account.apis:
  90. let limit = account.apis[api]
  91. if limit.reset >= reset and limit.remaining < remaining:
  92. return
  93. if limit.reset == reset and limit.remaining >= remaining:
  94. account.apis[api].remaining = remaining
  95. return
  96. account.apis[api] = RateLimit(remaining: remaining, reset: reset)
  97. proc initAccountPool*(cfg: Config; accounts: JsonNode) =
  98. enableLogging = cfg.enableDebug
  99. for account in accounts:
  100. accountPool.add GuestAccount(
  101. id: account{"user", "id_str"}.getStr,
  102. oauthToken: account{"oauth_token"}.getStr,
  103. oauthSecret: account{"oauth_token_secret"}.getStr,
  104. )