auth.nim 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. #SPDX-License-Identifier: AGPL-3.0-only
  2. import std/[asyncdispatch, times, json, random, strutils, tables, packedsets, os]
  3. import types
  4. import experimental/parser/guestaccount
  5. # max requests at a time per account to avoid race conditions
  6. const
  7. maxConcurrentReqs = 2
  8. dayInSeconds = 24 * 60 * 60
  9. apiMaxReqs: Table[Api, int] = {
  10. Api.search: 50,
  11. Api.tweetDetail: 150,
  12. Api.photoRail: 180,
  13. Api.userTweets: 500,
  14. Api.userTweetsAndReplies: 500,
  15. Api.userMedia: 500,
  16. Api.userRestId: 500,
  17. Api.userScreenName: 500,
  18. Api.tweetResult: 500,
  19. Api.list: 500,
  20. Api.listTweets: 500,
  21. Api.listMembers: 500,
  22. Api.listBySlug: 500
  23. }.toTable
  24. var
  25. accountPool: seq[GuestAccount]
  26. enableLogging = false
  27. template log(str: varargs[string, `$`]) =
  28. if enableLogging: echo "[accounts] ", str.join("")
  29. proc getAccountPoolHealth*(): JsonNode =
  30. let now = epochTime().int
  31. var
  32. totalReqs = 0
  33. limited: PackedSet[BiggestInt]
  34. reqsPerApi: Table[string, int]
  35. oldest = now.int64
  36. newest = 0'i64
  37. average = 0'i64
  38. for account in accountPool:
  39. # Twitter snowflake conversion
  40. let created = int64(((account.id shr 22) + 1288834974657) div 1000)
  41. if created > newest:
  42. newest = created
  43. if created < oldest:
  44. oldest = created
  45. average += created
  46. for api in account.apis.keys:
  47. let
  48. apiStatus = account.apis[api]
  49. reqs = apiMaxReqs[api] - apiStatus.remaining
  50. reqsPerApi.mgetOrPut($api, 0).inc reqs
  51. totalReqs.inc reqs
  52. if apiStatus.limited:
  53. limited.incl account.id
  54. if accountPool.len > 0:
  55. average = average div accountPool.len
  56. else:
  57. oldest = 0
  58. average = 0
  59. return %*{
  60. "accounts": %*{
  61. "total": accountPool.len,
  62. "active": accountPool.len - limited.card,
  63. "limited": limited.card,
  64. "oldest": $fromUnix(oldest),
  65. "newest": $fromUnix(newest),
  66. "average": $fromUnix(average)
  67. },
  68. "requests": %*{
  69. "total": totalReqs,
  70. "apis": reqsPerApi
  71. }
  72. }
  73. proc getAccountPoolDebug*(): JsonNode =
  74. let now = epochTime().int
  75. var list = newJObject()
  76. for account in accountPool:
  77. let accountJson = %*{
  78. "apis": newJObject(),
  79. "pending": account.pending,
  80. }
  81. for api in account.apis.keys:
  82. let
  83. apiStatus = account.apis[api]
  84. obj = %*{}
  85. if apiStatus.reset > now.int:
  86. obj["remaining"] = %apiStatus.remaining
  87. if "remaining" notin obj and not apiStatus.limited:
  88. continue
  89. if apiStatus.limited:
  90. obj["limited"] = %true
  91. accountJson{"apis", $api} = obj
  92. list[$account.id] = accountJson
  93. return %list
  94. proc rateLimitError*(): ref RateLimitError =
  95. newException(RateLimitError, "rate limited")
  96. proc isLimited(account: GuestAccount; api: Api): bool =
  97. if account.isNil:
  98. return true
  99. if api in account.apis:
  100. let limit = account.apis[api]
  101. if limit.limited and (epochTime().int - limit.limitedAt) > dayInSeconds:
  102. account.apis[api].limited = false
  103. log "resetting limit, api: ", api, ", id: ", account.id
  104. return limit.limited or (limit.remaining <= 10 and limit.reset > epochTime().int)
  105. else:
  106. return false
  107. proc isReady(account: GuestAccount; api: Api): bool =
  108. not (account.isNil or account.pending > maxConcurrentReqs or account.isLimited(api))
  109. proc invalidate*(account: var GuestAccount) =
  110. if account.isNil: return
  111. log "invalidating expired account: ", account.id
  112. # TODO: This isn't sufficient, but it works for now
  113. let idx = accountPool.find(account)
  114. if idx > -1: accountPool.delete(idx)
  115. account = nil
  116. proc release*(account: GuestAccount) =
  117. if account.isNil: return
  118. dec account.pending
  119. proc getGuestAccount*(api: Api): Future[GuestAccount] {.async.} =
  120. for i in 0 ..< accountPool.len:
  121. if result.isReady(api): break
  122. result = accountPool.sample()
  123. if not result.isNil and result.isReady(api):
  124. inc result.pending
  125. else:
  126. log "no accounts available for API: ", api
  127. raise rateLimitError()
  128. proc setLimited*(account: GuestAccount; api: Api) =
  129. account.apis[api].limited = true
  130. account.apis[api].limitedAt = epochTime().int
  131. log "rate limited, api: ", api, ", reqs left: ", account.apis[api].remaining, ", id: ", account.id
  132. proc setRateLimit*(account: GuestAccount; api: Api; remaining, reset: int) =
  133. # avoid undefined behavior in race conditions
  134. if api in account.apis:
  135. let limit = account.apis[api]
  136. if limit.reset >= reset and limit.remaining < remaining:
  137. return
  138. if limit.reset == reset and limit.remaining >= remaining:
  139. account.apis[api].remaining = remaining
  140. return
  141. account.apis[api] = RateLimit(remaining: remaining, reset: reset)
  142. proc initAccountPool*(cfg: Config; path: string) =
  143. enableLogging = cfg.enableDebug
  144. let jsonlPath = if path.endsWith(".json"): (path & 'l') else: path
  145. if fileExists(jsonlPath):
  146. log "Parsing JSONL guest accounts file: ", jsonlPath
  147. for line in jsonlPath.lines:
  148. accountPool.add parseGuestAccount(line)
  149. elif fileExists(path):
  150. log "Parsing JSON guest accounts file: ", path
  151. accountPool = parseGuestAccounts(path)
  152. else:
  153. echo "[accounts] ERROR: ", path, " not found. This file is required to authenticate API requests."
  154. quit 1