auth.nim 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. #SPDX-License-Identifier: AGPL-3.0-only
  2. import std/[asyncdispatch, times, json, random, strutils, tables, packedsets, os]
  3. import types, consts
  4. import experimental/parser/session
  5. const hourInSeconds = 60 * 60
  6. var
  7. sessionPool: seq[Session]
  8. enableLogging = false
  9. # max requests at a time per session to avoid race conditions
  10. maxConcurrentReqs = 2
  11. proc setMaxConcurrentReqs*(reqs: int) =
  12. if reqs > 0:
  13. maxConcurrentReqs = reqs
  14. template log(str: varargs[string, `$`]) =
  15. echo "[sessions] ", str.join("")
  16. proc endpoint*(req: ApiReq; session: Session): string =
  17. case session.kind
  18. of oauth: req.oauth.endpoint
  19. of cookie: req.cookie.endpoint
  20. proc pretty*(session: Session): string =
  21. if session.isNil:
  22. return "<null>"
  23. if session.id > 0 and session.username.len > 0:
  24. result = $session.id & " (" & session.username & ")"
  25. elif session.username.len > 0:
  26. result = session.username
  27. elif session.id > 0:
  28. result = $session.id
  29. else:
  30. result = "<unknown>"
  31. result = $session.kind & " " & result
  32. proc snowflakeToEpoch(flake: int64): int64 =
  33. int64(((flake shr 22) + 1288834974657) div 1000)
  34. proc getSessionPoolHealth*(): JsonNode =
  35. let now = epochTime().int
  36. var
  37. totalReqs = 0
  38. limited: PackedSet[int64]
  39. reqsPerApi: Table[string, int]
  40. oldest = now.int64
  41. newest = 0'i64
  42. average = 0'i64
  43. oauthTotal, cookieTotal = 0
  44. oauthLimited, cookieLimited = 0
  45. for session in sessionPool:
  46. let created = snowflakeToEpoch(session.id)
  47. if created > newest:
  48. newest = created
  49. if created < oldest:
  50. oldest = created
  51. average += created
  52. case session.kind
  53. of oauth: inc oauthTotal
  54. of cookie: inc cookieTotal
  55. if session.limited:
  56. limited.incl session.id
  57. case session.kind
  58. of oauth: inc oauthLimited
  59. of cookie: inc cookieLimited
  60. for api in session.apis.keys:
  61. let
  62. apiStatus = session.apis[api]
  63. reqs = apiStatus.limit - apiStatus.remaining
  64. # no requests made with this session and endpoint since the limit reset
  65. if apiStatus.reset < now:
  66. continue
  67. reqsPerApi.mgetOrPut($api, 0).inc reqs
  68. totalReqs.inc reqs
  69. if sessionPool.len > 0:
  70. average = average div sessionPool.len
  71. else:
  72. oldest = 0
  73. average = 0
  74. return %*{
  75. "sessions": %*{
  76. "total": sessionPool.len,
  77. "limited": limited.card,
  78. "oauth": %*{"total": oauthTotal, "limited": oauthLimited},
  79. "cookie": %*{"total": cookieTotal, "limited": cookieLimited},
  80. "oldest": $fromUnix(oldest),
  81. "newest": $fromUnix(newest),
  82. "average": $fromUnix(average)
  83. },
  84. "requests": %*{
  85. "total": totalReqs,
  86. "apis": reqsPerApi
  87. }
  88. }
  89. proc getSessionPoolDebug*(): JsonNode =
  90. let now = epochTime().int
  91. var list = newJObject()
  92. for session in sessionPool:
  93. let sessionJson = %*{
  94. "kind": $session.kind,
  95. "apis": newJObject(),
  96. "pending": session.pending,
  97. }
  98. if session.limited:
  99. sessionJson["limited"] = %true
  100. for api in session.apis.keys:
  101. let
  102. apiStatus = session.apis[api]
  103. obj = %*{}
  104. if apiStatus.reset > now.int:
  105. obj["remaining"] = %apiStatus.remaining
  106. obj["reset"] = %apiStatus.reset
  107. if "remaining" notin obj:
  108. continue
  109. sessionJson{"apis", $api} = obj
  110. list[$session.id] = sessionJson
  111. return %list
  112. proc rateLimitError*(): ref RateLimitError =
  113. newException(RateLimitError, "rate limited")
  114. proc noSessionsError*(): ref NoSessionsError =
  115. newException(NoSessionsError, "no sessions available")
  116. proc isLimited(session: Session; req: ApiReq): bool =
  117. if session.isNil:
  118. return true
  119. let api = req.endpoint(session)
  120. if session.limited and api != graphUserTweetsV2:
  121. if (epochTime().int - session.limitedAt) > hourInSeconds:
  122. session.limited = false
  123. log "resetting limit: ", session.pretty
  124. return false
  125. else:
  126. return true
  127. if api in session.apis:
  128. let limit = session.apis[api]
  129. return limit.remaining <= 10 and limit.reset > epochTime().int
  130. else:
  131. return false
  132. proc isReady(session: Session; req: ApiReq): bool =
  133. not (session.isNil or session.pending > maxConcurrentReqs or session.isLimited(req))
  134. proc invalidate*(session: var Session) =
  135. if session.isNil: return
  136. log "invalidating: ", session.pretty
  137. # TODO: This isn't sufficient, but it works for now
  138. let idx = sessionPool.find(session)
  139. if idx > -1: sessionPool.delete(idx)
  140. session = nil
  141. proc release*(session: Session) =
  142. if session.isNil: return
  143. dec session.pending
  144. proc getSession*(req: ApiReq): Future[Session] {.async.} =
  145. for i in 0 ..< sessionPool.len:
  146. if result.isReady(req): break
  147. result = sessionPool.sample()
  148. if not result.isNil and result.isReady(req):
  149. inc result.pending
  150. else:
  151. if result.isNil:
  152. log "no sessions available for API: ", req.cookie.endpoint
  153. else:
  154. log "no sessions available for API: ", req.endpoint(result), ", last tried: ", result.pretty
  155. raise noSessionsError()
  156. proc setLimited*(session: Session; req: ApiReq) =
  157. let api = req.endpoint(session)
  158. session.limited = true
  159. session.limitedAt = epochTime().int
  160. log "rate limited by api: ", api, ", reqs left: ", session.apis[api].remaining, ", ", session.pretty
  161. proc setRateLimit*(session: Session; req: ApiReq; remaining, reset, limit: int) =
  162. # avoid undefined behavior in race conditions
  163. let api = req.endpoint(session)
  164. if api in session.apis:
  165. let rateLimit = session.apis[api]
  166. if rateLimit.reset >= reset and rateLimit.remaining < remaining:
  167. return
  168. if rateLimit.reset == reset and rateLimit.remaining >= remaining:
  169. session.apis[api].remaining = remaining
  170. return
  171. session.apis[api] = RateLimit(limit: limit, remaining: remaining, reset: reset)
  172. proc initSessionPool*(cfg: Config; path: string) =
  173. enableLogging = cfg.enableDebug
  174. if path.endsWith(".json"):
  175. log "ERROR: .json is not supported, the file must be a valid JSONL file ending in .jsonl"
  176. quit 1
  177. if not fileExists(path):
  178. log "ERROR: ", path, " not found. This file is required to authenticate API requests."
  179. quit 1
  180. log "parsing JSONL account sessions file: ", path
  181. for line in path.lines:
  182. sessionPool.add parseSession(line)
  183. log "successfully added ", sessionPool.len, " valid account sessions"