Skip to content

Instantly share code, notes, and snippets.

@christineponyl
Created April 27, 2018 04:32
Show Gist options
  • Save christineponyl/2add7ad5e9f6daead0ab57234b2ce601 to your computer and use it in GitHub Desktop.
Save christineponyl/2add7ad5e9f6daead0ab57234b2ce601 to your computer and use it in GitHub Desktop.
use "net"
use "time"
use "json"
use "net/http"
use "promises"
use "buffered"
use "collections"
use @time[USize](ptr: USize)
type _RestResponse is Promise[JsonType val]
type _RestData is Option[(JsonObject | JsonArray)] val
class _RestClient
let _limiter: _RateLimiter
new create(auth: TCPConnectionAuth, token: String, logger: DiscordLogger) =>
_limiter = _RateLimiter(auth, token, logger)
fun get(path: String, bucket: String): _RestResponse ? =>
_request("GET", path, bucket, None)?
fun put(path: String, bucket: String, data: _RestData): _RestResponse ? =>
_request("PUT", path, bucket, data)?
fun post(path: String, bucket: String, data: _RestData): _RestResponse ? =>
_request("POST", path, bucket, data)?
fun delete(path: String, bucket: String, data: _RestData): _RestResponse ? =>
_request("DELETE", path, bucket, data)?
fun _request(method: String, path: String, bucket: String, data: _RestData): _RestResponse ? =>
let response = _RestResponse
let url = URL.valid(_Discord.base_url() + path)?
let request = _RestRequest(consume url, method, bucket, consume data, response)
_limiter.submit(consume request)
response
class _RestBucket
var is_rate_limited: Bool = false
embed queue: List[_RestRequest] = queue.create()
class _RestRateLimit is TimerNotify
let _bucket_id: String
let _limiter: _RateLimiter
new iso create(limiter: _RateLimiter, id: String) =>
_bucket_id = id
_limiter = limiter
fun ref apply(timer: Timer, count: U64): Bool =>
_limiter.release_ratelimit(_bucket_id)
false
actor _RateLimiter
let _token: String
let _logger: DiscordLogger
let _rate_limit: Timers = Timers
embed _client: HTTPClient
embed _global_bucket: _RestBucket = _RestBucket
embed _buckets: Map[String, _RestBucket] = _buckets.create()
new create(auth: TCPConnectionAuth, token: String, logger: DiscordLogger) =>
_token = token
_logger = logger
_client = HTTPClient(consume auth)
fun _get_bucket(key: (Bool | String)): _RestBucket ? =>
match key
| true => _global_bucket
| let bucket_id: String =>
if bucket_id.size() isnt 0
then _buckets(bucket_id)?
else _global_bucket end
else error end
fun ref _execute(request: _RestRequest val) =>
try
let dispatcher = recover val _RestDispatcher.create(this, request) end
_client(request.to_payload(_token), consume dispatcher)?
else
request.respond.reject()
end
be release_ratelimit(bucket_id: String) =>
if bucket_id.size() is 0 then _global_bucket
else _buckets(bucket_id)?
be submit(request: _RestRequest val) =>
if _global_bucket.is_rate_limited then
_global_bucket.queue.push(request)
elseif request.bucket_id.size() is 0 then
_execute(request)
else
try _buckets(request.bucket_id)?.queue.push(request) else
_buckets(request.bucket_id) = _RestBucket
_execute(request)
end
end
be complete(request: _RestRequest val, result: _RestResult val, retry: Bool) =>
if retry then submit(request); return end
if result.is_rate_limited then
try
let bucket =
if result.is_global then _global_bucket
else _buckets(request.bucket_id)? end
let bucket_id =
if bucket.is_global then ""
else request.bucket_id end
bucket.is_rate_limited = true
_rate_limit(_RestRateLimit(this, bucket_id))
end
end
class _RestDispatcher is HandlerFactory
let _limiter: _RateLimiter
let _request: _RestRequest val
new iso create(limiter: _RateLimiter, request: _RestRequest val) =>
_limiter = limiter
_request = request
fun apply(session: HTTPSession): HTTPHandler ref^ =>
_RestHandler.create(_limiter, _request, session)
class _RestHandler is HTTPHandler
var _status: U16 = 0
let _builder: Reader = Reader.create()
var _rate_limit: (Bool, Bool, U64) = (false, false, 0)
let _session: HTTPSession
let _request: _RestRequest
let _limiter: _RateLimiter
new ref create(limiter: _RateLimiter, request: _RestRequest, session: HTTPSession) =>
_limiter = limiter
_request = request
_session = session
fun ref chunk(data: ByteSeq val) =>
_builder.append(data)
fun ref finished() => _complete(false)
fun ref cancelled() => _complete(true)
fun ref _complete(retry: Bool) =>
_session.dispose()
_limiter.complete(_request, _result(), retry)
fun ref _result(): _RestResult iso ^ =>
(let global, let limited, let expires) = _rate_limit
let content = try String.from_array(_builder.block(_builder.size())?) else "." end
_RestResult(_status, expires, global, limited, consume content)
fun ref apply(resp: Payload val) =>
_status = resp.status
let headers = resp.headers()
var expires = try headers("X-RateLimit-Reset")?.u64()? else 0 end
let is_global = try headers("X-RateLimit-Global")?; true else false end
let is_limited = try headers("X-RateLimit-Remaining")? == "0" else false end
_rate_limit = (is_global, is_limited, expires)
try
for buffer in resp.body()?.values() do
_builder.append(buffer)
end
end
class _RestResult
let status: U16
let expires: U64
let is_global: Bool
let is_rate_limited: Bool
let json: JsonDoc = JsonDoc
new iso create(stat: U16, exp: U64, global: Bool, limited: Bool, data: String) =>
status = stat
expires = exp
is_global = global
is_rate_limited = limited
try json.parse(consume data)? end
class val _RestRequest
let _url: URL
let _method: String
let _data: _RestData
let bucket_id: String
let respond: _RestResponse
new val create(url: URL,
method: String, id: String,
data: _RestData, response: _RestResponse
) =>
_url = url
_data = data
_method = method
bucket_id = id
respond = response
fun to_payload(token: String): Payload ^ =>
let payload = Payload.request(_method, _url)
payload("Authorization") = "Bot " + token
payload("User-Agent") = _Discord.user_agent()
match _data
| let json: this->(JsonObject | JsonArray) =>
payload("Content-Type") = "application/json;charset=utf-8"
payload.add_chunk(json.string())
end
consume payload
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment