diff --git a/src/controllers/messages.ts b/src/controllers/messages.ts new file mode 100644 index 000000000..0b2175e2f --- /dev/null +++ b/src/controllers/messages.ts @@ -0,0 +1,107 @@ +import { eventHandlers } from "../module/client.ts"; +import { structures } from "../structures/mod.ts"; +import { DiscordPayload } from "../types/discord.ts"; +import { + MessageCreateOptions, + MessageDeletePayload, + MessageDeleteBulkPayload, +} from "../types/message.ts"; +import { cache } from "../utils/cache.ts"; + +export function handleInternalMessageCreate(data: DiscordPayload) { + if (data.t !== "MESSAGE_CREATE") return; + + const payload = data.d as MessageCreateOptions; + const channel = cache.channels.get(payload.channel_id); + if (channel) channel.lastMessageID = payload.id; + + const message = structures.createMessage(payload); + // Cache the message + cache.messages.set(payload.id, message); + const guild = payload.guild_id + ? cache.guilds.get(payload.guild_id) + : undefined; + + if (payload.member) { + // If in a guild cache the author as a member + guild?.members.set( + payload.author.id, + structures.createMember( + { ...payload.member, user: payload.author }, + guild, + ), + ); + } + + payload.mentions.forEach((mention) => { + // Cache the member if its a valid member + if (mention.member) { + guild?.members.set( + mention.id, + structures.createMember( + { ...mention.member, user: mention }, + guild, + ), + ); + } + }); + + eventHandlers.messageCreate?.(message); +} + +export function handleInternalMessageDelete(data: DiscordPayload) { + if (data.t !== "MESSAGE_DELETE") return; + + const payload = data.d as MessageDeletePayload; + const channel = cache.channels.get(payload.channel_id); + if (!channel) return; + + eventHandlers.messageDelete?.( + cache.messages.get(payload.id) || { id: payload.id, channel }, + ); + + cache.messages.delete(payload.id); +} + +export function handleInternalMessageDeleteBulk(data: DiscordPayload) { + if (data.t !== "MESSAGE_DELETE_BULK") return; + + const payload = data.d as MessageDeleteBulkPayload; + const channel = cache.channels.get(payload.channel_id); + if (!channel) return; + + payload.ids.forEach((id) => { + eventHandlers.messageDelete?.(cache.messages.get(id) || { id, channel }); + cache.messages.delete(id); + }); +} + +export function handleInternalMessageUpdate(data: DiscordPayload) { + if (data.t !== "MESSAGE_UPDATE") return; + + const payload = data.d as MessageCreateOptions; + const channel = cache.channels.get(payload.channel_id); + if (!channel) return; + + const cachedMessage = cache.messages.get(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; + } + + eventHandlers.messageUpdate?.(cachedMessage, oldMessage); +} diff --git a/src/controllers/misc.ts b/src/controllers/misc.ts index ed4caeecc..d2f3aa0c1 100644 --- a/src/controllers/misc.ts +++ b/src/controllers/misc.ts @@ -1,16 +1,28 @@ import { delay } from "https://deno.land/std@0.67.0/async/delay.ts"; import { eventHandlers, setBotID } from "../module/client.ts"; import { allowNextShard } from "../module/shardingManager.ts"; -import { DiscordPayload, ReadyPayload } from "../types/discord.ts"; +import { structures } from "../structures/mod.ts"; +import { + DiscordPayload, + PresenceUpdatePayload, + ReadyPayload, + TypingStartPayload, + VoiceStateUpdatePayload, + WebhookUpdatePayload, +} from "../types/discord.ts"; +import { UserPayload } from "../types/guild.ts"; import { cache } from "../utils/cache.ts"; -export async function handleInternalReady(data: DiscordPayload, shardID: number) { +export async function handleInternalReady( + data: DiscordPayload, + shardID: number, +) { if (data.t !== "READY") return; const payload = data.d as ReadyPayload; setBotID(payload.user.id); - // Triggered on each shard + // Triggered on each shard eventHandlers.shardReady?.(shardID); if (payload.shard && shardID === payload.shard[1] - 1) { // Wait 10 seconds to allow all guild create events to be processed @@ -19,7 +31,101 @@ export async function handleInternalReady(data: DiscordPayload, shardID: number) eventHandlers.ready?.(); } - // Wait 5 seconds to spawn next shard - await delay(5000); - allowNextShard() + // Wait 5 seconds to spawn next shard + await delay(5000); + allowNextShard(); +} + +export function handleInternalPresenceUpdate(data: DiscordPayload) { + if (data.t !== "PRESENCE_UPDATE") return; + + const payload = data.d as PresenceUpdatePayload; + const oldPresence = cache.presences.get(payload.user.id); + cache.presences.set(payload.user.id, payload); + return eventHandlers.presenceUpdate?.(payload, oldPresence); +} + +export function handleInternalTypingStart(data: DiscordPayload) { + if (data.t !== "TYPING_START") return; + eventHandlers.typingStart?.(data.d as TypingStartPayload); +} + +export function handleInternalUserUpdate(data: DiscordPayload) { + if (data.t !== "USER_UPDATE") return; + + const userData = data.d as UserPayload; + + cache.guilds.forEach((guild) => { + const member = guild.members.get(userData.id); + if (!member) return; + // member.author = userData; + Object.entries(userData).forEach(([key, value]) => { + // @ts-ignore + if (member[key] === value) return; + // @ts-ignore + member[key] = value; + }); + }); + return eventHandlers.botUpdate?.(userData); +} + +export function handleInternalVoiceStateUpdate(data: DiscordPayload) { + if (data.t !== "VOICE_STATE_UPDATE") return; + + const payload = data.d as VoiceStateUpdatePayload; + if (!payload.guild_id) return; + + const guild = cache.guilds.get(payload.guild_id); + if (!guild) return; + + const member = guild.members.get(payload.user_id) || + (payload.member + ? structures.createMember(payload.member, guild) + : undefined); + 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, + }); + + if (cachedState?.channelID !== payload.channel_id) { + // Either joined or moved channels + if (payload.channel_id) { + cachedState?.channelID + ? // Was in a channel before + eventHandlers.voiceChannelSwitch?.( + member, + payload.channel_id, + cachedState.channelID, + ) + : // 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); +} + +export function handleInternalWebhooksUpdate(data: DiscordPayload) { + if (data.t !== "WEBHOOKS_UPDATE") return; + + const options = data.d as WebhookUpdatePayload; + return eventHandlers.webhooksUpdate?.( + options.channel_id, + options.guild_id, + ); } diff --git a/src/controllers/mod.ts b/src/controllers/mod.ts index 353ebaecf..85f9dee42 100644 --- a/src/controllers/mod.ts +++ b/src/controllers/mod.ts @@ -19,7 +19,31 @@ import { handleInternalGuildMembersChunk, handleInternalGuildMemberUpdate, } from "./members.ts"; -import { handleInternalReady } from "./misc.ts"; +import { + handleInternalMessageCreate, + handleInternalMessageDelete, + handleInternalMessageDeleteBulk, + handleInternalMessageUpdate, +} from "./messages.ts"; +import { + handleInternalPresenceUpdate, + handleInternalReady, + handleInternalTypingStart, + handleInternalUserUpdate, + handleInternalVoiceStateUpdate, + handleInternalWebhooksUpdate, +} from "./misc.ts"; +import { + handleInternalMessageReactionAdd, + handleInternalMessageReactionRemove, + handleInternalMessageReactionRemoveAll, + handleInternalMessageReactionRemoveEmoji, +} from "./reactions.ts"; +import { + handleInternalGuildRoleCreate, + handleInternalGuildRoleDelete, + handleInternalGuildRoleUpdate, +} from "./roles.ts"; export let controllers = { READY: handleInternalReady, @@ -36,4 +60,26 @@ export let controllers = { GUILD_MEMBER_REMOVE: handleInternalGuildMemberRemove, GUILD_MEMBER_UPDATE: handleInternalGuildMemberUpdate, GUILD_MEMBERS_CHUNK: handleInternalGuildMembersChunk, + GUILD_ROLE_CREATE: handleInternalGuildRoleCreate, + GUILD_ROLE_DELETE: handleInternalGuildRoleDelete, + GUILD_ROLE_UPDATE: handleInternalGuildRoleUpdate, + 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, + WEBHOOKS_UPDATE: handleInternalWebhooksUpdate, }; + +export type Controllers = typeof controllers + +export function updateControllers(newControllers: Controllers) { + controllers = newControllers +} diff --git a/src/controllers/reactions.ts b/src/controllers/reactions.ts new file mode 100644 index 000000000..8c6e99874 --- /dev/null +++ b/src/controllers/reactions.ts @@ -0,0 +1,129 @@ +import { botID, eventHandlers } from "../module/client.ts"; +import { structures } from "../structures/mod.ts"; +import { DiscordPayload } from "../types/discord.ts"; +import { + BaseMessageReactionPayload, + MessageReactionPayload, + MessageReactionRemoveEmojiPayload, +} from "../types/message.ts"; +import { cache } from "../utils/cache.ts"; + +export function handleInternalMessageReactionAdd(data: DiscordPayload) { + if (data.t !== "MESSAGE_REACTION_ADD") return; + + const payload = data.d as MessageReactionPayload; + const message = cache.messages.get(payload.message_id); + + if (message) { + const previousReactions = message.reactions; + const reactionExisted = previousReactions?.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]; + } + + cache.messages.set(payload.message_id, message); + } + + if (payload.member && payload.guild_id) { + const guild = cache.guilds.get(payload.guild_id); + guild?.members.set( + payload.member.user.id, + structures.createMember( + payload.member, + guild, + ), + ); + } + + const uncachedOptions = { + ...payload, + id: payload.message_id, + channelID: payload.channel_id, + guildID: payload.guild_id, + }; + + eventHandlers.reactionAdd?.( + message || uncachedOptions, + payload.emoji, + payload.user_id, + ); +} + +export function handleInternalMessageReactionRemove(data: DiscordPayload) { + if (data.t !== "MESSAGE_REACTION_REMOVE") return; + + const payload = data.d as MessageReactionPayload; + const message = cache.messages.get(payload.message_id); + + if (message) { + const previousReactions = message.reactions; + const reactionExisted = previousReactions?.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]; + } + + cache.messages.set(payload.message_id, message); + } + + if (payload.member && payload.guild_id) { + const guild = cache.guilds.get(payload.guild_id); + guild?.members.set( + payload.member.user.id, + structures.createMember( + payload.member, + guild, + ), + ); + } + + const uncachedOptions = { + ...payload, + id: payload.message_id, + channelID: payload.channel_id, + guildID: payload.guild_id, + }; + + eventHandlers.reactionRemove?.( + message || uncachedOptions, + payload.emoji, + payload.user_id, + ); +} + +export function handleInternalMessageReactionRemoveAll(data: DiscordPayload) { + if (data.t !== "MESSAGE_REACTION_REMOVE_ALL") return; + + eventHandlers.reactionRemoveAll?.(data.d as BaseMessageReactionPayload); +} + +export function handleInternalMessageReactionRemoveEmoji(data: DiscordPayload) { + if (data.t !== "MESSAGE_REACTION_REMOVE_EMOJI") return; + + eventHandlers.reactionRemoveEmoji?.(data.d as MessageReactionRemoveEmojiPayload); +} diff --git a/src/module/shardingManager.ts b/src/module/shardingManager.ts index 0ebc598e9..153e15065 100644 --- a/src/module/shardingManager.ts +++ b/src/module/shardingManager.ts @@ -2,42 +2,25 @@ import { DiscordBotGatewayData, DiscordPayload, GatewayOpcode, - PresenceUpdatePayload, - TypingStartPayload, - VoiceStateUpdatePayload, - WebhookUpdatePayload, } from "../types/discord.ts"; import { eventHandlers, botGatewayData, identifyPayload, - botID, IdentifyPayload, } from "./client.ts"; import { delay } from "https://deno.land/std@0.67.0/async/delay.ts"; import { Guild } from "../structures/guild.ts"; import { - GuildRolePayload, - UserPayload, FetchMembersOptions, - GuildRoleDeletePayload, } from "../types/guild.ts"; import { cache } from "../utils/cache.ts"; -import { - MessageCreateOptions, - MessageDeletePayload, - MessageDeleteBulkPayload, - MessageReactionPayload, - BaseMessageReactionPayload, - MessageReactionRemoveEmojiPayload, -} from "../types/message.ts"; import { createBasicShard, requestGuildMembers, botGatewayStatusRequest, } from "./basicShard.ts"; import { BotStatusRequest } from "../utils/utils.ts"; -import { structures } from "../structures/mod.ts"; import { controllers } from "../controllers/mod.ts"; let shardCounter = 0; @@ -117,256 +100,7 @@ export async function handleDiscordPayload( case GatewayOpcode.Dispatch: if (!data.t) return; // Run the appropriate controller for this event. - controllers[data.t]?.(data, shardID); - - if (data.t === "MESSAGE_CREATE") { - const options = data.d as MessageCreateOptions; - const channel = cache.channels.get(options.channel_id); - if (channel) channel.lastMessageID = options.id; - - const message = structures.createMessage(options); - // Cache the message - cache.messages.set(options.id, message); - const guild = options.guild_id - ? cache.guilds.get(options.guild_id) - : undefined; - - if (options.member) { - // If in a guild cache the author as a member - guild?.members.set( - options.author.id, - structures.createMember( - { ...options.member, user: options.author }, - guild, - ), - ); - } - - options.mentions.forEach((mention) => { - // Cache the member if its a valid member - if (mention.member) { - guild?.members.set( - mention.id, - structures.createMember( - { ...mention.member, user: mention }, - guild, - ), - ); - } - }); - - return eventHandlers.messageCreate?.(message); - } - - if ( - data.t && ["MESSAGE_DELETE", "MESSAGE_DELETE_BULK"].includes(data.t) - ) { - const options = data.d as MessageDeletePayload; - const deletedMessages = data.t === "MESSAGE_DELETE" - ? [options.id] - : (data.d as MessageDeleteBulkPayload).ids; - - const channel = cache.channels.get(options.channel_id); - if (!channel) return; - - deletedMessages.forEach((id) => { - const message = cache.messages.get(id); - if (!message) return; - eventHandlers.messageDelete?.(message || { id, channel }); - cache.messages.delete(id); - }); - } - - if (data.t === "MESSAGE_UPDATE") { - const options = data.d as MessageCreateOptions; - const channel = cache.channels.get(options.channel_id); - if (!channel) return; - - const cachedMessage = cache.messages.get(options.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 ( - !options.edited_timestamp || - (cachedMessage.content !== options.content) - ) { - return; - } - - return eventHandlers.messageUpdate?.(cachedMessage, oldMessage); - } - - if ( - data.t && - ["MESSAGE_REACTION_ADD", "MESSAGE_REACTION_REMOVE"].includes(data.t) - ) { - const options = data.d as MessageReactionPayload; - const message = cache.messages.get(options.message_id); - const isAdd = data.t === "MESSAGE_REACTION_ADD"; - - if (message) { - const previousReactions = message.reactions; - const reactionExisted = previousReactions?.find( - (reaction) => - reaction.emoji.id === options.emoji.id && - reaction.emoji.name === options.emoji.name, - ); - if (reactionExisted) { - reactionExisted.count = isAdd - ? reactionExisted.count + 1 - : reactionExisted.count - 1; - } else { - const newReaction = { - count: 1, - me: options.user_id === botID, - emoji: { ...options.emoji, id: options.emoji.id || undefined }, - }; - message.reactions = message.reactions - ? [...message.reactions, newReaction] - : [newReaction]; - } - - cache.messages.set(options.message_id, message); - } - - if (options.member && options.guild_id) { - const guild = cache.guilds.get(options.guild_id); - guild?.members.set( - options.member.user.id, - structures.createMember( - options.member, - guild, - ), - ); - } - - const uncachedOptions = { - ...options, - id: options.message_id, - channelID: options.channel_id, - guildID: options.guild_id, - }; - - return isAdd - ? eventHandlers.reactionAdd?.( - message || uncachedOptions, - options.emoji, - options.user_id, - ) - : eventHandlers.reactionRemove?.( - message || uncachedOptions, - options.emoji, - options.user_id, - ); - } - - if (data.t === "MESSAGE_REACTION_REMOVE_ALL") { - return eventHandlers.reactionRemoveAll?.( - data.d as BaseMessageReactionPayload, - ); - } - - if (data.t === "MESSAGE_REACTION_REMOVE_EMOJI") { - return eventHandlers.reactionRemoveEmoji?.( - data.d as MessageReactionRemoveEmojiPayload, - ); - } - - if (data.t === "PRESENCE_UPDATE") { - const payload = data.d as PresenceUpdatePayload; - const oldPresence = cache.presences.get(payload.user.id); - cache.presences.set(payload.user.id, payload); - return eventHandlers.presenceUpdate?.(payload, oldPresence); - } - - if (data.t === "TYPING_START") { - return eventHandlers.typingStart?.(data.d as TypingStartPayload); - } - - if (data.t === "USER_UPDATE") { - const userData = data.d as UserPayload; - - cache.guilds.forEach((guild) => { - const member = guild.members.get(userData.id); - if (!member) return; - // member.author = userData; - Object.entries(userData).forEach(([key, value]) => { - // @ts-ignore - if (member[key] === value) return; - // @ts-ignore - member[key] = value; - }); - }); - return eventHandlers.botUpdate?.(userData); - } - - if (data.t === "VOICE_STATE_UPDATE") { - const payload = data.d as VoiceStateUpdatePayload; - if (!payload.guild_id) return; - - const guild = cache.guilds.get(payload.guild_id); - if (!guild) return; - - const member = guild.members.get(payload.user_id) || - (payload.member - ? structures.createMember(payload.member, guild) - : undefined); - 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, - }); - - if (cachedState?.channelID !== payload.channel_id) { - // Either joined or moved channels - if (payload.channel_id) { - cachedState?.channelID - ? // Was in a channel before - eventHandlers.voiceChannelSwitch?.( - member, - payload.channel_id, - cachedState.channelID, - ) - : // 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); - } - } - - return eventHandlers.voiceStateUpdate?.(member, payload); - } - - if (data.t === "WEBHOOKS_UPDATE") { - const options = data.d as WebhookUpdatePayload; - return eventHandlers.webhooksUpdate?.( - options.channel_id, - options.guild_id, - ); - } - - return; + return controllers[data.t]?.(data, shardID); default: return; } diff --git a/src/structures/mod.ts b/src/structures/mod.ts index d7c60460f..d69713721 100644 --- a/src/structures/mod.ts +++ b/src/structures/mod.ts @@ -13,7 +13,7 @@ export let structures = { createRole, }; -export type Structures = typeof structures[keyof typeof structures]; +export type Structures = typeof structures; /** This function is used to update/reload/customize the internal structure of Discordeno. * diff --git a/src/types/discord.ts b/src/types/discord.ts index aabde1db7..2872e2ef3 100644 --- a/src/types/discord.ts +++ b/src/types/discord.ts @@ -28,7 +28,20 @@ export interface DiscordPayload { | "GUILD_MEMBERS_CHUNK" | "GUILD_ROLE_CREATE" | "GUILD_ROLE_DELETE" - | "GUILD_ROLE_UPDATE"; + | "GUILD_ROLE_UPDATE" + | "MESSAGE_CREATE" + | "MESSAGE_DELETE" + | "MESSAGE_DELETE_BULK" + | "MESSAGE_UPDATE" + | "MESSAGE_REACTION_ADD" + | "MESSAGE_REACTION_REMOVE" + | "MESSAGE_REACTION_REMOVE_ALL" + | "MESSAGE_REACTION_REMOVE_EMOJI" + | "PRESENCE_UPDATE" + | "TYPING_START" + | "USER_UPDATE" + | "VOICE_STATE_UPDATE" + | "WEBHOOKS_UPDATE"; } export interface DiscordBotGatewayData {