Merge pull request #2 from SeanOMik/feature/fix-reconnecting
Fix Reconnecting
This commit is contained in:
commit
9637568de9
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
10
README.md
10
README.md
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,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.")
|
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"):
|
||||||
shard.lastSequence = json["s"].getInt()
|
shard.lastSequence = json["s"].getInt()
|
||||||
|
|
||||||
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 = %* {
|
||||||
"op": ord(opResume),
|
"op": ord(opResume),
|
||||||
"d": {
|
"d": {
|
||||||
"token": shard.client.token,
|
"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,
|
"session_id": shard.sessionID,
|
||||||
"seq": shard.lastSequence
|
"seq": shard.lastSequence
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await shard.sendGatewayRequest(resume)
|
await shard.sendGatewayRequest(resume)
|
||||||
|
else:
|
||||||
|
asyncCheck shard.reconnectShard()
|
||||||
else:
|
else:
|
||||||
shard.heartbeatInterval = json["d"]["heartbeat_interval"].getInt()
|
discard
|
||||||
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
|
|
||||||
|
|
||||||
proc newShard(shardID: int, client: DiscordClient): Shard =
|
proc newShard(shardID: int, client: DiscordClient): Shard =
|
||||||
return Shard(id: shardID, client: client)
|
return Shard(id: shardID, client: client)
|
||||||
|
@ -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:
|
||||||
poll()
|
try:
|
||||||
|
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!")
|
||||||
|
|
||||||
|
|
|
@ -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
|
Reference in New Issue