diff --git a/src/cache.ts b/src/cache.ts index 479268ac8..3d1e85551 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -30,7 +30,7 @@ export const cache = { executedSlashCommands: new Set(), get emojis() { return new Collection( - this.guilds.reduce((a, b) => [...a, ...b.emojis.map((e) => [e.id, e])], [] as any[]) + this.guilds.reduce((a, b) => [...a, ...b.emojis.map((e, id) => [id, e])], [] as any[]) ); }, activeGuildIds: new Set(), diff --git a/src/handlers/channels/THREAD_MEMBER_UPDATE.ts b/src/handlers/channels/THREAD_MEMBER_UPDATE.ts index 8233f5f11..a84556f26 100644 --- a/src/handlers/channels/THREAD_MEMBER_UPDATE.ts +++ b/src/handlers/channels/THREAD_MEMBER_UPDATE.ts @@ -6,7 +6,8 @@ import { snowflakeToBigint } from "../../util/bigint.ts"; export async function handleThreadMemberUpdate(data: DiscordGatewayPayload) { const payload = data.d as ThreadMember; - const thread = await cacheHandlers.get("threads", snowflakeToBigint(payload.id)); + // The id field is omitted from the thread member dispatched within the GUILD_CREATE gateway event. + const thread = await cacheHandlers.get("threads", snowflakeToBigint(payload.id!)); if (!thread) return; thread.botIsMember = true; diff --git a/src/helpers/interactions/send_interaction_response.ts b/src/helpers/interactions/send_interaction_response.ts index 04b8099a2..ebf995ee6 100644 --- a/src/helpers/interactions/send_interaction_response.ts +++ b/src/helpers/interactions/send_interaction_response.ts @@ -5,13 +5,18 @@ import type { DiscordenoInteractionResponse } from "../../types/discordeno/inter import { endpoints } from "../../util/constants.ts"; import { snakelize, validateComponents } from "../../util/utils.ts"; +// TODO: v12 remove | string /** * Send a response to a users slash command. The command data will have the id and token necessary to respond. * Interaction `tokens` are valid for **15 minutes** and can be used to send followup messages. * * NOTE: By default we will suppress mentions. To enable mentions, just pass any mentions object. */ -export async function sendInteractionResponse(id: bigint, token: string, options: DiscordenoInteractionResponse) { +export async function sendInteractionResponse( + id: bigint | string, + token: string, + options: DiscordenoInteractionResponse +) { // TODO: add more options validations if (options.data?.components) validateComponents(options.data?.components); @@ -37,5 +42,9 @@ export async function sendInteractionResponse(id: bigint, token: string, options cache.executedSlashCommands.delete(token); }, 900000); - return await rest.runMethod("post", endpoints.INTERACTION_ID_TOKEN(id, token), snakelize(options)); + return await rest.runMethod( + "post", + endpoints.INTERACTION_ID_TOKEN(typeof id === "bigint" ? id : BigInt(id), token), + snakelize(options) + ); } diff --git a/src/helpers/messages/edit_message.ts b/src/helpers/messages/edit_message.ts index cfd764247..192cbe665 100644 --- a/src/helpers/messages/edit_message.ts +++ b/src/helpers/messages/edit_message.ts @@ -29,6 +29,13 @@ export async function editMessage(channelId: bigint, messageId: bigint, content: validateComponents(content.components); } + // TODO: v12 remove + if (content.embed) { + content.embeds = [content.embed, ...(content.embeds || [])]; + content.embed = undefined; + } + content.embeds?.splice(10); + if (content.content && content.content.length > 2000) { throw new Error(Errors.MESSAGE_MAX_LENGTH); } diff --git a/src/helpers/messages/send_message.ts b/src/helpers/messages/send_message.ts index d45d4a7ad..13f3664b1 100644 --- a/src/helpers/messages/send_message.ts +++ b/src/helpers/messages/send_message.ts @@ -34,7 +34,16 @@ export async function sendMessage(channelId: bigint, content: string | CreateMes const requiredPerms: Set = new Set(["SEND_MESSAGES", "VIEW_CHANNEL"]); if (content.tts) requiredPerms.add("SEND_TTS_MESSAGES"); - if (content.embed) requiredPerms.add("EMBED_LINKS"); + // TODO: v12 remove + if (content.embed) { + content.embeds = [content.embed, ...(content.embeds || [])]; + content.embed = undefined; + } + if (content.embeds?.length) { + requiredPerms.add("EMBED_LINKS"); + content.embeds?.splice(10); + } + if (content.messageReference?.messageId || content.allowedMentions?.repliedUser) { requiredPerms.add("READ_MESSAGE_HISTORY"); } diff --git a/src/helpers/misc/edit_bot_profile.ts b/src/helpers/misc/edit_bot_profile.ts index 36404ddfe..886de902f 100644 --- a/src/helpers/misc/edit_bot_profile.ts +++ b/src/helpers/misc/edit_bot_profile.ts @@ -7,9 +7,9 @@ import { urlToBase64 } from "../../util/utils.ts"; /** Modifies the bot's username or avatar. * NOTE: username: if changed may cause the bot's discriminator to be randomized. */ -export async function editBotProfile(options: { username?: string; botAvatarURL?: string }) { +export async function editBotProfile(options: { username?: string; botAvatarURL?: string | null }) { // Nothing was edited - if (!options.username && !options.botAvatarURL) return; + if (!options.username && options.botAvatarURL === undefined) return; // Check username requirements if username was provided if (options.username) { if (options.username.length > 32) { @@ -26,7 +26,7 @@ export async function editBotProfile(options: { username?: string; botAvatarURL? } } - const avatar = options?.botAvatarURL ? await urlToBase64(options?.botAvatarURL) : undefined; + const avatar = options?.botAvatarURL ? await urlToBase64(options?.botAvatarURL) : options?.botAvatarURL; return await rest.runMethod("patch", endpoints.USER_BOT, { username: options.username?.trim(), diff --git a/src/helpers/mod.ts b/src/helpers/mod.ts index 639177c4e..047c5d480 100644 --- a/src/helpers/mod.ts +++ b/src/helpers/mod.ts @@ -133,6 +133,8 @@ import { createStageInstance } from "./channels/create_stage_instance.ts"; import { updateStageInstance } from "./channels/update_stage_instance.ts"; import { getStageInstance } from "./channels/get_stage_instance.ts"; import { deleteStageInstance } from "./channels/delete_stage_instance.ts"; +import { isSlashCommand } from "./type_guards/is_slash_command.ts"; +import { connectToVoiceChannel } from "./voice/connect_to_voice_channel.ts"; export { addDiscoverySubcategory, @@ -145,6 +147,7 @@ export { batchEditSlashCommandPermissions, categoryChildren, channelOverwriteHasPermission, + connectToVoiceChannel, createChannel, createEmoji, createGuild, @@ -242,6 +245,7 @@ export { guildSplashURL, isButton, isSelectMenu, + isSlashCommand, isChannelSynced, kick, kickMember, @@ -405,6 +409,8 @@ export let helpers = { getGuildTemplates, getTemplate, syncGuildTemplate, + // voice + connectToVoiceChannel, // webhooks createWebhook, deleteWebhookMessage, diff --git a/src/helpers/type_guards/is_slash_command.ts b/src/helpers/type_guards/is_slash_command.ts new file mode 100644 index 000000000..bba72dbeb --- /dev/null +++ b/src/helpers/type_guards/is_slash_command.ts @@ -0,0 +1,7 @@ +import { Interaction, SlashCommandInteraction } from "../../types/interactions/interaction.ts"; +import { DiscordInteractionTypes } from "../../types/interactions/interaction_types.ts"; + +/** A type guard function to tell if it is a slash command interaction */ +export function isSlashCommand(interaction: Interaction): interaction is SlashCommandInteraction { + return interaction.type === DiscordInteractionTypes.ApplicationCommand; +} diff --git a/src/helpers/voice/connect_to_voice_channel.ts b/src/helpers/voice/connect_to_voice_channel.ts new file mode 100644 index 000000000..dc3257387 --- /dev/null +++ b/src/helpers/voice/connect_to_voice_channel.ts @@ -0,0 +1,21 @@ +import { DiscordGatewayOpcodes } from "../../types/codes/gateway_opcodes.ts"; +import type { UpdateVoiceState } from "../../types/voice/update_voice_state.ts"; +import { requireBotChannelPermissions } from "../../util/permissions.ts"; +import { calculateShardId } from "../../util/calculate_shard_id.ts"; +import { snakelize } from "../../util/utils.ts"; +import { ws } from "../../ws/ws.ts"; +import type { AtLeastOne } from "../../types/util.ts"; + +/** Connect or join a voice channel inside a guild. By default, the "selfDeaf" option is true. Requires `CONNECT` and `VIEW_CHANNEL` permissions. */ +export async function connectToVoiceChannel( + guildId: bigint, + channelId: bigint, + options?: AtLeastOne> +) { + await requireBotChannelPermissions(channelId, ["CONNECT", "VIEW_CHANNEL"]); + + ws.sendShardMessage(calculateShardId(guildId), { + op: DiscordGatewayOpcodes.VoiceStateUpdate, + d: snakelize({guildId, channelId, selfMute: Boolean(options?.selfMute), selfDeaf: options.selfDeaf ?? true }), + }); +} diff --git a/src/helpers/webhooks/send_webhook.ts b/src/helpers/webhooks/send_webhook.ts index 1403dfd36..6d208d725 100644 --- a/src/helpers/webhooks/send_webhook.ts +++ b/src/helpers/webhooks/send_webhook.ts @@ -17,9 +17,7 @@ export async function sendWebhook(webhookId: bigint, webhookToken: string, optio throw Error(Errors.MESSAGE_MAX_LENGTH); } - if (options.embeds && options.embeds.length > 10) { - options.embeds.splice(10); - } + options.embeds?.splice(10); if (options.allowedMentions) { if (options.allowedMentions.users?.length) { diff --git a/src/structures/member.ts b/src/structures/member.ts index db38d6734..906c7536a 100644 --- a/src/structures/member.ts +++ b/src/structures/member.ts @@ -46,7 +46,7 @@ const baseMember: Partial = { return `<@!${this.id!}>`; }, get tag() { - return `${this.username!}#${this.discriminator!}`; + return `${this.username!}#${this.discriminator!.toString().padStart(4, "0")}`; }, // METHODS @@ -232,7 +232,9 @@ export interface DiscordenoMember extends Omit & { joinedAt?: number; premiumSince?: number; diff --git a/src/structures/message.ts b/src/structures/message.ts index 74d807936..d005ddf4f 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -228,7 +228,7 @@ export async function createDiscordenoMessage(data: Message) { props.authorId = createNewProp(snowflakeToBigint(author.id)); props.isBot = createNewProp(author.bot || false); - props.tag = createNewProp(`${author.username}#${author.discriminator}`); + props.tag = createNewProp(`${author.username}#${author.discriminator.toString().padStart(4, "0")}`); // Discord doesnt give guild id for getMessage() so this will fill it in const guildIdFinal = diff --git a/src/types/channels/channel.ts b/src/types/channels/channel.ts index 971b3bee8..e672ac298 100644 --- a/src/types/channels/channel.ts +++ b/src/types/channels/channel.ts @@ -56,6 +56,6 @@ export interface Channel { threadMetadata?: ThreadMetadata; /** Thread member object for the current user, if they have joined the thread, only included on certain API endpoints */ member?: ThreadMember; - /** the default duration for newly created threads in the channel, in minutes, to automatically archive the thread after recent activity */ - defaultAutoArchiveDuration: number; + /** Default duration for newly created threads, in minutes, to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + defaultAutoArchiveDuration?: number; } diff --git a/src/types/channels/threads/thread_member.ts b/src/types/channels/threads/thread_member.ts index 38805786c..a0ea96a37 100644 --- a/src/types/channels/threads/thread_member.ts +++ b/src/types/channels/threads/thread_member.ts @@ -5,9 +5,9 @@ export interface ThreadMemberBase { export interface ThreadMember extends ThreadMemberBase { /** The id of the thread */ - id: string; + id?: string; /** The id of the user */ - userId: string; + userId?: string; /** The time the current user last joined the thread */ joinTimestamp: string; } diff --git a/src/types/codes/json_error_codes.ts b/src/types/codes/json_error_codes.ts index e02487510..56959f3ca 100644 --- a/src/types/codes/json_error_codes.ts +++ b/src/types/codes/json_error_codes.ts @@ -53,6 +53,8 @@ export enum DiscordJsonErrorCodes { MaximumNumberOfGuildChannelsReached = 30013, MaximumNumberOfAttachmentsInAMessageReached = 30015, MaximumNumberOfInvitesReached, + MaximumNumberOfAnimatedEmojisReached = 30018, + MaximumNumberOfServerMembersReached, MaximumNumberOfGuildDiscoverySubcategoriesHasBeenReached = 30030, GuildAlreadyHasTemplate = 30031, MaximumNumberOfBansForNonGuildMembersHaveBeenExceeded = 30035, @@ -106,6 +108,10 @@ export enum DiscordJsonErrorCodes { NoUsersWithDiscordTagExist = 80004, ReqctionWasBlocked = 90001, ApiResourceIsCurrentlyOverloadedTryAgainALittleLater = 130000, + AThreadHasAlreadyBeenCreatedForThisMessage = 160004, + ThreadIsLocked = 160005, + MaximumNumberOfActiveThreadsReached = 160006, + MaximumNumberOfActiveAnnouncementThreadsReached = 160007, } export type JsonErrrorCodes = DiscordJsonErrorCodes; diff --git a/src/types/invites/invite.ts b/src/types/invites/invite.ts index 422da5f74..3808de739 100644 --- a/src/types/invites/invite.ts +++ b/src/types/invites/invite.ts @@ -3,6 +3,7 @@ import { Guild } from "../guilds/guild.ts"; import { Application } from "../applications/application.ts"; import { User } from "../users/user.ts"; import { DiscordTargetTypes } from "./target_types.ts"; +import { InviteStageInstance } from "./invite_stage_instance.ts"; /** https://discord.com/developers/docs/resources/invite#invite-object */ export interface Invite { @@ -26,4 +27,6 @@ export interface Invite { approximateMemberCount?: number; /** The expiration date of this invite, returned from the `GET /invites/` endpoint when `with_expiration` is `true` */ expiresAt?: string | null; + /** Stage instance data if there is a public Stage instance in the Stage channel this invite is for */ + stageInstance?: InviteStageInstance; } diff --git a/src/types/invites/invite_stage_instance.ts b/src/types/invites/invite_stage_instance.ts new file mode 100644 index 000000000..8ad8da371 --- /dev/null +++ b/src/types/invites/invite_stage_instance.ts @@ -0,0 +1,12 @@ +import { GuildMember } from "../members/guild_member.ts"; + +export interface InviteStageInstance { + /** The members speaking in the Stage */ + members: Partial[]; + /** The number of users in the Stage */ + participantCount: number; + /** The number of users speaking in the Stage */ + speakerCount: number; + /** The topic of the Stage instance (1-120 characters) */ + topic: string; +} diff --git a/src/types/messages/create_message.ts b/src/types/messages/create_message.ts index 80a586952..9cddc7a85 100644 --- a/src/types/messages/create_message.ts +++ b/src/types/messages/create_message.ts @@ -10,8 +10,13 @@ export interface CreateMessage { content?: string; /** true if this is a TTS message */ tts?: boolean; - /** Embedded `rich` content */ + // TODO: v12 remove + /** Embedded `rich` content + * @deprecated will be removed in Discordeno v12 use embeds + */ embed?: Embed; + /** Embedded `rich` content (up to 6000 characters) */ + embeds?: Embed[]; /** Allowed mentions for the message */ allowedMentions?: AllowedMentions; /** Include to make your message a reply */ diff --git a/src/types/messages/edit_message.ts b/src/types/messages/edit_message.ts index f7919d905..2d47780e6 100644 --- a/src/types/messages/edit_message.ts +++ b/src/types/messages/edit_message.ts @@ -8,8 +8,13 @@ import { MessageComponents } from "./components/message_components.ts"; export interface EditMessage { /** The new message contents (up to 2000 characters) */ content?: string | null; - /** Embedded `rich` content */ + // TODO: v12 remove + /** Embedded `rich` content + * @deprecated will be removed in Discordeno v12 use embeds + */ embed?: Embed | null; + /** Embedded `rich` content (up to 6000 characters) */ + embeds?: Embed[] | null; /** Edit the flags of the message (only `SUPRESS_EMBEDS` can currently be set/unset) */ flags?: 4 | null; /** The contents of the file being sent/edited */ diff --git a/src/types/messages/message.ts b/src/types/messages/message.ts index 8478757a4..2edc2be1c 100644 --- a/src/types/messages/message.ts +++ b/src/types/messages/message.ts @@ -78,7 +78,10 @@ export interface Message { messageReference?: Omit; /** Message flags combined as a bitfield */ flags?: number; - /** The stickers sent with the message (bots currently can only receive messages with stickers, not send) */ + /** + * The stickers sent with the message (bots currently can only receive messages with stickers, not send) + * @deprecated + */ stickers?: MessageSticker[]; /** * The message associated with the `message_reference` diff --git a/src/types/messages/message_sticker.ts b/src/types/messages/message_sticker.ts index 1de5e69d4..92d477daf 100644 --- a/src/types/messages/message_sticker.ts +++ b/src/types/messages/message_sticker.ts @@ -1,22 +1,32 @@ import { DiscordMessageStickerFormatTypes } from "./message_sticker_format_types.ts"; +import type { User } from "../users/user.ts"; /** https://discord.com/developers/docs/resources/channel#message-object-message-sticker-structure */ export interface MessageSticker { - /** id of the sticker */ + /** Id of the sticker */ id: string; - /** id of the pack the sticker is from */ - packId: string; + /** Id of the pack the sticker is from */ + packId?: string; /** Name of the sticker */ name: string; /** Description of the sticker */ description: string; - /** A comma-separated list of tags for the sticker */ - tags?: string; + /** For guild stickers, a unicode emoji representing the sticker's expression. For Nitro stickers, a comma-separated list of related expressions */ + tags: string; /** * Sticker asset hash * Note: The URL for fetching sticker assets is currently private. + * @deprecated the value of the asset field will an empty string. */ asset: string; /** Type of sticker format */ formatType: DiscordMessageStickerFormatTypes; + /** Whether or not the sticker is available */ + available?: boolean; + /** Id of the guild that owns this sticker */ + guildId?: string; + /** The user that uploaded the sticker */ + user?: User; + /** A sticker's sort order within a pack */ + sortValue?: number; } diff --git a/src/types/util.ts b/src/types/util.ts index 551cce9b6..c25e6f7fd 100644 --- a/src/types/util.ts +++ b/src/types/util.ts @@ -149,3 +149,5 @@ export type CamelCasedPropertiesDeep = Value extends Function : { [K in keyof Value as CamelCase]: CamelCasedPropertiesDeep; }; + +export type AtLeastOne }> = Partial & U[keyof U]; diff --git a/src/util/calculate_shard_id.ts b/src/util/calculate_shard_id.ts new file mode 100644 index 000000000..12012f09f --- /dev/null +++ b/src/util/calculate_shard_id.ts @@ -0,0 +1,7 @@ +import { ws } from "../ws/ws.ts"; + +export function calculateShardId(guildId: bigint) { + if (ws.maxShards === 1) return 0; + + return Number((guildId >> 22n) % BigInt(ws.maxShards - 1)); +} diff --git a/src/util/constants.ts b/src/util/constants.ts index 3c54978db..c9ee65268 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -9,7 +9,7 @@ export const GATEWAY_VERSION = 9; // TODO: update this version /** https://github.com/discordeno/discordeno/releases */ -export const DISCORDENO_VERSION = "11.0.3"; +export const DISCORDENO_VERSION = "11.2.0"; /** https://discord.com/developers/docs/reference#user-agent */ export const USER_AGENT = `DiscordBot (https://github.com/discordeno/discordeno, v${DISCORDENO_VERSION})`;