2021-01-20 10:05:15 +00:00
|
|
|
import { unzlib } from '../../deps.ts'
|
2021-04-04 05:42:15 +00:00
|
|
|
import { Client } from '../client/mod.ts'
|
2020-10-30 14:51:40 +00:00
|
|
|
import { GatewayResponse } from '../types/gatewayResponse.ts'
|
2020-11-02 06:58:23 +00:00
|
|
|
import {
|
|
|
|
GatewayOpcodes,
|
2020-11-25 11:53:40 +00:00
|
|
|
GatewayCloseCodes,
|
|
|
|
IdentityPayload,
|
2021-01-20 10:05:15 +00:00
|
|
|
StatusUpdatePayload,
|
|
|
|
GatewayEvents
|
2020-11-02 06:58:23 +00:00
|
|
|
} from '../types/gateway.ts'
|
2021-04-04 05:42:15 +00:00
|
|
|
import { gatewayHandlers } from './handlers/mod.ts'
|
2020-11-08 07:20:28 +00:00
|
|
|
import { GatewayCache } from '../managers/gatewayCache.ts'
|
2020-11-25 11:53:40 +00:00
|
|
|
import { delay } from '../utils/delay.ts'
|
2020-12-05 08:56:43 +00:00
|
|
|
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
|
|
|
import { Guild } from '../structures/guild.ts'
|
2021-01-20 10:05:15 +00:00
|
|
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
2021-04-04 04:51:39 +00:00
|
|
|
import { decodeText } from '../utils/encoding.ts'
|
2021-04-04 05:42:15 +00:00
|
|
|
import { Constants } from '../types/constants.ts'
|
2020-10-30 14:51:40 +00:00
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
export interface RequestMembersOptions {
|
|
|
|
limit?: number
|
|
|
|
presences?: boolean
|
|
|
|
query?: string
|
|
|
|
users?: string[]
|
|
|
|
}
|
|
|
|
|
2020-12-05 08:56:43 +00:00
|
|
|
export interface VoiceStateOptions {
|
|
|
|
mute?: boolean
|
|
|
|
deaf?: boolean
|
|
|
|
}
|
|
|
|
|
2020-12-04 05:53:10 +00:00
|
|
|
export const RECONNECT_REASON = 'harmony-reconnect'
|
|
|
|
|
2021-01-20 10:05:15 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
|
|
export type GatewayTypedEvents = {
|
|
|
|
[name in GatewayEvents]: [any]
|
|
|
|
} & {
|
|
|
|
connect: []
|
|
|
|
ping: [number]
|
|
|
|
resume: []
|
|
|
|
reconnectRequired: []
|
|
|
|
close: [number, string]
|
|
|
|
error: [Error, ErrorEvent]
|
|
|
|
sentIdentify: []
|
|
|
|
sentResume: []
|
|
|
|
reconnecting: []
|
|
|
|
init: []
|
|
|
|
}
|
|
|
|
|
2020-10-30 14:51:40 +00:00
|
|
|
/**
|
2020-12-20 09:45:49 +00:00
|
|
|
* Handles Discord Gateway connection.
|
2020-12-03 04:06:41 +00:00
|
|
|
*
|
2020-10-30 14:51:40 +00:00
|
|
|
* You should not use this and rather use Client class.
|
|
|
|
*/
|
2021-01-20 10:05:15 +00:00
|
|
|
export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
|
|
|
websocket?: WebSocket
|
2020-10-30 14:51:40 +00:00
|
|
|
connected = false
|
|
|
|
initialized = false
|
|
|
|
heartbeatInterval = 0
|
|
|
|
heartbeatIntervalID?: number
|
|
|
|
sequenceID?: number
|
2020-10-31 11:45:33 +00:00
|
|
|
lastPingTimestamp = 0
|
2020-11-01 11:22:09 +00:00
|
|
|
sessionID?: string
|
2020-10-30 14:51:40 +00:00
|
|
|
private heartbeatServerResponded = false
|
|
|
|
client: Client
|
2020-11-01 11:22:09 +00:00
|
|
|
cache: GatewayCache
|
2020-12-04 05:53:10 +00:00
|
|
|
private timedIdentify: number | null = null
|
2021-01-20 10:05:15 +00:00
|
|
|
shards?: number[]
|
2020-10-30 14:51:40 +00:00
|
|
|
|
2021-01-20 10:05:15 +00:00
|
|
|
constructor(client: Client, shards?: number[]) {
|
2020-12-20 09:45:49 +00:00
|
|
|
super()
|
2020-10-30 14:51:40 +00:00
|
|
|
this.client = client
|
2020-11-01 11:22:09 +00:00
|
|
|
this.cache = new GatewayCache(client)
|
2021-01-20 10:05:15 +00:00
|
|
|
this.shards = shards
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
private onopen(): void {
|
2020-10-30 14:51:40 +00:00
|
|
|
this.connected = true
|
2020-11-02 06:58:23 +00:00
|
|
|
this.debug('Connected to Gateway!')
|
2020-12-20 09:45:49 +00:00
|
|
|
this.emit('connect')
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
private async onmessage(event: MessageEvent): Promise<void> {
|
2020-10-30 14:51:40 +00:00
|
|
|
let data = event.data
|
|
|
|
if (data instanceof ArrayBuffer) {
|
|
|
|
data = new Uint8Array(data)
|
|
|
|
}
|
|
|
|
if (data instanceof Uint8Array) {
|
|
|
|
data = unzlib(data)
|
2021-04-04 04:51:39 +00:00
|
|
|
data = decodeText(data)
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const { op, d, s, t }: GatewayResponse = JSON.parse(data)
|
|
|
|
|
|
|
|
switch (op) {
|
|
|
|
case GatewayOpcodes.HELLO:
|
|
|
|
this.heartbeatInterval = d.heartbeat_interval
|
2020-11-02 06:58:23 +00:00
|
|
|
this.debug(
|
|
|
|
`Received HELLO. Heartbeat Interval: ${this.heartbeatInterval}`
|
|
|
|
)
|
2020-10-30 14:51:40 +00:00
|
|
|
|
2020-11-06 08:03:28 +00:00
|
|
|
this.sendHeartbeat()
|
|
|
|
this.heartbeatIntervalID = setInterval(() => {
|
|
|
|
this.heartbeat()
|
2020-10-30 14:51:40 +00:00
|
|
|
}, this.heartbeatInterval)
|
|
|
|
|
|
|
|
if (!this.initialized) {
|
|
|
|
this.initialized = true
|
2021-01-24 16:46:10 +00:00
|
|
|
this.enqueueIdentify(this.client.forceNewSession)
|
2020-10-30 14:51:40 +00:00
|
|
|
} else {
|
2020-11-02 06:58:23 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2020-10-30 14:51:40 +00:00
|
|
|
this.sendResume()
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case GatewayOpcodes.HEARTBEAT_ACK:
|
|
|
|
this.heartbeatServerResponded = true
|
2020-10-31 11:45:33 +00:00
|
|
|
this.client.ping = Date.now() - this.lastPingTimestamp
|
2020-12-20 09:45:49 +00:00
|
|
|
this.emit('ping', this.client.ping)
|
2020-11-02 06:58:23 +00:00
|
|
|
this.debug(
|
|
|
|
`Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms`
|
|
|
|
)
|
2020-10-30 14:51:40 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
case GatewayOpcodes.INVALID_SESSION:
|
|
|
|
// Because we know this gonna be bool
|
2020-12-04 05:53:10 +00:00
|
|
|
this.debug(
|
|
|
|
`Invalid Session received! Resumeable? ${d === true ? 'Yes' : 'No'}`
|
|
|
|
)
|
|
|
|
if (d !== true) {
|
|
|
|
this.debug(`Session was invalidated, deleting from cache`)
|
2021-01-24 16:46:10 +00:00
|
|
|
await this.cache.delete(`session_id_${this.shards?.join('-') ?? '0'}`)
|
|
|
|
await this.cache.delete(`seq_${this.shards?.join('-') ?? '0'}`)
|
2020-12-04 05:53:10 +00:00
|
|
|
this.sessionID = undefined
|
|
|
|
this.sequenceID = undefined
|
|
|
|
}
|
|
|
|
this.timedIdentify = setTimeout(async () => {
|
|
|
|
this.timedIdentify = null
|
2021-01-24 16:46:10 +00:00
|
|
|
this.enqueueIdentify(!(d as boolean))
|
2020-12-04 05:53:10 +00:00
|
|
|
}, 5000)
|
2020-10-30 14:51:40 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
case GatewayOpcodes.DISPATCH: {
|
|
|
|
this.heartbeatServerResponded = true
|
|
|
|
if (s !== null) {
|
|
|
|
this.sequenceID = s
|
2021-01-24 16:46:10 +00:00
|
|
|
await this.cache.set(`seq_${this.shards?.join('-') ?? '0'}`, s)
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
if (t !== null && t !== undefined) {
|
2021-01-20 10:05:15 +00:00
|
|
|
this.emit(t as any, d)
|
2020-11-15 08:04:24 +00:00
|
|
|
this.client.emit('raw', t, d)
|
2020-11-25 11:53:40 +00:00
|
|
|
|
2020-10-30 14:51:40 +00:00
|
|
|
const handler = gatewayHandlers[t]
|
|
|
|
|
2021-02-10 12:29:21 +00:00
|
|
|
if (handler !== undefined && d !== null) {
|
2020-10-30 14:51:40 +00:00
|
|
|
handler(this, d)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
2020-11-01 11:22:09 +00:00
|
|
|
case GatewayOpcodes.RESUME: {
|
|
|
|
// this.token = d.token
|
|
|
|
this.sessionID = d.session_id
|
|
|
|
this.sequenceID = d.seq
|
2021-01-24 16:46:10 +00:00
|
|
|
await this.cache.set(`seq_${this.shards?.join('-') ?? '0'}`, d.seq)
|
|
|
|
await this.cache.set(
|
|
|
|
`session_id_${this.shards?.join('-') ?? '0'}`,
|
|
|
|
this.sessionID
|
|
|
|
)
|
2020-12-20 09:45:49 +00:00
|
|
|
this.emit('resume')
|
2020-11-01 11:22:09 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
case GatewayOpcodes.RECONNECT: {
|
2020-12-20 09:45:49 +00:00
|
|
|
this.emit('reconnectRequired')
|
2021-03-29 04:09:37 +00:00
|
|
|
this.debug('Received OpCode RECONNECT')
|
|
|
|
await this.reconnect()
|
2020-11-01 11:22:09 +00:00
|
|
|
break
|
|
|
|
}
|
2020-10-30 14:51:40 +00:00
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-25 16:18:35 +00:00
|
|
|
private async onclose({ reason, code }: CloseEvent): Promise<void> {
|
|
|
|
if (reason === RECONNECT_REASON) return
|
|
|
|
this.emit('close', code, reason)
|
|
|
|
this.debug(`Connection Closed with code: ${code}`)
|
2020-11-01 11:22:09 +00:00
|
|
|
|
2020-12-25 16:18:35 +00:00
|
|
|
switch (code) {
|
2020-12-25 16:13:06 +00:00
|
|
|
case GatewayCloseCodes.UNKNOWN_ERROR:
|
|
|
|
this.debug('API has encountered Unknown Error. Reconnecting...')
|
2021-03-29 04:09:37 +00:00
|
|
|
await this.reconnect()
|
2020-12-25 16:13:06 +00:00
|
|
|
break
|
|
|
|
case GatewayCloseCodes.UNKNOWN_OPCODE:
|
2021-01-01 04:48:18 +00:00
|
|
|
throw new Error(
|
|
|
|
"Invalid OP Code or Payload was sent. This shouldn't happen!"
|
|
|
|
)
|
2020-12-25 16:13:06 +00:00
|
|
|
case GatewayCloseCodes.DECODE_ERROR:
|
|
|
|
throw new Error("Invalid Payload was sent. This shouldn't happen!")
|
|
|
|
case GatewayCloseCodes.NOT_AUTHENTICATED:
|
|
|
|
throw new Error('Not Authorized: Payload was sent before Identifying.')
|
|
|
|
case GatewayCloseCodes.AUTHENTICATION_FAILED:
|
|
|
|
throw new Error('Invalid Token provided!')
|
|
|
|
case GatewayCloseCodes.INVALID_SEQ:
|
|
|
|
this.debug('Invalid Seq was sent. Reconnecting.')
|
2021-03-29 04:09:37 +00:00
|
|
|
await this.reconnect()
|
2020-12-25 16:13:06 +00:00
|
|
|
break
|
|
|
|
case GatewayCloseCodes.RATE_LIMITED:
|
|
|
|
throw new Error("You're ratelimited. Calm down.")
|
|
|
|
case GatewayCloseCodes.SESSION_TIMED_OUT:
|
|
|
|
this.debug('Session Timeout. Reconnecting.')
|
2021-03-29 04:09:37 +00:00
|
|
|
await this.reconnect(true)
|
2020-12-25 16:13:06 +00:00
|
|
|
break
|
|
|
|
case GatewayCloseCodes.INVALID_SHARD:
|
|
|
|
this.debug('Invalid Shard was sent. Reconnecting.')
|
2021-03-29 04:09:37 +00:00
|
|
|
await this.reconnect()
|
2020-12-25 16:13:06 +00:00
|
|
|
break
|
|
|
|
case GatewayCloseCodes.SHARDING_REQUIRED:
|
|
|
|
throw new Error("Couldn't connect. Sharding is required!")
|
|
|
|
case GatewayCloseCodes.INVALID_API_VERSION:
|
|
|
|
throw new Error("Invalid API Version was used. This shouldn't happen!")
|
|
|
|
case GatewayCloseCodes.INVALID_INTENTS:
|
|
|
|
throw new Error('Invalid Intents')
|
|
|
|
case GatewayCloseCodes.DISALLOWED_INTENTS:
|
|
|
|
throw new Error("Given Intents aren't allowed")
|
|
|
|
default:
|
|
|
|
this.debug(
|
|
|
|
'Unknown Close code, probably connection error. Reconnecting in 5s.'
|
|
|
|
)
|
2021-01-24 16:46:10 +00:00
|
|
|
|
2020-12-25 16:13:06 +00:00
|
|
|
if (this.timedIdentify !== null) {
|
|
|
|
clearTimeout(this.timedIdentify)
|
|
|
|
this.debug('Timed Identify found. Cleared timeout.')
|
|
|
|
}
|
2021-01-24 16:46:10 +00:00
|
|
|
|
2020-12-25 16:13:06 +00:00
|
|
|
await delay(5000)
|
|
|
|
await this.reconnect(true)
|
|
|
|
break
|
2020-11-01 11:22:09 +00:00
|
|
|
}
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
|
2021-01-20 10:05:15 +00:00
|
|
|
private async onerror(event: ErrorEvent): Promise<void> {
|
|
|
|
const error = new Error(
|
|
|
|
Deno.inspect({
|
|
|
|
message: event.message,
|
|
|
|
error: event.error,
|
|
|
|
type: event.type,
|
|
|
|
target: event.target
|
|
|
|
})
|
|
|
|
)
|
|
|
|
error.name = 'ErrorEvent'
|
|
|
|
console.log(error)
|
|
|
|
this.emit('error', error, event)
|
2021-03-29 04:09:37 +00:00
|
|
|
this.client.emit('gatewayError', event, this.shards)
|
2021-01-24 16:46:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private enqueueIdentify(forceNew?: boolean): void {
|
|
|
|
this.client.shards.enqueueIdentify(
|
|
|
|
async () => await this.sendIdentify(forceNew)
|
|
|
|
)
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
private async sendIdentify(forceNewSession?: boolean): Promise<void> {
|
2021-02-12 11:37:38 +00:00
|
|
|
if (typeof this.client.token !== 'string')
|
|
|
|
throw new Error('Token not specified')
|
|
|
|
if (typeof this.client.intents !== 'object')
|
2021-01-20 10:05:15 +00:00
|
|
|
throw new Error('Intents not specified')
|
|
|
|
|
|
|
|
if (this.client.fetchGatewayInfo === true) {
|
|
|
|
this.debug('Fetching /gateway/bot...')
|
|
|
|
const info = await this.client.rest.api.gateway.bot.get()
|
|
|
|
if (info.session_start_limit.remaining === 0)
|
|
|
|
throw new Error(
|
|
|
|
`Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms`
|
|
|
|
)
|
|
|
|
this.debug(`Recommended Shards: ${info.shards}`)
|
|
|
|
this.debug('=== Session Limit Info ===')
|
|
|
|
this.debug(
|
|
|
|
`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`
|
2020-11-02 06:58:23 +00:00
|
|
|
)
|
2021-01-20 10:05:15 +00:00
|
|
|
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
|
|
|
|
}
|
2020-12-04 05:53:10 +00:00
|
|
|
|
|
|
|
if (forceNewSession === undefined || !forceNewSession) {
|
2021-01-24 16:46:10 +00:00
|
|
|
const sessionIDCached = await this.cache.get(
|
|
|
|
`session_id_${this.shards?.join('-') ?? '0'}`
|
|
|
|
)
|
2020-12-04 05:53:10 +00:00
|
|
|
if (sessionIDCached !== undefined) {
|
|
|
|
this.debug(`Found Cached SessionID: ${sessionIDCached}`)
|
|
|
|
this.sessionID = sessionIDCached
|
|
|
|
return await this.sendResume()
|
2020-11-01 11:22:09 +00:00
|
|
|
}
|
|
|
|
}
|
2020-11-06 10:42:00 +00:00
|
|
|
|
2020-11-25 11:53:40 +00:00
|
|
|
const payload: IdentityPayload = {
|
2021-02-12 11:37:38 +00:00
|
|
|
token: this.client.token,
|
2020-11-25 11:53:40 +00:00
|
|
|
properties: {
|
2020-12-09 05:18:39 +00:00
|
|
|
$os: this.client.clientProperties.os ?? Deno.build.os,
|
|
|
|
$browser: this.client.clientProperties.browser ?? 'harmony',
|
|
|
|
$device: this.client.clientProperties.device ?? 'harmony'
|
2020-11-25 11:53:40 +00:00
|
|
|
},
|
|
|
|
compress: true,
|
2021-01-20 10:05:15 +00:00
|
|
|
shard:
|
|
|
|
this.shards === undefined
|
|
|
|
? [0, 1]
|
|
|
|
: [this.shards[0] ?? 0, this.shards[1] ?? 1],
|
2021-02-12 11:37:38 +00:00
|
|
|
intents: this.client.intents.reduce(
|
2020-11-25 11:53:40 +00:00
|
|
|
(previous, current) => previous | current,
|
|
|
|
0
|
|
|
|
),
|
2020-12-02 12:29:52 +00:00
|
|
|
presence: this.client.presence.create()
|
2020-11-06 10:42:00 +00:00
|
|
|
}
|
|
|
|
|
2020-12-04 05:53:10 +00:00
|
|
|
this.debug('Sending Identify payload...')
|
2020-12-20 09:45:49 +00:00
|
|
|
this.emit('sentIdentify')
|
2020-11-25 11:53:40 +00:00
|
|
|
this.send({
|
|
|
|
op: GatewayOpcodes.IDENTIFY,
|
2020-12-02 12:29:52 +00:00
|
|
|
d: payload
|
2020-11-25 11:53:40 +00:00
|
|
|
})
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
private async sendResume(): Promise<void> {
|
2021-02-12 11:37:38 +00:00
|
|
|
if (typeof this.client.token !== 'string')
|
|
|
|
throw new Error('Token not specified')
|
2021-01-20 10:05:15 +00:00
|
|
|
|
2020-12-04 05:53:10 +00:00
|
|
|
if (this.sessionID === undefined) {
|
2021-01-24 16:46:10 +00:00
|
|
|
this.sessionID = await this.cache.get(
|
|
|
|
`session_id_${this.shards?.join('-') ?? '0'}`
|
|
|
|
)
|
|
|
|
if (this.sessionID === undefined) return this.enqueueIdentify()
|
2020-12-04 05:53:10 +00:00
|
|
|
}
|
2020-10-31 11:45:33 +00:00
|
|
|
this.debug(`Preparing to resume with Session: ${this.sessionID}`)
|
2020-11-02 07:27:14 +00:00
|
|
|
if (this.sequenceID === undefined) {
|
2021-01-24 16:46:10 +00:00
|
|
|
const cached = await this.cache.get(
|
|
|
|
`seq_${this.shards?.join('-') ?? '0'}`
|
|
|
|
)
|
2020-11-02 06:58:23 +00:00
|
|
|
if (cached !== undefined)
|
|
|
|
this.sequenceID = typeof cached === 'string' ? parseInt(cached) : cached
|
2020-11-01 11:22:09 +00:00
|
|
|
}
|
|
|
|
const resumePayload = {
|
|
|
|
op: GatewayOpcodes.RESUME,
|
|
|
|
d: {
|
2021-02-12 11:37:38 +00:00
|
|
|
token: this.client.token,
|
2020-11-01 11:22:09 +00:00
|
|
|
session_id: this.sessionID,
|
2020-12-02 12:29:52 +00:00
|
|
|
seq: this.sequenceID ?? null
|
|
|
|
}
|
2020-11-01 11:22:09 +00:00
|
|
|
}
|
2020-12-20 09:45:49 +00:00
|
|
|
this.emit('sentResume')
|
2020-12-04 05:53:10 +00:00
|
|
|
this.debug('Sending Resume payload...')
|
2020-11-02 07:27:14 +00:00
|
|
|
this.send(resumePayload)
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
requestMembers(guild: string, options: RequestMembersOptions = {}): string {
|
|
|
|
if (options.query !== undefined && options.limit === undefined)
|
|
|
|
throw new Error(
|
|
|
|
'Missing limit property when specifying query for Requesting Members!'
|
|
|
|
)
|
|
|
|
const nonce = `${guild}_${new Date().getTime()}`
|
|
|
|
this.send({
|
|
|
|
op: GatewayOpcodes.REQUEST_GUILD_MEMBERS,
|
|
|
|
d: {
|
|
|
|
guild_id: guild,
|
2021-01-01 04:48:18 +00:00
|
|
|
query: options.query ?? '',
|
|
|
|
limit: options.limit ?? 0,
|
2020-12-01 11:18:38 +00:00
|
|
|
presences: options.presences,
|
|
|
|
user_ids: options.users,
|
2020-12-02 12:29:52 +00:00
|
|
|
nonce
|
|
|
|
}
|
2020-12-01 11:18:38 +00:00
|
|
|
})
|
|
|
|
return nonce
|
|
|
|
}
|
|
|
|
|
2020-12-05 08:56:43 +00:00
|
|
|
updateVoiceState(
|
|
|
|
guild: Guild | string,
|
|
|
|
channel?: VoiceChannel | string,
|
|
|
|
voiceOptions: VoiceStateOptions = {}
|
|
|
|
): void {
|
|
|
|
this.send({
|
|
|
|
op: GatewayOpcodes.VOICE_STATE_UPDATE,
|
|
|
|
d: {
|
|
|
|
guild_id: typeof guild === 'string' ? guild : guild.id,
|
|
|
|
channel_id:
|
|
|
|
channel === undefined
|
|
|
|
? null
|
|
|
|
: typeof channel === 'string'
|
2021-04-04 04:51:39 +00:00
|
|
|
? channel
|
|
|
|
: channel?.id,
|
2020-12-05 08:56:43 +00:00
|
|
|
self_mute: voiceOptions.mute === undefined ? false : voiceOptions.mute,
|
|
|
|
self_deaf: voiceOptions.deaf === undefined ? false : voiceOptions.deaf
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
debug(msg: string): void {
|
2020-11-02 06:58:23 +00:00
|
|
|
this.client.debug('Gateway', msg)
|
2020-10-31 11:45:33 +00:00
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
async reconnect(forceNew?: boolean): Promise<void> {
|
2020-12-20 09:45:49 +00:00
|
|
|
this.emit('reconnecting')
|
2021-03-29 04:09:37 +00:00
|
|
|
this.debug('Reconnecting... (force new: ' + String(forceNew) + ')')
|
2021-01-24 16:46:10 +00:00
|
|
|
|
2020-11-01 11:22:09 +00:00
|
|
|
clearInterval(this.heartbeatIntervalID)
|
2020-12-04 05:53:10 +00:00
|
|
|
if (forceNew === true) {
|
2021-01-24 16:46:10 +00:00
|
|
|
await this.cache.delete(`session_id_${this.shards?.join('-') ?? '0'}`)
|
|
|
|
await this.cache.delete(`seq_${this.shards?.join('-') ?? '0'}`)
|
2020-12-04 05:53:10 +00:00
|
|
|
}
|
2021-01-24 16:46:10 +00:00
|
|
|
|
2020-12-04 05:53:10 +00:00
|
|
|
this.close(1000, RECONNECT_REASON)
|
2020-11-01 11:22:09 +00:00
|
|
|
this.initWebsocket()
|
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
initWebsocket(): void {
|
2020-12-20 09:45:49 +00:00
|
|
|
this.emit('init')
|
2020-12-04 05:53:10 +00:00
|
|
|
this.debug('Initializing WebSocket...')
|
2020-10-30 14:51:40 +00:00
|
|
|
this.websocket = new WebSocket(
|
|
|
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
2021-04-04 05:42:15 +00:00
|
|
|
`${Constants.DISCORD_GATEWAY_URL}/?v=${Constants.DISCORD_API_VERSION}&encoding=json`,
|
2020-10-30 14:51:40 +00:00
|
|
|
[]
|
|
|
|
)
|
|
|
|
this.websocket.binaryType = 'arraybuffer'
|
|
|
|
this.websocket.onopen = this.onopen.bind(this)
|
|
|
|
this.websocket.onmessage = this.onmessage.bind(this)
|
|
|
|
this.websocket.onclose = this.onclose.bind(this)
|
2021-01-20 10:05:15 +00:00
|
|
|
this.websocket.onerror = this.onerror.bind(this) as any
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
|
2020-12-04 05:53:10 +00:00
|
|
|
close(code: number = 1000, reason?: string): void {
|
2021-04-04 04:51:39 +00:00
|
|
|
this.debug(
|
|
|
|
`Closing with code ${code}${
|
|
|
|
reason !== undefined && reason !== '' ? ` and reason ${reason}` : ''
|
|
|
|
}`
|
|
|
|
)
|
2021-01-20 10:05:15 +00:00
|
|
|
return this.websocket?.close(code, reason)
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
2020-11-02 07:27:14 +00:00
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
send(data: GatewayResponse): boolean {
|
2021-01-20 10:05:15 +00:00
|
|
|
if (this.websocket?.readyState !== this.websocket?.OPEN) return false
|
2021-01-01 04:48:18 +00:00
|
|
|
const packet = JSON.stringify({
|
|
|
|
op: data.op,
|
|
|
|
d: data.d,
|
|
|
|
s: typeof data.s === 'number' ? data.s : null,
|
|
|
|
t: data.t === undefined ? null : data.t
|
|
|
|
})
|
2021-01-20 10:05:15 +00:00
|
|
|
this.websocket?.send(packet)
|
2020-11-02 07:27:14 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
sendPresence(data: StatusUpdatePayload): void {
|
2020-11-02 07:27:14 +00:00
|
|
|
this.send({
|
|
|
|
op: GatewayOpcodes.PRESENCE_UPDATE,
|
2020-12-02 12:29:52 +00:00
|
|
|
d: data
|
2020-11-02 07:27:14 +00:00
|
|
|
})
|
|
|
|
}
|
2020-11-06 08:03:28 +00:00
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
sendHeartbeat(): void {
|
2020-11-06 08:03:28 +00:00
|
|
|
const payload = {
|
|
|
|
op: GatewayOpcodes.HEARTBEAT,
|
2020-12-02 12:29:52 +00:00
|
|
|
d: this.sequenceID ?? null
|
2020-11-08 07:20:28 +00:00
|
|
|
}
|
2020-11-06 08:03:28 +00:00
|
|
|
|
|
|
|
this.send(payload)
|
|
|
|
this.lastPingTimestamp = Date.now()
|
|
|
|
}
|
|
|
|
|
2020-12-01 11:18:38 +00:00
|
|
|
heartbeat(): void {
|
2020-11-06 08:03:28 +00:00
|
|
|
if (this.heartbeatServerResponded) {
|
|
|
|
this.heartbeatServerResponded = false
|
|
|
|
} else {
|
2020-12-04 05:53:10 +00:00
|
|
|
this.debug('Found dead connection, reconnecting...')
|
2020-11-06 08:03:28 +00:00
|
|
|
clearInterval(this.heartbeatIntervalID)
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
|
|
this.reconnect()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-06 10:42:00 +00:00
|
|
|
this.sendHeartbeat()
|
2020-11-06 08:03:28 +00:00
|
|
|
}
|
2020-10-30 14:51:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export type GatewayEventHandler = (gateway: Gateway, d: any) => void
|