diff --git a/mod.ts b/mod.ts index d7d9a2bd5..412b757e7 100644 --- a/mod.ts +++ b/mod.ts @@ -1,13 +1,4 @@ -export * from "./src/api/controllers/bans.ts"; -export * from "./src/api/controllers/cache.ts"; -export * from "./src/api/controllers/channels.ts"; -export * from "./src/api/controllers/guilds.ts"; -export * from "./src/api/controllers/members.ts"; -export * from "./src/api/controllers/messages.ts"; -export * from "./src/api/controllers/misc.ts"; -export * from "./src/api/controllers/mod.ts"; -export * from "./src/api/controllers/reactions.ts"; -export * from "./src/api/controllers/roles.ts"; +export * from "./src/cache.ts"; export * from "./src/api/handlers/channel.ts"; export * from "./src/api/handlers/guild.ts"; export * from "./src/api/handlers/member.ts"; @@ -28,3 +19,4 @@ export * from "./src/util/collection.ts"; export * from "./src/util/permissions.ts"; export * from "./src/util/utils.ts"; export * from "./src/ws/mod.ts"; +export * from "./src/api/controllers/mod.ts"; diff --git a/src/api/controllers/READY.ts b/src/api/controllers/READY.ts index ac3e0ade0..0d6f176a9 100644 --- a/src/api/controllers/READY.ts +++ b/src/api/controllers/READY.ts @@ -10,10 +10,9 @@ import { delay } from "../../util/utils.ts"; import { allowNextShard, basicShards } from "../../ws/mod.ts"; import { initialMemberLoadQueue } from "../structures/guild.ts"; import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; +import { cacheHandlers } from "../../cache.ts"; -/** This function is the internal handler for the ready event. Users can override this with controllers if desired. */ -export async function handleInternalReady( +export async function handleReady( data: DiscordPayload, shardID: number, ) { diff --git a/src/api/controllers/bans.ts b/src/api/controllers/bans.ts deleted file mode 100644 index b3c406932..000000000 --- a/src/api/controllers/bans.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { DiscordPayload, GuildBanPayload } from "../../types/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalGuildBanAdd(data: DiscordPayload) { - const payload = data.d as GuildBanPayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const member = await cacheHandlers.get("members", payload.user.id); - eventHandlers.guildBanAdd?.(guild, payload.user, member); -} - -export async function handleInternalGuildBanRemove(data: DiscordPayload) { - const payload = data.d as GuildBanPayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const member = await cacheHandlers.get("members", payload.user.id); - eventHandlers.guildBanRemove?.(guild, payload.user, member); -} diff --git a/src/api/controllers/channels/CHANNEL_CREATE.ts b/src/api/controllers/channels/CHANNEL_CREATE.ts new file mode 100644 index 000000000..a2770aaf9 --- /dev/null +++ b/src/api/controllers/channels/CHANNEL_CREATE.ts @@ -0,0 +1,13 @@ +import { eventHandlers } from "../../../bot.ts"; +import { ChannelCreatePayload, DiscordPayload } from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleChannelCreate(data: DiscordPayload) { + const payload = data.d as ChannelCreatePayload; + + const channelStruct = await structures.createChannelStruct(payload); + await cacheHandlers.set("channels", channelStruct.id, channelStruct); + + eventHandlers.channelCreate?.(channelStruct); +} diff --git a/src/api/controllers/channels.ts b/src/api/controllers/channels/CHANNEL_DELETE.ts similarity index 52% rename from src/api/controllers/channels.ts rename to src/api/controllers/channels/CHANNEL_DELETE.ts index 55a14a443..a6c66bc41 100644 --- a/src/api/controllers/channels.ts +++ b/src/api/controllers/channels/CHANNEL_DELETE.ts @@ -1,22 +1,12 @@ -import { eventHandlers } from "../../bot.ts"; +import { eventHandlers } from "../../../bot.ts"; import { ChannelCreatePayload, ChannelTypes, DiscordPayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; +} from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; -export async function handleInternalChannelCreate(data: DiscordPayload) { - const payload = data.d as ChannelCreatePayload; - - const channelStruct = await structures.createChannelStruct(payload); - await cacheHandlers.set("channels", channelStruct.id, channelStruct); - - eventHandlers.channelCreate?.(channelStruct); -} - -export async function handleInternalChannelDelete(data: DiscordPayload) { +export async function handleChannelDelete(data: DiscordPayload) { const payload = data.d as ChannelCreatePayload; const cachedChannel = await cacheHandlers.get("channels", payload.id); @@ -48,15 +38,3 @@ export async function handleInternalChannelDelete(data: DiscordPayload) { }); eventHandlers.channelDelete?.(cachedChannel); } - -export async function handleInternalChannelUpdate(data: DiscordPayload) { - const payload = data.d as ChannelCreatePayload; - const cachedChannel = await cacheHandlers.get("channels", payload.id); - - const channelStruct = await structures.createChannelStruct(payload); - await cacheHandlers.set("channels", channelStruct.id, channelStruct); - - if (!cachedChannel) return; - - eventHandlers.channelUpdate?.(channelStruct, cachedChannel); -} diff --git a/src/api/controllers/CHANNEL_PINS_UPDATE.ts b/src/api/controllers/channels/CHANNEL_PINS_UPDATE.ts similarity index 78% rename from src/api/controllers/CHANNEL_PINS_UPDATE.ts rename to src/api/controllers/channels/CHANNEL_PINS_UPDATE.ts index 97633953b..e32db1a7e 100644 --- a/src/api/controllers/CHANNEL_PINS_UPDATE.ts +++ b/src/api/controllers/channels/CHANNEL_PINS_UPDATE.ts @@ -1,9 +1,9 @@ -import { eventHandlers } from "../../bot.ts"; +import { eventHandlers } from "../../../bot.ts"; import { DiscordChannelPinsUpdateEvent, DiscordPayload, -} from "../../types/mod.ts"; -import { cacheHandlers } from "./cache.ts"; +} from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; export async function handleChannelPinsUpdate(data: DiscordPayload) { const payload = data.d as DiscordChannelPinsUpdateEvent; diff --git a/src/api/controllers/channels/CHANNEL_UPDATE.ts b/src/api/controllers/channels/CHANNEL_UPDATE.ts new file mode 100644 index 000000000..288e4d4f8 --- /dev/null +++ b/src/api/controllers/channels/CHANNEL_UPDATE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../../bot.ts"; +import { ChannelCreatePayload, DiscordPayload } from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleChannelUpdate(data: DiscordPayload) { + const payload = data.d as ChannelCreatePayload; + const cachedChannel = await cacheHandlers.get("channels", payload.id); + + const channelStruct = await structures.createChannelStruct(payload); + await cacheHandlers.set("channels", channelStruct.id, channelStruct); + + if (!cachedChannel) return; + + eventHandlers.channelUpdate?.(channelStruct, cachedChannel); +} diff --git a/src/api/controllers/commands/APPLICATION_COMMAND_CREATE.ts b/src/api/controllers/commands/APPLICATION_COMMAND_CREATE.ts new file mode 100644 index 000000000..344bcdfd0 --- /dev/null +++ b/src/api/controllers/commands/APPLICATION_COMMAND_CREATE.ts @@ -0,0 +1,18 @@ +import { eventHandlers } from "../../../bot.ts"; +import { ApplicationCommandEvent, DiscordPayload } from "../../../types/mod.ts"; + +export function handleApplicationCommandCreate( + data: DiscordPayload, +) { + const { + guild_id: guildID, + application_id: applicationID, + ...rest + } = data.d as ApplicationCommandEvent; + + eventHandlers.applicationCommandCreate?.({ + ...rest, + guildID, + applicationID, + }); +} diff --git a/src/api/controllers/commands/APPLICATION_COMMAND_DELETE.ts b/src/api/controllers/commands/APPLICATION_COMMAND_DELETE.ts new file mode 100644 index 000000000..e85069148 --- /dev/null +++ b/src/api/controllers/commands/APPLICATION_COMMAND_DELETE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../../bot.ts"; +import { ApplicationCommandEvent, DiscordPayload } from "../../../types/mod.ts"; + +export function handleApplicationCommandDelete(data: DiscordPayload) { + const { + application_id: applicationID, + guild_id: guildID, + ...rest + } = data.d as ApplicationCommandEvent; + + eventHandlers.applicationCommandDelete?.({ + ...rest, + guildID, + applicationID, + }); +} diff --git a/src/api/controllers/commands/APPLICATION_COMMAND_UPDATE.ts b/src/api/controllers/commands/APPLICATION_COMMAND_UPDATE.ts new file mode 100644 index 000000000..c1bf97ba0 --- /dev/null +++ b/src/api/controllers/commands/APPLICATION_COMMAND_UPDATE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../../bot.ts"; +import { ApplicationCommandEvent, DiscordPayload } from "../../../types/mod.ts"; + +export function handleApplicationCommandUpdate(data: DiscordPayload) { + const { + application_id: applicationID, + guild_id: guildID, + ...rest + } = data.d as ApplicationCommandEvent; + + eventHandlers.applicationCommandUpdate?.({ + ...rest, + guildID, + applicationID, + }); +} diff --git a/src/api/controllers/guilds.ts b/src/api/controllers/guilds.ts deleted file mode 100644 index e828dc361..000000000 --- a/src/api/controllers/guilds.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - CreateGuildPayload, - DiscordPayload, - GuildDeletePayload, - GuildEmojisUpdatePayload, - GuildUpdateChange, - UpdateGuildPayload, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { Collection } from "../../util/collection.ts"; -import { basicShards } from "../../ws/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalGuildCreate( - data: DiscordPayload, - shardID: number, -) { - const payload = data.d as CreateGuildPayload; - // When shards resume they emit GUILD_CREATE again. - if (await cacheHandlers.has("guilds", payload.id)) return; - - const guildStruct = await structures.createGuildStruct( - data.d as CreateGuildPayload, - shardID, - ); - await cacheHandlers.set("guilds", guildStruct.id, guildStruct); - - const shard = basicShards.get(shardID); - - if (shard?.unavailableGuildIDs.has(payload.id)) { - await cacheHandlers.delete("unavailableGuilds", payload.id); - - shard.unavailableGuildIDs.delete(payload.id); - } - - if (!cache.isReady) return eventHandlers.guildLoaded?.(guildStruct); - eventHandlers.guildCreate?.(guildStruct); -} - -export async function handleInternalGuildDelete( - data: DiscordPayload, - shardID: number, -) { - const payload = data.d as GuildDeletePayload; - cacheHandlers.forEach("messages", (message) => { - if (message.guildID === payload.id) { - cacheHandlers.delete("messages", message.id); - } - }); - - cacheHandlers.forEach("channels", (channel) => { - if (channel.guildID === payload.id) { - cacheHandlers.delete("channels", channel.id); - } - }); - - cacheHandlers.forEach("members", async (member) => { - if (!member.guilds.has(payload.id)) return; - - member.guilds.delete(payload.id); - - if (!member.guilds.size) { - await cacheHandlers.delete("members", member.id); - return; - } - - await cacheHandlers.set("members", member.id, member); - }); - - if (payload.unavailable) { - const shard = basicShards.get(shardID); - if (shard) shard.unavailableGuildIDs.add(payload.id); - - return cacheHandlers.set("unavailableGuilds", payload.id, Date.now()); - } - - const guild = await cacheHandlers.get("guilds", payload.id); - if (!guild) return; - - await cacheHandlers.delete("guilds", payload.id); - - eventHandlers.guildDelete?.(guild); -} - -export async function handleInternalGuildUpdate(data: DiscordPayload) { - const payload = data.d as UpdateGuildPayload; - const cachedGuild = await cacheHandlers.get("guilds", payload.id); - if (!cachedGuild) return; - - const keysToSkip = [ - "roles", - "guild_hashes", - "guild_id", - "max_members", - "emojis", - ]; - - const changes = Object.entries(payload) - .map(([key, value]) => { - if (keysToSkip.includes(key)) return; - - // @ts-ignore index signature - const cachedValue = cachedGuild[key]; - if (cachedValue !== value) { - // Guild create sends undefined and update sends false. - if (!cachedValue && !value) return; - - if (Array.isArray(cachedValue) && Array.isArray(value)) { - const different = (cachedValue.length !== value.length) || - cachedValue.find((val) => !value.includes(val)) || - value.find((val) => !cachedValue.includes(val)); - if (!different) return; - } - - // @ts-ignore index signature - cachedGuild[key] = value; - return { key, oldValue: cachedValue, value }; - } - }).filter((change) => change) as GuildUpdateChange[]; - - await cacheHandlers.set("guilds", payload.id, cachedGuild); - - eventHandlers.guildUpdate?.(cachedGuild, changes); -} - -export async function handleInternalGuildEmojisUpdate(data: DiscordPayload) { - const payload = data.d as GuildEmojisUpdatePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const cachedEmojis = guild.emojis; - guild.emojis = new Collection( - payload.emojis.map((emoji) => [emoji.id ?? emoji.name, emoji]), - ); - - cacheHandlers.set("guilds", payload.guild_id, guild); - - eventHandlers.guildEmojisUpdate?.( - guild, - guild.emojis, - cachedEmojis, - ); -} diff --git a/src/api/controllers/guilds/GUILD_BAN_ADD.ts b/src/api/controllers/guilds/GUILD_BAN_ADD.ts new file mode 100644 index 000000000..ab3b00119 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_BAN_ADD.ts @@ -0,0 +1,12 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, GuildBanPayload } from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildBanAdd(data: DiscordPayload) { + const payload = data.d as GuildBanPayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const member = await cacheHandlers.get("members", payload.user.id); + eventHandlers.guildBanAdd?.(guild, payload.user, member); +} diff --git a/src/api/controllers/guilds/GUILD_BAN_REMOVE.ts b/src/api/controllers/guilds/GUILD_BAN_REMOVE.ts new file mode 100644 index 000000000..4f4c0c25f --- /dev/null +++ b/src/api/controllers/guilds/GUILD_BAN_REMOVE.ts @@ -0,0 +1,12 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, GuildBanPayload } from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildBanRemove(data: DiscordPayload) { + const payload = data.d as GuildBanPayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const member = await cacheHandlers.get("members", payload.user.id); + eventHandlers.guildBanRemove?.(guild, payload.user, member); +} diff --git a/src/api/controllers/guilds/GUILD_CREATE.ts b/src/api/controllers/guilds/GUILD_CREATE.ts new file mode 100644 index 000000000..56b9a4455 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_CREATE.ts @@ -0,0 +1,32 @@ +import { eventHandlers } from "../../../bot.ts"; +import { CreateGuildPayload, DiscordPayload } from "../../../types/mod.ts"; +import { cache } from "../../../util/cache.ts"; +import { basicShards } from "../../../ws/shard.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildCreate( + data: DiscordPayload, + shardID: number, +) { + const payload = data.d as CreateGuildPayload; + // When shards resume they emit GUILD_CREATE again. + if (await cacheHandlers.has("guilds", payload.id)) return; + + const guildStruct = await structures.createGuildStruct( + data.d as CreateGuildPayload, + shardID, + ); + await cacheHandlers.set("guilds", guildStruct.id, guildStruct); + + const shard = basicShards.get(shardID); + + if (shard?.unavailableGuildIDs.has(payload.id)) { + await cacheHandlers.delete("unavailableGuilds", payload.id); + + shard.unavailableGuildIDs.delete(payload.id); + } + + if (!cache.isReady) return eventHandlers.guildLoaded?.(guildStruct); + eventHandlers.guildCreate?.(guildStruct); +} diff --git a/src/api/controllers/guilds/GUILD_DELETE.ts b/src/api/controllers/guilds/GUILD_DELETE.ts new file mode 100644 index 000000000..6cdcc06f4 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_DELETE.ts @@ -0,0 +1,49 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, GuildDeletePayload } from "../../../types/mod.ts"; +import { basicShards } from "../../../ws/shard.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildDelete( + data: DiscordPayload, + shardID: number, +) { + const payload = data.d as GuildDeletePayload; + cacheHandlers.forEach("messages", (message) => { + if (message.guildID === payload.id) { + cacheHandlers.delete("messages", message.id); + } + }); + + cacheHandlers.forEach("channels", (channel) => { + if (channel.guildID === payload.id) { + cacheHandlers.delete("channels", channel.id); + } + }); + + cacheHandlers.forEach("members", async (member) => { + if (!member.guilds.has(payload.id)) return; + + member.guilds.delete(payload.id); + + if (!member.guilds.size) { + await cacheHandlers.delete("members", member.id); + return; + } + + await cacheHandlers.set("members", member.id, member); + }); + + if (payload.unavailable) { + const shard = basicShards.get(shardID); + if (shard) shard.unavailableGuildIDs.add(payload.id); + + return cacheHandlers.set("unavailableGuilds", payload.id, Date.now()); + } + + const guild = await cacheHandlers.get("guilds", payload.id); + if (!guild) return; + + await cacheHandlers.delete("guilds", payload.id); + + eventHandlers.guildDelete?.(guild); +} diff --git a/src/api/controllers/guilds/GUILD_EMOJIS_UPDATE.ts b/src/api/controllers/guilds/GUILD_EMOJIS_UPDATE.ts new file mode 100644 index 000000000..111c6f7e0 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_EMOJIS_UPDATE.ts @@ -0,0 +1,26 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + GuildEmojisUpdatePayload, +} from "../../../types/mod.ts"; +import { Collection } from "../../../util/collection.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildEmojisUpdate(data: DiscordPayload) { + const payload = data.d as GuildEmojisUpdatePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const cachedEmojis = guild.emojis; + guild.emojis = new Collection( + payload.emojis.map((emoji) => [emoji.id ?? emoji.name, emoji]), + ); + + cacheHandlers.set("guilds", payload.guild_id, guild); + + eventHandlers.guildEmojisUpdate?.( + guild, + guild.emojis, + cachedEmojis, + ); +} diff --git a/src/api/controllers/GUILD_INTEGRATIONS_UPDATE.ts b/src/api/controllers/guilds/GUILD_INTEGRATIONS_UPDATE.ts similarity index 73% rename from src/api/controllers/GUILD_INTEGRATIONS_UPDATE.ts rename to src/api/controllers/guilds/GUILD_INTEGRATIONS_UPDATE.ts index 163b21018..17568a2ff 100644 --- a/src/api/controllers/GUILD_INTEGRATIONS_UPDATE.ts +++ b/src/api/controllers/guilds/GUILD_INTEGRATIONS_UPDATE.ts @@ -1,9 +1,9 @@ -import { eventHandlers } from "../../bot.ts"; +import { eventHandlers } from "../../../bot.ts"; import { DiscordGuildIntegrationsUpdateEvent, DiscordPayload, -} from "../../types/mod.ts"; -import { cacheHandlers } from "./cache.ts"; +} from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; export async function handleGuildIntegrationsUpdate( data: DiscordPayload, diff --git a/src/api/controllers/guilds/GUILD_MEMBERS_CHUNK.ts b/src/api/controllers/guilds/GUILD_MEMBERS_CHUNK.ts new file mode 100644 index 000000000..e4d985302 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_MEMBERS_CHUNK.ts @@ -0,0 +1,44 @@ +import { DiscordPayload, GuildMemberChunkPayload } from "../../../types/mod.ts"; +import { cache } from "../../../util/cache.ts"; +import { Collection } from "../../../util/collection.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildMembersChunk(data: DiscordPayload) { + const payload = data.d as GuildMemberChunkPayload; + + const members = await Promise.all( + payload.members.map(async (member) => { + const memberStruct = await structures.createMemberStruct( + member, + payload.guild_id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + return memberStruct; + }), + ); + + // Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming + if ( + payload.nonce + ) { + const resolve = cache.fetchAllMembersProcessingRequests.get(payload.nonce); + if (!resolve) return; + + if (payload.chunk_index + 1 === payload.chunk_count) { + cache.fetchAllMembersProcessingRequests.delete(payload.nonce); + // Only 1 chunk most likely is all members or users only request a small amount of users + if (payload.chunk_count === 1) { + return resolve(new Collection(members.map((m) => [m.id, m]))); + } + + return resolve( + await cacheHandlers.filter( + "members", + (m) => m.guilds.has(payload.guild_id), + ), + ); + } + } +} diff --git a/src/api/controllers/guilds/GUILD_MEMBER_ADD.ts b/src/api/controllers/guilds/GUILD_MEMBER_ADD.ts new file mode 100644 index 000000000..4d0391a2f --- /dev/null +++ b/src/api/controllers/guilds/GUILD_MEMBER_ADD.ts @@ -0,0 +1,19 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, GuildMemberAddPayload } from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildMemberAdd(data: DiscordPayload) { + const payload = data.d as GuildMemberAddPayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + guild.memberCount++; + const memberStruct = await structures.createMemberStruct( + payload, + payload.guild_id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + eventHandlers.guildMemberAdd?.(guild, memberStruct); +} diff --git a/src/api/controllers/guilds/GUILD_MEMBER_REMOVE.ts b/src/api/controllers/guilds/GUILD_MEMBER_REMOVE.ts new file mode 100644 index 000000000..c48757c47 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_MEMBER_REMOVE.ts @@ -0,0 +1,18 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, GuildBanPayload } from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildMemberRemove(data: DiscordPayload) { + const payload = data.d as GuildBanPayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + guild.memberCount--; + const member = await cacheHandlers.get("members", payload.user.id); + eventHandlers.guildMemberRemove?.(guild, payload.user, member); + + member?.guilds.delete(guild.id); + if (member && !member.guilds.size) { + await cacheHandlers.delete("members", member.id); + } +} diff --git a/src/api/controllers/guilds/GUILD_MEMBER_UPDATE.ts b/src/api/controllers/guilds/GUILD_MEMBER_UPDATE.ts new file mode 100644 index 000000000..b1d74fbb0 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_MEMBER_UPDATE.ts @@ -0,0 +1,62 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + GuildMemberUpdatePayload, +} from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildMemberUpdate(data: DiscordPayload) { + const payload = data.d as GuildMemberUpdatePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const cachedMember = await cacheHandlers.get("members", payload.user.id); + const guildMember = cachedMember?.guilds.get(payload.guild_id); + + const newMemberData = { + ...payload, + // deno-lint-ignore camelcase + premium_since: payload.premium_since || undefined, + // deno-lint-ignore camelcase + joined_at: new Date(guildMember?.joinedAt || Date.now()) + .toISOString(), + deaf: guildMember?.deaf || false, + mute: guildMember?.mute || false, + roles: payload.roles, + }; + const memberStruct = await structures.createMemberStruct( + newMemberData, + payload.guild_id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + if (guildMember?.nick !== payload.nick) { + eventHandlers.nicknameUpdate?.( + guild, + memberStruct, + payload.nick, + guildMember?.nick, + ); + } + + if (payload.pending === false && guildMember?.pending === true) { + eventHandlers.membershipScreeningPassed?.(guild, memberStruct); + } + + const roleIDs = guildMember?.roles || []; + + roleIDs.forEach((id) => { + if (!payload.roles.includes(id)) { + eventHandlers.roleLost?.(guild, memberStruct, id); + } + }); + + payload.roles.forEach((id) => { + if (!roleIDs.includes(id)) { + eventHandlers.roleGained?.(guild, memberStruct, id); + } + }); + + eventHandlers.guildMemberUpdate?.(guild, memberStruct, cachedMember); +} diff --git a/src/api/controllers/guilds/GUILD_ROLE_CREATE.ts b/src/api/controllers/guilds/GUILD_ROLE_CREATE.ts new file mode 100644 index 000000000..a75ea4bf9 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_ROLE_CREATE.ts @@ -0,0 +1,20 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + GuildRoleDeletePayload, + GuildRolePayload, +} from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildRoleCreate(data: DiscordPayload) { + const payload = data.d as GuildRolePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const role = await structures.createRoleStruct(payload.role); + guild.roles = guild.roles.set(payload.role.id, role); + await cacheHandlers.set("guilds", payload.guild_id, guild); + + eventHandlers.roleCreate?.(guild, role); +} diff --git a/src/api/controllers/guilds/GUILD_ROLE_DELETE.ts b/src/api/controllers/guilds/GUILD_ROLE_DELETE.ts new file mode 100644 index 000000000..bca2d5ba9 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_ROLE_DELETE.ts @@ -0,0 +1,28 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, GuildRoleDeletePayload } from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildRoleDelete(data: DiscordPayload) { + const payload = data.d as GuildRoleDeletePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const cachedRole = guild.roles.get(payload.role_id)!; + guild.roles.delete(payload.role_id); + + if (cachedRole) eventHandlers.roleDelete?.(guild, cachedRole); + + // For bots without GUILD_MEMBERS member.roles is never updated breaking permissions checking. + cacheHandlers.forEach("members", (member) => { + // Not in the relevant guild so just skip. + if (!member.guilds.has(guild.id)) return; + + member.guilds.forEach((g) => { + // Member does not have this role + if (!g.roles.includes(payload.role_id)) return; + // Remove this role from the members cache + g.roles = g.roles.filter((id) => id !== payload.role_id); + cacheHandlers.set("members", member.id, member); + }); + }); +} diff --git a/src/api/controllers/guilds/GUILD_ROLE_UPDATE.ts b/src/api/controllers/guilds/GUILD_ROLE_UPDATE.ts new file mode 100644 index 000000000..d341d910c --- /dev/null +++ b/src/api/controllers/guilds/GUILD_ROLE_UPDATE.ts @@ -0,0 +1,23 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + GuildRoleDeletePayload, + GuildRolePayload, +} from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildRoleUpdate(data: DiscordPayload) { + const payload = data.d as GuildRolePayload; + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const cachedRole = guild.roles.get(payload.role.id); + if (!cachedRole) return; + + const role = await structures.createRoleStruct(payload.role); + guild.roles.set(payload.role.id, role); + await cacheHandlers.set("guilds", guild.id, guild); + + eventHandlers.roleUpdate?.(guild, role, cachedRole); +} diff --git a/src/api/controllers/guilds/GUILD_UPDATE.ts b/src/api/controllers/guilds/GUILD_UPDATE.ts new file mode 100644 index 000000000..0d890e969 --- /dev/null +++ b/src/api/controllers/guilds/GUILD_UPDATE.ts @@ -0,0 +1,48 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + GuildUpdateChange, + UpdateGuildPayload, +} from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleGuildUpdate(data: DiscordPayload) { + const payload = data.d as UpdateGuildPayload; + const cachedGuild = await cacheHandlers.get("guilds", payload.id); + if (!cachedGuild) return; + + const keysToSkip = [ + "roles", + "guild_hashes", + "guild_id", + "max_members", + "emojis", + ]; + + const changes = Object.entries(payload) + .map(([key, value]) => { + if (keysToSkip.includes(key)) return; + + // @ts-ignore index signature + const cachedValue = cachedGuild[key]; + if (cachedValue !== value) { + // Guild create sends undefined and update sends false. + if (!cachedValue && !value) return; + + if (Array.isArray(cachedValue) && Array.isArray(value)) { + const different = (cachedValue.length !== value.length) || + cachedValue.find((val) => !value.includes(val)) || + value.find((val) => !cachedValue.includes(val)); + if (!different) return; + } + + // @ts-ignore index signature + cachedGuild[key] = value; + return { key, oldValue: cachedValue, value }; + } + }).filter((change) => change) as GuildUpdateChange[]; + + await cacheHandlers.set("guilds", payload.id, cachedGuild); + + eventHandlers.guildUpdate?.(cachedGuild, changes); +} diff --git a/src/api/controllers/integrations/INTEGRATION_CREATE.ts b/src/api/controllers/integrations/INTEGRATION_CREATE.ts new file mode 100644 index 000000000..a9441f266 --- /dev/null +++ b/src/api/controllers/integrations/INTEGRATION_CREATE.ts @@ -0,0 +1,31 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + IntegrationCreateUpdateEvent, +} from "../../../types/mod.ts"; + +export function handleIntegrationCreate( + data: DiscordPayload, +) { + const { + guild_id: guildID, + enable_emoticons: enableEmoticons, + expire_behavior: expireBehavior, + expire_grace_period: expireGracePeriod, + subscriber_count: subscriberCount, + role_id: roleID, + synced_at: syncedAt, + ...rest + } = data.d as IntegrationCreateUpdateEvent; + + eventHandlers.integrationCreate?.({ + ...rest, + guildID, + enableEmoticons, + expireBehavior, + expireGracePeriod, + syncedAt, + subscriberCount, + roleID, + }); +} diff --git a/src/api/controllers/integrations/INTEGRATION_DELETE.ts b/src/api/controllers/integrations/INTEGRATION_DELETE.ts new file mode 100644 index 000000000..dee2349b3 --- /dev/null +++ b/src/api/controllers/integrations/INTEGRATION_DELETE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, IntegrationDeleteEvent } from "../../../types/mod.ts"; + +export function handleIntegrationDelete(data: DiscordPayload) { + const { + guild_id: guildID, + application_id: applicationID, + ...rest + } = data.d as IntegrationDeleteEvent; + + eventHandlers.integrationDelete?.({ + ...rest, + applicationID, + guildID, + }); +} diff --git a/src/api/controllers/integrations/INTEGRATION_UPDATE.ts b/src/api/controllers/integrations/INTEGRATION_UPDATE.ts new file mode 100644 index 000000000..8976a62df --- /dev/null +++ b/src/api/controllers/integrations/INTEGRATION_UPDATE.ts @@ -0,0 +1,29 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + IntegrationCreateUpdateEvent, +} from "../../../types/mod.ts"; + +export function handleIntegrationUpdate(data: DiscordPayload) { + const { + enable_emoticons: enableEmoticons, + expire_behavior: expireBehavior, + expire_grace_period: expireGracePeriod, + role_id: roleID, + subscriber_count: subscriberCount, + synced_at: syncedAt, + guild_id: guildID, + ...rest + } = data.d as IntegrationCreateUpdateEvent; + + eventHandlers.integrationUpdate?.({ + ...rest, + guildID, + subscriberCount, + enableEmoticons, + expireGracePeriod, + roleID, + expireBehavior, + syncedAt, + }); +} diff --git a/src/api/controllers/interactions.ts b/src/api/controllers/interactions.ts deleted file mode 100644 index e5fe13de3..000000000 --- a/src/api/controllers/interactions.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - ApplicationCommandEvent, - DiscordPayload, - InteractionCommandPayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalInteractionCreate(data: DiscordPayload) { - const payload = data.d as InteractionCommandPayload; - const memberStruct = await structures.createMemberStruct( - payload.member, - payload.guild_id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - eventHandlers.interactionCreate?.( - { - ...payload, - member: memberStruct, - }, - ); -} - -export function handleInternalApplicationCommandCreate( - data: DiscordPayload, -) { - const { - guild_id: guildID, - application_id: applicationID, - ...rest - } = data.d as ApplicationCommandEvent; - - eventHandlers.applicationCommandCreate?.({ - ...rest, - guildID, - applicationID, - }); -} - -export function handleInternalApplicationCommandUpdate(data: DiscordPayload) { - const { - application_id: applicationID, - guild_id: guildID, - ...rest - } = data.d as ApplicationCommandEvent; - - eventHandlers.applicationCommandUpdate?.({ - ...rest, - guildID, - applicationID, - }); -} - -export function handleInternalApplicationCommandDelete(data: DiscordPayload) { - const { - application_id: applicationID, - guild_id: guildID, - ...rest - } = data.d as ApplicationCommandEvent; - - eventHandlers.applicationCommandDelete?.({ - ...rest, - guildID, - applicationID, - }); -} diff --git a/src/api/controllers/interactions/INTERACTION_CREATE.ts b/src/api/controllers/interactions/INTERACTION_CREATE.ts new file mode 100644 index 000000000..fd303bf3c --- /dev/null +++ b/src/api/controllers/interactions/INTERACTION_CREATE.ts @@ -0,0 +1,23 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + InteractionCommandPayload, +} from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleInteractionCreate(data: DiscordPayload) { + const payload = data.d as InteractionCommandPayload; + const memberStruct = await structures.createMemberStruct( + payload.member, + payload.guild_id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + + eventHandlers.interactionCreate?.( + { + ...payload, + member: memberStruct, + }, + ); +} diff --git a/src/api/controllers/invites/INVITE_CREATE.ts b/src/api/controllers/invites/INVITE_CREATE.ts new file mode 100644 index 000000000..5a788fff7 --- /dev/null +++ b/src/api/controllers/invites/INVITE_CREATE.ts @@ -0,0 +1,28 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, InviteCreateEvent } from "../../../types/mod.ts"; + +export function handleInviteCreate(payload: DiscordPayload) { + if (payload.t !== "INVITE_CREATE") return; + //TODO: replace with tocamelcase + const { + channel_id: channelID, + created_at: createdAt, + max_age: maxAge, + guild_id: guildID, + target_user: targetUser, + target_user_type: targetUserType, + max_uses: maxUses, + ...rest + } = payload.d as InviteCreateEvent; + + eventHandlers.inviteCreate?.({ + ...rest, + channelID, + guildID, + maxAge, + targetUser, + targetUserType, + maxUses, + createdAt, + }); +} diff --git a/src/api/controllers/invites/INVITE_DELETE.ts b/src/api/controllers/invites/INVITE_DELETE.ts new file mode 100644 index 000000000..d69a2f1d2 --- /dev/null +++ b/src/api/controllers/invites/INVITE_DELETE.ts @@ -0,0 +1,18 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, InviteDeleteEvent } from "../../../types/mod.ts"; + +export function handleInviteDelete(payload: DiscordPayload) { + if (payload.t !== "INVITE_DELETE") return; + + const { + channel_id: channelID, + guild_id: guildID, + ...rest + } = payload.d as InviteDeleteEvent; + + eventHandlers.inviteDelete?.({ + ...rest, + channelID, + guildID, + }); +} diff --git a/src/api/controllers/members.ts b/src/api/controllers/members.ts deleted file mode 100644 index 39381d39a..000000000 --- a/src/api/controllers/members.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - DiscordPayload, - GuildBanPayload, - GuildMemberAddPayload, - GuildMemberChunkPayload, - GuildMemberUpdatePayload, -} from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { Collection } from "../../util/collection.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalGuildMemberAdd(data: DiscordPayload) { - const payload = data.d as GuildMemberAddPayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - guild.memberCount++; - const memberStruct = await structures.createMemberStruct( - payload, - payload.guild_id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - eventHandlers.guildMemberAdd?.(guild, memberStruct); -} - -export async function handleInternalGuildMemberRemove(data: DiscordPayload) { - const payload = data.d as GuildBanPayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - guild.memberCount--; - const member = await cacheHandlers.get("members", payload.user.id); - eventHandlers.guildMemberRemove?.(guild, payload.user, member); - - member?.guilds.delete(guild.id); - if (member && !member.guilds.size) { - await cacheHandlers.delete("members", member.id); - } -} - -export async function handleInternalGuildMemberUpdate(data: DiscordPayload) { - const payload = data.d as GuildMemberUpdatePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const cachedMember = await cacheHandlers.get("members", payload.user.id); - const guildMember = cachedMember?.guilds.get(payload.guild_id); - - const newMemberData = { - ...payload, - // deno-lint-ignore camelcase - premium_since: payload.premium_since || undefined, - // deno-lint-ignore camelcase - joined_at: new Date(guildMember?.joinedAt || Date.now()) - .toISOString(), - deaf: guildMember?.deaf || false, - mute: guildMember?.mute || false, - roles: payload.roles, - }; - const memberStruct = await structures.createMemberStruct( - newMemberData, - payload.guild_id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - if (guildMember?.nick !== payload.nick) { - eventHandlers.nicknameUpdate?.( - guild, - memberStruct, - payload.nick, - guildMember?.nick, - ); - } - - if (payload.pending === false && guildMember?.pending === true) { - eventHandlers.membershipScreeningPassed?.(guild, memberStruct); - } - - const roleIDs = guildMember?.roles || []; - - roleIDs.forEach((id) => { - if (!payload.roles.includes(id)) { - eventHandlers.roleLost?.(guild, memberStruct, id); - } - }); - - payload.roles.forEach((id) => { - if (!roleIDs.includes(id)) { - eventHandlers.roleGained?.(guild, memberStruct, id); - } - }); - - eventHandlers.guildMemberUpdate?.(guild, memberStruct, cachedMember); -} - -export async function handleInternalGuildMembersChunk(data: DiscordPayload) { - const payload = data.d as GuildMemberChunkPayload; - - const members = await Promise.all( - payload.members.map(async (member) => { - const memberStruct = await structures.createMemberStruct( - member, - payload.guild_id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - - return memberStruct; - }), - ); - - // Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming - if ( - payload.nonce - ) { - const resolve = cache.fetchAllMembersProcessingRequests.get(payload.nonce); - if (!resolve) return; - - if (payload.chunk_index + 1 === payload.chunk_count) { - cache.fetchAllMembersProcessingRequests.delete(payload.nonce); - // Only 1 chunk most likely is all members or users only request a small amount of users - if (payload.chunk_count === 1) { - return resolve(new Collection(members.map((m) => [m.id, m]))); - } - - return resolve( - await cacheHandlers.filter( - "members", - (m) => m.guilds.has(payload.guild_id), - ), - ); - } - } -} diff --git a/src/api/controllers/messages.ts b/src/api/controllers/messages.ts deleted file mode 100644 index 1d08da409..000000000 --- a/src/api/controllers/messages.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - DiscordPayload, - MessageCreateOptions, - MessageDeleteBulkPayload, - MessageDeletePayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalMessageCreate(data: DiscordPayload) { - const payload = data.d as MessageCreateOptions; - const channel = await cacheHandlers.get("channels", payload.channel_id); - if (channel) channel.lastMessageID = payload.id; - - const guild = payload.guild_id - ? await cacheHandlers.get("guilds", payload.guild_id) - : undefined; - - if (payload.member && guild) { - // If in a guild cache the author as a member - const memberStruct = await structures.createMemberStruct( - { ...payload.member, user: payload.author }, - guild.id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - } - - await Promise.all(payload.mentions.map(async (mention) => { - // Cache the member if its a valid member - if (mention.member && guild) { - const memberStruct = await structures.createMemberStruct( - { ...mention.member, user: mention }, - guild.id, - ); - - return cacheHandlers.set("members", memberStruct.id, memberStruct); - } - })); - - const message = await structures.createMessageStruct(payload); - // Cache the message - await cacheHandlers.set("messages", payload.id, message); - - eventHandlers.messageCreate?.(message); -} - -export async function handleInternalMessageDelete(data: DiscordPayload) { - const payload = data.d as MessageDeletePayload; - const channel = await cacheHandlers.get("channels", payload.channel_id); - if (!channel) return; - - eventHandlers.messageDelete?.( - { id: payload.id, channel }, - await cacheHandlers.get("messages", payload.id), - ); - - await cacheHandlers.delete("messages", payload.id); -} - -export async function handleInternalMessageDeleteBulk(data: DiscordPayload) { - const payload = data.d as MessageDeleteBulkPayload; - const channel = await cacheHandlers.get("channels", payload.channel_id); - if (!channel) return; - - return Promise.all(payload.ids.map(async (id) => { - eventHandlers.messageDelete?.( - { id, channel }, - await cacheHandlers.get("messages", id), - ); - await cacheHandlers.delete("messages", id); - })); -} - -export async function handleInternalMessageUpdate(data: DiscordPayload) { - const payload = data.d as MessageCreateOptions; - const channel = await cacheHandlers.get("channels", payload.channel_id); - if (!channel) return; - - const cachedMessage = await cacheHandlers.get("messages", payload.id); - if (!cachedMessage) return; - - const oldMessage = { - attachments: cachedMessage.attachments, - content: cachedMessage.content, - embeds: cachedMessage.embeds, - editedTimestamp: cachedMessage.editedTimestamp, - tts: cachedMessage.tts, - pinned: cachedMessage.pinned, - }; - - // Messages with embeds can trigger update but they wont have edited_timestamp - if ( - !payload.edited_timestamp || - (cachedMessage.content === payload.content) - ) { - return; - } - - const message = await structures.createMessageStruct(payload); - - await cacheHandlers.set("messages", payload.id, message); - - eventHandlers.messageUpdate?.(message, oldMessage); -} diff --git a/src/api/controllers/messages/MESSAGE_CREATE.ts b/src/api/controllers/messages/MESSAGE_CREATE.ts new file mode 100644 index 000000000..2e2523a8e --- /dev/null +++ b/src/api/controllers/messages/MESSAGE_CREATE.ts @@ -0,0 +1,41 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, MessageCreateOptions } from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleMessageCreate(data: DiscordPayload) { + const payload = data.d as MessageCreateOptions; + const channel = await cacheHandlers.get("channels", payload.channel_id); + if (channel) channel.lastMessageID = payload.id; + + const guild = payload.guild_id + ? await cacheHandlers.get("guilds", payload.guild_id) + : undefined; + + if (payload.member && guild) { + // If in a guild cache the author as a member + const memberStruct = await structures.createMemberStruct( + { ...payload.member, user: payload.author }, + guild.id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + } + + await Promise.all(payload.mentions.map(async (mention) => { + // Cache the member if its a valid member + if (mention.member && guild) { + const memberStruct = await structures.createMemberStruct( + { ...mention.member, user: mention }, + guild.id, + ); + + return cacheHandlers.set("members", memberStruct.id, memberStruct); + } + })); + + const message = await structures.createMessageStruct(payload); + // Cache the message + await cacheHandlers.set("messages", payload.id, message); + + eventHandlers.messageCreate?.(message); +} diff --git a/src/api/controllers/messages/MESSAGE_DELETE.ts b/src/api/controllers/messages/MESSAGE_DELETE.ts new file mode 100644 index 000000000..9e7a67ee8 --- /dev/null +++ b/src/api/controllers/messages/MESSAGE_DELETE.ts @@ -0,0 +1,16 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, MessageDeletePayload } from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleMessageDelete(data: DiscordPayload) { + const payload = data.d as MessageDeletePayload; + const channel = await cacheHandlers.get("channels", payload.channel_id); + if (!channel) return; + + eventHandlers.messageDelete?.( + { id: payload.id, channel }, + await cacheHandlers.get("messages", payload.id), + ); + + await cacheHandlers.delete("messages", payload.id); +} diff --git a/src/api/controllers/messages/MESSAGE_DELETE_BULK.ts b/src/api/controllers/messages/MESSAGE_DELETE_BULK.ts new file mode 100644 index 000000000..0173cee53 --- /dev/null +++ b/src/api/controllers/messages/MESSAGE_DELETE_BULK.ts @@ -0,0 +1,20 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + MessageDeleteBulkPayload, +} from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleMessageDeleteBulk(data: DiscordPayload) { + const payload = data.d as MessageDeleteBulkPayload; + const channel = await cacheHandlers.get("channels", payload.channel_id); + if (!channel) return; + + return Promise.all(payload.ids.map(async (id) => { + eventHandlers.messageDelete?.( + { id, channel }, + await cacheHandlers.get("messages", id), + ); + await cacheHandlers.delete("messages", id); + })); +} diff --git a/src/api/controllers/messages/MESSAGE_REACTION_ADD.ts b/src/api/controllers/messages/MESSAGE_REACTION_ADD.ts new file mode 100644 index 000000000..b4718cd99 --- /dev/null +++ b/src/api/controllers/messages/MESSAGE_REACTION_ADD.ts @@ -0,0 +1,56 @@ +import { botID, eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, MessageReactionPayload } from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleMessageReactionAdd(data: DiscordPayload) { + const payload = data.d as MessageReactionPayload; + const message = await cacheHandlers.get("messages", payload.message_id); + + if (message) { + const reactionExisted = message.reactions?.find( + (reaction) => + reaction.emoji.id === payload.emoji.id && + reaction.emoji.name === payload.emoji.name, + ); + + if (reactionExisted) reactionExisted.count++; + else { + const newReaction = { + count: 1, + me: payload.user_id === botID, + emoji: { ...payload.emoji, id: payload.emoji.id || undefined }, + }; + message.reactions = message.reactions + ? [...message.reactions, newReaction] + : [newReaction]; + } + + await cacheHandlers.set("messages", payload.message_id, message); + } + + if (payload.member && payload.guild_id) { + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (guild) { + const memberStruct = await structures.createMemberStruct( + payload.member, + guild.id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + } + } + + const uncachedOptions = { + ...payload, + id: payload.message_id, + channelID: payload.channel_id, + guildID: payload.guild_id || "", + }; + + eventHandlers.reactionAdd?.( + uncachedOptions, + payload.emoji, + payload.user_id, + message, + ); +} diff --git a/src/api/controllers/messages/MESSAGE_REACTION_REMOVE.ts b/src/api/controllers/messages/MESSAGE_REACTION_REMOVE.ts new file mode 100644 index 000000000..877b6df4f --- /dev/null +++ b/src/api/controllers/messages/MESSAGE_REACTION_REMOVE.ts @@ -0,0 +1,58 @@ +import { botID, eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, MessageReactionPayload } from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleMessageReactionRemove( + data: DiscordPayload, +) { + const payload = data.d as MessageReactionPayload; + const message = await cacheHandlers.get("messages", payload.message_id); + + if (message) { + const reactionExisted = message.reactions?.find( + (reaction) => + reaction.emoji.id === payload.emoji.id && + reaction.emoji.name === payload.emoji.name, + ); + + if (reactionExisted) reactionExisted.count--; + else { + const newReaction = { + count: 1, + me: payload.user_id === botID, + emoji: { ...payload.emoji, id: payload.emoji.id || undefined }, + }; + message.reactions = message.reactions + ? [...message.reactions, newReaction] + : [newReaction]; + } + + await cacheHandlers.set("messages", payload.message_id, message); + } + + if (payload.member && payload.guild_id) { + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (guild) { + const memberStruct = await structures.createMemberStruct( + payload.member, + guild.id, + ); + await cacheHandlers.set("members", memberStruct.id, memberStruct); + } + } + + const uncachedOptions = { + ...payload, + id: payload.message_id, + channelID: payload.channel_id, + guildID: payload.guild_id, + }; + + eventHandlers.reactionRemove?.( + uncachedOptions, + payload.emoji, + payload.user_id, + message, + ); +} diff --git a/src/api/controllers/messages/MESSAGE_REACTION_REMOVE_ALL.ts b/src/api/controllers/messages/MESSAGE_REACTION_REMOVE_ALL.ts new file mode 100644 index 000000000..419c6a777 --- /dev/null +++ b/src/api/controllers/messages/MESSAGE_REACTION_REMOVE_ALL.ts @@ -0,0 +1,21 @@ +import { eventHandlers } from "../../../bot.ts"; +import { + BaseMessageReactionPayload, + DiscordPayload, +} from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleMessageReactionRemoveAll( + data: DiscordPayload, +) { + const payload = data.d as BaseMessageReactionPayload; + const message = await cacheHandlers.get("messages", payload.message_id); + + if (message?.reactions) { + message.reactions = undefined; + + await cacheHandlers.set("messages", payload.message_id, message); + } + + eventHandlers.reactionRemoveAll?.(data.d as BaseMessageReactionPayload); +} diff --git a/src/api/controllers/messages/MESSAGE_REACTION_REMOVE_EMOJI.ts b/src/api/controllers/messages/MESSAGE_REACTION_REMOVE_EMOJI.ts new file mode 100644 index 000000000..165f6124d --- /dev/null +++ b/src/api/controllers/messages/MESSAGE_REACTION_REMOVE_EMOJI.ts @@ -0,0 +1,29 @@ +import { botID, eventHandlers } from "../../../bot.ts"; +import { + DiscordPayload, + MessageReactionRemoveEmojiPayload, +} from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleMessageReactionRemoveEmoji( + data: DiscordPayload, +) { + const payload = data.d as MessageReactionRemoveEmojiPayload; + const message = await cacheHandlers.get("messages", payload.message_id); + + if (message?.reactions) { + message.reactions = message.reactions?.filter( + (reaction) => + !( + reaction.emoji.id === payload.emoji.id && + reaction.emoji.name === payload.emoji.name + ), + ); + + await cacheHandlers.set("messages", payload.message_id, message); + } + + eventHandlers.reactionRemoveEmoji?.( + data.d as MessageReactionRemoveEmojiPayload, + ); +} diff --git a/src/api/controllers/messages/MESSAGE_UPDATE.ts b/src/api/controllers/messages/MESSAGE_UPDATE.ts new file mode 100644 index 000000000..69815874f --- /dev/null +++ b/src/api/controllers/messages/MESSAGE_UPDATE.ts @@ -0,0 +1,36 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, MessageCreateOptions } from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleMessageUpdate(data: DiscordPayload) { + const payload = data.d as MessageCreateOptions; + const channel = await cacheHandlers.get("channels", payload.channel_id); + if (!channel) return; + + const cachedMessage = await cacheHandlers.get("messages", payload.id); + if (!cachedMessage) return; + + const oldMessage = { + attachments: cachedMessage.attachments, + content: cachedMessage.content, + embeds: cachedMessage.embeds, + editedTimestamp: cachedMessage.editedTimestamp, + tts: cachedMessage.tts, + pinned: cachedMessage.pinned, + }; + + // Messages with embeds can trigger update but they wont have edited_timestamp + if ( + !payload.edited_timestamp || + (cachedMessage.content === payload.content) + ) { + return; + } + + const message = await structures.createMessageStruct(payload); + + await cacheHandlers.set("messages", payload.id, message); + + eventHandlers.messageUpdate?.(message, oldMessage); +} diff --git a/src/api/controllers/misc.ts b/src/api/controllers/misc.ts deleted file mode 100644 index a5c34d293..000000000 --- a/src/api/controllers/misc.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - DiscordPayload, - IntegrationCreateUpdateEvent, - IntegrationDeleteEvent, - InviteCreateEvent, - InviteDeleteEvent, - PresenceUpdatePayload, - TypingStartPayload, - UserPayload, - VoiceStateUpdatePayload, - WebhookUpdatePayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -/** This function is the internal handler for the presence update event. Users can override this with controllers if desired. */ -export async function handleInternalPresenceUpdate(data: DiscordPayload) { - const payload = data.d as PresenceUpdatePayload; - const oldPresence = await cacheHandlers.get("presences", payload.user.id); - await cacheHandlers.set("presences", payload.user.id, payload); - - eventHandlers.presenceUpdate?.(payload, oldPresence); -} - -/** This function is the internal handler for the typings event. Users can override this with controllers if desired. */ -export function handleInternalTypingStart(data: DiscordPayload) { - eventHandlers.typingStart?.(data.d as TypingStartPayload); -} - -/** This function is the internal handler for the user update event. Users can override this with controllers if desired. */ -export async function handleInternalUserUpdate(data: DiscordPayload) { - const userData = data.d as UserPayload; - - const member = await cacheHandlers.get("members", userData.id); - if (!member) return; - - Object.entries(userData).forEach(([key, value]) => { - // @ts-ignore index signatures - if (member[key] !== value) return member[key] = value; - }); - - await cacheHandlers.set("members", userData.id, member); - - eventHandlers.botUpdate?.(userData); -} - -/** This function is the internal handler for the voice state update event. Users can override this with controllers if desired. */ -export async function handleInternalVoiceStateUpdate(data: DiscordPayload) { - const payload = data.d as VoiceStateUpdatePayload; - if (!payload.guild_id) return; - - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const member = payload.member - ? await structures.createMemberStruct(payload.member, guild.id) - : await cacheHandlers.get("members", payload.user_id); - if (!member) return; - - // No cached state before so lets make one for em - const cachedState = guild.voiceStates.get(payload.user_id); - - guild.voiceStates.set(payload.user_id, { - ...payload, - guildID: payload.guild_id, - channelID: payload.channel_id || "", - userID: payload.user_id, - sessionID: payload.session_id, - selfDeaf: payload.self_deaf, - selfMute: payload.self_mute, - selfStream: payload.self_stream || false, - }); - - await cacheHandlers.set("guilds", payload.guild_id, guild); - - if (cachedState?.channelID !== payload.channel_id) { - // Either joined or moved channels - if (payload.channel_id) { - if (cachedState?.channelID) { // Was in a channel before - eventHandlers.voiceChannelSwitch?.( - member, - payload.channel_id, - cachedState.channelID, - ); - } else { // Was not in a channel before so user just joined - eventHandlers.voiceChannelJoin?.(member, payload.channel_id); - } - } // Left the channel - else if (cachedState?.channelID) { - guild.voiceStates.delete(payload.user_id); - eventHandlers.voiceChannelLeave?.(member, cachedState.channelID); - } - } - - eventHandlers.voiceStateUpdate?.(member, payload); -} - -/** This function is the internal handler for the webhooks update event. Users can override this with controllers if desired. */ -export function handleInternalWebhooksUpdate(data: DiscordPayload) { - const options = data.d as WebhookUpdatePayload; - eventHandlers.webhooksUpdate?.( - options.channel_id, - options.guild_id, - ); -} - -export function handleInternalIntegrationCreate( - data: DiscordPayload, -) { - const { - guild_id: guildID, - enable_emoticons: enableEmoticons, - expire_behavior: expireBehavior, - expire_grace_period: expireGracePeriod, - subscriber_count: subscriberCount, - role_id: roleID, - synced_at: syncedAt, - ...rest - } = data.d as IntegrationCreateUpdateEvent; - - eventHandlers.integrationCreate?.({ - ...rest, - guildID, - enableEmoticons, - expireBehavior, - expireGracePeriod, - syncedAt, - subscriberCount, - roleID, - }); -} - -export function handleInternalIntegrationUpdate(data: DiscordPayload) { - const { - enable_emoticons: enableEmoticons, - expire_behavior: expireBehavior, - expire_grace_period: expireGracePeriod, - role_id: roleID, - subscriber_count: subscriberCount, - synced_at: syncedAt, - guild_id: guildID, - ...rest - } = data.d as IntegrationCreateUpdateEvent; - - eventHandlers.integrationUpdate?.({ - ...rest, - guildID, - subscriberCount, - enableEmoticons, - expireGracePeriod, - roleID, - expireBehavior, - syncedAt, - }); -} - -export function handleInternalIntegrationDelete(data: DiscordPayload) { - const { - guild_id: guildID, - application_id: applicationID, - ...rest - } = data.d as IntegrationDeleteEvent; - - eventHandlers.integrationDelete?.({ - ...rest, - applicationID, - guildID, - }); -} - -export function handleInternalInviteCreate(payload: DiscordPayload) { - if (payload.t !== "INVITE_CREATE") return; - //TODO: replace with tocamelcase - const { - channel_id: channelID, - created_at: createdAt, - max_age: maxAge, - guild_id: guildID, - target_user: targetUser, - target_user_type: targetUserType, - max_uses: maxUses, - ...rest - } = payload.d as InviteCreateEvent; - - eventHandlers.inviteCreate?.({ - ...rest, - channelID, - guildID, - maxAge, - targetUser, - targetUserType, - maxUses, - createdAt, - }); -} - -export function handleInternalInviteDelete(payload: DiscordPayload) { - if (payload.t !== "INVITE_DELETE") return; - - const { - channel_id: channelID, - guild_id: guildID, - ...rest - } = payload.d as InviteDeleteEvent; - - eventHandlers.inviteDelete?.({ - ...rest, - channelID, - guildID, - }); -} diff --git a/src/api/controllers/mod.ts b/src/api/controllers/mod.ts index f306356b7..239888d42 100644 --- a/src/api/controllers/mod.ts +++ b/src/api/controllers/mod.ts @@ -1,107 +1,142 @@ -import { - handleInternalGuildBanAdd, - handleInternalGuildBanRemove, -} from "./bans.ts"; -import { - handleInternalChannelCreate, - handleInternalChannelDelete, - handleInternalChannelUpdate, -} from "./channels.ts"; -import { handleChannelPinsUpdate } from "./CHANNEL_PINS_UPDATE.ts"; -import { - handleInternalGuildCreate, - handleInternalGuildDelete, - handleInternalGuildEmojisUpdate, - handleInternalGuildUpdate, -} from "./guilds.ts"; -import { handleGuildIntegrationsUpdate } from "./GUILD_INTEGRATIONS_UPDATE.ts"; -import { - handleInternalApplicationCommandCreate, - handleInternalApplicationCommandDelete, - handleInternalApplicationCommandUpdate, - handleInternalInteractionCreate, -} from "./interactions.ts"; -import { - handleInternalGuildMemberAdd, - handleInternalGuildMemberRemove, - handleInternalGuildMembersChunk, - handleInternalGuildMemberUpdate, -} from "./members.ts"; -import { - handleInternalMessageCreate, - handleInternalMessageDelete, - handleInternalMessageDeleteBulk, - handleInternalMessageUpdate, -} from "./messages.ts"; -import { - handleInternalIntegrationCreate, - handleInternalIntegrationDelete, - handleInternalIntegrationUpdate, - handleInternalInviteCreate, - handleInternalInviteDelete, - handleInternalPresenceUpdate, - handleInternalTypingStart, - handleInternalUserUpdate, - handleInternalVoiceStateUpdate, - handleInternalWebhooksUpdate, -} from "./misc.ts"; -import { - handleInternalMessageReactionAdd, - handleInternalMessageReactionRemove, - handleInternalMessageReactionRemoveAll, - handleInternalMessageReactionRemoveEmoji, -} from "./reactions.ts"; -import { handleInternalReady } from "./READY.ts"; -import { - handleInternalGuildRoleCreate, - handleInternalGuildRoleDelete, - handleInternalGuildRoleUpdate, -} from "./roles.ts"; -import { handleVoiceServerUpdate } from "./VOICE_SERVER_UPDATE.ts"; +import { handleChannelCreate } from "./channels/CHANNEL_CREATE.ts"; +import { handleChannelDelete } from "./channels/CHANNEL_DELETE.ts"; +import { handleChannelPinsUpdate } from "./channels/CHANNEL_PINS_UPDATE.ts"; +import { handleChannelUpdate } from "./channels/CHANNEL_UPDATE.ts"; +import { handleApplicationCommandCreate } from "./commands/APPLICATION_COMMAND_CREATE.ts"; +import { handleApplicationCommandDelete } from "./commands/APPLICATION_COMMAND_DELETE.ts"; +import { handleApplicationCommandUpdate } from "./commands/APPLICATION_COMMAND_UPDATE.ts"; +import { handleGuildBanAdd } from "./guilds/GUILD_BAN_ADD.ts"; +import { handleGuildBanRemove } from "./guilds/GUILD_BAN_REMOVE.ts"; +import { handleGuildCreate } from "./guilds/GUILD_CREATE.ts"; +import { handleGuildDelete } from "./guilds/GUILD_DELETE.ts"; +import { handleGuildEmojisUpdate } from "./guilds/GUILD_EMOJIS_UPDATE.ts"; +import { handleGuildIntegrationsUpdate } from "./guilds/GUILD_INTEGRATIONS_UPDATE.ts"; +import { handleGuildMembersChunk } from "./guilds/GUILD_MEMBERS_CHUNK.ts"; +import { handleGuildMemberAdd } from "./guilds/GUILD_MEMBER_ADD.ts"; +import { handleGuildMemberRemove } from "./guilds/GUILD_MEMBER_REMOVE.ts"; +import { handleGuildMemberUpdate } from "./guilds/GUILD_MEMBER_UPDATE.ts"; +import { handleGuildRoleCreate } from "./guilds/GUILD_ROLE_CREATE.ts"; +import { handleGuildRoleDelete } from "./guilds/GUILD_ROLE_DELETE.ts"; +import { handleGuildRoleUpdate } from "./guilds/GUILD_ROLE_UPDATE.ts"; +import { handleGuildUpdate } from "./guilds/GUILD_UPDATE.ts"; +import { handleIntegrationCreate } from "./integrations/INTEGRATION_CREATE.ts"; +import { handleIntegrationDelete } from "./integrations/INTEGRATION_DELETE.ts"; +import { handleIntegrationUpdate } from "./integrations/INTEGRATION_UPDATE.ts"; +import { handleInteractionCreate } from "./interactions/INTERACTION_CREATE.ts"; +import { handleInviteCreate } from "./invites/INVITE_CREATE.ts"; +import { handleMessageCreate } from "./messages/MESSAGE_CREATE.ts"; +import { handleMessageDelete } from "./messages/MESSAGE_DELETE.ts"; +import { handleMessageDeleteBulk } from "./messages/MESSAGE_DELETE_BULK.ts"; +import { handleMessageReactionAdd } from "./messages/MESSAGE_REACTION_ADD.ts"; +import { handleMessageReactionRemove } from "./messages/MESSAGE_REACTION_REMOVE.ts"; +import { handleMessageReactionRemoveAll } from "./messages/MESSAGE_REACTION_REMOVE_ALL.ts"; +import { handleMessageReactionRemoveEmoji } from "./messages/MESSAGE_REACTION_REMOVE_EMOJI.ts"; +import { handleMessageUpdate } from "./messages/MESSAGE_UPDATE.ts"; +import { handlePresenceUpdate } from "./presence/PRESENCE_UPDATE.ts"; +import { handleTypingStart } from "./presence/TYPING_START.ts"; +import { handleUserUpdate } from "./presence/USER_UPDATE.ts"; +import { handleReady } from "./READY.ts"; +import { handleVoiceServerUpdate } from "./voice/VOICE_SERVER_UPDATE.ts"; +import { handleVoiceStateUpdate } from "./voice/VOICE_STATE_UPDATE.ts"; +import { handleWebhooksUpdate } from "./webhooks/WEBHOOKS_UPDATE.ts"; + +export { + handleApplicationCommandCreate, + handleApplicationCommandDelete, + handleApplicationCommandUpdate, + handleChannelCreate, + handleChannelDelete, + handleChannelPinsUpdate, + handleChannelUpdate, + handleGuildBanAdd, + handleGuildBanRemove, + handleGuildCreate, + handleGuildDelete, + handleGuildEmojisUpdate, + handleGuildIntegrationsUpdate, + handleGuildMemberAdd, + handleGuildMemberRemove, + handleGuildMembersChunk, + handleGuildMemberUpdate, + handleGuildRoleCreate, + handleGuildRoleDelete, + handleGuildRoleUpdate, + handleGuildUpdate, + handleIntegrationCreate, + handleIntegrationDelete, + handleIntegrationUpdate, + handleInteractionCreate, + handleInviteCreate, + handleMessageCreate, + handleMessageDelete, + handleMessageDeleteBulk, + handleMessageReactionAdd, + handleMessageReactionRemove, + handleMessageReactionRemoveAll, + handleMessageReactionRemoveEmoji, + handleMessageUpdate, + handlePresenceUpdate, + handleReady, + handleTypingStart, + handleUserUpdate, + handleVoiceServerUpdate, + handleVoiceStateUpdate, + handleWebhooksUpdate, +}; export let controllers = { - READY: handleInternalReady, - CHANNEL_CREATE: handleInternalChannelCreate, - CHANNEL_DELETE: handleInternalChannelDelete, - CHANNEL_UPDATE: handleInternalChannelUpdate, + READY: handleReady, + // channels + CHANNEL_CREATE: handleChannelCreate, + CHANNEL_DELETE: handleChannelDelete, CHANNEL_PINS_UPDATE: handleChannelPinsUpdate, - GUILD_CREATE: handleInternalGuildCreate, - GUILD_DELETE: handleInternalGuildDelete, - GUILD_UPDATE: handleInternalGuildUpdate, - GUILD_BAN_ADD: handleInternalGuildBanAdd, - GUILD_BAN_REMOVE: handleInternalGuildBanRemove, - GUILD_EMOJIS_UPDATE: handleInternalGuildEmojisUpdate, + CHANNEL_UPDATE: handleChannelUpdate, + // commands + APPLICATION_COMMAND_CREATE: handleApplicationCommandCreate, + APPLICATION_COMMAND_DELETE: handleApplicationCommandDelete, + APPLICATION_COMMAND_UPDATE: handleApplicationCommandUpdate, + // guilds + GUILD_BAN_ADD: handleGuildBanAdd, + GUILD_BAN_REMOVE: handleGuildBanRemove, + GUILD_CREATE: handleGuildCreate, + GUILD_DELETE: handleGuildDelete, + GUILD_EMOJIS_UPDATE: handleGuildEmojisUpdate, GUILD_INTEGRATIONS_UPDATE: handleGuildIntegrationsUpdate, - GUILD_MEMBER_ADD: handleInternalGuildMemberAdd, - GUILD_MEMBER_REMOVE: handleInternalGuildMemberRemove, - GUILD_MEMBER_UPDATE: handleInternalGuildMemberUpdate, - GUILD_MEMBERS_CHUNK: handleInternalGuildMembersChunk, - GUILD_ROLE_CREATE: handleInternalGuildRoleCreate, - GUILD_ROLE_DELETE: handleInternalGuildRoleDelete, - GUILD_ROLE_UPDATE: handleInternalGuildRoleUpdate, - INTERACTION_CREATE: handleInternalInteractionCreate, - APPLICATION_COMMAND_CREATE: handleInternalApplicationCommandCreate, - APPLICATION_COMMAND_DELETE: handleInternalApplicationCommandDelete, - APPLICATION_COMMAND_UPDATE: handleInternalApplicationCommandUpdate, - MESSAGE_CREATE: handleInternalMessageCreate, - MESSAGE_DELETE: handleInternalMessageDelete, - MESSAGE_DELETE_BULK: handleInternalMessageDeleteBulk, - MESSAGE_UPDATE: handleInternalMessageUpdate, - MESSAGE_REACTION_ADD: handleInternalMessageReactionAdd, - MESSAGE_REACTION_REMOVE: handleInternalMessageReactionRemove, - MESSAGE_REACTION_REMOVE_ALL: handleInternalMessageReactionRemoveAll, - MESSAGE_REACTION_REMOVE_EMOJI: handleInternalMessageReactionRemoveEmoji, - PRESENCE_UPDATE: handleInternalPresenceUpdate, - TYPING_START: handleInternalTypingStart, - USER_UPDATE: handleInternalUserUpdate, - VOICE_STATE_UPDATE: handleInternalVoiceStateUpdate, + GUILD_MEMBER_ADD: handleGuildMemberAdd, + GUILD_MEMBER_REMOVE: handleGuildMemberRemove, + GUILD_MEMBER_UPDATE: handleGuildMemberUpdate, + GUILD_MEMBERS_CHUNK: handleGuildMembersChunk, + GUILD_ROLE_CREATE: handleGuildRoleCreate, + GUILD_ROLE_DELETE: handleGuildRoleDelete, + GUILD_ROLE_UPDATE: handleGuildRoleUpdate, + GUILD_UPDATE: handleGuildUpdate, + // interactions + INTERACTION_CREATE: handleInteractionCreate, + // invites + INVITE_CREATE: handleInviteCreate, + INVITE_DELETE: handleInviteCreate, + // messages + MESSAGE_CREATE: handleMessageCreate, + MESSAGE_DELETE_BULK: handleMessageDeleteBulk, + MESSAGE_DELETE: handleMessageDelete, + MESSAGE_REACTION_ADD: handleMessageReactionAdd, + MESSAGE_REACTION_REMOVE_ALL: handleMessageReactionRemoveAll, + MESSAGE_REACTION_REMOVE_EMOJI: handleMessageReactionRemoveEmoji, + MESSAGE_REACTION_REMOVE: handleMessageReactionRemove, + MESSAGE_UPDATE: handleMessageUpdate, + // presence + PRESENCE_UPDATE: handlePresenceUpdate, + TYPING_START: handleTypingStart, + USER_UPDATE: handleUserUpdate, + // voice VOICE_SERVER_UPDATE: handleVoiceServerUpdate, - WEBHOOKS_UPDATE: handleInternalWebhooksUpdate, - INTEGRATION_CREATE: handleInternalIntegrationCreate, - INTEGRATION_UPDATE: handleInternalIntegrationUpdate, - INTEGRATION_DELETE: handleInternalIntegrationDelete, - INVITE_CREATE: handleInternalInviteCreate, - INVITE_DELETE: handleInternalInviteDelete, + VOICE_STATE_UPDATE: handleVoiceStateUpdate, + // webhooks + WEBHOOKS_UPDATE: handleWebhooksUpdate, + // integrations + INTEGRATION_CREATE: handleIntegrationCreate, + INTEGRATION_UPDATE: handleIntegrationUpdate, + INTEGRATION_DELETE: handleIntegrationDelete, }; export type Controllers = typeof controllers; diff --git a/src/api/controllers/presence/PRESENCE_UPDATE.ts b/src/api/controllers/presence/PRESENCE_UPDATE.ts new file mode 100644 index 000000000..4c1920c62 --- /dev/null +++ b/src/api/controllers/presence/PRESENCE_UPDATE.ts @@ -0,0 +1,11 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, PresenceUpdatePayload } from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handlePresenceUpdate(data: DiscordPayload) { + const payload = data.d as PresenceUpdatePayload; + const oldPresence = await cacheHandlers.get("presences", payload.user.id); + await cacheHandlers.set("presences", payload.user.id, payload); + + eventHandlers.presenceUpdate?.(payload, oldPresence); +} diff --git a/src/api/controllers/presence/TYPING_START.ts b/src/api/controllers/presence/TYPING_START.ts new file mode 100644 index 000000000..f221c6fe4 --- /dev/null +++ b/src/api/controllers/presence/TYPING_START.ts @@ -0,0 +1,6 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, TypingStartPayload } from "../../../types/mod.ts"; + +export function handleTypingStart(data: DiscordPayload) { + eventHandlers.typingStart?.(data.d as TypingStartPayload); +} diff --git a/src/api/controllers/presence/USER_UPDATE.ts b/src/api/controllers/presence/USER_UPDATE.ts new file mode 100644 index 000000000..e1d20ddb8 --- /dev/null +++ b/src/api/controllers/presence/USER_UPDATE.ts @@ -0,0 +1,19 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, UserPayload } from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleUserUpdate(data: DiscordPayload) { + const userData = data.d as UserPayload; + + const member = await cacheHandlers.get("members", userData.id); + if (!member) return; + + Object.entries(userData).forEach(([key, value]) => { + // @ts-ignore index signatures + if (member[key] !== value) return member[key] = value; + }); + + await cacheHandlers.set("members", userData.id, member); + + eventHandlers.botUpdate?.(userData); +} diff --git a/src/api/controllers/reactions.ts b/src/api/controllers/reactions.ts deleted file mode 100644 index 623c7fa35..000000000 --- a/src/api/controllers/reactions.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { botID, eventHandlers } from "../../bot.ts"; -import { - BaseMessageReactionPayload, - DiscordPayload, - MessageReactionPayload, - MessageReactionRemoveEmojiPayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalMessageReactionAdd(data: DiscordPayload) { - const payload = data.d as MessageReactionPayload; - const message = await cacheHandlers.get("messages", payload.message_id); - - if (message) { - const reactionExisted = message.reactions?.find( - (reaction) => - reaction.emoji.id === payload.emoji.id && - reaction.emoji.name === payload.emoji.name, - ); - - if (reactionExisted) reactionExisted.count++; - else { - const newReaction = { - count: 1, - me: payload.user_id === botID, - emoji: { ...payload.emoji, id: payload.emoji.id || undefined }, - }; - message.reactions = message.reactions - ? [...message.reactions, newReaction] - : [newReaction]; - } - - await cacheHandlers.set("messages", payload.message_id, message); - } - - if (payload.member && payload.guild_id) { - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (guild) { - const memberStruct = await structures.createMemberStruct( - payload.member, - guild.id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - } - } - - const uncachedOptions = { - ...payload, - id: payload.message_id, - channelID: payload.channel_id, - guildID: payload.guild_id || "", - }; - - eventHandlers.reactionAdd?.( - uncachedOptions, - payload.emoji, - payload.user_id, - message, - ); -} - -export async function handleInternalMessageReactionRemove( - data: DiscordPayload, -) { - const payload = data.d as MessageReactionPayload; - const message = await cacheHandlers.get("messages", payload.message_id); - - if (message) { - const reactionExisted = message.reactions?.find( - (reaction) => - reaction.emoji.id === payload.emoji.id && - reaction.emoji.name === payload.emoji.name, - ); - - if (reactionExisted) reactionExisted.count--; - else { - const newReaction = { - count: 1, - me: payload.user_id === botID, - emoji: { ...payload.emoji, id: payload.emoji.id || undefined }, - }; - message.reactions = message.reactions - ? [...message.reactions, newReaction] - : [newReaction]; - } - - await cacheHandlers.set("messages", payload.message_id, message); - } - - if (payload.member && payload.guild_id) { - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (guild) { - const memberStruct = await structures.createMemberStruct( - payload.member, - guild.id, - ); - await cacheHandlers.set("members", memberStruct.id, memberStruct); - } - } - - const uncachedOptions = { - ...payload, - id: payload.message_id, - channelID: payload.channel_id, - guildID: payload.guild_id, - }; - - eventHandlers.reactionRemove?.( - uncachedOptions, - payload.emoji, - payload.user_id, - message, - ); -} - -export async function handleInternalMessageReactionRemoveAll( - data: DiscordPayload, -) { - const payload = data.d as BaseMessageReactionPayload; - const message = await cacheHandlers.get("messages", payload.message_id); - - if (message?.reactions) { - message.reactions = undefined; - - await cacheHandlers.set("messages", payload.message_id, message); - } - - eventHandlers.reactionRemoveAll?.(data.d as BaseMessageReactionPayload); -} - -export async function handleInternalMessageReactionRemoveEmoji( - data: DiscordPayload, -) { - const payload = data.d as MessageReactionRemoveEmojiPayload; - const message = await cacheHandlers.get("messages", payload.message_id); - - if (message?.reactions) { - message.reactions = message.reactions?.filter( - (reaction) => - !( - reaction.emoji.id === payload.emoji.id && - reaction.emoji.name === payload.emoji.name - ), - ); - - await cacheHandlers.set("messages", payload.message_id, message); - } - - eventHandlers.reactionRemoveEmoji?.( - data.d as MessageReactionRemoveEmojiPayload, - ); -} diff --git a/src/api/controllers/roles.ts b/src/api/controllers/roles.ts deleted file mode 100644 index d1dfb65c3..000000000 --- a/src/api/controllers/roles.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { eventHandlers } from "../../bot.ts"; -import { - DiscordPayload, - GuildRoleDeletePayload, - GuildRolePayload, -} from "../../types/mod.ts"; -import { structures } from "../structures/mod.ts"; -import { cacheHandlers } from "./cache.ts"; - -export async function handleInternalGuildRoleCreate(data: DiscordPayload) { - const payload = data.d as GuildRolePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const role = await structures.createRoleStruct(payload.role); - guild.roles = guild.roles.set(payload.role.id, role); - await cacheHandlers.set("guilds", payload.guild_id, guild); - - eventHandlers.roleCreate?.(guild, role); -} - -export async function handleInternalGuildRoleDelete(data: DiscordPayload) { - const payload = data.d as GuildRoleDeletePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const cachedRole = guild.roles.get(payload.role_id)!; - guild.roles.delete(payload.role_id); - - if (cachedRole) eventHandlers.roleDelete?.(guild, cachedRole); - - // For bots without GUILD_MEMBERS member.roles is never updated breaking permissions checking. - cacheHandlers.forEach("members", (member) => { - // Not in the relevant guild so just skip. - if (!member.guilds.has(guild.id)) return; - - member.guilds.forEach((g) => { - // Member does not have this role - if (!g.roles.includes(payload.role_id)) return; - // Remove this role from the members cache - g.roles = g.roles.filter((id) => id !== payload.role_id); - cacheHandlers.set("members", member.id, member); - }); - }); -} - -export async function handleInternalGuildRoleUpdate(data: DiscordPayload) { - const payload = data.d as GuildRolePayload; - const guild = await cacheHandlers.get("guilds", payload.guild_id); - if (!guild) return; - - const cachedRole = guild.roles.get(payload.role.id); - if (!cachedRole) return; - - const role = await structures.createRoleStruct(payload.role); - guild.roles.set(payload.role.id, role); - await cacheHandlers.set("guilds", guild.id, guild); - - eventHandlers.roleUpdate?.(guild, role, cachedRole); -} diff --git a/src/api/controllers/VOICE_SERVER_UPDATE.ts b/src/api/controllers/voice/VOICE_SERVER_UPDATE.ts similarity index 73% rename from src/api/controllers/VOICE_SERVER_UPDATE.ts rename to src/api/controllers/voice/VOICE_SERVER_UPDATE.ts index 9548321dd..3f33680a6 100644 --- a/src/api/controllers/VOICE_SERVER_UPDATE.ts +++ b/src/api/controllers/voice/VOICE_SERVER_UPDATE.ts @@ -1,9 +1,9 @@ -import { eventHandlers } from "../../bot.ts"; +import { eventHandlers } from "../../../bot.ts"; import { DiscordPayload, DiscordVoiceServerUpdateEvent, -} from "../../types/mod.ts"; -import { cacheHandlers } from "./cache.ts"; +} from "../../../types/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; export async function handleVoiceServerUpdate(data: DiscordPayload) { const payload = data.d as DiscordVoiceServerUpdateEvent; diff --git a/src/api/controllers/voice/VOICE_STATE_UPDATE.ts b/src/api/controllers/voice/VOICE_STATE_UPDATE.ts new file mode 100644 index 000000000..6464d1dea --- /dev/null +++ b/src/api/controllers/voice/VOICE_STATE_UPDATE.ts @@ -0,0 +1,54 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, VoiceStateUpdatePayload } from "../../../types/mod.ts"; +import { structures } from "../../structures/mod.ts"; +import { cacheHandlers } from "../../../cache.ts"; + +export async function handleVoiceStateUpdate(data: DiscordPayload) { + const payload = data.d as VoiceStateUpdatePayload; + if (!payload.guild_id) return; + + const guild = await cacheHandlers.get("guilds", payload.guild_id); + if (!guild) return; + + const member = payload.member + ? await structures.createMemberStruct(payload.member, guild.id) + : await cacheHandlers.get("members", payload.user_id); + if (!member) return; + + // No cached state before so lets make one for em + const cachedState = guild.voiceStates.get(payload.user_id); + + guild.voiceStates.set(payload.user_id, { + ...payload, + guildID: payload.guild_id, + channelID: payload.channel_id || "", + userID: payload.user_id, + sessionID: payload.session_id, + selfDeaf: payload.self_deaf, + selfMute: payload.self_mute, + selfStream: payload.self_stream || false, + }); + + await cacheHandlers.set("guilds", payload.guild_id, guild); + + if (cachedState?.channelID !== payload.channel_id) { + // Either joined or moved channels + if (payload.channel_id) { + if (cachedState?.channelID) { // Was in a channel before + eventHandlers.voiceChannelSwitch?.( + member, + payload.channel_id, + cachedState.channelID, + ); + } else { // Was not in a channel before so user just joined + eventHandlers.voiceChannelJoin?.(member, payload.channel_id); + } + } // Left the channel + else if (cachedState?.channelID) { + guild.voiceStates.delete(payload.user_id); + eventHandlers.voiceChannelLeave?.(member, cachedState.channelID); + } + } + + eventHandlers.voiceStateUpdate?.(member, payload); +} diff --git a/src/api/controllers/webhooks/WEBHOOKS_UPDATE.ts b/src/api/controllers/webhooks/WEBHOOKS_UPDATE.ts new file mode 100644 index 000000000..303995f88 --- /dev/null +++ b/src/api/controllers/webhooks/WEBHOOKS_UPDATE.ts @@ -0,0 +1,10 @@ +import { eventHandlers } from "../../../bot.ts"; +import { DiscordPayload, WebhookUpdatePayload } from "../../../types/mod.ts"; + +export function handleWebhooksUpdate(data: DiscordPayload) { + const options = data.d as WebhookUpdatePayload; + eventHandlers.webhooksUpdate?.( + options.channel_id, + options.guild_id, + ); +} diff --git a/src/api/handlers/channel.ts b/src/api/handlers/channel.ts index 16c687859..54f4c02d4 100644 --- a/src/api/handlers/channel.ts +++ b/src/api/handlers/channel.ts @@ -23,7 +23,7 @@ import { botHasPermission, calculateBits, } from "../../util/permissions.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; +import { cacheHandlers } from "../../cache.ts"; import { structures } from "../structures/mod.ts"; /** Checks if a channel overwrite for a user id or a role id has permission in this channel */ diff --git a/src/api/handlers/guild.ts b/src/api/handlers/guild.ts index 182ef9c45..5d0f955f9 100644 --- a/src/api/handlers/guild.ts +++ b/src/api/handlers/guild.ts @@ -44,7 +44,7 @@ import { urlToBase64, } from "../../util/utils.ts"; import { requestAllMembers } from "../../ws/shard_manager.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; +import { cacheHandlers } from "../../cache.ts"; import { Guild, Member, structures } from "../structures/mod.ts"; /** Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. This endpoint can be used only by bots in less than 10 guilds. */ diff --git a/src/api/handlers/member.ts b/src/api/handlers/member.ts index b28170a3c..78d978172 100644 --- a/src/api/handlers/member.ts +++ b/src/api/handlers/member.ts @@ -17,7 +17,7 @@ import { highestRole, } from "../../util/permissions.ts"; import { formatImageURL, urlToBase64 } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; +import { cacheHandlers } from "../../cache.ts"; import { Member, structures } from "../structures/mod.ts"; import { sendMessage } from "./channel.ts"; diff --git a/src/api/handlers/message.ts b/src/api/handlers/message.ts index deceb5694..617d44ffd 100644 --- a/src/api/handlers/message.ts +++ b/src/api/handlers/message.ts @@ -11,7 +11,7 @@ import { Collection } from "../../util/collection.ts"; import { endpoints } from "../../util/constants.ts"; import { botHasChannelPermissions } from "../../util/permissions.ts"; import { delay } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; +import { cacheHandlers } from "../../cache.ts"; import { Message, structures } from "../structures/mod.ts"; /** Delete a message with the channel id and message id only. */ diff --git a/src/api/structures/guild.ts b/src/api/structures/guild.ts index e7b8f19f9..6b3fbefd3 100644 --- a/src/api/structures/guild.ts +++ b/src/api/structures/guild.ts @@ -16,7 +16,7 @@ import { import { cache } from "../../util/cache.ts"; import { Collection } from "../../util/collection.ts"; import { createNewProp } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; +import { cacheHandlers } from "../../cache.ts"; import { ban, deleteServer, diff --git a/src/api/structures/member.ts b/src/api/structures/member.ts index df18dfa29..87e4072ec 100644 --- a/src/api/structures/member.ts +++ b/src/api/structures/member.ts @@ -10,7 +10,7 @@ import { import { cache } from "../../util/cache.ts"; import { Collection } from "../../util/collection.ts"; import { createNewProp } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; +import { cacheHandlers } from "../../cache.ts"; import { ban } from "../handlers/guild.ts"; import { addRole, diff --git a/src/api/structures/message.ts b/src/api/structures/message.ts index c6e0240cc..bdacc3ea4 100644 --- a/src/api/structures/message.ts +++ b/src/api/structures/message.ts @@ -14,7 +14,7 @@ import { } from "../../types/mod.ts"; import { cache } from "../../util/cache.ts"; import { createNewProp } from "../../util/utils.ts"; -import { cacheHandlers } from "../controllers/cache.ts"; +import { cacheHandlers } from "../../cache.ts"; import { sendMessage } from "../handlers/channel.ts"; import { sendDirectMessage } from "../handlers/member.ts"; import { diff --git a/src/api/controllers/cache.ts b/src/cache.ts similarity index 94% rename from src/api/controllers/cache.ts rename to src/cache.ts index 0077c98e5..82ae7daf8 100644 --- a/src/api/controllers/cache.ts +++ b/src/cache.ts @@ -1,9 +1,9 @@ // deno-lint-ignore-file require-await no-explicit-any prefer-const -import { PresenceUpdatePayload } from "../../types/mod.ts"; -import { cache } from "../../util/cache.ts"; -import { Collection } from "../../util/collection.ts"; -import { Channel, Guild, Member, Message } from "../structures/mod.ts"; +import { PresenceUpdatePayload } from "./types/mod.ts"; +import { cache } from "./util/cache.ts"; +import { Collection } from "./util/collection.ts"; +import { Channel, Guild, Member, Message } from "./api/structures/mod.ts"; export type TableName = | "guilds" diff --git a/src/util/permissions.ts b/src/util/permissions.ts index 8579cb99b..b637a7ef6 100644 --- a/src/util/permissions.ts +++ b/src/util/permissions.ts @@ -1,4 +1,4 @@ -import { cacheHandlers } from "../api/controllers/cache.ts"; +import { cacheHandlers } from "../cache.ts"; import { Guild, Role } from "../api/structures/mod.ts"; import { botID } from "../bot.ts"; import { Permission, Permissions, RawOverwrite } from "../types/mod.ts"; diff --git a/src/ws/shard_manager.ts b/src/ws/shard_manager.ts index f2b244c75..ab02046da 100644 --- a/src/ws/shard_manager.ts +++ b/src/ws/shard_manager.ts @@ -8,7 +8,6 @@ import { DiscordPayload, FetchMembersOptions, GatewayOpcode, - GatewayStatusUpdatePayload, } from "../types/mod.ts"; import { cache } from "../util/cache.ts"; import { Collection } from "../util/collection.ts";