From 1c02edb0152f643af0c55d08679c01055ddfc2c1 Mon Sep 17 00:00:00 2001 From: DjDeveloperr <=> Date: Sun, 1 Nov 2020 16:52:09 +0530 Subject: [PATCH] RedisCacheAdapter (!), fixed resuming, error code handling, resume event added. Some caching missing yet though --- src/gateway/handlers/channelCreate.ts | 5 +- src/gateway/handlers/channelDelete.ts | 6 +- src/gateway/handlers/channelPinsUpdate.ts | 8 +- src/gateway/handlers/channelUpdate.ts | 6 +- src/gateway/handlers/guildCreate.ts | 11 ++- src/gateway/handlers/guildDelete.ts | 6 +- src/gateway/handlers/guildUpdate.ts | 9 +- src/gateway/handlers/index.ts | 3 +- src/gateway/handlers/messageCreate.ts | 9 +- src/gateway/handlers/ready.ts | 3 +- src/gateway/handlers/resume.ts | 6 ++ src/gateway/index.ts | 112 +++++++++++++++++----- src/managers/BaseManager.ts | 16 ++-- src/managers/ChannelsManager.ts | 4 +- src/managers/GatewayCache.ts | 24 +++++ src/managers/MessagesManager.ts | 13 ++- src/models/CacheAdapter.ts | 66 +++++++++++-- src/models/client.ts | 5 + src/models/rest.ts | 3 +- src/structures/MessageMentions.ts | 3 + src/structures/channel.ts | 3 +- src/structures/dmChannel.ts | 2 +- src/structures/groupChannel.ts | 3 +- src/structures/guild.ts | 39 ++++---- src/structures/guildCategoryChannel.ts | 3 +- src/structures/guildTextChannel.ts | 3 +- src/structures/guildVoiceChannel.ts | 3 +- src/structures/member.ts | 3 +- src/structures/message.ts | 58 +++++------ src/structures/role.ts | 3 +- src/structures/textChannel.ts | 38 ++++---- src/structures/user.ts | 3 +- src/structures/voicestate.ts | 3 +- src/test/index.ts | 6 ++ src/utils/collection.ts | 10 +- 35 files changed, 340 insertions(+), 158 deletions(-) create mode 100644 src/gateway/handlers/resume.ts create mode 100644 src/managers/GatewayCache.ts create mode 100644 src/structures/MessageMentions.ts diff --git a/src/gateway/handlers/channelCreate.ts b/src/gateway/handlers/channelCreate.ts index 1b35974..c65ec3d 100644 --- a/src/gateway/handlers/channelCreate.ts +++ b/src/gateway/handlers/channelCreate.ts @@ -1,14 +1,13 @@ import { Gateway, GatewayEventHandler } from '../index.ts' import getChannelByType from '../../utils/getChannelByType.ts' -export const channelCreate: GatewayEventHandler = ( +export const channelCreate: GatewayEventHandler = async ( gateway: Gateway, d: any ) => { const channel = getChannelByType(gateway.client, d) - if (channel !== undefined) { - gateway.client.channels.set(d.id, d) + await gateway.client.channels.set(d.id, d) gateway.client.emit('channelCreate', channel) } } diff --git a/src/gateway/handlers/channelDelete.ts b/src/gateway/handlers/channelDelete.ts index 0db077d..71e7b32 100644 --- a/src/gateway/handlers/channelDelete.ts +++ b/src/gateway/handlers/channelDelete.ts @@ -1,13 +1,13 @@ import { Gateway, GatewayEventHandler } from '../index.ts' import { Channel } from '../../structures/channel.ts' -export const channelDelete: GatewayEventHandler = ( +export const channelDelete: GatewayEventHandler = async( gateway: Gateway, d: any ) => { - const channel: Channel = gateway.client.channels.get(d.id) + const channel: Channel = await gateway.client.channels.get(d.id) if (channel !== undefined) { - gateway.client.channels.delete(d.id) + await gateway.client.channels.delete(d.id) gateway.client.emit('channelDelete', channel) } } diff --git a/src/gateway/handlers/channelPinsUpdate.ts b/src/gateway/handlers/channelPinsUpdate.ts index f1fe526..80bc1dc 100644 --- a/src/gateway/handlers/channelPinsUpdate.ts +++ b/src/gateway/handlers/channelPinsUpdate.ts @@ -3,17 +3,17 @@ import cache from '../../models/cache.ts' import { TextChannel } from '../../structures/textChannel.ts' import { ChannelPayload } from "../../types/channelTypes.ts" -export const channelPinsUpdate: GatewayEventHandler = ( +export const channelPinsUpdate: GatewayEventHandler = async( gateway: Gateway, d: any ) => { - const after: TextChannel = gateway.client.channels.get(d.channel_id) + const after: TextChannel = await gateway.client.channels.get(d.channel_id) if (after !== undefined) { const before = after.refreshFromData({ last_pin_timestamp: d.last_pin_timestamp }) - const raw = gateway.client.channels._get(d.channel_id) ; - gateway.client.channels.set(after.id, Object.assign(raw, { last_pin_timestamp: d.last_pin_timestamp })) + const raw = await gateway.client.channels._get(d.channel_id) ; + await gateway.client.channels.set(after.id, Object.assign(raw, { last_pin_timestamp: d.last_pin_timestamp })) gateway.client.emit('channelPinsUpdate', before, after) } } diff --git a/src/gateway/handlers/channelUpdate.ts b/src/gateway/handlers/channelUpdate.ts index 6d70b50..27372c9 100644 --- a/src/gateway/handlers/channelUpdate.ts +++ b/src/gateway/handlers/channelUpdate.ts @@ -2,14 +2,14 @@ import { Channel } from '../../structures/channel.ts' import getChannelByType from '../../utils/getChannelByType.ts' import { Gateway, GatewayEventHandler } from '../index.ts' -export const channelUpdate: GatewayEventHandler = ( +export const channelUpdate: GatewayEventHandler = async ( gateway: Gateway, d: any ) => { - const oldChannel: Channel = gateway.client.channels.get(d.id) + const oldChannel: Channel = await gateway.client.channels.get(d.id) if (oldChannel !== undefined) { - gateway.client.channels.set(d.id, d) + await gateway.client.channels.set(d.id, d) if (oldChannel.type !== d.type) { const channel: Channel = getChannelByType(gateway.client, d) ?? oldChannel gateway.client.emit('channelUpdate', oldChannel, channel) diff --git a/src/gateway/handlers/guildCreate.ts b/src/gateway/handlers/guildCreate.ts index 48db410..b3596aa 100644 --- a/src/gateway/handlers/guildCreate.ts +++ b/src/gateway/handlers/guildCreate.ts @@ -1,15 +1,16 @@ import { Gateway, GatewayEventHandler } from '../index.ts' import { Guild } from '../../structures/guild.ts' +import { GuildPayload } from "../../types/guildTypes.ts" -export const guildCreate: GatewayEventHandler = (gateway: Gateway, d: any) => { - let guild: Guild | void = gateway.client.guilds.get(d.id) +export const guildCreate: GatewayEventHandler = async(gateway: Gateway, d: any) => { + let guild: Guild | void = await gateway.client.guilds.get(d.id) if (guild !== undefined) { // It was just lazy load, so we don't fire the event as its gonna fire for every guild bot is in - gateway.client.guilds.set(d.id, d) + await gateway.client.guilds.set(d.id, d) guild.refreshFromData(d) } else { - gateway.client.guilds.set(d.id, d) - guild = gateway.client.guilds.get(d.id) + await gateway.client.guilds.set(d.id, d) + guild = new Guild(gateway.client, d as GuildPayload) gateway.client.emit('guildCreate', guild) } } diff --git a/src/gateway/handlers/guildDelete.ts b/src/gateway/handlers/guildDelete.ts index 1849f60..7460e12 100644 --- a/src/gateway/handlers/guildDelete.ts +++ b/src/gateway/handlers/guildDelete.ts @@ -1,12 +1,12 @@ import { Guild } from '../../structures/guild.ts' import { Gateway, GatewayEventHandler } from '../index.ts' -export const guildDelte: GatewayEventHandler = (gateway: Gateway, d: any) => { - const guild: Guild | void = gateway.client.guilds.get(d.id) +export const guildDelte: GatewayEventHandler = async (gateway: Gateway, d: any) => { + const guild: Guild | void = await gateway.client.guilds.get(d.id) if (guild !== undefined) { guild.refreshFromData(d) - gateway.client.guilds.delete(d.id) + await gateway.client.guilds.delete(d.id) gateway.client.emit('guildDelete', guild) } } diff --git a/src/gateway/handlers/guildUpdate.ts b/src/gateway/handlers/guildUpdate.ts index 7acf31a..4e703e9 100644 --- a/src/gateway/handlers/guildUpdate.ts +++ b/src/gateway/handlers/guildUpdate.ts @@ -1,11 +1,10 @@ import { Gateway, GatewayEventHandler } from '../index.ts' -import cache from '../../models/cache.ts' import { Guild } from '../../structures/guild.ts' -export const guildUpdate: GatewayEventHandler = (gateway: Gateway, d: any) => { - const before: Guild | void = gateway.client.guilds.get(d.id) +export const guildUpdate: GatewayEventHandler = async(gateway: Gateway, d: any) => { + const before: Guild | void = await gateway.client.guilds.get(d.id) if(!before) return - gateway.client.guilds.set(d.id, d) - const after: Guild | void = gateway.client.guilds.get(d.id) + await gateway.client.guilds.set(d.id, d) + const after: Guild | void = await gateway.client.guilds.get(d.id) gateway.client.emit('guildUpdate', before, after) } diff --git a/src/gateway/handlers/index.ts b/src/gateway/handlers/index.ts index 81c92e2..a03804a 100644 --- a/src/gateway/handlers/index.ts +++ b/src/gateway/handlers/index.ts @@ -11,13 +11,14 @@ import { guildBanAdd } from './guildBanAdd.ts' import { ready } from './ready.ts' import { guildBanRemove } from './guildBanRemove.ts' import { messageCreate } from "./messageCreate.ts" +import { resume } from "./resume.ts" export const gatewayHandlers: { [eventCode in GatewayEvents]: GatewayEventHandler | undefined } = { READY: ready, RECONNECT: undefined, - RESUMED: undefined, + RESUMED: resume, CHANNEL_CREATE: channelCreate, CHANNEL_DELETE: channelDelete, CHANNEL_UPDATE: channelUpdate, diff --git a/src/gateway/handlers/messageCreate.ts b/src/gateway/handlers/messageCreate.ts index 5a5d0eb..a83504e 100644 --- a/src/gateway/handlers/messageCreate.ts +++ b/src/gateway/handlers/messageCreate.ts @@ -1,5 +1,7 @@ import { Channel } from "../../structures/channel.ts" import { Message } from "../../structures/message.ts" +import { MessageMentions } from "../../structures/MessageMentions.ts" +import { User } from "../../structures/user.ts" import { MessagePayload } from "../../types/channelTypes.ts" import { Gateway, GatewayEventHandler } from '../index.ts' @@ -7,9 +9,12 @@ export const messageCreate: GatewayEventHandler = async( gateway: Gateway, d: MessagePayload ) => { - let channel = gateway.client.channels.get(d.channel_id) + let channel = await gateway.client.channels.get(d.channel_id) // Fetch the channel if not cached if(!channel) channel = (await gateway.client.channels.fetch(d.channel_id) as any) as Channel - let message = new Message(gateway.client, d, channel) + let user = new User(gateway.client, d.author) + await gateway.client.users.set(d.author.id, d.author) + let mentions = new MessageMentions() + let message = new Message(gateway.client, d, channel, user, mentions) gateway.client.emit('messageCreate', message) } diff --git a/src/gateway/handlers/ready.ts b/src/gateway/handlers/ready.ts index 7a04e44..57b989e 100644 --- a/src/gateway/handlers/ready.ts +++ b/src/gateway/handlers/ready.ts @@ -2,10 +2,11 @@ import { User } from '../../structures/user.ts' import { GuildPayload } from '../../types/guildTypes.ts' import { Gateway, GatewayEventHandler } from '../index.ts' -export const ready: GatewayEventHandler = (gateway: Gateway, d: any) => { +export const ready: GatewayEventHandler = async (gateway: Gateway, d: any) => { gateway.client.user = new User(gateway.client, d.user) gateway.sessionID = d.session_id gateway.debug(`Received READY. Session: ${gateway.sessionID}`) + await gateway.cache.set("session_id", gateway.sessionID) d.guilds.forEach((guild: GuildPayload) => { gateway.client.guilds.set(guild.id, guild) }) diff --git a/src/gateway/handlers/resume.ts b/src/gateway/handlers/resume.ts new file mode 100644 index 0000000..c2d560c --- /dev/null +++ b/src/gateway/handlers/resume.ts @@ -0,0 +1,6 @@ +import { Gateway, GatewayEventHandler } from '../index.ts' + +export const resume: GatewayEventHandler = (gateway: Gateway, d: any) => { + gateway.debug(`Session Resumed!`) + gateway.client.emit('resume') +} \ No newline at end of file diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 247e1f7..5e6dcb4 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -5,10 +5,11 @@ import { DISCORD_API_VERSION } from '../consts/urlsAndVersions.ts' import { GatewayResponse } from '../types/gatewayResponse.ts' -import { GatewayOpcodes, GatewayIntents } from '../types/gatewayTypes.ts' +import { GatewayOpcodes, GatewayIntents, GatewayCloseCodes } from '../types/gatewayTypes.ts' 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" /** * Handles Discord gateway connection. @@ -25,15 +26,17 @@ class Gateway { heartbeatInterval = 0 heartbeatIntervalID?: number sequenceID?: number - sessionID?: string lastPingTimestamp = 0 + sessionID?: string private heartbeatServerResponded = false client: Client + cache: GatewayCache constructor (client: Client, token: string, intents: GatewayIntents[]) { this.token = token this.intents = intents this.client = client + this.cache = new GatewayCache(client) this.websocket = new WebSocket( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`, @@ -51,7 +54,7 @@ class Gateway { this.debug("Connected to Gateway!") } - private onmessage (event: MessageEvent): void { + private async onmessage (event: MessageEvent): Promise { let data = event.data if (data instanceof ArrayBuffer) { data = new Uint8Array(data) @@ -72,8 +75,7 @@ class Gateway { this.heartbeatServerResponded = false } else { clearInterval(this.heartbeatIntervalID) - this.websocket.close() - this.initWebsocket() + this.reconnect() return } @@ -90,6 +92,7 @@ class Gateway { this.sendIdentify() this.initialized = true } else { + console.log("Calling Resume") this.sendResume() } break @@ -102,18 +105,16 @@ 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 - if (!d) { - setTimeout(this.sendResume, 3000) - } else { - setTimeout(this.sendIdentify, 3000) - } + setTimeout(() => this.sendIdentify(true), 3000) break case GatewayOpcodes.DISPATCH: { this.heartbeatServerResponded = true if (s !== null) { this.sequenceID = s + await this.cache.set("seq", s) } if (t !== null && t !== undefined) { const handler = gatewayHandlers[t] @@ -124,23 +125,68 @@ class Gateway { } break } + case GatewayOpcodes.RESUME: { + // this.token = d.token + this.sessionID = d.session_id + this.sequenceID = d.seq + await this.cache.set("seq", d.seq) + await this.cache.set("session_id", this.sessionID) + break + } + case GatewayOpcodes.RECONNECT: { + this.reconnect() + break + } default: break } } private onclose (event: CloseEvent): void { - console.log(event.code) - // TODO: Handle close event codes. + this.debug("Connection Closed with code: " + event.code) + + if(event.code == GatewayCloseCodes.UNKNOWN_ERROR) { + this.debug("API has encountered Unknown Error. Reconnecting...") + this.reconnect() + } 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) { + throw new Error("Invalid Payload was sent. This shouldn't happen!") + } else if(event.code == GatewayCloseCodes.NOT_AUTHENTICATED) { + throw new Error("Not Authorized: Payload was sent before Identifying.") + } else if(event.code == GatewayCloseCodes.AUTHENTICATION_FAILED) { + throw new Error("Invalid Token provided!") + } else if(event.code == GatewayCloseCodes.INVALID_SEQ) { + this.debug("Invalid Seq was sent. Reconnecting.") + this.reconnect() + } else if(event.code == GatewayCloseCodes.RATE_LIMITED) { + throw new Error("You're ratelimited. Calm down.") + } else if(event.code == GatewayCloseCodes.SESSION_TIMED_OUT) { + this.debug("Session Timeout. Reconnecting.") + this.reconnect(true) + } else if(event.code == GatewayCloseCodes.INVALID_SHARD) { + this.debug("Invalid Shard was sent. Reconnecting.") + this.reconnect() + } else if(event.code == GatewayCloseCodes.SHARDING_REQUIRED) { + throw new Error("Couldn't connect. Sharding is requried!") + } 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) { + throw new Error("Invalid 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.") + this.reconnect() + } } private onerror (event: Event | ErrorEvent): void { const eventError = event as ErrorEvent - console.log(eventError) } - private async sendIdentify () { + 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") @@ -148,6 +194,14 @@ class Gateway { 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) { + let sessionIDCached = await this.cache.get("session_id") + if(sessionIDCached) { + this.debug("Found Cached SessionID: " + sessionIDCached) + this.sessionID = sessionIDCached + return this.sendResume() + } + } this.websocket.send( JSON.stringify({ op: GatewayOpcodes.IDENTIFY, @@ -175,17 +229,22 @@ class Gateway { ) } - private sendResume (): void { + private async sendResume (): Promise { this.debug(`Preparing to resume with Session: ${this.sessionID}`) + if(this.sequenceID === undefined) { + let cached = await this.cache.get("seq") + if(cached) this.sequenceID = typeof cached == "string" ? parseInt(cached) : cached + } + const resumePayload = { + op: GatewayOpcodes.RESUME, + d: { + token: this.token, + session_id: this.sessionID, + seq: this.sequenceID || null + } + } this.websocket.send( - JSON.stringify({ - op: GatewayOpcodes.RESUME, - d: { - token: this.token, - session_id: this.sessionID, - seq: this.sequenceID - } - }) + JSON.stringify(resumePayload) ) } @@ -193,6 +252,13 @@ class Gateway { this.client.debug("Gateway", msg) } + async reconnect(forceNew?: boolean) { + clearInterval(this.heartbeatIntervalID) + if(forceNew) await this.cache.delete("session_id") + this.close() + this.initWebsocket() + } + initWebsocket (): void { this.websocket = new WebSocket( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions diff --git a/src/managers/BaseManager.ts b/src/managers/BaseManager.ts index fef9e46..3efcf2c 100644 --- a/src/managers/BaseManager.ts +++ b/src/managers/BaseManager.ts @@ -4,29 +4,29 @@ import { Base } from "../structures/base.ts"; export class BaseManager { client: Client cacheName: string - dataType: typeof Base + dataType: any - constructor(client: Client, cacheName: string, dataType: typeof Base) { + constructor(client: Client, cacheName: string, dataType: any) { this.client = client this.cacheName = cacheName this.dataType = dataType } - _get(key: string): T { - return this.client.cache.get(this.cacheName, key) as T + _get(key: string): Promise { + return this.client.cache.get(this.cacheName, key) as Promise } - get(key: string): T2 | void { - const raw = this._get(key) + async get(key: string): Promise { + const raw = await this._get(key) if(!raw) return return new this.dataType(this.client, raw) as any } - set(key: string, value: T) { + async set(key: string, value: T) { return this.client.cache.set(this.cacheName, key, value) } - delete(key: string) { + async delete(key: string) { return this.client.cache.delete(this.cacheName, key) } } \ No newline at end of file diff --git a/src/managers/ChannelsManager.ts b/src/managers/ChannelsManager.ts index 09af63e..e75eb1b 100644 --- a/src/managers/ChannelsManager.ts +++ b/src/managers/ChannelsManager.ts @@ -11,11 +11,11 @@ export class ChannelsManager extends BaseManager { } // Override get method as Generic - get(key: string): T { + async get(key: string): Promise { return new this.dataType(this.client, this._get(key)) as any } - fetch(id: string) { + fetch(id: string): Promise { return new Promise((res, rej) => { this.client.rest.get(CHANNEL(id)).then(data => { this.set(id, data as ChannelPayload) diff --git a/src/managers/GatewayCache.ts b/src/managers/GatewayCache.ts new file mode 100644 index 0000000..72be001 --- /dev/null +++ b/src/managers/GatewayCache.ts @@ -0,0 +1,24 @@ +import { Client } from "../models/client.ts"; + +export class GatewayCache { + client: Client + cacheName: string = "discord_gateway_cache" + + constructor(client: Client, cacheName?: string) { + this.client = client + if(cacheName) this.cacheName = cacheName + } + + get(key: string) { + return this.client.cache.get(this.cacheName, key) + } + + set(key: string, value: any) { + return this.client.cache.set(this.cacheName, key, value) + } + + delete(key: string) { + console.log(`[GatewayCache] DEL ${key}`) + return this.client.cache.delete(this.cacheName, key) + } +} \ No newline at end of file diff --git a/src/managers/MessagesManager.ts b/src/managers/MessagesManager.ts index 472513f..616ad51 100644 --- a/src/managers/MessagesManager.ts +++ b/src/managers/MessagesManager.ts @@ -1,7 +1,10 @@ import { Client } from "../models/client.ts"; import { Message } from "../structures/message.ts"; +import { MessageMentions } from "../structures/MessageMentions.ts"; +import { User } from "../structures/user.ts"; import { MessagePayload } from "../types/channelTypes.ts"; import { CHANNEL_MESSAGE } from "../types/endpoint.ts"; +import { UserPayload } from "../types/userTypes.ts"; import { BaseManager } from "./BaseManager.ts"; export class MessagesManager extends BaseManager { @@ -11,9 +14,15 @@ export class MessagesManager extends BaseManager { fetch(channelID: string, id: string) { return new Promise((res, rej) => { - this.client.rest.get(CHANNEL_MESSAGE(channelID, id)).then(data => { + this.client.rest.get(CHANNEL_MESSAGE(channelID, id)).then(async data => { this.set(id, data as MessagePayload) - res(new Message(this.client, data as MessagePayload)) + let channel = await this.client.channels.get(channelID) + if(!channel) channel = await this.client.channels.fetch(channelID) + let author = new User(this.client, (data as MessagePayload).author as UserPayload) + await this.client.users.set(author.id, (data as MessagePayload).author) + // TODO: Make this thing work (MessageMentions) + let mentions = new MessageMentions() + res(new Message(this.client, data as MessagePayload, channel, author, mentions)) }).catch(e => rej(e)) }) } diff --git a/src/models/CacheAdapter.ts b/src/models/CacheAdapter.ts index 087558e..842afbf 100644 --- a/src/models/CacheAdapter.ts +++ b/src/models/CacheAdapter.ts @@ -1,12 +1,13 @@ import { Collection } from "../utils/collection.ts"; import { Client } from "./client.ts"; +import { connect, Redis, RedisConnectOptions } from "https://denopkg.com/keroxp/deno-redis/mod.ts"; export interface ICacheAdapter { client: Client - get: (cacheName: string, key: string) => any - set: (cacheName: string, key: string, value: any) => any - delete: (cacheName: string, key: string) => boolean - array: (cacheName: string) => void | any[] + get: (cacheName: string, key: string) => Promise | any + set: (cacheName: string, key: string, value: any) => Promise | any + delete: (cacheName: string, key: string) => Promise | boolean + array: (cacheName: string) => void | any[] | Promise } export class DefaultCacheAdapter implements ICacheAdapter { @@ -19,13 +20,13 @@ export class DefaultCacheAdapter implements ICacheAdapter { this.client = client } - get(cacheName: string, key: string) { + async get(cacheName: string, key: string) { const cache = this.data[cacheName] if (!cache) return; return cache.get(key) } - set(cacheName: string, key: string, value: any) { + async set(cacheName: string, key: string, value: any) { let cache = this.data[cacheName] if (!cache) { this.data[cacheName] = new Collection() @@ -34,15 +35,64 @@ export class DefaultCacheAdapter implements ICacheAdapter { cache.set(key, value) } - delete(cacheName: string, key: string) { + async delete(cacheName: string, key: string) { const cache = this.data[cacheName] if (!cache) return false return cache.delete(key) } - array(cacheName: string) { + async array(cacheName: string) { const cache = this.data[cacheName] if (!cache) return return cache.array() } +} + +export class RedisCacheAdapter implements ICacheAdapter { + client: Client + _redis: Promise + redis?: Redis + ready: boolean = false + + constructor(client: Client, options: RedisConnectOptions) { + this.client = client + this._redis = connect(options) + this._redis.then(redis => { + this.redis = redis + this.ready = true + }) + } + + async _checkReady() { + if(!this.ready) return await this._redis; + else return; + } + + async get(cacheName: string, key: string) { + await this._checkReady() + let cache = await this.redis?.hget(cacheName, key) + if(!cache) return + try { + return JSON.parse(cache as string) + } catch(e) { return cache } + } + + async set(cacheName: string, key: string, value: any) { + await this._checkReady() + return await this.redis?.hset(cacheName, key, typeof value === "object" ? JSON.stringify(value) : value) + } + + async delete(cacheName: string, key: string) { + await this._checkReady() + let exists = await this.redis?.hexists(cacheName, key) + if(!exists) return false + await this.redis?.hdel(cacheName, key) + return true + } + + async array(cacheName: string) { + await this._checkReady() + let data = await this.redis?.hvals(cacheName) + return data?.map((e: string) => JSON.parse(e)) + } } \ No newline at end of file diff --git a/src/models/client.ts b/src/models/client.ts index 074ff21..eed4704 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -42,6 +42,11 @@ export class Client extends EventEmitter { if(options.cache) this.cache = options.cache } + setAdapter(adapter: ICacheAdapter) { + this.cache = adapter + return this + } + debug(tag: string, msg: string) { this.emit("debug", `[${tag}] ${msg}`) } diff --git a/src/models/rest.ts b/src/models/rest.ts index e8824be..a3553fb 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -13,8 +13,7 @@ export enum HttpResponseCode { NotFound = 404, MethodNotAllowed = 405, TooManyRequests = 429, - GatewayUnavailable = 502, - // ServerError left untyped because it's 5xx. + GatewayUnavailable = 502 } export type RequestMethods = diff --git a/src/structures/MessageMentions.ts b/src/structures/MessageMentions.ts new file mode 100644 index 0000000..4888838 --- /dev/null +++ b/src/structures/MessageMentions.ts @@ -0,0 +1,3 @@ +export class MessageMentions { + str: string = "str" +} \ No newline at end of file diff --git a/src/structures/channel.ts b/src/structures/channel.ts index 8cf18d0..b8efa00 100644 --- a/src/structures/channel.ts +++ b/src/structures/channel.ts @@ -15,7 +15,8 @@ export class Channel extends Base { super(client, data) this.type = data.type this.id = data.id - this.client.channels.set(this.id, data) + // TODO: Cache in Gateway Event Code + // this.client.channels.set(this.id, data) } protected readFromData (data: ChannelPayload): void { diff --git a/src/structures/dmChannel.ts b/src/structures/dmChannel.ts index 104059f..03e59d1 100644 --- a/src/structures/dmChannel.ts +++ b/src/structures/dmChannel.ts @@ -10,7 +10,7 @@ export class DMChannel extends TextChannel { constructor (client: Client, data: DMChannelPayload) { super(client, data) this.recipients = data.recipients - cache.set('dmchannel', this.id, this) + // cache.set('dmchannel', this.id, this) } protected readFromData (data: DMChannelPayload): void { diff --git a/src/structures/groupChannel.ts b/src/structures/groupChannel.ts index e6f8918..1e60149 100644 --- a/src/structures/groupChannel.ts +++ b/src/structures/groupChannel.ts @@ -14,7 +14,8 @@ export class GroupDMChannel extends Channel { this.name = data.name this.icon = data.icon this.ownerID = data.owner_id - cache.set('groupchannel', this.id, this) + // TODO: Cache in Gateway Event Code + // cache.set('groupchannel', this.id, this) } protected readFromData (data: GroupDMChannelPayload): void { diff --git a/src/structures/guild.ts b/src/structures/guild.ts index efd2286..e6da85a 100644 --- a/src/structures/guild.ts +++ b/src/structures/guild.ts @@ -83,12 +83,12 @@ export class Guild extends Base { // this.roles = data.roles.map( // v => cache.get('role', v.id) ?? new Role(client, v) // ) - data.roles.forEach(role => { - this.roles.set(role.id, new Role(client, role)) - }) - this.emojis = data.emojis.map( - v => cache.get('emoji', v.id) ?? new Emoji(client, v) - ) + // data.roles.forEach(role => { + // this.roles.set(role.id, new Role(client, role)) + // }) + // this.emojis = data.emojis.map( + // v => cache.get('emoji', v.id) ?? new Emoji(client, v) + // ) this.features = data.features this.mfaLevel = data.mfa_level this.systemChannelID = data.system_channel_id @@ -97,19 +97,20 @@ export class Guild extends Base { this.joinedAt = data.joined_at this.large = data.large this.memberCount = data.member_count - this.voiceStates = data.voice_states?.map( - v => - cache.get('voiceState', `${v.guild_id}:${v.user_id}`) ?? - new VoiceState(client, v) - ) - this.members = data.members?.map( - v => - cache.get('member', `${this.id}:${v.user.id}`) ?? - new Member(client, v) - ) - this.channels = data.channels?.map( - v => cache.get('channel', v.id) ?? getChannelByType(this.client, v) - ) + // TODO: Cache in Gateway Event code + // this.voiceStates = data.voice_states?.map( + // v => + // cache.get('voiceState', `${v.guild_id}:${v.user_id}`) ?? + // new VoiceState(client, v) + // ) + // this.members = data.members?.map( + // v => + // cache.get('member', `${this.id}:${v.user.id}`) ?? + // new Member(client, v) + // ) + // this.channels = data.channels?.map( + // v => cache.get('channel', v.id) ?? getChannelByType(this.client, v) + // ) this.presences = data.presences this.maxPresences = data.max_presences this.maxMembers = data.max_members diff --git a/src/structures/guildCategoryChannel.ts b/src/structures/guildCategoryChannel.ts index bfb661d..06fddfd 100644 --- a/src/structures/guildCategoryChannel.ts +++ b/src/structures/guildCategoryChannel.ts @@ -22,7 +22,8 @@ export class CategoryChannel extends Channel { this.permissionOverwrites = data.permission_overwrites this.nsfw = data.nsfw this.parentID = data.parent_id - cache.set('guildcategorychannel', this.id, this) + // TODO: Cache in Gateway Event Code + // cache.set('guildcategorychannel', this.id, this) } protected readFromData (data: GuildChannelCategoryPayload): void { diff --git a/src/structures/guildTextChannel.ts b/src/structures/guildTextChannel.ts index 1b8b4ea..20cf189 100644 --- a/src/structures/guildTextChannel.ts +++ b/src/structures/guildTextChannel.ts @@ -27,7 +27,8 @@ export class GuildTextChannel extends TextChannel { this.parentID = data.parent_id this.topic = data.topic this.rateLimit = data.rate_limit_per_user - cache.set('guildtextchannel', this.id, this) + // TODO: Cache in Gateway Event Code + // cache.set('guildtextchannel', this.id, this) } protected readFromData (data: GuildTextChannelPayload): void { diff --git a/src/structures/guildVoiceChannel.ts b/src/structures/guildVoiceChannel.ts index 5f2fc33..5bc9048 100644 --- a/src/structures/guildVoiceChannel.ts +++ b/src/structures/guildVoiceChannel.ts @@ -23,7 +23,8 @@ export class VoiceChannel extends Channel { this.permissionOverwrites = data.permission_overwrites this.nsfw = data.nsfw this.parentID = data.parent_id - cache.set('guildvoicechannel', this.id, this) + // TODO: Cache in Gateway Event Code + // cache.set('guildvoicechannel', this.id, this) } protected readFromData (data: GuildVoiceChannelPayload): void { diff --git a/src/structures/member.ts b/src/structures/member.ts index 6086eb8..f29c28a 100644 --- a/src/structures/member.ts +++ b/src/structures/member.ts @@ -25,7 +25,8 @@ export class Member extends Base { this.premiumSince = data.premium_since this.deaf = data.deaf this.mute = data.mute - cache.set('member', this.id, this) + // TODO: Cache in Gateway Event Code + // cache.set('member', this.id, this) } protected readFromData (data: MemberPayload): void { diff --git a/src/structures/message.ts b/src/structures/message.ts index d5ec61a..9886800 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -16,6 +16,8 @@ import { Embed } from './embed.ts' import { CHANNEL_MESSAGE } from '../types/endpoint.ts' import cache from '../models/cache.ts' import { Channel } from "./channel.ts" +import { MessageMentions } from "./MessageMentions.ts" +import { TextChannel } from "./textChannel.ts" export class Message extends Base { // eslint-disable-next-line @typescript-eslint/prefer-readonly @@ -31,7 +33,7 @@ export class Message extends Base { editedTimestamp?: string tts: boolean mentionEveryone: boolean - mentions: User[] + mentions: MessageMentions mentionRoles: string[] mentionChannels?: ChannelMention[] attachments: Attachment[] @@ -46,22 +48,24 @@ export class Message extends Base { messageReference?: MessageReference flags?: number - constructor (client: Client, data: MessagePayload, channel?: Channel, noSave?: boolean) { + constructor (client: Client, data: MessagePayload, channel: Channel, author: User, mentions: MessageMentions) { super(client) this.data = data this.id = data.id this.channelID = data.channel_id this.guildID = data.guild_id - this.author = - this.client.users.get(data.author.id) || new User(this.client, data.author) + this.author = author + // this.author = + // this.client.users.get(data.author.id) || new User(this.client, data.author) this.content = data.content this.timestamp = data.timestamp this.editedTimestamp = data.edited_timestamp this.tts = data.tts this.mentionEveryone = data.mention_everyone - this.mentions = data.mentions.map( - v => this.client.users.get(v.id) || new User(client, v) - ) + this.mentions = mentions + // this.mentions = data.mentions.map( + // v => this.client.users.get(v.id) || new User(client, v) + // ) this.mentionRoles = data.mention_roles this.mentionChannels = data.mention_channels this.attachments = data.attachments @@ -75,28 +79,28 @@ export class Message extends Base { this.application = data.application this.messageReference = data.message_reference this.flags = data.flags - if(channel) this.channel = channel || this.client.channels.get(this.channelID) - else throw new Error("Message received without Channel (neither in cache)") // unlikely to happen - if(!noSave) this.client.messages.set(this.id, data) + this.channel = channel + // TODO: Cache in Gateway Event Code + // if(!noSave) this.client.messages.set(this.id, data) } protected readFromData (data: MessagePayload): void { super.readFromData(data) this.channelID = data.channel_id ?? this.channelID this.guildID = data.guild_id ?? this.guildID - this.author = - this.client.users.get(data.author.id) || - this.author || - new User(this.client, data.author) + // this.author = + // this.client.users.get(data.author.id) || + // this.author || + // new User(this.client, data.author) this.content = data.content ?? this.content this.timestamp = data.timestamp ?? this.timestamp this.editedTimestamp = data.edited_timestamp ?? this.editedTimestamp this.tts = data.tts ?? this.tts this.mentionEveryone = data.mention_everyone ?? this.mentionEveryone - this.mentions = - data.mentions.map( - v => this.client.users.get(v.id) || new User(this.client, v) - ) ?? this.mentions + // this.mentions = + // data.mentions.map( + // v => this.client.users.get(v.id) || new User(this.client, v) + // ) ?? this.mentions this.mentionRoles = data.mention_roles ?? this.mentionRoles this.mentionChannels = data.mention_channels ?? this.mentionChannels this.attachments = data.attachments ?? this.attachments @@ -112,24 +116,10 @@ export class Message extends Base { this.flags = data.flags ?? this.flags } - // TODO: We have to seperate fetch() - async edit (text?: string, option?: MessageOption): Promise { - if (text !== undefined && option !== undefined) { - throw new Error('Either text or option is necessary.') - } - - let newMsg = await this.client.rest.patch(CHANNEL_MESSAGE(this.channelID, this.id), { - content: text, - embed: option?.embed.toJSON(), - file: option?.file, - tts: option?.tts, - allowed_mentions: option?.allowedMention - }) as MessagePayload - - return new Message(this.client, newMsg) + edit (text?: string, option?: MessageOption): Promise { + return (this.channel as TextChannel).editMessage(this.id, text, option) } - // TODO: We have to seperate fetch() delete (): Promise { return this.client.rest.delete(CHANNEL_MESSAGE(this.channelID, this.id)) as any } diff --git a/src/structures/role.ts b/src/structures/role.ts index 442e24e..d41fb64 100644 --- a/src/structures/role.ts +++ b/src/structures/role.ts @@ -27,7 +27,8 @@ export class Role extends Base { this.permissions = data.permissions this.managed = data.managed this.mentionable = data.mentionable - cache.set('role', this.id, this) + // TODO: Cache in Gateway Event Code + // cache.set('role', this.id, this) } protected readFromData (data: RolePayload): void { diff --git a/src/structures/textChannel.ts b/src/structures/textChannel.ts index f328662..44ec99f 100644 --- a/src/structures/textChannel.ts +++ b/src/structures/textChannel.ts @@ -1,9 +1,11 @@ import cache from '../models/cache.ts' import { Client } from '../models/client.ts' -import { MessageOption, TextChannelPayload } from '../types/channelTypes.ts' +import { MessageOption, MessagePayload, TextChannelPayload } from '../types/channelTypes.ts' import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts' import { Channel } from './channel.ts' import { Message } from './message.ts' +import { MessageMentions } from "./MessageMentions.ts" +import { User } from "./user.ts" export class TextChannel extends Channel { lastMessageID?: string @@ -13,7 +15,8 @@ export class TextChannel extends Channel { super(client, data) this.lastMessageID = data.last_message_id this.lastPinTimestamp = data.last_pin_timestamp - cache.set('textchannel', this.id, this) + // TODO: Cache in Gateway Event Code + // cache.set('textchannel', this.id, this) } protected readFromData (data: TextChannelPayload): void { @@ -41,32 +44,29 @@ export class TextChannel extends Channel { }) }) - return new Message(this.client, await resp.json()) + return new Message(this.client, await resp.json(), this, this.client.user as User, new MessageMentions()) } async editMessage ( - messageID: string, + message: Message | string, text?: string, option?: MessageOption ): Promise { if (text !== undefined && option !== undefined) { throw new Error('Either text or option is necessary.') } - const resp = await fetch(CHANNEL_MESSAGE(this.id, messageID), { - headers: { - Authorization: `Bot ${this.client.token}`, - 'Content-Type': 'application/json' - }, - method: 'PATCH', - body: JSON.stringify({ - content: text, - embed: option?.embed, - file: option?.file, - tts: option?.tts, - allowed_mentions: option?.allowedMention - }) - }) - return new Message(this.client, await resp.json()) + let newMsg = await this.client.rest.patch(CHANNEL_MESSAGE(this.id, typeof message == "string" ? message : message.id), { + content: text, + embed: option?.embed.toJSON(), + file: option?.file, + tts: option?.tts, + allowed_mentions: option?.allowedMention + }) as MessagePayload + + // TODO: Actually construct this object + let mentions = new MessageMentions() + + return new Message(this.client, newMsg, this, this.client.user as User, mentions) } } diff --git a/src/structures/user.ts b/src/structures/user.ts index ff14c84..e5d83d0 100644 --- a/src/structures/user.ts +++ b/src/structures/user.ts @@ -45,7 +45,8 @@ export class User extends Base { this.flags = data.flags this.premiumType = data.premium_type this.publicFlags = data.public_flags - cache.set('user', this.id, this) + // TODO: Cache in Gateway Event Code + // cache.set('user', this.id, this) } protected readFromData (data: UserPayload): void { diff --git a/src/structures/voicestate.ts b/src/structures/voicestate.ts index 717880d..0bcd8b3 100644 --- a/src/structures/voicestate.ts +++ b/src/structures/voicestate.ts @@ -30,7 +30,8 @@ export class VoiceState extends Base { this.selfStream = data.self_stream this.selfVideo = data.self_video this.suppress = data.suppress - cache.set('voiceState', `${this.guildID}:${this.userID}`, this) + // TODO: Cache in Gateway Event Code + // cache.set('voiceState', `${this.guildID}:${this.userID}`, this) } protected readFromData (data: VoiceStatePayload): void { diff --git a/src/test/index.ts b/src/test/index.ts index cfcdbda..ba2e34d 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -7,9 +7,15 @@ import { TextChannel } from '../structures/textChannel.ts' import { Guild } from '../structures/guild.ts' import { User } from '../structures/user.ts' import { Message } from "../structures/message.ts" +import { RedisCacheAdapter } from "../models/CacheAdapter.ts" const bot = new Client() +bot.setAdapter(new RedisCacheAdapter(bot, { + hostname: "127.0.0.1", + port: 6379 +})) + bot.on('ready', () => { console.log(`[Login] Logged in as ${bot.user?.tag}!`) }) diff --git a/src/utils/collection.ts b/src/utils/collection.ts index 58b749e..728fcea 100644 --- a/src/utils/collection.ts +++ b/src/utils/collection.ts @@ -1,4 +1,4 @@ -export class Collection extends Map { +export class Collection extends Map { maxSize?: number; set(key: K, value: V) { @@ -84,4 +84,12 @@ export class Collection extends Map { return accumulator } + + static fromObject(object: { [key: string]: V }) { + return new Collection(Object.entries(object)) + } + + toObject() { + return Object.entries(this) + } } \ No newline at end of file