From 265f16e7685fac46bcebf2a70d145f3f67559c18 Mon Sep 17 00:00:00 2001
From: SeanOMik
Date: Fri, 25 Sep 2020 15:21:41 -0500
Subject: [PATCH 1/3] Fix reconnecting
---
.vscode/launch.json | 29 ----------------
.vscode/tasks.json | 16 ---------
README.md | 6 ++--
nimcord.nimble | 2 +-
src/nimcord/client.nim | 61 +++++++++++++++++++--------------
src/nimcord/clientobjects.nim | 7 ++--
src/nimcord/eventdispatcher.nim | 2 +-
7 files changed, 44 insertions(+), 79 deletions(-)
delete mode 100644 .vscode/launch.json
delete mode 100644 .vscode/tasks.json
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..a456b8f 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,12 +30,10 @@ 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.
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..ad780ec 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,28 +114,27 @@ 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.} =
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)
+ 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
# If we fail to parse the json just stop this loop
try:
- json = parseJson(packet.data)
+ json = parseJson(packet)
except:
- shard.client.log.error("[SHARD " & $shard.id & "] Failed to parse websocket payload: " & $packet.data)
+ shard.client.log.error("[SHARD " & $shard.id & "] Failed to parse websocket payload: " & $packet)
continue
if json.contains("s"):
@@ -132,7 +143,7 @@ proc handleWebsocketPacket(shard: Shard) {.async.} =
case json["op"].getInt()
of ord(DiscordOpCode.opHello):
if shard.reconnecting:
- shard.client.log.info("[SHARD " & $shard.id & "Reconnected!")
+ shard.client.log.info("[SHARD " & $shard.id & "] Reconnected!")
shard.reconnecting = false
let resume = %* {
@@ -149,8 +160,10 @@ proc handleWebsocketPacket(shard: Shard) {.async.} =
shard.heartbeatInterval = json["d"]["heartbeat_interval"].getInt()
await shard.sendGatewayRequest(shard.getIdentifyPacket())
- asyncCheck shard.handleHeartbeat()
- shard.heartbeatAcked = true
+ # Don't start a new
+ if (not shard.isHandlingHeartbeat):
+ asyncCheck shard.handleHeartbeat()
+ shard.heartbeatAcked = true
of ord(DiscordOpCode.opHeartbeatAck):
shard.heartbeatAcked = true
of ord(DiscordOpCode.opDispatch):
@@ -208,8 +221,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,8 +231,7 @@ 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()
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) =
From 77e5a74141360c6d9978ecad79cb8171c084dd4a Mon Sep 17 00:00:00 2001
From: SeanOMik
Date: Fri, 25 Sep 2020 15:22:56 -0500
Subject: [PATCH 2/3] Add todo
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index a456b8f..c81d6e2 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ You can view examples in the [examples](examples) directory.
- [x] Finish all REST API calls.
- [x] Handle all gateway events.
- [x] Reconnecting
+- [ ] Add library to official Nimble package repository.
- [ ] Memory optimizations.
- [ ] Member
- [ ] Channel
From 44d52848aa89abdd64c01534878d41528a28cd34 Mon Sep 17 00:00:00 2001
From: SeanOMik
Date: Fri, 25 Sep 2020 16:21:35 -0500
Subject: [PATCH 3/3] Fix creating several heartbeat threads
---
README.md | 3 +-
src/nimcord/client.nim | 118 ++++++++++++++++++++++-------------------
2 files changed, 66 insertions(+), 55 deletions(-)
diff --git a/README.md b/README.md
index c81d6e2..5053f27 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,8 @@ 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
diff --git a/src/nimcord/client.nim b/src/nimcord/client.nim
index ad780ec..786284b 100644
--- a/src/nimcord/client.nim
+++ b/src/nimcord/client.nim
@@ -120,70 +120,77 @@ proc handleGatewayDisconnect(shard: Shard, error: string) {.async.} =
#TODO: Reconnecting may be done, just needs testing.
proc handleWebsocketPacket(shard: Shard) {.async.} =
+ var hasStartedHeartbeatThread = false;
while true:
- var packet = await shard.ws.receiveStrPacket()
- shard.client.log.debug("[SHARD " & $shard.id & "] Received gateway payload: " & $packet)
+ # 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.Close:
- # await shard.handleGatewayDisconnect(packet)
+ #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)
- except:
- shard.client.log.error("[SHARD " & $shard.id & "] Failed to parse websocket payload: " & $packet)
- 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())
-
- # Don't start a new
- if (not shard.isHandlingHeartbeat):
- 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)
@@ -233,12 +240,15 @@ proc startConnection*(client: DiscordClient, shardAmount: int = 1) {.async.} =
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!")