tokens.nim 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #i hate begging for this too em 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 = 2
  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. totalLimited = 0
  19. reqsPerApi: Table[string, int]
  20. let now = epochTime().int
  21. for account in accountPool:
  22. totalPending.inc(account.pending)
  23. list[account.id] = %*{
  24. "apis": newJObject(),
  25. "pending": account.pending,
  26. }
  27. for api in account.apis.keys:
  28. let
  29. apiStatus = account.apis[api]
  30. obj = %*{}
  31. if apiStatus.limited:
  32. obj["limited"] = %true
  33. inc totalLimited
  34. if apiStatus.reset > now.int:
  35. obj["remaining"] = %apiStatus.remaining
  36. if "remaining" notin obj and not apiStatus.limited:
  37. continue
  38. list[account.id]["apis"][$api] = obj
  39. let
  40. maxReqs =
  41. case api
  42. of Api.search: 50
  43. of Api.photoRail: 180
  44. of Api.userTweets, Api.userTweetsAndReplies, Api.userMedia,
  45. Api.userRestId, Api.userScreenName,
  46. Api.tweetResult, Api.tweetDetail,
  47. Api.list, Api.listTweets, Api.listMembers, Api.listBySlug: 500
  48. of Api.userSearch: 900
  49. reqs = maxReqs - apiStatus.remaining
  50. reqsPerApi[$api] = reqsPerApi.getOrDefault($api, 0) + reqs
  51. totalReqs.inc(reqs)
  52. return %*{
  53. "amount": accountPool.len,
  54. "limited": totalLimited,
  55. "requests": totalReqs,
  56. "pending": totalPending,
  57. "apis": reqsPerApi,
  58. "accounts": list
  59. }
  60. proc rateLimitError*(): ref RateLimitError =
  61. newException(RateLimitError, "rate limited")
  62. proc isLimited(account: GuestAccount; api: Api): bool =
  63. if account.isNil:
  64. return true
  65. if api in account.apis:
  66. let limit = account.apis[api]
  67. if limit.limited and (epochTime().int - limit.limitedAt) > dayInSeconds:
  68. account.apis[api].limited = false
  69. log "resetting limit, api: " & $api & ", id: " & $account.id
  70. return limit.limited or (limit.remaining <= 10 and limit.reset > epochTime().int)
  71. else:
  72. return false
  73. proc isReady(account: GuestAccount; api: Api): bool =
  74. not (account.isNil or account.pending > maxConcurrentReqs or account.isLimited(api))
  75. proc release*(account: GuestAccount; used=false; invalid=false) =
  76. if account.isNil: return
  77. if invalid:
  78. log "discarding invalid account: " & account.id
  79. let idx = accountPool.find(account)
  80. if idx > -1: accountPool.delete(idx)
  81. elif used:
  82. dec account.pending
  83. proc getGuestAccount*(api: Api): Future[GuestAccount] {.async.} =
  84. for i in 0 ..< accountPool.len:
  85. if result.isReady(api): break
  86. release(result)
  87. result = accountPool.sample()
  88. if not result.isNil and result.isReady(api):
  89. inc result.pending
  90. else:
  91. log "no accounts available for API: " & $api
  92. raise rateLimitError()
  93. proc setRateLimit*(account: GuestAccount; api: Api; remaining, reset: int) =
  94. # avoid undefined behavior in race conditions
  95. if api in account.apis:
  96. let limit = account.apis[api]
  97. if limit.reset >= reset and limit.remaining < remaining:
  98. return
  99. if limit.reset == reset and limit.remaining >= remaining:
  100. account.apis[api].remaining = remaining
  101. return
  102. account.apis[api] = RateLimit(remaining: remaining, reset: reset)
  103. proc initAccountPool*(cfg: Config; accounts: JsonNode) =
  104. enableLogging = cfg.enableDebug
  105. for account in accounts:
  106. accountPool.add GuestAccount(
  107. id: account{"user", "id_str"}.getStr,
  108. oauthToken: account{"oauth_token"}.getStr,
  109. oauthSecret: account{"oauth_token_secret"}.getStr,
  110. )