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>
# 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

View File

@ -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"

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 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!")

View File

@ -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
isHandlingHeartbeat*: bool