You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

258 lines
5.6 KiB
Lua

local json = require('json')
local timer = require('timer')
local EventHandler = require('client/EventHandler')
local WebSocket = require('client/WebSocket')
local constants = require('constants')
local enums = require('enums')
local logLevel = enums.logLevel
local min, max, random = math.min, math.max, math.random
local null = json.null
local format = string.format
local sleep = timer.sleep
local setInterval, clearInterval = timer.setInterval, timer.clearInterval
local wrap = coroutine.wrap
local ID_DELAY = constants.ID_DELAY
local DISPATCH = 0
local HEARTBEAT = 1
local IDENTIFY = 2
local STATUS_UPDATE = 3
local VOICE_STATE_UPDATE = 4
-- local VOICE_SERVER_PING = 5 -- TODO
local RESUME = 6
local RECONNECT = 7
local REQUEST_GUILD_MEMBERS = 8
local INVALID_SESSION = 9
local HELLO = 10
local HEARTBEAT_ACK = 11
local GUILD_SYNC = 12
local ignore = {
['CALL_DELETE'] = true,
['CHANNEL_PINS_ACK'] = true,
['GUILD_INTEGRATIONS_UPDATE'] = true,
['MESSAGE_ACK'] = true,
['PRESENCES_REPLACE'] = true,
['USER_SETTINGS_UPDATE'] = true,
['USER_GUILD_SETTINGS_UPDATE'] = true,
['SESSIONS_REPLACE'] = true,
['INVITE_CREATE'] = true,
['INVITE_DELETE'] = true,
['INTEGRATION_CREATE'] = true,
['INTEGRATION_UPDATE'] = true,
['INTEGRATION_DELETE'] = true,
}
local Shard = require('class')('Shard', WebSocket)
function Shard:__init(id, client)
WebSocket.__init(self, client)
self._id = id
self._client = client
self._backoff = 1000
end
for name in pairs(logLevel) do
Shard[name] = function(self, fmt, ...)
local client = self._client
return client[name](client, format('Shard %i : %s', self._id, fmt), ...)
end
end
function Shard:__tostring()
return format('Shard: %i', self._id)
end
local function getReconnectTime(self, n, m)
return self._backoff * (n + random() * (m - n))
end
local function incrementReconnectTime(self)
self._backoff = min(self._backoff * 2, 60000)
end
local function decrementReconnectTime(self)
self._backoff = max(self._backoff / 2, 1000)
end
function Shard:handleDisconnect(url, path)
self._client:emit('shardDisconnect', self._id)
if self._reconnect then
self:info('Reconnecting...')
return self:connect(url, path)
elseif self._reconnect == nil and self._client._options.autoReconnect then
local backoff = getReconnectTime(self, 0.9, 1.1)
incrementReconnectTime(self)
self:info('Reconnecting after %i ms...', backoff)
sleep(backoff)
return self:connect(url, path)
end
end
function Shard:handlePayload(payload)
local client = self._client
local s = payload.s
local t = payload.t
local d = payload.d
local op = payload.op
if t ~= null then
self:debug('WebSocket OP %s : %s : %s', op, t, s)
else
self:debug('WebSocket OP %s', op)
end
if op == DISPATCH then
self._seq = s
if not ignore[t] then
EventHandler[t](d, client, self)
end
elseif op == HEARTBEAT then
self:heartbeat()
elseif op == RECONNECT then
self:info('Discord has requested a reconnection')
self:disconnect(true)
elseif op == INVALID_SESSION then
if payload.d and self._session_id then
self:info('Session invalidated, resuming...')
self:resume()
else
self:info('Session invalidated, re-identifying...')
sleep(random(1000, 5000))
self:identify()
end
elseif op == HELLO then
self:info('Received HELLO')
self:startHeartbeat(d.heartbeat_interval)
if self._session_id then
self:resume()
else
self:identify()
end
elseif op == HEARTBEAT_ACK then
client:emit('heartbeat', self._id, self._sw.milliseconds)
elseif op then
self:warning('Unhandled WebSocket payload OP %i', op)
end
end
local function loop(self)
decrementReconnectTime(self)
return wrap(self.heartbeat)(self)
end
function Shard:startHeartbeat(interval)
if self._heartbeat then
clearInterval(self._heartbeat)
end
self._heartbeat = setInterval(interval, loop, self)
end
function Shard:stopHeartbeat()
if self._heartbeat then
clearInterval(self._heartbeat)
end
self._heartbeat = nil
end
function Shard:identifyWait()
if self:waitFor('READY', 1.5 * ID_DELAY) then
return sleep(ID_DELAY)
end
end
function Shard:heartbeat()
self._sw:reset()
return self:_send(HEARTBEAT, self._seq or json.null)
end
function Shard:identify()
local client = self._client
local mutex = client._mutex
local options = client._options
mutex:lock()
wrap(function()
self:identifyWait()
mutex:unlock()
end)()
self._seq = nil
self._session_id = nil
self._ready = false
self._loading = {guilds = {}, chunks = {}, syncs = {}}
return self:_send(IDENTIFY, {
token = client._token,
properties = {
['$os'] = jit.os,
['$browser'] = 'Discordia',
['$device'] = 'Discordia',
['$referrer'] = '',
['$referring_domain'] = '',
},
compress = options.compress,
large_threshold = options.largeThreshold,
shard = {self._id, client._total_shard_count},
presence = next(client._presence) and client._presence,
}, true)
end
function Shard:resume()
return self:_send(RESUME, {
token = self._client._token,
session_id = self._session_id,
seq = self._seq
})
end
function Shard:requestGuildMembers(id)
return self:_send(REQUEST_GUILD_MEMBERS, {
guild_id = id,
query = '',
limit = 0,
})
end
function Shard:updateStatus(presence)
return self:_send(STATUS_UPDATE, presence)
end
function Shard:updateVoice(guild_id, channel_id, self_mute, self_deaf)
return self:_send(VOICE_STATE_UPDATE, {
guild_id = guild_id,
channel_id = channel_id or null,
self_mute = self_mute or false,
self_deaf = self_deaf or false,
})
end
function Shard:syncGuilds(ids)
return self:_send(GUILD_SYNC, ids)
end
return Shard