diff --git a/src/constants/discord.ts b/src/constants/discord.ts index cac7dbc70..94c9dfbc2 100644 --- a/src/constants/discord.ts +++ b/src/constants/discord.ts @@ -77,6 +77,8 @@ export const endpoints = { GUILD_VANITY_URL: (id: string) => `${GUILDS_BASE(id)}/vanity-url`, GUILD_WEBHOOKS: (id: string) => `${GUILDS_BASE(id)}/webhooks`, + WEBHOOK: (id: string, token: string) => `${baseEndpoints.BASE_URL}/webhooks/${id}/${token}`, + // User endpoints USER_AVATAR: (id: string, icon: string) => `${baseEndpoints.CDN_URL}/avatars/${id}/${icon}`, diff --git a/src/handlers/webhook.ts b/src/handlers/webhook.ts new file mode 100644 index 000000000..3b1dfd472 --- /dev/null +++ b/src/handlers/webhook.ts @@ -0,0 +1,102 @@ +import { + WebhookCreateOptions, + WebhookPayload, + ExecuteWebhookOptions, +} from "../types/webhook.ts"; +import { botHasChannelPermissions } from "../utils/permissions.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 { createMessage } from "../structures/message.ts"; +import { MessageCreateOptions } from "../types/message.ts"; +import { urlToBase64 } from "../utils/utils.ts"; + +/** Create a new webhook. Requires the MANAGE_WEBHOOKS permission. Returns a webhook object on success. Webhook names follow our naming restrictions that can be found in our Usernames and Nicknames documentation, with the following additional stipulations: +* +* Webhook names cannot be: 'clyde' +*/ +export async function createWebhook( + channelID: string, + options: WebhookCreateOptions, +) { + if ( + !botHasChannelPermissions( + channelID, + [Permissions.MANAGE_WEBHOOKS], + ) + ) { + throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); + } + + if ( + // Specific usernames that discord does not allow + options.name === "clyde" || + // Character limit checks. [...] checks are because of js unicode length handling + [...options.name].length < 2 || [...options.name].length > 32 + ) { + throw new Error(Errors.INVALID_WEBHOOK_NAME); + } + + return RequestManager.post( + endpoints.CHANNEL_WEBHOOKS(channelID), + { + ...options, + avatar: options.avatar ? await urlToBase64(options.avatar) : undefined, + }, + ) as Promise; +} + +export async function executeWebhook( + webhookID: string, + webhookToken: string, + options: ExecuteWebhookOptions, +) { + if (!options.content && !options.file && !options.embeds) { + throw new Error(Errors.INVALID_WEBHOOK_OPTIONS); + } + + if (options.embeds && options.embeds.length > 10) { + options.embeds.splice(10); + } + + if (options.mentions) { + if (options.mentions.users?.length) { + if (options.mentions.parse.includes("users")) { + options.mentions.parse = options.mentions.parse.filter((p) => + p !== "users" + ); + } + + if (options.mentions.users.length > 100) { + options.mentions.users = options.mentions.users.slice(0, 100); + } + } + + if (options.mentions.roles?.length) { + if (options.mentions.parse.includes("roles")) { + options.mentions.parse = options.mentions.parse.filter((p) => + p !== "roles" + ); + } + + if (options.mentions.roles.length > 100) { + options.mentions.roles = options.mentions.roles.slice(0, 100); + } + } + } + + const result = await RequestManager.post( + `${endpoints.WEBHOOK(webhookID, webhookToken)}${ + options.wait ? "?wait=true" : "" + }`, + { + ...options, + allowed_mentions: options.mentions, + avatar_url: options.avatar_url, + }, + ); + if (!options.wait) return; + + return createMessage(result as MessageCreateOptions); +} diff --git a/src/types/errors.ts b/src/types/errors.ts index 9b7972d84..7e160d49f 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -28,4 +28,6 @@ export enum Errors { REQUEST_UNKNOWN_ERROR = "REQUEST_UNKNOWN_ERROR", BOTS_HIGHEST_ROLE_TOO_LOW = "BOTS_HIGHEST_ROLE_TOO_LOW", CHANNEL_NOT_IN_GUILD = "CHANNEL_NOT_IN_GUILD", + INVALID_WEBHOOK_NAME = "INVALID_WEBHOOK_NAME", + INVALID_WEBHOOK_OPTIONS = "INVALID_WEBHOOK_OPTIONS", } diff --git a/src/types/webhook.ts b/src/types/webhook.ts new file mode 100644 index 000000000..8974e6575 --- /dev/null +++ b/src/types/webhook.ts @@ -0,0 +1,61 @@ +import { UserPayload } from "./guild.ts"; +import { Embed } from "./message.ts"; + +export interface WebhookPayload { + /** The id of the webhook */ + id: string; + /** The type of the webhook */ + type: WebhookType; + /** The guild id this webhook is for */ + guild_id?: string; + /** The channel id this webhook is for */ + channel_id: string; + /** The user this webhook was created by(not returned when getting a webhook with its token) */ + user?: UserPayload; + /** The default name of the webhook */ + name?: string; + /** The default avatar of the webhook */ + avatar?: string; + /** The secure token of the webhook(returned for Incoming Webhooks) */ + token?: string; +} + +export enum WebhookType { + /** Incoming Webhooks can post messages to channels with a generated token */ + INCOMING = 1, + /** Channel Follower Webhooks are internal webhooks used with Channel Following to post new messages into channels */ + CHANNEL_FOLLOWER = 2, +} + +export interface WebhookCreateOptions { + /** Name of the webhook (1-80 characters) */ + name: string; + /** Image url for avatar image for the default webhook avatar */ + avatar?: string; +} + +export interface ExecuteWebhookOptions { + /** waits for server confirmation of message send before response, and returns the created message body (defaults to false; when false a message that is not saved does not return an error) */ + wait?: boolean; + /** the message contents (up to 2000 characters) */ + content?: string; + /** override the default username of the webhook */ + username?: string; + /** override the default avatar of the webhook*/ + avatar_url?: string; + /** true if this is a TTS message */ + tts?: boolean; + /** file contents the contents of the file being sent one of content, file, embeds */ + file?: { blob: unknown; name: string }; + /** array of up to 10 embed objects embedded rich content. */ + embeds?: Embed[]; + /** allowed mentions for the message */ + mentions?: { + /** An array of allowed mention types to parse from the content. */ + parse: ("roles" | "users" | "everyone")[]; + /** Array of role_ids to mention (Max size of 100) */ + roles?: string[]; + /** Array of user_ids to mention (Max size of 100) */ + users?: string[]; + }; +}