diff --git a/README.md b/README.md index 1118f081b..2b886ca89 100644 --- a/README.md +++ b/README.md @@ -62,18 +62,18 @@ This section will list out all the available methods and functionality in the li ## Events ```ts -.botUpdate(user, cachedUser) +.botUpdate(userData) .channelCreate(channel) .channelUpdate(channel, cachedChannel) .channelDelete(channel) -.guildBanAdd(guild, user) -.guildBanRemove(guild, user) +.guildBanAdd(guild, memberOrUserData) +.guildBanRemove(guild, memberOrUserData) .guildCreate(guild) .guildUpdate(guild, cachedGuild) .guildDelete(guild) .guildEmojisUpdate(guild, emojis, cachedEmojis) .guildMemberAdd(guild, member) -.guildMemberRemove(guild, member) +.guildMemberRemove(guild, memberOrUserData) .guildMemberUpdate(guild, member, cachedMember) .heartbeat() .messageCreate(message) @@ -286,27 +286,6 @@ This section will list out all the available methods and functionality in the li - mentionable - mention -## User - -- id -- username -- discriminator -- avatar -- bot -- system -- mfaEnabled -- locale -- verified -- email -- flags -- premiumType -- mention -- tag -```ts -- .avatarURL(size, format) -- .sendMessage(content) -``` - ## Utils ```ts diff --git a/module/shard.ts b/module/shard.ts index 31c1c9731..8677e21ed 100644 --- a/module/shard.ts +++ b/module/shard.ts @@ -9,7 +9,7 @@ import { DiscordHeartbeatPayload, ReadyPayload, } from "../types/discord.ts"; -import { logRed } from "../utils/logger.ts"; +import { logRed, logBlue } from "../utils/logger.ts"; import { FetchMembersOptions } from "../types/guild.ts"; import { delay } from "https://deno.land/std@0.50.0/async/delay.ts"; @@ -21,6 +21,45 @@ let sessionID = ""; // Discord requests null if no number has yet been sent by discord let previousSequenceNumber: number | null = null; let needToResume = false; +let shardID = 0; + +const RequestMembersQueue: RequestMemberQueuedRequest[] = []; +let processQueue = false; + +interface RequestMemberQueuedRequest { + guildID: string; + nonce: string; + options?: FetchMembersOptions; +} + +async function processRequestMembersQueue() { + if (!RequestMembersQueue.length) { + processQueue = false; + return; + } + + // 2 events per second is the rate limit. + const request = RequestMembersQueue.shift(); + if (request) { + requestGuildMembers(request.guildID, request.nonce, request.options, true); + + const secondRequest = RequestMembersQueue.shift(); + if (secondRequest) { + requestGuildMembers( + secondRequest.guildID, + secondRequest.nonce, + secondRequest.options, + true, + ); + } + } + + await delay(1500); + logBlue( + `There are still ${RequestMembersQueue.length} requests in queue on shard ${shardID}`, + ); + processRequestMembersQueue(); +} // 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. async function sendConstantHeartbeats( @@ -107,14 +146,16 @@ const createShard = async ( type: "HANDLE_DISCORD_PAYLOAD", payload: message, resumeInterval, + shardID, }, ); break; } } else if (isWebSocketCloseEvent(message)) { - logRed(`Close :( ${JSON.stringify(message)}`); + logRed(`Closeing: ${JSON.stringify(message)}`); // These error codes should just crash the projects if ([4004, 4005, 4012, 4013, 4014].includes(message.code)) { + logRed(`Close :( ${JSON.stringify(message)}`); throw new Error( "Shard.ts: Error occurred that is not resumeable or able to be reconnected.", ); @@ -134,7 +175,29 @@ function requestGuildMembers( guildID: string, nonce: string, options?: FetchMembersOptions, + queuedRequest = false, ) { + // This request was not from this queue so we add it to queue first + if (!queuedRequest) { + RequestMembersQueue.push({ + guildID, + nonce, + options, + }); + + if (!processQueue) { + processQueue = true; + processRequestMembersQueue(); + } + return; + } + + // If its closed add back to queue to redo on resume + if (shardSocket.isClosed) { + requestGuildMembers(guildID, nonce, options); + return; + } + shardSocket.send(JSON.stringify({ op: GatewayOpcode.RequestGuildMembers, d: { @@ -158,6 +221,7 @@ onmessage = (message: MessageEvent) => { message.data.botGatewayData, message.data.identifyPayload, ); + shardID = message.data.shardID; } if (message.data.type === "FETCH_MEMBERS") { diff --git a/module/shardingManager.ts b/module/shardingManager.ts index 27aee7f77..87c430902 100644 --- a/module/shardingManager.ts +++ b/module/shardingManager.ts @@ -20,7 +20,7 @@ import { handleInternalChannelDelete, } from "../events/channels.ts"; import { ChannelCreatePayload } from "../types/channel.ts"; -import { createGuild } from "../structures/guild.ts"; +import { createGuild, Guild } from "../structures/guild.ts"; import { CreateGuildPayload, GuildDeletePayload, @@ -40,20 +40,17 @@ import { handleInternalGuildDelete, } from "../events/guilds.ts"; import { cache } from "../utils/cache.ts"; -import { createUser } from "../structures/user.ts"; import { createMember } from "../structures/member.ts"; import { createRole } from "../structures/role.ts"; import { MessageCreateOptions, MessageDeletePayload, MessageDeleteBulkPayload, - MessageUpdatePayload, MessageReactionPayload, BaseMessageReactionPayload, MessageReactionRemoveEmojiPayload, } from "../types/message.ts"; import { createMessage } from "../structures/message.ts"; -import { logGreen, logBlue } from "../utils/logger.ts"; import { GuildUpdateChange } from "../types/options.ts"; let shardCounter = 0; @@ -66,7 +63,7 @@ export interface FetchAllMembersRequest { const fetchAllMembersProcessingRequests = new Map< string, - FetchAllMembersRequest + Function >(); const shards: Worker[] = []; let createNextShard = true; @@ -77,7 +74,7 @@ export function createShardWorker(shardID?: number) { shard.onmessage = (message) => { if (message.data.type === "REQUEST_CLIENT_OPTIONS") { identifyPayload.shard = [ - shardID || shardCounter++, + shardID || shardCounter, botGatewayData.shards, ]; @@ -86,10 +83,16 @@ export function createShardWorker(shardID?: number) { type: "CREATE_SHARD", botGatewayData, identifyPayload, + shardID: shardCounter, }, ); + // Update the shard counter + shardCounter++; } else if (message.data.type === "HANDLE_DISCORD_PAYLOAD") { - handleDiscordPayload(JSON.parse(message.data.payload)); + handleDiscordPayload( + JSON.parse(message.data.payload), + message.data.shardID, + ); } }; shards.push(shard); @@ -112,7 +115,7 @@ export const spawnShards = async ( } }; -async function handleDiscordPayload(data: DiscordPayload) { +async function handleDiscordPayload(data: DiscordPayload, shardID: number) { eventHandlers.raw?.(data); switch (data.op) { @@ -140,11 +143,18 @@ async function handleDiscordPayload(data: DiscordPayload) { } 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); + const options = data.d as CreateGuildPayload; + // When shards resume they emit GUILD_CREATE again. + if (cache.guilds.has(options.id)) { + return; } + + const guild = createGuild(data.d as CreateGuildPayload, shardID); + handleInternalGuildCreate(guild); + if (cache.unavailableGuilds.get(options.id)) { + cache.unavailableGuilds.delete(options.id); + } + return eventHandlers.guildCreate?.(guild); } @@ -207,10 +217,11 @@ async function handleDiscordPayload(data: DiscordPayload) { const guild = cache.guilds.get(options.guild_id); if (!guild) return; - const user = createUser(options.user); + const member = guild.members.get(options.user.id); + return data.t === "GUILD_BAN_ADD" - ? eventHandlers.guildBanAdd?.(guild, user) - : eventHandlers.guildBanRemove?.(guild, user); + ? eventHandlers.guildBanAdd?.(guild, member || options.user) + : eventHandlers.guildBanRemove?.(guild, member || options.user); } if (data.t === "GUILD_EMOJIS_UPDATE") { @@ -240,7 +251,6 @@ async function handleDiscordPayload(data: DiscordPayload) { guild, ); guild.members.set(options.user.id, member); - cache.users.set(options.user.id, createUser(member.user)); return eventHandlers.guildMemberAdd?.(guild, member); } @@ -256,7 +266,7 @@ async function handleDiscordPayload(data: DiscordPayload) { const member = guild.members.get(options.user.id); return eventHandlers.guildMemberRemove?.( guild, - member || createUser(options.user), + member || options.user, ); } @@ -280,7 +290,6 @@ async function handleDiscordPayload(data: DiscordPayload) { guild, ); guild.members.set(options.user.id, member); - cache.users.set(options.user.id, createUser(member.user)); if (cachedMember?.nick !== options.nick) { eventHandlers.nicknameUpdate?.( @@ -320,19 +329,18 @@ async function handleDiscordPayload(data: DiscordPayload) { guild, ), ); - cache.users.set(member.user.id, createUser(member.user)); }); // Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming if ( options.nonce ) { - const request = fetchAllMembersProcessingRequests.get(options.nonce); - if (!request) return; + const resolve = fetchAllMembersProcessingRequests.get(options.nonce); + if (!resolve) return; if (options.chunk_index + 1 === options.chunk_count) { fetchAllMembersProcessingRequests.delete(options.nonce); - request.resolve(); + resolve(); } } } @@ -377,9 +385,6 @@ async function handleDiscordPayload(data: DiscordPayload) { const channel = cache.channels.get(options.channel_id); if (channel) channel.last_message_id = options.id; - // Cache the message author themself - cache.users.set(options.author.id, createUser(options.author)); - const message = createMessage(options); // Cache the message cache.messages.set(options.id, message); @@ -399,8 +404,6 @@ async function handleDiscordPayload(data: DiscordPayload) { } options.mentions.forEach((mention) => { - // For each mention cache the user - cache.users.set(mention.id, createUser(mention)); // Cache the member if its a valid member if (mention.member) { guild?.members.set( @@ -433,12 +436,31 @@ async function handleDiscordPayload(data: DiscordPayload) { } if (data.t === "MESSAGE_UPDATE") { - const options = data.d as MessageUpdatePayload; + const options = data.d as MessageCreateOptions; const channel = cache.channels.get(options.channel_id); if (!channel) return; - // const cachedMessage = channel.messages().get(options.id) - // return eventHandlers.message_update?.(message, cachedMessage) + 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 ( @@ -476,17 +498,13 @@ async function handleDiscordPayload(data: DiscordPayload) { if (options.member && options.guild_id) { const guild = cache.guilds.get(options.guild_id); - if (guild) { - const member = createMember( + guild?.members.set( + options.member.user.id, + createMember( options.member, guild, - ); - guild.members.set( - options.member.user.id, - member, - ); - cache.users.set(options.member.user.id, createUser(member.user)); - } + ), + ); } return isAdd @@ -524,10 +542,19 @@ async function handleDiscordPayload(data: DiscordPayload) { 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); + + 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") { @@ -537,7 +564,8 @@ async function handleDiscordPayload(data: DiscordPayload) { const guild = cache.guilds.get(payload.guild_id); if (!guild) return; - const member = guild.members.get(payload.user_id); + const member = guild.members.get(payload.user_id) || + (payload.member ? createMember(payload.member, guild) : undefined); if (!member) return; // No cached state before so lets make one for em @@ -592,31 +620,16 @@ async function handleDiscordPayload(data: DiscordPayload) { } export async function requestAllMembers( - guildID: string, + guild: Guild, resolve: Function, - memberCount: number, options?: FetchMembersOptions, ) { - if (fetchAllMembersProcessingRequests.size >= 5) { - await delay(1000); - requestAllMembers(guildID, resolve, memberCount, options); - return; - } - - const payload = { - resolve, - requestedMax: options?.query - ? 100 - : options?.userIDs?.length || options?.limit || memberCount, - receivedAmount: 0, - }; - const nonce = Math.random().toString(); - fetchAllMembersProcessingRequests.set(nonce, payload); + fetchAllMembersProcessingRequests.set(nonce, resolve); - return shards[0].postMessage({ + shards[guild.shardID].postMessage({ type: "FETCH_MEMBERS", - guildID, + guildID: guild.id, nonce, options, }); diff --git a/structures/guild.ts b/structures/guild.ts index aa5183be1..dd0c95287 100644 --- a/structures/guild.ts +++ b/structures/guild.ts @@ -31,9 +31,11 @@ import { RoleData } from "../types/role.ts"; import { Intents } from "../types/options.ts"; import { requestAllMembers } from "../module/shardingManager.ts"; -export const createGuild = (data: CreateGuildPayload) => { +export const createGuild = (data: CreateGuildPayload, shardID: number) => { const guild = { ...data, + /** The shard id that this guild is on */ + shardID, /** The owner id of the guild. */ ownerID: data.owner_id, /** The afk channel id for this guild. */ @@ -293,7 +295,7 @@ export const createGuild = (data: CreateGuildPayload) => { } return new Promise((resolve) => { - requestAllMembers(data.id, resolve, guild.memberCount, options); + requestAllMembers(guild, resolve, options); }); }, /** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */ diff --git a/structures/member.ts b/structures/member.ts index 82c314ca3..eb21e6819 100644 --- a/structures/member.ts +++ b/structures/member.ts @@ -14,145 +14,163 @@ import { RequestManager } from "../module/requestManager.ts"; import { botID } from "../module/client.ts"; import { Guild } from "./guild.ts"; import { cache } from "../utils/cache.ts"; -import { createUser } from "./user.ts"; +import { MessageContent, DMChannelCreatePayload } from "../types/channel.ts"; +import { createChannel } from "./channel.ts"; -export const createMember = (data: MemberCreatePayload, guild: Guild) => { - // Add the user to cache as well - cache.users.set(data.user.id, createUser(data.user)); +export const createMember = (data: MemberCreatePayload, guild: Guild) => ({ + ...data, + /** When the user joined the guild */ + joinedAt: Date.parse(data.joined_at), + /** When the user used their nitro boost on the server. */ + premiumSince: data.premium_since ? Date.parse(data.premium_since) : undefined, + /** The full username#discriminator */ + tag: `${data.user.username}#${data.user.discriminator}`, + /** The user mention with nickname if possible */ + mention: `<@!${data.user.id}>`, + /** The guild id where this member exists */ + guildID: guild.id, + /** Whether or not this user has 2FA enabled. */ + mfaEnabled: data.user.mfa_enabled, + /** The premium type for this user */ + premiumType: data.user.premium_type, - return { - ...data, - /** When the user joined the guild */ - joinedAt: Date.parse(data.joined_at), - /** When the user used their nitro boost on the server. */ - premiumSince: data.premium_since - ? Date.parse(data.premium_since) - : undefined, - /** The full username#discriminator */ - tag: `${data.user.username}#${data.user.discriminator}`, - /** The user mention with nickname if possible */ - mention: `<@!${data.user.id}>`, - /** The guild id where this member exists */ - guildID: guild.id, + /** Gets the guild object from cache for this member. This is a method instead of a prop to preserve memory. */ + guild: () => cache.guilds.get(guild.id)!, + /** Send a message to a users DM. Note: this takes 2 API calls. 1 is to fetch the users dm channel. 2 is to send a message to that channel. */ + sendMessage: async function (content: string | MessageContent) { + let dmChannel = cache.channels.get(data.user.id); + if (!dmChannel) { + // If not available in cache create a new one. + const dmChannelData = await RequestManager.post( + endpoints.USER_CREATE_DM, + { recipient_id: data.user.id }, + ) as DMChannelCreatePayload; + // Channel create event will have added this channel to the cache + cache.channels.delete(dmChannelData.id); + const channel = createChannel(dmChannelData); + // Recreate the channel and add it undert he users id + cache.channels.set(data.user.id, channel); + dmChannel = channel; + } - /** Gets the guild object from cache for this member. This is a method instead of a prop to preserve memory. */ - guild: () => cache.guilds.get(guild.id)!, - /** The users custom avatar or the default avatar */ - avatarURL: (size: ImageSize = 128, format?: ImageFormats) => - data.user.avatar - ? formatImageURL( - endpoints.USER_AVATAR(data.user.id, data.user.avatar), - size, - format, - ) - : endpoints.USER_DEFAULT_AVATAR(Number(data.user.discriminator) % 5), - /** Add a role to the member */ - addRole: (roleID: string, reason?: string) => { - const botsHighestRole = highestRole(guild.id, botID); + // If it does exist try sending a message to this user + return dmChannel?.sendMessage(content); + }, + /** The users custom avatar or the default avatar */ + avatarURL: (size: ImageSize = 128, format?: ImageFormats) => + data.user.avatar + ? formatImageURL( + endpoints.USER_AVATAR(data.user.id, data.user.avatar), + size, + format, + ) + : endpoints.USER_DEFAULT_AVATAR(Number(data.user.discriminator) % 5), + /** Add a role to the member */ + addRole: (roleID: string, reason?: string) => { + const botsHighestRole = highestRole(guild.id, botID); + if ( + botsHighestRole && + !higherRolePosition(guild.id, botsHighestRole.id, roleID) + ) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + } + + if (!botHasPermission(guild.id, [Permissions.MANAGE_ROLES])) { + throw new Error(Errors.MISSING_MANAGE_ROLES); + } + + return RequestManager.put( + endpoints.GUILD_MEMBER_ROLE(guild.id, data.user.id, roleID), + { reason }, + ); + }, + /** Remove a role from the member */ + removeRole: (roleID: string, reason?: string) => { + const botsHighestRole = highestRole(guild.id, botID); + if ( + botsHighestRole && + !higherRolePosition(guild.id, botsHighestRole.id, roleID) + ) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + } + + if (!botHasPermission(guild.id, [Permissions.MANAGE_ROLES])) { + throw new Error(Errors.MISSING_MANAGE_ROLES); + } + return RequestManager.delete( + endpoints.GUILD_MEMBER_ROLE(guild.id, data.user.id, roleID), + { reason }, + ); + }, + /** Kick a member from the server */ + kick: (reason?: string) => { + const botsHighestRole = highestRole(guild.id, botID); + const membersHighestRole = highestRole(guild.id, data.user.id); + if ( + botsHighestRole && membersHighestRole && + botsHighestRole.position <= membersHighestRole.position + ) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + } + + if (!botHasPermission(guild.id, [Permissions.KICK_MEMBERS])) { + throw new Error(Errors.MISSING_KICK_MEMBERS); + } + return RequestManager.delete( + endpoints.GUILD_MEMBER(guild.id, data.user.id), + { reason }, + ); + }, + /** Edit the member */ + edit: (options: EditMemberOptions) => { + if (options.nick) { + if (options.nick.length > 32) { + throw new Error(Errors.NICKNAMES_MAX_LENGTH); + } + if (!botHasPermission(guild.id, [Permissions.MANAGE_NICKNAMES])) { + throw new Error(Errors.MISSING_MANAGE_NICKNAMES); + } + } + + if ( + options.roles && + !botHasPermission(guild.id, [Permissions.MANAGE_ROLES]) + ) { + throw new Error(Errors.MISSING_MANAGE_ROLES); + } + + if (options.mute) { + // TODO: This should check if the member is in a voice channel if ( - botsHighestRole && - !higherRolePosition(guild.id, botsHighestRole.id, roleID) + !botHasPermission(guild.id, [Permissions.MUTE_MEMBERS]) ) { - throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + throw new Error(Errors.MISSING_MUTE_MEMBERS); } + } - if (!botHasPermission(guild.id, [Permissions.MANAGE_ROLES])) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } + if ( + options.deaf && + !botHasPermission(guild.id, [Permissions.DEAFEN_MEMBERS]) + ) { + throw new Error(Errors.MISSING_DEAFEN_MEMBERS); + } - return RequestManager.put( - endpoints.GUILD_MEMBER_ROLE(guild.id, data.user.id, roleID), - { reason }, - ); - }, - /** Remove a role from the member */ - removeRole: (roleID: string, reason?: string) => { - const botsHighestRole = highestRole(guild.id, botID); - if ( - botsHighestRole && - !higherRolePosition(guild.id, botsHighestRole.id, roleID) - ) { - throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); - } + // TODO: if channel id is provided check if the bot has CONNECT and MOVE in channel and current channel - if (!botHasPermission(guild.id, [Permissions.MANAGE_ROLES])) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - return RequestManager.delete( - endpoints.GUILD_MEMBER_ROLE(guild.id, data.user.id, roleID), - { reason }, - ); - }, - /** Kick a member from the server */ - kick: (reason?: string) => { - const botsHighestRole = highestRole(guild.id, botID); - const membersHighestRole = highestRole(guild.id, data.user.id); - if ( - botsHighestRole && membersHighestRole && - botsHighestRole.position <= membersHighestRole.position - ) { - throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); - } - - if (!botHasPermission(guild.id, [Permissions.KICK_MEMBERS])) { - throw new Error(Errors.MISSING_KICK_MEMBERS); - } - return RequestManager.delete( - endpoints.GUILD_MEMBER(guild.id, data.user.id), - { reason }, - ); - }, - /** Edit the member */ - edit: (options: EditMemberOptions) => { - if (options.nick) { - if (options.nick.length > 32) { - throw new Error(Errors.NICKNAMES_MAX_LENGTH); - } - if (!botHasPermission(guild.id, [Permissions.MANAGE_NICKNAMES])) { - throw new Error(Errors.MISSING_MANAGE_NICKNAMES); - } - } - - if ( - options.roles && - !botHasPermission(guild.id, [Permissions.MANAGE_ROLES]) - ) { - throw new Error(Errors.MISSING_MANAGE_ROLES); - } - - if (options.mute) { - // TODO: This should check if the member is in a voice channel - if ( - !botHasPermission(guild.id, [Permissions.MUTE_MEMBERS]) - ) { - throw new Error(Errors.MISSING_MUTE_MEMBERS); - } - } - - if ( - options.deaf && - !botHasPermission(guild.id, [Permissions.DEAFEN_MEMBERS]) - ) { - throw new Error(Errors.MISSING_DEAFEN_MEMBERS); - } - - // TODO: if channel id is provided check if the bot has CONNECT and MOVE in channel and current channel - - return RequestManager.patch( - endpoints.GUILD_MEMBER(guild.id, data.user.id), - options, - ); - }, - /** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */ - hasPermissions: (permissions: Permission[]) => { - return memberHasPermission( - data.user.id, - guild, - data.roles, - permissions, - ); - }, - }; -}; + return RequestManager.patch( + endpoints.GUILD_MEMBER(guild.id, data.user.id), + options, + ); + }, + /** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */ + hasPermissions: (permissions: Permission[]) => { + return memberHasPermission( + data.user.id, + guild, + data.roles, + permissions, + ); + }, +}); export interface Member extends ReturnType {} diff --git a/structures/message.ts b/structures/message.ts index 54a694b9f..e18935708 100644 --- a/structures/message.ts +++ b/structures/message.ts @@ -1,7 +1,6 @@ import { MessageCreateOptions } from "../types/message.ts"; import { endpoints } from "../constants/discord.ts"; import { MessageContent } from "../types/channel.ts"; -import { createUser } from "./user.ts"; import { UserPayload } from "../types/guild.ts"; import { botHasPermission } from "../utils/permissions.ts"; import { Errors } from "../types/errors.ts"; @@ -18,18 +17,16 @@ export function createMessage(data: MessageCreateOptions) { mentionsEveryone: data.mentions_everyone, mentionRoles: data.mention_roles, mentionChannels: data.mention_channels, - mentions: data.mentions.map((user) => cache.users.get(user.id)!), webhookID: data.webhook_id, messageReference: data.message_reference, - - author: cache.users.get(data.author.id)!, timestamp: Date.parse(data.timestamp), editedTimestamp: data.edited_timestamp - ? Date.parse(data.edited_timestamp) - : undefined, + ? Date.parse(data.edited_timestamp) + : undefined, channel: cache.channels.get(data.channel_id)!, guild: () => data.guild_id ? cache.guilds.get(data.guild_id) : undefined, member: () => message.guild()?.members.get(data.author.id)!, + mentions: () => data.mentions.map((mention) => message.guild()?.members.get(mention.id) || mention), /** Delete a message */ delete: (reason?: string) => { @@ -119,7 +116,11 @@ export function createMessage(data: MessageCreateOptions) { const result = (await RequestManager.get( endpoints.CHANNEL_MESSAGE_REACTION(data.channel_id, data.id, reaction), )) as UserPayload[]; - return result.map((res) => createUser(res)); + const guild = message.guild(); + + return result.map((res) => { + return guild?.members.get(res.id) || res; + }); }, /** Edit the message. */ edit: async (content: string | MessageContent) => { diff --git a/structures/user.ts b/structures/user.ts index b90ae9d30..2e5ec1379 100644 --- a/structures/user.ts +++ b/structures/user.ts @@ -1,57 +1,56 @@ -import { formatImageURL } from "../utils/cdn.ts"; -import { endpoints } from "../constants/discord.ts"; -import { ImageSize, ImageFormats } from "../types/cdn.ts"; -import { UserPayload } from "../types/guild.ts"; -import { RequestManager } from "../module/requestManager.ts"; -import { MessageContent, DMChannelCreatePayload } from "../types/channel.ts"; -import { cache } from "../utils/cache.ts"; -import { logRed, logYellow } from "../utils/logger.ts"; -import { createChannel } from "./channel.ts"; +// import { formatImageURL } from "../utils/cdn.ts"; +// import { endpoints } from "../constants/discord.ts"; +// import { ImageSize, ImageFormats } from "../types/cdn.ts"; +// import { UserPayload } from "../types/guild.ts"; +// import { RequestManager } from "../module/requestManager.ts"; +// import { MessageContent, DMChannelCreatePayload } from "../types/channel.ts"; +// import { cache } from "../utils/cache.ts"; +// import { createChannel } from "./channel.ts"; -export const enum PremiumType { - NitroClassic = 1, - Nitro, -} +// export const enum PremiumType { +// NitroClassic = 1, +// Nitro, +// } -export const createUser = (data: UserPayload) => ({ - ...data, - /** Whether or not this user has 2FA enabled. */ - mfaEnabled: data.mfa_enabled, - /** The premium type for this user */ - premiumType: data.premium_type, - /** This will return the mention for the user. */ - mention: `<@!${data.id}>`, - /** The tag for the user */ - tag: `${data.username}#${data.discriminator}`, - /** The full URL of the users avatar from Discords CDN. */ - avatarURL: (size: ImageSize = 128, format?: ImageFormats) => - data.avatar - ? formatImageURL( - endpoints.USER_AVATAR(data.id, data.avatar), - size, - format, - ) - : endpoints.USER_DEFAULT_AVATAR(Number(data.discriminator) % 5), - /** Send a message to a users DM. Note: this takes 2 API calls. 1 is to fetch the users dm channel. 2 is to send a message to that channel. */ - sendMessage: async function (content: string | MessageContent) { - let dmChannel = cache.channels.get(data.id); - if (!dmChannel) { - // If not available in cache create a new one. - const dmChannelData = await RequestManager.post( - endpoints.USER_CREATE_DM, - { recipient_id: data.id }, - ) as DMChannelCreatePayload; - // Channel create event will have added this channel to the cache - cache.channels.delete(dmChannelData.id); - const channel = createChannel(dmChannelData); - // Recreate the channel and add it undert he users id - cache.channels.set(data.id, channel); - dmChannel = channel; - } +// export const createUser = (data: UserPayload) => ({ +// ...data, +// /** Whether or not this user has 2FA enabled. */ +// mfaEnabled: data.mfa_enabled, +// /** The premium type for this user */ +// premiumType: data.premium_type, +// /** This will return the mention for the user. */ +// mention: `<@!${data.id}>`, +// /** The tag for the user */ +// tag: `${data.username}#${data.discriminator}`, +// /** The full URL of the users avatar from Discords CDN. */ +// avatarURL: (size: ImageSize = 128, format?: ImageFormats) => +// data.avatar +// ? formatImageURL( +// endpoints.USER_AVATAR(data.id, data.avatar), +// size, +// format, +// ) +// : endpoints.USER_DEFAULT_AVATAR(Number(data.discriminator) % 5), +// /** Send a message to a users DM. Note: this takes 2 API calls. 1 is to fetch the users dm channel. 2 is to send a message to that channel. */ +// sendMessage: async function (content: string | MessageContent) { +// let dmChannel = cache.channels.get(data.id); +// if (!dmChannel) { +// // If not available in cache create a new one. +// const dmChannelData = await RequestManager.post( +// endpoints.USER_CREATE_DM, +// { recipient_id: data.id }, +// ) as DMChannelCreatePayload; +// // Channel create event will have added this channel to the cache +// cache.channels.delete(dmChannelData.id); +// const channel = createChannel(dmChannelData); +// // Recreate the channel and add it undert he users id +// cache.channels.set(data.id, channel); +// dmChannel = channel; +// } - // If it does exist try sending a message to this user - return dmChannel?.sendMessage(content); - }, -}); +// // If it does exist try sending a message to this user +// return dmChannel?.sendMessage(content); +// }, +// }); -export interface User extends ReturnType {} +// export interface User extends ReturnType {} diff --git a/types/guild.ts b/types/guild.ts index 5a5247b2f..42bfd7489 100644 --- a/types/guild.ts +++ b/types/guild.ts @@ -1,5 +1,4 @@ import { Emoji, StatusType } from "./discord.ts"; -import { User } from "../structures/user.ts"; import { Permission } from "./permission.ts"; import { RoleData } from "./role.ts"; import { MemberCreatePayload } from "./member.ts"; @@ -204,7 +203,7 @@ export interface BannedUser { /** The reason for the ban */ reason?: string; /** The banned user object */ - user: User; + user: UserPayload; } export interface PositionSwap { diff --git a/types/message.ts b/types/message.ts index be7f38cc4..f18a940ce 100644 --- a/types/message.ts +++ b/types/message.ts @@ -1,10 +1,9 @@ import { ChannelType, UserPayload } from "./guild.ts"; -import { User } from "../structures/user.ts"; import { MemberCreatePayload } from "./member.ts"; import { Channel } from "../structures/channel.ts"; import { Member } from "../structures/member.ts"; -export interface MentionedUser extends User { +export interface MentionedUser extends UserPayload { member: Member; } @@ -209,7 +208,7 @@ export interface EmojiReaction { /** The roles this emoji is whitelisted to */ roles?: string[]; /** The user that created this emoji */ - user?: User; + user?: UserPayload; /** Whether this emoji must be wrapped in colons */ require_colons?: boolean; /** Whether this emoji is managed */ diff --git a/types/options.ts b/types/options.ts index 08d44a7cc..96393a15f 100644 --- a/types/options.ts +++ b/types/options.ts @@ -6,7 +6,6 @@ import { TypingStartPayload, VoiceStateUpdatePayload, } from "./discord.ts"; -import { User } from "../structures/user.ts"; import { Role } from "../structures/role.ts"; import { Message } from "../structures/message.ts"; import { @@ -21,6 +20,7 @@ import { import { Channel } from "../structures/channel.ts"; import { Guild } from "../structures/guild.ts"; import { Member } from "../structures/member.ts"; +import { UserPayload } from "./guild.ts"; export interface Fulfilled_Client_Options { token: string; @@ -54,12 +54,12 @@ export interface OldMessage { } export interface EventHandlers { - botUpdate?: (user: User, cachedUser?: User) => unknown; + botUpdate?: (user: UserPayload) => unknown; channelCreate?: (channel: Channel) => unknown; channelUpdate?: (channel: Channel, cachedChannel: Channel) => unknown; channelDelete?: (channel: Channel) => unknown; - guildBanAdd?: (guild: Guild, user: User) => unknown; - guildBanRemove?: (guild: Guild, user: User) => unknown; + guildBanAdd?: (guild: Guild, user: Member | UserPayload) => unknown; + guildBanRemove?: (guild: Guild, user: Member | UserPayload) => unknown; guildCreate?: (guild: Guild) => unknown; guildUpdate?: (guild: Guild, changes: GuildUpdateChange[]) => unknown; guildDelete?: (guild: Guild) => unknown; @@ -69,7 +69,7 @@ export interface EventHandlers { cachedEmojis: Emoji[], ) => unknown; guildMemberAdd?: (guild: Guild, member: Member) => unknown; - guildMemberRemove?: (guild: Guild, member: Member | User) => unknown; + guildMemberRemove?: (guild: Guild, member: Member | UserPayload) => unknown; guildMemberUpdate?: ( guild: Guild, member: Member, diff --git a/utils/cache.ts b/utils/cache.ts index c642d0808..c2d576b79 100644 --- a/utils/cache.ts +++ b/utils/cache.ts @@ -1,11 +1,9 @@ -import { User } from "../structures/user.ts"; import { Message } from "../structures/message.ts"; import { Guild } from "../structures/guild.ts"; import { Channel } from "../structures/channel.ts"; export interface CacheData { guilds: Map; - users: Map; channels: Map; messages: Map; unavailableGuilds: Map; @@ -13,7 +11,6 @@ export interface CacheData { export const cache: CacheData = { guilds: new Map(), - users: new Map(), channels: new Map(), messages: new Map(), unavailableGuilds: new Map(),