diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index baca545..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "deno.enable": true, - "deno.lint": false, - "deno.unstable": false, - "deepscan.enable": true, - "deno.import_intellisense_origins": { - "https://deno.land": true - }, - "editor.tabSize": 2, - "editor.formatOnSave": true -} diff --git a/mod.ts b/mod.ts index b47aa82..98f5d5b 100644 --- a/mod.ts +++ b/mod.ts @@ -55,7 +55,11 @@ export { Invite } from './src/structures/invite.ts' export * from './src/structures/member.ts' export { Message } from './src/structures/message.ts' export { MessageMentions } from './src/structures/messageMentions.ts' -export { Presence, ClientPresence } from './src/structures/presence.ts' +export { + Presence, + ClientPresence, + ActivityTypes +} from './src/structures/presence.ts' export { Role } from './src/structures/role.ts' export { Snowflake } from './src/structures/snowflake.ts' export { TextChannel, GuildTextChannel } from './src/structures/textChannel.ts' @@ -67,4 +71,12 @@ export { Intents } from './src/utils/intents.ts' // export { getBuildInfo } from './src/utils/buildInfo.ts' export * from './src/utils/permissions.ts' export { UserFlagsManager } from './src/utils/userFlags.ts' +export type { EveryChannelTypes } from './src/utils/getChannelByType.ts' export * from './src/utils/bitfield.ts' +export type { + ActivityGame, + ClientActivity, + ClientStatus, + StatusType +} from './src/types/presence.ts' +export { ChannelTypes } from './src/types/channel.ts' diff --git a/src/gateway/index.ts b/src/gateway/index.ts index d234661..f0ad00d 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -24,6 +24,8 @@ export interface RequestMembersOptions { users?: string[] } +export const RECONNECT_REASON = 'harmony-reconnect' + /** * Handles Discord gateway connection. * @@ -43,6 +45,7 @@ class Gateway { private heartbeatServerResponded = false client: Client cache: GatewayCache + private timedIdentify: number | null = null constructor(client: Client, token: string, intents: GatewayIntents[]) { this.token = token @@ -109,10 +112,20 @@ class Gateway { case GatewayOpcodes.INVALID_SESSION: // Because we know this gonna be bool - this.debug(`Invalid Session! Identifying with forced new session`) - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - // eslint-disable-next-line @typescript-eslint/promise-function-async - setTimeout(() => this.sendIdentify(true), 3000) + this.debug( + `Invalid Session received! Resumeable? ${d === true ? 'Yes' : 'No'}` + ) + if (d !== true) { + this.debug(`Session was invalidated, deleting from cache`) + await this.cache.delete('session_id') + await this.cache.delete('seq') + this.sessionID = undefined + this.sequenceID = undefined + } + this.timedIdentify = setTimeout(async () => { + this.timedIdentify = null + await this.sendIdentify(!(d as boolean)) + }, 5000) break case GatewayOpcodes.DISPATCH: { @@ -151,6 +164,7 @@ class Gateway { } private async onclose(event: CloseEvent): Promise { + if (event.reason === RECONNECT_REASON) return this.debug(`Connection Closed with code: ${event.code}`) if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) { @@ -180,7 +194,7 @@ class Gateway { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.reconnect() } else if (event.code === GatewayCloseCodes.SHARDING_REQUIRED) { - throw new Error("Couldn't connect. Sharding is requried!") + throw new Error("Couldn't connect. Sharding is required!") } else if (event.code === GatewayCloseCodes.INVALID_API_VERSION) { throw new Error("Invalid API Version was used. This shouldn't happen!") } else if (event.code === GatewayCloseCodes.INVALID_INTENTS) { @@ -191,9 +205,12 @@ class Gateway { this.debug( 'Unknown Close code, probably connection error. Reconnecting in 5s.' ) + if (this.timedIdentify !== null) { + clearTimeout(this.timedIdentify) + this.debug('Timed Identify found. Cleared timeout.') + } await delay(5000) - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.reconnect() + await this.reconnect(true) } } @@ -216,13 +233,14 @@ class Gateway { `Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}` ) this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`) - if (forceNewSession === undefined || !forceNewSession) { - const sessionIDCached = await this.cache.get('session_id') - if (sessionIDCached !== undefined) { - this.debug(`Found Cached SessionID: ${sessionIDCached}`) - this.sessionID = sessionIDCached - return await this.sendResume() - } + } else this.debug('Skipping /gateway/bot because bot: false') + + if (forceNewSession === undefined || !forceNewSession) { + const sessionIDCached = await this.cache.get('session_id') + if (sessionIDCached !== undefined) { + this.debug(`Found Cached SessionID: ${sessionIDCached}`) + this.sessionID = sessionIDCached + return await this.sendResume() } } @@ -255,6 +273,7 @@ class Gateway { } } + this.debug('Sending Identify payload...') this.send({ op: GatewayOpcodes.IDENTIFY, d: payload @@ -262,6 +281,10 @@ class Gateway { } private async sendResume(): Promise { + if (this.sessionID === undefined) { + this.sessionID = await this.cache.get('session_id') + if (this.sessionID === undefined) return await this.sendIdentify() + } this.debug(`Preparing to resume with Session: ${this.sessionID}`) if (this.sequenceID === undefined) { const cached = await this.cache.get('seq') @@ -276,6 +299,7 @@ class Gateway { seq: this.sequenceID ?? null } } + this.debug('Sending Resume payload...') this.send(resumePayload) } @@ -305,13 +329,16 @@ class Gateway { async reconnect(forceNew?: boolean): Promise { clearInterval(this.heartbeatIntervalID) - if (forceNew === undefined || !forceNew) + if (forceNew === true) { await this.cache.delete('session_id') - this.close() + await this.cache.delete('seq') + } + this.close(1000, RECONNECT_REASON) this.initWebsocket() } initWebsocket(): void { + this.debug('Initializing WebSocket...') this.websocket = new WebSocket( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`, @@ -324,8 +351,8 @@ class Gateway { this.websocket.onerror = this.onerror.bind(this) } - close(): void { - this.websocket.close(1000) + close(code: number = 1000, reason?: string): void { + return this.websocket.close(code, reason) } send(data: GatewayResponse): boolean { @@ -362,6 +389,7 @@ class Gateway { if (this.heartbeatServerResponded) { this.heartbeatServerResponded = false } else { + this.debug('Found dead connection, reconnecting...') clearInterval(this.heartbeatIntervalID) // eslint-disable-next-line @typescript-eslint/no-floating-promises this.reconnect() diff --git a/src/structures/presence.ts b/src/structures/presence.ts index 2ab724d..0fdac0d 100644 --- a/src/structures/presence.ts +++ b/src/structures/presence.ts @@ -11,7 +11,7 @@ import { Guild } from './guild.ts' import { User } from './user.ts' import { Client } from '../models/client.ts' -enum ActivityTypes { +export enum ActivityTypes { PLAYING = 0, STREAMING = 1, LISTENING = 2, diff --git a/src/test/index.ts b/src/test/index.ts index 65c4b95..2da0845 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -92,4 +92,10 @@ client.on('messageCreate', async (msg: Message) => { } }) -client.connect(TOKEN, Intents.None) +client.connect(TOKEN, Intents.All) + +// OLD: Was a way to reproduce reconnect infinite loop +// setTimeout(() => { +// console.log('[DEBUG] Reconnect') +// client.gateway?.reconnect() +// }, 1000 * 4)