diff --git a/src/controllers/bans.ts b/src/controllers/bans.ts index fca5c7f6f..0d1c1cf81 100644 --- a/src/controllers/bans.ts +++ b/src/controllers/bans.ts @@ -10,7 +10,7 @@ export async function handleInternalGuildBanAdd(data: DiscordPayload) { const guild = await cacheHandlers.get("guilds", payload.guild_id); if (!guild) return; - const member = guild.members.get(payload.user.id); + const member = await cacheHandlers.get("members", payload.user.id); eventHandlers.guildBanAdd?.(guild, member || payload.user); } @@ -21,6 +21,6 @@ export async function handleInternalGuildBanRemove(data: DiscordPayload) { const guild = await cacheHandlers.get("guilds", payload.guild_id); if (!guild) return; - const member = guild.members.get(payload.user.id); + const member = await cacheHandlers.get("members", payload.user.id); eventHandlers.guildBanRemove?.(guild, member || payload.user); } diff --git a/src/controllers/cache.ts b/src/controllers/cache.ts index b7208334a..6577bef04 100644 --- a/src/controllers/cache.ts +++ b/src/controllers/cache.ts @@ -1,5 +1,6 @@ import { Channel } from "../structures/channel.ts"; import { Guild } from "../structures/guild.ts"; +import { Member } from "../structures/member.ts"; import { Message } from "../structures/message.ts"; import { PresenceUpdatePayload } from "../types/discord.ts"; import { cache } from "../utils/cache.ts"; @@ -7,10 +8,11 @@ import { Collection } from "../utils/collection.ts"; export type TableName = | "guilds" + | "unavailableGuilds" | "channels" | "messages" - | "presences" - | "unavailableGuilds"; + | "members" + | "presences"; function set( table: "guilds", @@ -27,6 +29,11 @@ function set( key: string, value: Message, ): Promise>; +function set( + table: "members", + key: string, + value: Member, +): Promise>; function set( table: "presences", key: string, @@ -44,6 +51,7 @@ async function set(table: TableName, key: string, value: any) { function get(table: "guilds", key: string): Promise; function get(table: "channels", key: string): Promise; function get(table: "messages", key: string): Promise; +function get(table: "members", key: string): Promise; function get( table: "presences", key: string, @@ -72,6 +80,10 @@ function forEach( table: "messages", callback: (value: Message, key: string, map: Map) => unknown, ): void; +function forEach( + table: "members", + callback: (value: Member, key: string, map: Map) => unknown, +): void; function forEach( table: TableName, callback: (value: any, key: string, map: Map) => unknown, @@ -82,20 +94,24 @@ function forEach( function filter( table: "guilds", callback: (value: Guild, key: string) => boolean, -): Collection; +): Promise>; function filter( table: "unavailableGuilds", callback: (value: Guild, key: string) => boolean, -): Collection; +): Promise>; function filter( table: "channels", callback: (value: Channel, key: string) => boolean, -): Collection; +): Promise>; function filter( table: "messages", callback: (value: Message, key: string) => boolean, -): Collection; +): Promise>; function filter( + table: "members", + callback: (value: Member, key: string) => boolean, +): Promise>; +async function filter( table: TableName, callback: (value: any, key: string) => boolean, ) { diff --git a/src/controllers/channels.ts b/src/controllers/channels.ts index 1812e6c20..f34887ed1 100644 --- a/src/controllers/channels.ts +++ b/src/controllers/channels.ts @@ -11,11 +11,6 @@ export async function handleInternalChannelCreate(data: DiscordPayload) { const channel = await structures.createChannel(payload); await cacheHandlers.set("channels", channel.id, channel); - if (channel.guildID) { - const guild = await cacheHandlers.get("guilds", channel.guildID); - guild?.channels.set(channel.id, channel); - } - eventHandlers.channelCreate?.(channel); } @@ -31,20 +26,18 @@ export async function handleInternalChannelDelete(data: DiscordPayload) { const guild = await cacheHandlers.get("guilds", payload.guild_id); if (guild) { - guild.voiceStates.forEach((vs, key) => { + guild.voiceStates.forEach(async (vs, key) => { if (vs.channelID !== payload.id) return; // Since this channel was deleted all voice states for this channel should be deleted guild.voiceStates.delete(key); - const member = guild.members.get(vs.userID); + const member = await cacheHandlers.get("members", vs.userID); if (!member) return; eventHandlers.voiceChannelLeave?.(member, vs.channelID); }); } - - guild?.channels.delete(payload.id); } cacheHandlers.delete("channels", payload.id); @@ -66,10 +59,5 @@ export async function handleInternalChannelUpdate(data: DiscordPayload) { if (!cachedChannel) return; - if (channel.guildID) { - const guild = await cacheHandlers.get("guilds", channel.guildID); - guild?.channels.set(channel.id, channel); - } - eventHandlers.channelUpdate?.(channel, cachedChannel); } diff --git a/src/controllers/members.ts b/src/controllers/members.ts index f1101525b..251b352bf 100644 --- a/src/controllers/members.ts +++ b/src/controllers/members.ts @@ -22,7 +22,6 @@ export async function handleInternalGuildMemberAdd(data: DiscordPayload) { payload, guild.id, ); - guild.members.set(payload.user.id, member); eventHandlers.guildMemberAdd?.(guild, member); } @@ -35,13 +34,14 @@ export async function handleInternalGuildMemberRemove(data: DiscordPayload) { if (!guild) return; guild.memberCount--; - const member = guild.members.get(payload.user.id); + const member = await cacheHandlers.get("members", payload.user.id); eventHandlers.guildMemberRemove?.( guild, member || payload.user, ); - guild.members.delete(payload.user.id); + member?.guilds.delete(guild.id); + if (member && !member.guilds.size) cacheHandlers.delete("members", member.id); } export async function handleInternalGuildMemberUpdate(data: DiscordPayload) { @@ -51,7 +51,7 @@ export async function handleInternalGuildMemberUpdate(data: DiscordPayload) { const guild = await cacheHandlers.get("guilds", payload.guild_id); if (!guild) return; - const cachedMember = guild.members.get(payload.user.id); + const cachedMember = await cacheHandlers.get("members", payload.user.id); const newMemberData = { ...payload, @@ -60,12 +60,12 @@ export async function handleInternalGuildMemberUpdate(data: DiscordPayload) { .toISOString(), deaf: cachedMember?.deaf || false, mute: cachedMember?.mute || false, + roles: payload.roles, }; const member = await structures.createMember( newMemberData, guild.id, ); - guild.members.set(payload.user.id, member); if (cachedMember?.nick !== payload.nick) { eventHandlers.nicknameUpdate?.( @@ -99,15 +99,9 @@ export async function handleInternalGuildMembersChunk(data: DiscordPayload) { const guild = await cacheHandlers.get("guilds", payload.guild_id); if (!guild) return; - payload.members.forEach(async (member) => { - guild.members.set( - member.user.id, - await structures.createMember( - member, - guild.id, - ), - ); - }); + await Promise.all( + payload.members.map((member) => structures.createMember(member, guild.id)), + ); // Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming if ( @@ -118,7 +112,7 @@ export async function handleInternalGuildMembersChunk(data: DiscordPayload) { if (payload.chunk_index + 1 === payload.chunk_count) { cache.fetchAllMembersProcessingRequests.delete(payload.nonce); - resolve(guild.members); + resolve(await cacheHandlers.filter("members", (m) => m.guilds.has(guild.id))); } } } diff --git a/src/controllers/misc.ts b/src/controllers/misc.ts index 66eb2d94b..9b9d4614a 100644 --- a/src/controllers/misc.ts +++ b/src/controllers/misc.ts @@ -52,21 +52,17 @@ export function handleInternalTypingStart(data: DiscordPayload) { eventHandlers.typingStart?.(data.d as TypingStartPayload); } -export function handleInternalUserUpdate(data: DiscordPayload) { +export async function handleInternalUserUpdate(data: DiscordPayload) { if (data.t !== "USER_UPDATE") return; const userData = data.d as UserPayload; - cacheHandlers.forEach("guilds", (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; - }); + const member = await cacheHandlers.get("members", userData.id); + if (!member) return; + + Object.entries(userData).forEach(([key, value]) => { + // @ts-ignore + if (member[key] !== value) return member[key] = value; }); return eventHandlers.botUpdate?.(userData); } @@ -80,10 +76,9 @@ export async function handleInternalVoiceStateUpdate(data: DiscordPayload) { const guild = await cacheHandlers.get("guilds", payload.guild_id); if (!guild) return; - const member = guild.members.get(payload.user_id) || - (payload.member + const member = payload.member ? await structures.createMember(payload.member, guild.id) - : undefined); + : await cacheHandlers.get("members", payload.user_id); if (!member) return; // No cached state before so lets make one for em diff --git a/src/controllers/roles.ts b/src/controllers/roles.ts index fe6718798..ca2241069 100644 --- a/src/controllers/roles.ts +++ b/src/controllers/roles.ts @@ -29,8 +29,16 @@ export async function handleInternalGuildRoleDelete(data: DiscordPayload) { eventHandlers.roleDelete?.(guild, cachedRole); // For bots without GUILD_MEMBERS member.roles is never updated breaking permissions checking. - guild.members.forEach((member) => { - member.roles = member.roles.filter((id) => id !== payload.role_id); + cacheHandlers.forEach("members", member => { + // Not in the relevant guild so just skip. + if (!member.guilds.has(guild.id)) return; + + member.guilds.forEach(g => { + // Member does not have this role + if (!g.roles.includes(payload.role_id)) return; + // Remove this role from the members cache + g.roles = g.roles.filter(id => id !== payload.role_id); + }) }); } diff --git a/src/handlers/guild.ts b/src/handlers/guild.ts index 40fe61552..2759fa2bd 100644 --- a/src/handlers/guild.ts +++ b/src/handlers/guild.ts @@ -216,7 +216,6 @@ export async function getMember( ) as MemberCreatePayload; const member = await structures.createMember(data, guildID); - guild?.members.set(id, member); return member; } diff --git a/src/structures/guild.ts b/src/structures/guild.ts index 51b76877f..d1756d6c3 100644 --- a/src/structures/guild.ts +++ b/src/structures/guild.ts @@ -1,7 +1,6 @@ import { CreateGuildPayload } from "../types/guild.ts"; import { Unpromise } from "../types/misc.ts"; import { Collection } from "../utils/collection.ts"; -import { Member } from "./member.ts"; import { structures } from "./mod.ts"; export async function createGuild(data: CreateGuildPayload, shardID: number) { @@ -23,6 +22,7 @@ export async function createGuild(data: CreateGuildPayload, shardID: number) { joined_at: joinedAt, member_count: memberCount, voice_states: voiceStates, + channels, ...rest } = data; @@ -31,7 +31,7 @@ export async function createGuild(data: CreateGuildPayload, shardID: number) { ); await Promise.all( - data.channels.map((c) => structures.createChannel(c, data.id)), + channels.map((c) => structures.createChannel(c, data.id)), ); const guild = { @@ -70,8 +70,6 @@ export async function createGuild(data: CreateGuildPayload, shardID: number) { roles: new Collection(roles.map((r) => [r.id, r])), /** When this guild was joined at. */ joinedAt: Date.parse(joinedAt), - /** The users in this guild. */ - members: new Collection(), /** The presences of all the users in the guild. */ presences: new Collection(data.presences.map((p) => [p.user.id, p])), /** The total number of members in this guild. This value is updated as members leave and join the server. However, if you do not have the intent enabled to be able to listen to these events, then this will not be accurate. */ @@ -89,9 +87,7 @@ export async function createGuild(data: CreateGuildPayload, shardID: number) { }])), }; - data.members.forEach(async (m) => - guild.members.set(m.user.id, await structures.createMember(m, guild.id)) - ); + data.members.forEach((m) => structures.createMember(m, guild.id)); return guild; } diff --git a/src/structures/member.ts b/src/structures/member.ts index 97e91e832..7d1261694 100644 --- a/src/structures/member.ts +++ b/src/structures/member.ts @@ -1,5 +1,7 @@ -import { MemberCreatePayload } from "../types/member.ts"; +import { cacheHandlers } from "../controllers/cache.ts"; +import { GuildMember, MemberCreatePayload } from "../types/member.ts"; import { Unpromise } from "../types/misc.ts"; +import { Collection } from "../utils/collection.ts"; export async function createMember(data: MemberCreatePayload, guildID: string) { const { @@ -14,22 +16,65 @@ export async function createMember(data: MemberCreatePayload, guildID: string) { ...user } = data.user || {}; + const cached = await cacheHandlers.get("members", user.id); + if (cached) { + // Check if any of the others need updating + if (mfaEnabled) cached.mfaEnabled = mfaEnabled; + if (premiumType) cached.premiumType = premiumType; + if (user.username) cached.username = user.username; + if (user.discriminator) cached.discriminator = user.discriminator; + if (user.avatar) cached.avatar = user.avatar; + if (user.bot) cached.bot = user.bot; + if (user.system) cached.system = user.system; + if (user.locale) cached.locale = user.locale; + if (user.verified) cached.verified = user.verified; + if (user.email) cached.email = user.email; + if (user.flags) cached.flags = user.flags; + + // Set the guild data + cached.guilds.set(guildID, { + /** The user's guild nickname if one is set. */ + nick: data.nick, + /** Array of role ids that the member has */ + roles: data.roles, + /** When the user joined the guild. */ + joinedAt: Date.parse(joinedAt), + /** When the user used their nitro boost on the server. */ + premiumSince: premiumSince ? Date.parse(premiumSince) : undefined, + /** Whether the user is deafened in voice channels */ + deaf: data.deaf, + /** Whether the user is muted in voice channels */ + mute: data.mute, + }); + } + const member = { ...rest, // Only use those that we have not removed above - user: user, + ...user, /** When the user joined the guild */ joinedAt: Date.parse(joinedAt), /** When the user used their nitro boost on the server. */ premiumSince: premiumSince ? Date.parse(premiumSince) : undefined, - /** The guild id where this member exists */ - guildID, /** Whether or not this user has 2FA enabled. */ mfaEnabled, /** The premium type for this user */ premiumType, + /** The guild related data mapped by guild id */ + guilds: new Collection(), }; + member.guilds.set(guildID, { + nick: data.nick, + roles: data.roles, + joinedAt: Date.parse(joinedAt), + premiumSince: premiumSince ? Date.parse(premiumSince) : undefined, + deaf: data.deaf, + mute: data.mute, + }); + + await cacheHandlers.set("members", member.id, member); + return member; } diff --git a/src/types/member.ts b/src/types/member.ts index 199e0fef5..0fca94654 100644 --- a/src/types/member.ts +++ b/src/types/member.ts @@ -29,3 +29,18 @@ export interface MemberCreatePayload { /** Whether the user is muted in voice channels */ mute: boolean; } + +export interface GuildMember { + /** The user's guild nickname if one is set. */ + nick?: string; + /** Array of role ids that the member has */ + roles: string[]; + /** When the user joined the guild. */ + joinedAt: number; + /** When the user used their nitro boost on the server. */ + premiumSince?: number; + /** Whether the user is deafened in voice channels */ + deaf: boolean; + /** Whether the user is muted in voice channels */ + mute: boolean; +} diff --git a/src/utils/cache.ts b/src/utils/cache.ts index dbd89b898..d897e4b3e 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,5 +1,6 @@ import { Channel } from "../structures/channel.ts"; import { Guild } from "../structures/guild.ts"; +import { Member } from "../structures/member.ts"; import { Message } from "../structures/message.ts"; import { PresenceUpdatePayload } from "../types/discord.ts"; import { Collection } from "./collection.ts"; @@ -9,6 +10,7 @@ export interface CacheData { guilds: Collection; channels: Collection; messages: Collection; + members: Collection; unavailableGuilds: Collection; presences: Collection; fetchAllMembersProcessingRequests: Collection; @@ -19,6 +21,7 @@ export const cache: CacheData = { guilds: new Collection(), channels: new Collection(), messages: new Collection(), + members: new Collection(), unavailableGuilds: new Collection(), presences: new Collection(), fetchAllMembersProcessingRequests: new Collection(), diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index f009150f3..825b0b5a6 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -2,7 +2,7 @@ import { cacheHandlers } from "../controllers/cache.ts"; import { botID } from "../module/client.ts"; import { Guild } from "../structures/guild.ts"; import { Role } from "../structures/role.ts"; -import { PermissionOverwrite } from "../types/guild.ts"; +import { RawOverwrite } from "../types/guild.ts"; import { Permission, Permissions } from "../types/permission.ts"; /** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */ @@ -16,10 +16,10 @@ export async function memberIDHasPermission( if (memberID === guild.ownerID) return true; - const member = guild.members.get(memberID); + const member = (await cacheHandlers.get("members", memberID))?.guilds.get(guildID); if (!member) return false; - return memberHasPermission(member.guildID, guild, member.roles, permissions); + return memberHasPermission(memberID, guild, member.roles, permissions); } /** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */ @@ -58,7 +58,7 @@ export async function botHasPermission( // Check if the bot is the owner of the guild, if it is, returns true if (guild.ownerID === botID) return true; - const member = guild.members.get(botID); + const member = await cacheHandlers.get("members", botID); if (!member) return false; // The everyone role is not in member.roles @@ -105,12 +105,12 @@ export async function hasChannelPermissions( return true; } - const member = guild.members.get(memberID); + const member = (await cacheHandlers.get("members", memberID))?.guilds.get(guild.id); if (!member) return false; - let memberOverwrite: PermissionOverwrite | undefined; - let everyoneOverwrite: PermissionOverwrite | undefined; - let rolesOverwrites: PermissionOverwrite[] = []; + let memberOverwrite: RawOverwrite | undefined; + let everyoneOverwrite: RawOverwrite | undefined; + let rolesOverwrites: RawOverwrite[] = []; for (const overwrite of channel.permissionOverwrites || []) { // If the overwrite on this channel is specific to this member @@ -125,8 +125,8 @@ export async function hasChannelPermissions( // Member perms override everything so we must check them first if (memberOverwrite) { - const allowBits = calculateBits(memberOverwrite.allow); - const denyBits = calculateBits(memberOverwrite.deny); + const allowBits = memberOverwrite.allow; + const denyBits = memberOverwrite.deny; for (const perm of permissions) { // One of the necessary permissions is denied. Since this is main permission we can cancel if its denied. if (BigInt(denyBits) & BigInt(perm)) return false; @@ -146,19 +146,19 @@ export async function hasChannelPermissions( if (allowedPermissions.has(perm)) continue; for (const overwrite of rolesOverwrites) { - const allowBits = calculateBits(overwrite.allow); + const allowBits = overwrite.allow; // This perm is allowed so we save it if (BigInt(allowBits) & BigInt(perm)) { allowedPermissions.add(perm); break; } - const denyBits = calculateBits(overwrite.deny); + const denyBits = overwrite.deny; // If this role denies it we need to save and check if another role allows it, allows > deny if (BigInt(denyBits) & BigInt(perm)) { // This role denies his perm, but before denying we need to check all other roles if any allow as allow > deny const isAllowed = rolesOverwrites.some((o) => - BigInt(calculateBits(o.allow)) & BigInt(perm) + BigInt(o.allow) & BigInt(perm) ); if (isAllowed) continue; // This permission is in fact denied. Since Roles overrule everything below here we can cancel ou here @@ -168,8 +168,8 @@ export async function hasChannelPermissions( } if (everyoneOverwrite) { - const allowBits = calculateBits(everyoneOverwrite.allow); - const denyBits = calculateBits(everyoneOverwrite.deny); + const allowBits = everyoneOverwrite.allow; + const denyBits = everyoneOverwrite.deny; for (const perm of permissions) { // Already allowed perm if (allowedPermissions.has(perm)) continue; @@ -210,7 +210,7 @@ export async function highestRole(guildID: string, memberID: string) { const guild = await cacheHandlers.get("guilds", guildID); if (!guild) return; - const member = guild?.members.get(memberID); + const member = await cacheHandlers.get("members", memberID); if (!member) return; let memberHighestRole: Role | undefined;