diff --git a/handlers/channel.ts b/handlers/channel.ts new file mode 100644 index 000000000..3f8ca59ae --- /dev/null +++ b/handlers/channel.ts @@ -0,0 +1,207 @@ +import { Permissions } from "../types/permission.ts"; +import { Channel } from "../structures/channel.ts"; +import { botHasPermission } from "../utils/permissions.ts"; +import { Errors } from "../types/errors.ts"; +import { RequestManager } from "../module/requestManager.ts"; +import { endpoints } from "../constants/discord.ts"; +import { MessageCreateOptions } from "../types/message.ts"; +import { createMessage } from "../structures/message.ts"; +import { + GetMessagesAfter, + GetMessagesBefore, + GetMessagesAround, + GetMessages, + MessageContent, + CreateInviteOptions, + ChannelEditOptions, +} from "../types/channel.ts"; +import { logYellow } from "../utils/logger.ts"; + +/** Checks if a user id or a role id has permission in this channel */ +export function hasChannelPermission( + channel: Channel, + id: string, + permissions: Permissions[], +) { + const overwrite = + channel.permission_overwrites?.find((perm) => perm.id === id) || + channel.permission_overwrites?.find((perm) => perm.id === channel.guildID); + + return permissions.every((perm) => { + if (overwrite) { + if (overwrite.deny & perm) return false; + if (overwrite.allow & perm) return true; + } + if (channel.guildID) { + return botHasPermission(channel.guildID, [perm]); + } + return false; + }); +} + +/** Fetch a single message from the server. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */ +export async function getMessage(channel: Channel, id: string) { + if (channel.guildID) { + if ( + !botHasPermission(channel.guildID, [Permissions.VIEW_CHANNEL]) + ) { + throw new Error(Errors.MISSING_VIEW_CHANNEL); + } + if ( + !botHasPermission( + channel.guildID, + [Permissions.READ_MESSAGE_HISTORY], + ) + ) { + throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); + } + } + const result = await RequestManager.get( + endpoints.CHANNEL_MESSAGE(channel.id, id), + ) as MessageCreateOptions; + return createMessage(result); +} + +/** Fetches between 2-100 messages. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */ +export async function getMessages( + channel: Channel, + options?: + | GetMessagesAfter + | GetMessagesBefore + | GetMessagesAround + | GetMessages, +) { + if (channel.guildID) { + if ( + !botHasPermission(channel.guildID, [Permissions.VIEW_CHANNEL]) + ) { + throw new Error(Errors.MISSING_VIEW_CHANNEL); + } + if ( + !botHasPermission( + channel.guildID, + [Permissions.READ_MESSAGE_HISTORY], + ) + ) { + throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY); + } + } + + if (options?.limit && options.limit > 100) return; + + const result = (await RequestManager.get( + endpoints.CHANNEL_MESSAGES(channel.id), + options, + )) as MessageCreateOptions[]; + return result.map((res) => createMessage(res)); +} + +/** Get pinned messages in this channel. */ +export async function getPins(channelID: string) { + const result = (await RequestManager.get( + endpoints.CHANNEL_PINS(channelID), + )) as MessageCreateOptions[]; + return result.map((res) => createMessage(res)); +} + +/** Send a message to the channel. Requires SEND_MESSAGES permission. */ +export async function sendMessage( + channel: Channel, + content: string | MessageContent, +) { + if (typeof content === "string") content = { content }; + + if (channel.guildID) { + if ( + !botHasPermission(channel.guildID, [Permissions.SEND_MESSAGES]) + ) { + throw new Error(Errors.MISSING_SEND_MESSAGES); + } + if ( + content.tts && + !botHasPermission( + channel.guildID, + [Permissions.SEND_TTS_MESSAGES], + ) + ) { + throw new Error(Errors.MISSING_SEND_TTS_MESSAGE); + } + } + + if (content.content && content.content.length > 2000) { + throw new Error(Errors.MESSAGE_MAX_LENGTH); + } + + const result = await RequestManager.post( + endpoints.CHANNEL_MESSAGES(channel.id), + content, + ); + + return createMessage(result as MessageCreateOptions); +} + +/** Delete messages from the channel. 2-100. Requires the MANAGE_MESSAGES permission */ +export function deleteMessages( + channel: Channel, + ids: string[], + reason?: string, +) { + if ( + channel.guildID && + !botHasPermission(channel.guildID, [Permissions.MANAGE_MESSAGES]) + ) { + throw new Error(Errors.MISSING_MANAGE_MESSAGES); + } + if (ids.length < 2) throw new Error(Errors.DELETE_MESSAGES_MIN); + + if (ids.length > 100) { + logYellow( + `This endpoint only accepts a maximum of 100 messages. Deleting the first 100 message ids provided.`, + ); + } + + return RequestManager.post(endpoints.CHANNEL_BULK_DELETE(channel.id), { + messages: ids.splice(0, 100), + reason, + }); +} + +/** Gets the invites for this channel. Requires MANAGE_CHANNEL */ +export function getChannelInvites(channel: Channel) { + if ( + channel.guildID && + !botHasPermission(channel.guildID, [Permissions.MANAGE_CHANNELS]) + ) { + throw new Error(Errors.MISSING_MANAGE_CHANNELS); + } + return RequestManager.get(endpoints.CHANNEL_INVITES(channel.id)); +} + +/** Creates a new invite for this channel. Requires CREATE_INSTANT_INVITE */ +export function createInvite(channel: Channel, options: CreateInviteOptions) { + if ( + channel.guildID && + !botHasPermission( + channel.guildID, + [Permissions.CREATE_INSTANT_INVITE], + ) + ) { + throw new Error(Errors.MISSING_CREATE_INSTANT_INVITE); + } + return RequestManager.post(endpoints.CHANNEL_INVITES(channel.id), options); +} + +/** Gets the webhooks for this channel. Requires MANAGE_WEBHOOKS */ +export function getChannelWebhooks(channel: Channel) { + if ( + channel.guildID && + !botHasPermission(channel.guildID, [Permissions.MANAGE_WEBHOOKS]) + ) { + throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); + } + return RequestManager.get(endpoints.CHANNEL_WEBHOOKS(channel.id)); +} + +export function editChannel(channel: Channel, options: ChannelEditOptions) { + return RequestManager.patch(endpoints.GUILD_CHANNELS(channel.id), options); +} diff --git a/handlers/guild.ts b/handlers/guild.ts new file mode 100644 index 000000000..106a72e5f --- /dev/null +++ b/handlers/guild.ts @@ -0,0 +1,449 @@ +import { Guild } from "../structures/guild.ts" +import { createChannel } from "../structures/channel.ts" + +import { formatImageURL } from "../utils/cdn.ts" +import { botHasPermission } from "../utils/permissions.ts" + +import { RequestManager } from "../module/requestManager.ts" + +import { endpoints } from "../constants/discord.ts" + +import { Errors } from "../types/errors.ts" +import { Permissions, Permission } from "../types/permission.ts" +import { CreateChannelOptions, ChannelCreatePayload, ChannelTypes } from "../types/channel.ts" +import { ImageFormats, ImageSize } from "../types/cdn.ts" +import { CreateEmojisOptions, PositionSwap, EditEmojisOptions, CreateRoleOptions, PrunePayload, FetchMembersOptions, GetAuditLogsOptions, EditIntegrationOptions, BanOptions, GuildEditOptions } from "../types/guild.ts" +import { RoleData } from "../types/role.ts" +import { createRole } from "../structures/role.ts" +import { Intents } from "../types/options.ts" +import { identifyPayload } from "../module/client.ts" +import { requestAllMembers } from "../module/shardingManager.ts" + +/** Gets an array of all the channels ids that are the children of this category. */ +export function categoryChildrenIDs(guild: Guild, id: string) +{ + const channelIDs: string[] = [] + guild.channels.forEach(channel => { + if (channel.parentID === id) channelIDs.push(channel.id) + }) + + return channelIDs +} + +/** The full URL of the icon from Discords CDN. Undefined when no icon is set. */ +export function guildIconURL(guild: Guild, size: ImageSize = 128, format?: ImageFormats) +{ + return guild.icon + ? formatImageURL(endpoints.GUILD_ICON(guild.id, guild.icon), size, format) + : undefined +} + +/** The full URL of the splash from Discords CDN. Undefined if no splash is set. */ +export function guildSplashURL(guild: Guild, size: ImageSize = 128, format?: ImageFormats) +{ + return guild.splash + ? formatImageURL( + endpoints.GUILD_SPLASH(guild.id, guild.splash), + size, + format, + ) + : undefined +} +/** The full URL of the banner from Discords CDN. Undefined if no banner is set. */ +export function guildBannerURL(guild: Guild, size: ImageSize = 128, format?: ImageFormats) +{ + return guild.banner + ? formatImageURL( + endpoints.GUILD_BANNER(guild.id, guild.banner), + size, + format, + ) + : undefined +} + +/** Create a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */ +export async function createGuildChannel(guild: Guild, name: string, options: CreateChannelOptions) { +if (!botHasPermission(guild.id, [Permissions.MANAGE_CHANNELS])) { + throw new Error(Errors.MISSING_MANAGE_CHANNELS); +} +const result = + (await RequestManager.post(endpoints.GUILD_CHANNELS(guild.id), { + name, + permission_overwrites: options?.permission_overwrites + ? options.permission_overwrites.map((perm) => ({ + ...perm, + allow: perm.allow.map((p) => Permissions[p]), + deny: perm.deny.map((p) => Permissions[p]), + })) + : undefined, + ...options, + type: options.type ? ChannelTypes[options.type] : undefined, + })) as ChannelCreatePayload; + +const channel = createChannel(result); +guild.channels.set(result.id, channel); +return channel; +} + +/** Returns a list of guild channel objects. +* +* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your channels will be cached in your guild.** +*/ +export function getChannels(guildID: string) { + return RequestManager.get(endpoints.GUILD_CHANNELS(guildID)); +} + +/** Modify the positions of channels on the guild. Requires MANAGE_CHANNELS permisison. */ +export function swapChannels(guildID: string, channelPositions: PositionSwap[]) { +if (channelPositions.length < 2) { + throw "You must provide atleast two channels to be swapped."; +} +return RequestManager.patch( + endpoints.GUILD_CHANNELS(guildID), + channelPositions, +); +} + +/** Returns a guild member object for the specified user. +* +* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your members will be cached in your guild.** +*/ +export function getMember(guildID: string, id: string) { + return RequestManager.get(endpoints.GUILD_MEMBER(guildID, id)); +} + +/** Create an emoji in the server. Emojis and animated emojis have a maximum file size of 256kb. Attempting to upload an emoji larger than this limit will fail and return 400 Bad Request and an error message, but not a JSON status code. */ +export function createEmoji( + guildID: string, +name: string, +image: string, +options: CreateEmojisOptions, +) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_EMOJIS]) +) { + throw new Error(Errors.MISSING_MANAGE_EMOJIS); +} +return RequestManager.post(endpoints.GUILD_EMOJIS(guildID), { + ...options, + name, + image, +}); +} + +/** Modify the given emoji. Requires the MANAGE_EMOJIS permission. */ +export function editEmoji(guildID: string, id: string, options: EditEmojisOptions) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_EMOJIS]) +) { + throw new Error(Errors.MISSING_MANAGE_EMOJIS); +} +return RequestManager.patch(endpoints.GUILD_EMOJI(guildID, id), { + name: options.name, + roles: options.roles, +}); +} + +/** Delete the given emoji. Requires the MANAGE_EMOJIS permission. Returns 204 No Content on success. */ +export function deleteEmoji(guildID: string, id: string, reason?: string) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_EMOJIS]) +) { + throw new Error(Errors.MISSING_MANAGE_EMOJIS); +} +return RequestManager.delete( + endpoints.GUILD_EMOJI(guildID, id), + { reason }, +); +} + +/** Create a new role for the guild. Requires the MANAGE_ROLES permission. */ +export async function createGuildRole(guild: Guild, options: CreateRoleOptions, reason?: string) { +if ( + !botHasPermission(guild.id, [Permissions.MANAGE_ROLES]) +) { + throw new Error(Errors.MISSING_MANAGE_ROLES); +} +const role_data = await RequestManager.post( + endpoints.GUILD_ROLES(guild.id), + { + ...options, + permissions: options.permissions?.map((perm) => Permissions[perm]), + reason, + }, +); + +const roleData = role_data as RoleData; +const role = createRole(roleData); +guild.roles.set(role.id, role); +return role; +} + +/** Edit a guild role. Requires the MANAGE_ROLES permission. */ +export function editRole(guildID: string, id: string, options: CreateRoleOptions) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_ROLES]) +) { + throw new Error(Errors.MISSING_MANAGE_ROLES); +} +return RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), options); +} + +/** Delete a guild role. Requires the MANAGE_ROLES permission. */ +export function deleteRole(guildID: string, id: string) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_ROLES]) +) { + throw new Error(Errors.MISSING_MANAGE_ROLES); +} +return RequestManager.delete(endpoints.GUILD_ROLE(guildID, id)); +} + +/** Returns a list of role objects for the guild. +* +* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your roles will be cached in your guild.** +*/ +export function getRoles(guildID: string) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_ROLES]) +) { + throw new Error(Errors.MISSING_MANAGE_ROLES); +} +return RequestManager.get(endpoints.GUILD_ROLES(guildID)); +} + +/** Modify the positions of a set of role objects for the guild. Requires the MANAGE_ROLES permission. */ +export function swapRoles(guildID: string, rolePositons: PositionSwap) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_ROLES]) +) { + throw new Error(Errors.MISSING_MANAGE_ROLES); +} +return RequestManager.patch(endpoints.GUILD_ROLES(guildID), rolePositons); +} + +/** Check how many members would be removed from the server in a prune operation. Requires the KICK_MEMBERS permission */ +export async function getPruneCount(guildID: string, days: number) { +if (days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); +if ( + !botHasPermission(guildID, [Permissions.KICK_MEMBERS]) +) { + throw new Error(Errors.MISSING_KICK_MEMBERS); +} +const result = (await RequestManager.get( + endpoints.GUILD_PRUNE(guildID), + { days }, +)) as PrunePayload; +return result.pruned; +} + +/** Begin pruning all members in the given time period */ +export function pruneMembers(guildID: string, days: number) { +if (days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); +if ( + !botHasPermission(guildID, [Permissions.KICK_MEMBERS]) +) { + throw new Error(Errors.MISSING_KICK_MEMBERS); +} +return RequestManager.post(endpoints.GUILD_PRUNE(guildID), { days }); +} + +export function fetchMembers(guild: Guild, options?: FetchMembersOptions) { +if (!(identifyPayload.intents & Intents.GUILD_MEMBERS)) { + throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS); +} + +return new Promise((resolve) => { + requestAllMembers(guild, resolve, options); +}); +} + +/** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */ +export function getAuditLogs(guildID: string, options: GetAuditLogsOptions) { +if (!botHasPermission(guildID, [Permissions.VIEW_AUDIT_LOG])) { + throw new Error(Errors.MISSING_VIEW_AUDIT_LOG); +} + +return RequestManager.get(endpoints.GUILD_AUDIT_LOGS(guildID), { + ...options, + limit: options.limit && options.limit >= 1 && options.limit <= 100 + ? options.limit + : 50, +}); +} + +/** Returns the guild embed object. Requires the MANAGE_GUILD permission. */ +export function getEmbed(guildID: string, ) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_GUILD]) +) { + throw new Error(Errors.MISSING_MANAGE_GUILD); +} +return RequestManager.get(endpoints.GUILD_EMBED(guildID)); +} + +/** Modify a guild embed object for the guild. Requires the MANAGE_GUILD permission. */ +export function editEmbed(guildID: string, enabled: boolean, channelID?: string | null) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_GUILD]) +) { + throw new Error(Errors.MISSING_MANAGE_GUILD); +} +return RequestManager.patch( + endpoints.GUILD_EMBED(guildID), + { enabled, channel_id: channelID }, +); +} + +/** Returns the code and uses of the vanity url for this server if it is enabled. Requires the MANAGE_GUILD permission. */ +export function getVanityURL(guildID: string, ) { +return RequestManager.get(endpoints.GUILD_VANITY_URL(guildID)); +} + +/** Returns a list of integrations for the guild. Requires the MANAGE_GUILD permission. */ +export function getIntegrations(guildID: string, ) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_GUILD]) +) { + throw new Error(Errors.MISSING_MANAGE_GUILD); +} +return RequestManager.get(endpoints.GUILD_INTEGRATIONS(guildID)); +} + +/** Modify the behavior and settings of an integration object for the guild. Requires the MANAGE_GUILD permission. */ +export function editIntegration(guildID: string, id: string, options: EditIntegrationOptions) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_GUILD]) +) { + throw new Error(Errors.MISSING_MANAGE_GUILD); +} +return RequestManager.patch( + endpoints.GUILD_INTEGRATION(guildID, id), + options, +); +} + +/** Delete the attached integration object for the guild with this id. Requires MANAGE_GUILD permission. */ +export function deleteIntegration(guildID: string, id: string) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_GUILD]) +) { + throw new Error(Errors.MISSING_MANAGE_GUILD); +} +return RequestManager.delete(endpoints.GUILD_INTEGRATION(guildID, id)); +} + +/** Sync an integration. Requires teh MANAGE_GUILD permission. */ +export function syncIntegration(guildID: string, id: string) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_GUILD]) +) { + throw new Error(Errors.MISSING_MANAGE_GUILD); +} +return RequestManager.post(endpoints.GUILD_INTEGRATION_SYNC(guildID, id)); +} + +/** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */ +export function getBans(guildID: string) { +if ( + !botHasPermission(guildID, [Permissions.BAN_MEMBERS]) +) { + throw new Error(Errors.MISSING_BAN_MEMBERS); +} +return RequestManager.get(endpoints.GUILD_BANS(guildID)); +} + +/** Ban a user from the guild and optionally delete previous messages sent by the user. Requires teh BAN_MEMBERS permission. */ +export function ban(guildID: string, id: string, options: BanOptions) { +if ( + !botHasPermission(guildID, [Permissions.BAN_MEMBERS]) +) { + throw new Error(Errors.MISSING_BAN_MEMBERS); +} +return RequestManager.put(endpoints.GUILD_BAN(guildID, id), options); +} + +/** Remove the ban for a user. REquires BAN_MEMBERS permission */ +export function unban(guildID: string, id: string) { +if ( + !botHasPermission(guildID, [Permissions.BAN_MEMBERS]) +) { + throw new Error(Errors.MISSING_BAN_MEMBERS); +} +return RequestManager.delete(endpoints.GUILD_BAN(guildID, id)); +} + +/** Check whether a member has certain permissions in this channel. */ +export function channelHasPermissions( +guild: Guild, +channelID: string, +memberID: string, +permissions: Permission[], +) { +if (memberID === guild.ownerID) return true; + +const member = guild.members.get(memberID); +if (!member) { + throw new Error("Invalid member id provided. This member was not found in the cache. Please fetch them with getMember on guild."); +} + +const channel = guild.channels.get(channelID); +if (!channel) { + throw new Error("Invalid channel id provided. This channel was not found in the cache."); +} + +let permissionBits = member.roles.reduce((bits, roleID) => { + const role = guild.roles.get(roleID); + if (!role) return bits; + + bits |= role.permissions; + + return bits; +}, 0); + +if (permissionBits & Permissions.ADMINISTRATOR) return true; + +return permissions.every((permission) => + permissionBits & Permissions[permission] +); +} + +/** Modify a guilds settings. Requires the MANAGE_GUILD permission. */ +export function editGuild(guildID: string, options: GuildEditOptions) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_GUILD]) +) { + throw new Error(Errors.MISSING_MANAGE_GUILD); +} +return RequestManager.patch(endpoints.GUILD(guildID), options); +} + +/** Get all the invites for this guild. Requires MANAGE_GUILD permission */ +export function getInvites(guildID: string, ) { +if ( + !botHasPermission(guildID, [Permissions.MANAGE_GUILD]) +) { + throw new Error(Errors.MISSING_MANAGE_GUILD); +} +return RequestManager.get(endpoints.GUILD_INVITES(guildID)); +} + +/** Leave a guild */ +export function leave(guildID: string, ) { +return RequestManager.delete(endpoints.GUILD_LEAVE(guildID)); +} + +/** Returns a list of voice region objects for the guild. Unlike the similar /voice route, this returns VIP servers when the guild is VIP-enabled. */ +export function getVoiceRegions(guildID: string, ) { +return RequestManager.get(endpoints.GUILD_REGIONS(guildID)); +} + +/** Returns a list of guild webhooks objects. Requires the MANAGE_WEBHOOKs permission. */ +export function getWebhooks(guildID: string, ) { +if (!botHasPermission(guildID, [Permissions.MANAGE_WEBHOOKS])) { + throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); +} + +return RequestManager.get(endpoints.GUILD_WEBHOOKS(guildID)); +} + diff --git a/handlers/member.ts b/handlers/member.ts new file mode 100644 index 000000000..0d86c3cd4 --- /dev/null +++ b/handlers/member.ts @@ -0,0 +1,173 @@ +import { Member } from "../structures/member.ts"; +import { ImageSize, ImageFormats } from "../types/cdn.ts"; +import { formatImageURL } from "../utils/cdn.ts"; +import { endpoints } from "../constants/discord.ts"; +import { + highestRole, + higherRolePosition, + botHasPermission, +} from "../utils/permissions.ts"; +import { botID } from "../module/client.ts"; +import { Guild } from "../structures/guild.ts"; +import { Permissions } from "../types/permission.ts"; +import { Errors } from "../types/errors.ts"; +import { RequestManager } from "../module/requestManager.ts"; +import { MessageContent, DMChannelCreatePayload } from "../types/channel.ts"; +import { cache } from "../utils/cache.ts"; +import { createChannel } from "../structures/channel.ts"; +import { EditMemberOptions } from "../types/member.ts"; +import { sendMessage } from "./channel.ts"; + +/** The users custom avatar or the default avatar */ +export function avatarURL( + member: Member, + size: ImageSize = 128, + format?: ImageFormats, +) { + return member.user.avatar + ? formatImageURL( + endpoints.USER_AVATAR(member.user.id, member.user.avatar), + size, + format, + ) + : endpoints.USER_DEFAULT_AVATAR(Number(member.user.discriminator) % 5); +} + +/** Add a role to the member */ +export function addRole( + guild: Guild, + memberID: string, + 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, memberID, roleID), + { reason }, + ); +} + +/** Remove a role from the member */ +export function removeRole( + guild: Guild, + memberID: string, + 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, memberID, roleID), + { reason }, + ); +} + +/** 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. */ +export async function sendDirectMessage( + memberID: string, + content: string | MessageContent, +) { + let dmChannel = cache.channels.get(memberID); + if (!dmChannel) { + // If not available in cache create a new one. + const dmChannelData = await RequestManager.post( + endpoints.USER_CREATE_DM, + { recipient_id: memberID }, + ) 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(memberID, channel); + dmChannel = channel; + } + + // If it does exist try sending a message to this user + return sendMessage(dmChannel, content); +} + +/** Kick a member from the server */ +export function kick(guild: Guild, memberID: string, reason?: string) { + const botsHighestRole = highestRole(guild.id, botID); + const membersHighestRole = highestRole(guild.id, memberID); + 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, memberID), + { reason }, + ); +} + +/** Edit the member */ +export function editMember( + guild: Guild, + memberID: string, + 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, memberID), + options, + ); +} diff --git a/handlers/message.ts b/handlers/message.ts new file mode 100644 index 000000000..ba60a2167 --- /dev/null +++ b/handlers/message.ts @@ -0,0 +1,166 @@ +import { Message, createMessage } from "../structures/message.ts"; +import { delay } from "https://deno.land/std@0.50.0/async/delay.ts"; +import { botID } from "../module/client.ts"; +import { hasChannelPermission } from "./channel.ts"; +import { Permissions } from "../types/permission.ts"; +import { Errors } from "../types/errors.ts"; +import { RequestManager } from "../module/requestManager.ts"; +import { endpoints } from "../constants/discord.ts"; +import { botHasPermission } from "../utils/permissions.ts"; +import { MessageContent } from "../types/channel.ts"; +import { UserPayload } from "../types/guild.ts"; +import { MessageCreateOptions } from "../types/message.ts"; + +/** Delete a message */ +export async function deleteMessage( + message: Message, + reason?: string, + delayMilliseconds = 0, +) { + if (message.author.id !== botID) { + // This needs to check the channels permission not the guild permission + if ( + !message.guildID || + !hasChannelPermission( + message.channel, + botID, + [Permissions.MANAGE_MESSAGES], + ) + ) { + throw new Error(Errors.MISSING_MANAGE_MESSAGES); + } + } + + if (delayMilliseconds) await delay(delayMilliseconds); + + return RequestManager.delete( + endpoints.CHANNEL_MESSAGE(message.channelID, message.id), + { reason }, + ); +} + +/** Pin a message in a channel. Requires MANAGE_MESSAGES. Max pins allowed in a channel = 50. */ +export function pin(message: Message) { + if ( + message.guildID && + !botHasPermission(message.guildID, [Permissions.MANAGE_MESSAGES]) + ) { + throw new Error(Errors.MISSING_MANAGE_MESSAGES); + } + RequestManager.put(endpoints.CHANNEL_MESSAGE(message.channelID, message.id)); +} + +/** Unpin a message in a channel. Requires MANAGE_MESSAGES. */ +export function unpin(message: Message) { + if ( + message.guildID && + !botHasPermission(message.guildID, [Permissions.MANAGE_MESSAGES]) + ) { + throw new Error(Errors.MISSING_MANAGE_MESSAGES); + } + RequestManager.delete( + endpoints.CHANNEL_MESSAGE(message.channelID, message.id), + ); +} + +/** Create a reaction for the message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. Requires READ_MESSAGE_HISTORY and ADD_REACTIONS */ +export function addReaction(message: Message, reaction: string) { + RequestManager.put( + endpoints.CHANNEL_MESSAGE_REACTION_ME( + message.channelID, + message.id, + reaction, + ), + ); +} + +/** Removes a reaction from the bot on this message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. */ +export function removeReaction(message: Message, reaction: string) { + RequestManager.delete( + endpoints.CHANNEL_MESSAGE_REACTION_ME( + message.channelID, + message.id, + reaction, + ), + ); +} + +/** Removes all reactions for all emojis on this message. */ +export function removeAllReactions(message: Message) { + if ( + message.guildID && + !botHasPermission(message.guildID, [Permissions.MANAGE_MESSAGES]) + ) { + throw new Error(Errors.MISSING_MANAGE_MESSAGES); + } + RequestManager.delete( + endpoints.CHANNEL_MESSAGE_REACTIONS(message.channelID, message.id), + ); +} + +/** Removes all reactions for a single emoji on this message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. */ +export function removeReactionEmoji(message: Message, reaction: string) { + if ( + message.guildID && + !botHasPermission(message.guildID, [Permissions.MANAGE_MESSAGES]) + ) { + throw new Error(Errors.MISSING_MANAGE_MESSAGES); + } + RequestManager.delete( + endpoints.CHANNEL_MESSAGE_REACTION(message.channelID, message.id, reaction), + ); +} + +/** Get a list of users that reacted with this emoji. */ +export async function getReactions(message: Message, reaction: string) { + const result = (await RequestManager.get( + endpoints.CHANNEL_MESSAGE_REACTION(message.channelID, message.id, reaction), + )) as UserPayload[]; + const guild = message.guild(); + + return result.map((res) => { + return guild?.members.get(res.id) || res; + }); +} + +/** Edit the message. */ +export async function editMessage( + message: Message, + content: string | MessageContent, +) { + if ( + message.author.id !== botID + ) { + throw "You can only edit a message that was sent by the bot."; + } + + if (typeof content === "string") content = { content }; + + if (message.guildID) { + if ( + !botHasPermission(message.guildID, [Permissions.SEND_MESSAGES]) + ) { + throw new Error(Errors.MISSING_SEND_MESSAGES); + } + + if ( + content.tts && + !botHasPermission( + message.guildID, + [Permissions.SEND_TTS_MESSAGES], + ) + ) { + throw new Error(Errors.MISSING_SEND_TTS_MESSAGE); + } + } + + if (content.content && content.content.length > 2000) { + throw new Error(Errors.MESSAGE_MAX_LENGTH); + } + + const result = await RequestManager.patch( + endpoints.CHANNEL_MESSAGE(message.channelID, message.id), + content, + ); + return createMessage(result as MessageCreateOptions); +} diff --git a/mod.ts b/mod.ts index beafb725c..e9e7799a3 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,5 @@ +import createClient from "./module/client.ts"; + export * from "./module/client.ts"; export * from "./module/requestManager.ts"; export * from "./module/shardingManager.ts"; @@ -8,6 +10,11 @@ export * from "./structures/member.ts"; export * from "./structures/message.ts"; export * from "./structures/role.ts"; +export * from "./handlers/channel.ts"; +export * from "./handlers/guild.ts"; +export * from "./handlers/member.ts"; +export * from "./handlers/message.ts"; + export * from "./types/activity.ts"; export * from "./types/cdn.ts"; export * from "./types/channel.ts"; @@ -27,3 +34,5 @@ export * from "./utils/cdn.ts"; export * from "./utils/logger.ts"; export * from "./utils/permissions.ts"; export * from "./utils/utils.ts"; + +export default createClient