From 04056a7f9c00634152ced890f9d7e66720105ef9 Mon Sep 17 00:00:00 2001 From: DjDeveloperr <=> Date: Mon, 2 Nov 2020 12:57:14 +0530 Subject: [PATCH] Added Presence --- src/gateway/index.ts | 132 ++++++++++++++++++++----------------- src/models/client.ts | 14 +++- src/structures/presence.ts | 120 +++++++++++++++++++++++++++++++++ src/test/index.ts | 15 ++++- 4 files changed, 217 insertions(+), 64 deletions(-) create mode 100644 src/structures/presence.ts diff --git a/src/gateway/index.ts b/src/gateway/index.ts index e6b1d31..7aa72a8 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -10,6 +10,7 @@ import { gatewayHandlers } from './handlers/index.ts' import { GATEWAY_BOT } from '../types/endpoint.ts' import { GatewayBotPayload } from "../types/gatewayBot.ts" import { GatewayCache } from "../managers/GatewayCache.ts" +import { ClientActivityPayload } from "../structures/presence.ts" /** * Handles Discord gateway connection. @@ -32,7 +33,7 @@ class Gateway { client: Client cache: GatewayCache - constructor (client: Client, token: string, intents: GatewayIntents[]) { + constructor(client: Client, token: string, intents: GatewayIntents[]) { this.token = token this.intents = intents this.client = client @@ -49,12 +50,12 @@ class Gateway { this.websocket.onerror = this.onerror.bind(this) } - private onopen (): void { + private onopen(): void { this.connected = true this.debug("Connected to Gateway!") } - private async onmessage (event: MessageEvent): Promise { + private async onmessage(event: MessageEvent): Promise { let data = event.data if (data instanceof ArrayBuffer) { data = new Uint8Array(data) @@ -79,12 +80,10 @@ class Gateway { return } - this.websocket.send( - JSON.stringify({ - op: GatewayOpcodes.HEARTBEAT, - d: this.sequenceID ?? null - }) - ) + this.send({ + op: GatewayOpcodes.HEARTBEAT, + d: this.sequenceID ?? null + }) this.lastPingTimestamp = Date.now() }, this.heartbeatInterval) @@ -142,38 +141,38 @@ class Gateway { } } - private onclose (event: CloseEvent): void { + private onclose(event: CloseEvent): void { this.debug("Connection Closed with code: " + event.code) - if(event.code == GatewayCloseCodes.UNKNOWN_ERROR) { + if (event.code == GatewayCloseCodes.UNKNOWN_ERROR) { this.debug("API has encountered Unknown Error. Reconnecting...") this.reconnect() - } else if(event.code == GatewayCloseCodes.UNKNOWN_OPCODE) { + } else if (event.code == GatewayCloseCodes.UNKNOWN_OPCODE) { throw new Error("Unknown OP Code was sent. This shouldn't happen!") - } else if(event.code == GatewayCloseCodes.DECODE_ERROR) { + } else if (event.code == GatewayCloseCodes.DECODE_ERROR) { throw new Error("Invalid Payload was sent. This shouldn't happen!") - } else if(event.code == GatewayCloseCodes.NOT_AUTHENTICATED) { + } else if (event.code == GatewayCloseCodes.NOT_AUTHENTICATED) { throw new Error("Not Authorized: Payload was sent before Identifying.") - } else if(event.code == GatewayCloseCodes.AUTHENTICATION_FAILED) { + } else if (event.code == GatewayCloseCodes.AUTHENTICATION_FAILED) { throw new Error("Invalid Token provided!") - } else if(event.code == GatewayCloseCodes.INVALID_SEQ) { + } else if (event.code == GatewayCloseCodes.INVALID_SEQ) { this.debug("Invalid Seq was sent. Reconnecting.") this.reconnect() - } else if(event.code == GatewayCloseCodes.RATE_LIMITED) { + } else if (event.code == GatewayCloseCodes.RATE_LIMITED) { throw new Error("You're ratelimited. Calm down.") - } else if(event.code == GatewayCloseCodes.SESSION_TIMED_OUT) { + } else if (event.code == GatewayCloseCodes.SESSION_TIMED_OUT) { this.debug("Session Timeout. Reconnecting.") this.reconnect(true) - } else if(event.code == GatewayCloseCodes.INVALID_SHARD) { + } else if (event.code == GatewayCloseCodes.INVALID_SHARD) { this.debug("Invalid Shard was sent. Reconnecting.") this.reconnect() - } else if(event.code == GatewayCloseCodes.SHARDING_REQUIRED) { + } else if (event.code == GatewayCloseCodes.SHARDING_REQUIRED) { throw new Error("Couldn't connect. Sharding is requried!") - } else if(event.code == GatewayCloseCodes.INVALID_API_VERSION) { + } 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) { + } else if (event.code == GatewayCloseCodes.INVALID_INTENTS) { throw new Error("Invalid Intents") - } else if(event.code == GatewayCloseCodes.DISALLOWED_INTENTS) { + } else if (event.code == GatewayCloseCodes.DISALLOWED_INTENTS) { throw new Error("Given Intents aren't allowed") } else { this.debug("Unknown Close code, probably connection error. Reconnecting.") @@ -181,59 +180,52 @@ class Gateway { } } - private onerror (event: Event | ErrorEvent): void { + private onerror(event: Event | ErrorEvent): void { const eventError = event as ErrorEvent console.log(eventError) } - private async sendIdentify (forceNewSession?: boolean) { + private async sendIdentify(forceNewSession?: boolean) { this.debug("Fetching /gateway/bot...") const info = await this.client.rest.get(GATEWAY_BOT()) as GatewayBotPayload - if(info.session_start_limit.remaining == 0) throw new Error("Session Limit Reached. Retry After " + info.session_start_limit.reset_after + "ms") + 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}`) this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`) - if(!forceNewSession) { + if (!forceNewSession) { let sessionIDCached = await this.cache.get("session_id") - if(sessionIDCached) { + if (sessionIDCached) { this.debug("Found Cached SessionID: " + sessionIDCached) this.sessionID = sessionIDCached return this.sendResume() } } - this.websocket.send( - JSON.stringify({ - op: GatewayOpcodes.IDENTIFY, - d: { - token: this.token, - properties: { - $os: Deno.build.os, - $browser: 'discord.deno', - $device: 'discord.deno' - }, - compress: true, - shard: [0, 1], // TODO: Make sharding possible - intents: this.intents.reduce( - (previous, current) => previous | current, - 0 - ), - presence: { - // TODO: User should can customize this - status: 'online', - since: null, - afk: false - } - } - }) - ) + this.send({ + op: GatewayOpcodes.IDENTIFY, + d: { + token: this.token, + properties: { + $os: Deno.build.os, + $browser: 'discord.deno', + $device: 'discord.deno' + }, + compress: true, + shard: [0, 1], // TODO: Make sharding possible + intents: this.intents.reduce( + (previous, current) => previous | current, + 0 + ), + presence: this.client.presence.create() + } + }) } - private async sendResume (): Promise { + private async sendResume(): Promise { this.debug(`Preparing to resume with Session: ${this.sessionID}`) - if(this.sequenceID === undefined) { + if (this.sequenceID === undefined) { let cached = await this.cache.get("seq") - if(cached) this.sequenceID = typeof cached == "string" ? parseInt(cached) : cached + if (cached) this.sequenceID = typeof cached == "string" ? parseInt(cached) : cached } const resumePayload = { op: GatewayOpcodes.RESUME, @@ -243,9 +235,7 @@ class Gateway { seq: this.sequenceID || null } } - this.websocket.send( - JSON.stringify(resumePayload) - ) + this.send(resumePayload) } debug(msg: string) { @@ -254,12 +244,12 @@ class Gateway { async reconnect(forceNew?: boolean) { clearInterval(this.heartbeatIntervalID) - if(forceNew) await this.cache.delete("session_id") + if (forceNew) await this.cache.delete("session_id") this.close() this.initWebsocket() } - initWebsocket (): void { + initWebsocket(): void { this.websocket = new WebSocket( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`, @@ -272,9 +262,27 @@ class Gateway { this.websocket.onerror = this.onerror.bind(this) } - close (): void { + close(): void { this.websocket.close(1000) } + + send(data: GatewayResponse) { + if (this.websocket.readyState != this.websocket.OPEN) return false + this.websocket.send(JSON.stringify({ + op: data.op, + d: data.d, + s: typeof data.s == "number" ? data.s : null, + t: data.t || null, + })) + return true + } + + sendPresence(data: ClientActivityPayload) { + this.send({ + op: GatewayOpcodes.PRESENCE_UPDATE, + d: data + }) + } } export type GatewayEventHandler = (gateway: Gateway, d: any) => void diff --git a/src/models/client.ts b/src/models/client.ts index c452735..9059091 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -9,13 +9,15 @@ import { GuildManager } from "../managers/GuildsManager.ts" import { EmojisManager } from "../managers/EmojisManager.ts" import { ChannelsManager } from "../managers/ChannelsManager.ts" import { MessagesManager } from "../managers/MessagesManager.ts" +import { ActivityGame, ClientActivity, ClientActivityPayload, ClientPresence } from "../structures/presence.ts" /** Some Client Options to modify behaviour */ export interface ClientOptions { token?: string intents?: GatewayIntents[] cache?: ICacheAdapter, - forceNewSession?: boolean + forceNewSession?: boolean, + presence?: ClientPresence | ClientActivity | ActivityGame } /** @@ -37,12 +39,15 @@ export class Client extends EventEmitter { messages: MessagesManager = new MessagesManager(this) emojis: EmojisManager = new EmojisManager(this) + presence: ClientPresence = new ClientPresence() + constructor (options: ClientOptions = {}) { super() this.token = options.token this.intents = options.intents this.forceNewSession = options.forceNewSession if(options.cache) this.cache = options.cache + if(options.presence) this.presence = options.presence instanceof ClientPresence ? options.presence : new ClientPresence(options.presence) } setAdapter(adapter: ICacheAdapter) { @@ -50,6 +55,13 @@ export class Client extends EventEmitter { return this } + setPresence(presence: ClientPresence | ClientActivity | ActivityGame) { + if(presence instanceof ClientPresence) { + this.presence = presence + } else this.presence = new ClientPresence(presence) + this.gateway?.sendPresence(this.presence.create()) + } + debug(tag: string, msg: string) { this.emit("debug", `[${tag}] ${msg}`) } diff --git a/src/structures/presence.ts b/src/structures/presence.ts new file mode 100644 index 0000000..f4c7175 --- /dev/null +++ b/src/structures/presence.ts @@ -0,0 +1,120 @@ +export type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' | 'WATCHING' | 'CUSTOM_STATUS' | 'COMPETING'; +export type StatusType = 'online' | 'invisible' | 'offline' | 'idle' | 'dnd'; + +export enum ActivityTypes { + PLAYING = 0, + STREAMING = 1, + LISTENING = 2, + WATCHING = 3, + CUSTOM_STATUS = 4, + COMPETING = 5, +} + +export interface ActivityGame { + name: string; + type: 0 | 1 | 2 | 3 | 4 | 5 | ActivityType; + url?: string; +} + +export interface ClientActivity { + status?: StatusType + activity?: ActivityGame | ActivityGame[] + since?: number | null + afk?: boolean +} + +export interface ClientActivityPayload { + status: StatusType + activities: ActivityGame[] | null + since: number | null + afk: boolean +} + +export class ClientPresence { + status: StatusType = 'online' + activity?: ActivityGame | ActivityGame[] + since?: number | null + afk?: boolean + + constructor(data?: ClientActivity | ClientActivityPayload | ActivityGame) { + if (data) { + if((data as ClientActivity).activity !== undefined) { + Object.assign(this, data) + } else if((data as ClientActivityPayload).activities !== undefined) { + + } else if((data as ActivityGame).name !== undefined) { + if(!this.activity) { + this.activity = data as ActivityGame + } else if(this.activity instanceof Array) { + this.activity.push(data as ActivityGame) + } else this.activity = [ this.activity, data as ActivityGame ] + } + } + } + + parse(payload: ClientActivityPayload) { + this.afk = payload.afk + this.activity = payload.activities ?? undefined + this.since = payload.since + this.status = payload.status + } + + static parse(payload: ClientActivityPayload) { + return new ClientPresence().parse(payload) + } + + create(): ClientActivityPayload { + return { + afk: this.afk || false, + activities: this.createActivity(), + since: this.since || null, + status: this.status || 'online' + } + } + + createActivity() { + let activity = this.activity == undefined ? null : (this.activity instanceof Array ? this.activity : [this.activity]) || null + if(activity == null) return activity + else { + activity.map(e => { + if(typeof e.type == "string") e.type = ActivityTypes[e.type] + return e + }) + return activity + } + } + + setStatus(status: StatusType) { + this.status = status + return this + } + + setActivity(activity: ActivityGame) { + this.activity = activity + return this + } + + setActivities(activities: ActivityGame[]) { + this.activity = activities + return this + } + + setAFK(afk: boolean) { + this.afk = afk + } + + removeAFK() { + this.afk = false + return this + } + + toggleAFK() { + this.afk = !(this.afk || true) + return this + } + + setSince(since?: number) { + this.since = since + return this + } +} \ No newline at end of file diff --git a/src/test/index.ts b/src/test/index.ts index 971a2d4..575f10a 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -8,8 +8,17 @@ import { Guild } from '../structures/guild.ts' import { User } from '../structures/user.ts' import { Message } from "../structures/message.ts" import { RedisCacheAdapter } from "../models/CacheAdapter.ts" +import { ClientPresence } from "../structures/presence.ts" -const bot = new Client() +const bot = new Client({ + presence: new ClientPresence({ + activity: { + name: "Testing", + type: 'COMPETING' + } + }), + forceNewSession: true +}) bot.setAdapter(new RedisCacheAdapter(bot, { hostname: "127.0.0.1", @@ -18,6 +27,10 @@ bot.setAdapter(new RedisCacheAdapter(bot, { bot.on('ready', () => { console.log(`[Login] Logged in as ${bot.user?.tag}!`) + bot.setPresence({ + name: "Test After Ready", + type: 'COMPETING' + }) }) bot.on('debug', console.log)