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
|
||||||
|
@ -18,28 +18,31 @@ export class DefaultCacheAdapter implements ICacheAdapter {
|
||||||
[name: string]: Collection<string, any>
|
[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]
|
const cache = this.data[cacheName]
|
||||||
if (cache === undefined) return
|
if (cache === undefined) return
|
||||||
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> {
|
||||||
const cache = this.data[cacheName]
|
const cache = this.data[cacheName]
|
||||||
if (cache === undefined) return false
|
if (cache === undefined) return false
|
||||||
return cache.delete(key)
|
return cache.delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async array (cacheName: string): Promise<any[] | undefined> {
|
async array(cacheName: string): Promise<any[] | undefined> {
|
||||||
const cache = this.data[cacheName]
|
const cache = this.data[cacheName]
|
||||||
if (cache === undefined) return
|
if (cache === undefined) return
|
||||||
return cache.array()
|
return cache.array()
|
||||||
|
@ -55,13 +58,16 @@ 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)
|
||||||
this._redis.then(
|
this._redis.then(
|
||||||
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,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
|
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()
|
await this._checkReady()
|
||||||
const cache = await this.redis?.hget(cacheName, key)
|
const cache = await this.redis?.hget(cacheName, key)
|
||||||
if (cache === undefined) return
|
if (cache === undefined) return
|
||||||
|
@ -84,10 +110,11 @@ 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,10 +122,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete (cacheName: string, key: string): Promise<boolean> {
|
async delete(cacheName: string, key: string): Promise<boolean> {
|
||||||
await this._checkReady()
|
await this._checkReady()
|
||||||
const exists = await this.redis?.hexists(cacheName, key)
|
const exists = await this.redis?.hexists(cacheName, key)
|
||||||
if (exists === 0) return false
|
if (exists === 0) return false
|
||||||
|
@ -106,7 +140,7 @@ export class RedisCacheAdapter implements ICacheAdapter {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
async array (cacheName: string): Promise<any[] | undefined> {
|
async array(cacheName: string): Promise<any[] | undefined> {
|
||||||
await this._checkReady()
|
await this._checkReady()
|
||||||
const data = await this.redis?.hvals(cacheName)
|
const data = await this.redis?.hvals(cacheName)
|
||||||
return data?.map((e: string) => JSON.parse(e))
|
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 { 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…
Reference in a new issue