Merge pull request #2 from SeanOMik/feature/fix-reconnecting

Fix Reconnecting
This commit is contained in:
SeanOMik 2020-09-26 11:11:39 -05:00 committed by GitHub
commit 9637568de9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 100 additions and 123 deletions

29
.vscode/launch.json vendored
View File

@ -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
}
]
}
]
}

16
.vscode/tasks.json vendored
View File

@ -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
}
}
]
}

View File

@ -7,13 +7,13 @@ Memory optimized, simple, and feature rich Discord API wrapper written in Nim.
</p> </p>
# NimCord # 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 ## 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. 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 ## Dependencies
* [Websocket.nim](https://github.com/niv/websocket.nim) * [ws](https://github.com/treeform/ws)
## Documenation ## Documenation
You can generate documenation by running `generate_docs.bat/sh` (depending on your system). Documentation is outputted to the `docs` directory. 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` * 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: ### 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` * To compile you must define `ssl` example: `nim compile -d:ssl --run .\examples\basic.nim`
You can view examples in the [examples](examples) directory. You can view examples in the [examples](examples) directory.
# Todo: # Todo:
- [x] Finish all REST API calls. - [x] Finish all REST API calls.
- [x] Handle all gateway events. - [x] Handle all gateway events.
- [x] Reconnecting - [x] Reconnecting.
- [ ] Configurable Logger.
- [ ] Add library to official Nimble package repository.
- [ ] Memory optimizations. - [ ] Memory optimizations.
- [ ] Member - [ ] Member
- [ ] Channel - [ ] Channel

View File

@ -4,4 +4,4 @@ description = "Discord API wrapper written in Nim. Inspired by DisC++, my othe
license = "MIT" license = "MIT"
srcDir = "src" srcDir = "src"
requires "nim >= 1.2.0", "websocket >= 0.4.0 & <= 0.4.1" requires "nim >= 1.2.0", "ws >= 0.4"

View File

@ -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 nimcordutils, cache, clientobjects, strutils, options, presence, log
import tables import tables
@ -37,7 +37,7 @@ proc sendGatewayRequest*(shard: Shard, request: JsonNode, msg: string = "") {.as
else: else:
shard.client.log.debug("[SHARD " & $shard.id & "] " & msg) shard.client.log.debug("[SHARD " & $shard.id & "] " & msg)
await shard.ws.sendText($request) await shard.ws.send($request)
proc handleHeartbeat(shard: Shard) {.async.} = proc handleHeartbeat(shard: Shard) {.async.} =
while true: 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...") shard.client.log.debug("[SHARD " & $shard.id & "] Waiting " & $shard.heartbeatInterval & " ms until next heartbeat...")
await sleepAsync(shard.heartbeatInterval) 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 = proc getIdentifyPacket(shard: Shard): JsonNode =
result = %* { result = %* {
"op": ord(DiscordOpCode.opIdentify), "op": ord(DiscordOpCode.opIdentify),
@ -71,24 +75,32 @@ proc getIdentifyPacket(shard: Shard): JsonNode =
proc closeConnection*(shard: Shard, code: int = 1000) {.async.} = proc closeConnection*(shard: Shard, code: int = 1000) {.async.} =
shard.client.log.warn("[SHARD " & $shard.id & "] Disconnecting with code: " & $code) 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.client.log.info("[SHARD " & $shard.id & "] Reconnecting...")
shard.reconnecting = true 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, try:
path = "/v=6&encoding=json", true) 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.reconnecting = false
shard.heartbeatAcked = true shard.heartbeatAcked = true
# waitFor client.startConnection()
# Handle discord disconnect. If it detects that we can reconnect, it will. # Handle discord disconnect. If it detects that we can reconnect, it will.
proc handleGatewayDisconnect(shard: Shard, error: string) {.async.} = 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) shard.client.log.warn("[SHARD " & $shard.id & "] Discord gateway disconnected! Error code: " & $disconnectData.code & ", msg: " & disconnectData.reason)
@ -102,28 +114,30 @@ 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.") shard.client.log.error("[SHARD " & $shard.id & "] The Discord gateway sent a disconnect code that we cannot reconnect to.")
else: else:
if not shard.reconnecting: if not shard.reconnecting:
waitFor shard.reconnectShard() shard.reconnectShard()
else: 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. #TODO: Reconnecting may be done, just needs testing.
proc handleWebsocketPacket(shard: Shard) {.async.} = proc handleWebsocketPacket(shard: Shard) {.async.} =
var hasStartedHeartbeatThread = false;
while true: while true:
var packet: tuple[opcode: Opcode, data: string]
packet = await shard.ws.readData() # Skip if the websocket isn't open
shard.client.log.debug("[SHARD " & $shard.id & "] Received gateway payload: " & $packet.data) 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: #if packet == Opcode.Close:
await shard.handleGatewayDisconnect(packet.data) # await shard.handleGatewayDisconnect(packet)
var json: JsonNode var json: JsonNode
# If we fail to parse the json just stop this loop # If we fail to parse the json just stop this loop
try: try:
json = parseJson(packet.data) json = parseJson(packet)
except: 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 continue
if json.contains("s"): if json.contains("s"):
@ -132,7 +146,7 @@ proc handleWebsocketPacket(shard: Shard) {.async.} =
case json["op"].getInt() case json["op"].getInt()
of ord(DiscordOpCode.opHello): of ord(DiscordOpCode.opHello):
if shard.reconnecting: if shard.reconnecting:
shard.client.log.info("[SHARD " & $shard.id & "Reconnected!") shard.client.log.info("[SHARD " & $shard.id & "] Reconnected!")
shard.reconnecting = false shard.reconnecting = false
let resume = %* { let resume = %* {
@ -149,8 +163,14 @@ proc handleWebsocketPacket(shard: Shard) {.async.} =
shard.heartbeatInterval = json["d"]["heartbeat_interval"].getInt() shard.heartbeatInterval = json["d"]["heartbeat_interval"].getInt()
await shard.sendGatewayRequest(shard.getIdentifyPacket()) 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() asyncCheck shard.handleHeartbeat()
shard.heartbeatAcked = true hasStartedHeartbeatThread = true
else:
echo "Not gonna start a new heartbeat thread since. shard.heartbeatAcked is ", shard.heartbeatAcked
of ord(DiscordOpCode.opHeartbeatAck): of ord(DiscordOpCode.opHeartbeatAck):
shard.heartbeatAcked = true shard.heartbeatAcked = true
of ord(DiscordOpCode.opDispatch): of ord(DiscordOpCode.opDispatch):
@ -208,8 +228,7 @@ proc startConnection*(client: DiscordClient, shardAmount: int = 1) {.async.} =
var shard = newShard(index, client) var shard = newShard(index, client)
client.shards.add(shard) client.shards.add(shard)
shard.ws = await newAsyncWebsocketClient(url[6..url.high], Port 443, shard.ws = await newWebSocket(shard.client.endpoint & "/v=6&encoding=json")
path = "/v=6&encoding=json", true)
asyncCheck shard.handleWebsocketPacket() asyncCheck shard.handleWebsocketPacket()
@ -219,15 +238,17 @@ proc startConnection*(client: DiscordClient, shardAmount: int = 1) {.async.} =
var shard = newShard(shardCount - 1, client) var shard = newShard(shardCount - 1, client)
client.shards.add(shard) client.shards.add(shard)
shard.ws = await newAsyncWebsocketClient(url[6..url.high], Port 443, shard.ws = await newWebSocket(shard.client.endpoint & "/v=6&encoding=json")
path = "/v=6&encoding=json", true)
asyncCheck shard.handleWebsocketPacket() await shard.handleWebsocketPacket()
# Just wait. Don't poll while we're reconnecting # Just wait. Don't poll while we're reconnecting
while true: #[ while true:
if not shard.reconnecting: if not shard.reconnecting:
try:
poll() poll()
except WebSocketError:
echo "WebSocketError" ]#
else: else:
raise newException(IOError, "Failed to get gateway url, token may of been incorrect!") raise newException(IOError, "Failed to get gateway url, token may of been incorrect!")

View File

@ -1,4 +1,4 @@
import websocket, cache, user, log import ws, cache, user, log
type type
DiscordClient* = ref object DiscordClient* = ref object
@ -15,9 +15,10 @@ type
Shard* = ref object Shard* = ref object
id*: int id*: int
client*: DiscordClient client*: DiscordClient
ws*: AsyncWebSocket ws*: WebSocket
heartbeatInterval*: int heartbeatInterval*: int
heartbeatAcked*: bool heartbeatAcked*: bool
lastSequence*: int lastSequence*: int
reconnecting*: bool reconnecting*: bool
sessionID*: string sessionID*: string
isHandlingHeartbeat*: bool