diff --git a/.gitignore b/.gitignore index 39ac177f5..45eeac2c0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,5 @@ dist/ public/ .cache/ -# Visual Studio Code -.vscode/ - # IntelliJ IDEA .idea/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..8b3b2fa17 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "deno.enable": true, + "editor.defaultFormatter": "denoland.vscode-deno", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } +} \ No newline at end of file diff --git a/src/api/handlers/member.ts b/src/api/handlers/member.ts index 71965e557..020203bd7 100644 --- a/src/api/handlers/member.ts +++ b/src/api/handlers/member.ts @@ -61,7 +61,10 @@ export async function addRole( botsHighestRole.id, roleID, ); - if (!hasHigherRolePosition) { + if ( + !hasHigherRolePosition && + (await cacheHandlers.get("guilds", guildID))?.ownerID !== botID + ) { throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); } } @@ -92,7 +95,10 @@ export async function removeRole( botsHighestRole.id, roleID, ); - if (!hasHigherRolePosition) { + if ( + !hasHigherRolePosition && + (await cacheHandlers.get("guilds", guildID))?.ownerID !== botID + ) { throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); } } diff --git a/src/api/structures/channel.ts b/src/api/structures/channel.ts index d050f3cf4..5c8bcfb6e 100644 --- a/src/api/structures/channel.ts +++ b/src/api/structures/channel.ts @@ -1,5 +1,26 @@ -import { ChannelCreatePayload, Unpromise } from "../../types/types.ts"; +import { + ChannelCreatePayload, + ChannelType, + RawOverwrite, +} from "../../types/types.ts"; +import { cache } from "../../util/cache.ts"; +import { Collection } from "../../util/collection.ts"; +import { createNewProp } from "../../util/utils.ts"; import { cacheHandlers } from "../controllers/cache.ts"; +import { Guild } from "./guild.ts"; +import { Message } from "./message.ts"; + +const baseChannel: any = { + get guild() { + return cache.guilds.get(this.guildID); + }, + get messages() { + return cache.messages.filter((m) => m.channelID === this.id); + }, + get mention() { + return `<#${this.id}>`; + }, +}; export async function createChannel( data: ChannelCreatePayload, @@ -13,33 +34,77 @@ export async function createChannel( parent_id: parentID, last_pin_timestamp: lastPinTimestamp, permission_overwrites, + nsfw, ...rest } = data; - const channel = { - ...rest, - /** The guild id of the channel if it is a guild channel. */ - guildID: guildID || rawGuildID || "", - /** The id of the last message sent in this channel */ - lastMessageID, - /** The amount of users allowed in this voice channel. */ - userLimit, - /** The rate limit(slowmode) in this text channel that users can send messages. */ - rateLimitPerUser, - /** The category id for this channel */ - parentID, - /** The last time when a message was pinned in this channel */ - lastPinTimestamp, - /** The permission overwrites for this channel */ - permissionOverwrites: permission_overwrites, - /** Whether this channel is nsfw or not */ - nsfw: data.nsfw || false, - /** The mention of the channel */ - mention: `<#${data.id}>`, - }; + const restProps: Record> = {}; + for (const key of Object.keys(rest)) { + restProps[key] = createNewProp((rest as any)[key]); + } + + const channel = Object.create(baseChannel, { + ...restProps, + guildID: createNewProp(guildID || rawGuildID || ""), + lastMessageID: createNewProp(lastMessageID), + userLimit: createNewProp(userLimit), + rateLimitPerUser: createNewProp(rateLimitPerUser), + parentID: createNewProp(parentID || undefined), + lastPinTimestamp: createNewProp( + lastPinTimestamp ? Date.parse(lastPinTimestamp) : undefined, + ), + permissionOverwrites: createNewProp(permission_overwrites || []), + nsfw: createNewProp(data.nsfw || false), + }); cacheHandlers.set("channels", data.id, channel); - return channel; + return channel as Channel; } -export interface Channel extends Unpromise> {} +export interface Channel { + /** The id of this channel */ + id: string; + /** Sorting position of the channel */ + position?: number; + /** The name of the channel (2-100 characters) */ + name?: string; + /** The channel topic (0-1024 characters) */ + topic?: string; + /** The bitrate (in bits) of the voice channel */ + bitrate?: number; + /** The type of the channel */ + type: ChannelType; + /** The guild id of the channel if it is a guild channel. */ + guildID: string; + /** The id of the last message sent in this channel */ + lastMessageID?: string; + /** The amount of users allowed in this voice channel. */ + userLimit?: number; + /** The rate limit(slowmode) in this text channel that users can send messages. */ + rateLimitPerUser?: number; + /** The category id for this channel */ + parentID?: string; + /** The last time when a message was pinned in this channel */ + lastPinTimestamp?: number; + /** The permission overwrites for this channel */ + permissionOverwrites: RawOverwrite[]; + /** Whether this channel is nsfw or not */ + nsfw: boolean; + + // GETTERS + + /** + * Gets the guild object for this channel. + * + * ⚠️ ADVANCED: If you use the custom cache, these will not work for you. Getters can not be async and custom cache requires async. + */ + guild?: Guild; + /** + * Gets the messages from cache that were sent in this channel + * + * ⚠️ ADVANCED: If you use the custom cache, these will not work for you. Getters can not be async and custom cache requires async. + */ + messages: Collection; + /** The mention of the channel */ + mention: string; +} diff --git a/src/api/structures/guild.ts b/src/api/structures/guild.ts index 88d89dccf..60fa94a05 100644 --- a/src/api/structures/guild.ts +++ b/src/api/structures/guild.ts @@ -1,15 +1,115 @@ +import { botID } from "../../bot.ts"; import { + BannedUser, + ChannelCreatePayload, CreateGuildPayload, + Emoji, + GetAuditLogsOptions, + GuildEditOptions, + GuildFeatures, + GuildMember, + ImageFormats, + ImageSize, MemberCreatePayload, - Unpromise, + Presence, + VoiceState, } from "../../types/types.ts"; +import { cache } from "../../util/cache.ts"; import { Collection } from "../../util/collection.ts"; -import { structures } from "./mod.ts"; +import { + deleteServer, + editGuild, + getAuditLogs, + getBan, + getBans, + getInvites, + guildBannerURL, + guildIconURL, + leaveGuild, +} from "../handlers/guild.ts"; +import { Member } from "./member.ts"; +import { Role, structures } from "./mod.ts"; +import { Channel } from "./structures.ts"; export const initialMemberLoadQueue = new Map(); +const baseGuild: Partial = { + get members() { + return cache.members.filter((member) => member.guilds.has(this.id!)); + }, + get channels() { + return cache.channels.filter((channel) => channel.guildID === this.id); + }, + get afkChannel() { + return cache.channels.get(this.afkChannelID!); + }, + get publicUpdatesChannel() { + return cache.channels.get(this.publicUpdatesChannelID!); + }, + get rulesChannel() { + return cache.channels.get(this.rulesChannelID!); + }, + get systemChannel() { + return cache.channels.get(this.systemChannelID!); + }, + get bot() { + return cache.members.get(botID); + }, + get botMember() { + return this.bot?.guilds.get(this.id!); + }, + get botVoice() { + return this.voiceStates.get(botID); + }, + get owner() { + return cache.members.get(this.ownerID!); + }, + get partnered() { + return Boolean(this.features?.includes("PARTNERED")); + }, + get verified() { + return Boolean(this.features?.includes("VERIFIED")); + }, + bannerURL(size, format) { + return guildBannerURL(this as Guild, size, format); + }, + delete() { + return deleteServer(this.id!); + }, + edit(options) { + return editGuild(this.id!, options); + }, + auditLogs(options) { + return getAuditLogs(this.id!, options); + }, + getBan(memberID) { + return getBan(this.id!, memberID); + }, + bans() { + return getBans(this.id!); + }, + invites() { + return getInvites(this.id!); + }, + iconURL(size, format) { + return guildIconURL(this as Guild, size, format); + }, + leave() { + return leaveGuild(this.id!); + }, +}; + export async function createGuild(data: CreateGuildPayload, shardID: number) { const { + disovery_splash: discoverySplash, + default_message_notifications: defaultMessageNotifications, + explicit_content_filter: explicitContentFilter, + system_channel_flags: systemChannelFlags, + rules_channel_id: rulesChannelID, + public_updates_channel_id: publicUpdatesChannelID, + max_video_channel_users: maxVideoChannelUsers, + approximate_member_count: approximateMemberCount, + approximate_presence_count: approximatePresenceCount, owner_id: ownerID, afk_channel_id: afkChannelID, afk_timeout: afkTimeout, @@ -42,45 +142,33 @@ export async function createGuild(data: CreateGuildPayload, shardID: number) { const guild = { ...rest, - /** The shard id that this guild is on */ + discoverySplash, + defaultMessageNotifications, + explicitContentFilter, + rulesChannelID, + publicUpdatesChannelID, + maxVideoChannelUsers, + approximateMemberCount, + approximatePresenceCount, shardID, - /** The owner id of the guild. */ ownerID, - /** The afk channel id for this guild. */ afkChannelID, - /** The amount of time before a user is moved to AFK. */ afkTimeout, - /** Whether or not the embed is enabled in this server. */ widgetEnabled, - /** The channel id for the guild embed in this server. */ widgetChannelID, - /** The verification level for this server. */ verificationLevel, - /** The MFA level for this server. */ mfaLevel, - /** The system channel id for this server. */ systemChannelID, - /** The max presences for this server. */ maxPresences, - /** The maximum members in this server. */ maxMembers, - /** The vanity URL code for this server. */ vanityURLCode, - /** The premium tier for this server. */ premiumTier, - /** The subscription count for this server. */ premiumSubscriptionCount, - /** The preferred language in this server. */ preferredLocale, - /** The roles in the guild */ roles: new Collection(roles.map((r) => [r.id, r])), - /** When this guild was joined at. */ joinedAt: Date.parse(joinedAt), - /** 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. */ memberCount: memberCount || 0, - /** The Voice State data for each user in a voice channel in this server. */ voiceStates: new Collection(voiceStates.map((vs) => [vs.user_id, { ...vs, guildID: vs.guild_id, @@ -94,9 +182,146 @@ export async function createGuild(data: CreateGuildPayload, shardID: number) { }; initialMemberLoadQueue.set(guild.id, members); - // members.forEach((m) => structures.createMember(m, guild.id)); return guild; } -export interface Guild extends Unpromise> {} +export interface Guild { + /** The guild id */ + id: string; + /** The guild name 2-100 characters */ + name: string; + /** The guild icon image hash */ + icon: string | null; + /** The guild splash image hash */ + splash: string | null; + /** Discovery splash has; only present for guilds with the "DISCOVERABLE" feature */ + disoverySplash: string | null; + /** The voice region id for the guild */ + region: string; + /** Default message notifications level */ + defaultMessageNotifications: number; + /** Explicit content filter level */ + explicitContentFilter: number; + /** The custom guild emojis */ + emojis: Emoji[]; + /** Enabled guild features */ + features: GuildFeatures[]; + /** System channel flags */ + systemChannelFlags: number; + /** The id of the channel where guilds with the PUBLIC feature can display rules and or guidelines. */ + rulesChannelID: string | null; + /** The description for the guild */ + description: string | null; + /** The banner hash */ + banner: string | null; + /** The id of the channel where admins and moderators of guilds with the PUBLIC feature receive notices from Discord */ + publicUpdatesChannelID: string | null; + /** The maximum amount of users in a video channel. */ + maxVideoChannelUsers?: number; + /** The approximate number of members in this guild, returned from the GET /guild/id endpoint when with_counts is true */ + approximateMemberCount?: number; + /** The approximate number of non-offline members in this guild, returned from the GET /guild/id endpoint when with_counts is true */ + approximatePresenceCount?: number; + /** Whether this is considered a large guild */ + large: boolean; + /** Whether this guild is unavailable */ + unavailable: boolean; + /** The shard id that this guild is on */ + shardID: number; + /** The owner id of the guild. */ + ownerID: string; + /** The afk channel id for this guild. */ + afkChannelID: string; + /** The amount of time before a user is moved to AFK. */ + afkTimeout: number; + /** Whether or not the embed is enabled in this server. */ + widgetEnabled: boolean; + /** The channel id for the guild embed in this server. */ + widgetChannelID: string; + /** The verification level for this server. */ + verificationLevel: number; + /** The MFA level for this server. */ + mfaLevel: number; + /** The system channel id for this server. */ + systemChannelID: string; + /** The max presences for this server. */ + maxPresences: number; + /** The maximum members in this server. */ + maxMembers: number; + /** The vanity URL code for this server. */ + vanityURLCode: string; + /** The premium tier for this server. */ + premiumTier: number; + /** The subscription count for this server. */ + premiumSubscriptionCount: number; + /** The preferred language in this server. */ + preferredLocale: string; + /** The roles in the guild */ + roles: Collection; + /** When this guild was joined at. */ + joinedAt: number; + /** The presences of all the users in the guild. */ + presences: Collection; + /** 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. */ + memberCount: number; + /** The Voice State data for each user in a voice channel in this server. */ + voiceStates: Collection; + + // GETTERS + /** Members in this guild. */ + members: Collection; + /** Channels in this guild. */ + channels: Collection; + /** The afk channel if one is set */ + afkChannel?: Channel; + /** The public update channel if one is set */ + publicUpdatesChannel?: Channel; + /** The rules channel in this guild if one is set */ + rulesChannel?: Channel; + /** The system channel in this guild if one is set */ + systemChannel?: Channel; + /** The bot member in this guild if cached */ + bot?: Member; + /** The bot guild member in this guild if cached */ + botMember?: GuildMember; + /** The bots voice state if there is one in this guild */ + botVoice?: CleanVoiceState; + /** The owner member of this guild */ + owner?: Member; + /** Whether or not this guild is partnered */ + partnered: boolean; + /** Whether or not this guild is verified */ + verified: boolean; + + // METHODS + + /** The banner url for this server */ + bannerURL(size?: ImageSize, format?: ImageFormats): string | undefined; + /** The full URL of the icon from Discords CDN. Undefined when no icon is set. */ + iconURL(size?: ImageSize, format?: ImageFormats): string | undefined; + /** Delete a guild permanently. User must be owner. Returns 204 No Content on success. Fires a Guild Delete Gateway event. */ + delete(): Promise; + /** Leave a guild */ + leave(): Promise; + /** Edit the server. Requires the MANAGE_GUILD permission. */ + edit(options: GuildEditOptions): Promise; + /** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */ + auditLogs(options: GetAuditLogsOptions): Promise; + /** Returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. */ + getBan(memberID: string): Promise; + /** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */ + bans(): Promise>; + /** Get all the invites for this guild. Requires MANAGE_GUILD permission */ + invites(): Promise; +} + +interface CleanVoiceState extends VoiceState { + guildID: string; + channelID: string; + userID: string; + sessionID: string; + selfDeaf: boolean; + selfMute: boolean; + selfStream: boolean; +} diff --git a/src/api/structures/member.ts b/src/api/structures/member.ts index 58dee1832..584db9090 100644 --- a/src/api/structures/member.ts +++ b/src/api/structures/member.ts @@ -1,10 +1,55 @@ import { + BanOptions, + EditMemberOptions, GuildMember, MemberCreatePayload, - Unpromise, + MessageContent, } from "../../types/types.ts"; +import { cache } from "../../util/cache.ts"; import { Collection } from "../../util/collection.ts"; +import { createNewProp } from "../../util/utils.ts"; import { cacheHandlers } from "../controllers/cache.ts"; +import { ban } from "../handlers/guild.ts"; +import { + addRole, + editMember, + kick, + removeRole, + sendDirectMessage, +} from "../handlers/member.ts"; +import { Guild } from "./guild.ts"; + +const baseMember: Partial = { + // METHODS + + guild(guildID) { + return cache.guilds.get(guildID); + }, + name(guildID) { + return this.guildMember!(guildID)?.nick || this.username!; + }, + guildMember(guildID) { + return this.guilds?.get(guildID); + }, + sendDM(content) { + return sendDirectMessage(this.id!, content); + }, + kick(guildID, reason) { + return kick(guildID, this.id!, reason); + }, + edit(guildID, options) { + return editMember(guildID, this.id!, options); + }, + ban(guildID, options) { + return ban(guildID, this.id!, options); + }, + addRole(guildID, roleID, reason) { + return addRole(guildID, this.id!, roleID, reason); + }, + removeRole(guildID, roleID, reason) { + return removeRole(guildID, this.id!, roleID, reason); + }, +}; export async function createMember(data: MemberCreatePayload, guildID: string) { const { @@ -24,17 +69,23 @@ export async function createMember(data: MemberCreatePayload, guildID: string) { ...user } = data.user || {}; - const member = { - ...rest, - // Only use those that we have not removed above - ...user, - /** Whether or not this user has 2FA enabled. */ - mfaEnabled, - /** The premium type for this user */ - premiumType, + const restProps: Record> = {}; + for (const key of Object.keys(rest)) { + restProps[key] = createNewProp((rest as any)[key]); + } + + for (const key of Object.keys(user)) { + // @ts-ignore + restProps[key] = createNewProp(user[key]); + } + + const member = Object.create(baseMember, { + ...restProps, + mfaEnabled: createNewProp(mfaEnabled), + premiumType: createNewProp(premiumType), /** The guild related data mapped by guild id */ - guilds: new Collection(), - }; + guilds: createNewProp(new Collection()), + }); const cached = await cacheHandlers.get("members", user.id); if (cached) { @@ -58,4 +109,52 @@ export async function createMember(data: MemberCreatePayload, guildID: string) { return member; } -export interface Member extends Unpromise> {} +export interface Member { + /** The user's id */ + id: string; + /** the user's username, not unique across the platform */ + username: string; + /** The user's 4 digit discord tag */ + discriminator: string; + /** The user's avatar hash */ + avatar: string | null; + /** Whether the user is a bot */ + bot?: boolean; + /** Whether the user is an official discord system user (part of the urgent message system.) */ + system?: boolean; + /** the user's chosen language option */ + locale?: string; + /** Whether the email on this account has been verified */ + verified?: boolean; + /** The user's email */ + email?: string; + /** The flags on a user's account. */ + flags?: number; + /** Whether or not this user has 2FA enabled. */ + mfaEnabled?: boolean; + /** The premium type for this user */ + premiumType?: number; + /** The guild related data mapped by guild id */ + guilds: Collection; + + // METHODS + + /** Returns the guild for this guildID */ + guild(guildID: string): Guild | undefined; + /** Get the nickname or the username if no nickname */ + name(guildID: string): string; + /** Get the nickname */ + guildMember(guildID: string): GuildMember | undefined; + /** Send a direct message to the user is possible */ + sendDM(content: string | MessageContent): Promise; + /** Kick the member from a guild */ + kick(guildID: string, reason?: string): Promise; + /** Edit the member in a guild */ + edit(guildID: string, options: EditMemberOptions): Promise; + /** Ban a member in a guild */ + ban(guildID: string, options: BanOptions): Promise; + /** Add a role to the member */ + addRole(guildID: string, roleID: string, reason?: string): Promise; + /** Remove a role from the member */ + removeRole(guildID: string, roleID: string, reason?: string): Promise; +} diff --git a/src/api/structures/message.ts b/src/api/structures/message.ts index b63977e67..a307cefc9 100644 --- a/src/api/structures/message.ts +++ b/src/api/structures/message.ts @@ -1,12 +1,125 @@ -import { MessageCreateOptions, Unpromise } from "../../types/types.ts"; +import { + Activity, + Application, + Attachment, + Embed, + GuildMember, + MessageContent, + MessageCreateOptions, + MessageSticker, + Reaction, + Reference, + UserPayload, +} from "../../types/types.ts"; +import { cache } from "../../util/cache.ts"; +import { createNewProp } from "../../util/utils.ts"; +import { sendMessage } from "../handlers/channel.ts"; +import { + addReaction, + addReactions, + deleteMessageByID, + editMessage, + pin, + removeAllReactions, + removeReaction, + removeReactionEmoji, +} from "../handlers/message.ts"; +import { Channel } from "./channel.ts"; +import { Guild } from "./guild.ts"; +import { Member } from "./member.ts"; +import { Role } from "./role.ts"; + +const baseMessage: Partial = { + get guild() { + if (!this.guildID) return; + return cache.guilds.get(this.guildID); + }, + get member() { + if (!this.author?.id) return; + return cache.members.get(this.author?.id); + }, + get guildMember() { + if (!this.guildID) return; + return this.member?.guilds.get(this.guildID); + }, + get link() { + return `https://discord.com/channels/${this.guildID || + "@me"}/${this.channelID}/${this.id}`; + }, + get mentionedRoles() { + // TODO: add getters for Guild structure, that will fix this error + return this.mentionRoleIDs?.map((id) => this.guild?.roles.get(id)) || []; + }, + get mentionedChannels() { + return this.mentionChannelIDs?.map((id) => cache.channels.get(id)) || []; + }, + get mentionedMembers() { + return this.mentions?.map((id) => cache.members.get(id)) || []; + }, + + // METHODS + delete(reason, delayMilliseconds) { + return deleteMessageByID( + this.channelID!, + this.id!, + reason, + delayMilliseconds, + ); + }, + edit(content) { + return editMessage(this as Message, content); + }, + pin() { + return pin(this.channelID!, this.id!); + }, + addReaction(reaction) { + return addReaction(this.channelID!, this.id!, reaction); + }, + addReactions(reactions, ordered) { + return addReactions(this.channelID!, this.id!, reactions, ordered); + }, + sendResponse(content) { + const contentWithMention = typeof content === "string" + ? { content, mentions: { repliedUser: true }, replyMessageID: this.id } + : { + ...content, + mentions: { ...(content.mentions || {}), repliedUser: true }, + replyMessageID: this.id, + }; + + return sendMessage(this.channelID!, contentWithMention); + }, + reply(content) { + return sendMessage(this.channelID!, content); + }, + alert(content, timeout = 10, reason = "") { + return sendMessage(this.channelID!, content).then((response) => + response.delete(reason, timeout * 1000).catch(console.error) + ); + }, + alertResponse(content, timeout = 10, reason = "") { + return this.sendResponse!(content).then((response) => + response.delete(reason, timeout * 1000).catch(console.error) + ); + }, + removeAllReactions() { + return removeAllReactions(this.channelID!, this.id!); + }, + removeReactionEmoji(reaction) { + return removeReactionEmoji(this.channelID!, this.id!, reaction); + }, + removeReaction(reaction) { + return removeReaction(this.channelID!, this.id!, reaction); + }, +}; export async function createMessage(data: MessageCreateOptions) { const { guild_id: guildID, channel_id: channelID, mentions_everyone: mentionsEveryone, - mention_channels: mentionChannels, - mention_roles: mentionRoles, + mention_channels: mentionChannelIDs, + mention_roles: mentionRoleIDs, webhook_id: webhookID, message_reference: messageReference, edited_timestamp: editedTimestamp, @@ -14,23 +127,133 @@ export async function createMessage(data: MessageCreateOptions) { ...rest } = data; - const message = { - ...rest, + const restProps: Record> = {}; + for (const key of Object.keys(rest)) { + restProps[key] = createNewProp((rest as any)[key]); + } + + const message = Object.create(baseMessage, { + ...restProps, /** The message id of the original message if this message was sent as a reply. If null, the original message was deleted. */ - referencedMessageID, - channelID, - guildID: guildID || "", - mentions: data.mentions.map((m) => m.id), - mentionsEveryone, - mentionRoles, - mentionChannels: mentionChannels || [], - webhookID, - messageReference, - timestamp: Date.parse(data.timestamp), - editedTimestamp: editedTimestamp ? Date.parse(editedTimestamp) : undefined, - }; + referencedMessageID: createNewProp(referencedMessageID), + channelID: createNewProp(channelID), + guildID: createNewProp(guildID || ""), + mentions: createNewProp(data.mentions.map((m) => m.id)), + mentionsEveryone: createNewProp(mentionsEveryone), + mentionRoleIDs: createNewProp(mentionRoleIDs), + mentionChannelIDs: createNewProp(mentionChannelIDs?.map((m) => m.id) || []), + webhookID: createNewProp(webhookID), + messageReference: createNewProp(messageReference), + timestamp: createNewProp(Date.parse(data.timestamp)), + editedTimestamp: createNewProp( + editedTimestamp ? Date.parse(editedTimestamp) : undefined, + ), + }); return message; } -export interface Message extends Unpromise> {} +export interface Message { + /** The id of the message */ + id: string; + /** The id of the channel the message was sent in */ + channelID: string; + /** The id of the guild the message was sent in */ + guildID: string; + /** The author of this message (not guaranteed to be a valid user such as a webhook.) */ + author: UserPayload; + /** The contents of the message */ + content: string; + /** When this message was sent */ + timestamp: string; + /** When this message was edited (if it was not edited, null) */ + editedTimestamp?: string; + /** Whether this was a TextToSpeech message. */ + tts: boolean; + /** Whether this message mentions everyone */ + mentionsEveryone: boolean; + /** Users specifically mentioned in the message. */ + mentions: string[]; + /** Roles specifically mentioned in this message */ + mentionRoleIDs: string[]; + /** Channels specifically mentioned in this message */ + mentionChannelIDs: string[]; + /** Any attached files */ + attachments: Attachment[]; + /** Any embedded content */ + embeds: Embed[]; + /** Reactions to the message */ + reactions?: Reaction[]; + /** Used for validating a message was sent */ + nonce?: number | string; + /** Whether this message is pinned */ + pinned: boolean; + /** If the message is generated by a webhook, this is the webhooks id */ + webhook_id?: string; + /** The type of message */ + type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + /** The activities sent with Rich Presence-related chat embeds. */ + activity?: Activity; + /** Applications that sent with Rich Presence related chat embeds. */ + applications?: Application; + /** The reference data sent with crossposted messages */ + messageReference?: Reference; + /** The message flags combined like permission bits describe extra features of the message */ + flags?: 1 | 2 | 4 | 8 | 16; + /** the stickers sent with the message (bots currently can only receive messages with stickers, not send) */ + stickers?: MessageSticker[]; + /** The message id of the original message if this message was sent as a reply. If null, the original message was deleted. */ + referencedMessageID?: MessageCreateOptions | null; + + // GETTERS + + /** The guild of this message. Can be undefined if not in cache or in DM */ + guild?: Guild; + /** The member for the user who sent the message. Can be undefined if not in cache or in dm. */ + member?: Member; + /** The guild member details for this guild and member. Can be undefined if not in cache or in dm. */ + guildMember?: GuildMember; + /** The url link to this message */ + link: string; + /** The role objects for all the roles that were mentioned in this message */ + mentionedRoles: (Role | undefined)[]; + /** The channel objects for all the channels that were mentioned in this message. */ + mentionedChannels: (Channel | undefined)[]; + /** The member objects for all the members that were mentioned in this message. */ + mentionedMembers: (Member | undefined)[]; + + // METHODS + + /** Delete the message */ + delete(reason?: string, delayMilliseconds?: number): Promise; + /** Edit the message */ + edit(content: string | MessageContent): Promise; + /** Pins the message in the channel */ + pin(): Promise; + /** Add a reaction to the message */ + addReaction(reaction: string): Promise; + /** Add multiple reactions to the message without or without order. */ + addReactions(reactions: string[], ordered?: boolean): Promise; + /** Send a inline reply to this message */ + sendResponse(content: string | MessageContent): Promise; + /** Send a message to this channel where this message is */ + reply(content: string | MessageContent): Promise; + /** Send a message to this channel and then delete it after a bit. By default it will delete after 10 seconds with no reason provided. */ + alert( + content: string | MessageContent, + timeout?: number, + reason?: string, + ): Promise; + /** Send a inline reply to this message but then delete it after a bit. By default it will delete after 10 seconds with no reason provided. */ + alertResponse( + content: string | MessageContent, + timeout?: number, + reason?: string, + ): Promise; + /** Remove all reactions */ + removeAllReactions(): Promise; + /** Remove all reactions */ + removeReactionEmoji(reaction: string): Promise; + /** Remove all reactions */ + removeReaction(reaction: string): Promise; +} diff --git a/src/api/structures/mod.ts b/src/api/structures/mod.ts index 5ad8b97ba..8a2896232 100644 --- a/src/api/structures/mod.ts +++ b/src/api/structures/mod.ts @@ -17,9 +17,10 @@ export let structures = { export type Structures = typeof structures; -/** This function is used to update/reload/customize the internal structure of Discordeno. +/** This function is used to update/reload/customize the internal structures of Discordeno. * - * ⚠️ **ADVANCED USE ONLY: If you customize this in a wrong way, you could potentially create many new errors/bugs. Please take caution when using this. + * ⚠️ **ADVANCED USE ONLY: If you customize this incorrectly, you could potentially create many new errors/bugs. + * Please take caution when using this.** */ export function updateStructures(newStructures: Structures) { structures = { diff --git a/src/api/structures/role.ts b/src/api/structures/role.ts index ae1e1db50..f3bacba3d 100644 --- a/src/api/structures/role.ts +++ b/src/api/structures/role.ts @@ -1,20 +1,130 @@ -import { RoleData, Unpromise } from "../../types/types.ts"; +import { CreateRoleOptions, RoleData } from "../../types/types.ts"; +import { cache } from "../../util/cache.ts"; +import { Collection } from "../../util/collection.ts"; +import { createNewProp } from "../../util/utils.ts"; +import { deleteRole, editRole } from "../handlers/guild.ts"; +import { Guild } from "./guild.ts"; +import { Member } from "./member.ts"; + +const baseRole: any = { + get guild() { + return cache.guilds.find((g) => g.roles.has(this.id)); + }, + get hexColor() { + return this.color!.toString(16); + }, + get members() { + return cache.members.filter((m) => + m.guilds.some((g) => g.roles.includes(this.id)) + ); + }, + get mention() { + return `<@&${this.id}>`; + }, + + // METHODS + delete(guildID?: string) { + // If not guild id was provided try and find one + if (!guildID) guildID = guildID || this.guild?.id; + // If a guild id is still not available error out + if (!guildID) { + throw new Error( + "role.delete() did not find a valid guild in cache. Please provide the guildID like role.delete(guildID)", + ); + } + + return deleteRole(guildID, this.id!).catch(console.error); + }, + edit(options: CreateRoleOptions, guildID?: string) { + // If not guild id was provided try and find one + if (!guildID) guildID = guildID || this.guild?.id; + // If a guild id is still not available error out + if (!guildID) { + throw new Error( + "role.edit() did not find a valid guild in cache. Please provide the guildID like role.edit({}, guildID)", + ); + } + + return editRole(guildID, this.id!, options); + }, + higherThanRoleID(roleID: string, position?: number) { + // If no position try and find one from cache + if (!position) position = this.guild?.roles.get(roleID)?.position; + // If still none error out. + if (!position) { + throw new Error( + "role.higherThanRoleID() did not have a position provided and the role or guild was not found in cache. Please provide a position like role.higherThanRoleID(roleID, position)", + ); + } + + // Rare edge case handling + if (this.position === position) { + return this.id! < roleID; + } + + return this.position! > position; + }, +}; export async function createRole(data: RoleData) { const { tags, ...rest } = data; - const roleTags = { - botID: tags?.bot_id, - premiumSubscriber: "premium_subscriber" in (tags ?? {}), - integrationID: tags?.integration_id, - }; + const restProps: Record> = {}; + for (const key of Object.keys(rest)) { + restProps[key] = createNewProp((rest as any)[key]); + } - return { - ...rest, - /** The @ mention of the role in a string. */ - mention: `<@&${data.id}>`, - tags: roleTags, - }; + const role = Object.create(baseRole, { + ...restProps, + botID: createNewProp(tags?.bot_id), + isNitroBoostRole: createNewProp("premium_subscriber" in (tags ?? {})), + integrationID: createNewProp(tags?.integration_id), + }); + + return role as Role; } -export interface Role extends Unpromise> {} +export interface Role { + /** role id */ + id: string; + /** role name */ + name: string; + /** integer representation of hexadecimal color code */ + color: number; + /** if this role is pinned in the user listing */ + hoist: boolean; + /** position of this role */ + position: number; + /** permission bit set */ + permissions: string; + /** whether this role is managed by an integration */ + managed: boolean; + /** whether this role is mentionable */ + mentionable: boolean; + /** The bot id that is associated with this role. */ + botID?: string; + /** If this role is the nitro boost role. */ + isNitroBoostRole: boolean; + /** The integration id that is associated with this role */ + integrationID: string; + + // GETTERS + + /** The guild where this role is. If undefined, the guild is not cached */ + guild?: Guild; + /** The hex color for this role. */ + hexColor: string; + /** The cached members that have this role */ + members: Collection; + /** The @ mention of the role in a string. */ + mention: string; + + // METHODS + + /** Delete the role */ + delete(guildID?: string): Promise; + /** Edits the role */ + edit(options: CreateRoleOptions): Promise; + /** Checks if this role is higher than another role. */ + higherThanRoleID(roleID: string, position?: number): boolean; +} diff --git a/src/api/structures/template.ts b/src/api/structures/template.ts index 248af8861..cf76a1efd 100644 --- a/src/api/structures/template.ts +++ b/src/api/structures/template.ts @@ -1,4 +1,13 @@ -import { GuildTemplate } from "../../types/types.ts"; +import { GuildTemplate, UserPayload } from "../../types/types.ts"; +import { cache } from "../../util/cache.ts"; +import { createNewProp } from "../../util/utils.ts"; +import { Guild } from "./guild.ts"; + +const baseTemplate: any = { + get sourceGuild() { + return cache.guilds.get(this.sourceGuildID); + }, +}; export function createTemplate( data: GuildTemplate, @@ -14,18 +23,50 @@ export function createTemplate( ...rest } = data; - const template = { - ...rest, - usageCount, - creatorID, - createdAt, - updatedAt, - sourceGuildID, - serializedSourceGuild, - isDirty, - }; + const restProps: Record> = {}; + for (const key of Object.keys(rest)) { + restProps[key] = createNewProp((rest as any)[key]); + } + + const template = Object.create(baseTemplate, { + ...restProps, + usageCount: createNewProp(sourceGuildID), + creatorID: createNewProp(creatorID), + createdAt: createNewProp(createdAt), + updatedAt: createNewProp(updatedAt), + sourceGuildID: createNewProp(sourceGuildID), + serializedSourceGuild: createNewProp(serializedSourceGuild), + isDirty: createNewProp(isDirty), + }); return template; } -export interface Template extends ReturnType {} +export interface Template { + /** the template code (unique ID) */ + code: string; + /** template name */ + name: string; + /** the description for the template */ + description: string | null; + /** number of times this template has been used */ + usageCount: number; + /** the ID of the user who created the template */ + createdID: string; + /** the user who created the template */ + creator: UserPayload; + /** when this template was created */ + createdAt: string; + /** when this template was last synced to the source guild */ + updatedAt: string; + /** the ID of the guild this template is based on */ + sourceGuildID: string; + /** the guild snapshot this template contains */ + serializedSourceGuild: Partial; + /** whether the template has unsynced changes */ + isDirty: boolean | null; + + // GETTERS + + sourceGuild: Guild | undefined; +} diff --git a/src/rest/request_manager.ts b/src/rest/request_manager.ts index 7676daf48..8b09589ee 100644 --- a/src/rest/request_manager.ts +++ b/src/rest/request_manager.ts @@ -1,6 +1,12 @@ import { authorization, eventHandlers } from "../bot.ts"; import { Errors, HttpResponseCode, RequestMethods } from "../types/types.ts"; -import { API_VERSION, baseEndpoints, BASE_URL, IMAGE_BASE_URL, USER_AGENT } from "../util/constants.ts"; +import { + API_VERSION, + BASE_URL, + baseEndpoints, + IMAGE_BASE_URL, + USER_AGENT, +} from "../util/constants.ts"; import { delay } from "../util/utils.ts"; const pathQueues: { [key: string]: QueuedRequest[] } = {}; diff --git a/src/types/misc.ts b/src/types/misc.ts deleted file mode 100644 index 84bb25051..000000000 --- a/src/types/misc.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type Unpromise> = T extends Promise - ? K - : never; diff --git a/src/util/constants.ts b/src/util/constants.ts index 061676c2b..697780293 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -8,7 +8,8 @@ export const API_VERSION = 8; export const GATEWAY_VERSION = 8; /** https://discord.com/developers/docs/reference#user-agent */ -export const USER_AGENT = "DiscordBot (https://github.com/discordeno/discordeno, v10)"; +export const USER_AGENT = + "DiscordBot (https://github.com/discordeno/discordeno, v10)"; /** https://discord.com/developers/docs/reference#image-formatting-image-base-url */ export const IMAGE_BASE_URL = "https://cdn.discordapp.com/"; diff --git a/src/util/permissions.ts b/src/util/permissions.ts index a6f7720ed..8cc9dfe5f 100644 --- a/src/util/permissions.ts +++ b/src/util/permissions.ts @@ -244,9 +244,6 @@ export async function higherRolePosition( const guild = await cacheHandlers.get("guilds", guildID); if (!guild) return; - // If the bot is the owner of the guild, higher-role-checking is not necessary. - if (guild.ownerID === botID) return true; - const role = guild.roles.get(roleID); const otherRole = guild.roles.get(otherRoleID); if (!role || !otherRole) return; diff --git a/src/util/utils.ts b/src/util/utils.ts index 5d4d3ec0a..f274492a1 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -38,6 +38,11 @@ export async function urlToBase64(url: string) { return `data:image/${type};base64,${imageStr}`; } +/** Allows easy way to add a prop to a base object when needing to use complicated getters solution. */ +export function createNewProp(value: any): Partial { + return { configurable: true, enumerable: true, writable: true, value }; +} + export function delay(ms: number): Promise { return new Promise((res): number => setTimeout((): void => {