import type { Bot } from "./bot.ts"; import type { DiscordenoChannel } from "./transformers/channel.ts"; import type { DiscordenoGuild } from "./transformers/guild.ts"; import type { DiscordenoMember, DiscordenoUser } from "./transformers/member.ts"; import type { DiscordenoMessage } from "./transformers/message.ts"; import { DiscordenoPresence } from "./transformers/presence.ts"; import { GuildMember } from "./types/members/guildMember.ts"; import { Collection } from "./util/collection.ts"; function messageSweeper(bot: Bot, message: DiscordenoMessage) { // DM messages aren't needed if (!message.guildId) return true; // Only delete messages older than 10 minutes return Date.now() - message.timestamp > 600000; } function memberSweeper(bot: Bot, member: DiscordenoMember) { // Don't sweep the bot else strange things will happen if (member.id === bot.id) return false; // Only sweep members who were not active the last 30 minutes return Date.now() - member.cachedAt > 1800000; } function guildSweeper(bot: Bot, guild: DiscordenoGuild) { // Reset activity for next interval if (bot.cache.activeGuildIds.delete(guild.id)) return false; // This is inactive guild. Not a single thing has happened for atleast 30 minutes. // Not a reaction, not a message, not any event! bot.cache.dispatchedGuildIds.add(guild.id); return true; } function channelSweeper(bot: Bot, channel: DiscordenoChannel, key: bigint) { // If this is in a guild and the guild was dispatched, then we can dispatch the channel if (channel.guildId && bot.cache.dispatchedGuildIds.has(channel.guildId)) { bot.cache.dispatchedChannelIds.add(channel.id); return true; } // THE KEY DM CHANNELS ARE STORED BY IS THE USER ID. If the user is not cached, we dont need to cache their dm channel. if (!channel.guildId && !bot.cache.members.has(key)) return true; return false; } export function createCache( bot: Bot, options: { isAsync: true; tableCreator: (bot: Bot, tableName: TableNames) => AsyncCacheHandler; } ): AsyncCache; export function createCache( bot: Bot, options: { isAsync: false; tableCreator?: (bot: Bot, tableName: TableNames) => CacheHandler; } ): Cache; export function createCache( bot: Bot, options: { isAsync: boolean; tableCreator?: (bot: Bot, tableName: TableNames) => CacheHandler | AsyncCacheHandler; } ): Omit | Omit { let cache: Cache | AsyncCache; if (options.isAsync) { if (!options.tableCreator) { throw new Error("Async cache requires a tableCreator to be passed."); } cache = { guilds: options.tableCreator(bot, "guilds"), users: options.tableCreator(bot, "users"), members: options.tableCreator(bot, "members"), channels: options.tableCreator(bot, "channels"), messages: options.tableCreator(bot, "messages"), presences: options.tableCreator(bot, "presences"), // threads: options.tableCreator(bot, "threads"), unavailableGuilds: options.tableCreator(bot, "unavailableGuilds"), dispatchedGuildIds: options.tableCreator(bot, "dispatchedGuildIds"), dispatchedChannelIds: options.tableCreator(bot, "dispatchedChannelIds"), activeGuildIds: options.tableCreator(bot, "activeGuildIds"), unrepliedInteractions: new Set(), fetchAllMembersProcessingRequests: new Map(), execute: async function () { throw new Error("Async Cache requires a custom execute function to be implemented."); }, } as AsyncCache; } else { if (!options.tableCreator) options.tableCreator = createTable; cache = { guilds: options.tableCreator(bot, "guilds"), users: options.tableCreator(bot, "users"), members: options.tableCreator(bot, "members"), channels: options.tableCreator(bot, "channels"), messages: options.tableCreator(bot, "messages"), presences: options.tableCreator(bot, "presences"), // threads: options.tableCreator(bot, "threads"), unavailableGuilds: options.tableCreator(bot, "unavailableGuilds"), dispatchedGuildIds: new Set(), dispatchedChannelIds: new Set(), activeGuildIds: new Set(), unrepliedInteractions: new Set(), fetchAllMembersProcessingRequests: new Map(), } as Cache; cache.execute = createExecute(cache); } // Interaction sweeper in case users don't reply do slash commands // PS: always reply .-. its good practise // setInterval(() => { // const values = cache.unrepliedInteractions.values(); // const now = Date.now(); // for (let val; (val = values.next().value); ) { // // Interaction is older than 15 minutes // // and a reply has never been send // // so remove it from cache // // PS: DON'T USE THIS CODE TO CONVERT DC SNOWFLAKES TO UNIX // // SINCE U WILL GET AN INVALID RESULT // if ((val >> 22n) + 1420071300000n < now) { // cache.unrepliedInteractions.delete(val); // } // } // }, 300000); return cache; } export type CachedDiscordenoUser = DiscordenoUser & { guilds: Map }; export interface Cache { guilds: CacheHandler; users: CacheHandler; members: CacheHandler; channels: CacheHandler; messages: CacheHandler; presences: CacheHandler; // threads: CacheHandler; unavailableGuilds: CacheHandler; dispatchedGuildIds: Set; dispatchedChannelIds: Set; activeGuildIds: Set; unrepliedInteractions: Set; fetchAllMembersProcessingRequests: Map; execute: CacheExecutor; } export interface CachedUnavailableGuild { shardId: number; since: number; dispatched?: true; } export interface AsyncCache { guilds: AsyncCacheHandler; users: AsyncCacheHandler; members: CacheHandler; channels: AsyncCacheHandler; messages: AsyncCacheHandler; presences: AsyncCacheHandler; // threads: AsyncCacheHandler; unavailableGuilds: AsyncCacheHandler; dispatchedGuildIds: AsyncCacheHandler; dispatchedChannelIds: AsyncCacheHandler; activeGuildIds: AsyncCacheHandler; unrepliedInteractions: Set; fetchAllMembersProcessingRequests: Map; execute: CacheExecutor; } function createTable(bot: Bot, _table: TableNames): CacheHandler { const table = new Collection(); // @ts-ignore TODO: fix type error itoh pwease if (_table === "guilds") table.startSweeper({ filter: guildSweeper, interval: 3660000, bot }); // @ts-ignore TODO: fix type error itoh pwease if (_table === "channels") table.startSweeper({ filter: channelSweeper, interval: 3660000, bot }); // @ts-ignore TODO: fix type error itoh pwease if (_table === "messages") table.startSweeper({ filter: messageSweeper, interval: 300000, bot }); // @ts-ignore TODO: fix type error itoh pwease if (_table === "members") table.startSweeper({ filter: memberSweeper, interval: 300000, bot }); if (_table === "presences") table.startSweeper({ filter: () => true, interval: 300000, bot }); return { clear: () => table.clear(), delete: (key) => table.delete(key), has: (key) => table.has(key), size: () => table.size, set: (key, data) => !!table.set(key, data), get: (key) => table.get(key), forEach: (callback) => table.forEach(callback), filter: (callback) => table.filter(callback), }; } export interface CacheHandler { /** Completely empty this table. */ clear(): void; /** Delete the data related to this key from table. */ delete(key: bigint): boolean; /** Check if there is data assigned to this key. */ has(key: bigint): boolean; /** Check how many items are stored in this table. */ size(): number; /** Store new data to this table. */ set(key: bigint, data: T): boolean; /** Get a stored item from the table. */ get(key: bigint): T | undefined; // TODO: maybe its possible to stringify the function and send it to the custom cache handler :thinking: /** * Loop over each entry and execute callback function. * @important This function NOT optimised and will force load everything when using custom cache. */ forEach(callback: (value: T, key: bigint) => unknown): void; // TODO: maybe its possible to stringify the function and send it to the custom cache handler :thinking: /** * Loop over each entry and execute callback function. * @important This function NOT optimised and will force load everything when using custom cache. */ filter(callback: (value: T, key: bigint) => boolean): Collection; } export type AsyncCacheHandler = { [K in keyof CacheHandler]: (...args: Parameters[K]>) => Promise[K]>>; }; export type CacheExecutor = ( type: | "GET_ALL_MEMBERS" | "DELETE_MESSAGES_FROM_CHANNEL" | "DELETE_ROLE_FROM_MEMBER" | "BULK_DELETE_MESSAGES" | "GUILD_MEMBER_CHUNK" | "GUILD_MEMBER_COUNT_DECREMENT" | "GUILD_MEMBER_COUNT_INCREMENT" | "DELETE_MESSAGES_FROM_GUILD" | "DELETE_CHANNELS_FROM_GUILD" | "DELETE_GUILD_FROM_MEMBER", options: Record ) => Promise; export function createExecute(cache: Cache): CacheExecutor { return function (type, options) { switch (type) { case "DELETE_MESSAGES_FROM_CHANNEL": cache.messages.forEach((message) => { if (message.channelId === options.channelId) { cache.messages.delete(message.id); } }); return; case "BULK_DELETE_MESSAGES": return options.messageIds .map((id: bigint) => { const cached = cache.messages.get(id); if (!cached) return; cache.messages.delete(id); return cached; }) .filter((m: DiscordenoMessage) => m); case "GUILD_MEMBER_CHUNK": options.users.forEach((user: DiscordenoUser) => { cache.users.set(user.id, user); }); // TODO: FIND A GOOD WAY FOR MEMBERS CACHE (GUILD ID) // options.members.forEach((member) => { // cache.members.set(member.id, member); // }); return; } }; } export type TableNames = | "channels" | "users" | "guilds" | "messages" | "presences" | "threads" | "unavailableGuilds" | "members" | "dispatchedGuildIds" | "dispatchedChannelIds" | "activeGuildIds";