diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 1f54545..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Nim Project", - "type": "gdb", - "request": "launch", - "program": "${workspaceFolder}/src/client.exe", - "args": [], - "stopAtEntry": false, - "cwd": "${workspaceFolder}", - "environment": [], - "externalConsole": false, - "valuesFormatting": "parseText", - "MIMode": "gdb", - "miDebuggerPath": "D:/Code/nim/nim-1.2.0/dist/mingw64/bin/gdb.exe", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - } - ] - } - ] -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 90e8776..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "Build Nim Project", - "type": "shell", - "command": "nim compile -d:ssl -d:debug --debugger:native ./src/client.nim", - "group": { - "kind": "build", - "isDefault": true - } - } - ] -} \ No newline at end of file diff --git a/README.md b/README.md index 20978a5..5053f27 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ Memory optimized, simple, and feature rich Discord API wrapper written in Nim.

# NimCord -A Discord API wrapper written in Nim. Inspired, and created by the author of a memory optimized Discord Library named DisC++. +A Discord API wrapper written in Nim. Inspired, and created by the author of a memory optimized Discord Library named [DisC++](https://github.com/DisCPP/DisCPP). ## State NimCord is currently in a testing state. If you want to use it, you can but you may encounter errors. If you do encounter errors, create a GitHub issue or join the Discord server. ## Dependencies -* [Websocket.nim](https://github.com/niv/websocket.nim) +* [ws](https://github.com/treeform/ws) ## Documenation You can generate documenation by running `generate_docs.bat/sh` (depending on your system). Documentation is outputted to the `docs` directory. @@ -30,16 +30,16 @@ You can generate documenation by running `generate_docs.bat/sh` (depending on yo * NimCord is not yet available on the official Nimble package repository. To install it, you need to clone this repo and in the project root, run: `nimble install` ### Note: -* If you already have Websocket.nim installed, you need to make sure you have version 0.4.1 installed. * To compile you must define `ssl` example: `nim compile -d:ssl --run .\examples\basic.nim` You can view examples in the [examples](examples) directory. - # Todo: - [x] Finish all REST API calls. - [x] Handle all gateway events. -- [x] Reconnecting +- [x] Reconnecting. +- [ ] Configurable Logger. +- [ ] Add library to official Nimble package repository. - [ ] Memory optimizations. - [ ] Member - [ ] Channel diff --git a/nimcord.nimble b/nimcord.nimble index 27c36e1..167b1ac 100644 --- a/nimcord.nimble +++ b/nimcord.nimble @@ -4,4 +4,4 @@ description = "Discord API wrapper written in Nim. Inspired by DisC++, my othe license = "MIT" srcDir = "src" -requires "nim >= 1.2.0", "websocket >= 0.4.0 & <= 0.4.1" +requires "nim >= 1.2.0", "ws >= 0.4" diff --git a/src/nimcord/client.nim b/src/nimcord/client.nim index ecd97c4..786284b 100644 --- a/src/nimcord/client.nim +++ b/src/nimcord/client.nim @@ -1,4 +1,4 @@ -import websocket, asyncdispatch, json, httpClient, eventdispatcher, strformat +import ws, asyncdispatch, json, httpClient, eventdispatcher, strformat import nimcordutils, cache, clientobjects, strutils, options, presence, log import tables @@ -37,7 +37,7 @@ proc sendGatewayRequest*(shard: Shard, request: JsonNode, msg: string = "") {.as else: shard.client.log.debug("[SHARD " & $shard.id & "] " & msg) - await shard.ws.sendText($request) + await shard.ws.send($request) proc handleHeartbeat(shard: Shard) {.async.} = while true: @@ -53,6 +53,10 @@ proc handleHeartbeat(shard: Shard) {.async.} = shard.client.log.debug("[SHARD " & $shard.id & "] Waiting " & $shard.heartbeatInterval & " ms until next heartbeat...") await sleepAsync(shard.heartbeatInterval) + if (not shard.heartbeatAcked and not shard.reconnecting): + shard.client.log.debug("[SHARD " & $shard.id & "] Heartbeat not acked! Reconnecting...") + asyncCheck shard.reconnectShard() + proc getIdentifyPacket(shard: Shard): JsonNode = result = %* { "op": ord(DiscordOpCode.opIdentify), @@ -71,24 +75,32 @@ proc getIdentifyPacket(shard: Shard): JsonNode = proc closeConnection*(shard: Shard, code: int = 1000) {.async.} = shard.client.log.warn("[SHARD " & $shard.id & "] Disconnecting with code: " & $code) - await shard.ws.close(code) + shard.ws.close() -proc reconnectShard(shard: Shard) {.async.} = +proc reconnectShard*(shard: Shard) {.async.} = shard.client.log.info("[SHARD " & $shard.id & "] Reconnecting...") shard.reconnecting = true - waitFor shard.ws.close(1000) + #waitFor shard.ws.close(1000) - shard.ws = waitFor newAsyncWebsocketClient(shard.client.endpoint[6..shard.client.endpoint.high], Port 443, - path = "/v=6&encoding=json", true) + try: + shard.ws = waitFor newWebSocket(shard.client.endpoint & "/v=6&encoding=json") + #shard.ws = waitFor newAsyncWebsocketClient(shard.client.endpoint[6..shard.client.endpoint.high], Port 443, + # path = "/v=6&encoding=json", true) + except OSError: + shard.client.log.error("[SHARD " & $shard.id & "] Failed to reconnect to websocket with OSError trying again!") + asyncCheck shard.reconnectShard() + except IOError: + shard.client.log.error("[SHARD " & $shard.id & "] Failed to reconnect to websocket with IOError trying again!") + asyncCheck shard.reconnectShard() shard.reconnecting = false shard.heartbeatAcked = true - # waitFor client.startConnection() # Handle discord disconnect. If it detects that we can reconnect, it will. proc handleGatewayDisconnect(shard: Shard, error: string) {.async.} = - let disconnectData = extractCloseData(error) + shard.client.log.warn("[SHARD " & $shard.id & "] Discord gateway disconnected!") + #[[let disconnectData = extractCloseData(error) shard.client.log.warn("[SHARD " & $shard.id & "] Discord gateway disconnected! Error code: " & $disconnectData.code & ", msg: " & disconnectData.reason) @@ -102,75 +114,83 @@ proc handleGatewayDisconnect(shard: Shard, error: string) {.async.} = shard.client.log.error("[SHARD " & $shard.id & "] The Discord gateway sent a disconnect code that we cannot reconnect to.") else: if not shard.reconnecting: - waitFor shard.reconnectShard() + shard.reconnectShard() else: - shard.client.log.debug("[SHARD " & $shard.id & "] Gateway cannot reconnect due to already reconnecting...") + shard.client.log.debug("[SHARD " & $shard.id & "] Gateway cannot reconnect due to already reconnecting...")]]# #TODO: Reconnecting may be done, just needs testing. proc handleWebsocketPacket(shard: Shard) {.async.} = + var hasStartedHeartbeatThread = false; while true: - var packet: tuple[opcode: Opcode, data: string] - packet = await shard.ws.readData() - shard.client.log.debug("[SHARD " & $shard.id & "] Received gateway payload: " & $packet.data) + # Skip if the websocket isn't open + if shard.ws.readyState == Open: + var packet = await shard.ws.receiveStrPacket() + shard.client.log.debug("[SHARD " & $shard.id & "] Received gateway payload: " & $packet) - if packet.opcode == Opcode.Close: - await shard.handleGatewayDisconnect(packet.data) + #if packet == Opcode.Close: + # await shard.handleGatewayDisconnect(packet) - var json: JsonNode + var json: JsonNode - # If we fail to parse the json just stop this loop - try: - json = parseJson(packet.data) - except: - shard.client.log.error("[SHARD " & $shard.id & "] Failed to parse websocket payload: " & $packet.data) - continue + # If we fail to parse the json just stop this loop + try: + json = parseJson(packet) + except: + shard.client.log.error("[SHARD " & $shard.id & "] Failed to parse websocket payload: " & $packet) + continue - if json.contains("s"): - shard.lastSequence = json["s"].getInt() + if json.contains("s"): + shard.lastSequence = json["s"].getInt() - case json["op"].getInt() - of ord(DiscordOpCode.opHello): - if shard.reconnecting: - shard.client.log.info("[SHARD " & $shard.id & "Reconnected!") - shard.reconnecting = false + case json["op"].getInt() + of ord(DiscordOpCode.opHello): + if shard.reconnecting: + shard.client.log.info("[SHARD " & $shard.id & "] Reconnected!") + shard.reconnecting = false - let resume = %* { - "op": ord(opResume), - "d": { - "token": shard.client.token, + let resume = %* { + "op": ord(opResume), + "d": { + "token": shard.client.token, + "session_id": shard.sessionID, + "seq": shard.lastSequence + } + } + + await shard.sendGatewayRequest(resume) + else: + shard.heartbeatInterval = json["d"]["heartbeat_interval"].getInt() + await shard.sendGatewayRequest(shard.getIdentifyPacket()) + + # Don't start a new heartbeat thread if one is already started + echo "About to start a heartbeat thread! shard.heartbeatAcked is ", shard.heartbeatAcked + if not hasStartedHeartbeatThread: + echo "Starting new heartbeat thread! shard.heartbeatAcked is ", shard.heartbeatAcked + asyncCheck shard.handleHeartbeat() + hasStartedHeartbeatThread = true + else: + echo "Not gonna start a new heartbeat thread since. shard.heartbeatAcked is ", shard.heartbeatAcked + of ord(DiscordOpCode.opHeartbeatAck): + shard.heartbeatAcked = true + of ord(DiscordOpCode.opDispatch): + asyncCheck handleDiscordEvent(shard, json["d"], json["t"].getStr()) + of ord(DiscordOpCode.opReconnect): + asyncCheck shard.reconnectShard() + of ord(DiscordOpCode.opInvalidSession): + # If the json field `d` is true then the session may be resumable. + if json["d"].getBool(): + let resume = %* { + "op": ord(opResume), "session_id": shard.sessionID, "seq": shard.lastSequence } - } - await shard.sendGatewayRequest(resume) + await shard.sendGatewayRequest(resume) + else: + asyncCheck shard.reconnectShard() else: - shard.heartbeatInterval = json["d"]["heartbeat_interval"].getInt() - await shard.sendGatewayRequest(shard.getIdentifyPacket()) - - asyncCheck shard.handleHeartbeat() - shard.heartbeatAcked = true - of ord(DiscordOpCode.opHeartbeatAck): - shard.heartbeatAcked = true - of ord(DiscordOpCode.opDispatch): - asyncCheck handleDiscordEvent(shard, json["d"], json["t"].getStr()) - of ord(DiscordOpCode.opReconnect): - asyncCheck shard.reconnectShard() - of ord(DiscordOpCode.opInvalidSession): - # If the json field `d` is true then the session may be resumable. - if json["d"].getBool(): - let resume = %* { - "op": ord(opResume), - "session_id": shard.sessionID, - "seq": shard.lastSequence - } - - await shard.sendGatewayRequest(resume) - else: - asyncCheck shard.reconnectShard() - else: - discard + discard proc newShard(shardID: int, client: DiscordClient): Shard = return Shard(id: shardID, client: client) @@ -208,8 +228,7 @@ proc startConnection*(client: DiscordClient, shardAmount: int = 1) {.async.} = var shard = newShard(index, client) client.shards.add(shard) - shard.ws = await newAsyncWebsocketClient(url[6..url.high], Port 443, - path = "/v=6&encoding=json", true) + shard.ws = await newWebSocket(shard.client.endpoint & "/v=6&encoding=json") asyncCheck shard.handleWebsocketPacket() @@ -219,15 +238,17 @@ proc startConnection*(client: DiscordClient, shardAmount: int = 1) {.async.} = var shard = newShard(shardCount - 1, client) client.shards.add(shard) - shard.ws = await newAsyncWebsocketClient(url[6..url.high], Port 443, - path = "/v=6&encoding=json", true) + shard.ws = await newWebSocket(shard.client.endpoint & "/v=6&encoding=json") - asyncCheck shard.handleWebsocketPacket() + await shard.handleWebsocketPacket() # Just wait. Don't poll while we're reconnecting - while true: + #[ while true: if not shard.reconnecting: - poll() + try: + poll() + except WebSocketError: + echo "WebSocketError" ]# else: raise newException(IOError, "Failed to get gateway url, token may of been incorrect!") diff --git a/src/nimcord/clientobjects.nim b/src/nimcord/clientobjects.nim index 76e7db4..bbfe552 100644 --- a/src/nimcord/clientobjects.nim +++ b/src/nimcord/clientobjects.nim @@ -1,4 +1,4 @@ -import websocket, cache, user, log +import ws, cache, user, log type DiscordClient* = ref object @@ -15,9 +15,10 @@ type Shard* = ref object id*: int client*: DiscordClient - ws*: AsyncWebSocket + ws*: WebSocket heartbeatInterval*: int heartbeatAcked*: bool lastSequence*: int reconnecting*: bool - sessionID*: string \ No newline at end of file + sessionID*: string + isHandlingHeartbeat*: bool \ No newline at end of file diff --git a/src/nimcord/eventdispatcher.nim b/src/nimcord/eventdispatcher.nim index 17732d0..ff671cc 100644 --- a/src/nimcord/eventdispatcher.nim +++ b/src/nimcord/eventdispatcher.nim @@ -19,7 +19,7 @@ proc readyEvent(shard: Shard, json: JsonNode) = shard.client.clientUser = newUser(userJson) shard.sessionID = json["session_id"].getStr() - + dispatchEvent(readyEvent) proc channelCreateEvent(shard: Shard, json: JsonNode) =