feat(messages): add message caching and messageDelete event
This commit is contained in:
		
							parent
							
								
									c92f2e3d49
								
							
						
					
					
						commit
						5dbd4a84f5
					
				
					 8 changed files with 100 additions and 22 deletions
				
			
		|  | @ -13,6 +13,7 @@ import { guildBanRemove } from './guildBanRemove.ts' | ||||||
| import { messageCreate } from './messageCreate.ts' | import { messageCreate } from './messageCreate.ts' | ||||||
| import { resume } from './resume.ts' | import { resume } from './resume.ts' | ||||||
| import { reconnect } from './reconnect.ts' | import { reconnect } from './reconnect.ts' | ||||||
|  | import { messageDelete } from "./messageDelete.ts" | ||||||
| 
 | 
 | ||||||
| export const gatewayHandlers: { | export const gatewayHandlers: { | ||||||
|   [eventCode in GatewayEvents]: GatewayEventHandler | undefined |   [eventCode in GatewayEvents]: GatewayEventHandler | undefined | ||||||
|  | @ -42,7 +43,7 @@ export const gatewayHandlers: { | ||||||
|   INVITE_DELETE: undefined, |   INVITE_DELETE: undefined, | ||||||
|   MESSAGE_CREATE: messageCreate, |   MESSAGE_CREATE: messageCreate, | ||||||
|   MESSAGE_UPDATE: undefined, |   MESSAGE_UPDATE: undefined, | ||||||
|   MESSAGE_DELETE: undefined, |   MESSAGE_DELETE: messageDelete, | ||||||
|   MESSAGE_DELETE_BULK: undefined, |   MESSAGE_DELETE_BULK: undefined, | ||||||
|   MESSAGE_REACTION_ADD: undefined, |   MESSAGE_REACTION_ADD: undefined, | ||||||
|   MESSAGE_REACTION_REMOVE: undefined, |   MESSAGE_REACTION_REMOVE: undefined, | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ export const messageCreate: GatewayEventHandler = async ( | ||||||
|   if (channel === undefined) |   if (channel === undefined) | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
 |     // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
 | ||||||
|     channel = (await gateway.client.channels.fetch(d.channel_id)) as TextChannel |     channel = (await gateway.client.channels.fetch(d.channel_id)) as TextChannel | ||||||
|  |   await channel.messages.set(d.id, d) | ||||||
|   const user = new User(gateway.client, d.author) |   const user = new User(gateway.client, d.author) | ||||||
|   await gateway.client.users.set(d.author.id, d.author) |   await gateway.client.users.set(d.author.id, d.author) | ||||||
|   let guild |   let guild | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								src/gateway/handlers/messageDelete.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/gateway/handlers/messageDelete.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | import { TextChannel } from '../../structures/textChannel.ts' | ||||||
|  | import { MessageDeletePayload } from "../../types/gateway.ts" | ||||||
|  | import { Gateway, GatewayEventHandler } from '../index.ts' | ||||||
|  | 
 | ||||||
|  | export const messageDelete: GatewayEventHandler = async ( | ||||||
|  |   gateway: Gateway, | ||||||
|  |   d: MessageDeletePayload | ||||||
|  | ) => { | ||||||
|  |   let channel = await gateway.client.channels.get<TextChannel>(d.channel_id) | ||||||
|  |   // Fetch the channel if not cached
 | ||||||
|  |   if (channel === undefined) | ||||||
|  |     // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
 | ||||||
|  |     channel = (await gateway.client.channels.fetch(d.channel_id)) as TextChannel | ||||||
|  | 
 | ||||||
|  |   const message = await channel.messages.get(d.id) | ||||||
|  |   if (message === undefined) return gateway.client.emit('messageDeleteUncached', d) | ||||||
|  |   await channel.messages.delete(d.id) | ||||||
|  |   gateway.client.emit('messageDelete', message) | ||||||
|  | } | ||||||
|  | @ -7,8 +7,11 @@ import { CHANNEL_MESSAGE } from '../types/endpoint.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
| export class MessagesManager extends BaseManager<MessagePayload, Message> { | export class MessagesManager extends BaseManager<MessagePayload, Message> { | ||||||
|   constructor (client: Client) { |   channel: TextChannel | ||||||
|  | 
 | ||||||
|  |   constructor (client: Client, channel: TextChannel) { | ||||||
|     super(client, 'messages', Message) |     super(client, 'messages', Message) | ||||||
|  |     this.channel = channel | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async get (key: string): Promise<Message | undefined> { |   async get (key: string): Promise<Message | undefined> { | ||||||
|  | @ -26,18 +29,22 @@ export class MessagesManager extends BaseManager<MessagePayload, Message> { | ||||||
|     return res |     return res | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async fetch (channelID: string, id: string): Promise<Message> { |   async set (key: string, value: MessagePayload): Promise<any> { | ||||||
|  |     return this.client.cache.set(this.cacheName, key, value, this.client.messageCacheLifetime) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async fetch (id: string): Promise<Message> { | ||||||
|     return await new Promise((resolve, reject) => { |     return await new Promise((resolve, reject) => { | ||||||
|       this.client.rest |       this.client.rest | ||||||
|         .get(CHANNEL_MESSAGE(channelID, id)) |         .get(CHANNEL_MESSAGE(this.channel.id, id)) | ||||||
|         .then(async data => { |         .then(async data => { | ||||||
|           this.set(id, data as MessagePayload) |           await this.set(id, data as MessagePayload) | ||||||
| 
 | 
 | ||||||
|           let channel: any = await this.client.channels.get<TextChannel>( |           let channel: any = await this.client.channels.get<TextChannel>( | ||||||
|             channelID |             this.channel.id | ||||||
|           ) |           ) | ||||||
|           if (channel === undefined) |           if (channel === undefined) | ||||||
|             channel = await this.client.channels.fetch(channelID) |             channel = await this.client.channels.fetch(this.channel.id) | ||||||
| 
 | 
 | ||||||
|           const author = new User(this.client, (data as MessagePayload).author) |           const author = new User(this.client, (data as MessagePayload).author) | ||||||
|           await this.client.users.set( |           await this.client.users.set( | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ import { | ||||||
| 
 | 
 | ||||||
| export interface ICacheAdapter { | export interface ICacheAdapter { | ||||||
|   get: (cacheName: string, key: string) => Promise<any> | any |   get: (cacheName: string, key: string) => Promise<any> | any | ||||||
|   set: (cacheName: string, key: string, value: any) => Promise<any> | any |   set: (cacheName: string, key: string, value: any, expire?: number) => Promise<any> | any | ||||||
|   delete: (cacheName: string, key: string) => Promise<boolean> | boolean |   delete: (cacheName: string, key: string) => Promise<boolean> | boolean | ||||||
|   array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined> |   array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined> | ||||||
|   deleteCache: (cacheName: string) => any |   deleteCache: (cacheName: string) => any | ||||||
|  | @ -24,13 +24,16 @@ export class DefaultCacheAdapter implements ICacheAdapter { | ||||||
|     return cache.get(key) |     return cache.get(key) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async set (cacheName: string, key: string, value: any): Promise<any> { |   async set(cacheName: string, key: string, value: any, expire?: number): Promise<any> { | ||||||
|     let cache = this.data[cacheName] |     let cache = this.data[cacheName] | ||||||
|     if (cache === undefined) { |     if (cache === undefined) { | ||||||
|       this.data[cacheName] = new Collection() |       this.data[cacheName] = new Collection() | ||||||
|       cache = this.data[cacheName] |       cache = this.data[cacheName] | ||||||
|     } |     } | ||||||
|     return cache.set(key, value) |     cache.set(key, value) | ||||||
|  |     if (expire !== undefined) setTimeout(() => { | ||||||
|  |       cache.delete(key) | ||||||
|  |     }, expire) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async delete(cacheName: string, key: string): Promise<boolean> { |   async delete(cacheName: string, key: string): Promise<boolean> { | ||||||
|  | @ -55,6 +58,8 @@ export class RedisCacheAdapter implements ICacheAdapter { | ||||||
|   _redis: Promise<Redis> |   _redis: Promise<Redis> | ||||||
|   redis?: Redis |   redis?: Redis | ||||||
|   ready: boolean = false |   ready: boolean = false | ||||||
|  |   readonly _expireIntervalTimer: number = 5000 | ||||||
|  |   private _expireInterval?: number | ||||||
| 
 | 
 | ||||||
|   constructor(options: RedisConnectOptions) { |   constructor(options: RedisConnectOptions) { | ||||||
|     this._redis = connect(options) |     this._redis = connect(options) | ||||||
|  | @ -62,6 +67,7 @@ export class RedisCacheAdapter implements ICacheAdapter { | ||||||
|       redis => { |       redis => { | ||||||
|         this.redis = redis |         this.redis = redis | ||||||
|         this.ready = true |         this.ready = true | ||||||
|  |         this._startExpireInterval() | ||||||
|       }, |       }, | ||||||
|       () => { |       () => { | ||||||
|         // TODO: Make error for this
 |         // TODO: Make error for this
 | ||||||
|  | @ -69,6 +75,26 @@ export class RedisCacheAdapter implements ICacheAdapter { | ||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private _startExpireInterval(): void { | ||||||
|  |     this._expireInterval = setInterval(() => { | ||||||
|  |       this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => { | ||||||
|  |         for (const name of names) { | ||||||
|  |           this.redis?.hvals(name).then(vals => { | ||||||
|  |             for (const val of vals) { | ||||||
|  |               const expireVal: { | ||||||
|  |                 name: string, | ||||||
|  |                 key: string, | ||||||
|  |                 at: number | ||||||
|  |               } = JSON.parse(val) | ||||||
|  |               const expired = new Date().getTime() > expireVal.at | ||||||
|  |               if (expired) this.redis?.hdel(expireVal.name, expireVal.key) | ||||||
|  |             } | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, this._expireIntervalTimer) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async _checkReady(): Promise<void> { |   async _checkReady(): Promise<void> { | ||||||
|     if (!this.ready) await this._redis |     if (!this.ready) await this._redis | ||||||
|   } |   } | ||||||
|  | @ -87,7 +113,8 @@ export class RedisCacheAdapter implements ICacheAdapter { | ||||||
|   async set( |   async set( | ||||||
|     cacheName: string, |     cacheName: string, | ||||||
|     key: string, |     key: string, | ||||||
|     value: any |     value: any, | ||||||
|  |     expire?: number | ||||||
|   ): Promise<number | undefined> { |   ): Promise<number | undefined> { | ||||||
|     await this._checkReady() |     await this._checkReady() | ||||||
|     const result = await this.redis?.hset( |     const result = await this.redis?.hset( | ||||||
|  | @ -95,6 +122,13 @@ export class RedisCacheAdapter implements ICacheAdapter { | ||||||
|       key, |       key, | ||||||
|       typeof value === 'object' ? JSON.stringify(value) : value |       typeof value === 'object' ? JSON.stringify(value) : value | ||||||
|     ) |     ) | ||||||
|  |     if (expire !== undefined) { | ||||||
|  |       await this.redis?.hset( | ||||||
|  |         `${cacheName}:expires`, | ||||||
|  |         key, | ||||||
|  |         JSON.stringify({ name: cacheName, key, at: new Date().getTime() + expire }) | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|     return result |     return result | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,7 +7,6 @@ import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' | ||||||
| import { UserManager } from '../managers/users.ts' | import { UserManager } from '../managers/users.ts' | ||||||
| import { GuildManager } from '../managers/guilds.ts' | import { GuildManager } from '../managers/guilds.ts' | ||||||
| import { ChannelsManager } from '../managers/channels.ts' | import { ChannelsManager } from '../managers/channels.ts' | ||||||
| import { MessagesManager } from '../managers/messages.ts' |  | ||||||
| import { | import { | ||||||
|   ActivityGame, |   ActivityGame, | ||||||
|   ClientActivity, |   ClientActivity, | ||||||
|  | @ -31,6 +30,8 @@ export interface ClientOptions { | ||||||
|   bot?: boolean |   bot?: boolean | ||||||
|   /** Force all requests to Canary API */ |   /** Force all requests to Canary API */ | ||||||
|   canary?: boolean |   canary?: boolean | ||||||
|  |   /** Time till which Messages are to be cached, in MS. Default is 3600000 */ | ||||||
|  |   messageCacheLifetime?: number | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -53,11 +54,12 @@ export class Client extends EventEmitter { | ||||||
|   intents?: GatewayIntents[] |   intents?: GatewayIntents[] | ||||||
|   /** Whether to force new session or not */ |   /** Whether to force new session or not */ | ||||||
|   forceNewSession?: boolean |   forceNewSession?: boolean | ||||||
|  |   /** Time till messages to stay cached, in MS. */ | ||||||
|  |   messageCacheLifetime: number = 3600000 | ||||||
| 
 | 
 | ||||||
|   users: UserManager = new UserManager(this) |   users: UserManager = new UserManager(this) | ||||||
|   guilds: GuildManager = new GuildManager(this) |   guilds: GuildManager = new GuildManager(this) | ||||||
|   channels: ChannelsManager = new ChannelsManager(this) |   channels: ChannelsManager = new ChannelsManager(this) | ||||||
|   messages: MessagesManager = new MessagesManager(this) |  | ||||||
|   emojis: EmojisManager = new EmojisManager(this) |   emojis: EmojisManager = new EmojisManager(this) | ||||||
|    |    | ||||||
|   /** Whether this client will login as bot user or not */ |   /** Whether this client will login as bot user or not */ | ||||||
|  | @ -80,6 +82,7 @@ export class Client extends EventEmitter { | ||||||
|           : new ClientPresence(options.presence) |           : new ClientPresence(options.presence) | ||||||
|     if (options.bot === false) this.bot = false |     if (options.bot === false) this.bot = false | ||||||
|     if (options.canary === true) this.canary = true |     if (options.canary === true) this.canary = true | ||||||
|  |     if (options.messageCacheLifetime !== undefined) this.messageCacheLifetime = options.messageCacheLifetime | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Set Cache Adapter */ |   /** Set Cache Adapter */ | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | import { MessagesManager } from "../../mod.ts" | ||||||
| import { Client } from '../models/client.ts' | import { Client } from '../models/client.ts' | ||||||
| import { GuildTextChannelPayload, MessageOption, Overwrite, TextChannelPayload } from '../types/channel.ts' | import { GuildTextChannelPayload, MessageOption, Overwrite, TextChannelPayload } from '../types/channel.ts' | ||||||
| import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts' | import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts' | ||||||
|  | @ -11,9 +12,11 @@ type AllMessageOptions = MessageOption | Embed | ||||||
| export class TextChannel extends Channel { | export class TextChannel extends Channel { | ||||||
|   lastMessageID?: string |   lastMessageID?: string | ||||||
|   lastPinTimestamp?: string |   lastPinTimestamp?: string | ||||||
|  |   messages: MessagesManager | ||||||
| 
 | 
 | ||||||
|   constructor (client: Client, data: TextChannelPayload) { |   constructor (client: Client, data: TextChannelPayload) { | ||||||
|     super(client, data) |     super(client, data) | ||||||
|  |     this.messages = new MessagesManager(this.client, this) | ||||||
|     this.lastMessageID = data.last_message_id |     this.lastMessageID = data.last_message_id | ||||||
|     this.lastPinTimestamp = data.last_pin_timestamp |     this.lastPinTimestamp = data.last_pin_timestamp | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ import { Command, CommandClient, Intents } from '../../mod.ts' | ||||||
| import { GuildChannel } from "../managers/guildChannels.ts" | import { GuildChannel } from "../managers/guildChannels.ts" | ||||||
| import { CommandContext } from "../models/command.ts" | import { CommandContext } from "../models/command.ts" | ||||||
| import { Extension } from "../models/extensions.ts" | import { Extension } from "../models/extensions.ts" | ||||||
|  | import { Message } from "../structures/message.ts" | ||||||
|  | import { MessageDeletePayload } from "../types/gateway.ts" | ||||||
| import { TOKEN } from './config.ts' | import { TOKEN } from './config.ts' | ||||||
| 
 | 
 | ||||||
| const client = new CommandClient({ | const client = new CommandClient({ | ||||||
|  | @ -16,6 +18,14 @@ client.on('ready', () => { | ||||||
|   console.log(`[Login] Logged in as ${client.user?.tag}!`) |   console.log(`[Login] Logged in as ${client.user?.tag}!`) | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
|  | client.on('messageDelete', (msg: Message) => { | ||||||
|  |   console.log(`Message Deleted: ${msg.id}, ${msg.author.tag}, ${msg.content}`) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | client.on('messageDeleteUncached', (d: MessageDeletePayload) => { | ||||||
|  |   console.log(`Uncached Message Deleted: ${d.id} in ${d.channel_id}`) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
| // client.on('messageCreate', msg => console.log(`${msg.author.tag}: ${msg.content}`))
 | // client.on('messageCreate', msg => console.log(`${msg.author.tag}: ${msg.content}`))
 | ||||||
| 
 | 
 | ||||||
| client.on("commandError", console.error) | client.on("commandError", console.error) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue