diff --git a/module/client.ts b/module/client.ts index d4b6c7553..858d7bab8 100644 --- a/module/client.ts +++ b/module/client.ts @@ -1,20 +1,20 @@ -import { endpoints } from "../constants/discord.ts" -import { DiscordBotGatewayData } from "../types/discord.ts" -import { ClientOptions, EventHandlers } from "../types/options.ts" -import { RequestManager } from "./requestManager.ts" -import { Channel } from "../structures/channel.ts" -import { spawnShards } from "./shardingManager.ts" -import { cache } from "../utils/cache.ts" +import { endpoints } from "../constants/discord.ts"; +import { DiscordBotGatewayData } from "../types/discord.ts"; +import { ClientOptions, EventHandlers } from "../types/options.ts"; +import { RequestManager } from "./requestManager.ts"; +import { Channel } from "../structures/channel.ts"; +import { spawnShards } from "./shardingManager.ts"; +import { cache } from "../utils/cache.ts"; +// import { connectWebSocket } from "https://deno.land/std@0.50.0/ws/mod.ts"; -export let authorization = "" -export let botID = "" +export let authorization = ""; +export let botID = ""; /** The bot's token. This should never be used by end users. It is meant to be used internally to make requests to the Discord API. */ -export let token = "" +export let token = ""; -export let eventHandlers: EventHandlers = {} - -export let botGatewayData: DiscordBotGatewayData +export let eventHandlers: EventHandlers = {}; +export let botGatewayData: DiscordBotGatewayData; export const identifyPayload = { token: "", @@ -25,29 +25,29 @@ export const identifyPayload = { $device: "Discordeno", }, intents: 0, - shards: [0, 0], -} + shard: [0, 0], +}; export const createClient = async (data: ClientOptions) => { - // Assign some defaults to the options to make them fulfilled / not annoying to use. - botID = data.botID - token = data.token - if (data.eventHandlers) eventHandlers = data.eventHandlers - authorization = `Bot ${data.token}` + botID = data.botID; + token = data.token; + if (data.eventHandlers) eventHandlers = data.eventHandlers; + authorization = `Bot ${data.token}`; // Initial API connection to get info about bots connection - botGatewayData = await RequestManager.get(endpoints.GATEWAY_BOT) + botGatewayData = await RequestManager.get(endpoints.GATEWAY_BOT); - identifyPayload.token = data.token - identifyPayload.intents = data.intents.reduce((bits, next) => (bits |= next), 0) + identifyPayload.token = data.token; + identifyPayload.intents = data.intents.reduce( + (bits, next) => (bits |= next), + 0, + ); - spawnShards(botGatewayData, identifyPayload) -} + spawnShards(botGatewayData, identifyPayload); +}; - - -export default createClient +export default createClient; export const updateChannelCache = (key: string, value: Channel) => { - cache.channels.set(key, value) -} + cache.channels.set(key, value); +}; diff --git a/module/gateway.ts b/module/gateway.ts index 5bd9d2295..b59cefde2 100644 --- a/module/gateway.ts +++ b/module/gateway.ts @@ -1,17 +1,22 @@ -import { WebSocket } from "https://deno.land/std@v1.0.0-rc1/ws/mod.ts" -import { GatewayOpcode } from "../types/discord.ts" -import { delay } from "https://deno.land/std@v1.0.0-rc1/util/async.ts" +import { WebSocket } from "https://deno.land/std@0.50.0/ws/mod.ts"; +import { GatewayOpcode } from "../types/discord.ts"; +import { delay } from "https://deno.land/std@0.50.0/async/mod.ts"; // Discord requests null if no number has yet been sent by discord -export let previousSequenceNumber: number | null = null +export let previousSequenceNumber: number | null = null; // TODO: If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume. -export const sendConstantHeartbeats = async (socket: WebSocket, interval: number) => { - await delay(interval) - socket.send(JSON.stringify({ op: GatewayOpcode.Heartbeat, d: previousSequenceNumber })) - sendConstantHeartbeats(socket, interval) -} +export const sendConstantHeartbeats = async ( + socket: WebSocket, + interval: number, +) => { + await delay(interval); + socket.send( + JSON.stringify({ op: GatewayOpcode.Heartbeat, d: previousSequenceNumber }), + ); + sendConstantHeartbeats(socket, interval); +}; export const updatePreviousSequenceNumber = (sequence: number) => { - previousSequenceNumber = sequence -} + previousSequenceNumber = sequence; +}; diff --git a/module/requestManager.ts b/module/requestManager.ts index 289275310..f410303e6 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -1,108 +1,118 @@ -import { RequestMethod } from "../types/fetch.ts" -import { authorization } from "./client.ts" -import { sleep } from "../utils/utils.ts" +import { RequestMethod } from "../types/fetch.ts"; +import { authorization } from "./client.ts"; +import { sleep } from "../utils/utils.ts"; -const ratelimitedPaths = new Map() +const ratelimitedPaths = new Map(); export interface RateLimitedPath { - url: string - resetTimestamp: number + url: string; + resetTimestamp: number; } setInterval(() => { - const now = Date.now() + const now = Date.now(); ratelimitedPaths.forEach((value, key) => { - if (value.resetTimestamp > now) return - ratelimitedPaths.delete(key) - }) -}, 1000) + if (value.resetTimestamp > now) return; + ratelimitedPaths.delete(key); + }); +}, 1000); export const RequestManager = { // Something off about using runMethod with get breaks when using fetch get: async (url: string, body?: unknown) => { - await checkRatelimits(url) - const result = await fetch(url, createRequestBody(body)) - processHeaders(url, result.headers) + await checkRatelimits(url); - return result.json() + const result = await fetch(url, createRequestBody(body)); + processHeaders(url, result.headers); + + return result.json(); }, post: (url: string, body?: unknown) => { - return runMethod(RequestMethod.Post, url, body) + return runMethod(RequestMethod.Post, url, body); }, delete: (url: string, body?: unknown) => { - return runMethod(RequestMethod.Delete, url, body) + return runMethod(RequestMethod.Delete, url, body); }, patch: (url: string, body?: unknown) => { - return runMethod(RequestMethod.Patch, url, body) + return runMethod(RequestMethod.Patch, url, body); }, put: (url: string, body?: unknown) => { - return runMethod(RequestMethod.Put, url, body) + return runMethod(RequestMethod.Put, url, body); }, -} +}; const createRequestBody = (body: any, method?: RequestMethod) => { return { headers: { Authorization: authorization, - "User-Agent": `DiscordBot (https://github.com/skillz4killz/discordeno, 0.0.1)`, + "User-Agent": + `DiscordBot (https://github.com/skillz4killz/discordeno, 0.0.1)`, "Content-Type": "application/json", "X-Audit-Log-Reason": body ? encodeURIComponent(body.reason) : "", }, body: JSON.stringify(body), method: method?.toUpperCase(), - } -} + }; +}; -const runMethod = async (method: RequestMethod, url: string, body?: unknown) => { - await checkRatelimits(url) - const response = await fetch(url, createRequestBody(body, method)) - processHeaders(url, response.headers) +const runMethod = async ( + method: RequestMethod, + url: string, + body?: unknown, +) => { + await checkRatelimits(url); + const response = await fetch(url, createRequestBody(body, method)); + processHeaders(url, response.headers); // Sometimes Discord returns an empty 204 response that can't be made to JSON. - if (response.status === 204) return + if (response.status === 204) return; - return await response.json() -} + return await response.json(); +}; const checkRatelimits = async (url: string) => { - const ratelimited = ratelimitedPaths.get(url) - const global = ratelimitedPaths.get("global") + const ratelimited = ratelimitedPaths.get(url); + const global = ratelimitedPaths.get("global"); - const now = Date.now() - if (ratelimited && now < ratelimited.resetTimestamp) await sleep(now - ratelimited.resetTimestamp) - if (global && now < global.resetTimestamp) await sleep(now - global.resetTimestamp) -} + const now = Date.now(); + if (ratelimited && now < ratelimited.resetTimestamp) { + await sleep(now - ratelimited.resetTimestamp); + } + if (global && now < global.resetTimestamp) { + await sleep(now - global.resetTimestamp); + } +}; const processHeaders = (url: string, headers: Headers) => { // If a rate limit response is encountered this will become true and returned - let ratelimited = false + let ratelimited = false; // Get all useful headers - const remaining = headers.get("x-ratelimit-remaining") - const resetTimestamp = headers.get("x-ratelimit-reset") - const retryAfter = headers.get("retry-after") - const global = headers.get("x-ratelimit-global") + const remaining = headers.get("x-ratelimit-remaining"); + const resetTimestamp = headers.get("x-ratelimit-reset"); + const retryAfter = headers.get("retry-after"); + const global = headers.get("x-ratelimit-global"); // If there is no remaining rate limit for this endpoint, we save it in cache if (remaining && remaining === "0") { - ratelimited = true + ratelimited = true; ratelimitedPaths.set(url, { url, resetTimestamp: Number(resetTimestamp), - }) + }); } // If there is no remaining global limit, we save it in cache if (global) { - ratelimited = true + ratelimited = true; ratelimitedPaths.set("global", { url: "global", resetTimestamp: Date.now() + Number(retryAfter), - }) + }); } // Returns a boolean to check if we need to request again once the rate limit resets - return ratelimited -} + return ratelimited; +}; diff --git a/module/shard.ts b/module/shard.ts index 3ae6fb7f6..9077c244a 100644 --- a/module/shard.ts +++ b/module/shard.ts @@ -1,20 +1,35 @@ -import { connectWebSocket, isWebSocketCloseEvent } from "https://deno.land/std@v1.0.0-rc1/ws/mod.ts" -import { botGatewayData, identifyPayload } from "./client.ts" -import { GatewayOpcode } from "../types/discord.ts" -import { logRed } from "../utils/logger.ts" -import { handleDiscordPayload, resumeConnection } from "./shardingManager.ts" +import { + connectWebSocket, + isWebSocketCloseEvent, +} from "https://deno.land/std@0.50.0/ws/mod.ts"; +import { GatewayOpcode, DiscordBotGatewayData } from "../types/discord.ts"; +import { logRed } from "../utils/logger.ts"; +import { handleDiscordPayload, resumeConnection } from "./shardingManager.ts"; -let shardSocket = await connectWebSocket(botGatewayData.url) -let resumeInterval = 0 +export const createShard = async ( + botGatewayData: DiscordBotGatewayData, + identifyPayload: object, +) => { + const shardSocket = await connectWebSocket(botGatewayData.url); + let resumeInterval = 0; -// Intial identify with the gateway -await shardSocket.send(JSON.stringify({ op: GatewayOpcode.Identify, d: identifyPayload })) - -for await (const message of shardSocket) { - if (typeof message === "string") { - handleDiscordPayload(JSON.parse(message), shardSocket, resumeInterval) - } else if (isWebSocketCloseEvent(message)) { - logRed(`Close :( ${JSON.stringify(message)}`) - resumeInterval = await resumeConnection(identifyPayload, shardSocket) + // Intial identify with the gateway + await shardSocket.send( + JSON.stringify({ op: GatewayOpcode.Identify, d: identifyPayload }), + ); + for await (const message of shardSocket) { + if (typeof message === "string") { + handleDiscordPayload(JSON.parse(message), shardSocket, resumeInterval); + } else if (isWebSocketCloseEvent(message)) { + logRed(`Close :( ${JSON.stringify(message)}`); + resumeInterval = await resumeConnection(identifyPayload, shardSocket); + } } -} +}; + +postMessage({ type: "REQUEST_CLIENT_OPTIONS" }); +onmessage = (message) => { + if (message.data.type === "CREATE_SHARD") { + createShard(message.data.botGatewayData, message.data.identifyPayload); + } +}; diff --git a/module/shardingManager.ts b/module/shardingManager.ts index f5974b6d1..34272977d 100644 --- a/module/shardingManager.ts +++ b/module/shardingManager.ts @@ -8,202 +8,266 @@ import { TypingStartPayload, VoiceStateUpdatePayload, WebhookUpdatePayload, -} from "../types/discord.ts" -import { eventHandlers, botID, botGatewayData, identifyPayload } from "./client.ts" -import { updatePreviousSequenceNumber, sendConstantHeartbeats, previousSequenceNumber } from "./gateway.ts" -import { WebSocket, connectWebSocket } from "https://deno.land/std@v1.0.0-rc1/ws/mod.ts" -import { handleInternalChannelCreate } from "../events/channels.ts" -import { handleInternalChannelUpdate } from "../events/channels.ts" -import { handleInternalChannelDelete } from "../events/channels.ts" -import { createGuild } from "../structures/guild.ts" -import { handleInternalGuildCreate } from "../events/guilds.ts" -import { cache } from "../utils/cache.ts" -import { handleInternalGuildUpdate } from "../events/guilds.ts" -import { CreateGuildPayload, GuildMemberChunkPayload, GuildRolePayload, UserPayload } from "../types/guild.ts" -import { GuildDeletePayload } from "../types/guild.ts" -import { handleInternalGuildDelete } from "../events/guilds.ts" -import { GuildBanPayload } from "../types/guild.ts" -import { createUser } from "../structures/user.ts" -import { GuildEmojisUpdatePayload } from "../types/guild.ts" -import { GuildMemberAddPayload } from "../types/guild.ts" -import { createMember } from "../structures/member.ts" -import { GuildMemberUpdatePayload } from "../types/guild.ts" -import { ChannelCreatePayload } from "../types/channel.ts" -import { createRole } from "../structures/role.ts" -import { MessageCreateOptions } from "../types/message.ts" -import { createMessage } from "../structures/message.ts" -import { MessageDeletePayload } from "../types/message.ts" -import { MessageDeleteBulkPayload } from "../types/message.ts" -import { MessageUpdatePayload } from "../types/message.ts" -import { MessageReactionPayload } from "../types/message.ts" -import { BaseMessageReactionPayload } from "../types/message.ts" -import { MessageReactionRemoveEmojiPayload } from "../types/message.ts" +} from "../types/discord.ts"; +import { + eventHandlers, + botID, + botGatewayData, + identifyPayload, +} from "./client.ts"; +import { + updatePreviousSequenceNumber, + sendConstantHeartbeats, + previousSequenceNumber, +} from "./gateway.ts"; +import { + WebSocket, + connectWebSocket, +} from "https://deno.land/std@0.50.0/ws/mod.ts"; +import { handleInternalChannelCreate } from "../events/channels.ts"; +import { handleInternalChannelUpdate } from "../events/channels.ts"; +import { handleInternalChannelDelete } from "../events/channels.ts"; +import { createGuild } from "../structures/guild.ts"; +import { handleInternalGuildCreate } from "../events/guilds.ts"; +import { cache } from "../utils/cache.ts"; +import { handleInternalGuildUpdate } from "../events/guilds.ts"; +import { + CreateGuildPayload, + GuildMemberChunkPayload, + GuildRolePayload, + UserPayload, +} from "../types/guild.ts"; +import { GuildDeletePayload } from "../types/guild.ts"; +import { handleInternalGuildDelete } from "../events/guilds.ts"; +import { GuildBanPayload } from "../types/guild.ts"; +import { createUser } from "../structures/user.ts"; +import { GuildEmojisUpdatePayload } from "../types/guild.ts"; +import { GuildMemberAddPayload } from "../types/guild.ts"; +import { createMember } from "../structures/member.ts"; +import { GuildMemberUpdatePayload } from "../types/guild.ts"; +import { ChannelCreatePayload } from "../types/channel.ts"; +import { createRole } from "../structures/role.ts"; +import { MessageCreateOptions } from "../types/message.ts"; +import { createMessage } from "../structures/message.ts"; +import { MessageDeletePayload } from "../types/message.ts"; +import { MessageDeleteBulkPayload } from "../types/message.ts"; +import { MessageUpdatePayload } from "../types/message.ts"; +import { MessageReactionPayload } from "../types/message.ts"; +import { BaseMessageReactionPayload } from "../types/message.ts"; +import { MessageReactionRemoveEmojiPayload } from "../types/message.ts"; /** The session id is needed for RESUME functionality when discord disconnects randomly. */ -let sessionID = "" +let sessionID = ""; +let shardCounter = 0; function createShardWorker() { - new Worker("./module/shard.ts", { type: "module" }) + const shard = new Worker("./module/shard.ts", { type: "module", deno: true }); + shard.onmessage = (message) => { + if (message.data.type === "REQUEST_CLIENT_OPTIONS") { + identifyPayload.shard = [shardCounter++, botGatewayData.shards]; + shard.postMessage( + { type: "CREATE_SHARD", botGatewayData, identifyPayload }, + ); + } + }; } -export const spawnShards = (data: DiscordBotGatewayData, payload: unknown, id = 0) => { - identifyPayload.shards = [id, data.shards] - createShardWorker() - if (id < data.shards) spawnShards(data, payload, id + 1) -} +export const spawnShards = async ( + data: DiscordBotGatewayData, + payload: unknown, + id = 1, +) => { + createShardWorker(); + if (id < data.shards) spawnShards(data, payload, id + 1); +}; -export function handleDiscordPayload(data: DiscordPayload, socket: WebSocket, resumeInterval: number) { +export function handleDiscordPayload( + data: DiscordPayload, + socket: WebSocket, + resumeInterval: number, +) { // Update the sequence number if it is present - if (data.s) updatePreviousSequenceNumber(data.s) - eventHandlers.raw?.(data) + if (data.s) updatePreviousSequenceNumber(data.s); + eventHandlers.raw?.(data); switch (data.op) { case GatewayOpcode.Hello: - sendConstantHeartbeats(socket, (data.d as DiscordHeartbeatPayload).heartbeat_interval) - return + sendConstantHeartbeats( + socket, + (data.d as DiscordHeartbeatPayload).heartbeat_interval, + ); + return; case GatewayOpcode.HeartbeatACK: // Incase the user wants to listen to heartbeat responses - return eventHandlers.heartbeat?.() + return eventHandlers.heartbeat?.(); case GatewayOpcode.Reconnect: case GatewayOpcode.InvalidSession: // Reconnect to the gateway https://discordapp.com/developers/docs/topics/gateway#reconnect // I think this should be handled automatically when the websocket closes - return + return; case GatewayOpcode.Resume: - return clearInterval(resumeInterval) + return clearInterval(resumeInterval); case GatewayOpcode.Dispatch: if (data.t === "READY") { // Important for RESUME - sessionID = (data.d as ReadyPayload).session_id - return eventHandlers.ready?.() + sessionID = (data.d as ReadyPayload).session_id; + return eventHandlers.ready?.(); } - if (data.t === "CHANNEL_CREATE") return handleInternalChannelCreate(data.d as ChannelCreatePayload) - if (data.t === "CHANNEL_UPDATE") return handleInternalChannelUpdate(data.d as ChannelCreatePayload) - if (data.t === "CHANNEL_DELETE") return handleInternalChannelDelete(data.d as ChannelCreatePayload) + if (data.t === "CHANNEL_CREATE") { + return handleInternalChannelCreate(data.d as ChannelCreatePayload); + } + if (data.t === "CHANNEL_UPDATE") { + return handleInternalChannelUpdate(data.d as ChannelCreatePayload); + } + if (data.t === "CHANNEL_DELETE") { + return handleInternalChannelDelete(data.d as ChannelCreatePayload); + } if (data.t === "GUILD_CREATE") { - const guild = createGuild(data.d as CreateGuildPayload) - handleInternalGuildCreate(guild) - if (cache.unavailableGuilds.get(guild.id)) return cache.unavailableGuilds.delete(guild.id) - return eventHandlers.guildCreate?.(guild) + const guild = createGuild(data.d as CreateGuildPayload); + handleInternalGuildCreate(guild); + if (cache.unavailableGuilds.get(guild.id)) { + return cache.unavailableGuilds.delete(guild.id); + } + return eventHandlers.guildCreate?.(guild); } if (data.t === "GUILD_UPDATE") { - const options = data.d as CreateGuildPayload - const cachedGuild = cache.guilds.get(options.id) - const guild = createGuild(options) - handleInternalGuildUpdate(guild) - if (!cachedGuild) return + const options = data.d as CreateGuildPayload; + const cachedGuild = cache.guilds.get(options.id); + const guild = createGuild(options); + handleInternalGuildUpdate(guild); + if (!cachedGuild) return; - return eventHandlers.guildUpdate?.(guild, cachedGuild) + return eventHandlers.guildUpdate?.(guild, cachedGuild); } if (data.t === "GUILD_DELETE") { - const options = data.d as GuildDeletePayload - const guild = cache.guilds.get(options.id) - if (!guild) return + const options = data.d as GuildDeletePayload; + const guild = cache.guilds.get(options.id); + if (!guild) return; - guild.channels.forEach((channel) => cache.channels.delete(channel.id)) - if (options.unavailable) return cache.unavailableGuilds.set(options.id, Date.now()) + guild.channels.forEach((channel) => cache.channels.delete(channel.id)); + if (options.unavailable) { + return cache.unavailableGuilds.set(options.id, Date.now()); + } - handleInternalGuildDelete(guild) - return eventHandlers.guildDelete?.(guild) + handleInternalGuildDelete(guild); + return eventHandlers.guildDelete?.(guild); } if (data.t && ["GUILD_BAN_ADD", "GUILD_BAN_REMOVE"].includes(data.t)) { - const options = data.d as GuildBanPayload - const guild = cache.guilds.get(options.guild_id) - if (!guild) return + const options = data.d as GuildBanPayload; + const guild = cache.guilds.get(options.guild_id); + if (!guild) return; - const user = createUser(options.user) + const user = createUser(options.user); return data.t === "GUILD_BAN_ADD" ? eventHandlers.guildBanAdd?.(guild, user) - : eventHandlers.guildBanRemove?.(guild, user) + : eventHandlers.guildBanRemove?.(guild, user); } if (data.t === "GUILD_EMOJIS_UPDATE") { - const options = data.d as GuildEmojisUpdatePayload - const guild = cache.guilds.get(options.guild_id) - if (!guild) return + const options = data.d as GuildEmojisUpdatePayload; + const guild = cache.guilds.get(options.guild_id); + if (!guild) return; - const cachedEmojis = guild.emojis - guild.emojis = options.emojis + const cachedEmojis = guild.emojis; + guild.emojis = options.emojis; - return eventHandlers.guildEmojisUpdate?.(guild, options.emojis, cachedEmojis) + return eventHandlers.guildEmojisUpdate?.( + guild, + options.emojis, + cachedEmojis, + ); } if (data.t === "GUILD_MEMBER_ADD") { - const options = data.d as GuildMemberAddPayload - const guild = cache.guilds.get(options.guild_id) - if (!guild) return + const options = data.d as GuildMemberAddPayload; + const guild = cache.guilds.get(options.guild_id); + if (!guild) return; - const memberCount = guild.memberCount + 1 - guild.memberCount = memberCount + const memberCount = guild.memberCount + 1; + guild.memberCount = memberCount; const member = createMember( options, options.guild_id, [...guild.roles.values()].map((role) => role.raw), - guild.owner_id - ) - guild.members.set(options.user.id, member) + guild.owner_id, + ); + guild.members.set(options.user.id, member); - return eventHandlers.guildMemberAdd?.(guild, member) + return eventHandlers.guildMemberAdd?.(guild, member); } if (data.t === "GUILD_MEMBER_REMOVE") { - const options = data.d as GuildBanPayload - const guild = cache.guilds.get(options.guild_id) - if (!guild) return + const options = data.d as GuildBanPayload; + const guild = cache.guilds.get(options.guild_id); + if (!guild) return; - const memberCount = guild.memberCount - 1 - guild.memberCount = memberCount + const memberCount = guild.memberCount - 1; + guild.memberCount = memberCount; - const member = guild.members.get(options.user.id) - return eventHandlers.guildMemberRemove?.(guild, member || createUser(options.user)) + const member = guild.members.get(options.user.id); + return eventHandlers.guildMemberRemove?.( + guild, + member || createUser(options.user), + ); } if (data.t === "GUILD_MEMBER_UPDATE") { - const options = data.d as GuildMemberUpdatePayload - const guild = cache.guilds.get(options.guild_id) - if (!guild) return + const options = data.d as GuildMemberUpdatePayload; + const guild = cache.guilds.get(options.guild_id); + if (!guild) return; - const cachedMember = guild.members.get(options.user.id) + const cachedMember = guild.members.get(options.user.id); const newMemberData = { ...options, premium_since: options.premium_since || undefined, - joined_at: new Date(cachedMember?.joined_at || Date.now()).toISOString(), + joined_at: new Date(cachedMember?.joined_at || Date.now()) + .toISOString(), deaf: cachedMember?.deaf || false, mute: cachedMember?.mute || false, - } + }; const member = createMember( newMemberData, options.guild_id, [...guild.roles.values()].map((r) => r.raw), - guild.owner_id - ) - guild.members.set(options.user.id, member) + guild.owner_id, + ); + guild.members.set(options.user.id, member); - if (cachedMember?.nick !== options.nick) - eventHandlers.nicknameUpdate?.(guild, member, options.nick, cachedMember?.nick) - const roleIDs = cachedMember?.roles || [] + if (cachedMember?.nick !== options.nick) { + eventHandlers.nicknameUpdate?.( + guild, + member, + options.nick, + cachedMember?.nick, + ); + } + const roleIDs = cachedMember?.roles || []; roleIDs.forEach((id) => { - if (!options.roles.includes(id)) eventHandlers.role_lost?.(guild, member, id) - }) + if (!options.roles.includes(id)) { + eventHandlers.role_lost?.(guild, member, id); + } + }); options.roles.forEach((id) => { - if (!roleIDs.includes(id)) eventHandlers.role_gained?.(guild, member, id) - }) + if (!roleIDs.includes(id)) { + eventHandlers.role_gained?.(guild, member, id); + } + }); - return eventHandlers.guild_member_update?.(guild, member, cachedMember) + return eventHandlers.guild_member_update?.(guild, member, cachedMember); } if (data.t === "GUILD_MEMBERS_CHUNK") { - const options = data.d as GuildMemberChunkPayload - const guild = cache.guilds.get(options.guild_id) - if (!guild) return + const options = data.d as GuildMemberChunkPayload; + const guild = cache.guilds.get(options.guild_id); + if (!guild) return; options.members.forEach((member) => guild.members.set( @@ -212,44 +276,48 @@ export function handleDiscordPayload(data: DiscordPayload, socket: WebSocket, re member, options.guild_id, [...guild.roles.values()].map((r) => r.raw), - guild.owner_id - ) + guild.owner_id, + ), ) - ) + ); } - if (data.t && ["GUILD_ROLE_CREATE", "GUILD_ROLE_DELETE", "GUILD_ROLE_UPDATE"].includes(data.t)) { - const options = data.d as GuildRolePayload - const guild = cache.guilds.get(options.guild_id) - if (!guild) return + if ( + data.t && + ["GUILD_ROLE_CREATE", "GUILD_ROLE_DELETE", "GUILD_ROLE_UPDATE"] + .includes(data.t) + ) { + const options = data.d as GuildRolePayload; + const guild = cache.guilds.get(options.guild_id); + if (!guild) return; if (data.t === "GUILD_ROLE_CREATE") { - const role = createRole(options.role) - const roles = guild.roles.set(options.role.id, role) - guild.roles = roles - return eventHandlers.roleCreate?.(guild, role) + const role = createRole(options.role); + const roles = guild.roles.set(options.role.id, role); + guild.roles = roles; + return eventHandlers.roleCreate?.(guild, role); } - const cached_role = guild.roles.get(options.role.id) - if (!cached_role) return + const cached_role = guild.roles.get(options.role.id); + if (!cached_role) return; if (data.t === "GUILD_ROLE_DELETE") { - const roles = guild.roles - roles.delete(options.role.id) - guild.roles = roles - return eventHandlers.roleDelete?.(guild, cached_role) + const roles = guild.roles; + roles.delete(options.role.id); + guild.roles = roles; + return eventHandlers.roleDelete?.(guild, cached_role); } if (data.t === "GUILD_ROLE_UPDATE") { - const role = createRole(options.role) - return eventHandlers.roleUpdate?.(guild, role, cached_role) + const role = createRole(options.role); + return eventHandlers.roleUpdate?.(guild, role, cached_role); } } if (data.t === "MESSAGE_CREATE") { - const options = data.d as MessageCreateOptions - const channel = cache.channels.get(options.channel_id) - const message = createMessage(options) + const options = data.d as MessageCreateOptions; + const channel = cache.channels.get(options.channel_id); + const message = createMessage(options); if (channel) { // channel.last_message_id = () => options.id @@ -257,103 +325,131 @@ export function handleDiscordPayload(data: DiscordPayload, socket: WebSocket, re // TODO: LIMIT THIS TO 100 messages // } } - return eventHandlers.messageCreate?.(message) + 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 + 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 + const channel = cache.channels.get(options.channel_id); + if (!channel) return; deletedMessages.forEach((id) => { - console.log(id) + console.log(id); // const message = channel.messages().get(id) // if (message) { // // TODO: update the messages cache // } // return eventHandlers.message_delete?.(message || { id, channel }) - }) + }); } if (data.t === "MESSAGE_UPDATE") { - const options = data.d as MessageUpdatePayload - const channel = cache.channels.get(options.channel_id) - if (!channel) return + const options = data.d as MessageUpdatePayload; + const channel = cache.channels.get(options.channel_id); + if (!channel) return; // const cachedMessage = channel.messages().get(options.id) // return eventHandlers.message_update?.(message, cachedMessage) } - 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 ( + 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 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 { + (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] + }; + message.reactions = message.reactions + ? [...message.reactions, newReaction] + : [newReaction]; } - cache.messages.set(options.message_id, message) + cache.messages.set(options.message_id, message); } return isAdd - ? eventHandlers.reactionAdd?.(message || options, options.emoji, options.user_id) - : eventHandlers.reactionRemove?.(message || options, options.emoji, options.user_id) + ? eventHandlers.reactionAdd?.( + message || options, + options.emoji, + options.user_id, + ) + : eventHandlers.reactionRemove?.( + message || options, + options.emoji, + options.user_id, + ); } if (data.t === "MESSAGE_REACTION_REMOVE_ALL") { - return eventHandlers.reactionRemoveAll?.(data.d as BaseMessageReactionPayload) + return eventHandlers.reactionRemoveAll?.( + data.d as BaseMessageReactionPayload, + ); } if (data.t === "MESSAGE_REACTION_REMOVE_EMOJI") { - return eventHandlers.reactionRemoveEmoji?.(data.d as MessageReactionRemoveEmojiPayload) + return eventHandlers.reactionRemoveEmoji?.( + data.d as MessageReactionRemoveEmojiPayload, + ); } if (data.t === "PRESENCE_UPDATE") { - return eventHandlers.presenceUpdate?.(data.d as PresenceUpdatePayload) + return eventHandlers.presenceUpdate?.(data.d as PresenceUpdatePayload); } if (data.t === "TYPING_START") { - return eventHandlers.typingStart?.(data.d as TypingStartPayload) + return eventHandlers.typingStart?.(data.d as TypingStartPayload); } if (data.t === "USER_UPDATE") { - const userData = data.d as UserPayload - const cachedUser = cache.users.get(botID) - const user = createUser(userData) - cache.users.set(userData.id, user) - return eventHandlers.botUpdate?.(user, cachedUser) + const userData = data.d as UserPayload; + const cachedUser = cache.users.get(botID); + const user = createUser(userData); + cache.users.set(userData.id, user); + return eventHandlers.botUpdate?.(user, cachedUser); } if (data.t === "VOICE_STATE_UPDATE") { - const payload = data.d as VoiceStateUpdatePayload - if (!payload.guild_id) return + const payload = data.d as VoiceStateUpdatePayload; + if (!payload.guild_id) return; - const guild = cache.guilds.get(payload.guild_id) - if (!guild) return + const guild = cache.guilds.get(payload.guild_id); + if (!guild) return; - const member = guild.members.get(payload.user_id) - if (!member) return + const member = guild.members.get(payload.user_id); + if (!member) return; - const cached_state = guild.voice_states.find((state) => state.user_id === payload.user_id) + const cached_state = guild.voice_states.find((state) => + state.user_id === payload.user_id + ); // No cached state before so lets make one for em if (!cached_state) { - guild.voice_states = [...guild.voice_states, payload] - return + guild.voice_states = [...guild.voice_states, payload]; + return; } if (cached_state.channel_id !== payload.channel_id) { @@ -361,33 +457,39 @@ export function handleDiscordPayload(data: DiscordPayload, socket: WebSocket, re if (payload.channel_id) { cached_state.channel_id ? // Was in a channel before - eventHandlers.voiceChannelSwitch?.(member, payload.channel_id, cached_state.channel_id) + eventHandlers.voiceChannelSwitch?.( + member, + payload.channel_id, + cached_state.channel_id, + ) : // Was not in a channel before so user just joined - eventHandlers.voiceChannelJoin?.(member, payload.channel_id) - } - // Left the channel + eventHandlers.voiceChannelJoin?.(member, payload.channel_id); + } // Left the channel else if (cached_state.channel_id) { - eventHandlers.voiceChannelLeave?.(member, cached_state.channel_id) + eventHandlers.voiceChannelLeave?.(member, cached_state.channel_id); } } - return eventHandlers.voiceStateUpdate?.(member, payload) + 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) + const options = data.d as WebhookUpdatePayload; + return eventHandlers.webhooksUpdate?.( + options.channel_id, + options.guild_id, + ); } - return + return; default: - return + return; } } export async function resumeConnection(payload: object, socket: WebSocket) { return setInterval(async () => { - socket = await connectWebSocket(botGatewayData.url) + socket = await connectWebSocket(botGatewayData.url); await socket.send( JSON.stringify({ op: GatewayOpcode.Resume, @@ -396,7 +498,7 @@ export async function resumeConnection(payload: object, socket: WebSocket) { session_id: sessionID, seq: previousSequenceNumber, }, - }) - ) - }, 1000 * 15) + }), + ); + }, 1000 * 15); } diff --git a/utils/logger.ts b/utils/logger.ts index 8b8748698..c9d000195 100644 --- a/utils/logger.ts +++ b/utils/logger.ts @@ -1,32 +1,39 @@ -import { blue, green, red, yellow } from "https://deno.land/std@v1.0.0-rc1/fmt/colors.ts" +import { + blue, + green, + red, + yellow, +} from "https://deno.land/std@0.50.0/fmt/colors.ts"; export const getTime = () => { - const now = new Date() - const hours = now.getHours() - const minute = now.getMinutes() + const now = new Date(); + const hours = now.getHours(); + const minute = now.getMinutes(); - let hour = hours - let amOrPm = `AM` + let hour = hours; + let amOrPm = `AM`; if (hour > 12) { - amOrPm = `PM` - hour = hour - 12 + amOrPm = `PM`; + hour = hour - 12; } - return `${hour >= 10 ? hour : `0${hour}`}:${minute >= 10 ? minute : `0${minute}`} ${amOrPm}` -} + return `${hour >= 10 ? hour : `0${hour}`}:${ + minute >= 10 ? minute : `0${minute}` + } ${amOrPm}`; +}; export const logGreen = (text: unknown) => { - console.log(green(`[${getTime()}] => ${JSON.stringify(text)}`)) -} + console.log(green(`[${getTime()}] => ${JSON.stringify(text)}`)); +}; export const logBlue = (text: unknown) => { - console.log(blue(`[${getTime()}] => ${JSON.stringify(text)}`)) -} + console.log(blue(`[${getTime()}] => ${JSON.stringify(text)}`)); +}; export const logRed = (text: unknown) => { - console.log(red(`[${getTime()}] => ${JSON.stringify(text)}`)) -} + console.log(red(`[${getTime()}] => ${JSON.stringify(text)}`)); +}; export const logYellow = (text: unknown) => { - console.log(yellow(`[${getTime()}] => ${JSON.stringify(text)}`)) -} + console.log(yellow(`[${getTime()}] => ${JSON.stringify(text)}`)); +};