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