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 { resume } from './resume.ts' | ||||
| import { reconnect } from './reconnect.ts' | ||||
| import { messageDelete } from "./messageDelete.ts" | ||||
| 
 | ||||
| export const gatewayHandlers: { | ||||
|   [eventCode in GatewayEvents]: GatewayEventHandler | undefined | ||||
|  | @ -42,7 +43,7 @@ export const gatewayHandlers: { | |||
|   INVITE_DELETE: undefined, | ||||
|   MESSAGE_CREATE: messageCreate, | ||||
|   MESSAGE_UPDATE: undefined, | ||||
|   MESSAGE_DELETE: undefined, | ||||
|   MESSAGE_DELETE: messageDelete, | ||||
|   MESSAGE_DELETE_BULK: undefined, | ||||
|   MESSAGE_REACTION_ADD: undefined, | ||||
|   MESSAGE_REACTION_REMOVE: undefined, | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ export const messageCreate: GatewayEventHandler = async ( | |||
|   if (channel === undefined) | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
 | ||||
|     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) | ||||
|   await gateway.client.users.set(d.author.id, d.author) | ||||
|   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' | ||||
| 
 | ||||
| export class MessagesManager extends BaseManager<MessagePayload, Message> { | ||||
|   constructor (client: Client) { | ||||
|   channel: TextChannel | ||||
| 
 | ||||
|   constructor (client: Client, channel: TextChannel) { | ||||
|     super(client, 'messages', Message) | ||||
|     this.channel = channel | ||||
|   } | ||||
| 
 | ||||
|   async get (key: string): Promise<Message | undefined> { | ||||
|  | @ -26,18 +29,22 @@ export class MessagesManager extends BaseManager<MessagePayload, Message> { | |||
|     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) => { | ||||
|       this.client.rest | ||||
|         .get(CHANNEL_MESSAGE(channelID, id)) | ||||
|         .get(CHANNEL_MESSAGE(this.channel.id, id)) | ||||
|         .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>( | ||||
|             channelID | ||||
|             this.channel.id | ||||
|           ) | ||||
|           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) | ||||
|           await this.client.users.set( | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import { | |||
| 
 | ||||
| export interface ICacheAdapter { | ||||
|   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 | ||||
|   array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined> | ||||
|   deleteCache: (cacheName: string) => any | ||||
|  | @ -18,28 +18,31 @@ export class DefaultCacheAdapter implements ICacheAdapter { | |||
|     [name: string]: Collection<string, any> | ||||
|   } = {} | ||||
| 
 | ||||
|   async get (cacheName: string, key: string): Promise<undefined | any> { | ||||
|   async get(cacheName: string, key: string): Promise<undefined | any> { | ||||
|     const cache = this.data[cacheName] | ||||
|     if (cache === undefined) return | ||||
|     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] | ||||
|     if (cache === undefined) { | ||||
|       this.data[cacheName] = new Collection() | ||||
|       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> { | ||||
|     const cache = this.data[cacheName] | ||||
|     if (cache === undefined) return false | ||||
|     return cache.delete(key) | ||||
|   } | ||||
| 
 | ||||
|   async array (cacheName: string): Promise<any[] | undefined> { | ||||
|   async array(cacheName: string): Promise<any[] | undefined> { | ||||
|     const cache = this.data[cacheName] | ||||
|     if (cache === undefined) return | ||||
|     return cache.array() | ||||
|  | @ -55,13 +58,16 @@ export class RedisCacheAdapter implements ICacheAdapter { | |||
|   _redis: Promise<Redis> | ||||
|   redis?: Redis | ||||
|   ready: boolean = false | ||||
|   readonly _expireIntervalTimer: number = 5000 | ||||
|   private _expireInterval?: number | ||||
| 
 | ||||
|   constructor (options: RedisConnectOptions) { | ||||
|   constructor(options: RedisConnectOptions) { | ||||
|     this._redis = connect(options) | ||||
|     this._redis.then( | ||||
|       redis => { | ||||
|         this.redis = redis | ||||
|         this.ready = true | ||||
|         this._startExpireInterval() | ||||
|       }, | ||||
|       () => { | ||||
|         // TODO: Make error for this
 | ||||
|  | @ -69,11 +75,31 @@ export class RedisCacheAdapter implements ICacheAdapter { | |||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   async _checkReady (): Promise<void> { | ||||
|   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> { | ||||
|     if (!this.ready) await this._redis | ||||
|   } | ||||
| 
 | ||||
|   async get (cacheName: string, key: string): Promise<string | undefined> { | ||||
|   async get(cacheName: string, key: string): Promise<string | undefined> { | ||||
|     await this._checkReady() | ||||
|     const cache = await this.redis?.hget(cacheName, key) | ||||
|     if (cache === undefined) return | ||||
|  | @ -84,10 +110,11 @@ export class RedisCacheAdapter implements ICacheAdapter { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async set ( | ||||
|   async set( | ||||
|     cacheName: string, | ||||
|     key: string, | ||||
|     value: any | ||||
|     value: any, | ||||
|     expire?: number | ||||
|   ): Promise<number | undefined> { | ||||
|     await this._checkReady() | ||||
|     const result = await this.redis?.hset( | ||||
|  | @ -95,10 +122,17 @@ export class RedisCacheAdapter implements ICacheAdapter { | |||
|       key, | ||||
|       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 | ||||
|   } | ||||
| 
 | ||||
|   async delete (cacheName: string, key: string): Promise<boolean> { | ||||
|   async delete(cacheName: string, key: string): Promise<boolean> { | ||||
|     await this._checkReady() | ||||
|     const exists = await this.redis?.hexists(cacheName, key) | ||||
|     if (exists === 0) return false | ||||
|  | @ -106,7 +140,7 @@ export class RedisCacheAdapter implements ICacheAdapter { | |||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   async array (cacheName: string): Promise<any[] | undefined> { | ||||
|   async array(cacheName: string): Promise<any[] | undefined> { | ||||
|     await this._checkReady() | ||||
|     const data = await this.redis?.hvals(cacheName) | ||||
|     return data?.map((e: string) => JSON.parse(e)) | ||||
|  |  | |||
|  | @ -7,7 +7,6 @@ import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' | |||
| import { UserManager } from '../managers/users.ts' | ||||
| import { GuildManager } from '../managers/guilds.ts' | ||||
| import { ChannelsManager } from '../managers/channels.ts' | ||||
| import { MessagesManager } from '../managers/messages.ts' | ||||
| import { | ||||
|   ActivityGame, | ||||
|   ClientActivity, | ||||
|  | @ -31,6 +30,8 @@ export interface ClientOptions { | |||
|   bot?: boolean | ||||
|   /** Force all requests to Canary API */ | ||||
|   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[] | ||||
|   /** Whether to force new session or not */ | ||||
|   forceNewSession?: boolean | ||||
|   /** Time till messages to stay cached, in MS. */ | ||||
|   messageCacheLifetime: number = 3600000 | ||||
| 
 | ||||
|   users: UserManager = new UserManager(this) | ||||
|   guilds: GuildManager = new GuildManager(this) | ||||
|   channels: ChannelsManager = new ChannelsManager(this) | ||||
|   messages: MessagesManager = new MessagesManager(this) | ||||
|   emojis: EmojisManager = new EmojisManager(this) | ||||
|    | ||||
|   /** Whether this client will login as bot user or not */ | ||||
|  | @ -80,6 +82,7 @@ export class Client extends EventEmitter { | |||
|           : new ClientPresence(options.presence) | ||||
|     if (options.bot === false) this.bot = false | ||||
|     if (options.canary === true) this.canary = true | ||||
|     if (options.messageCacheLifetime !== undefined) this.messageCacheLifetime = options.messageCacheLifetime | ||||
|   } | ||||
| 
 | ||||
|   /** Set Cache Adapter */ | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import { MessagesManager } from "../../mod.ts" | ||||
| import { Client } from '../models/client.ts' | ||||
| import { GuildTextChannelPayload, MessageOption, Overwrite, TextChannelPayload } from '../types/channel.ts' | ||||
| import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts' | ||||
|  | @ -11,9 +12,11 @@ type AllMessageOptions = MessageOption | Embed | |||
| export class TextChannel extends Channel { | ||||
|   lastMessageID?: string | ||||
|   lastPinTimestamp?: string | ||||
|   messages: MessagesManager | ||||
| 
 | ||||
|   constructor (client: Client, data: TextChannelPayload) { | ||||
|     super(client, data) | ||||
|     this.messages = new MessagesManager(this.client, this) | ||||
|     this.lastMessageID = data.last_message_id | ||||
|     this.lastPinTimestamp = data.last_pin_timestamp | ||||
|   } | ||||
|  |  | |||
|  | @ -2,6 +2,8 @@ import { Command, CommandClient, Intents } from '../../mod.ts' | |||
| import { GuildChannel } from "../managers/guildChannels.ts" | ||||
| import { CommandContext } from "../models/command.ts" | ||||
| import { Extension } from "../models/extensions.ts" | ||||
| import { Message } from "../structures/message.ts" | ||||
| import { MessageDeletePayload } from "../types/gateway.ts" | ||||
| import { TOKEN } from './config.ts' | ||||
| 
 | ||||
| const client = new CommandClient({ | ||||
|  | @ -16,6 +18,14 @@ client.on('ready', () => { | |||
|   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("commandError", console.error) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue