From 2bf20854d66e5e05591180acb6169e30df1f7d11 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Wed, 17 Jun 2020 00:34:23 -0500 Subject: [PATCH] Push untested ratelimiting. --- src/client.nim | 38 +++++++++++++++---- src/nimcordutils.nim | 87 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 111 insertions(+), 14 deletions(-) diff --git a/src/client.nim b/src/client.nim index fb4eaf9..e9ce022 100644 --- a/src/client.nim +++ b/src/client.nim @@ -1,4 +1,4 @@ -import websocket, asyncnet, asyncdispatch, json, httpClient, strformat, eventdispatcher, eventhandler, streams +import websocket, asyncnet, asyncdispatch, json, httpClient, eventdispatcher, strformat, eventhandler, streams, nimcordutils, discordobject type DiscordOpCode = enum @@ -24,6 +24,31 @@ type heartbeatAcked: bool lastSequence: int +var globalClient: DiscordClient + +proc defaultHeaders*(client: DiscordClient, added: HttpHeaders = newHttpHeaders()): HttpHeaders = + added.add("Authorization", fmt("Bot {client.token}")) + added.add("User-Agent", "NimCord (https://github.com/SeanOMik/nimcord, v0.0.0)") + added.add("X-RateLimit-Precision", "millisecond") + return added; + +proc sendRequest*(endpoint: string, httpMethod: HttpMethod, headers: HttpHeaders, objectID: snowflake = 0, bucketType: RateLimitBucketType = global, jsonBody: JsonNode = %*{}): JsonNode = + var client = newHttpClient() + # Add headers + client.headers = headers + + var strPayload: string + if ($jsonBody == "{}"): + strPayload = "" + else: + strPayload = $jsonBody + + echo "Sending GET request, URL: ", endpoint, ", body: ", strPayload + + waitForRateLimits(objectID, bucketType) + + return handleResponse(client.request(endpoint, httpMethod, strPayload), objectId, bucketType) + proc sendGatewayRequest*(client: DiscordClient, request: JsonNode, msg: string = "") {.async.} = if (msg.len == 0): echo "Sending gateway payload: ", request @@ -74,21 +99,18 @@ proc handleWebsocketPacket(client: DiscordClient) {.async.} = handleDiscordEvent(json["d"], json["t"].getStr()) else: discard - -proc endpoint*(url: string): string = - return fmt("https://discord.com/api/v6{url}") proc startConnection*(client: DiscordClient) {.async.} = + globalClient = client + client.httpClient = newAsyncHttpClient() client.httpClient.headers = newHttpHeaders({"Authorization": fmt("Bot {client.token}")}) - let urlResult = parseJson(await client.httpClient.getContent(endpoint("/gateway/bot"))) - echo "Got result: ", $urlResult - + let urlResult = sendRequest(endpoint("/gateway/bot"), HttpMethod.HttpGet, client.defaultHeaders()) if (urlResult.contains("url")): let url = urlResult["url"].getStr() - client.ws = await newAsyncWebsocketClient(url[6..url.high], Port 443 , + client.ws = await newAsyncWebsocketClient(url[6..url.high], Port 443, path = "/v=6&encoding=json", true) echo "Connected!" diff --git a/src/nimcordutils.nim b/src/nimcordutils.nim index 52bc362..d65a0b5 100644 --- a/src/nimcordutils.nim +++ b/src/nimcordutils.nim @@ -1,4 +1,4 @@ -include parseutils, json, httpClient +import parseutils, json, httpClient, strformat, tables, times, asyncdispatch from discordobject import snowflake proc getIDFromJson*(str: string): uint64 = @@ -6,9 +6,84 @@ proc getIDFromJson*(str: string): uint64 = discard parseOct(str, num) return num +proc endpoint*(url: string): string = + return fmt("https://discord.com/api/v6{url}") -type RateLimitBucketType = enum - CHANNEL, - GUILD, - WEBHOOK, - GLOBAL \ No newline at end of file +type + RateLimitBucketType* = enum + channel, + guild, + webhook, + global + RateLimit = ref object {.requiresInit.} + limit: int + remainingLimit: int + ratelimitReset: float + +proc newRateLimit(lmt: int = 500, remLmnt: int = 500, ratelmtReset: float = 0): RateLimit = + return RateLimit(limit: lmt, remainingLimit: remLmnt, ratelimitReset: ratelmtReset) + +# Rate limit buckets +let channelRatelimitBucket = newTable[snowflake, RateLimit]() +let guildRatelimitBucket = newTable[snowflake, RateLimit]() +let webhookRatelimitBucket = newTable[snowflake, RateLimit]() +var globalRateLimit: RateLimit = newRateLimit() + +proc handleRateLimits*(headers: HttpHeaders, objectID: snowflake, bucketType: RateLimitBucketType) = + var obj: RateLimit + if (headers.hasKey("x-ratelimit-global")): + obj = globalRateLimit + elif (headers.hasKey("x-ratelimit-limit")): + case bucketType: + of RateLimitBucketType.channel: + obj = channelRatelimitBucket[objectID] + discard + of RateLimitBucketType.guild: + obj = guildRatelimitBucket[objectID] + discard + of RateLimitBucketType.webhook: + obj = webhookRatelimitBucket[objectID] + discard + of RateLimitBucketType.global: + obj = globalRateLimit + discard + else: + return + + discard parseInt(headers["x-ratelimit-limit"], obj.limit) + discard parseInt(headers["x-ratelimit-remaining"], obj.remainingLimit) + discard parseFloat(headers["x-ratelimit-reset"], obj.ratelimitReset) + + +proc handleResponse*(response: Response, objectID: snowflake, bucketType: RateLimitBucketType): JsonNode = + echo fmt("Received requested payload: {response.body}") + + handleRateLimits(response.headers, objectID, bucketType) + + return parseJson(response.body()) + +proc waitForRateLimits*(objectID: snowflake, bucketType: RateLimitBucketType) = + var rlmt: RateLimit + if (globalRateLimit.remainingLimit == 0): + rlmt = globalRateLimit + else: + case bucketType: + of RateLimitBucketType.channel: + rlmt = channelRatelimitBucket[objectID] + discard + of RateLimitBucketType.guild: + rlmt = guildRatelimitBucket[objectID] + discard + of RateLimitBucketType.webhook: + rlmt = webhookRatelimitBucket[objectID] + discard + of RateLimitBucketType.global: + rlmt = globalRateLimit + discard + + if (rlmt.remainingLimit == 0): + let millisecondTime: float = rlmt.ratelimitReset * 1000 - epochTime() * 1000 + + if (millisecondTime > 0): + echo fmt("Rate limit wait time: {millisecondTime} miliseconds") + discard sleepAsync(millisecondTime) \ No newline at end of file