diff --git a/packages/bot/src/bot.ts b/packages/bot/src/bot.ts index 716c72d75..9362df425 100644 --- a/packages/bot/src/bot.ts +++ b/packages/bot/src/bot.ts @@ -1,39 +1,192 @@ -import { calculateShardId, createGatewayManager, CreateShardManager, ShardSocketCloseCodes } from '@discordeno/gateway' +import { + calculateShardId, + createGatewayManager, + CreateShardManager, + ShardSocketCloseCodes +} from '@discordeno/gateway' import { createRestManager, CreateRestManagerOptions } from '@discordeno/rest' import { - AllowedMentions, BigString, DiscordActivity, DiscordAllowedMentions, DiscordApplication, DiscordApplicationCommand, DiscordApplicationCommandOption, DiscordApplicationCommandOptionChoice, DiscordAttachment, DiscordAuditLogEntry, DiscordAutoModerationActionExecution, DiscordAutoModerationRule, DiscordChannel, DiscordComponent, DiscordCreateApplicationCommand, DiscordEmbed, DiscordEmoji, DiscordGatewayPayload, DiscordGetGatewayBot, DiscordGuild, DiscordGuildApplicationCommandPermissions, DiscordGuildWidget, DiscordGuildWidgetSettings, DiscordIntegrationCreateUpdate, DiscordInteraction, DiscordInteractionDataOption, DiscordInteractionResponse, DiscordInviteCreate, DiscordMember, DiscordMessage, DiscordPresenceUpdate, DiscordReady, DiscordRole, DiscordScheduledEvent, DiscordStageInstance, DiscordSticker, DiscordStickerPack, DiscordTeam, DiscordTemplate, DiscordThreadMember, DiscordUser, DiscordVoiceRegion, DiscordVoiceState, DiscordWebhook, DiscordWelcomeScreen, Errors, GatewayDispatchEventNames, GatewayIntents, GetGatewayBot + AllowedMentions, + BigString, + DiscordActivity, + DiscordAllowedMentions, + DiscordApplication, + DiscordApplicationCommand, + DiscordApplicationCommandOption, + DiscordApplicationCommandOptionChoice, + DiscordAttachment, + DiscordAuditLogEntry, + DiscordAutoModerationActionExecution, + DiscordAutoModerationRule, + DiscordChannel, + DiscordComponent, + DiscordCreateApplicationCommand, + DiscordEmbed, + DiscordEmoji, + DiscordGatewayPayload, + DiscordGetGatewayBot, + DiscordGuild, + DiscordGuildApplicationCommandPermissions, + DiscordGuildWidget, + DiscordGuildWidgetSettings, + DiscordIntegrationCreateUpdate, + DiscordInteraction, + DiscordInteractionDataOption, + DiscordInteractionResponse, + DiscordInviteCreate, + DiscordMember, + DiscordMessage, + DiscordPresenceUpdate, + DiscordReady, + DiscordRole, + DiscordScheduledEvent, + DiscordStageInstance, + DiscordSticker, + DiscordStickerPack, + DiscordTeam, + DiscordTemplate, + DiscordThreadMember, + DiscordUser, + DiscordVoiceRegion, + DiscordVoiceState, + DiscordWebhook, + DiscordWelcomeScreen, + Errors, + GatewayDispatchEventNames, + GatewayIntents, + GetGatewayBot } from '@discordeno/types' import { - baseEndpoints, bigintToSnowflake, calculateBits, calculatePermissions, CHANNEL_MENTION_REGEX, Collection, CONTEXT_MENU_COMMANDS_NAME_REGEX, delay, DISCORDENO_VERSION, DISCORD_SNOWFLAKE_REGEX, getBotIdFromToken, iconBigintToHash, iconHashToBigInt, removeTokenPrefix, SLASH_COMMANDS_NAME_REGEX, snowflakeToBigint, urlToBase64, USER_AGENT, validateLength + baseEndpoints, + bigintToSnowflake, + calculateBits, + calculatePermissions, + CHANNEL_MENTION_REGEX, + Collection, + CONTEXT_MENU_COMMANDS_NAME_REGEX, + delay, + DISCORDENO_VERSION, + DISCORD_SNOWFLAKE_REGEX, + formatImageURL, + getBotIdFromToken, + iconBigintToHash, + iconHashToBigInt, + removeTokenPrefix, + SLASH_COMMANDS_NAME_REGEX, + snowflakeToBigint, + urlToBase64, + USER_AGENT, + validateLength } from '@discordeno/utils' import * as handlers from './handlers/index.js' import * as helpers from './helpers/index.js' import { Activity, transformActivity } from './transformers/activity.js' -import { Application, transformApplication } from './transformers/application.js' -import { ApplicationCommand, transformApplicationCommand } from './transformers/applicationCommand.js' -import { ApplicationCommandOption, transformApplicationCommandOption } from './transformers/applicationCommandOption.js' -import { ApplicationCommandPermission, transformApplicationCommandPermission } from './transformers/applicationCommandPermission.js' +import { + Application, + transformApplication +} from './transformers/application.js' +import { + ApplicationCommand, + transformApplicationCommand +} from './transformers/applicationCommand.js' +import { + ApplicationCommandOption, + transformApplicationCommandOption +} from './transformers/applicationCommandOption.js' +import { + ApplicationCommandPermission, + transformApplicationCommandPermission +} from './transformers/applicationCommandPermission.js' import { Attachment, transformAttachment } from './transformers/attachment.js' -import { AuditLogEntry, transformAuditLogEntry } from './transformers/auditLogEntry.js' +import { + AuditLogEntry, + transformAuditLogEntry +} from './transformers/auditLogEntry.js' import { Component, transformComponent } from './transformers/component.js' import { Embed, transformEmbed } from './transformers/embed.js' import { Emoji, transformEmoji } from './transformers/emoji.js' import { transformGatewayBot } from './transformers/gatewayBot.js' import { - ApplicationCommandOptionChoice, AutoModerationActionExecution, AutoModerationRule, Channel, Guild, GuildWidget, GuildWidgetSettings, Integration, Interaction, InteractionDataOption, Invite, Member, Message, PresenceUpdate, Role, ScheduledEvent, StageInstance, Sticker, StickerPack, Team, Template, ThreadMember, transformActivityToDiscordActivity, transformAllowedMentionsToDiscordAllowedMentions, transformApplicationCommandOptionChoice, transformApplicationCommandOptionChoiceToDiscordApplicationCommandOptionChoice, transformApplicationCommandOptionToDiscordApplicationCommandOption, transformApplicationCommandToDiscordApplicationCommand, transformApplicationToDiscordApplication, transformAttachmentToDiscordAttachment, transformAutoModerationActionExecution, transformAutoModerationRule, transformChannel, transformComponentToDiscordComponent, transformCreateApplicationCommandToDiscordCreateApplicationCommand, transformEmbedToDiscordEmbed, transformGuild, transformIntegration, transformInteraction, transformInteractionDataOption, transformInteractionResponseToDiscordInteractionResponse, transformInvite, transformMember, transformMemberToDiscordMember, transformMessage, transformPresence, transformRole, transformScheduledEvent, transformStageInstance, transformSticker, transformStickerPack, transformTeam, transformTeamToDiscordTeam, transformTemplate, transformThreadMember, transformUser, transformUserToDiscordUser, transformVoiceRegion, transformVoiceState, transformWebhook, transformWelcomeScreen, transformWidget, transformWidgetSettings, User, VoiceRegions, VoiceState, Webhook, WelcomeScreen + ApplicationCommandOptionChoice, + AutoModerationActionExecution, + AutoModerationRule, + Channel, + Guild, + GuildWidget, + GuildWidgetSettings, + Integration, + Interaction, + InteractionDataOption, + Invite, + Member, + Message, + PresenceUpdate, + Role, + ScheduledEvent, + StageInstance, + Sticker, + StickerPack, + Team, + Template, + ThreadMember, + transformActivityToDiscordActivity, + transformAllowedMentionsToDiscordAllowedMentions, + transformApplicationCommandOptionChoice, + transformApplicationCommandOptionChoiceToDiscordApplicationCommandOptionChoice, + transformApplicationCommandOptionToDiscordApplicationCommandOption, + transformApplicationCommandToDiscordApplicationCommand, + transformApplicationToDiscordApplication, + transformAttachmentToDiscordAttachment, + transformAutoModerationActionExecution, + transformAutoModerationRule, + transformChannel, + transformComponentToDiscordComponent, + transformCreateApplicationCommandToDiscordCreateApplicationCommand, + transformEmbedToDiscordEmbed, + transformGuild, + transformIntegration, + transformInteraction, + transformInteractionDataOption, + transformInteractionResponseToDiscordInteractionResponse, + transformInvite, + transformMember, + transformMemberToDiscordMember, + transformMessage, + transformPresence, + transformRole, + transformScheduledEvent, + transformStageInstance, + transformSticker, + transformStickerPack, + transformTeam, + transformTeamToDiscordTeam, + transformTemplate, + transformThreadMember, + transformUser, + transformUserToDiscordUser, + transformVoiceRegion, + transformVoiceState, + transformWebhook, + transformWelcomeScreen, + transformWidget, + transformWidgetSettings, + User, + VoiceRegions, + VoiceState, + Webhook, + WelcomeScreen } from './transformers/index.js' -import { - CreateApplicationCommand, - InteractionResponse -} from './types.js' +import { CreateApplicationCommand, InteractionResponse } from './types.js' import { routes } from './utils/routes.js' -import { formatImageURL } from './utils/utils.js' export function createBot (options: CreateBotOptions): Bot { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const bot = { id: options.botId ?? getBotIdFromToken(options.token), - applicationId: options.applicationId ?? options.botId ?? getBotIdFromToken(options.token), + applicationId: + options.applicationId ?? + options.botId ?? + getBotIdFromToken(options.token), token: removeTokenPrefix(options.token), events: createEventHandlers(options.events ?? {}), intents: options.intents, @@ -58,7 +211,7 @@ export function createBot (options: CreateBotOptions): Bot { bot.helpers = createHelpers(bot, options.helpers ?? {}) bot.gateway = createGatewayManager({ - gatewayBot: bot.botGatewayData ?? {} as any, + gatewayBot: bot.botGatewayData ?? ({} as any), gatewayConfig: { token: options.token, intents: options.intents @@ -66,7 +219,8 @@ export function createBot (options: CreateBotOptions): Bot { debug: bot.events.debug, - handleDiscordPayload: bot.handleDiscordPayload ?? + handleDiscordPayload: + bot.handleDiscordPayload ?? async function (shard, data: DiscordGatewayPayload) { // TRIGGER RAW EVENT bot.events.raw(bot, data, shard.id) @@ -89,7 +243,7 @@ export function createBot (options: CreateBotOptions): Bot { export function createEventHandlers ( events: Partial ): EventHandlers { - function ignore (): void { } + function ignore (): void {} return { debug: events.debug ?? ignore, @@ -194,7 +348,10 @@ export interface HelperUtils { } export async function stopBot (bot: Bot): Promise { - await bot.gateway.stop(ShardSocketCloseCodes.Shutdown, 'User requested bot stop') + await bot.gateway.stop( + ShardSocketCloseCodes.Shutdown, + 'User requested bot stop' + ) return bot } @@ -214,7 +371,8 @@ export interface CreateBotOptions { helpers?: Partial } -export type UnPromise> = T extends Promise ? K +export type UnPromise> = T extends Promise + ? K : never export interface Bot { @@ -244,7 +402,7 @@ export interface Bot { export const defaultHelpers = { ...helpers } export type DefaultHelpers = typeof defaultHelpers // deno-lint-ignore no-empty-interface -export interface Helpers extends DefaultHelpers { } // Use interface for declaration merging +export interface Helpers extends DefaultHelpers {} // Use interface for declaration merging export function createHelpers ( bot: Bot, @@ -252,11 +410,9 @@ export function createHelpers ( ): FinalHelpers { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const converted = {} as FinalHelpers - for ( - const [name, fun] of Object.entries({ - ...createBaseHelpers(customHelpers ?? {}) - }) - ) { + for (const [name, fun] of Object.entries({ + ...createBaseHelpers(customHelpers ?? {}) + })) { // @ts-expect-error - TODO: make the types better converted[name as keyof FinalHelpers] = ( ...args: RemoveFirstFromTuple> @@ -268,7 +424,9 @@ export function createHelpers ( return converted } -export function createBaseHelpers (options: Partial): DefaultHelpers & Partial { +export function createBaseHelpers ( + options: Partial +): DefaultHelpers & Partial { return { ...defaultHelpers, ...options @@ -277,7 +435,10 @@ export function createBaseHelpers (options: Partial): DefaultHelpers & export interface Transformers { reverse: { - allowedMentions: (bot: Bot, payload: AllowedMentions) => DiscordAllowedMentions + allowedMentions: ( + bot: Bot, + payload: AllowedMentions + ) => DiscordAllowedMentions embed: (bot: Bot, payload: Embed) => DiscordEmbed component: (bot: Bot, payload: Component) => DiscordComponent activity: (bot: Bot, payload: Activity) => DiscordActivity @@ -286,30 +447,71 @@ export interface Transformers { team: (bot: Bot, payload: Team) => DiscordTeam application: (bot: Bot, payload: Application) => DiscordApplication snowflake: (snowflake: BigString) => string - createApplicationCommand: (bot: Bot, payload: CreateApplicationCommand) => DiscordCreateApplicationCommand - applicationCommand: (bot: Bot, payload: ApplicationCommand) => DiscordApplicationCommand - applicationCommandOption: (bot: Bot, payload: ApplicationCommandOption) => DiscordApplicationCommandOption + createApplicationCommand: ( + bot: Bot, + payload: CreateApplicationCommand + ) => DiscordCreateApplicationCommand + applicationCommand: ( + bot: Bot, + payload: ApplicationCommand + ) => DiscordApplicationCommand + applicationCommandOption: ( + bot: Bot, + payload: ApplicationCommandOption + ) => DiscordApplicationCommandOption applicationCommandOptionChoice: ( bot: Bot, - payload: ApplicationCommandOptionChoice, + payload: ApplicationCommandOptionChoice ) => DiscordApplicationCommandOptionChoice - interactionResponse: (bot: Bot, payload: InteractionResponse) => DiscordInteractionResponse + interactionResponse: ( + bot: Bot, + payload: InteractionResponse + ) => DiscordInteractionResponse attachment: (bot: Bot, payload: Attachment) => DiscordAttachment } snowflake: (snowflake: BigString) => bigint gatewayBot: (payload: DiscordGetGatewayBot) => GetGatewayBot - automodRule: (bot: Bot, payload: DiscordAutoModerationRule) => AutoModerationRule - automodActionExecution: (bot: Bot, payload: DiscordAutoModerationActionExecution) => AutoModerationActionExecution - channel: (bot: Bot, payload: { channel: DiscordChannel } & { guildId?: bigint }) => Channel - guild: (bot: Bot, payload: { guild: DiscordGuild } & { shardId: number }) => Guild + automodRule: ( + bot: Bot, + payload: DiscordAutoModerationRule + ) => AutoModerationRule + automodActionExecution: ( + bot: Bot, + payload: DiscordAutoModerationActionExecution + ) => AutoModerationActionExecution + channel: ( + bot: Bot, + payload: { channel: DiscordChannel } & { guildId?: bigint } + ) => Channel + guild: ( + bot: Bot, + payload: { guild: DiscordGuild } & { shardId: number } + ) => Guild user: (bot: Bot, payload: DiscordUser) => User - member: (bot: Bot, payload: DiscordMember, guildId: bigint, userId: bigint) => Member + member: ( + bot: Bot, + payload: DiscordMember, + guildId: bigint, + userId: bigint + ) => Member message: (bot: Bot, payload: DiscordMessage) => Message - role: (bot: Bot, payload: { role: DiscordRole } & { guildId: bigint }) => Role - voiceState: (bot: Bot, payload: { voiceState: DiscordVoiceState } & { guildId: bigint }) => VoiceState + role: ( + bot: Bot, + payload: { role: DiscordRole } & { guildId: bigint } + ) => Role + voiceState: ( + bot: Bot, + payload: { voiceState: DiscordVoiceState } & { guildId: bigint } + ) => VoiceState interaction: (bot: Bot, payload: DiscordInteraction) => Interaction - interactionDataOptions: (bot: Bot, payload: DiscordInteractionDataOption) => InteractionDataOption - integration: (bot: Bot, payload: DiscordIntegrationCreateUpdate) => Integration + interactionDataOptions: ( + bot: Bot, + payload: DiscordInteractionDataOption + ) => InteractionDataOption + integration: ( + bot: Bot, + payload: DiscordIntegrationCreateUpdate + ) => Integration invite: (bot: Bot, invite: DiscordInviteCreate) => Invite application: (bot: Bot, payload: DiscordApplication) => Application team: (bot: Bot, payload: DiscordTeam) => Team @@ -321,24 +523,33 @@ export interface Transformers { component: (bot: Bot, payload: DiscordComponent) => Component webhook: (bot: Bot, payload: DiscordWebhook) => Webhook auditLogEntry: (bot: Bot, payload: DiscordAuditLogEntry) => AuditLogEntry - applicationCommand: (bot: Bot, payload: DiscordApplicationCommand) => ApplicationCommand - applicationCommandOption: (bot: Bot, payload: DiscordApplicationCommandOption) => ApplicationCommandOption + applicationCommand: ( + bot: Bot, + payload: DiscordApplicationCommand + ) => ApplicationCommand + applicationCommandOption: ( + bot: Bot, + payload: DiscordApplicationCommandOption + ) => ApplicationCommandOption applicationCommandPermission: ( bot: Bot, - payload: DiscordGuildApplicationCommandPermissions, + payload: DiscordGuildApplicationCommandPermissions ) => ApplicationCommandPermission scheduledEvent: (bot: Bot, payload: DiscordScheduledEvent) => ScheduledEvent threadMember: (bot: Bot, payload: DiscordThreadMember) => ThreadMember welcomeScreen: (bot: Bot, payload: DiscordWelcomeScreen) => WelcomeScreen voiceRegion: (bot: Bot, payload: DiscordVoiceRegion) => VoiceRegions widget: (bot: Bot, payload: DiscordGuildWidget) => GuildWidget - widgetSettings: (bot: Bot, payload: DiscordGuildWidgetSettings) => GuildWidgetSettings + widgetSettings: ( + bot: Bot, + payload: DiscordGuildWidgetSettings + ) => GuildWidgetSettings stageInstance: (bot: Bot, payload: DiscordStageInstance) => StageInstance sticker: (bot: Bot, payload: DiscordSticker) => Sticker stickerPack: (bot: Bot, payload: DiscordStickerPack) => StickerPack applicationCommandOptionChoice: ( bot: Bot, - payload: DiscordApplicationCommandOptionChoice, + payload: DiscordApplicationCommandOptionChoice ) => ApplicationCommandOptionChoice template: (bot: Bot, payload: DiscordTemplate) => Template } @@ -347,69 +558,85 @@ export interface Transformers { export function createTransformers (options: Partial) { return { reverse: { - allowedMentions: options.reverse?.allowedMentions ?? transformAllowedMentionsToDiscordAllowedMentions, + allowedMentions: + options.reverse?.allowedMentions ?? + transformAllowedMentionsToDiscordAllowedMentions, embed: options.reverse?.embed ?? transformEmbedToDiscordEmbed, - component: options.reverse?.component ?? transformComponentToDiscordComponent, + component: + options.reverse?.component ?? transformComponentToDiscordComponent, activity: options.reverse?.activity ?? transformActivityToDiscordActivity, member: options.reverse?.member ?? transformMemberToDiscordMember, user: options.reverse?.user ?? transformUserToDiscordUser, team: options.reverse?.team ?? transformTeamToDiscordTeam, - application: options.reverse?.application ?? transformApplicationToDiscordApplication, + application: + options.reverse?.application ?? + transformApplicationToDiscordApplication, snowflake: options.reverse?.snowflake ?? bigintToSnowflake, - createApplicationCommand: options.reverse?.createApplicationCommand ?? + createApplicationCommand: + options.reverse?.createApplicationCommand ?? transformCreateApplicationCommandToDiscordCreateApplicationCommand, - applicationCommand: options.reverse?.applicationCommand ?? + applicationCommand: + options.reverse?.applicationCommand ?? transformApplicationCommandToDiscordApplicationCommand, - applicationCommandOption: options.reverse?.applicationCommandOption ?? + applicationCommandOption: + options.reverse?.applicationCommandOption ?? transformApplicationCommandOptionToDiscordApplicationCommandOption, - applicationCommandOptionChoice: options.reverse?.applicationCommandOptionChoice ?? + applicationCommandOptionChoice: + options.reverse?.applicationCommandOptionChoice ?? transformApplicationCommandOptionChoiceToDiscordApplicationCommandOptionChoice, - interactionResponse: options.reverse?.interactionResponse ?? + interactionResponse: + options.reverse?.interactionResponse ?? transformInteractionResponseToDiscordInteractionResponse, - attachment: options.reverse?.attachment ?? transformAttachmentToDiscordAttachment + attachment: + options.reverse?.attachment ?? transformAttachmentToDiscordAttachment }, - automodRule: (options.automodRule) ?? transformAutoModerationRule, - automodActionExecution: (options.automodActionExecution) ?? transformAutoModerationActionExecution, - activity: (options.activity) ?? transformActivity, - application: (options.application) ?? transformApplication, - attachment: (options.attachment) ?? transformAttachment, - channel: (options.channel) ?? transformChannel, - component: (options.component) ?? transformComponent, - embed: (options.embed) ?? transformEmbed, - emoji: (options.emoji) ?? transformEmoji, - guild: (options.guild) ?? transformGuild, - integration: (options.integration) ?? transformIntegration, - interaction: (options.interaction) ?? transformInteraction, - interactionDataOptions: (options.interactionDataOptions) ?? transformInteractionDataOption, - invite: (options.invite) ?? transformInvite, - member: (options.member) ?? transformMember, - message: (options.message) ?? transformMessage, - presence: (options.presence) ?? transformPresence, - role: (options.role) ?? transformRole, - user: (options.user) ?? transformUser, - team: (options.team) ?? transformTeam, - voiceState: (options.voiceState) ?? transformVoiceState, - snowflake: (options.snowflake) ?? snowflakeToBigint, - webhook: (options.webhook) ?? transformWebhook, - auditLogEntry: (options.auditLogEntry) ?? transformAuditLogEntry, - applicationCommand: (options.applicationCommand) ?? - transformApplicationCommand, - applicationCommandOption: (options.applicationCommandOption) ?? - transformApplicationCommandOption, - applicationCommandPermission: (options.applicationCommandPermission) ?? + automodRule: options.automodRule ?? transformAutoModerationRule, + automodActionExecution: + options.automodActionExecution ?? transformAutoModerationActionExecution, + activity: options.activity ?? transformActivity, + application: options.application ?? transformApplication, + attachment: options.attachment ?? transformAttachment, + channel: options.channel ?? transformChannel, + component: options.component ?? transformComponent, + embed: options.embed ?? transformEmbed, + emoji: options.emoji ?? transformEmoji, + guild: options.guild ?? transformGuild, + integration: options.integration ?? transformIntegration, + interaction: options.interaction ?? transformInteraction, + interactionDataOptions: + options.interactionDataOptions ?? transformInteractionDataOption, + invite: options.invite ?? transformInvite, + member: options.member ?? transformMember, + message: options.message ?? transformMessage, + presence: options.presence ?? transformPresence, + role: options.role ?? transformRole, + user: options.user ?? transformUser, + team: options.team ?? transformTeam, + voiceState: options.voiceState ?? transformVoiceState, + snowflake: options.snowflake ?? snowflakeToBigint, + webhook: options.webhook ?? transformWebhook, + auditLogEntry: options.auditLogEntry ?? transformAuditLogEntry, + applicationCommand: + options.applicationCommand ?? transformApplicationCommand, + applicationCommandOption: + options.applicationCommandOption ?? transformApplicationCommandOption, + applicationCommandPermission: + options.applicationCommandPermission ?? transformApplicationCommandPermission, - scheduledEvent: (options.scheduledEvent) ?? transformScheduledEvent, - threadMember: (options.threadMember) ?? transformThreadMember, - welcomeScreen: (options.welcomeScreen) ?? transformWelcomeScreen, - voiceRegion: (options.voiceRegion) ?? transformVoiceRegion, - widget: (options.widget) ?? transformWidget, - widgetSettings: (options.widgetSettings) ?? transformWidgetSettings, - stageInstance: (options.stageInstance) ?? transformStageInstance, - sticker: (options.sticker) ?? transformSticker, - stickerPack: (options.stickerPack) ?? transformStickerPack, - gatewayBot: (options.gatewayBot) ?? transformGatewayBot, - applicationCommandOptionChoice: (options.applicationCommandOptionChoice) ?? transformApplicationCommandOptionChoice, - template: (options.template) ?? transformTemplate + scheduledEvent: options.scheduledEvent ?? transformScheduledEvent, + threadMember: options.threadMember ?? transformThreadMember, + welcomeScreen: options.welcomeScreen ?? transformWelcomeScreen, + voiceRegion: options.voiceRegion ?? transformVoiceRegion, + widget: options.widget ?? transformWidget, + widgetSettings: options.widgetSettings ?? transformWidgetSettings, + stageInstance: options.stageInstance ?? transformStageInstance, + sticker: options.sticker ?? transformSticker, + stickerPack: options.stickerPack ?? transformStickerPack, + gatewayBot: options.gatewayBot ?? transformGatewayBot, + applicationCommandOptionChoice: + options.applicationCommandOptionChoice ?? + transformApplicationCommandOptionChoice, + template: options.template ?? transformTemplate } } @@ -418,15 +645,21 @@ export interface EventHandlers { automodRuleCreate: (bot: Bot, rule: AutoModerationRule) => unknown automodRuleUpdate: (bot: Bot, rule: AutoModerationRule) => unknown automodRuleDelete: (bot: Bot, rule: AutoModerationRule) => unknown - automodActionExecution: (bot: Bot, payload: AutoModerationActionExecution) => unknown + automodActionExecution: ( + bot: Bot, + payload: AutoModerationActionExecution + ) => unknown threadCreate: (bot: Bot, thread: Channel) => unknown threadDelete: (bot: Bot, thread: Channel) => unknown - threadMemberUpdate: (bot: Bot, payload: { - id: bigint - guildId: bigint - joinedAt: number - flags: number - }) => unknown + threadMemberUpdate: ( + bot: Bot, + payload: { + id: bigint + guildId: bigint + joinedAt: number + flags: number + } + ) => unknown threadMembersUpdate: ( bot: Bot, payload: { @@ -434,7 +667,7 @@ export interface EventHandlers { guildId: bigint addedMembers?: ThreadMember[] removedMemberIds?: bigint[] - }, + } ) => unknown threadUpdate: (bot: Bot, thread: Channel) => unknown scheduledEventCreate: (bot: Bot, event: ScheduledEvent) => unknown @@ -447,7 +680,7 @@ export interface EventHandlers { guildScheduledEventId: bigint guildId: bigint userId: bigint - }, + } ) => unknown /** Sent when a user has unsubscribed to a guild scheduled event. EXPERIMENTAL! */ scheduledEventUserRemove: ( @@ -456,7 +689,7 @@ export interface EventHandlers { guildScheduledEventId: bigint guildId: bigint userId: bigint - }, + } ) => unknown ready: ( bot: Bot, @@ -469,13 +702,13 @@ export interface EventHandlers { shard?: number[] applicationId: bigint }, - rawPayload: DiscordReady, + rawPayload: DiscordReady ) => unknown interactionCreate: (bot: Bot, interaction: Interaction) => unknown integrationCreate: (bot: Bot, integration: Integration) => unknown integrationDelete: ( bot: Bot, - payload: { id: bigint, guildId: bigint, applicationId?: bigint }, + payload: { id: bigint, guildId: bigint, applicationId?: bigint } ) => unknown integrationUpdate: (bot: Bot, payload: { guildId: bigint }) => unknown inviteCreate: (bot: Bot, invite: Invite) => unknown @@ -485,31 +718,22 @@ export interface EventHandlers { channelId: bigint guildId?: bigint code: string - }, - ) => unknown - guildMemberAdd: ( - bot: Bot, - member: Member, - user: User, + } ) => unknown + guildMemberAdd: (bot: Bot, member: Member, user: User) => unknown guildMemberRemove: (bot: Bot, user: User, guildId: bigint) => unknown - guildMemberUpdate: ( - bot: Bot, - member: Member, - user: User, - ) => unknown + guildMemberUpdate: (bot: Bot, member: Member, user: User) => unknown messageCreate: (bot: Bot, message: Message) => unknown messageDelete: ( bot: Bot, payload: { id: bigint, channelId: bigint, guildId?: bigint }, - message?: Message, + message?: Message ) => unknown - messageDeleteBulk: (bot: Bot, payload: { ids: bigint[], channelId: bigint, guildId?: bigint }) => unknown - messageUpdate: ( + messageDeleteBulk: ( bot: Bot, - message: Message, - oldMessage?: Message, + payload: { ids: bigint[], channelId: bigint, guildId?: bigint } ) => unknown + messageUpdate: (bot: Bot, message: Message, oldMessage?: Message) => unknown reactionAdd: ( bot: Bot, payload: { @@ -520,7 +744,7 @@ export interface EventHandlers { member?: Member user?: User emoji: Emoji - }, + } ) => unknown reactionRemove: ( bot: Bot, @@ -530,7 +754,7 @@ export interface EventHandlers { messageId: bigint guildId?: bigint emoji: Emoji - }, + } ) => unknown reactionRemoveEmoji: ( bot: Bot, @@ -539,7 +763,7 @@ export interface EventHandlers { messageId: bigint guildId?: bigint emoji: Emoji - }, + } ) => unknown reactionRemoveAll: ( bot: Bot, @@ -547,31 +771,28 @@ export interface EventHandlers { channelId: bigint messageId: bigint guildId?: bigint - }, + } ) => unknown presenceUpdate: ( bot: Bot, presence: PresenceUpdate, - oldPresence?: PresenceUpdate, + oldPresence?: PresenceUpdate ) => unknown voiceServerUpdate: ( bot: Bot, - payload: { token: string, endpoint?: string, guildId: bigint }, - ) => unknown - voiceStateUpdate: ( - bot: Bot, - voiceState: VoiceState, + payload: { token: string, endpoint?: string, guildId: bigint } ) => unknown + voiceStateUpdate: (bot: Bot, voiceState: VoiceState) => unknown channelCreate: (bot: Bot, channel: Channel) => unknown dispatchRequirements: ( bot: Bot, data: DiscordGatewayPayload, - shardId: number, + shardId: number ) => unknown channelDelete: (bot: Bot, channel: Channel) => unknown channelPinsUpdate: ( bot: Bot, - data: { guildId?: bigint, channelId: bigint, lastPinTimestamp?: number }, + data: { guildId?: bigint, channelId: bigint, lastPinTimestamp?: number } ) => unknown channelUpdate: (bot: Bot, channel: Channel) => unknown stageInstanceCreate: ( @@ -581,7 +802,7 @@ export interface EventHandlers { guildId: bigint channelId: bigint topic: string - }, + } ) => unknown stageInstanceDelete: ( bot: Bot, @@ -590,7 +811,7 @@ export interface EventHandlers { guildId: bigint channelId: bigint topic: string - }, + } ) => unknown stageInstanceUpdate: ( bot: Bot, @@ -599,14 +820,14 @@ export interface EventHandlers { guildId: bigint channelId: bigint topic: string - }, + } ) => unknown guildEmojisUpdate: ( bot: Bot, payload: { guildId: bigint emojis: Collection - }, + } ) => unknown guildBanAdd: (bot: Bot, user: User, guildId: bigint) => unknown guildBanRemove: (bot: Bot, user: User, guildId: bigint) => unknown @@ -615,11 +836,14 @@ export interface EventHandlers { guildUpdate: (bot: Bot, guild: Guild) => unknown raw: (bot: Bot, data: DiscordGatewayPayload, shardId: number) => unknown roleCreate: (bot: Bot, role: Role) => unknown - roleDelete: (bot: Bot, payload: { guildId: bigint, roleId: bigint }) => unknown + roleDelete: ( + bot: Bot, + payload: { guildId: bigint, roleId: bigint } + ) => unknown roleUpdate: (bot: Bot, role: Role) => unknown webhooksUpdate: ( bot: Bot, - payload: { channelId: bigint, guildId: bigint }, + payload: { channelId: bigint, guildId: bigint } ) => unknown botUpdate: (bot: Bot, user: User) => unknown typingStart: ( @@ -630,7 +854,7 @@ export interface EventHandlers { userId: bigint timestamp: number member: Member | undefined - }, + } ) => unknown } @@ -719,98 +943,109 @@ export function createBotGatewayHandlers ( // channels CHANNEL_CREATE: options.CHANNEL_CREATE ?? handlers.handleChannelCreate, CHANNEL_DELETE: options.CHANNEL_DELETE ?? handlers.handleChannelDelete, - CHANNEL_PINS_UPDATE: options.CHANNEL_PINS_UPDATE ?? - handlers.handleChannelPinsUpdate, + CHANNEL_PINS_UPDATE: + options.CHANNEL_PINS_UPDATE ?? handlers.handleChannelPinsUpdate, CHANNEL_UPDATE: options.CHANNEL_UPDATE ?? handlers.handleChannelUpdate, THREAD_CREATE: options.THREAD_CREATE ?? handlers.handleThreadCreate, THREAD_UPDATE: options.THREAD_UPDATE ?? handlers.handleThreadUpdate, THREAD_DELETE: options.THREAD_DELETE ?? handlers.handleThreadDelete, THREAD_LIST_SYNC: options.THREAD_LIST_SYNC ?? handlers.handleThreadListSync, - THREAD_MEMBERS_UPDATE: options.THREAD_MEMBERS_UPDATE ?? handlers.handleThreadMembersUpdate, - STAGE_INSTANCE_CREATE: options.STAGE_INSTANCE_CREATE ?? - handlers.handleStageInstanceCreate, - STAGE_INSTANCE_UPDATE: options.STAGE_INSTANCE_UPDATE ?? - handlers.handleStageInstanceUpdate, - STAGE_INSTANCE_DELETE: options.STAGE_INSTANCE_DELETE ?? - handlers.handleStageInstanceDelete, + THREAD_MEMBERS_UPDATE: + options.THREAD_MEMBERS_UPDATE ?? handlers.handleThreadMembersUpdate, + STAGE_INSTANCE_CREATE: + options.STAGE_INSTANCE_CREATE ?? handlers.handleStageInstanceCreate, + STAGE_INSTANCE_UPDATE: + options.STAGE_INSTANCE_UPDATE ?? handlers.handleStageInstanceUpdate, + STAGE_INSTANCE_DELETE: + options.STAGE_INSTANCE_DELETE ?? handlers.handleStageInstanceDelete, // guilds GUILD_BAN_ADD: options.GUILD_BAN_ADD ?? handlers.handleGuildBanAdd, GUILD_BAN_REMOVE: options.GUILD_BAN_REMOVE ?? handlers.handleGuildBanRemove, GUILD_CREATE: options.GUILD_CREATE ?? handlers.handleGuildCreate, GUILD_DELETE: options.GUILD_DELETE ?? handlers.handleGuildDelete, - GUILD_EMOJIS_UPDATE: options.GUILD_EMOJIS_UPDATE ?? - handlers.handleGuildEmojisUpdate, - GUILD_INTEGRATIONS_UPDATE: options.GUILD_INTEGRATIONS_UPDATE ?? + GUILD_EMOJIS_UPDATE: + options.GUILD_EMOJIS_UPDATE ?? handlers.handleGuildEmojisUpdate, + GUILD_INTEGRATIONS_UPDATE: + options.GUILD_INTEGRATIONS_UPDATE ?? handlers.handleGuildIntegrationsUpdate, GUILD_MEMBER_ADD: options.GUILD_MEMBER_ADD ?? handlers.handleGuildMemberAdd, - GUILD_MEMBER_REMOVE: options.GUILD_MEMBER_REMOVE ?? - handlers.handleGuildMemberRemove, - GUILD_MEMBER_UPDATE: options.GUILD_MEMBER_UPDATE ?? - handlers.handleGuildMemberUpdate, - GUILD_MEMBERS_CHUNK: options.GUILD_MEMBERS_CHUNK ?? - handlers.handleGuildMembersChunk, - GUILD_ROLE_CREATE: options.GUILD_ROLE_CREATE ?? - handlers.handleGuildRoleCreate, - GUILD_ROLE_DELETE: options.GUILD_ROLE_DELETE ?? - handlers.handleGuildRoleDelete, - GUILD_ROLE_UPDATE: options.GUILD_ROLE_UPDATE ?? - handlers.handleGuildRoleUpdate, + GUILD_MEMBER_REMOVE: + options.GUILD_MEMBER_REMOVE ?? handlers.handleGuildMemberRemove, + GUILD_MEMBER_UPDATE: + options.GUILD_MEMBER_UPDATE ?? handlers.handleGuildMemberUpdate, + GUILD_MEMBERS_CHUNK: + options.GUILD_MEMBERS_CHUNK ?? handlers.handleGuildMembersChunk, + GUILD_ROLE_CREATE: + options.GUILD_ROLE_CREATE ?? handlers.handleGuildRoleCreate, + GUILD_ROLE_DELETE: + options.GUILD_ROLE_DELETE ?? handlers.handleGuildRoleDelete, + GUILD_ROLE_UPDATE: + options.GUILD_ROLE_UPDATE ?? handlers.handleGuildRoleUpdate, GUILD_UPDATE: options.GUILD_UPDATE ?? handlers.handleGuildUpdate, // guild events - GUILD_SCHEDULED_EVENT_CREATE: options.GUILD_SCHEDULED_EVENT_CREATE ?? + GUILD_SCHEDULED_EVENT_CREATE: + options.GUILD_SCHEDULED_EVENT_CREATE ?? handlers.handleGuildScheduledEventCreate, - GUILD_SCHEDULED_EVENT_DELETE: options.GUILD_SCHEDULED_EVENT_DELETE ?? + GUILD_SCHEDULED_EVENT_DELETE: + options.GUILD_SCHEDULED_EVENT_DELETE ?? handlers.handleGuildScheduledEventDelete, - GUILD_SCHEDULED_EVENT_UPDATE: options.GUILD_SCHEDULED_EVENT_UPDATE ?? + GUILD_SCHEDULED_EVENT_UPDATE: + options.GUILD_SCHEDULED_EVENT_UPDATE ?? handlers.handleGuildScheduledEventUpdate, - GUILD_SCHEDULED_EVENT_USER_ADD: options.GUILD_SCHEDULED_EVENT_USER_ADD ?? + GUILD_SCHEDULED_EVENT_USER_ADD: + options.GUILD_SCHEDULED_EVENT_USER_ADD ?? handlers.handleGuildScheduledEventUserAdd, - GUILD_SCHEDULED_EVENT_USER_REMOVE: options.GUILD_SCHEDULED_EVENT_USER_REMOVE ?? + GUILD_SCHEDULED_EVENT_USER_REMOVE: + options.GUILD_SCHEDULED_EVENT_USER_REMOVE ?? handlers.handleGuildScheduledEventUserRemove, // interactions - INTERACTION_CREATE: options.INTERACTION_CREATE ?? - handlers.handleInteractionCreate, + INTERACTION_CREATE: + options.INTERACTION_CREATE ?? handlers.handleInteractionCreate, // invites INVITE_CREATE: options.INVITE_CREATE ?? handlers.handleInviteCreate, INVITE_DELETE: options.INVITE_DELETE ?? handlers.handleInviteCreate, // messages MESSAGE_CREATE: options.MESSAGE_CREATE ?? handlers.handleMessageCreate, - MESSAGE_DELETE_BULK: options.MESSAGE_DELETE_BULK ?? - handlers.handleMessageDeleteBulk, + MESSAGE_DELETE_BULK: + options.MESSAGE_DELETE_BULK ?? handlers.handleMessageDeleteBulk, MESSAGE_DELETE: options.MESSAGE_DELETE ?? handlers.handleMessageDelete, - MESSAGE_REACTION_ADD: options.MESSAGE_REACTION_ADD ?? - handlers.handleMessageReactionAdd, - MESSAGE_REACTION_REMOVE_ALL: options.MESSAGE_REACTION_REMOVE_ALL ?? + MESSAGE_REACTION_ADD: + options.MESSAGE_REACTION_ADD ?? handlers.handleMessageReactionAdd, + MESSAGE_REACTION_REMOVE_ALL: + options.MESSAGE_REACTION_REMOVE_ALL ?? handlers.handleMessageReactionRemoveAll, - MESSAGE_REACTION_REMOVE_EMOJI: options.MESSAGE_REACTION_REMOVE_EMOJI ?? + MESSAGE_REACTION_REMOVE_EMOJI: + options.MESSAGE_REACTION_REMOVE_EMOJI ?? handlers.handleMessageReactionRemoveEmoji, - MESSAGE_REACTION_REMOVE: options.MESSAGE_REACTION_REMOVE ?? - handlers.handleMessageReactionRemove, + MESSAGE_REACTION_REMOVE: + options.MESSAGE_REACTION_REMOVE ?? handlers.handleMessageReactionRemove, MESSAGE_UPDATE: options.MESSAGE_UPDATE ?? handlers.handleMessageUpdate, // presence PRESENCE_UPDATE: options.PRESENCE_UPDATE ?? handlers.handlePresenceUpdate, TYPING_START: options.TYPING_START ?? handlers.handleTypingStart, USER_UPDATE: options.USER_UPDATE ?? handlers.handleUserUpdate, // voice - VOICE_SERVER_UPDATE: options.VOICE_SERVER_UPDATE ?? - handlers.handleVoiceServerUpdate, - VOICE_STATE_UPDATE: options.VOICE_STATE_UPDATE ?? - handlers.handleVoiceStateUpdate, + VOICE_SERVER_UPDATE: + options.VOICE_SERVER_UPDATE ?? handlers.handleVoiceServerUpdate, + VOICE_STATE_UPDATE: + options.VOICE_STATE_UPDATE ?? handlers.handleVoiceStateUpdate, // webhooks WEBHOOKS_UPDATE: options.WEBHOOKS_UPDATE ?? handlers.handleWebhooksUpdate, // integrations - INTEGRATION_CREATE: options.INTEGRATION_CREATE ?? - handlers.handleIntegrationCreate, - INTEGRATION_UPDATE: options.INTEGRATION_UPDATE ?? - handlers.handleIntegrationUpdate, - INTEGRATION_DELETE: options.INTEGRATION_DELETE ?? - handlers.handleIntegrationDelete + INTEGRATION_CREATE: + options.INTEGRATION_CREATE ?? handlers.handleIntegrationCreate, + INTEGRATION_UPDATE: + options.INTEGRATION_UPDATE ?? handlers.handleIntegrationUpdate, + INTEGRATION_DELETE: + options.INTEGRATION_DELETE ?? handlers.handleIntegrationDelete } } -export type RemoveFirstFromTuple = T['length'] extends 0 ? [] - : ((...b: T) => void) extends (a: any, ...b: infer I) => void ? I +export type RemoveFirstFromTuple = T['length'] extends 0 + ? [] + : ((...b: T) => void) extends (a: any, ...b: infer I) => void + ? I : [] export type FinalHelpers = { [K in keyof Helpers]: ( diff --git a/packages/bot/src/helpers/messages/getMessages.ts b/packages/bot/src/helpers/messages/getMessages.ts index 3c159ffaf..30435a95d 100644 --- a/packages/bot/src/helpers/messages/getMessages.ts +++ b/packages/bot/src/helpers/messages/getMessages.ts @@ -1,8 +1,7 @@ import { BigString, DiscordMessage } from '@discordeno/types' -import { Collection } from '@discordeno/utils' +import { Collection, hasProperty } from '@discordeno/utils' import type { Bot } from '../../bot.js' import { Message } from '../../transformers/message.js' -import { hasProperty } from '../../utils/utils.js' /** * Gets multiple messages from a channel. @@ -67,20 +66,32 @@ export interface GetMessagesAfter extends GetMessagesLimit { after?: BigString } -export type GetMessagesOptions = GetMessagesAfter | GetMessagesBefore | GetMessagesAround | GetMessagesLimit +export type GetMessagesOptions = + | GetMessagesAfter + | GetMessagesBefore + | GetMessagesAround + | GetMessagesLimit -export function isGetMessagesAfter (options: GetMessagesOptions): options is GetMessagesAfter { +export function isGetMessagesAfter ( + options: GetMessagesOptions +): options is GetMessagesAfter { return hasProperty(options, 'after') } -export function isGetMessagesBefore (options: GetMessagesOptions): options is GetMessagesBefore { +export function isGetMessagesBefore ( + options: GetMessagesOptions +): options is GetMessagesBefore { return hasProperty(options, 'before') } -export function isGetMessagesAround (options: GetMessagesOptions): options is GetMessagesAround { +export function isGetMessagesAround ( + options: GetMessagesOptions +): options is GetMessagesAround { return hasProperty(options, 'around') } -export function isGetMessagesLimit (options: GetMessagesOptions): options is GetMessagesLimit { +export function isGetMessagesLimit ( + options: GetMessagesOptions +): options is GetMessagesLimit { return hasProperty(options, 'limit') } diff --git a/packages/bot/src/helpers/messages/reactions/deleteReactionsAll.ts b/packages/bot/src/helpers/messages/reactions/deleteReactionsAll.ts index fe7d08d8e..acb3b9c6f 100644 --- a/packages/bot/src/helpers/messages/reactions/deleteReactionsAll.ts +++ b/packages/bot/src/helpers/messages/reactions/deleteReactionsAll.ts @@ -1,5 +1,5 @@ import { BigString } from '@discordeno/types' -import type { Bot } from '../../../bot' +import type { Bot } from '../../../bot.js' /** * Deletes all reactions for all emojis from a message. @@ -17,7 +17,11 @@ import type { Bot } from '../../../bot' * * @see {@link https://discord.com/developers/docs/resources/channel#delete-all-reactions} */ -export async function deleteReactionsAll (bot: Bot, channelId: BigString, messageId: BigString): Promise { +export async function deleteReactionsAll ( + bot: Bot, + channelId: BigString, + messageId: BigString +): Promise { return await bot.rest.runMethod( bot.rest, 'DELETE', diff --git a/packages/bot/src/helpers/messages/reactions/deleteReactionsEmoji.ts b/packages/bot/src/helpers/messages/reactions/deleteReactionsEmoji.ts index ae4b7593b..53d9b1ee0 100644 --- a/packages/bot/src/helpers/messages/reactions/deleteReactionsEmoji.ts +++ b/packages/bot/src/helpers/messages/reactions/deleteReactionsEmoji.ts @@ -1,5 +1,5 @@ import { BigString } from '@discordeno/types' -import type { Bot } from '../../../bot' +import type { Bot } from '../../../bot.js' import { processReactionString } from './getReactions.js' /** @@ -30,6 +30,10 @@ export async function deleteReactionsEmoji ( return await bot.rest.runMethod( bot.rest, 'DELETE', - bot.constants.routes.CHANNEL_MESSAGE_REACTION(channelId, messageId, reaction) + bot.constants.routes.CHANNEL_MESSAGE_REACTION( + channelId, + messageId, + reaction + ) ) } diff --git a/packages/bot/src/helpers/messages/reactions/getReactions.ts b/packages/bot/src/helpers/messages/reactions/getReactions.ts index 4aaabc563..38f8795b1 100644 --- a/packages/bot/src/helpers/messages/reactions/getReactions.ts +++ b/packages/bot/src/helpers/messages/reactions/getReactions.ts @@ -1,6 +1,6 @@ import { BigString, DiscordUser } from '@discordeno/types' import { Collection } from '@discordeno/utils' -import type { Bot } from '../../../bot' +import type { Bot } from '../../../bot.js' import { User } from '../../../transformers/member.js' /** Get a list of users that reacted with this emoji. */ @@ -28,7 +28,12 @@ export async function getReactions ( const results = await bot.rest.runMethod( bot.rest, 'GET', - bot.constants.routes.CHANNEL_MESSAGE_REACTION(channelId, messageId, reaction, options) + bot.constants.routes.CHANNEL_MESSAGE_REACTION( + channelId, + messageId, + reaction, + options + ) ) return new Collection( diff --git a/packages/bot/src/index.ts b/packages/bot/src/index.ts index 1aa1d1941..0e6dc2c71 100644 --- a/packages/bot/src/index.ts +++ b/packages/bot/src/index.ts @@ -5,4 +5,3 @@ export * from './handlers/index.js' export * from './helpers/index.js' export * from './transformers/index.js' export * from './types.js' -export * from './utils/index.js' diff --git a/packages/bot/src/utils/index.ts b/packages/bot/src/utils/index.ts index 00b08e9d2..65c4c3791 100644 --- a/packages/bot/src/utils/index.ts +++ b/packages/bot/src/utils/index.ts @@ -1,2 +1 @@ export * from './routes.js' -export * from './utils.js' diff --git a/packages/bot/src/utils/utils.ts b/packages/bot/src/utils/utils.ts deleted file mode 100644 index c5423bb4a..000000000 --- a/packages/bot/src/utils/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ImageFormat, ImageSize } from '@discordeno/types' - -/** Help format an image url. */ -export function formatImageURL ( - url: string, - size: ImageSize = 128, - format?: ImageFormat -): string { - return `${url}.${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - format ?? (url.includes('/a_') ? 'gif' : 'jpg') - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }?size=${size}` -} - -// Typescript is not so good as we developers so we need this little utility function to help it out -// Taken from https://fettblog.eu/typescript-hasownproperty/ -/** TS save way to check if a property exists in an object */ -export function hasProperty ( - obj: T, - prop: Y -): obj is T & Record { - // eslint-disable-next-line no-prototype-builtins - return obj.hasOwnProperty(prop) -} diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 6644aec9b..94cc2aef9 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -62,6 +62,7 @@ import { delay, DISCORDENO_VERSION, DISCORD_SNOWFLAKE_REGEX, + formatImageURL, getBotIdFromToken, iconBigintToHash, iconHashToBigInt, @@ -170,7 +171,6 @@ import { WelcomeScreen } from './transformers/index.js' import { CreateApplicationCommand, InteractionResponse } from './types.js' -import { formatImageURL } from './utils/utils.js' export function createClient (options: CreateClientOptions): Client { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 4096e35e8..e49c755e0 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -4,4 +4,3 @@ export * from './client.js' export * from './handlers/index.js' export * from './transformers/index.js' export * from './types.js' -export * from './utils/index.js' diff --git a/packages/client/src/utils/index.ts b/packages/client/src/utils/index.ts deleted file mode 100644 index 84814912c..000000000 --- a/packages/client/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './utils.js' diff --git a/packages/client/src/utils/utils.ts b/packages/client/src/utils/utils.ts deleted file mode 100644 index c5423bb4a..000000000 --- a/packages/client/src/utils/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ImageFormat, ImageSize } from '@discordeno/types' - -/** Help format an image url. */ -export function formatImageURL ( - url: string, - size: ImageSize = 128, - format?: ImageFormat -): string { - return `${url}.${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - format ?? (url.includes('/a_') ? 'gif' : 'jpg') - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }?size=${size}` -} - -// Typescript is not so good as we developers so we need this little utility function to help it out -// Taken from https://fettblog.eu/typescript-hasownproperty/ -/** TS save way to check if a property exists in an object */ -export function hasProperty ( - obj: T, - prop: Y -): obj is T & Record { - // eslint-disable-next-line no-prototype-builtins - return obj.hasOwnProperty(prop) -} diff --git a/packages/gateway/src/index.ts b/packages/gateway/src/index.ts index e9e44d354..24bf3d3c3 100644 --- a/packages/gateway/src/index.ts +++ b/packages/gateway/src/index.ts @@ -1,5 +1,6 @@ +import { calculateShardId } from './utils/index.js' export * from '@discordeno/types' export * from '@discordeno/utils' export * from './manager/index.js' export * from './shard/index.js' -export * from './utils/index.js' +export { calculateShardId } diff --git a/packages/rest/src/helpers/channels/announcements/followAnnouncementChannel.ts b/packages/rest/src/helpers/channels/announcements/followAnnouncementChannel.ts new file mode 100644 index 000000000..151b80760 --- /dev/null +++ b/packages/rest/src/helpers/channels/announcements/followAnnouncementChannel.ts @@ -0,0 +1,34 @@ +import { BigString, DiscordFollowedChannel } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Follows an announcement channel, allowing messages posted within it to be cross-posted into the target channel. + * + * @param bot - The bot instance to use to make the request. + * @param sourceChannelId - The ID of the announcement channel to follow. + * @param targetChannelId - The ID of the target channel - the channel to cross-post to. + * @returns An instance of {@link FollowedChannel}. + * + * @remarks + * Requires the `MANAGE_WEBHOOKS` permission in the __target channel__. + * + * Fires a _Webhooks Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#follow-announcement-channel} + */ +export async function followAnnouncementChannel ( + rest: RestManager, + sourceChannelId: BigString, + targetChannelId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.CHANNEL_FOLLOW(sourceChannelId), + { + webhook_channel_id: targetChannelId + } + ) + + return rest.transformers.snowflake(result.webhook_id) +} diff --git a/packages/rest/src/helpers/channels/announcements/index.ts b/packages/rest/src/helpers/channels/announcements/index.ts new file mode 100644 index 000000000..3bab59960 --- /dev/null +++ b/packages/rest/src/helpers/channels/announcements/index.ts @@ -0,0 +1 @@ +export * from './followAnnouncementChannel.js' diff --git a/packages/rest/src/helpers/channels/createChannel.ts b/packages/rest/src/helpers/channels/createChannel.ts new file mode 100644 index 000000000..466ca81b0 --- /dev/null +++ b/packages/rest/src/helpers/channels/createChannel.ts @@ -0,0 +1,143 @@ +import { + BigString, + ChannelTypes, + DiscordChannel, + OverwriteReadable, + SortOrderTypes +} from '@discordeno/types' +import { calculateBits } from '@discordeno/utils' +import { WithReason } from '../../index.js' +import type { RestManager } from '../../restManager.js' +import { Channel } from '../../transformers/channel.js' + +/** + * Creates a channel within a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to create the channel within. + * @param options - The parameters for the creation of the channel. + * @returns An instance of the created {@link Channel}. + * + * @remarks + * Requires the `MANAGE_CHANNELS` permission. + * + * If setting permission overwrites, only the permissions the bot user has in the guild can be allowed or denied. + * + * Setting the `MANAGE_ROLES` permission is only possible for guild administrators. + * + * Fires a _Channel Create_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#create-guild-channel} + */ +export async function createChannel ( + rest: RestManager, + guildId: BigString, + options: CreateGuildChannel +): Promise { + // BITRATE IS IN THOUSANDS SO IF USER PROVIDES 32 WE CONVERT TO 32000 + if (options?.bitrate && options.bitrate < 1000) options.bitrate *= 1000 + + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.GUILD_CHANNELS(guildId), + options + ? { + name: options.name, + topic: options.topic, + bitrate: options.bitrate, + user_limit: options.userLimit, + rate_limit_per_user: options.rateLimitPerUser, + position: options.position, + parent_id: options.parentId?.toString(), + nsfw: options.nsfw, + permission_overwrites: options?.permissionOverwrites?.map( + (overwrite) => ({ + id: overwrite.id.toString(), + type: overwrite.type, + allow: overwrite.allow ? calculateBits(overwrite.allow) : null, + deny: overwrite.deny ? calculateBits(overwrite.deny) : null + }) + ), + type: options?.type ?? ChannelTypes.GuildText, + default_sort_order: options.defaultSortOrder, + reason: options.reason, + default_auto_archive_duration: options?.defaultAutoArchiveDuration, + default_reaction_emoji: options.defaultReactionEmoji + ? { + emoji_id: options.defaultReactionEmoji.emojiId + ? rest.transformers.reverse.snowflake( + options.defaultReactionEmoji.emojiId + ) + : options.defaultReactionEmoji.emojiId, + emoji_name: options.defaultReactionEmoji.emojiName + } + : undefined, + + available_tags: options.availableTags + ? options.availableTags.map((availableTag) => ({ + id: rest.transformers.reverse.snowflake(availableTag.id), + name: availableTag.name, + moderated: availableTag.moderated, + emoji_name: availableTag.emojiName, + emoji_id: availableTag.emojiId + ? rest.transformers.reverse.snowflake(availableTag.emojiId) + : undefined + })) + : undefined + } + : {} + ) + + return rest.transformers.channel(rest, { + channel: result, + guildId: rest.transformers.snowflake(guildId) + }) +} + +export interface CreateGuildChannel extends WithReason { + /** Channel name (1-100 characters) */ + name: string + /** The type of channel */ + type?: ChannelTypes + /** Channel topic (0-1024 characters) */ + topic?: string + /** The bitrate (in bits) of the voice channel (voice only) */ + bitrate?: number + /** The user limit of the voice channel (voice only) */ + userLimit?: number + /** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission `manage_messages` or `manage_channel`, are unaffected */ + rateLimitPerUser?: number + /** Sorting position of the channel */ + position?: number + /** The channel's permission overwrites */ + permissionOverwrites?: OverwriteReadable[] + /** Id of the parent category for a channel */ + parentId?: BigString + /** Whether the channel is nsfw */ + nsfw?: boolean + /** Default duration (in minutes) that clients (not the API) use for newly created threads in this channel, to determine when to automatically archive the thread after the last activity */ + defaultAutoArchiveDuration?: number + /** Emoji to show in the add reaction button on a thread in a forum channel */ + defaultReactionEmoji?: { + /** The id of a guild's custom emoji. Exactly one of `emojiId` and `emojiName` must be set. */ + emojiId?: BigString | null + /** The unicode character of the emoji. Exactly one of `emojiId` and `emojiName` must be set. */ + emojiName?: string | null + } + /** Set of tags that can be used in a forum channel */ + availableTags?: Array<{ + /** The id of the tag */ + id: BigString + /** The name of the tag (0-20 characters) */ + name: string + /** whether this tag can only be added to or removed from threads by a member with the MANAGE_THREADS permission */ + moderated: boolean + /** The id of a guild's custom emoji */ + emojiId: BigString + /** The unicode character of the emoji */ + emojiName?: string + }> + /** the default sort order type used to order posts in forum channels */ + defaultSortOrder?: SortOrderTypes | null +} diff --git a/packages/rest/src/helpers/channels/deleteChannel.ts b/packages/rest/src/helpers/channels/deleteChannel.ts new file mode 100644 index 000000000..c099bb8ac --- /dev/null +++ b/packages/rest/src/helpers/channels/deleteChannel.ts @@ -0,0 +1,42 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Deletes a channel from within a guild. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to delete. + * @returns An instance of the deleted {@link Channel}. + * + * @remarks + * For community guilds, the _Rules_, _Guidelines_ and _Community Update_ channels cannot be deleted. + * + * If the channel is a thread: + * - Requires the `MANAGE_THREADS` permission. + * + * - Fires a _Thread Delete_ gateway event. + * + * Otherwise: + * - Requires the `MANAGE_CHANNELS` permission. + * + * - ⚠️ Deleting a category channel does not delete its child channels. + * Instead, they will have their `parent_id` property removed, and a `Channel Update` gateway event will fire for each of them. + * + * - Fires a _Channel Delete_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#deleteclose-channel} + */ +export async function deleteChannel ( + rest: RestManager, + channelId: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.CHANNEL(channelId), + { + reason + } + ) +} diff --git a/packages/rest/src/helpers/channels/deleteChannelPermissionOverride.ts b/packages/rest/src/helpers/channels/deleteChannelPermissionOverride.ts new file mode 100644 index 000000000..69659b38c --- /dev/null +++ b/packages/rest/src/helpers/channels/deleteChannelPermissionOverride.ts @@ -0,0 +1,30 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Deletes a permission override for a user or role in a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to delete the permission override of. + * @param overwriteId - The ID of the permission override to delete. + * + * @remarks + * Requires the `MANAGE_ROLES` permission. + * + * Fires a _Channel Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#delete-channel-permission} + */ +export async function deleteChannelPermissionOverride ( + rest: RestManager, + channelId: BigString, + overwriteId: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.CHANNEL_OVERWRITE(channelId, overwriteId), + reason ? { reason } : undefined + ) +} diff --git a/packages/rest/src/helpers/channels/editChannel.ts b/packages/rest/src/helpers/channels/editChannel.ts new file mode 100644 index 000000000..48671d3c6 --- /dev/null +++ b/packages/rest/src/helpers/channels/editChannel.ts @@ -0,0 +1,250 @@ +import { + BigString, + ChannelTypes, + DiscordChannel, + OverwriteReadable, + SortOrderTypes, + VideoQualityModes +} from '@discordeno/types' +import { calculateBits } from '@discordeno/utils' +import { WithReason } from '../../index.js' +import type { RestManager } from '../../restManager.js' +import { Channel } from '../../transformers/channel.js' + +/** + * Edits a channel's settings. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to edit. + * @param options - The parameters for the edit of the channel. + * @returns An instance of the edited {@link Channel}. + * + * @remarks + * If editing a channel of type {@link ChannelTypes.GroupDm}: + * - Fires a _Channel Update_ gateway event. + * + * If editing a thread channel: + * - Requires the `MANAGE_THREADS` permission __unless__ if setting the `archived` property to `false` when the `locked` property is also `false`, in which case only the `SEND_MESSAGES` permission is required. + * + * - Fires a _Thread Update_ gateway event. + * + * If editing a guild channel: + * - Requires the `MANAGE_CHANNELS` permission. + * + * - If modifying permission overrides: + * - Requires the `MANAGE_ROLES` permission. + * + * - Only permissions the bot user has in the guild or parent channel can be allowed/denied __unless__ the bot user has a `MANAGE_ROLES` permission override in the channel. + * + * - If modifying a channel of type {@link ChannelTypes.GuildCategory}: + * - Fires a _Channel Update_ gateway event for each child channel impacted in this change. + * - Otherwise: + * - Fires a _Channel Update_ gateway event. + */ +export async function editChannel ( + rest: RestManager, + channelId: BigString, + options: ModifyChannel +): Promise { + if (options.name ?? options.topic) { + const request = editChannelNameTopicQueue.get(channelId) + if (request == null) { + // If this hasn't been done before simply add 1 for it + editChannelNameTopicQueue.set(channelId, { + channelId, + amount: 1, + // 10 minutes from now + timestamp: Date.now() + 600000, + items: [] + }) + } else if (request.amount === 1) { + // Start queuing future requests to this channel + request.amount = 2 + request.timestamp = Date.now() + 600000 + } else { + return await new Promise((resolve, reject) => { + // 2 have already been used add to queue + request.items.push({ channelId, options, resolve, reject }) + if (editChannelProcessing) return + editChannelProcessing = true + processEditChannelQueue(rest) + }) + } + } + + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.CHANNEL(channelId), + { + name: options.name, + topic: options.topic, + bitrate: options.bitrate, + user_limit: options.userLimit, + rate_limit_per_user: options.rateLimitPerUser, + position: options.position, + parent_id: + options.parentId === null ? null : options.parentId?.toString(), + nsfw: options.nsfw, + type: options.type, + archived: options.archived, + auto_archive_duration: options.autoArchiveDuration, + locked: options.locked, + invitable: options.invitable, + permission_overwrites: options.permissionOverwrites + ? options.permissionOverwrites?.map((overwrite) => ({ + id: overwrite.id.toString(), + type: overwrite.type, + allow: overwrite.allow ? calculateBits(overwrite.allow) : null, + deny: overwrite.deny ? calculateBits(overwrite.deny) : null + })) + : undefined, + available_tags: options.availableTags + ? options.availableTags.map((availableTag) => ({ + id: availableTag.id, + name: availableTag.name, + moderated: availableTag.moderated, + emoji_id: availableTag.emojiId, + emoji_name: availableTag.emojiName + })) + : undefined, + applied_tags: options.appliedTags?.map((appliedTag) => + appliedTag.toString() + ), + default_reaction_emoji: options.defaultReactionEmoji + ? { + emoji_id: options.defaultReactionEmoji.emojiId, + emoji_name: options.defaultReactionEmoji.emojiName + } + : undefined, + default_sort_order: options.defaultSortOrder, + reason: options.reason + } + ) + + return rest.transformers.channel(rest, { + channel: result, + guildId: rest.transformers.snowflake(result.guild_id!) + }) +} + +interface EditChannelRequest { + amount: number + timestamp: number + channelId: BigString + items: Array<{ + channelId: BigString + options: ModifyChannel + resolve: (channel: Channel) => void + // deno-lint-ignore no-explicit-any + reject: (error: any) => void + }> +} + +const editChannelNameTopicQueue = new Map() +let editChannelProcessing = false + +function processEditChannelQueue (rest: RestManager): void { + if (!editChannelProcessing) return + + const now = Date.now() + editChannelNameTopicQueue.forEach((request) => { + (async () => { + rest.debug('Running forEach loop in edit_channel file.') + if (now < request.timestamp) return + // 10 minutes have passed so we can reset this channel again + if (request.items.length === 0) { + editChannelNameTopicQueue.delete(request.channelId) + return + } + request.amount = 0 + // There are items to process for this request + const details = request.items.shift() + + if (details == null) return + + await rest.helpers + .editChannel(details.channelId, details.options) + .then((result) => details.resolve(result)) + .catch(details.reject) + const secondDetails = request.items.shift() + if (secondDetails == null) return + + await rest.helpers + .editChannel(secondDetails.channelId, secondDetails.options) + .then((result) => secondDetails.resolve(result)) + .catch(secondDetails.reject) + })() + }) + + if (editChannelNameTopicQueue.size) { + setTimeout(() => { + rest.debug('Running setTimeout in EDIT_CHANNEL file.') + processEditChannelQueue(rest) + }, 60000) + } else { + editChannelProcessing = false + } +} + +export interface ModifyChannel extends WithReason { + /** 1-100 character channel name */ + name?: string + /** The type of channel; only conversion between text and news is supported and only in guilds with the "NEWS" feature */ + type?: ChannelTypes + /** The position of the channel in the left-hand listing */ + position?: number | null + /** 0-1024 character channel topic */ + topic?: string | null + /** Whether the channel is nsfw */ + nsfw?: boolean | null + /** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission `manage_messages` or `manage_channel`, are unaffected */ + rateLimitPerUser?: number | null + /** The bitrate (in bits) of the voice channel; 8000 to 96000 (128000 for VIP servers) */ + bitrate?: number | null + /** The user limit of the voice channel; 0 refers to no limit, 1 to 99 refers to a user limit */ + userLimit?: number | null + /** Channel or category-specific permissions */ + permissionOverwrites?: OverwriteReadable[] | null + /** Id of the new parent category for a channel */ + parentId?: BigString | null + /** Voice region id for the voice channel, automatic when set to null */ + rtcRegion?: string | null + /** The camera video quality mode of the voice channel */ + videoQualityMode?: VideoQualityModes + /** Whether the thread is archived */ + archived?: boolean + /** Duration in minutes to automatically archive the thread after recent activity */ + autoArchiveDuration?: 60 | 1440 | 4320 | 10080 + /** When a thread is locked, only users with `MANAGE_THREADS` can unarchive it */ + locked?: boolean + /** whether non-moderators can add other non-moderators to a thread; only available on private threads */ + invitable?: boolean + + /** The set of tags that can be used in a GUILD_FORUM channel */ + availableTags?: Array<{ + /** The id of the tag */ + id: string + /** The name of the tag (0-20 characters) */ + name: string + /** Whether this tag can only be added to or removed from threads by a member with the MANAGE_THREADS permission */ + moderated: boolean + /** The id of a guild's custom emoji At most one of emoji_id and emoji_name may be set. */ + emojiId: string + /** The unicode character of the emoji */ + emojiName: string + }> + /** The IDs of the set of tags that have been applied to a thread in a GUILD_FORUM channel; limited to 5 */ + appliedTags?: BigString[] + /** the emoji to show in the add reaction button on a thread in a GUILD_FORUM channel */ + defaultReactionEmoji?: { + /** The id of a guild's custom emoji */ + emojiId: string + /** The unicode character of the emoji */ + emojiName: string | null + } + /** the initial rate_limit_per_user to set on newly created threads in a channel. this field is copied to the thread at creation time and does not live update. */ + defaultThreadRateLimitPerUser?: number + /** the default sort order type used to order posts in forum channels */ + defaultSortOrder?: SortOrderTypes | null +} diff --git a/packages/rest/src/helpers/channels/editChannelPermissionOverrides.ts b/packages/rest/src/helpers/channels/editChannelPermissionOverrides.ts new file mode 100644 index 000000000..ba797abe9 --- /dev/null +++ b/packages/rest/src/helpers/channels/editChannelPermissionOverrides.ts @@ -0,0 +1,42 @@ +import { OverwriteReadable } from '@discordeno/types' +import { calculateBits } from '@discordeno/utils' +import { BigString, WithReason } from '../../index.js' +import type { RestManager } from '../../restManager.js' + +/** + * Edits the permission overrides for a user or role in a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to edit the permission overrides of. + * @param options - The permission override. + * + * @remarks + * Requires the `MANAGE_ROLES` permission. + * + * Only permissions the bot user has in the guild or parent channel can be allowed/denied __unless__ the bot user has a `MANAGE_ROLES` permission override in the channel. + * + * Fires a _Channel Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#edit-channel-permissions} + */ +export async function editChannelPermissionOverrides ( + rest: RestManager, + channelId: BigString, + options: EditChannelPermissionOverridesOptions +): Promise { + return await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.CHANNEL_OVERWRITE(channelId, options.id), + { + allow: options.allow ? calculateBits(options.allow) : '0', + deny: options.deny ? calculateBits(options.deny) : '0', + type: options.type, + reason: options.reason + } + ) +} + +export interface EditChannelPermissionOverridesOptions + extends OverwriteReadable, + WithReason {} diff --git a/packages/rest/src/helpers/channels/editChannelPositions.ts b/packages/rest/src/helpers/channels/editChannelPositions.ts new file mode 100644 index 000000000..dad909a30 --- /dev/null +++ b/packages/rest/src/helpers/channels/editChannelPositions.ts @@ -0,0 +1,52 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +export const swapChannels = editChannelPositions + +/** + * Edits the positions of a set of channels in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild in which to edit the positions of the channels. + * @param channelPositions - A set of objects defining the updated positions of the channels. + * + * @remarks + * Requires the `MANAGE_CHANNELS` permission. + * + * Fires a _Channel Update_ gateway event for every channel impacted in this change. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-guild-channel-positions} + */ +export async function editChannelPositions ( + rest: RestManager, + guildId: BigString, + channelPositions: ModifyGuildChannelPositions[] +): Promise { + if (channelPositions.length === 0) { + throw new Error('You must provide at least one channels to be moved.') + } + + return await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD_CHANNELS(guildId), + channelPositions.map((channelPosition) => ({ + id: channelPosition.id, + position: channelPosition.position, + lock_positions: channelPosition.lockPositions, + parent_id: channelPosition.parentId + })) + ) +} + +/** https://discord.com/developers/docs/resources/guild#modify-guild-channel-positions */ +export interface ModifyGuildChannelPositions { + /** Channel id */ + id: string + /** Sorting position of the channel */ + position: number | null + /** Syncs the permission overwrites with the new parent, if moving to a new category */ + lockPositions?: boolean | null + /** The new parent ID for the channel that is moved */ + parentId?: string | null +} diff --git a/packages/rest/src/helpers/channels/forums/createForumThread.ts b/packages/rest/src/helpers/channels/forums/createForumThread.ts new file mode 100644 index 000000000..1f29fc742 --- /dev/null +++ b/packages/rest/src/helpers/channels/forums/createForumThread.ts @@ -0,0 +1,88 @@ +import { + AllowedMentions, + BigString, + DiscordChannel, + FileContent, + MessageComponents, + WithReason +} from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { Channel } from '../../../transformers/channel.js' +import { Embed } from '../../../transformers/embed.js' + +/** + * Creates a new thread in a forum channel, and sends a message within the created thread. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the forum channel to create the thread within. + * @param options - The parameters for the creation of the thread. + * @returns An instance of {@link Channel} with a nested {@link Message} object. + * + * @remarks + * Requires the `CREATE_MESSAGES` permission. + * + * Fires a _Thread Create_ gateway event. + * Fires a _Message Create_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#start-thread-in-forum-channel} + * + * @experimental + */ +export async function createForumThread ( + rest: RestManager, + channelId: BigString, + options: CreateForumPostWithMessage +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.FORUM_START(channelId), + { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + rate_limit_per_user: options.rateLimitPerUser, + reason: options.reason, + + content: options.content, + embeds: options.embeds?.map((embed) => + rest.transformers.reverse.embed(rest, embed) + ), + allowed_mentions: options.allowedMentions + ? { + parse: options.allowedMentions?.parse, + roles: options.allowedMentions?.roles?.map((id) => id.toString()), + users: options.allowedMentions?.users?.map((id) => id.toString()), + replied_user: options.allowedMentions?.repliedUser + } + : undefined, + file: options.file, + components: options.components?.map((component) => + rest.transformers.reverse.component(rest, component) + ) + } + ) + + return rest.transformers.channel(rest, { + channel: result, + guildId: rest.transformers.snowflake(result.guild_id!) + }) +} + +export interface CreateForumPostWithMessage extends WithReason { + /** 1-100 character thread name */ + name: string + /** Duration in minutes to automatically archive the thread after recent activity */ + autoArchiveDuration: 60 | 1440 | 4320 | 10080 + /** Amount of seconds a user has to wait before sending another message (0-21600) */ + rateLimitPerUser?: number | null + /** The message contents (up to 2000 characters) */ + content?: string + /** Embedded `rich` content (up to 6000 characters) */ + embeds?: Embed[] + /** Allowed mentions for the message */ + allowedMentions?: AllowedMentions + /** The contents of the file being sent */ + file?: FileContent | FileContent[] + /** The components you would like to have sent in this message */ + components?: MessageComponents +} diff --git a/packages/rest/src/helpers/channels/forums/index.ts b/packages/rest/src/helpers/channels/forums/index.ts new file mode 100644 index 000000000..6849b18bf --- /dev/null +++ b/packages/rest/src/helpers/channels/forums/index.ts @@ -0,0 +1 @@ +export * from './createForumThread.js' diff --git a/packages/rest/src/helpers/channels/getChannel.ts b/packages/rest/src/helpers/channels/getChannel.ts new file mode 100644 index 000000000..307f1fc60 --- /dev/null +++ b/packages/rest/src/helpers/channels/getChannel.ts @@ -0,0 +1,34 @@ +import { BigString, DiscordChannel } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Channel } from '../../transformers/channel.js' + +/** + * Gets a channel by its ID. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to get. + * @returns An instance of {@link Channel}. + * + * @remarks + * If the channel is a thread, a {@link ThreadMember} object is included in the result. + * + * @see {@link https://discord.com/developers/docs/resources/channel#get-channel} + */ +export async function getChannel ( + rest: RestManager, + channelId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.CHANNEL(channelId) + ) + + // IF A CHANNEL DOESN'T EXIST, DISCORD RETURNS `{}` + return rest.transformers.channel(rest, { + channel: result, + guildId: result.guild_id + ? rest.transformers.snowflake(result.guild_id) + : undefined + }) +} diff --git a/packages/rest/src/helpers/channels/getChannelInvites.ts b/packages/rest/src/helpers/channels/getChannelInvites.ts new file mode 100644 index 000000000..3edb2f921 --- /dev/null +++ b/packages/rest/src/helpers/channels/getChannelInvites.ts @@ -0,0 +1,76 @@ +import { + BigString, + DiscordInviteMetadata, + TargetTypes +} from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { InviteMetadata } from '../guilds/invites/index.js' + +/** + * Gets the list of invites for a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to get the invites of. + * @returns A collection of {@link InviteMetadata} objects assorted by invite code. + * + * @remarks + * Requires the `MANAGE_CHANNELS` permission. + * + * Only usable for guild channels. + * + * @see {@link https://discord.com/developers/docs/resources/channel#get-channel-invites} + */ +export async function getChannelInvites ( + rest: RestManager, + channelId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.CHANNEL_INVITES(channelId) + ) + + return new Collection( + results.map<[string, InviteMetadata]>((result) => { + const invite = { + code: result.code, + guildId: result.guild?.id + ? rest.transformers.snowflake(result.guild.id) + : undefined, + channelId: result.channel?.id + ? rest.transformers.snowflake(result.channel.id) + : undefined, + inviter: result.inviter + ? rest.transformers.user(rest, result.inviter) + : undefined, + targetType: result.target_type + ? result.target_type === 1 + ? TargetTypes.Stream + : TargetTypes.EmbeddedApplication + : undefined, + targetUser: result.target_user + ? rest.transformers.user(rest, result.target_user) + : undefined, + targetApplicationId: result.target_application?.id + ? rest.transformers.snowflake(result.target_application.id) + : undefined, + approximatePresenceCount: result.approximate_presence_count, + approximateMemberCount: result.approximate_member_count, + expiresAt: result.expires_at + ? Date.parse(result.expires_at) + : undefined, + guildScheduledEvent: result.guild_scheduled_event + ? rest.transformers.scheduledEvent(rest, result.guild_scheduled_event) + : undefined, + // Metadata structure + uses: result.uses, + maxUses: result.max_uses, + maxAge: result.max_age, + temporary: result.temporary, + createdAt: Date.parse(result.created_at) + } + return [invite.code, invite] + }) + ) +} diff --git a/packages/rest/src/helpers/channels/getChannels.ts b/packages/rest/src/helpers/channels/getChannels.ts new file mode 100644 index 000000000..3d53cb770 --- /dev/null +++ b/packages/rest/src/helpers/channels/getChannels.ts @@ -0,0 +1,39 @@ +import { BigString, DiscordChannel } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Channel } from '../../transformers/channel.js' + +/** + * Gets the list of channels for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the channels of. + * @returns A collection of {@link Channel} objects assorted by channel ID. + * + * @remarks + * Excludes threads. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-channels} + */ +export async function getChannels ( + rest: RestManager, + guildId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_CHANNELS(guildId) + ) + + const id = rest.transformers.snowflake(guildId) + + return new Collection( + results.map((result) => { + const channel = rest.transformers.channel(rest, { + channel: result, + guildId: id + }) + return [channel.id, channel] + }) + ) +} diff --git a/packages/rest/src/helpers/channels/index.ts b/packages/rest/src/helpers/channels/index.ts new file mode 100644 index 000000000..6eb1a0513 --- /dev/null +++ b/packages/rest/src/helpers/channels/index.ts @@ -0,0 +1,14 @@ +export * from './announcements/index.js' +export * from './createChannel.js' +export * from './deleteChannel.js' +export * from './deleteChannelPermissionOverride.js' +export * from './editChannel.js' +export * from './editChannelPermissionOverrides.js' +export * from './editChannelPositions.js' +export * from './forums/index.js' +export * from './getChannel.js' +export * from './getChannelInvites.js' +export * from './getChannels.js' +export * from './stages/index.js' +export * from './threads/index.js' +export * from './triggerTypingIndicator.js' diff --git a/packages/rest/src/helpers/channels/stages/createStageInstance.ts b/packages/rest/src/helpers/channels/stages/createStageInstance.ts new file mode 100644 index 000000000..7d63792af --- /dev/null +++ b/packages/rest/src/helpers/channels/stages/createStageInstance.ts @@ -0,0 +1,44 @@ +import { DiscordStageInstance } from '@discordeno/types' +import { BigString, WithReason } from '../../../index.js' +import type { RestManager } from '../../../restManager.js' +import { StageInstance } from '../../../transformers/stageInstance.js' + +/** + * Creates a stage instance associated with a stage channel. + * + * @param bot - The bot instance to use to make the request. + * @param options - The parameters for the creation of the stage instance. + * @returns An instance of the created {@link StageInstance}. + * + * @remarks + * Requires the user to be a moderator of the stage channel. + * + * Fires a _Stage Instance Create_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/stage-instance#create-stage-instance} + */ +export async function createStageInstance ( + rest: RestManager, + options: CreateStageInstance +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.STAGE_INSTANCES(), + { + channel_id: options.channelId.toString(), + topic: options.topic, + send_start_notification: options.sendStartNotification, + reason: options.reason + } + ) + + return rest.transformers.stageInstance(rest, result) +} + +export interface CreateStageInstance extends WithReason { + channelId: BigString + topic: string + /** Notify @everyone that the stage instance has started. Requires the MENTION_EVERYONE permission. */ + sendStartNotification?: boolean +} diff --git a/packages/rest/src/helpers/channels/stages/deleteStageInstance.ts b/packages/rest/src/helpers/channels/stages/deleteStageInstance.ts new file mode 100644 index 000000000..77ca89041 --- /dev/null +++ b/packages/rest/src/helpers/channels/stages/deleteStageInstance.ts @@ -0,0 +1,28 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Deletes the stage instance associated with a stage channel, if one exists. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the stage channel the stage instance is associated with. + * + * @remarks + * Requires the user to be a moderator of the stage channel. + * + * Fires a _Stage Instance Delete_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/stage-instance#delete-stage-instance} + */ +export async function deleteStageInstance ( + rest: RestManager, + channelId: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.STAGE_INSTANCE(channelId), + reason ? { reason } : undefined + ) +} diff --git a/packages/rest/src/helpers/channels/stages/editStageInstance.ts b/packages/rest/src/helpers/channels/stages/editStageInstance.ts new file mode 100644 index 000000000..ba3243910 --- /dev/null +++ b/packages/rest/src/helpers/channels/stages/editStageInstance.ts @@ -0,0 +1,40 @@ +import { DiscordStageInstance } from '@discordeno/types' +import { BigString, WithReason } from '../../../index.js' +import type { RestManager } from '../../../restManager.js' +import { StageInstance } from '../../../transformers/stageInstance.js' + +/** + * Edits a stage instance. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the stage channel the stage instance is associated with. + * @returns An instance of the updated {@link StageInstance}. + * + * @remarks + * Requires the user to be a moderator of the stage channel. + * + * Fires a _Stage Instance Update_ event. + * + * @see {@link https://discord.com/developers/docs/resources/stage-instance#modify-stage-instance} + */ +export async function editStageInstance ( + rest: RestManager, + channelId: BigString, + data: EditStageInstanceOptions +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.STAGE_INSTANCE(channelId), + { + topic: data.topic + } + ) + + return rest.transformers.stageInstance(rest, result) +} + +export interface EditStageInstanceOptions extends WithReason { + /** The topic of the Stage instance (1-120 characters) */ + topic: string +} diff --git a/packages/rest/src/helpers/channels/stages/getStageInstance.ts b/packages/rest/src/helpers/channels/stages/getStageInstance.ts new file mode 100644 index 000000000..ecda1a2eb --- /dev/null +++ b/packages/rest/src/helpers/channels/stages/getStageInstance.ts @@ -0,0 +1,25 @@ +import { BigString, DiscordStageInstance } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { StageInstance } from '../../../transformers/stageInstance.js' + +/** + * Gets the stage instance associated with a stage channel, if one exists. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the stage channel the stage instance is associated with. + * @returns An instance of {@link StageInstance}. + * + * @see {@link https://discord.com/developers/docs/resources/stage-instance#get-stage-instance} + */ +export async function getStageInstance ( + rest: RestManager, + channelId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.STAGE_INSTANCE(channelId) + ) + + return rest.transformers.stageInstance(rest, result) +} diff --git a/packages/rest/src/helpers/channels/stages/index.ts b/packages/rest/src/helpers/channels/stages/index.ts new file mode 100644 index 000000000..daf1fbac8 --- /dev/null +++ b/packages/rest/src/helpers/channels/stages/index.ts @@ -0,0 +1,4 @@ +export * from './createStageInstance.js' +export * from './deleteStageInstance.js' +export * from './editStageInstance.js' +export * from './getStageInstance.js' diff --git a/packages/rest/src/helpers/channels/threads/addThreadMember.ts b/packages/rest/src/helpers/channels/threads/addThreadMember.ts new file mode 100644 index 000000000..bcd92754e --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/addThreadMember.ts @@ -0,0 +1,29 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Adds a member to a thread. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the thread to add the member to. + * @param userId - The user ID of the member to add to the thread. + * + * @remarks + * Requires the ability to send messages in the thread. + * Requires the thread not be archived. + * + * Fires a _Thread Members Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#add-thread-member} + */ +export async function addThreadMember ( + rest: RestManager, + channelId: BigString, + userId: BigString +): Promise { + return await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.THREAD_USER(channelId, userId) + ) +} diff --git a/packages/rest/src/helpers/channels/threads/getActiveThreads.ts b/packages/rest/src/helpers/channels/threads/getActiveThreads.ts new file mode 100644 index 000000000..fc04a5de0 --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/getActiveThreads.ts @@ -0,0 +1,50 @@ +import { BigString, DiscordListActiveThreads } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { Channel } from '../../../transformers/channel.js' +import { ThreadMember } from '../../../transformers/threadMember.js' + +/** + * Gets the list of all active threads for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the threads of. + * @returns An instance of {@link ActiveThreads}. + * + * @remarks + * Returns both public and private threads. + * + * Threads are ordered by the `id` property in descending order. + * + * @see {@link https://discord.com/developers/docs/resources/guild#list-active-guild-threads} + */ +export async function getActiveThreads ( + rest: RestManager, + guildId: BigString +): Promise { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.THREAD_ACTIVE(guildId) + ) + + return { + threads: new Collection( + results.threads.map((result) => { + const thread = rest.transformers.channel(rest, { channel: result }) + return [thread.id, thread] + }) + ), + members: new Collection( + results.members.map((result) => { + const member = rest.transformers.threadMember(rest, result) + return [member.id!, member] + }) + ) + } +} + +export interface ActiveThreads { + threads: Collection + members: Collection +} diff --git a/packages/rest/src/helpers/channels/threads/getPrivateArchivedThreads.ts b/packages/rest/src/helpers/channels/threads/getPrivateArchivedThreads.ts new file mode 100644 index 000000000..66ecb34ee --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/getPrivateArchivedThreads.ts @@ -0,0 +1,53 @@ +import { BigString, DiscordListArchivedThreads } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { + ArchivedThreads, + ListArchivedThreads +} from './getPublicArchivedThreads.js' + +/** + * Gets the list of private archived threads for a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to get the archived threads for. + * @param options - The parameters for the fetching of threads. + * @returns An instance of {@link ArchivedThreads}. + * + * @remarks + * Requires the `READ_MESSAGE_HISTORY` permission. + * Requires the `MANAGE_THREADS` permission. + * + * Returns threads of type {@link ChannelTypes.GuildPrivateThread}. + * + * Threads are ordered by the `archive_timestamp` property included in the metadata of the object in descending order. + * + * @see {@link https://discord.com/developers/docs/resources/channel#list-private-archived-threads} + */ +export async function getPrivateArchivedThreads ( + rest: RestManager, + channelId: BigString, + options?: ListArchivedThreads +): Promise { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.THREAD_ARCHIVED_PRIVATE(channelId, options) + ) + + return { + threads: new Collection( + results.threads.map((result) => { + const thread = rest.transformers.channel(rest, { channel: result }) + return [thread.id, thread] + }) + ), + members: new Collection( + results.members.map((result) => { + const member = rest.transformers.threadMember(rest, result) + return [member.id!, member] + }) + ), + hasMore: results.has_more + } +} diff --git a/packages/rest/src/helpers/channels/threads/getPrivateJoinedArchivedThreads.ts b/packages/rest/src/helpers/channels/threads/getPrivateJoinedArchivedThreads.ts new file mode 100644 index 000000000..db857829a --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/getPrivateJoinedArchivedThreads.ts @@ -0,0 +1,52 @@ +import { BigString, DiscordListArchivedThreads } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { + ArchivedThreads, + ListArchivedThreads +} from './getPublicArchivedThreads.js' + +/** + * Gets the list of private archived threads the bot is a member of for a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to get the archived threads for. + * @param options - The parameters for the fetching of threads. + * @returns An instance of {@link ArchivedThreads}. + * + * @remarks + * Requires the `READ_MESSAGE_HISTORY` permission. + * + * Returns threads of type {@link ChannelTypes.GuildPrivateThread}. + * + * Threads are ordered by the `id` property in descending order. + * + * @see {@link https://discord.com/developers/docs/resources/channel#list-joined-private-archived-threads} + */ +export async function getPrivateJoinedArchivedThreads ( + rest: RestManager, + channelId: BigString, + options?: ListArchivedThreads +): Promise { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.THREAD_ARCHIVED_PRIVATE_JOINED(channelId, options) + ) + + return { + threads: new Collection( + results.threads.map((result) => { + const thread = rest.transformers.channel(rest, { channel: result }) + return [thread.id, thread] + }) + ), + members: new Collection( + results.members.map((result) => { + const member = rest.transformers.threadMember(rest, result) + return [member.id!, member] + }) + ), + hasMore: results.has_more + } +} diff --git a/packages/rest/src/helpers/channels/threads/getPublicArchivedThreads.ts b/packages/rest/src/helpers/channels/threads/getPublicArchivedThreads.ts new file mode 100644 index 000000000..fc11baafd --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/getPublicArchivedThreads.ts @@ -0,0 +1,62 @@ +import { BigString, DiscordListArchivedThreads } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { ActiveThreads } from './getActiveThreads.js' + +/** + * Gets the list of public archived threads for a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to get the archived threads for. + * @param options - The parameters for the fetching of threads. + * @returns An instance of {@link ArchivedThreads}. + * + * @remarks + * Requires the `READ_MESSAGE_HISTORY` permission. + * + * If called on a channel of type {@link ChannelTypes.GuildText}, returns threads of type {@link ChannelTypes.GuildPublicThread}. + * If called on a channel of type {@link ChannelTypes.GuildNews}, returns threads of type {@link ChannelTypes.GuildNewsThread}. + * + * Threads are ordered by the `archive_timestamp` property included in the metadata of the object in descending order. + * + * @see {@link https://discord.com/developers/docs/resources/channel#list-public-archived-threads} + */ +export async function getPublicArchivedThreads ( + rest: RestManager, + channelId: BigString, + options?: ListArchivedThreads +): Promise { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.THREAD_ARCHIVED_PUBLIC(channelId, options) + ) + + return { + threads: new Collection( + results.threads.map((result) => { + const thread = rest.transformers.channel(rest, { channel: result }) + return [thread.id, thread] + }) + ), + members: new Collection( + results.members.map((result) => { + const member = rest.transformers.threadMember(rest, result) + return [member.id!, member] + }) + ), + hasMore: results.has_more + } +} + +/** https://discord.com/developers/docs/resources/channel#list-public-archived-threads-query-string-params */ +export interface ListArchivedThreads { + /** Returns threads before this timestamp */ + before?: number + /** Optional maximum number of threads to return */ + limit?: number +} + +export type ArchivedThreads = ActiveThreads & { + hasMore: boolean +} diff --git a/packages/rest/src/helpers/channels/threads/getThreadMember.ts b/packages/rest/src/helpers/channels/threads/getThreadMember.ts new file mode 100644 index 000000000..820c8c578 --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/getThreadMember.ts @@ -0,0 +1,27 @@ +import { BigString, DiscordThreadMember } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { ThreadMember } from '../../../transformers/threadMember.js' + +/** + * Gets a thread member by their user ID. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the thread to get the thread member of. + * @param userId - The user ID of the thread member to get. + * @returns An instance of {@link ThreadMember}. + * + * @see {@link https://discord.com/developers/docs/resources/channel#get-thread-member} + */ +export async function getThreadMember ( + rest: RestManager, + channelId: BigString, + userId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.THREAD_USER(channelId, userId) + ) + + return rest.transformers.threadMember(rest, result) +} diff --git a/packages/rest/src/helpers/channels/threads/getThreadMembers.ts b/packages/rest/src/helpers/channels/threads/getThreadMembers.ts new file mode 100644 index 000000000..cb61fd725 --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/getThreadMembers.ts @@ -0,0 +1,34 @@ +import { BigString, DiscordThreadMember } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { ThreadMember } from '../../../transformers/threadMember.js' + +/** + * Gets the list of thread members for a thread. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the thread to get the thread members of. + * @returns A collection of {@link ThreadMember} assorted by user ID. + * + * @remarks + * Requires the application to have the `GUILD_MEMBERS` privileged intent enabled. + * + * @see {@link https://discord.com/developers/docs/resources/channel#list-thread-members} + */ +export async function getThreadMembers ( + rest: RestManager, + channelId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.THREAD_MEMBERS(channelId) + ) + + return new Collection( + results.map((result) => { + const member = rest.transformers.threadMember(rest, result) + return [member.id!, member] + }) + ) +} diff --git a/packages/rest/src/helpers/channels/threads/index.ts b/packages/rest/src/helpers/channels/threads/index.ts new file mode 100644 index 000000000..0cdef48ae --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/index.ts @@ -0,0 +1,12 @@ +export * from './addThreadMember.js' +export * from './getActiveThreads.js' +export * from './getPrivateArchivedThreads.js' +export * from './getPrivateJoinedArchivedThreads.js' +export * from './getPublicArchivedThreads.js' +export * from './getThreadMember.js' +export * from './getThreadMembers.js' +export * from './joinThread.js' +export * from './leaveThread.js' +export * from './removeThreadMember.js' +export * from './startThreadWithMessage.js' +export * from './startThreadWithoutMessage.js' diff --git a/packages/rest/src/helpers/channels/threads/joinThread.ts b/packages/rest/src/helpers/channels/threads/joinThread.ts new file mode 100644 index 000000000..3127ac576 --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/joinThread.ts @@ -0,0 +1,26 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Adds the bot user to a thread. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the thread to add the bot user to. + * + * @remarks + * Requires the thread not be archived. + * + * Fires a _Thread Members Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#join-thread} + */ +export async function joinThread ( + rest: RestManager, + channelId: BigString +): Promise { + return await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.THREAD_ME(channelId) + ) +} diff --git a/packages/rest/src/helpers/channels/threads/leaveThread.ts b/packages/rest/src/helpers/channels/threads/leaveThread.ts new file mode 100644 index 000000000..43fd8bd5c --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/leaveThread.ts @@ -0,0 +1,26 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Removes the bot user from a thread. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the thread to remove the bot user from. + * + * @remarks + * Requires the thread not be archived. + * + * Fires a _Thread Members Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#leave-thread} + */ +export async function leaveThread ( + rest: RestManager, + channelId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.THREAD_ME(channelId) + ) +} diff --git a/packages/rest/src/helpers/channels/threads/removeThreadMember.ts b/packages/rest/src/helpers/channels/threads/removeThreadMember.ts new file mode 100644 index 000000000..03eed9740 --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/removeThreadMember.ts @@ -0,0 +1,31 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Removes a member from a thread. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the thread to remove the thread member of. + * @param userId - The user ID of the thread member to remove. + * + * @remarks + * If the thread is of type {@link ChannelTypes.GuildPrivateThread}, requires to be the creator of the thread. + * Otherwise, requires the `MANAGE_THREADS` permission. + * + * Requires the thread not be archived. + * + * Fires a _Thread Members Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#remove-thread-member} + */ +export async function removeThreadMember ( + rest: RestManager, + channelId: BigString, + userId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.THREAD_USER(channelId, userId) + ) +} diff --git a/packages/rest/src/helpers/channels/threads/startThreadWithMessage.ts b/packages/rest/src/helpers/channels/threads/startThreadWithMessage.ts new file mode 100644 index 000000000..843dcceab --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/startThreadWithMessage.ts @@ -0,0 +1,57 @@ +import { DiscordChannel } from '@discordeno/types' +import { BigString, WithReason } from '../../../index.js' +import type { RestManager } from '../../../restManager.js' +import { Channel } from '../../../transformers/channel.js' + +/** + * Creates a thread, using an existing message as its point of origin. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel in which to create the thread. + * @param messageId - The ID of the message to use as the thread's point of origin. + * @param options - The parameters to use for the creation of the thread. + * @returns An instance of the created {@link Channel | Thread}. + * + * @remarks + * If called on a channel of type {@link ChannelTypes.GuildText}, creates a {@link ChannelTypes.GuildPublicThread}. + * If called on a channel of type {@link ChannelTypes.GuildNews}, creates a {@link ChannelTypes.GuildNewsThread}. + * Does not work on channels of type {@link ChannelTypes.GuildForum}. + * + * The ID of the created thread will be the same as the ID of the source message. + * + * Fires a _Thread Create_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#start-thread-from-message} + */ +export async function startThreadWithMessage ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + options: StartThreadWithMessage +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.THREAD_START_PUBLIC(channelId, messageId), + { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + rate_limit_per_user: options.rateLimitPerUser, + reason: options.reason + } + ) + + return rest.transformers.channel(rest, { + channel: result, + guildId: rest.transformers.snowflake(result.guild_id!) + }) +} + +export interface StartThreadWithMessage extends WithReason { + /** 1-100 character thread name */ + name: string + /** Duration in minutes to automatically archive the thread after recent activity */ + autoArchiveDuration: 60 | 1440 | 4320 | 10080 + /** Amount of seconds a user has to wait before sending another message (0-21600) */ + rateLimitPerUser?: number | null +} diff --git a/packages/rest/src/helpers/channels/threads/startThreadWithoutMessage.ts b/packages/rest/src/helpers/channels/threads/startThreadWithoutMessage.ts new file mode 100644 index 000000000..927e08829 --- /dev/null +++ b/packages/rest/src/helpers/channels/threads/startThreadWithoutMessage.ts @@ -0,0 +1,62 @@ +import { BigString, ChannelTypes, DiscordChannel } from '@discordeno/types' +import { WithReason } from '../../../index.js' +import type { RestManager } from '../../../restManager.js' +import { Channel } from '../../../transformers/channel.js' + +/** + * Creates a thread without using a message as the thread's point of origin. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel in which to create the thread. + * @param options - The parameters to use for the creation of the thread. + * @returns An instance of the created {@link Channel | Thread}. + * + * @remarks + * Creating a private thread requires the server to be boosted. + * + * Fires a _Thread Create_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#start-thread-without-message} + */ +export async function startThreadWithoutMessage ( + rest: RestManager, + channelId: BigString, + options: StartThreadWithoutMessage +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.THREAD_START_PRIVATE(channelId), + { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + rate_limit_per_user: options.rateLimitPerUser, + type: options.type, + invitable: options.invitable, + reason: options.reason + } + ) + + return rest.transformers.channel(rest, { + channel: result, + guildId: result.guild_id + ? rest.transformers.snowflake(result.guild_id) + : undefined + }) +} + +export interface StartThreadWithoutMessage extends WithReason { + /** 1-100 character thread name */ + name: string + /** Duration in minutes to automatically archive the thread after recent activity */ + autoArchiveDuration: 60 | 1440 | 4320 | 10080 + /** Amount of seconds a user has to wait before sending another message (0-21600) */ + rateLimitPerUser?: number | null + /** the type of thread to create */ + type: + | ChannelTypes.AnnouncementThread + | ChannelTypes.PublicThread + | ChannelTypes.PrivateThread + /** whether non-moderators can add other non-moderators to a thread; only available when creating a private thread */ + invitable?: boolean +} diff --git a/packages/rest/src/helpers/channels/triggerTypingIndicator.ts b/packages/rest/src/helpers/channels/triggerTypingIndicator.ts new file mode 100644 index 000000000..53f5439b4 --- /dev/null +++ b/packages/rest/src/helpers/channels/triggerTypingIndicator.ts @@ -0,0 +1,28 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +export const startTyping = triggerTypingIndicator + +/** + * Triggers a typing indicator for the bot user. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel in which to trigger the typing indicator. + * + * @remarks + * Generally, bots should _not_ use this route. + * + * Fires a _Typing Start_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#trigger-typing-indicator} + */ +export async function triggerTypingIndicator ( + rest: RestManager, + channelId: BigString +): Promise { + return await rest.runMethod( + rest, + 'POST', + rest.constants.routes.CHANNEL_TYPING(channelId) + ) +} diff --git a/packages/rest/src/helpers/emojis/createEmoji.ts b/packages/rest/src/helpers/emojis/createEmoji.ts new file mode 100644 index 000000000..5b9d6e8c1 --- /dev/null +++ b/packages/rest/src/helpers/emojis/createEmoji.ts @@ -0,0 +1,56 @@ +import { DiscordEmoji } from '@discordeno/types' +import { urlToBase64 } from '@discordeno/utils' +import { BigString, WithReason } from '../../index.js' +import type { RestManager } from '../../restManager.js' +import { Emoji } from '../../transformers/emoji.js' + +/** + * Creates an emoji in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild in which to create the emoji. + * @param options - The parameters for the creation of the emoji. + * @returns An instance of the created {@link Emoji}. + * + * @remarks + * Requires the `MANAGE_EMOJIS_AND_STICKERS` permission. + * + * Emojis have a maximum file size of 256 kilobits. Attempting to upload a larger emoji will cause the route to return 400 Bad Request. + * + * Fires a _Guild Emojis Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/emoji#create-guild-emoji} + */ +export async function createEmoji ( + rest: RestManager, + guildId: BigString, + options: CreateGuildEmoji +): Promise { + if (options.image && !options.image.startsWith('data:image/')) { + options.image = await urlToBase64(options.image) + } + + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.GUILD_EMOJIS(guildId), + { + name: options.name, + image: options.image, + roles: options.roles?.map((role) => role.toString()), + reason: options.reason + } + ) + + return rest.transformers.emoji(rest, result) +} + +/** https://discord.com/developers/docs/resources/emoji#create-guild-emoji */ +export interface CreateGuildEmoji extends WithReason { + /** Name of the emoji */ + name: string + /** The 128x128 emoji image. 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. If a URL is provided to the image parameter, Discordeno will automatically convert it to a base64 string internally. */ + image: string + /** Roles allowed to use this emoji */ + roles?: BigString[] +} diff --git a/packages/rest/src/helpers/emojis/deleteEmoji.ts b/packages/rest/src/helpers/emojis/deleteEmoji.ts new file mode 100644 index 000000000..826124ef8 --- /dev/null +++ b/packages/rest/src/helpers/emojis/deleteEmoji.ts @@ -0,0 +1,32 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Deletes an emoji from a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild from which to delete the emoji. + * @param id - The ID of the emoji to delete. + * + * @remarks + * Requires the `MANAGE_EMOJIS_AND_STICKERS` permission. + * + * Fires a _Guild Emojis Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/emoji#delete-guild-emoji} + */ +export async function deleteEmoji ( + rest: RestManager, + guildId: BigString, + id: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD_EMOJI(guildId, id), + { + reason + } + ) +} diff --git a/packages/rest/src/helpers/emojis/editEmoji.ts b/packages/rest/src/helpers/emojis/editEmoji.ts new file mode 100644 index 000000000..a8a854a82 --- /dev/null +++ b/packages/rest/src/helpers/emojis/editEmoji.ts @@ -0,0 +1,51 @@ +import { DiscordEmoji } from '@discordeno/types' +import { BigString, WithReason } from '../../index.js' +import type { RestManager } from '../../restManager.js' +import { Emoji } from '../../transformers/emoji.js' + +/** + * Edits an emoji. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild in which to edit the emoji. + * @param id - The ID of the emoji to edit. + * @param options - The parameters for the edit of the emoji. + * @returns An instance of the updated {@link Emoji}. + * + * @remarks + * Requires the `MANAGE_EMOJIS_AND_STICKERS` permission. + * + * Fires a `Guild Emojis Update` gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/emoji#modify-guild-emoji} + */ +export async function editEmoji ( + rest: RestManager, + guildId: BigString, + id: BigString, + options: ModifyGuildEmoji +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD_EMOJI(guildId, id), + { + name: options.name, + // NEED TERNARY TO SUPPORT NULL AS VALID + roles: options.roles + ? options.roles.map((role) => role.toString()) + : options.roles, + reason: options.reason + } + ) + + return rest.transformers.emoji(rest, result) +} + +/** https://discord.com/developers/docs/resources/emoji#modify-guild-emoji */ +export interface ModifyGuildEmoji extends WithReason { + /** Name of the emoji */ + name?: string + /** Roles allowed to use this emoji */ + roles?: BigString[] | null +} diff --git a/packages/rest/src/helpers/emojis/getEmoji.ts b/packages/rest/src/helpers/emojis/getEmoji.ts new file mode 100644 index 000000000..c54f8eaee --- /dev/null +++ b/packages/rest/src/helpers/emojis/getEmoji.ts @@ -0,0 +1,27 @@ +import { BigString, DiscordEmoji } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Emoji } from '../../transformers/emoji.js' + +/** + * Gets an emoji by its ID. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild from which to get the emoji. + * @param emojiId - The ID of the emoji to get. + * @returns An instance of {@link Emoji}. + * + * @see {@link https://discord.com/developers/docs/resources/emoji#get-guild-emoji} + */ +export async function getEmoji ( + rest: RestManager, + guildId: BigString, + emojiId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_EMOJI(guildId, emojiId) + ) + + return rest.transformers.emoji(rest, result) +} diff --git a/packages/rest/src/helpers/emojis/getEmojiUrl.ts b/packages/rest/src/helpers/emojis/getEmojiUrl.ts new file mode 100644 index 000000000..47a3008ee --- /dev/null +++ b/packages/rest/src/helpers/emojis/getEmojiUrl.ts @@ -0,0 +1,19 @@ +import { BigString } from '@discordeno/types' +import { RestManager } from '../../restManager.js' + +/** + * Builds a URL to an emoji in the Discord CDN. + * + * @param emojiId - The ID of the emoji to access. + * @param animated - Whether the emoji is animated or static. + * @returns The link to the resource. + */ +export function getEmojiURL ( + _rest: RestManager, + emojiId: BigString, + animated = false +): string { + return `https://cdn.discordapp.com/emojis/${emojiId}.${ + animated ? 'gif' : 'png' + }` +} diff --git a/packages/rest/src/helpers/emojis/getEmojis.ts b/packages/rest/src/helpers/emojis/getEmojis.ts new file mode 100644 index 000000000..df33e6d50 --- /dev/null +++ b/packages/rest/src/helpers/emojis/getEmojis.ts @@ -0,0 +1,31 @@ +import { BigString, DiscordEmoji } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Emoji } from '../../transformers/emoji.js' + +/** + * Gets the list of emojis for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild which to get the emojis of. + * @returns A collection of {@link Emoji} objects assorted by emoji ID. + * + * @see {@link https://discord.com/developers/docs/resources/emoji#list-guild-emojis} + */ +export async function getEmojis ( + rest: RestManager, + guildId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_EMOJIS(guildId) + ) + + return new Collection( + results.map((result) => { + const emoji = rest.transformers.emoji(rest, result) + return [emoji.id!, emoji] + }) + ) +} diff --git a/packages/rest/src/helpers/emojis/index.ts b/packages/rest/src/helpers/emojis/index.ts new file mode 100644 index 000000000..20170f756 --- /dev/null +++ b/packages/rest/src/helpers/emojis/index.ts @@ -0,0 +1,6 @@ +export * from './createEmoji.js' +export * from './deleteEmoji.js' +export * from './editEmoji.js' +export * from './getEmoji.js' +export * from './getEmojis.js' +export * from './getEmojiUrl.js' diff --git a/packages/rest/src/helpers/guilds/automod/createAutomodRule.ts b/packages/rest/src/helpers/guilds/automod/createAutomodRule.ts new file mode 100644 index 000000000..dbd5b713a --- /dev/null +++ b/packages/rest/src/helpers/guilds/automod/createAutomodRule.ts @@ -0,0 +1,102 @@ +import { + AutoModerationActionType, + AutoModerationEventTypes, + AutoModerationTriggerTypes, + DiscordAutoModerationRule, + DiscordAutoModerationRuleTriggerMetadataPresets, + DiscordCreateAutomoderationRule +} from '@discordeno/types' +import { BigString, WithReason } from '../../../index.js' +import { RestManager } from '../../../restManager.js' +import { AutoModerationRule } from '../../../transformers/automodRule.js' + +/** + * Creates an automod rule in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to create the rule in. + * @param options - The parameters for the creation of the rule. + * @returns An instance of the created {@link AutoModerationRule}. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * Fires an _Auto Moderation Rule Create_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/auto-moderation#create-auto-moderation-rule} + */ +export async function createAutomodRule ( + rest: RestManager, + guildId: BigString, + options: CreateAutoModerationRuleOptions +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.AUTOMOD_RULES(guildId), + { + name: options.name, + event_type: options.eventType, + trigger_type: options.triggerType, + trigger_metadata: { + keyword_filter: options.triggerMetadata.keywordFilter, + presets: options.triggerMetadata.presets, + allow_list: options.triggerMetadata.allowList, + mention_total_limit: options.triggerMetadata.mentionTotalLimit + }, + actions: options.actions.map((action) => ({ + type: action.type, + metadata: action.metadata + ? { + channel_id: action.metadata.channelId?.toString(), + duration_seconds: action.metadata.durationSeconds + } + : undefined + })), + enabled: options.enabled ?? true, + exempt_roles: options.exemptRoles?.map((id) => id.toString()), + exempt_channels: options.exemptChannels?.map((id) => id.toString()), + reason: options.reason + } as DiscordCreateAutomoderationRule + ) + + return rest.transformers.automodRule(rest, result) +} + +export interface CreateAutoModerationRuleOptions extends WithReason { + /** The name of the rule. */ + name: string + /** The type of event to trigger the rule on. */ + eventType: AutoModerationEventTypes + /** The type of trigger to use for the rule. */ + triggerType: AutoModerationTriggerTypes + /** The metadata to use for the trigger. */ + triggerMetadata: { + /** The keywords needed to match. Only present when TriggerType.Keyword */ + keywordFilter?: string[] + /** The pre-defined lists of words to match from. Only present when TriggerType.KeywordPreset */ + presets?: DiscordAutoModerationRuleTriggerMetadataPresets[] + /** The substrings which will exempt from triggering the preset trigger type. Only present when TriggerType.KeywordPreset */ + allowList?: string[] + /** Total number of mentions (role & user) allowed per message (Maximum of 50). Only present when TriggerType.MentionSpam */ + mentionTotalLimit?: number + } + /** The actions that will trigger for this rule */ + actions: Array<{ + /** The type of action to take when a rule is triggered */ + type: AutoModerationActionType + /** additional metadata needed during execution for this specific action type */ + metadata?: { + /** The id of channel to which user content should be logged. Only in SendAlertMessage */ + channelId?: BigString + /** Timeout duration in seconds. Max is 2419200(4 weeks). Only supported for TriggerType.Keyword */ + durationSeconds?: number + } + }> + /** Whether the rule should be enabled, true by default. */ + enabled?: boolean + /** The role ids that should not be effected by the rule */ + exemptRoles?: BigString[] + /** The channel ids that should not be effected by the rule. */ + exemptChannels?: BigString[] +} diff --git a/packages/rest/src/helpers/guilds/automod/deleteAutomodRule.ts b/packages/rest/src/helpers/guilds/automod/deleteAutomodRule.ts new file mode 100644 index 000000000..2e1e848d8 --- /dev/null +++ b/packages/rest/src/helpers/guilds/automod/deleteAutomodRule.ts @@ -0,0 +1,30 @@ +import { BigString } from '@discordeno/types' +import { RestManager } from '../../../restManager.js' + +/** + * Deletes an automod rule. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to delete the rule from. + * @param ruleId - The ID of the automod rule to delete. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * Fires an _Auto Moderation Rule Delete_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/auto-moderation#delete-auto-moderation-rule} + */ +export async function deleteAutomodRule ( + rest: RestManager, + guildId: BigString, + ruleId: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.AUTOMOD_RULE(guildId, ruleId), + { reason } + ) +} diff --git a/packages/rest/src/helpers/guilds/automod/editAutomodRule.ts b/packages/rest/src/helpers/guilds/automod/editAutomodRule.ts new file mode 100644 index 000000000..1dce95c37 --- /dev/null +++ b/packages/rest/src/helpers/guilds/automod/editAutomodRule.ts @@ -0,0 +1,100 @@ +import { + AutoModerationActionType, + AutoModerationEventTypes, + DiscordAutoModerationRule, + DiscordAutoModerationRuleTriggerMetadataPresets +} from '@discordeno/types' +import { BigString, WithReason } from '../../../index.js' +import { RestManager } from '../../../restManager.js' +import { AutoModerationRule } from '../../../transformers/automodRule.js' + +/** + * Edits an automod rule. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to edit the rule in. + * @param ruleId - The ID of the rule to edit. + * @param options - The parameters for the edit of the rule. + * @returns An instance of the edited {@link AutoModerationRule}. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * Fires an _Auto Moderation Rule Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/auto-moderation#modify-auto-moderation-rule} + */ +export async function editAutomodRule ( + rest: RestManager, + guildId: BigString, + ruleId: BigString, + options: Partial +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.AUTOMOD_RULE(guildId, ruleId), + { + name: options.name, + event_type: options.eventType, + trigger_metadata: options.triggerMetadata + ? { + keyword_filter: options.triggerMetadata.keywordFilter, + presets: options.triggerMetadata.presets, + allow_list: options.triggerMetadata.allowList, + mention_total_limit: options.triggerMetadata.mentionTotalLimit + } + : undefined, + actions: options.actions?.map((action) => ({ + type: action.type, + metadata: { + channel_id: action.metadata.channelId?.toString(), + duration_seconds: action.metadata.durationSeconds + } + })), + enabled: options.enabled ?? true, + exempt_roles: options.exemptRoles?.map((id) => id.toString()), + exempt_channels: options.exemptChannels?.map((id) => id.toString()), + reason: options.reason + } + ) + + return rest.transformers.automodRule(rest, result) +} + +export interface EditAutoModerationRuleOptions extends WithReason { + /** The name of the rule. */ + name: string + /** The type of event to trigger the rule on. */ + eventType: AutoModerationEventTypes + /** The metadata to use for the trigger. */ + triggerMetadata: { + /** The keywords needed to match. Only present when TriggerType.Keyword */ + keywordFilter?: string[] + // TODO: This may need a special type or enum + /** The pre-defined lists of words to match from. Only present when TriggerType.KeywordPreset */ + presets?: DiscordAutoModerationRuleTriggerMetadataPresets[] + /** The substrings which will exempt from triggering the preset trigger type. Only present when TriggerType.KeywordPreset */ + allowList?: string[] + /** Total number of mentions (role & user) allowed per message (Maximum of 50) */ + mentionTotalLimit: number + } + /** The actions that will trigger for this rule */ + actions: Array<{ + /** The type of action to take when a rule is triggered */ + type: AutoModerationActionType + /** additional metadata needed during execution for this specific action type */ + metadata: { + /** The id of channel to which user content should be logged. Only in SendAlertMessage */ + channelId?: BigString + /** Timeout duration in seconds. Only supported for TriggerType.Keyword */ + durationSeconds?: number + } + }> + /** Whether the rule should be enabled. */ + enabled?: boolean + /** The role ids that should not be effected by the rule */ + exemptRoles?: BigString[] + /** The channel ids that should not be effected by the rule. */ + exemptChannels?: BigString[] +} diff --git a/packages/rest/src/helpers/guilds/automod/getAutomodRule.ts b/packages/rest/src/helpers/guilds/automod/getAutomodRule.ts new file mode 100644 index 000000000..33fab1a36 --- /dev/null +++ b/packages/rest/src/helpers/guilds/automod/getAutomodRule.ts @@ -0,0 +1,30 @@ +import { BigString, DiscordAutoModerationRule } from '@discordeno/types' +import { RestManager } from '../../../restManager.js' +import { AutoModerationRule } from '../../../transformers/automodRule.js' + +/** + * Gets an automod rule by its ID. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the rule of. + * @param ruleId - The ID of the rule to get. + * @returns An instance of {@link AutoModerationRule}. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * @see {@link https://discord.com/developers/docs/resources/auto-moderation#get-auto-moderation-rule} + */ +export async function getAutomodRule ( + rest: RestManager, + guildId: BigString, + ruleId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.AUTOMOD_RULE(guildId, ruleId) + ) + + return rest.transformers.automodRule(rest, result) +} diff --git a/packages/rest/src/helpers/guilds/automod/getAutomodRules.ts b/packages/rest/src/helpers/guilds/automod/getAutomodRules.ts new file mode 100644 index 000000000..9a3f88bd1 --- /dev/null +++ b/packages/rest/src/helpers/guilds/automod/getAutomodRules.ts @@ -0,0 +1,34 @@ +import { BigString, DiscordAutoModerationRule } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import { RestManager } from '../../../restManager.js' +import { AutoModerationRule } from '../../../transformers/automodRule.js' + +/** + * Gets the list of automod rules for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the rules from. + * @returns A collection of {@link AutoModerationRule} objects assorted by rule ID. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * @see {@link https://discord.com/developers/docs/resources/auto-moderation#list-auto-moderation-rules-for-guild} + */ +export async function getAutomodRules ( + rest: RestManager, + guildId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.AUTOMOD_RULES(guildId) + ) + + return new Collection( + results.map((result) => { + const rule = rest.transformers.automodRule(rest, result) + return [rule.id, rule] + }) + ) +} diff --git a/packages/rest/src/helpers/guilds/automod/index.ts b/packages/rest/src/helpers/guilds/automod/index.ts new file mode 100644 index 000000000..99f43e0e3 --- /dev/null +++ b/packages/rest/src/helpers/guilds/automod/index.ts @@ -0,0 +1,5 @@ +export * from './createAutomodRule.js' +export * from './deleteAutomodRule.js' +export * from './editAutomodRule.js' +export * from './getAutomodRule.js' +export * from './getAutomodRules.js' diff --git a/packages/rest/src/helpers/guilds/createGuild.ts b/packages/rest/src/helpers/guilds/createGuild.ts new file mode 100644 index 000000000..0f1792221 --- /dev/null +++ b/packages/rest/src/helpers/guilds/createGuild.ts @@ -0,0 +1,77 @@ +import { + DefaultMessageNotificationLevels, + DiscordGuild, + ExplicitContentFilterLevels, + SystemChannelFlags, + VerificationLevels +} from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Channel } from '../../transformers/channel.js' +import { Guild } from '../../transformers/guild.js' +import { Role } from '../../transformers/role.js' + +/** + * Creates a guild. + * + * @param bot - The bot instance to use to make the request. + * @param options - The parameters for the creation of the guild. + * @returns An instance of the created {@link Guild}. + * + * @remarks + * ⚠️ This route can only be used by bots in __fewer than 10 guilds__. + * + * Fires a _Guild Create_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#create-guild} + */ +export async function createGuild ( + rest: RestManager, + options: CreateGuild +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.GUILDS(), + { + name: options.name, + afk_channel_id: options.afkChannelId, + afk_timeout: options.afkTimeout, + channels: options.channels, + default_message_notifications: options.defaultMessageNotifications, + explicit_content_filter: options.explicitContentFilter, + icon: options.icon, + roles: options.roles, + system_channel_flags: options.systemChannelFlags, + system_channel_id: options.systemChannelId, + verification_level: options.verificationLevel + } + ) + + return rest.transformers.guild(rest, { guild: result, shardId: 0 }) +} + +/** https://discord.com/developers/docs/resources/guild#create-guild */ +export interface CreateGuild { + /** Name of the guild (1-100 characters) */ + name: string + /** Base64 128x128 image for the guild icon */ + icon?: string + /** Verification level */ + verificationLevel?: VerificationLevels + /** Default message notification level */ + defaultMessageNotifications?: DefaultMessageNotificationLevels + /** Explicit content filter level */ + explicitContentFilter?: ExplicitContentFilterLevels + /** New guild roles (first role is the everyone role) */ + roles?: Role[] + /** New guild's channels */ + channels?: Array> + /** Id for afk channel */ + afkChannelId?: string + /** Afk timeout in seconds */ + afkTimeout?: number + /** The id of the channel where guild notices such as welcome messages and boost events are posted */ + systemChannelId?: string + /** System channel flags */ + systemChannelFlags?: SystemChannelFlags +} diff --git a/packages/rest/src/helpers/guilds/deleteGuild.ts b/packages/rest/src/helpers/guilds/deleteGuild.ts new file mode 100644 index 000000000..941e5b464 --- /dev/null +++ b/packages/rest/src/helpers/guilds/deleteGuild.ts @@ -0,0 +1,26 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Deletes a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to delete. + * + * @remarks + * The bot user must be the owner of the guild. + * + * Fires a _Guild Delete_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#delete-guild} + */ +export async function deleteGuild ( + rest: RestManager, + guildId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD(guildId) + ) +} diff --git a/packages/rest/src/helpers/guilds/editGuild.ts b/packages/rest/src/helpers/guilds/editGuild.ts new file mode 100644 index 000000000..aacb3858f --- /dev/null +++ b/packages/rest/src/helpers/guilds/editGuild.ts @@ -0,0 +1,128 @@ +import { + BigString, + DefaultMessageNotificationLevels, + DiscordGuild, + ExplicitContentFilterLevels, + GuildFeatures, + SystemChannelFlags, + VerificationLevels +} from '@discordeno/types' +import { urlToBase64 } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Guild } from '../../transformers/guild.js' + +// TODO: Put the `shardId` parameter before `options`. + +/** + * Edits a guild's settings. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to edit. + * @param shardId - The ID of the shard the guild is in. + * @param options - The parameters for the edit of the guild. + * @returns An instance of the edited {@link Guild}. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * If attempting to add or remove the {@link GuildFeatures.Community} feature: + * - Requires the `ADMINISTRATOR` permission. + * + * Fires a _Guild Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-guild} + */ +export async function editGuild ( + rest: RestManager, + guildId: BigString, + options: ModifyGuild, + shardId: number +): Promise { + if (options.icon && !options.icon.startsWith('data:image/')) { + options.icon = await urlToBase64(options.icon) + } + + if (options.banner && !options.banner.startsWith('data:image/')) { + options.banner = await urlToBase64(options.banner) + } + + if (options.splash && !options.splash.startsWith('data:image/')) { + options.splash = await urlToBase64(options.splash) + } + + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD(guildId), + { + name: options.name, + verification_levels: options.verificationLevel, + default_message_notifications: options.defaultMessageNotifications, + explicit_content_filter: options.explicitContentFilter, + afk_channel_id: options.afkChannelId + ? options.afkChannelId.toString() + : options.afkChannelId, + afk_timeout: options.afkTimeout, + icon: options.icon, + owner_id: options.ownerId ? options.ownerId.toString() : options.ownerId, + splash: options.splash, + discovery_splash: options.discoverySplash, + banner: options.banner, + system_channel_id: options.systemChannelId + ? options.systemChannelId.toString() + : options.systemChannelId, + system_channel_flags: options.systemChannelFlags, + rules_channel_id: options.rulesChannelId + ? options.rulesChannelId.toString() + : options.rulesChannelId, + public_updates_channel_id: options.publicUpdatesChannelId + ? options.publicUpdatesChannelId.toString() + : options.publicUpdatesChannelId, + preferred_locale: options.preferredLocale, + features: options.features, + premium_progress_bar_enabled: options.premiumProgressBarEnabled + } + ) + + return rest.transformers.guild(rest, { guild: result, shardId }) +} + +/** https://discord.com/developers/docs/resources/guild#modify-guild */ +export interface ModifyGuild { + /** Guild name */ + name?: string + /** Verification level */ + verificationLevel?: VerificationLevels | null + /** Default message notification filter level */ + defaultMessageNotifications?: DefaultMessageNotificationLevels | null + /** Explicit content filter level */ + explicitContentFilter?: ExplicitContentFilterLevels | null + /** Id for afk channel */ + afkChannelId?: BigString | null + /** Afk timeout in seconds */ + afkTimeout?: number + /** Base64 1024x1024 png/jpeg/gif image for the guild icon (can be animated gif when the server has the `ANIMATED_ICON` feature) */ + icon?: string | null + /** User id to transfer guild ownership to (must be owner) */ + ownerId?: BigString + /** Base64 16:9 png/jpeg image for the guild splash (when the server has `INVITE_SPLASH` feature) */ + splash?: string | null + /** Base64 16:9 png/jpeg image for the guild discovery spash (when the server has the `DISCOVERABLE` feature) */ + discoverySplash?: string | null + /** Base64 16:9 png/jpeg image for the guild banner (when the server has BANNER feature) */ + banner?: string | null + /** The id of the channel where guild notices such as welcome messages and boost events are posted */ + systemChannelId?: BigString | null + /** System channel flags */ + systemChannelFlags?: SystemChannelFlags + /** The id of the channel where Community guilds display rules and/or guidelines */ + rulesChannelId?: BigString | null + /** The id of the channel where admins and moderators of Community guilds receive notices from Discord */ + publicUpdatesChannelId?: BigString | null + /** The preferred locale of a Community guild used in server discovery and notices from Discord; defaults to "en-US" */ + preferredLocale?: string | null + /** Enabled guild features */ + features?: GuildFeatures[] + /** Whether the guild's boost progress bar should be enabled */ + premiumProgressBarEnabled?: boolean +} diff --git a/packages/rest/src/helpers/guilds/editGuildMfaLevel.ts b/packages/rest/src/helpers/guilds/editGuildMfaLevel.ts new file mode 100644 index 000000000..4df4f5fd1 --- /dev/null +++ b/packages/rest/src/helpers/guilds/editGuildMfaLevel.ts @@ -0,0 +1,17 @@ +import type { BigString, MfaLevels } from '@discordeno/types' +import type { RestManager } from '../../index.js' + +/** Modify a guild's MFA level. Requires guild ownership. */ +export async function editGuildMfaLevel ( + rest: RestManager, + guildId: BigString, + mfaLevel: MfaLevels, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'POST', + rest.constants.routes.GUILD_MFA_LEVEL(guildId), + { level: mfaLevel, reason } + ) +} diff --git a/packages/rest/src/helpers/guilds/editWelcomeScreen.ts b/packages/rest/src/helpers/guilds/editWelcomeScreen.ts new file mode 100644 index 000000000..94fd0924c --- /dev/null +++ b/packages/rest/src/helpers/guilds/editWelcomeScreen.ts @@ -0,0 +1,63 @@ +import { BigString, DiscordWelcomeScreen } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { WelcomeScreen } from '../../transformers/welcomeScreen.js' + +/** + * Edits a guild's welcome screen. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to edit the welcome screen of. + * @param options - The parameters for the edit of the welcome screen. + * @returns An instance of the edited {@link WelcomeScreen}. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * Fires a _Guild Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-guild-welcome-screen} + */ +export async function editWelcomeScreen ( + rest: RestManager, + guildId: BigString, + options: ModifyGuildWelcomeScreen +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD_WELCOME_SCREEN(guildId), + { + enabled: options.enabled, + welcome_screen: options.welcomeScreen?.map((welcomeScreen) => ({ + channel_id: welcomeScreen.channelId, + description: welcomeScreen.description, + emoji_id: welcomeScreen.emojiId, + emoji_name: welcomeScreen.emojiName + })), + description: options.description + } + ) + + return rest.transformers.welcomeScreen(rest, result) +} + +/** https://discord.com/developers/docs/resources/guild#modify-guild-welcome-screen */ +export interface ModifyGuildWelcomeScreen { + /** Whether the welcome screen is enabled */ + enabled?: boolean | null + /** Channels linked in the welcome screen and their display options */ + welcomeScreen?: WelcomeScreenChannel[] | null + /** The server description to show in the welcome screen */ + description?: string | null +} + +export interface WelcomeScreenChannel { + /** The channel's id */ + channelId: BigString + /** The emoji id, if the emoji is custom */ + emojiId?: BigString + /** The emoji name if custom, the unicode character if standard, or `null` if no emoji is set */ + emojiName?: string + /** The description shown for the channel */ + description: string +} diff --git a/packages/rest/src/helpers/guilds/events/createScheduledEvent.ts b/packages/rest/src/helpers/guilds/events/createScheduledEvent.ts new file mode 100644 index 000000000..fe8a0913f --- /dev/null +++ b/packages/rest/src/helpers/guilds/events/createScheduledEvent.ts @@ -0,0 +1,111 @@ +import { + BigString, + DiscordScheduledEvent, + ScheduledEventEntityType, + ScheduledEventPrivacyLevel +} from '@discordeno/types' +import { validateLength } from '@discordeno/utils' +import { WithReason } from '../../../index.js' +import { RestManager } from '../../../restManager.js' +import { ScheduledEvent } from '../../../transformers/scheduledEvent.js' + +/** + * Creates a scheduled event in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to create the scheduled event in. + * @param options - The parameters for the creation of the scheduled event. + * @returns An instance of the created {@link ScheduledEvent}. + * + * @remarks + * Requires the `MANAGE_EVENTS` permission. + * + * A guild can only have a maximum of 100 events with a status of {@link ScheduledEventStatus.Active} or {@link ScheduledEventStatus.Scheduled} (inclusive). + * + * Fires a _Guild Scheduled Event Create_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#create-guild-scheduled-event} + */ +export async function createScheduledEvent ( + rest: RestManager, + guildId: BigString, + options: CreateScheduledEvent +): Promise { + if (!validateLength(options.name, { min: 1, max: 100 })) { + throw new Error('Name must be between 1-100 characters.') + } + if ( + options.description && + !validateLength(options.description, { max: 1000 }) + ) { + throw new Error('Description must be below 1000 characters.') + } + if (options.location) { + if (!validateLength(options.location, { max: 100 })) { + throw new Error('Location must be below 100 characters.') + } + if (options.entityType === ScheduledEventEntityType.Voice) { + throw new Error('Location can not be provided for a Voice event.') + } + } + if (options.entityType === ScheduledEventEntityType.External) { + if (!options.scheduledEndTime) { + throw new Error( + 'A scheduled end time is required when making an External event.' + ) + } + if (!options.location) { + throw new Error('A location is required when making an External event.') + } + } + if ( + options.scheduledStartTime && + options.scheduledEndTime && + options.scheduledStartTime > options.scheduledEndTime + ) { + throw new Error('Cannot schedule event to end before starting.') + } + + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.GUILD_SCHEDULED_EVENTS(guildId), + { + channel_id: options.channelId?.toString(), + entity_metadata: options.location + ? { location: options.location } + : undefined, + name: options.name, + description: options.description, + scheduled_start_time: new Date(options.scheduledStartTime).toISOString(), + scheduled_end_time: options.scheduledEndTime + ? new Date(options.scheduledEndTime).toISOString() + : undefined, + privacy_level: + options.privacyLevel ?? ScheduledEventPrivacyLevel.GuildOnly, + entity_type: options.entityType, + reason: options.reason + } + ) + + return rest.transformers.scheduledEvent(rest, result) +} + +export interface CreateScheduledEvent extends WithReason { + /** the channel id of the scheduled event. */ + channelId?: BigString + /** location of the event. Required for events with `entityType: ScheduledEventEntityType.External` */ + location?: string + /** the name of the scheduled event */ + name: string + /** the description of the scheduled event */ + description: string + /** the time the scheduled event will start */ + scheduledStartTime: number + /** the time the scheduled event will end if it does end. Required for events with `entityType: ScheduledEventEntityType.External` */ + scheduledEndTime?: number + /** the privacy level of the scheduled event */ + privacyLevel?: ScheduledEventPrivacyLevel + /** the type of hosting entity associated with a scheduled event */ + entityType: ScheduledEventEntityType +} diff --git a/packages/rest/src/helpers/guilds/events/deleteScheduledEvent.ts b/packages/rest/src/helpers/guilds/events/deleteScheduledEvent.ts new file mode 100644 index 000000000..98721057e --- /dev/null +++ b/packages/rest/src/helpers/guilds/events/deleteScheduledEvent.ts @@ -0,0 +1,28 @@ +import { BigString } from '@discordeno/types' +import { RestManager } from '../../../restManager.js' + +/** + * Deletes a scheduled event from a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to delete the scheduled event from. + * @param eventId - The ID of the scheduled event to delete. + * + * @remarks + * Requires the `MANAGE_EVENTS` permission. + * + * Fires a _Guild Scheduled Event Delete_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#delete-guild-scheduled-event} + */ +export async function deleteScheduledEvent ( + rest: RestManager, + guildId: BigString, + eventId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD_SCHEDULED_EVENT(guildId, eventId) + ) +} diff --git a/packages/rest/src/helpers/guilds/events/editScheduledEvent.ts b/packages/rest/src/helpers/guilds/events/editScheduledEvent.ts new file mode 100644 index 000000000..403e4f40c --- /dev/null +++ b/packages/rest/src/helpers/guilds/events/editScheduledEvent.ts @@ -0,0 +1,103 @@ +import { + BigString, + DiscordScheduledEvent, + ScheduledEventEntityType, + ScheduledEventPrivacyLevel, + ScheduledEventStatus +} from '@discordeno/types' +import { validateLength } from '@discordeno/utils' +import { WithReason } from '../../../index.js' +import { RestManager } from '../../../restManager.js' +import { ScheduledEvent } from '../../../transformers/scheduledEvent.js' + +/** + * Edits a scheduled event. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to edit the scheduled event in. + * @param eventId - The ID of the scheduled event to edit. + * @returns An instance of the edited {@link ScheduledEvent}. + * + * @remarks + * Requires the `MANAGE_EVENTS` permission. + * + * To start or end an event, modify the event's `status` property. + * + * The `entity_metadata` property is discarded for events whose `entity_type` is not {@link ScheduledEventEntityType.External}. + * + * Fires a _Guild Scheduled Event Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#modify-guild-scheduled-event} + */ +export async function editScheduledEvent ( + rest: RestManager, + guildId: BigString, + eventId: BigString, + options: Partial +): Promise { + if (options.name && !validateLength(options.name, { min: 1, max: 100 })) { + throw new Error('Name must be between 1-100 characters.') + } + if ( + options.description && + !validateLength(options.description, { max: 1000 }) + ) { + throw new Error('Description must be below 1000 characters.') + } + if (options.location && !validateLength(options.location, { max: 100 })) { + throw new Error('Location must be below 100 characters.') + } + if ( + options.scheduledStartTime && + options.scheduledEndTime && + options.scheduledStartTime > options.scheduledEndTime + ) { + throw new Error('Cannot schedule event to end before starting.') + } + + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD_SCHEDULED_EVENT(guildId, eventId), + { + channel_id: + options.channelId === null ? null : options.channelId?.toString(), + entity_metadata: options.location ? { location: options.location } : null, + name: options.name, + description: options.description, + scheduled_start_time: options.scheduledStartTime + ? new Date(options.scheduledStartTime).toISOString() + : undefined, + scheduled_end_time: options.scheduledEndTime + ? new Date(options.scheduledEndTime).toISOString() + : undefined, + privacy_level: options.privacyLevel, + entity_type: options.entityType, + status: options.status, + reason: options.reason + } + ) + + return rest.transformers.scheduledEvent(rest, result) +} + +export interface EditScheduledEvent extends WithReason { + /** the channel id of the scheduled event. null if switching to external event. */ + channelId: BigString | null + /** location of the event */ + location?: string + /** the name of the scheduled event */ + name: string + /** the description of the scheduled event */ + description?: string + /** the time the scheduled event will start */ + scheduledStartTime: number + /** the time the scheduled event will end if it does end. */ + scheduledEndTime?: number + /** the privacy level of the scheduled event */ + privacyLevel: ScheduledEventPrivacyLevel + /** the type of hosting entity associated with a scheduled event */ + entityType: ScheduledEventEntityType + /** the status of the scheduled event */ + status: ScheduledEventStatus +} diff --git a/packages/rest/src/helpers/guilds/events/getScheduledEvent.ts b/packages/rest/src/helpers/guilds/events/getScheduledEvent.ts new file mode 100644 index 000000000..f4c4ac260 --- /dev/null +++ b/packages/rest/src/helpers/guilds/events/getScheduledEvent.ts @@ -0,0 +1,33 @@ +import { BigString, DiscordScheduledEvent } from '@discordeno/types' +import { RestManager } from '../../../restManager.js' +import { ScheduledEvent } from '../../../transformers/scheduledEvent.js' + +/** + * Gets a scheduled event by its ID. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the scheduled event from. + * @param eventId - The ID of the scheduled event to get. + * @param options - The parameters for the fetching of the scheduled event. + * @returns An instance of {@link ScheduledEvent}. + * + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event} + */ +export async function getScheduledEvent ( + rest: RestManager, + guildId: BigString, + eventId: BigString, + options?: { withUserCount?: boolean } +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_SCHEDULED_EVENT( + guildId, + eventId, + options?.withUserCount + ) + ) + + return rest.transformers.scheduledEvent(rest, result) +} diff --git a/packages/rest/src/helpers/guilds/events/getScheduledEventUsers.ts b/packages/rest/src/helpers/guilds/events/getScheduledEventUsers.ts new file mode 100644 index 000000000..2a8a928a5 --- /dev/null +++ b/packages/rest/src/helpers/guilds/events/getScheduledEventUsers.ts @@ -0,0 +1,101 @@ +import { BigString, DiscordMember, DiscordUser } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import { RestManager } from '../../../restManager.js' +import { Member, User } from '../../../transformers/member.js' + +// TODO: This endpoint discards certain data from the result. +// Create `ScheduledEventUser` type and parse the data to it. + +/** + * Gets the list of subscribers to a scheduled event from a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the subscribers to the scheduled event from. + * @param eventId - The ID of the scheduled event to get the subscribers of. + * @param options - The parameters for the fetching of the subscribers. + * @returns A collection of {@link User} objects assorted by user ID. + * + * @remarks + * Requires the `MANAGE_EVENTS` permission. + * + * Users are ordered by their IDs in _ascending_ order. + * + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event-users} + */ +export async function getScheduledEventUsers ( + rest: RestManager, + guildId: BigString, + eventId: BigString, + options?: GetScheduledEventUsers & { withMember?: false } +): Promise> +export async function getScheduledEventUsers ( + rest: RestManager, + guildId: BigString, + eventId: BigString, + options?: GetScheduledEventUsers & { withMember: true } +): Promise> +export async function getScheduledEventUsers ( + rest: RestManager, + guildId: BigString, + eventId: BigString, + options?: GetScheduledEventUsers +): Promise< + Collection | Collection + > { + let url = rest.constants.routes.GUILD_SCHEDULED_EVENT_USERS( + guildId, + eventId, + options + ) + + if (options != null) { + url = '?' + + if (options.limit) url += `limit=${options.limit}` + if (options.withMember !== undefined) { + url += `&with_member=${options.withMember.toString()}` + } + if (options.after) url += `&after=${options.after}` + if (options.before) url += `&before=${options.before}` + } + + const results = await rest.runMethod< + Array<{ user: DiscordUser, member?: DiscordMember }> + >(rest, 'GET', url) + + if (!options?.withMember) { + return new Collection( + results.map((result) => { + const user = rest.transformers.user(rest, result.user) + return [user.id, user] + }) + ) + } + + const id = rest.transformers.snowflake(guildId) + + return new Collection( + results.map((result) => { + const user = rest.transformers.user(rest, result.user) + const member = rest.transformers.member( + rest, + result.member!, + id, + user.id + ) + + return [user.id, { member, user }] + }) + ) +} + +export interface GetScheduledEventUsers { + /** number of users to return (up to maximum 100), defaults to 100 */ + limit?: number + /** whether to also have member objects provided, defaults to false */ + withMember?: boolean + /** consider only users before given user id */ + before?: BigString + /** consider only users after given user id. If both before and after are provided, only before is respected. Fetching users in-between before and after is not supported. */ + after?: BigString +} diff --git a/packages/rest/src/helpers/guilds/events/getScheduledEvents.ts b/packages/rest/src/helpers/guilds/events/getScheduledEvents.ts new file mode 100644 index 000000000..621b2ea92 --- /dev/null +++ b/packages/rest/src/helpers/guilds/events/getScheduledEvents.ts @@ -0,0 +1,41 @@ +import { BigString, DiscordScheduledEvent } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import { RestManager } from '../../../restManager.js' +import { ScheduledEvent } from '../../../transformers/scheduledEvent.js' + +/** + * Gets the list of scheduled events for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the scheduled events from. + * @param options - The parameters for the fetching of the scheduled events. + * @returns A collection of {@link ScheduledEvent} objects assorted by event ID. + * + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#list-scheduled-events-for-guild} + */ +export async function getScheduledEvents ( + rest: RestManager, + guildId: BigString, + options?: GetScheduledEvents +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_SCHEDULED_EVENTS( + guildId, + options?.withUserCount + ) + ) + + return new Collection( + results.map((result) => { + const event = rest.transformers.scheduledEvent(rest, result) + return [event.id, event] + }) + ) +} + +export interface GetScheduledEvents { + /** include number of users subscribed to each event */ + withUserCount?: boolean +} diff --git a/packages/rest/src/helpers/guilds/events/index.ts b/packages/rest/src/helpers/guilds/events/index.ts new file mode 100644 index 000000000..5502b9a8f --- /dev/null +++ b/packages/rest/src/helpers/guilds/events/index.ts @@ -0,0 +1,6 @@ +export * from './createScheduledEvent.js' +export * from './deleteScheduledEvent.js' +export * from './editScheduledEvent.js' +export * from './getScheduledEvent.js' +export * from './getScheduledEvents.js' +export * from './getScheduledEventUsers.js' diff --git a/packages/rest/src/helpers/guilds/getAuditLog.ts b/packages/rest/src/helpers/guilds/getAuditLog.ts new file mode 100644 index 000000000..741f94878 --- /dev/null +++ b/packages/rest/src/helpers/guilds/getAuditLog.ts @@ -0,0 +1,131 @@ +import { AuditLogEvents, BigString, DiscordAuditLog } from '@discordeno/types' +import { iconHashToBigInt } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { ApplicationCommand } from '../../transformers/applicationCommand.js' +import { AuditLogEntry } from '../../transformers/auditLogEntry.js' +import { AutoModerationRule } from '../../transformers/automodRule.js' +import { Channel } from '../../transformers/channel.js' +import { Integration } from '../../transformers/integration.js' +import { User } from '../../transformers/member.js' +import { ScheduledEvent } from '../../transformers/scheduledEvent.js' +import { Webhook } from '../../transformers/webhook.js' + +export interface AuditLog { + auditLogEntries: AuditLogEntry[] + autoModerationRules?: AutoModerationRule[] + guildScheduledEvents?: ScheduledEvent[] + integrations: Array>> + threads: Channel[] + users: User[] + webhooks: Webhook[] + applicationCommands: ApplicationCommand[] +} + +// TODO: Move `AuditLog` into its own transformer file. + +/** + * Gets a guild's audit log. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the audit log of. + * @param options - The parameters for the fetching of the audit log. + * @returns An instance of {@link AuditLog}. + * + * @remarks + * Requires the `VIEW_AUDIT_LOG` permission. + * + * @see {@link https://discord.com/developers/docs/resources/audit-log#get-guild-audit-log} + */ +export async function getAuditLog ( + rest: RestManager, + guildId: BigString, + options?: GetGuildAuditLog +): Promise { + if (options?.limit) { + options.limit = + options.limit >= 1 && options.limit <= 100 ? options.limit : 50 + } + + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_AUDIT_LOGS(guildId, options) + ) + + const id = rest.transformers.snowflake(guildId) + return { + auditLogEntries: result.audit_log_entries.map((entry) => + rest.transformers.auditLogEntry(rest, entry) + ), + autoModerationRules: result.auto_moderation_rules?.map((rule) => + rest.transformers.automodRule(rest, rule) + ), + guildScheduledEvents: result.guild_scheduled_events?.map((event) => + rest.transformers.scheduledEvent(rest, event) + ), + integrations: result.integrations.map((integration) => ({ + id: integration.id + ? rest.transformers.snowflake(integration.id) + : undefined, + name: integration.name, + type: integration.type, + enabled: integration.enabled, + syncing: integration.syncing, + roleId: integration.role_id + ? rest.transformers.snowflake(integration.role_id) + : undefined, + enableEmoticons: integration.enable_emoticons, + expireBehavior: integration.expire_behavior, + expireGracePeriod: integration.expire_grace_period, + user: integration.user + ? rest.transformers.user(rest, integration.user) + : undefined, + account: integration.account + ? { + id: rest.transformers.snowflake(integration.account.id), + name: integration.account.name + } + : undefined, + syncedAt: integration.synced_at + ? Date.parse(integration.synced_at) + : undefined, + subscriberCount: integration.subscriber_count, + revoked: integration.revoked, + application: integration.application + ? { + id: rest.transformers.snowflake(integration.application.id), + name: integration.application.name, + icon: integration.application.icon + ? iconHashToBigInt(integration.application.icon) + : undefined, + description: integration.application.description, + bot: integration.application.bot + ? rest.transformers.user(rest, integration.application.bot) + : undefined + } + : undefined + })), + threads: result.threads.map((thread) => + rest.transformers.channel(rest, { channel: thread, guildId: id }) + ), + users: result.users.map((user) => rest.transformers.user(rest, user)), + webhooks: result.webhooks.map((hook) => + rest.transformers.webhook(rest, hook) + ), + applicationCommands: result.application_commands.map((applicationCommand) => + rest.transformers.applicationCommand(rest, applicationCommand) + ) + } +} + +/** https://discord.com/developers/docs/resources/audit-log#get-guild-audit-log-query-string-parameters */ +export interface GetGuildAuditLog { + /** Entries from a specific user ID */ + userId?: BigString | string + /** Entries for a specific audit log event */ + actionType?: AuditLogEvents + /** Entries that preceded a specific audit log entry ID */ + before?: BigString | string + /** Maximum number of entries (between 1-100) to return, defaults to 50 */ + limit?: number +} diff --git a/packages/rest/src/helpers/guilds/getBan.ts b/packages/rest/src/helpers/guilds/getBan.ts new file mode 100644 index 000000000..ed93489e0 --- /dev/null +++ b/packages/rest/src/helpers/guilds/getBan.ts @@ -0,0 +1,40 @@ +import { BigString, DiscordBan } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { User } from '../../transformers/member.js' + +export interface Ban { + reason?: string + user: User +} + +// TODO: Move `Ban` into its own transformer file. + +/** + * Gets a ban by user ID. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the ban from. + * @param userId - The ID of the user to get the ban for. + * @returns An instance of {@link Ban}. + * + * @remarks + * Requires the `BAN_MEMBERS` permission. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-ban} + */ +export async function getBan ( + rest: RestManager, + guildId: BigString, + userId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_BAN(guildId, userId) + ) + + return { + reason: result.reason ?? undefined, + user: rest.transformers.user(rest, result.user) + } +} diff --git a/packages/rest/src/helpers/guilds/getBans.ts b/packages/rest/src/helpers/guilds/getBans.ts new file mode 100644 index 000000000..ea9fa5850 --- /dev/null +++ b/packages/rest/src/helpers/guilds/getBans.ts @@ -0,0 +1,53 @@ +import { BigString, DiscordBan } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Ban } from './getBan.js' + +/** + * Gets the list of bans for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the list of bans for. + * @param options - The parameters for the fetching of the list of bans. + * @returns A collection of {@link Ban} objects assorted by user ID. + * + * @remarks + * Requires the `BAN_MEMBERS` permission. + * + * Users are ordered by their IDs in _ascending_ order. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-bans} + */ +export async function getBans ( + rest: RestManager, + guildId: BigString, + options?: GetBans +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_BANS(guildId, options) + ) + + return new Collection( + results.map<[bigint, Ban]>((result) => { + const user = rest.transformers.user(rest, result.user) + return [ + user.id, + { + reason: result.reason ?? undefined, + user + } + ] + }) + ) +} + +export interface GetBans { + /** Number of users to return (up to maximum 1000). Default: 1000 */ + limit?: number + /** Consider only users before given user id */ + before?: BigString + /** Consider only users after given user id */ + after?: BigString +} diff --git a/packages/rest/src/helpers/guilds/getGuildBannerUrl.ts b/packages/rest/src/helpers/guilds/getGuildBannerUrl.ts new file mode 100644 index 000000000..c7109f03e --- /dev/null +++ b/packages/rest/src/helpers/guilds/getGuildBannerUrl.ts @@ -0,0 +1,36 @@ +import { BigString, ImageFormat, ImageSize } from '@discordeno/types' +import { formatImageURL, iconBigintToHash } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' + +// TODO: Move `banner` from `options` into the parameters and rename to `imageHash`. + +/** + * Builds a URL to the guild banner stored in the Discord CDN. + * + * @param bot - The bot instance to use to build the URL. + * @param guildId - The ID of the guild to get the link to the banner for. + * @param options - The parameters for the building of the URL. + * @returns The link to the resource or `undefined` if no banner has been set. + */ +export function getGuildBannerURL ( + rest: RestManager, + guildId: BigString, + options: { + banner?: string | bigint + size?: ImageSize + format?: ImageFormat + } +): string | undefined { + return options.banner + ? formatImageURL( + rest.constants.routes.GUILD_BANNER( + guildId, + typeof options.banner === 'string' + ? options.banner + : iconBigintToHash(options.banner) + ), + options.size ?? 128, + options.format + ) + : undefined +} diff --git a/packages/rest/src/helpers/guilds/getGuildIconUrl.ts b/packages/rest/src/helpers/guilds/getGuildIconUrl.ts new file mode 100644 index 000000000..d6a5ba409 --- /dev/null +++ b/packages/rest/src/helpers/guilds/getGuildIconUrl.ts @@ -0,0 +1,34 @@ +import { BigString, ImageFormat, ImageSize } from '@discordeno/types' +import { formatImageURL, iconBigintToHash } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' + +/** + * Builds a URL to the guild icon stored in the Discord CDN. + * + * @param bot - The bot instance to use to build the URL. + * @param guildId - The ID of the guild to get the link to the banner for. + * @param options - The parameters for the building of the URL. + * @returns The link to the resource or `undefined` if no banner has been set. + */ +export function getGuildIconURL ( + rest: RestManager, + guildId: BigString, + imageHash: BigString | undefined, + options?: { + size?: ImageSize + format?: ImageFormat + } +): string | undefined { + return imageHash + ? formatImageURL( + rest.constants.routes.GUILD_ICON( + guildId, + typeof imageHash === 'string' + ? imageHash + : iconBigintToHash(imageHash) + ), + options?.size ?? 128, + options?.format + ) + : undefined +} diff --git a/packages/rest/src/helpers/guilds/getGuildPreview.ts b/packages/rest/src/helpers/guilds/getGuildPreview.ts new file mode 100644 index 000000000..dc9725b0c --- /dev/null +++ b/packages/rest/src/helpers/guilds/getGuildPreview.ts @@ -0,0 +1,63 @@ +import { + BigString, + DiscordGuildPreview, + GuildFeatures +} from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Emoji } from '../../transformers/emoji.js' +import { Sticker } from '../../transformers/sticker.js' + +export interface GuildPreview { + id: BigString + name?: string + icon?: string + splash?: string + discoverySplash?: string + emojis?: Emoji[] + features: GuildFeatures[] + approximateMemberCount: number + approximatePresenceCount: number + description?: string + stickers: Sticker[] +} + +// TODO: Move `GuildPreview` into its own transformer file. + +/** + * Gets the preview of a guild by a guild's ID. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the preview of. + * @returns An instance of {@link GuildPreview}. + * + * @remarks + * If the bot user is not in the guild, the guild must be lurkable. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-preview} + */ +export async function getGuildPreview ( + rest: RestManager, + guildId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_PREVIEW(guildId) + ) + + return { + id: rest.transformers.snowflake(result.id), + name: result.name, + icon: result.icon ?? undefined, + splash: result.splash ?? undefined, + discoverySplash: result.discovery_splash ?? undefined, + emojis: result.emojis.map((emoji) => rest.transformers.emoji(rest, emoji)), + features: result.features, + approximateMemberCount: result.approximate_member_count, + approximatePresenceCount: result.approximate_presence_count, + description: result.description ?? undefined, + stickers: result.stickers.map((sticker) => + rest.transformers.sticker(rest, sticker) + ) + } +} diff --git a/packages/rest/src/helpers/guilds/getGuildSplashUrl.ts b/packages/rest/src/helpers/guilds/getGuildSplashUrl.ts new file mode 100644 index 000000000..209381d94 --- /dev/null +++ b/packages/rest/src/helpers/guilds/getGuildSplashUrl.ts @@ -0,0 +1,35 @@ +import { BigString, ImageFormat, ImageSize } from '@discordeno/types' +import { formatImageURL, iconBigintToHash } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' + +/** + * Builds the URL to a guild splash stored in the Discord CDN. + * + * @param bot - The bot instance to use to build the URL. + * @param guildId - The ID of the guild to get the splash of. + * @param imageHash - The hash identifying the splash image. + * @param options - The parameters for the building of the URL. + * @returns The link to the resource or `undefined` if the guild does not have a splash image set. + */ +export function getGuildSplashURL ( + rest: RestManager, + guildId: BigString, + imageHash: BigString | undefined, + options?: { + size?: ImageSize + format?: ImageFormat + } +): string | undefined { + return imageHash + ? formatImageURL( + rest.constants.routes.GUILD_SPLASH( + guildId, + typeof imageHash === 'string' + ? imageHash + : iconBigintToHash(imageHash) + ), + options?.size ?? 128, + options?.format + ) + : undefined +} diff --git a/packages/rest/src/helpers/guilds/getPruneCount.ts b/packages/rest/src/helpers/guilds/getPruneCount.ts new file mode 100644 index 000000000..99ec1ec4e --- /dev/null +++ b/packages/rest/src/helpers/guilds/getPruneCount.ts @@ -0,0 +1,44 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +interface DiscordPrunedCount { + pruned: number +} + +/** + * Gets the number of members that would be kicked from a guild during pruning. + * + * @param bot - The bot instance used to make the request + * @param guildId - The ID of the guild to get the prune count of. + * @param options - The parameters for the fetching of the prune count. + * @returns A number indicating the number of members that would be kicked. + * + * @remarks + * Requires the `KICK_MEMBERS` permission. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-prune-count} + */ +export async function getPruneCount ( + rest: RestManager, + guildId: BigString, + options?: GetGuildPruneCountQuery +): Promise { + if (options?.days && options.days < 1) { throw new Error(rest.constants.Errors.PRUNE_MIN_DAYS) } + if (options?.days && options.days > 30) { throw new Error(rest.constants.Errors.PRUNE_MAX_DAYS) } + + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_PRUNE(guildId) + ) + + return result.pruned +} + +/** https://discord.com/developers/docs/resources/guild#get-guild-prune-count */ +export interface GetGuildPruneCountQuery { + /** Number of days to count prune for (1 or more), default: 7 */ + days?: number + /** Role(s) to include, default: none */ + includeRoles?: string | string[] +} diff --git a/packages/rest/src/helpers/guilds/getVanityUrl.ts b/packages/rest/src/helpers/guilds/getVanityUrl.ts new file mode 100644 index 000000000..7442dca73 --- /dev/null +++ b/packages/rest/src/helpers/guilds/getVanityUrl.ts @@ -0,0 +1,34 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +export interface VanityUrl { + code: string | null + uses: number +} + +// TODO: Move `VanityUrl` into its own transformer file. + +/** + * Gets information about the vanity url of a guild. + * + * @param bot - The bot instance used to make the request + * @param guildId - The ID of the guild to get the vanity url information for. + * @returns An instance of {@link VanityUrl}. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * The `code` property will be `null` if the guild does not have a set vanity url. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-vanity-url} + */ +export async function getVanityUrl ( + rest: RestManager, + guildId: BigString +): Promise { + return await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_VANITY_URL(guildId) + ) +} diff --git a/packages/rest/src/helpers/guilds/getWelcomeScreen.ts b/packages/rest/src/helpers/guilds/getWelcomeScreen.ts new file mode 100644 index 000000000..24b905b55 --- /dev/null +++ b/packages/rest/src/helpers/guilds/getWelcomeScreen.ts @@ -0,0 +1,29 @@ +import { BigString, DiscordWelcomeScreen } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { WelcomeScreen } from '../../transformers/welcomeScreen.js' + +/** + * Gets the welcome screen for a guild. + * + * @param bot - The bot instance used to make the request + * @param guildId - The ID of the guild to get the welcome screen for. + * @returns An instance of {@link WelcomeScreen}. + * + * @remarks + * If the welcome screen is not enabled: + * - Requires the `MANAGE_GUILD` permission. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-welcome-screen} + */ +export async function getWelcomeScreen ( + rest: RestManager, + guildId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_WELCOME_SCREEN(guildId) + ) + + return rest.transformers.welcomeScreen(rest, result) +} diff --git a/packages/rest/src/helpers/guilds/index.ts b/packages/rest/src/helpers/guilds/index.ts new file mode 100644 index 000000000..700dd0322 --- /dev/null +++ b/packages/rest/src/helpers/guilds/index.ts @@ -0,0 +1,22 @@ +export * from './automod/index.js' +export * from './createGuild.js' +export * from './deleteGuild.js' +export * from './editGuild.js' +export * from './editGuildMfaLevel.js' +export * from './editWelcomeScreen.js' +export * from './events/index.js' +export * from './getAuditLog.js' +export * from './getBan.js' +export * from './getBans.js' +export * from './getGuildBannerUrl.js' +export * from './getGuildIconUrl.js' +export * from './getGuildPreview.js' +export * from './getGuildSplashUrl.js' +export * from './getPruneCount.js' +export * from './getVanityUrl.js' +export * from './getWelcomeScreen.js' +export * from './integrations/index.js' +export * from './invites/index.js' +export * from './leaveGuild.js' +export * from './voice/index.js' +export * from './widget/index.js' diff --git a/packages/rest/src/helpers/guilds/integrations/deleteIntegration.ts b/packages/rest/src/helpers/guilds/integrations/deleteIntegration.ts new file mode 100644 index 000000000..3ed0f27ef --- /dev/null +++ b/packages/rest/src/helpers/guilds/integrations/deleteIntegration.ts @@ -0,0 +1,31 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Deletes an integration attached to a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild from which to delete the integration. + * @param integrationId - The ID of the integration to delete from the guild. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * Deletes all webhooks associated with the integration, and kicks the associated bot if there is one. + * + * Fires a _Guild Integrations Update_ gateway event. + * Fires a _Integration Delete_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#delete-guild-integration} + */ +export async function deleteIntegration ( + rest: RestManager, + guildId: BigString, + integrationId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD_INTEGRATION(guildId, integrationId) + ) +} diff --git a/packages/rest/src/helpers/guilds/integrations/getIntegrations.ts b/packages/rest/src/helpers/guilds/integrations/getIntegrations.ts new file mode 100644 index 000000000..0978e2437 --- /dev/null +++ b/packages/rest/src/helpers/guilds/integrations/getIntegrations.ts @@ -0,0 +1,52 @@ +import { BigString, DiscordIntegration } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { Integration } from '../../../transformers/integration.js' + +/** + * Gets the list of integrations attached to a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the list of integrations from. + * @returns A collection of {@link Integration} objects assorted by integration ID. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-integrations} + */ +export async function getIntegrations ( + rest: RestManager, + guildId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_INTEGRATIONS(guildId) + ) + + return new Collection( + results.map((result) => { + const integration = rest.transformers.integration(rest, { + guild_id: guildId.toString(), + id: result.id, + name: result.name, + type: result.type, + enabled: result.enabled, + syncing: result.syncing, + role_id: result.role_id, + enable_emoticons: result.enable_emoticons, + expire_behavior: result.expire_behavior, + expire_grace_period: result.expire_grace_period, + user: result.user, + account: result.account, + synced_at: result.synced_at, + subscriber_count: result.subscriber_count, + revoked: result.revoked, + application: result.application, + scopes: result.scopes + }) + return [integration.id, integration] + }) + ) +} diff --git a/packages/rest/src/helpers/guilds/integrations/index.ts b/packages/rest/src/helpers/guilds/integrations/index.ts new file mode 100644 index 000000000..c6b9ef597 --- /dev/null +++ b/packages/rest/src/helpers/guilds/integrations/index.ts @@ -0,0 +1,2 @@ +export * from './deleteIntegration.js' +export * from './getIntegrations.js' diff --git a/packages/rest/src/helpers/guilds/invites/createInvite.ts b/packages/rest/src/helpers/guilds/invites/createInvite.ts new file mode 100644 index 000000000..896de9e4c --- /dev/null +++ b/packages/rest/src/helpers/guilds/invites/createInvite.ts @@ -0,0 +1,84 @@ +import { BigString, DiscordInvite, TargetTypes } from '@discordeno/types' +import { WithReason } from '../../../index.js' +import type { RestManager } from '../../../restManager.js' +import { BaseInvite } from './getInvite.js' + +/** + * Creates an invite to a channel in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to create the invite to. + * @param options - The parameters for the creation of the invite. + * @returns An instance of the created {@link BaseInvite | Invite}. + * + * @remarks + * Requires the `CREATE_INSTANT_INVITE` permission. + * + * Fires an _Invite Create_ gateway event. + * + * @privateRemarks + * The request body is not optional, and an empty JSON object must be sent regardless of whether any fields are being transmitted. + * + * @see {@link https://discord.com/developers/docs/resources/channel#create-channel-invite} + */ +export async function createInvite ( + rest: RestManager, + channelId: BigString, + options: CreateChannelInvite = {} +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.CHANNEL_INVITES(channelId), + { + max_age: options.maxAge, + max_uses: options.maxUses, + temporary: options.temporary, + unique: options.unique, + target_type: options.targetType, + target_user_id: options.targetUserId?.toString(), + target_application_id: options.targetApplicationId?.toString(), + reason: options.reason + } + ) + + return { + code: result.code, + guildId: result.guild?.id + ? rest.transformers.snowflake(result.guild.id) + : undefined, + channelId: result.channel?.id + ? rest.transformers.snowflake(result.channel.id) + : undefined, + inviter: result.inviter + ? rest.transformers.user(rest, result.inviter) + : undefined, + targetType: result.target_type, + targetUser: result.target_user + ? rest.transformers.user(rest, result.target_user) + : undefined, + targetApplicationId: result.target_application?.id + ? rest.transformers.snowflake(result.target_application.id) + : undefined, + approximatePresenceCount: result.approximate_presence_count, + approximateMemberCount: result.approximate_member_count, + expiresAt: result.expires_at ? Date.parse(result.expires_at) : undefined + } +} + +export interface CreateChannelInvite extends WithReason { + /** Duration of invite in seconds before expiry, or 0 for never. Between 0 and 604800 (7 days). Default: 86400 (24 hours) */ + maxAge?: number + /** Max number of users or 0 for unlimited. Between 0 and 100. Default: 0 */ + maxUses?: number + /** Whether this invite only grants temporary membership. Default: false */ + temporary?: boolean + /** If true, don't try to reuse similar invite (useful for creating many unique one time use invites). Default: false */ + unique?: boolean + /** The type of target for this voice channel invite */ + targetType?: TargetTypes + /** The id of the user whose stream to display for this invite, required if `target_type` is 1, the user must be streaming in the channel */ + targetUserId?: BigString + /** The id of the embedded application to open for this invite, required if `target_type` is 2, the application must have the `EMBEDDED` flag */ + targetApplicationId?: BigString +} diff --git a/packages/rest/src/helpers/guilds/invites/deleteInvite.ts b/packages/rest/src/helpers/guilds/invites/deleteInvite.ts new file mode 100644 index 000000000..40731119d --- /dev/null +++ b/packages/rest/src/helpers/guilds/invites/deleteInvite.ts @@ -0,0 +1,27 @@ +import type { RestManager } from '../../../restManager.js' + +/** + * Deletes an invite to a channel. + * + * @param bot - The bot instance to use to make the request. + * @param inviteCode - The invite code of the invite to delete. + * + * @remarks + * Requires the `MANAGE_CHANNELS` permission. + * + * Fires an _Invite Delete_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#delete-channel-invite} + */ +export async function deleteInvite ( + rest: RestManager, + inviteCode: string, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.INVITE(inviteCode), + reason ? { reason } : undefined + ) +} diff --git a/packages/rest/src/helpers/guilds/invites/getInvite.ts b/packages/rest/src/helpers/guilds/invites/getInvite.ts new file mode 100644 index 000000000..0fcb8930e --- /dev/null +++ b/packages/rest/src/helpers/guilds/invites/getInvite.ts @@ -0,0 +1,89 @@ +import { + BigString, + DiscordInviteMetadata, + TargetTypes +} from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { User } from '../../../transformers/member.js' +import { ScheduledEvent } from '../../../transformers/scheduledEvent.js' + +export interface BaseInvite { + code: string + guildId?: BigString + channelId?: BigString + inviter?: User + targetType?: TargetTypes + targetUser?: User + targetApplicationId?: BigString + approximatePresenceCount?: number + approximateMemberCount?: number + expiresAt?: number + guildScheduledEvent?: ScheduledEvent +} + +export type InviteMetadata = BaseInvite & { + uses: number + maxUses: number + maxAge: number + temporary: boolean + createdAt: number +} + +/** + * Gets an invite to a channel by its invite code. + * + * @param bot - The bot instance to use to make the request. + * @param inviteCode - The invite code of the invite to get. + * @param options - The parameters for the fetching of the invite. + * @returns An instance of {@link BaseInvite | Invite}. + * + * @see {@link https://discord.com/developers/docs/resources/invite#get-invite} + */ +export async function getInvite ( + rest: RestManager, + inviteCode: string, + options?: GetInvite +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.INVITE(inviteCode, options) + ) + + return { + code: result.code, + guildId: result.guild?.id + ? rest.transformers.snowflake(result.guild.id) + : undefined, + channelId: result.channel?.id + ? rest.transformers.snowflake(result.channel.id) + : undefined, + inviter: result.inviter + ? rest.transformers.user(rest, result.inviter) + : undefined, + targetType: result.target_type + ? result.target_type === 1 + ? TargetTypes.Stream + : TargetTypes.EmbeddedApplication + : undefined, + targetUser: result.target_user + ? rest.transformers.user(rest, result.target_user) + : undefined, + targetApplicationId: result.target_application?.id + ? rest.transformers.snowflake(result.target_application.id) + : undefined, + approximatePresenceCount: result.approximate_presence_count, + approximateMemberCount: result.approximate_member_count, + expiresAt: result.expires_at ? Date.parse(result.expires_at) : undefined + } +} + +/** https://discord.com/developers/docs/resources/invite#get-invite */ +export interface GetInvite { + /** Whether the invite should contain approximate member counts */ + withCounts?: boolean + /** Whether the invite should contain the expiration date */ + withExpiration?: boolean + /** the guild scheduled event to include with the invite */ + scheduledEventId?: BigString +} diff --git a/packages/rest/src/helpers/guilds/invites/getInvites.ts b/packages/rest/src/helpers/guilds/invites/getInvites.ts new file mode 100644 index 000000000..f1d52f76d --- /dev/null +++ b/packages/rest/src/helpers/guilds/invites/getInvites.ts @@ -0,0 +1,74 @@ +import { + BigString, + DiscordInviteMetadata, + TargetTypes +} from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { InviteMetadata } from './getInvite.js' + +/** + * Gets the list of invites for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the invites from. + * @returns A collection of {@link InviteMetadata | Invite} objects assorted by invite code. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * @see {@link https://discord.com/developers/docs/resources/invite#get-invites} + */ +export async function getInvites ( + rest: RestManager, + guildId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_INVITES(guildId) + ) + + return new Collection( + results.map<[string, InviteMetadata]>((result) => { + const inviteMetadata = { + code: result.code, + guildId: result.guild?.id + ? rest.transformers.snowflake(result.guild.id) + : undefined, + channelId: result.channel?.id + ? rest.transformers.snowflake(result.channel.id) + : undefined, + inviter: result.inviter + ? rest.transformers.user(rest, result.inviter) + : undefined, + targetType: result.target_type + ? result.target_type === 1 + ? TargetTypes.Stream + : TargetTypes.EmbeddedApplication + : undefined, + targetUser: result.target_user + ? rest.transformers.user(rest, result.target_user) + : undefined, + targetApplicationId: result.target_application?.id + ? rest.transformers.snowflake(result.target_application.id) + : undefined, + approximatePresenceCount: result.approximate_presence_count, + approximateMemberCount: result.approximate_member_count, + expiresAt: result.expires_at + ? Date.parse(result.expires_at) + : undefined, + guildScheduledEvent: result.guild_scheduled_event + ? rest.transformers.scheduledEvent(rest, result.guild_scheduled_event) + : undefined, + // Metadata structure + uses: result.uses, + maxUses: result.max_uses, + maxAge: result.max_age, + temporary: result.temporary, + createdAt: Date.parse(result.created_at) + } + return [inviteMetadata.code, inviteMetadata] + }) + ) +} diff --git a/packages/rest/src/helpers/guilds/invites/index.ts b/packages/rest/src/helpers/guilds/invites/index.ts new file mode 100644 index 000000000..fa1aa2bd4 --- /dev/null +++ b/packages/rest/src/helpers/guilds/invites/index.ts @@ -0,0 +1,4 @@ +export * from './createInvite.js' +export * from './deleteInvite.js' +export * from './getInvite.js' +export * from './getInvites.js' diff --git a/packages/rest/src/helpers/guilds/leaveGuild.ts b/packages/rest/src/helpers/guilds/leaveGuild.ts new file mode 100644 index 000000000..a2435822c --- /dev/null +++ b/packages/rest/src/helpers/guilds/leaveGuild.ts @@ -0,0 +1,24 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Leaves a guild. + * + * @param bot - The bot instance used to make the request + * @param guildId - The ID of the guild to leave. + * + * @remarks + * Fires a _Guild Delete_ event. + * + * @see {@link https://discord.com/developers/docs/resources/user#leave-guild} + */ +export async function leaveGuild ( + rest: RestManager, + guildId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD_LEAVE(guildId) + ) +} diff --git a/packages/rest/src/helpers/guilds/voice/editVoiceState.ts b/packages/rest/src/helpers/guilds/voice/editVoiceState.ts new file mode 100644 index 000000000..6db89b105 --- /dev/null +++ b/packages/rest/src/helpers/guilds/voice/editVoiceState.ts @@ -0,0 +1,94 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +export const updateBotVoiceState = editOwnVoiceState + +/** + * Edits the voice state of the bot user. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild in which to edit the voice state of the bot user. + * @param options - The parameters for the edit of the voice state. + * + * @remarks + * The {@link EditOwnVoiceState.channelId | channelId} property of the {@link options} object parameter must point to a stage channel, and the bot user must already have joined it. + * + * If attempting to unmute oneself: + * - Requires the `MUTE_MEMBERS` permission. + * + * If attempting to request to speak: + * - Requires the `REQUEST_TO_SPEAK` permission. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state} + */ +export async function editOwnVoiceState ( + rest: RestManager, + guildId: BigString, + options: EditOwnVoiceState +): Promise { + return await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.UPDATE_VOICE_STATE(guildId), + { + channel_id: options.channelId, + suppress: options.suppress, + request_to_speak_timestamp: options.requestToSpeakTimestamp + ? new Date(options.requestToSpeakTimestamp).toISOString() + : options.requestToSpeakTimestamp + } + ) +} + +// TODO: Make the `userId` property of `options` its own parameter. + +/** + * Edits the voice state of another user. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild in which to edit the voice state of the bot user. + * @param options - The parameters for the edit of the voice state. + * + * @remarks + * The {@link EditOwnVoiceState.channelId | channelId} property of the {@link options} object parameter must point to a stage channel, and the user must already have joined it. + * + * Requires the `MUTE_MEMBERS` permission. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state} + */ +export async function editUserVoiceState ( + rest: RestManager, + guildId: BigString, + options: EditUserVoiceState +): Promise { + return await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.UPDATE_VOICE_STATE(guildId, options.userId), + { + channel_id: options.channelId, + suppress: options.suppress, + user_id: options.userId + } + ) +} + +/** https://discord.com/developers/docs/resources/guild#update-current-user-voice-state */ +export interface EditOwnVoiceState { + /** The id of the channel the user is currently in */ + channelId: BigString + /** Toggles the user's suppress state */ + suppress?: boolean + /** Sets the user's request to speak */ + requestToSpeakTimestamp?: number | null +} + +/** https://discord.com/developers/docs/resources/guild#update-user-voice-state */ +export interface EditUserVoiceState { + /** The id of the channel the user is currently in */ + channelId: BigString + /** Toggles the user's suppress state */ + suppress?: boolean + /** The user id to target */ + userId: BigString +} diff --git a/packages/rest/src/helpers/guilds/voice/getAvailableVoiceRegions.ts b/packages/rest/src/helpers/guilds/voice/getAvailableVoiceRegions.ts new file mode 100644 index 000000000..c7a7122f7 --- /dev/null +++ b/packages/rest/src/helpers/guilds/voice/getAvailableVoiceRegions.ts @@ -0,0 +1,27 @@ +import { DiscordVoiceRegion } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { VoiceRegions } from '../../../transformers/voiceRegion.js' + +/** + * Gets the list of available voice regions. + * + * @param bot - The bot instance to use to make the request. + * @returns A collection of {@link VoiceRegions | VoiceRegion} objects assorted by voice region ID. + */ +export async function getAvailableVoiceRegions ( + rest: RestManager +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.VOICE_REGIONS() + ) + + return new Collection( + results.map((result) => { + const region = rest.transformers.voiceRegion(rest, result) + return [region.id, region] + }) + ) +} diff --git a/packages/rest/src/helpers/guilds/voice/getVoiceRegions.ts b/packages/rest/src/helpers/guilds/voice/getVoiceRegions.ts new file mode 100644 index 000000000..5e9623679 --- /dev/null +++ b/packages/rest/src/helpers/guilds/voice/getVoiceRegions.ts @@ -0,0 +1,31 @@ +import { BigString, DiscordVoiceRegion } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { VoiceRegions } from '../../../transformers/voiceRegion.js' + +/** + * Gets the list of voice regions for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the voice regions for. + * @returns A collection of {@link VoiceRegions | VoiceRegion} objects assorted by voice region ID. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-voice-regions} + */ +export async function getVoiceRegions ( + rest: RestManager, + guildId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_REGIONS(guildId) + ) + + return new Collection( + results.map((result) => { + const region = rest.transformers.voiceRegion(rest, result) + return [region.id, region] + }) + ) +} diff --git a/packages/rest/src/helpers/guilds/voice/index.ts b/packages/rest/src/helpers/guilds/voice/index.ts new file mode 100644 index 000000000..d48947f05 --- /dev/null +++ b/packages/rest/src/helpers/guilds/voice/index.ts @@ -0,0 +1,3 @@ +export * from './editVoiceState.js' +export * from './getAvailableVoiceRegions.js' +export * from './getVoiceRegions.js' diff --git a/packages/rest/src/helpers/guilds/widget/editWidgetSettings.ts b/packages/rest/src/helpers/guilds/widget/editWidgetSettings.ts new file mode 100644 index 000000000..3521194e8 --- /dev/null +++ b/packages/rest/src/helpers/guilds/widget/editWidgetSettings.ts @@ -0,0 +1,38 @@ +import { BigString, DiscordGuildWidgetSettings } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { GuildWidgetSettings } from '../../../transformers/widgetSettings.js' + +// TODO: Use `options` instead of `enabled` and `channelId`. + +/** + * Edits the settings of a guild's widget. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to edit the settings of the widget of. + * @returns An instance of the edited {@link GuildWidgetSettings}. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * Fires a _Guild Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-guild-widget} + */ +export async function editWidgetSettings ( + rest: RestManager, + guildId: BigString, + enabled: boolean, + channelId?: string | null +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD_WIDGET(guildId), + { + enabled, + channel_id: channelId + } + ) + + return rest.transformers.widgetSettings(rest, result) +} diff --git a/packages/rest/src/helpers/guilds/widget/getWidget.ts b/packages/rest/src/helpers/guilds/widget/getWidget.ts new file mode 100644 index 000000000..c0c23b260 --- /dev/null +++ b/packages/rest/src/helpers/guilds/widget/getWidget.ts @@ -0,0 +1,25 @@ +import { BigString, DiscordGuildWidget } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { GuildWidget } from '../../../transformers/widget.js' + +/** + * Gets the guild widget by guild ID. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the widget of. + * @returns An instance of {@link GuildWidget}. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-widget} + */ +export async function getWidget ( + rest: RestManager, + guildId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_WIDGET_JSON(guildId) + ) + + return rest.transformers.widget(rest, result) +} diff --git a/packages/rest/src/helpers/guilds/widget/getWidgetImageUrl.ts b/packages/rest/src/helpers/guilds/widget/getWidgetImageUrl.ts new file mode 100644 index 000000000..56eaf08df --- /dev/null +++ b/packages/rest/src/helpers/guilds/widget/getWidgetImageUrl.ts @@ -0,0 +1,32 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Builds a URL to the guild widget image stored in the Discord CDN. + * + * @param bot - The bot instance to use to build the URL. + * @param guildId - The ID of the guild to get the link to the widget image for. + * @param options - The parameters for the building of the URL. + * @returns The link to the resource. + */ +export function getWidgetImageURL ( + rest: RestManager, + guildId: BigString, + options?: GetGuildWidgetImageQuery +): string { + return rest.constants.routes.GUILD_WIDGET_IMAGE(guildId, options?.style) +} + +/** https://discord.com/developers/docs/resources/guild#get-guild-widget-image-query-string-params */ +export interface GetGuildWidgetImageQuery { + /** + * Style of the widget returned, default: shield + * + * Shield: Widget with Discord icon and guild members online count. + * Banner1: Large image with guild icon, name and online count. "POWERED BY DISCORD" as the footer of the widget + * Banner2: Smaller widget style with guild icon, name and online count. Split on the right with Discord logo + * Banner3: Large image with guild icon, name and online count. In the footer, Discord logo on the left and "Chat Now" on the right + * Banner4: Large Discord logo at the top of the widget. Guild icon, name and online count in the middle portion of the widget and a "JOIN MY SERVER" button at the bottom + */ + style?: 'shield' | 'banner1' | 'banner2' | 'banner3' | 'banner4' +} diff --git a/packages/rest/src/helpers/guilds/widget/getWidgetSettings.ts b/packages/rest/src/helpers/guilds/widget/getWidgetSettings.ts new file mode 100644 index 000000000..6a15bdd70 --- /dev/null +++ b/packages/rest/src/helpers/guilds/widget/getWidgetSettings.ts @@ -0,0 +1,28 @@ +import { BigString, DiscordGuildWidgetSettings } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { GuildWidgetSettings } from '../../../transformers/widgetSettings.js' + +/** + * Gets the settings of a guild's widget. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the widget of. + * @returns An instance of {@link GuildWidgetSettings}. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-widget-settings} + */ +export async function getWidgetSettings ( + rest: RestManager, + guildId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_WIDGET(guildId) + ) + + return rest.transformers.widgetSettings(rest, result) +} diff --git a/packages/rest/src/helpers/guilds/widget/index.ts b/packages/rest/src/helpers/guilds/widget/index.ts new file mode 100644 index 000000000..9be195699 --- /dev/null +++ b/packages/rest/src/helpers/guilds/widget/index.ts @@ -0,0 +1,4 @@ +export * from './editWidgetSettings.js' +export * from './getWidget.js' +export * from './getWidgetImageUrl.js' +export * from './getWidgetSettings.js' diff --git a/packages/rest/src/helpers/index.ts b/packages/rest/src/helpers/index.ts new file mode 100644 index 000000000..a372e2cc3 --- /dev/null +++ b/packages/rest/src/helpers/index.ts @@ -0,0 +1,11 @@ +export * from './channels/index.js' +export * from './emojis/index.js' +export * from './guilds/index.js' +export * from './interactions/index.js' +export * from './members/index.js' +export * from './messages/index.js' +export * from './misc/index.js' +export * from './roles/index.js' +export * from './stickers/index.js' +export * from './templates/index.js' +export * from './webhooks/index.js' diff --git a/packages/rest/src/helpers/interactions/commands/createGlobalApplicationCommand.ts b/packages/rest/src/helpers/interactions/commands/createGlobalApplicationCommand.ts new file mode 100644 index 000000000..22b94634c --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/createGlobalApplicationCommand.ts @@ -0,0 +1,32 @@ +import { DiscordApplicationCommand } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' +import { CreateApplicationCommand } from '../../../types.js' + +/** + * Creates an application command accessible globally; across different guilds and channels. + * + * @param bot - The bot instance to use to make the request. + * @param command - The command to create. + * @returns An instance of the created {@link ApplicationCommand}. + * + * @remarks + * ⚠️ Creating a command with the same name as an existing command for your application will overwrite the old command. + * ⚠️ Global commands once created are cached for periods of __an hour__, so changes made to existing commands will take an hour to surface. + * ⚠️ You can only create up to 200 _new_ commands daily. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#create-global-application-command} + */ +export async function createGlobalApplicationCommand ( + rest: RestManager, + command: CreateApplicationCommand +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.COMMANDS(rest.applicationId), + rest.transformers.reverse.createApplicationCommand(rest, command) + ) + + return rest.transformers.applicationCommand(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/commands/createGuildApplicationCommand.ts b/packages/rest/src/helpers/interactions/commands/createGuildApplicationCommand.ts new file mode 100644 index 000000000..361b350a0 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/createGuildApplicationCommand.ts @@ -0,0 +1,33 @@ +import { BigString, DiscordApplicationCommand } from '@discordeno/types' +import { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' +import { CreateApplicationCommand } from '../../../types.js' + +/** + * Creates an application command only accessible in a specific guild. + * + * @param bot - The bot instance to use to make the request. + * @param command - The command to create. + * @param guildId - The ID of the guild to create the command for. + * @returns An instance of the created {@link ApplicationCommand}. + * + * @remarks + * ⚠️ Creating a command with the same name as an existing command for your application will overwrite the old command. + * ⚠️ You can only create up to 200 _new_ commands daily. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command} + */ +export async function createGuildApplicationCommand ( + rest: RestManager, + command: CreateApplicationCommand, + guildId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.COMMANDS_GUILD(rest.applicationId, guildId), + rest.transformers.reverse.createApplicationCommand(rest, command) + ) + + return rest.transformers.applicationCommand(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/commands/deleteGlobalApplicationCommand.ts b/packages/rest/src/helpers/interactions/commands/deleteGlobalApplicationCommand.ts new file mode 100644 index 000000000..803975b90 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/deleteGlobalApplicationCommand.ts @@ -0,0 +1,21 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Deletes an application command registered globally. + * + * @param bot - The bot instance to use to make the request. + * @param commandId - The ID of the command to delete. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#delete-global-application-command} + */ +export async function deleteGlobalApplicationCommand ( + rest: RestManager, + commandId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.COMMANDS_ID(rest.applicationId, commandId) + ) +} diff --git a/packages/rest/src/helpers/interactions/commands/deleteGuildApplicationCommand.ts b/packages/rest/src/helpers/interactions/commands/deleteGuildApplicationCommand.ts new file mode 100644 index 000000000..d49a865e1 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/deleteGuildApplicationCommand.ts @@ -0,0 +1,29 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +// TODO: `guildId` and `commandId` should be swapped. + +/** + * Deletes an application command registered in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to delete the command from. + * @param commandId - The ID of the command to delete from the guild. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#delete-guild-application-command} + */ +export async function deleteGuildApplicationCommand ( + rest: RestManager, + commandId: BigString, + guildId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.COMMANDS_GUILD_ID( + rest.applicationId, + guildId, + commandId + ) + ) +} diff --git a/packages/rest/src/helpers/interactions/commands/editApplicationCommandPermissions.ts b/packages/rest/src/helpers/interactions/commands/editApplicationCommandPermissions.ts new file mode 100644 index 000000000..b9041a353 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/editApplicationCommandPermissions.ts @@ -0,0 +1,62 @@ +import { + ApplicationCommandPermissionTypes, + BigString, + DiscordGuildApplicationCommandPermissions +} from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommandPermission } from '../../../transformers/applicationCommandPermission.js' + +// TODO: Make `options` into an object with a `permissions` field. + +/** + * Edits the permissions for a guild application command. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild the command is registered in. + * @param commandId - The ID of the command to edit the permissions of. + * @param bearerToken - The bearer token to use to make the request. + * @param options - The parameters for the edit of the command permissions. + * @returns An instance of the edited {@link ApplicationCommandPermission}. + * + * @remarks + * The bearer token requires the `applications.commands.permissions.update` scope to be enabled, and to have access to the guild whose ID has been provided in the parameters. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions} + */ +export async function editApplicationCommandPermissions ( + rest: RestManager, + guildId: BigString, + commandId: BigString, + /** Bearer token which has the `applications.commands.permissions.update` scope and also access to this guild. */ + bearerToken: string, + options: ApplicationCommandPermissions[] +): Promise { + const result = + await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.COMMANDS_PERMISSION( + rest.applicationId, + guildId, + commandId + ), + { + permissions: options + }, + { + headers: { authorization: `Bearer ${bearerToken}` } + } + ) + + return rest.transformers.applicationCommandPermission(rest, result) +} + +/** https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions */ +export interface ApplicationCommandPermissions { + /** The id of the role or user */ + id: string + /** Role or User */ + type: ApplicationCommandPermissionTypes + /** `true` to allow, `false`, to disallow */ + permission: boolean +} diff --git a/packages/rest/src/helpers/interactions/commands/editGlobalApplicationCommand.ts b/packages/rest/src/helpers/interactions/commands/editGlobalApplicationCommand.ts new file mode 100644 index 000000000..5e2f46244 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/editGlobalApplicationCommand.ts @@ -0,0 +1,29 @@ +import { BigString, DiscordApplicationCommand } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' +import { CreateApplicationCommand } from '../../../types' + +/** + * Edits a global application command. + * + * @param bot - The bot instance to use to make the request. + * @param commandId - The ID of the command to edit. + * @param options - The parameters for the edit of the command. + * @returns An instance of the edited {@link ApplicationCommand}. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#edit-global-application-command} + */ +export async function editGlobalApplicationCommand ( + rest: RestManager, + commandId: BigString, + options: CreateApplicationCommand +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.COMMANDS_ID(rest.applicationId, commandId), + rest.transformers.reverse.createApplicationCommand(rest, options) + ) + + return rest.transformers.applicationCommand(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/commands/editGuildApplicationCommand.ts b/packages/rest/src/helpers/interactions/commands/editGuildApplicationCommand.ts new file mode 100644 index 000000000..a4b493e41 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/editGuildApplicationCommand.ts @@ -0,0 +1,37 @@ +import { BigString, DiscordApplicationCommand } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' +import { CreateApplicationCommand } from '../../../types' + +// TODO: Swap `commandId` and `guildId` parameters. + +/** + * Edits an application command registered in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild the command is registered in. + * @param commandId - The ID of the command to edit. + * @param options - The parameters for the edit of the command. + * @returns An instance of the edited {@link ApplicationCommand}. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#edit-guild-application-command} + */ +export async function editGuildApplicationCommand ( + rest: RestManager, + commandId: BigString, + guildId: BigString, + options: CreateApplicationCommand +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.COMMANDS_GUILD_ID( + rest.applicationId, + guildId, + commandId + ), + rest.transformers.reverse.createApplicationCommand(rest, options) + ) + + return rest.transformers.applicationCommand(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/commands/getApplicationCommandPermission.ts b/packages/rest/src/helpers/interactions/commands/getApplicationCommandPermission.ts new file mode 100644 index 000000000..b496fa18b --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/getApplicationCommandPermission.ts @@ -0,0 +1,35 @@ +import { + BigString, + DiscordGuildApplicationCommandPermissions +} from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommandPermission } from '../../../transformers/applicationCommandPermission.js' + +/** + * Gets the permissions of a guild application command. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild the command is registered in. + * @param commandId - The ID of the command to get the permissions of. + * @returns An instance of {@link ApplicationCommandPermission}. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#get-application-command-permissions} + */ +export async function getApplicationCommandPermission ( + rest: RestManager, + guildId: BigString, + commandId: BigString +): Promise { + const result = + await rest.runMethod( + rest, + 'GET', + rest.constants.routes.COMMANDS_PERMISSION( + rest.applicationId, + guildId, + commandId + ) + ) + + return rest.transformers.applicationCommandPermission(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/commands/getApplicationCommandPermissions.ts b/packages/rest/src/helpers/interactions/commands/getApplicationCommandPermissions.ts new file mode 100644 index 000000000..6f342ddf6 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/getApplicationCommandPermissions.ts @@ -0,0 +1,39 @@ +import { + BigString, + DiscordGuildApplicationCommandPermissions +} from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommandPermission } from '../../../transformers/applicationCommandPermission.js' + +/** + * Gets the permissions of all application commands registered in a guild by the ID of the guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the permissions objects of. + * @returns A collection of {@link ApplicationCommandPermission} objects assorted by command ID. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command-permissions} + */ +export async function getApplicationCommandPermissions ( + rest: RestManager, + guildId: BigString +): Promise> { + const results = await rest.runMethod< + DiscordGuildApplicationCommandPermissions[] + >( + rest, + 'GET', + rest.constants.routes.COMMANDS_PERMISSIONS(rest.applicationId, guildId) + ) + + return new Collection( + results.map((result) => { + const permission = rest.transformers.applicationCommandPermission( + rest, + result + ) + return [permission.id, permission] + }) + ) +} diff --git a/packages/rest/src/helpers/interactions/commands/getGlobalApplicationCommand.ts b/packages/rest/src/helpers/interactions/commands/getGlobalApplicationCommand.ts new file mode 100644 index 000000000..8cdfa0bb5 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/getGlobalApplicationCommand.ts @@ -0,0 +1,25 @@ +import { BigString, DiscordApplicationCommand } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' + +/** + * Gets a global application command by its ID. + * + * @param bot - The bot instance to use to make the request. + * @param commandId - The ID of the command to get. + * @returns An instance of {@link ApplicationCommand}. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#get-global-application-command} + */ +export async function getGlobalApplicationCommand ( + rest: RestManager, + commandId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.COMMANDS_ID(rest.applicationId, commandId) + ) + + return rest.transformers.applicationCommand(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/commands/getGlobalApplicationCommands.ts b/packages/rest/src/helpers/interactions/commands/getGlobalApplicationCommands.ts new file mode 100644 index 000000000..e7bcedf32 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/getGlobalApplicationCommands.ts @@ -0,0 +1,31 @@ +import { DiscordApplicationCommand } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' + +// TODO: Implement `with_localizations` options field. + +/** + * Gets the list of your bot's global application commands. + * + * @param bot - The bot instance to use to make the request. + * @returns A collection of {@link ApplicationCommand} objects assorted by command ID. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#get-global-application-commands} + */ +export async function getGlobalApplicationCommands ( + rest: RestManager +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.COMMANDS(rest.applicationId) + ) + + return new Collection( + results.map((result) => { + const command = rest.transformers.applicationCommand(rest, result) + return [command.id, command] + }) + ) +} diff --git a/packages/rest/src/helpers/interactions/commands/getGuildApplicationCommand.ts b/packages/rest/src/helpers/interactions/commands/getGuildApplicationCommand.ts new file mode 100644 index 000000000..789ada815 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/getGuildApplicationCommand.ts @@ -0,0 +1,33 @@ +import { BigString, DiscordApplicationCommand } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' + +// TODO: Swap `commandId` and `guildId` parameters. + +/** + * Gets a guild application command by its ID. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild the command is registered in. + * @param commandId - The ID of the command to get. + * @returns An instance of {@link ApplicationCommand}. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command} + */ +export async function getGuildApplicationCommand ( + rest: RestManager, + commandId: BigString, + guildId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.COMMANDS_GUILD_ID( + rest.applicationId, + guildId, + commandId + ) + ) + + return rest.transformers.applicationCommand(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/commands/getGuildApplicationCommands.ts b/packages/rest/src/helpers/interactions/commands/getGuildApplicationCommands.ts new file mode 100644 index 000000000..68007625f --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/getGuildApplicationCommands.ts @@ -0,0 +1,31 @@ +import { BigString, DiscordApplicationCommand } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' + +/** + * Gets the list of application commands registered by your bot in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild the commands are registered in. + * @returns A collection of {@link ApplicationCommand} objects assorted by command ID. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#get-global-application-commandss} + */ +export async function getGuildApplicationCommands ( + rest: RestManager, + guildId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.COMMANDS_GUILD(rest.applicationId, guildId) + ) + + return new Collection( + results.map((result) => { + const command = rest.transformers.applicationCommand(rest, result) + return [command.id, command] + }) + ) +} diff --git a/packages/rest/src/helpers/interactions/commands/index.ts b/packages/rest/src/helpers/interactions/commands/index.ts new file mode 100644 index 000000000..3f786b509 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/index.ts @@ -0,0 +1,15 @@ +export * from './createGlobalApplicationCommand.js' +export * from './createGuildApplicationCommand.js' +export * from './deleteGlobalApplicationCommand.js' +export * from './deleteGuildApplicationCommand.js' +export * from './editApplicationCommandPermissions.js' +export * from './editGlobalApplicationCommand.js' +export * from './editGuildApplicationCommand.js' +export * from './getApplicationCommandPermission.js' +export * from './getApplicationCommandPermissions.js' +export * from './getGlobalApplicationCommand.js' +export * from './getGlobalApplicationCommands.js' +export * from './getGuildApplicationCommand.js' +export * from './getGuildApplicationCommands.js' +export * from './upsertGlobalApplicationCommands.js' +export * from './upsertGuildApplicationCommands.js' diff --git a/packages/rest/src/helpers/interactions/commands/upsertGlobalApplicationCommands.ts b/packages/rest/src/helpers/interactions/commands/upsertGlobalApplicationCommands.ts new file mode 100644 index 000000000..30885dc29 --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/upsertGlobalApplicationCommands.ts @@ -0,0 +1,40 @@ +import { DiscordApplicationCommand } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' +import { CreateApplicationCommand } from '../../../types' + +/** + * Re-registers the list of global application commands, overwriting the previous commands completely. + * + * @param bot - The bot instance to use to make the request. + * @param commands - The list of commands to use to overwrite the previous list. + * @returns A collection of {@link ApplicationCommand} objects assorted by command ID. + * + * @remarks + * ❗ Commands that are not present in the `commands` array will be __deleted__. + * + * ⚠️ Commands that do not already exist will count towards the daily limit of _200_ new commands. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands} + */ +export async function upsertGlobalApplicationCommands ( + rest: RestManager, + commands: CreateApplicationCommand[] +): Promise> { + const results = await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.COMMANDS(rest.applicationId), + commands.map((command) => + rest.transformers.reverse.createApplicationCommand(rest, command) + ) + ) + + return new Collection( + results.map((result) => { + const command = rest.transformers.applicationCommand(rest, result) + return [command.id, command] + }) + ) +} diff --git a/packages/rest/src/helpers/interactions/commands/upsertGuildApplicationCommands.ts b/packages/rest/src/helpers/interactions/commands/upsertGuildApplicationCommands.ts new file mode 100644 index 000000000..7db0ab37f --- /dev/null +++ b/packages/rest/src/helpers/interactions/commands/upsertGuildApplicationCommands.ts @@ -0,0 +1,42 @@ +import { BigString, DiscordApplicationCommand } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { ApplicationCommand } from '../../../transformers/applicationCommand.js' +import { CreateApplicationCommand } from '../../../types' + +/** + * Re-registers the list of application commands registered in a guild, overwriting the previous commands completely. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild whose list of commands to overwrite. + * @param commands - The list of commands to use to overwrite the previous list. + * @returns A collection of {@link ApplicationCommand} objects assorted by command ID. + * + * @remarks + * ❗ Commands that are not present in the `commands` array will be __deleted__. + * + * ⚠️ Commands that do not already exist will count towards the daily limit of _200_ new commands. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands} + */ +export async function upsertGuildApplicationCommands ( + rest: RestManager, + guildId: BigString, + commands: CreateApplicationCommand[] +): Promise> { + const results = await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.COMMANDS_GUILD(rest.applicationId, guildId), + commands.map((command) => + rest.transformers.reverse.createApplicationCommand(rest, command) + ) + ) + + return new Collection( + results.map((result) => { + const command = rest.transformers.applicationCommand(rest, result) + return [command.id, command] + }) + ) +} diff --git a/packages/rest/src/helpers/interactions/index.ts b/packages/rest/src/helpers/interactions/index.ts new file mode 100644 index 000000000..e66d8fb8d --- /dev/null +++ b/packages/rest/src/helpers/interactions/index.ts @@ -0,0 +1,2 @@ +export * from './commands/index.js' +export * from './responses/index.js' diff --git a/packages/rest/src/helpers/interactions/responses/deleteFollowupMessage.ts b/packages/rest/src/helpers/interactions/responses/deleteFollowupMessage.ts new file mode 100644 index 000000000..c5c8a7ea9 --- /dev/null +++ b/packages/rest/src/helpers/interactions/responses/deleteFollowupMessage.ts @@ -0,0 +1,32 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Deletes a follow-up message to an interaction. + * + * @param bot - The bot instance to use to make the request. + * @param token - The interaction token to use, provided in the original interaction. + * @param messageId - The ID of the message to delete. + * + * @remarks + * Unlike `deleteMessage()`, this endpoint allows the bot user to act without needing to see the channel the message is in. + * + * Fires a _Message Delete_ event. + * + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#delete-followup-message} + */ +export async function deleteFollowupMessage ( + rest: RestManager, + token: string, + messageId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.INTERACTION_ID_TOKEN_MESSAGE_ID( + rest.applicationId, + token, + messageId + ) + ) +} diff --git a/packages/rest/src/helpers/interactions/responses/deleteOriginalInteractionResponse.ts b/packages/rest/src/helpers/interactions/responses/deleteOriginalInteractionResponse.ts new file mode 100644 index 000000000..9abb02126 --- /dev/null +++ b/packages/rest/src/helpers/interactions/responses/deleteOriginalInteractionResponse.ts @@ -0,0 +1,28 @@ +import type { RestManager } from '../../../restManager.js' + +/** + * Deletes the initial message response to an interaction. + * + * @param bot - The bot instance to use to make the request. + * @param token - The interaction token to use, provided in the original interaction. + * + * @remarks + * Unlike `deleteMessage()`, this endpoint allows the bot user to act without needing to see the channel the message is in. + * + * Fires a _Message Delete_ event. + * + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#delete-original-interaction-response} + */ +export async function deleteOriginalInteractionResponse ( + rest: RestManager, + token: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.INTERACTION_ORIGINAL_ID_TOKEN( + rest.applicationId, + token + ) + ) +} diff --git a/packages/rest/src/helpers/interactions/responses/editFollowupMessage.ts b/packages/rest/src/helpers/interactions/responses/editFollowupMessage.ts new file mode 100644 index 000000000..b1c35acd4 --- /dev/null +++ b/packages/rest/src/helpers/interactions/responses/editFollowupMessage.ts @@ -0,0 +1,49 @@ +import { + BigString, + DiscordMessage, + InteractionResponseTypes +} from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { Message } from '../../../transformers/message.js' +import { InteractionCallbackData } from '../../../types.js' + +/** + * Edits a follow-up message to an interaction. + * + * @param bot - The bot instance to use to make the request. + * @param token - The interaction token to use, provided in the original interaction. + * @param messageId - The ID of the message to edit. + * @param options - The parameters for the edit of the message. + * @returns An instance of the edited {@link Message}. + * + * @remarks + * Unlike `editMessage()`, this endpoint allows the bot user to act without needing to see the channel the message is in. + * + * Does not support ephemeral follow-up messages due to these being stateless. + * + * Fires a _Message Update_ event. + * + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#edit-followup-message} + */ +export async function editFollowupMessage ( + rest: RestManager, + token: string, + messageId: BigString, + options: InteractionCallbackData +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.WEBHOOK_MESSAGE(rest.applicationId, token, messageId), + { + messageId: messageId.toString(), + ...rest.transformers.reverse.interactionResponse(rest, { + type: InteractionResponseTypes.UpdateMessage, + data: options + }).data, + file: options.file + } + ) + + return rest.transformers.message(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/responses/editOriginalInteractionResponse.ts b/packages/rest/src/helpers/interactions/responses/editOriginalInteractionResponse.ts new file mode 100644 index 000000000..897ac5e7d --- /dev/null +++ b/packages/rest/src/helpers/interactions/responses/editOriginalInteractionResponse.ts @@ -0,0 +1,45 @@ +import { DiscordMessage, InteractionResponseTypes } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { Message } from '../../../transformers/message.js' +import { InteractionCallbackData } from '../../../types' + +/** + * Edits the initial message response to an interaction. + * + * @param bot - The bot instance to use to make the request. + * @param token - The interaction token to use, provided in the original interaction. + * @param options - The parameters for the edit of the response. + * @returns An instance of the edited {@link Message}. + * + * @remarks + * Unlike `editMessage()`, this endpoint allows the bot user to act without needing to see the channel the message is in. + * + * Does not support ephemeral follow-up messages due to these being stateless. + * + * Fires a _Message Update_ event. + * + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#edit-original-interaction-response} + */ +export async function editOriginalInteractionResponse ( + rest: RestManager, + token: string, + options: InteractionCallbackData +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.INTERACTION_ORIGINAL_ID_TOKEN( + rest.applicationId, + token + ), + { + ...rest.transformers.reverse.interactionResponse(rest, { + type: InteractionResponseTypes.UpdateMessage, + data: options + }).data, + file: options.file + } + ) + + return rest.transformers.message(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/responses/getFollowupMessage.ts b/packages/rest/src/helpers/interactions/responses/getFollowupMessage.ts new file mode 100644 index 000000000..fcb093bc6 --- /dev/null +++ b/packages/rest/src/helpers/interactions/responses/getFollowupMessage.ts @@ -0,0 +1,38 @@ +import { BigString, DiscordMessage } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { Message } from '../../../transformers/message.js' + +/** + * Gets a follow-up message to an interaction by the ID of the message. + * + * @param bot - The bot instance to use to make the request. + * @param token - The interaction token to use, provided in the original interaction. + * @param messageId - The ID of the message to get. + * @returns An instance of {@link Message}. + * + * @remarks + * Unlike `getMessage()`, this endpoint allows the bot user to act without: + * - Needing to be able to see the contents of the channel that the message is in. (`READ_MESSAGES` permission.) + * - Requiring the `MESSAGE_CONTENT` intent. + * + * Does not support ephemeral follow-up messages due to these being stateless. + * + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#get-followup-message} + */ +export async function getFollowupMessage ( + rest: RestManager, + token: string, + messageId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.INTERACTION_ID_TOKEN_MESSAGE_ID( + rest.applicationId, + token, + messageId + ) + ) + + return rest.transformers.message(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/responses/getOriginalInteractionResponse.ts b/packages/rest/src/helpers/interactions/responses/getOriginalInteractionResponse.ts new file mode 100644 index 000000000..5605bdd3e --- /dev/null +++ b/packages/rest/src/helpers/interactions/responses/getOriginalInteractionResponse.ts @@ -0,0 +1,35 @@ +import { DiscordMessage } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { Message } from '../../../transformers/message.js' + +/** + * Gets the initial message response to an interaction. + * + * @param bot - The bot instance to use to make the request. + * @param token - The interaction token to use, provided in the original interaction. + * @returns An instance of {@link Message}. + * + * @remarks + * Unlike `getMessage()`, this endpoint allows the bot user to act without: + * - Needing to be able to see the contents of the channel that the message is in. (`READ_MESSAGES` permission.) + * - Requiring the `MESSAGE_CONTENT` intent. + * + * Does not support ephemeral follow-up messages due to these being stateless. + * + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#get-original-interaction-response} + */ +export async function getOriginalInteractionResponse ( + rest: RestManager, + token: string +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.INTERACTION_ORIGINAL_ID_TOKEN( + rest.applicationId, + token + ) + ) + + return rest.transformers.message(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/responses/index.ts b/packages/rest/src/helpers/interactions/responses/index.ts new file mode 100644 index 000000000..d3de98948 --- /dev/null +++ b/packages/rest/src/helpers/interactions/responses/index.ts @@ -0,0 +1,8 @@ +export * from './deleteFollowupMessage.js' +export * from './deleteOriginalInteractionResponse.js' +export * from './editFollowupMessage.js' +export * from './editOriginalInteractionResponse.js' +export * from './getFollowupMessage.js' +export * from './getOriginalInteractionResponse.js' +export * from './sendFollowupMessage.js' +export * from './sendInteractionResponse.js' diff --git a/packages/rest/src/helpers/interactions/responses/sendFollowupMessage.ts b/packages/rest/src/helpers/interactions/responses/sendFollowupMessage.ts new file mode 100644 index 000000000..6ee7adc58 --- /dev/null +++ b/packages/rest/src/helpers/interactions/responses/sendFollowupMessage.ts @@ -0,0 +1,47 @@ +import { DiscordMessage } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { Message } from '../../../transformers/message.js' +import { InteractionResponse } from '../../../types.js' + +/** + * Sends a follow-up message to an interaction. + * + * @param bot - The bot instance to use to make the request. + * @param token - The interaction token to use, provided in the original interaction. + * @param options - The parameters for the creation of the message. + * @returns An instance of the created {@link Message}. + * + * @remarks + * ⚠️ Interaction tokens are only valid for _15 minutes_. + * + * By default, mentions are suppressed. To enable mentions, pass a mention object with the callback data. + * + * Unlike `sendMessage()`, this endpoint allows the bot user to act without: + * - Needing to be able to see the contents of the channel that the message is in. (`READ_MESSAGES` permission.) + * - Requiring the `MESSAGE_CONTENT` intent. + * + * Fires a _Message Create_ event. + * + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#create-followup-message} + */ +export async function sendFollowupMessage ( + rest: RestManager, + token: string, + options: InteractionResponse +): Promise { + const result = await rest.sendRequest(rest, { + url: rest.constants.routes.WEBHOOK(rest.applicationId, token), + method: 'POST', + payload: rest.createRequestBody(rest, { + method: 'POST', + body: { + ...rest.transformers.reverse.interactionResponse(rest, options).data, + file: options.data?.file + }, + // remove authorization header + headers: { Authorization: '' } + }) + }) + + return rest.transformers.message(rest, result) +} diff --git a/packages/rest/src/helpers/interactions/responses/sendInteractionResponse.ts b/packages/rest/src/helpers/interactions/responses/sendInteractionResponse.ts new file mode 100644 index 000000000..716d87c4d --- /dev/null +++ b/packages/rest/src/helpers/interactions/responses/sendInteractionResponse.ts @@ -0,0 +1,46 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { InteractionResponse } from '../../../types' + +/** + * Sends a response to an interaction. + * + * @param bot - The bot instance to use to make the request. + * @param interactionId - The ID of the interaction to respond to. + * @param token - The interaction token to use, provided in the original interaction. + * @param options - The parameters for the creation of the message. + * @returns An instance of the created {@link Message}. + * + * @remarks + * ⚠️ Interaction tokens are only valid for _15 minutes_. + * + * By default, mentions are suppressed. To enable mentions, pass a mention object with the callback data. + * + * Unlike `sendMessage()`, this endpoint allows the bot user to act without: + * - Needing to be able to see the contents of the channel that the message is in. (`READ_MESSAGES` permission.) + * - Requiring the `MESSAGE_CONTENT` intent. + * + * Fires a _Message Create_ event. + * + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response} + */ +export async function sendInteractionResponse ( + rest: RestManager, + interactionId: BigString, + token: string, + options: InteractionResponse +): Promise { + return await rest.sendRequest(rest, { + url: rest.constants.routes.INTERACTION_ID_TOKEN(interactionId, token), + method: 'POST', + payload: rest.createRequestBody(rest, { + method: 'POST', + body: { + ...rest.transformers.reverse.interactionResponse(rest, options), + file: options.data?.file + }, + // Remove authorization header + headers: { Authorization: '' } + }) + }) +} diff --git a/packages/rest/src/helpers/members/banMember.ts b/packages/rest/src/helpers/members/banMember.ts new file mode 100644 index 000000000..120d67db1 --- /dev/null +++ b/packages/rest/src/helpers/members/banMember.ts @@ -0,0 +1,40 @@ +import { BigString, WithReason } from '../../index.js' +import type { RestManager } from '../../restManager.js' + +/** + * Bans a user from a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to ban the user from. + * @param userId - The ID of the user to ban from the guild. + * @param options - The parameters for the creation of the ban. + * + * @remarks + * Requires the `BAN_MEMBERS` permission. + * + * Fires a _Guild Ban Add_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#create-guild-ban} + */ +export async function banMember ( + rest: RestManager, + guildId: BigString, + userId: BigString, + options?: CreateGuildBan +): Promise { + return await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.GUILD_BAN(guildId, userId), + { + delete_message_seconds: options?.deleteMessageSeconds, + reason: options?.reason + } + ) +} + +/** https://discord.com/developers/docs/resources/guild#create-guild-ban */ +export interface CreateGuildBan extends WithReason { + /** Number of seconds to delete messages for, between 0 and 604800 (7 days) */ + deleteMessageSeconds?: number +} diff --git a/packages/rest/src/helpers/members/editBotMember.ts b/packages/rest/src/helpers/members/editBotMember.ts new file mode 100644 index 000000000..0b3040a98 --- /dev/null +++ b/packages/rest/src/helpers/members/editBotMember.ts @@ -0,0 +1,43 @@ +import { BigString, DiscordMember, WithReason } from '../../index.js' +import type { RestManager } from '../../restManager.js' +import { Member } from '../../transformers/member.js' + +/** + * Edits the nickname of the bot user. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to edit the nickname of the bot user in. + * @param options - The parameters for the edit of the nickname. + * @returns An instance of the edited {@link Member} + * + * @remarks + * Fires a _Guild Member Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-current-member} + */ +export async function editBotMember ( + rest: RestManager, + guildId: BigString, + options: EditBotMemberOptions +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.USER_NICK(guildId), + { + nick: options.nick, + reason: options.reason + } + ) + + return rest.transformers.member( + rest, + result, + rest.transformers.snowflake(guildId), + rest.id + ) +} + +export interface EditBotMemberOptions extends WithReason { + nick?: string | null +} diff --git a/packages/rest/src/helpers/members/editMember.ts b/packages/rest/src/helpers/members/editMember.ts new file mode 100644 index 000000000..4320d0c32 --- /dev/null +++ b/packages/rest/src/helpers/members/editMember.ts @@ -0,0 +1,65 @@ +import { BigString, DiscordMemberWithUser } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Member } from '../../transformers/member.js' + +/** + * Edits a member's properties. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to edit the member of. + * @param userId - The user ID of the member to edit. + * @param options - The parameters for the edit of the user. + * + * @remarks + * This endpoint requires various permissions depending on what is edited about the member. + * To find out the required permission to enact a change, read the documentation of this endpoint's {@link ModifyGuildMember | parameters}. + * + * Fires a _Guild Member Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-guild-member} + */ +export async function editMember ( + rest: RestManager, + guildId: BigString, + userId: BigString, + options: ModifyGuildMember +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD_MEMBER(guildId, userId), + { + nick: options.nick, + roles: options.roles?.map((id) => id.toString()), + mute: options.mute, + deaf: options.deaf, + channel_id: options.channelId?.toString(), + communication_disabled_until: options.communicationDisabledUntil + ? new Date(options.communicationDisabledUntil).toISOString() + : options.communicationDisabledUntil + } + ) + + return rest.transformers.member( + rest, + result, + rest.transformers.snowflake(guildId), + rest.transformers.snowflake(userId) + ) +} + +/** https://discord.com/developers/docs/resources/guild#modify-guild-member */ +export interface ModifyGuildMember { + /** Value to set users nickname to. Requires the `MANAGE_NICKNAMES` permission */ + nick?: string | null + /** Array of role ids the member is assigned. Requires the `MANAGE_ROLES` permission */ + roles?: BigString[] | null + /** Whether the user is muted in voice channels. Will throw a 400 if the user is not in a voice channel. Requires the `MUTE_MEMBERS` permission */ + mute?: boolean | null + /** Whether the user is deafened in voice channels. Will throw a 400 if the user is not in a voice channel. Requires the `MOVE_MEMBERS` permission */ + deaf?: boolean | null + /** Id of channel to move user to (if they are connected to voice). Requires the `MOVE_MEMBERS` permission */ + channelId?: BigString | null + /** when the user's timeout will expire and the user will be able to communicate in the guild again (up to 28 days in the future), set to null to remove timeout. Requires the `MODERATE_MEMBERS` permission */ + communicationDisabledUntil?: number | null +} diff --git a/packages/rest/src/helpers/members/getAvatarUrl.ts b/packages/rest/src/helpers/members/getAvatarUrl.ts new file mode 100644 index 000000000..d7aaef10e --- /dev/null +++ b/packages/rest/src/helpers/members/getAvatarUrl.ts @@ -0,0 +1,36 @@ +import { BigString, ImageFormat, ImageSize } from '@discordeno/types' +import { formatImageURL, iconBigintToHash } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' + +/** + * Builds a URL to a user's avatar stored in the Discord CDN. + * + * @param bot - The bot instance to use to build the URL. + * @param userId - The ID of the user to get the avatar of. + * @param discriminator - The user's discriminator. (4-digit tag after the hashtag.) + * @param options - The parameters for the building of the URL. + * @returns The link to the resource. + */ +export function getAvatarURL ( + rest: RestManager, + userId: BigString, + discriminator: string, + options?: { + avatar: BigString | undefined + size?: ImageSize + format?: ImageFormat + } +): string { + return options?.avatar + ? formatImageURL( + rest.constants.routes.USER_AVATAR( + userId, + typeof options?.avatar === 'string' + ? options.avatar + : iconBigintToHash(options?.avatar) + ), + options?.size ?? 128, + options?.format + ) + : rest.constants.routes.USER_DEFAULT_AVATAR(Number(discriminator) % 5) +} diff --git a/packages/rest/src/helpers/members/getDmChannel.ts b/packages/rest/src/helpers/members/getDmChannel.ts new file mode 100644 index 000000000..c3a791878 --- /dev/null +++ b/packages/rest/src/helpers/members/getDmChannel.ts @@ -0,0 +1,30 @@ +import { BigString, DiscordChannel } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Channel } from '../../transformers/channel.js' + +/** + * Gets or creates a DM channel with a user. + * + * @param bot - The bot instance to use to make the request. + * @param userId - The ID of the user to create the DM channel with. + * @returns An instance of {@link Channel}. + * + * @see {@link https://discord.com/developers/docs/resources/user#create-dm} + */ +export async function getDmChannel ( + rest: RestManager, + userId: BigString +): Promise { + if (userId === rest.id) { throw new Error(rest.constants.Errors.YOU_CAN_NOT_DM_THE_BOT_ITSELF) } + + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.USER_DM(), + { + recipient_id: userId.toString() + } + ) + + return rest.transformers.channel(rest, { channel: result }) +} diff --git a/packages/rest/src/helpers/members/getMember.ts b/packages/rest/src/helpers/members/getMember.ts new file mode 100644 index 000000000..4ac7fb44a --- /dev/null +++ b/packages/rest/src/helpers/members/getMember.ts @@ -0,0 +1,32 @@ +import { BigString, DiscordMemberWithUser } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Member } from '../../transformers/member.js' + +/** + * Gets the member object by user ID. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the member object for. + * @param userId - The ID of the user to get the member object for. + * @returns An instance of {@link Member}. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-member} + */ +export async function getMember ( + rest: RestManager, + guildId: BigString, + userId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_MEMBER(guildId, userId) + ) + + return rest.transformers.member( + rest, + result, + rest.transformers.snowflake(guildId), + rest.transformers.snowflake(userId) + ) +} diff --git a/packages/rest/src/helpers/members/getMembers.ts b/packages/rest/src/helpers/members/getMembers.ts new file mode 100644 index 000000000..d25d84268 --- /dev/null +++ b/packages/rest/src/helpers/members/getMembers.ts @@ -0,0 +1,59 @@ +import { BigString, DiscordMemberWithUser } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Member } from '../../transformers/member.js' + +// TODO: make options optional + +/** + * Gets the list of members for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the list of members for. + * @param options - The parameters for the fetching of the members. + * @returns A collection of {@link Member} objects assorted by user ID. + * + * @remarks + * Requires the `GUILD_MEMBERS` intent. + * + * ⚠️ It is not recommended to use this endpoint with very large bots. Instead, opt to use `fetchMembers()`: + * REST communication only permits 50 requests to be made per second, while gateways allow for up to 120 requests + * per minute per shard. For more information, read {@link https://discord.com/developers/docs/topics/rate-limits#rate-limits}. + * + * @see {@link https://discord.com/developers/docs/resources/guild#list-guild-members} + * @see {@link https://discord.com/developers/docs/topics/gateway#request-guild-members} + * @see {@link https://discord.com/developers/docs/topics/rate-limits#rate-limits} + */ +export async function getMembers ( + rest: RestManager, + guildId: BigString, + options: ListGuildMembers +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_MEMBERS(guildId, options) + ) + + const id = rest.transformers.snowflake(guildId) + + return new Collection( + results.map((result) => { + const member = rest.transformers.member( + rest, + result, + id, + rest.transformers.snowflake(result.user.id) + ) + return [member.id, member] + }) + ) +} + +/** https://discord.com/developers/docs/resources/guild#list-guild-members */ +export interface ListGuildMembers { + /** Max number of members to return (1-1000). Default: 1000 */ + limit?: number + /** The highest user id in the previous page. Default: 0 */ + after?: string +} diff --git a/packages/rest/src/helpers/members/index.ts b/packages/rest/src/helpers/members/index.ts new file mode 100644 index 000000000..0e7770d0c --- /dev/null +++ b/packages/rest/src/helpers/members/index.ts @@ -0,0 +1,11 @@ +export * from './banMember.js' +export * from './editBotMember.js' +export * from './editMember.js' +export * from './getAvatarUrl.js' +export * from './getDmChannel.js' +export * from './getMember.js' +export * from './getMembers.js' +export * from './kickMember.js' +export * from './pruneMembers.js' +export * from './searchMembers.js' +export * from './unbanMember.js' diff --git a/packages/rest/src/helpers/members/kickMember.ts b/packages/rest/src/helpers/members/kickMember.ts new file mode 100644 index 000000000..99dd62273 --- /dev/null +++ b/packages/rest/src/helpers/members/kickMember.ts @@ -0,0 +1,32 @@ +import { BigString } from '@discordeno/types' +import { RestManager } from '../../restManager.js' + +/** + * Kicks a member from a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to kick the member from. + * @param userId - The user ID of the member to kick from the guild. + * + * @remarks + * Requires the `KICK_MEMBERS` permission. + * + * Fires a _Guild Member Remove_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#remove-guild-member} + */ +export async function kickMember ( + rest: RestManager, + guildId: BigString, + userId: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD_MEMBER(guildId, userId), + { + reason + } + ) +} diff --git a/packages/rest/src/helpers/members/pruneMembers.ts b/packages/rest/src/helpers/members/pruneMembers.ts new file mode 100644 index 000000000..c2d1531d8 --- /dev/null +++ b/packages/rest/src/helpers/members/pruneMembers.ts @@ -0,0 +1,53 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Initiates the process of pruning inactive members. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to prune the members of. + * @param options - The parameters for the pruning of members. + * @returns A number indicating how many members were pruned. + * + * @remarks + * Requires the `KICK_MEMBERS` permission. + * + * ❗ Requests to this endpoint will time out for large guilds. To prevent this from happening, set the {@link BeginGuildPrune.computePruneCount} property of the {@link options} object parameter to `false`. This will begin the process of pruning, and immediately return `undefined`, rather than wait for the process to complete before returning the actual count of members that have been kicked. + * + * ⚠️ By default, this process will not remove members with a role. To include the members who have a _particular subset of roles_, specify the role(s) in the {@link BeginGuildPrune.includeRoles | includeRoles} property of the {@link options} object parameter. + * + * Fires a _Guild Member Remove_ gateway event for every member kicked. + * + * @see {@link https://discord.com/developers/docs/resources/guild#begin-guild-prune} + */ +export async function pruneMembers ( + rest: RestManager, + guildId: BigString, + options: BeginGuildPrune +): Promise { + if (options.days && options.days < 1) { throw new Error(rest.constants.Errors.PRUNE_MIN_DAYS) } + if (options.days && options.days > 30) { throw new Error(rest.constants.Errors.PRUNE_MAX_DAYS) } + + const result = await rest.runMethod<{ pruned: number | null }>( + rest, + 'POST', + rest.constants.routes.GUILD_PRUNE(guildId), + { + days: options.days, + compute_prune_count: options.computePruneCount, + include_roles: options.includeRoles + } + ) + + return result.pruned ?? undefined +} + +/** https://discord.com/developers/docs/resources/guild#begin-guild-prune */ +export interface BeginGuildPrune { + /** Number of days to prune (1 or more), default: 7 */ + days?: number + /** Whether 'pruned' is returned, discouraged for large guilds, default: true */ + computePruneCount?: boolean + /** Role(s) ro include, default: none */ + includeRoles?: string[] +} diff --git a/packages/rest/src/helpers/members/searchMembers.ts b/packages/rest/src/helpers/members/searchMembers.ts new file mode 100644 index 000000000..5fc6a372e --- /dev/null +++ b/packages/rest/src/helpers/members/searchMembers.ts @@ -0,0 +1,51 @@ +import type { DiscordMemberWithUser, SearchMembers } from '@discordeno/types' + +import { BigString } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import { RestManager } from '../../restManager.js' +import { Member } from '../../transformers/member.js' + +/** + * Gets the list of members whose usernames or nicknames start with a provided string. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to search in. + * @param query - The string to match usernames or nicknames against. + * @param options - The parameters for searching through the members. + * @returns A collection of {@link Member} objects assorted by user ID. + * + * @see {@link https://discord.com/developers/docs/resources/guild#search-guild-members} + */ +export async function searchMembers ( + rest: RestManager, + guildId: BigString, + query: string, + options?: Omit +): Promise> { + if (options?.limit) { + if (options.limit < 1) { throw new Error(rest.constants.Errors.MEMBER_SEARCH_LIMIT_TOO_LOW) } + if (options.limit > 1000) { + throw new Error(rest.constants.Errors.MEMBER_SEARCH_LIMIT_TOO_HIGH) + } + } + + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_MEMBERS_SEARCH(guildId, query, options) + ) + + const id = rest.transformers.snowflake(guildId) + + return new Collection( + results.map((result) => { + const member = rest.transformers.member( + rest, + result, + id, + rest.transformers.snowflake(result.user.id) + ) + return [member.id, member] + }) + ) +} diff --git a/packages/rest/src/helpers/members/unbanMember.ts b/packages/rest/src/helpers/members/unbanMember.ts new file mode 100644 index 000000000..e7196af4a --- /dev/null +++ b/packages/rest/src/helpers/members/unbanMember.ts @@ -0,0 +1,28 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Unbans a user from a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to unban the user in. + * @param userId - The ID of the user to unban. + * + * @remarks + * Requires the `BAN_MEMBERS` permission. + * + * Fires a _Guild Ban Remove_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#remove-guild-ban} + */ +export async function unbanMember ( + rest: RestManager, + guildId: BigString, + userId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD_BAN(guildId, userId) + ) +} diff --git a/packages/rest/src/helpers/messages/crosspostMessage.ts b/packages/rest/src/helpers/messages/crosspostMessage.ts new file mode 100644 index 000000000..4157f3acf --- /dev/null +++ b/packages/rest/src/helpers/messages/crosspostMessage.ts @@ -0,0 +1,37 @@ +import { BigString, DiscordMessage } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Message } from '../../transformers/message.js' + +export const publishMessage = crosspostMessage + +/** + * Cross-posts a message posted in an announcement channel to subscribed channels. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the announcement channel. + * @param messageId - The ID of the message to cross-post. + * @returns An instance of the cross-posted {@link Message}. + * + * @remarks + * Requires the `SEND_MESSAGES` permission. + * + * If not cross-posting own message: + * - Requires the `MANAGE_MESSAGES` permission. + * + * Fires a _Message Create_ event in the guilds the subscribed channels are in. + * + * @see {@link https://discord.com/developers/docs/resources/channel#crosspost-message} + */ +export async function crosspostMessage ( + rest: RestManager, + channelId: BigString, + messageId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.CHANNEL_MESSAGE_CROSSPOST(channelId, messageId) + ) + + return rest.transformers.message(rest, result) +} diff --git a/packages/rest/src/helpers/messages/deleteMessage.ts b/packages/rest/src/helpers/messages/deleteMessage.ts new file mode 100644 index 000000000..bfea8c0bd --- /dev/null +++ b/packages/rest/src/helpers/messages/deleteMessage.ts @@ -0,0 +1,37 @@ +import { BigString } from '@discordeno/types' +import { delay } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' + +// TODO: Remove `delayMilliseconds` parameter. + +/** + * Deletes a message from a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to delete the message from. + * @param messageId - The ID of the message to delete from the channel. + * + * @remarks + * If not deleting own message: + * - Requires the `MANAGE_MESSAGES` permission. + * + * Fires a _Message Delete_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#delete-message} + */ +export async function deleteMessage ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + reason?: string, + delayMilliseconds = 0 +): Promise { + if (delayMilliseconds) await delay(delayMilliseconds) + + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.CHANNEL_MESSAGE(channelId, messageId), + { reason } + ) +} diff --git a/packages/rest/src/helpers/messages/deleteMessages.ts b/packages/rest/src/helpers/messages/deleteMessages.ts new file mode 100644 index 000000000..713a811cc --- /dev/null +++ b/packages/rest/src/helpers/messages/deleteMessages.ts @@ -0,0 +1,45 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Deletes multiple messages from a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to delete the messages from. + * @param messageIds - The IDs of the messages to delete from the channel. + * + * @remarks + * Requires the `MANAGE_MESSAGES` permission. + * + * ⚠️ Messages older than 2 weeks old cannot be deleted. + * + * Fires a _Message Delete Bulk_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#bulk-delete-messages} + */ +export async function deleteMessages ( + rest: RestManager, + channelId: BigString, + messageIds: BigString[], + reason?: string +): Promise { + if (messageIds.length < 2) { + throw new Error(rest.constants.Errors.DELETE_MESSAGES_MIN) + } + + if (messageIds.length > 100) { + console.warn( + 'This endpoint only accepts a maximum of 100 messages. Using the first 100 message ids provided.' + ) + } + + return await rest.runMethod( + rest, + 'POST', + rest.constants.routes.CHANNEL_BULK_DELETE(channelId), + { + messages: messageIds.slice(0, 100).map((id) => id.toString()), + reason + } + ) +} diff --git a/packages/rest/src/helpers/messages/editMessage.ts b/packages/rest/src/helpers/messages/editMessage.ts new file mode 100644 index 000000000..dd0f17c8f --- /dev/null +++ b/packages/rest/src/helpers/messages/editMessage.ts @@ -0,0 +1,81 @@ +import { + AllowedMentions, + BigString, + DiscordMessage, + FileContent, + MessageComponents +} from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Attachment } from '../../transformers/attachment.js' +import { Embed } from '../../transformers/embed.js' +import { Message } from '../../transformers/message.js' + +/** + * Edits a message. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to edit the message in. + * @param messageId - The IDs of the message to edit. + * @param options - The parameters for the edit of the message. + * @returns An instance of the edited {@link Message}. + * + * @remarks + * If editing another user's message: + * - Requires the `MANAGE_MESSAGES` permission. + * - Only the {@link EditMessage.flags | flags} property of the {@link options} object parameter can be edited. + * + * Fires a _Message Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#edit-message} + */ +export async function editMessage ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + options: EditMessage +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.CHANNEL_MESSAGE(channelId, messageId), + { + content: options.content, + embeds: options.embeds?.map((embed) => + rest.transformers.reverse.embed(rest, embed) + ), + allowed_mentions: options.allowedMentions + ? rest.transformers.reverse.allowedMentions( + rest, + options.allowedMentions + ) + : undefined, + attachments: options.attachments?.map((attachment) => + rest.transformers.reverse.attachment(rest, attachment) + ), + file: options.file, + components: options.components?.map((component) => + rest.transformers.reverse.component(rest, component) + ) + } + ) + + return rest.transformers.message(rest, result) +} + +/** https://discord.com/developers/docs/resources/channel#edit-message-json-params */ +export interface EditMessage { + /** The new message contents (up to 2000 characters) */ + content?: string | null + /** Embedded `rich` content (up to 6000 characters) */ + embeds?: Embed[] | null + /** Edit the flags of the message (only `SUPPRESS_EMBEDS` can currently be set/unset) */ + flags?: 4 | null + /** The contents of the file being sent/edited */ + file?: FileContent | FileContent[] | null + /** Allowed mentions for the message */ + allowedMentions?: AllowedMentions + /** When specified (adding new attachments), attachments which are not provided in this list will be removed. */ + attachments?: Attachment[] + /** The components you would like to have sent in this message */ + components?: MessageComponents +} diff --git a/packages/rest/src/helpers/messages/getMessage.ts b/packages/rest/src/helpers/messages/getMessage.ts new file mode 100644 index 000000000..f9419b902 --- /dev/null +++ b/packages/rest/src/helpers/messages/getMessage.ts @@ -0,0 +1,33 @@ +import { BigString, DiscordMessage } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Message } from '../../transformers/message.js' + +/** + * Gets a message from a channel by the ID of the message. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel from which to get the message. + * @param messageId - The ID of the message to get. + * @returns An instance of {@link Message}. + * + * @remarks + * Requires that the bot user be able to see the contents of the channel in which the message was posted. + * + * If getting a message from a guild channel: + * - Requires the `READ_MESSAGE_HISTORY` permission. + * + * @see {@link https://discord.com/developers/docs/resources/channel#get-channel-message} + */ +export async function getMessage ( + rest: RestManager, + channelId: BigString, + messageId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.CHANNEL_MESSAGE(channelId, messageId) + ) + + return rest.transformers.message(rest, result) +} diff --git a/packages/rest/src/helpers/messages/getMessages.ts b/packages/rest/src/helpers/messages/getMessages.ts new file mode 100644 index 000000000..83e8f821b --- /dev/null +++ b/packages/rest/src/helpers/messages/getMessages.ts @@ -0,0 +1,97 @@ +import { BigString, DiscordMessage } from '@discordeno/types' +import { Collection, hasProperty } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Message } from '../../transformers/message.js' + +/** + * Gets multiple messages from a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel from which to get the messages. + * @param options - The parameters for the fetching of the messages. + * @returns A collection of {@link Message} objects assorted by message ID. + * + * @remarks + * Requires that the bot user be able to see the contents of the channel in which the messages were posted. + * + * If getting a messages from a guild channel: + * - Requires the `READ_MESSAGE_HISTORY` permission. + * + * @see {@link https://discord.com/developers/docs/resources/channel#get-channel-messages} + */ +export async function getMessages ( + rest: RestManager, + channelId: BigString, + options?: GetMessagesOptions +): Promise> { + if (options?.limit && (options.limit < 0 || options.limit > 100)) { + throw new Error(rest.constants.Errors.INVALID_GET_MESSAGES_LIMIT) + } + + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.CHANNEL_MESSAGES(channelId, options) + ) + + return new Collection( + results.map((result) => { + const message = rest.transformers.message(rest, result) + return [message.id, message] + }) + ) +} + +/** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */ +export interface GetMessagesLimit { + /** Max number of messages to return (1-100) default 50 */ + limit?: number +} + +/** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */ +export interface GetMessagesAround extends GetMessagesLimit { + /** Get messages around this message id */ + around?: BigString +} + +/** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */ +export interface GetMessagesBefore extends GetMessagesLimit { + /** Get messages before this message id */ + before?: BigString +} + +/** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */ +export interface GetMessagesAfter extends GetMessagesLimit { + /** Get messages after this message id */ + after?: BigString +} + +export type GetMessagesOptions = + | GetMessagesAfter + | GetMessagesBefore + | GetMessagesAround + | GetMessagesLimit + +export function isGetMessagesAfter ( + options: GetMessagesOptions +): options is GetMessagesAfter { + return hasProperty(options, 'after') +} + +export function isGetMessagesBefore ( + options: GetMessagesOptions +): options is GetMessagesBefore { + return hasProperty(options, 'before') +} + +export function isGetMessagesAround ( + options: GetMessagesOptions +): options is GetMessagesAround { + return hasProperty(options, 'around') +} + +export function isGetMessagesLimit ( + options: GetMessagesOptions +): options is GetMessagesLimit { + return hasProperty(options, 'limit') +} diff --git a/packages/rest/src/helpers/messages/getPinnedMessages.ts b/packages/rest/src/helpers/messages/getPinnedMessages.ts new file mode 100644 index 000000000..c3cdc0aa1 --- /dev/null +++ b/packages/rest/src/helpers/messages/getPinnedMessages.ts @@ -0,0 +1,37 @@ +import { BigString, DiscordMessage } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Message } from '../../transformers/message.js' + +/** + * Gets the pinned messages for a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel to get the pinned messages for. + * @returns A collection of {@link Message} objects assorted by message ID. + * + * @remarks + * Requires that the bot user be able to see the contents of the channel in which the messages were posted. + * + * If getting a message from a guild channel: + * - Requires the `READ_MESSAGE_HISTORY` permission. + * + * @see {@link https://discord.com/developers/docs/resources/channel#get-pinned-messages} + */ +export async function getPinnedMessages ( + rest: RestManager, + channelId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.CHANNEL_PINS(channelId) + ) + + return new Collection( + results.map((result) => { + const message = rest.transformers.message(rest, result) + return [message.id, message] + }) + ) +} diff --git a/packages/rest/src/helpers/messages/index.ts b/packages/rest/src/helpers/messages/index.ts new file mode 100644 index 000000000..e6c2b2459 --- /dev/null +++ b/packages/rest/src/helpers/messages/index.ts @@ -0,0 +1,11 @@ +export * from './crosspostMessage.js' +export * from './deleteMessage.js' +export * from './deleteMessages.js' +export * from './editMessage.js' +export * from './getMessage.js' +export * from './getMessages.js' +export * from './getPinnedMessages.js' +export * from './pinMessage.js' +export * from './reactions/index.js' +export * from './sendMessage.js' +export * from './unpinMessage.js' diff --git a/packages/rest/src/helpers/messages/pinMessage.ts b/packages/rest/src/helpers/messages/pinMessage.ts new file mode 100644 index 000000000..ba6eb7733 --- /dev/null +++ b/packages/rest/src/helpers/messages/pinMessage.ts @@ -0,0 +1,34 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Pins a message in a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel where the message is to be pinned. + * @param messageId - The ID of the message to pin. + * + * @remarks + * Requires that the bot user be able to see the contents of the channel in which the messages were posted. + * + * Requires the `MANAGE_MESSAGES` permission. + * + * ⚠️ There can only be at max 50 messages pinned in a channel. + * + * Fires a _Channel Pins Update_ event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#pin-message} + */ +export async function pinMessage ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.CHANNEL_PIN(channelId, messageId), + reason ? { reason } : undefined + ) +} diff --git a/packages/rest/src/helpers/messages/reactions/addReaction.ts b/packages/rest/src/helpers/messages/reactions/addReaction.ts new file mode 100644 index 000000000..873fdacd0 --- /dev/null +++ b/packages/rest/src/helpers/messages/reactions/addReaction.ts @@ -0,0 +1,43 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { processReactionString } from './getReactions.js' + +// TODO: Improve typing of the `reaction` parameter. + +/** + * Adds a reaction to a message. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel the message to add a reaction to is in. + * @param messageId - The ID of the message to add a reaction to. + * @param reaction - The reaction to add to the message. + * @returns + * + * @remarks + * Requires the `READ_MESSAGE_HISTORY` permission. + * + * If nobody else has reacted to the message: + * - Requires the `ADD_REACTIONS` permission. + * + * Fires a _Message Reaction Add_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#create-reaction} + */ +export async function addReaction ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + reaction: string +): Promise { + reaction = processReactionString(reaction) + + return await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.CHANNEL_MESSAGE_REACTION_ME( + channelId, + messageId, + reaction + ) + ) +} diff --git a/packages/rest/src/helpers/messages/reactions/addReactions.ts b/packages/rest/src/helpers/messages/reactions/addReactions.ts new file mode 100644 index 000000000..7c0fb51ea --- /dev/null +++ b/packages/rest/src/helpers/messages/reactions/addReactions.ts @@ -0,0 +1,45 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +// TODO: Improve typing of the `reactions` parameter. + +/** + * Adds multiple a reaction to a message. + * + * This function uses the `addReaction()` helper behind the scenes. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel the message to add reactions to is in. + * @param messageId - The ID of the message to add the reactions to. + * @param reactions - The reactions to add to the message. + * @param ordered - Whether the reactions must be added in order or not. + * + * @remarks + * Requires the `READ_MESSAGE_HISTORY` permission. + * + * If nobody else has reacted to the message: + * - Requires the `ADD_REACTIONS` permission. + * + * Fires a _Message Reaction Add_ gateway event for every reaction added. + */ +export async function addReactions ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + reactions: string[], + ordered = false +): Promise { + if (!ordered) { + await Promise.all( + reactions.map( + async (reaction) => + await rest.helpers.addReaction(channelId, messageId, reaction) + ) + ) + return + } + + for (const reaction of reactions) { + await rest.helpers.addReaction(channelId, messageId, reaction) + } +} diff --git a/packages/rest/src/helpers/messages/reactions/deleteReaction.ts b/packages/rest/src/helpers/messages/reactions/deleteReaction.ts new file mode 100644 index 000000000..dc39167ff --- /dev/null +++ b/packages/rest/src/helpers/messages/reactions/deleteReaction.ts @@ -0,0 +1,78 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { processReactionString } from './getReactions.js' + +// TODO: Improve typing of the `reaction` parameter. + +/** + * Deletes a reaction added by the bot user from a message. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel the message to delete the reaction from is in. + * @param messageId - The ID of the message to delete the reaction from. + * @param reaction - The reaction to delete from the message. + * + * @remarks + * Requires the `READ_MESSAGE_HISTORY` permission. + * + * Fires a _Message Reaction Remove_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#delete-own-reaction} + */ +export async function deleteOwnReaction ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + reaction: string +): Promise { + reaction = processReactionString(reaction) + + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.CHANNEL_MESSAGE_REACTION_ME( + channelId, + messageId, + reaction + ) + ) +} + +/** + * Deletes a user's reaction from a message. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel the message to delete the reaction from is in. + * @param messageId - The ID of the message to delete the reaction from. + * @param userId - The ID of the user whose reaction to delete. + * @param reaction - The reaction to delete from the message. + * + * @remarks + * Requires the `READ_MESSAGE_HISTORY` permission. + * + * Requires the `MANAGE_MESSAGES` permission. + * + * Fires a _Message Reaction Remove_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#delete-user-reaction} + */ +export async function deleteUserReaction ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + userId: BigString, + reaction: string +): Promise { + reaction = processReactionString(reaction) + + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.CHANNEL_MESSAGE_REACTION_USER( + channelId, + messageId, + reaction, + userId + ) + ) +} diff --git a/packages/rest/src/helpers/messages/reactions/deleteReactionsAll.ts b/packages/rest/src/helpers/messages/reactions/deleteReactionsAll.ts new file mode 100644 index 000000000..3fdad8fe3 --- /dev/null +++ b/packages/rest/src/helpers/messages/reactions/deleteReactionsAll.ts @@ -0,0 +1,30 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' + +/** + * Deletes all reactions for all emojis from a message. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel the message to delete the reactions from is in. + * @param messageId - The ID of the message to delete the reactions from. + * + * @remarks + * Requires the `READ_MESSAGE_HISTORY` permission. + * + * Requires the `MANAGE_MESSAGES` permission. + * + * Fires a _Message Reaction Remove All_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#delete-all-reactions} + */ +export async function deleteReactionsAll ( + rest: RestManager, + channelId: BigString, + messageId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.CHANNEL_MESSAGE_REACTIONS(channelId, messageId) + ) +} diff --git a/packages/rest/src/helpers/messages/reactions/deleteReactionsEmoji.ts b/packages/rest/src/helpers/messages/reactions/deleteReactionsEmoji.ts new file mode 100644 index 000000000..fbf7ee042 --- /dev/null +++ b/packages/rest/src/helpers/messages/reactions/deleteReactionsEmoji.ts @@ -0,0 +1,39 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../../restManager.js' +import { processReactionString } from './getReactions.js' + +/** + * Deletes all reactions for an emoji from a message. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel the message to delete the reactions from is in. + * @param messageId - The ID of the message to delete the reactions from. + * @param reaction - The reaction to remove from the message. + * + * @remarks + * Requires the `READ_MESSAGE_HISTORY` permission. + * + * Requires the `MANAGE_MESSAGES` permission. + * + * Fires a _Message Reaction Remove Emoji_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji} + */ +export async function deleteReactionsEmoji ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + reaction: string +): Promise { + reaction = processReactionString(reaction) + + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.CHANNEL_MESSAGE_REACTION( + channelId, + messageId, + reaction + ) + ) +} diff --git a/packages/rest/src/helpers/messages/reactions/getReactions.ts b/packages/rest/src/helpers/messages/reactions/getReactions.ts new file mode 100644 index 000000000..dd77a6093 --- /dev/null +++ b/packages/rest/src/helpers/messages/reactions/getReactions.ts @@ -0,0 +1,65 @@ +import { BigString, DiscordUser } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../../restManager.js' +import { User } from '../../../transformers/member.js' + +/** Get a list of users that reacted with this emoji. */ +/** + * Gets the list of users that reacted with an emoji to a message. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel the message to get the users for is in. + * @param messageId - The ID of the message to get the users for. + * @param reaction - The reaction for which to get the users. + * @param options - The parameters for the fetching of the users. + * @returns A collection of {@link User} objects assorted by user ID. + * + * @see {@link https://discord.com/developers/docs/resources/channel#get-reactions} + */ +export async function getReactions ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + reaction: string, + options?: GetReactions +): Promise> { + reaction = processReactionString(reaction) + + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.CHANNEL_MESSAGE_REACTION( + channelId, + messageId, + reaction, + options + ) + ) + + return new Collection( + results.map((result) => { + const user = rest.transformers.user(rest, result) + return [user.id, user] + }) + ) +} + +export function processReactionString (reaction: string): string { + if (reaction.startsWith('<:')) { + return reaction.substring(2, reaction.length - 1) + } + + if (reaction.startsWith(' { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.CHANNEL_MESSAGES(channelId), + { + content: options.content, + nonce: options.nonce, + tts: options.tts, + embeds: options.embeds?.map((embed) => + rest.transformers.reverse.embed(rest, embed) + ), + allowed_mentions: options.allowedMentions + ? { + parse: options.allowedMentions?.parse, + roles: options.allowedMentions?.roles?.map((id) => id.toString()), + users: options.allowedMentions?.users?.map((id) => id.toString()), + replied_user: options.allowedMentions?.repliedUser + } + : undefined, + file: options.file, + components: options.components?.map((component) => ({ + type: component.type, + components: component.components.map((subComponent) => { + if (subComponent.type === MessageComponentTypes.InputText) { + return { + type: subComponent.type, + style: subComponent.style, + custom_id: subComponent.customId, + label: subComponent.label, + placeholder: subComponent.placeholder, + min_length: + subComponent.minLength ?? subComponent.required === false + ? 0 + : subComponent.minLength, + max_length: subComponent.maxLength + } + } + + if (subComponent.type === MessageComponentTypes.SelectMenu) { + return { + type: subComponent.type, + custom_id: subComponent.customId, + placeholder: subComponent.placeholder, + min_values: subComponent.minValues, + max_values: subComponent.maxValues, + disabled: + 'disabled' in subComponent ? subComponent.disabled : undefined, + options: subComponent.options.map((option) => ({ + label: option.label, + value: option.value, + description: option.description, + emoji: option.emoji + ? { + id: option.emoji.id?.toString(), + name: option.emoji.name, + animated: option.emoji.animated + } + : undefined, + default: option.default + })) + } + } + + if ( + subComponent.type === MessageComponentTypes.SelectMenuChannels || + subComponent.type === MessageComponentTypes.SelectMenuRoles || + subComponent.type === MessageComponentTypes.SelectMenuUsers || + subComponent.type === MessageComponentTypes.SelectMenuUsersAndRoles + ) { + return { + type: subComponent.type, + custom_id: subComponent.customId, + placeholder: subComponent.placeholder, + min_values: subComponent.minValues, + max_values: subComponent.maxValues, + disabled: + 'disabled' in subComponent ? subComponent.disabled : undefined + } + } + + return { + type: subComponent.type, + custom_id: subComponent.customId, + label: subComponent.label, + style: subComponent.style, + emoji: + 'emoji' in subComponent && subComponent.emoji + ? { + id: subComponent.emoji.id?.toString(), + name: subComponent.emoji.name, + animated: subComponent.emoji.animated + } + : undefined, + url: 'url' in subComponent ? subComponent.url : undefined, + disabled: + 'disabled' in subComponent ? subComponent.disabled : undefined + } + }) + })), + ...(options.messageReference?.messageId + ? { + message_reference: { + message_id: options.messageReference.messageId.toString(), + channel_id: options.messageReference.channelId?.toString(), + guild_id: options.messageReference.guildId?.toString(), + fail_if_not_exists: options.messageReference.failIfNotExists + } + } + : {}), + sticker_ids: options.stickerIds?.map((sticker) => sticker.toString()) + } + ) + + return rest.transformers.message(rest, result) +} + +export interface CreateMessage { + /** The message contents (up to 2000 characters) */ + content?: string + /** Can be used to verify a message was sent (up to 25 characters). Value will appear in the Message Create event. */ + nonce?: string | number + /** true if this is a TTS message */ + tts?: boolean + /** Embedded `rich` content (up to 6000 characters) */ + embeds?: Embed[] + /** Allowed mentions for the message */ + allowedMentions?: AllowedMentions + /** Include to make your message a reply */ + messageReference?: { + /** id of the originating message */ + messageId?: BigString + /** + * id of the originating message's channel + * Note: `channel_id` is optional when creating a reply, but will always be present when receiving an event/response that includes this data model. + */ + channelId?: BigString + /** id of the originating message's guild */ + guildId?: BigString + /** When sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */ + failIfNotExists: boolean + } + /** The contents of the file being sent */ + file?: FileContent | FileContent[] + /** The components you would like to have sent in this message */ + components?: MessageComponents + /** IDs of up to 3 stickers in the server to send in the message */ + stickerIds?: [bigint] | [bigint, bigint] | [bigint, bigint, bigint] +} diff --git a/packages/rest/src/helpers/messages/unpinMessage.ts b/packages/rest/src/helpers/messages/unpinMessage.ts new file mode 100644 index 000000000..c651f4ed7 --- /dev/null +++ b/packages/rest/src/helpers/messages/unpinMessage.ts @@ -0,0 +1,32 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Unpins a pinned message in a channel. + * + * @param bot - The bot instance to use to make the request. + * @param channelId - The ID of the channel where the message is pinned. + * @param messageId - The ID of the message to unpin. + * + * @remarks + * Requires that the bot user be able to see the contents of the channel in which the messages were posted. + * + * Requires the `MANAGE_MESSAGES` permission. + * + * Fires a _Channel Pins Update_ event. + * + * @see {@link https://discord.com/developers/docs/resources/channel#unpin-message} + */ +export async function unpinMessage ( + rest: RestManager, + channelId: BigString, + messageId: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.CHANNEL_PIN(channelId, messageId), + reason ? { reason } : undefined + ) +} diff --git a/packages/rest/src/helpers/misc/editBotProfile.ts b/packages/rest/src/helpers/misc/editBotProfile.ts new file mode 100644 index 000000000..dd11658dd --- /dev/null +++ b/packages/rest/src/helpers/misc/editBotProfile.ts @@ -0,0 +1,29 @@ +import { DiscordUser } from '@discordeno/types' +import { urlToBase64 } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { User } from '../../transformers/member.js' + +/** + * Modifies the bot's username or avatar. + * NOTE: username: if changed may cause the bot's discriminator to be randomized. + */ +export async function editBotProfile ( + rest: RestManager, + options: { username?: string, botAvatarURL?: string | null } +): Promise { + const avatar = options?.botAvatarURL + ? await urlToBase64(options?.botAvatarURL) + : options?.botAvatarURL + + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.USER_BOT(), + { + username: options.username?.trim(), + avatar + } + ) + + return rest.transformers.user(rest, result) +} diff --git a/packages/rest/src/helpers/misc/getApplicationInfo.ts b/packages/rest/src/helpers/misc/getApplicationInfo.ts new file mode 100644 index 000000000..24b959188 --- /dev/null +++ b/packages/rest/src/helpers/misc/getApplicationInfo.ts @@ -0,0 +1,16 @@ +import { DiscordApplication } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { Application } from '../../transformers/application.js' + +/** Get the applications info */ +export async function getApplicationInfo ( + rest: RestManager +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.OAUTH2_APPLICATION() + ) + + return rest.transformers.application(rest, result) +} diff --git a/packages/rest/src/helpers/misc/getGatewayBot.ts b/packages/rest/src/helpers/misc/getGatewayBot.ts new file mode 100644 index 000000000..225b96793 --- /dev/null +++ b/packages/rest/src/helpers/misc/getGatewayBot.ts @@ -0,0 +1,13 @@ +import { DiscordGetGatewayBot, GetGatewayBot } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** Get the bots Gateway metadata that can help during the operation of large or sharded bots. */ +export async function getGatewayBot (rest: RestManager): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GATEWAY_BOT() + ) + + return rest.transformers.gatewayBot(result) +} diff --git a/packages/rest/src/helpers/misc/getNitroStickerPacks.ts b/packages/rest/src/helpers/misc/getNitroStickerPacks.ts new file mode 100644 index 000000000..a89111caf --- /dev/null +++ b/packages/rest/src/helpers/misc/getNitroStickerPacks.ts @@ -0,0 +1,29 @@ +import { DiscordStickerPack } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import { RestManager } from '../../restManager.js' +import { StickerPack } from '../../transformers/sticker.js' + +/** + * Returns the list of sticker packs available to Nitro subscribers. + * + * @param bot The bot instance to use to make the request. + * @returns A collection of {@link StickerPack} objects assorted by sticker ID. + * + * @see {@link https://discord.com/developers/docs/resources/sticker#list-nitro-sticker-packs} + */ +export async function getNitroStickerPacks ( + rest: RestManager +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.NITRO_STICKER_PACKS() + ) + + return new Collection( + results.map((result) => { + const pack = rest.transformers.stickerPack(rest, result) + return [pack.id, pack] + }) + ) +} diff --git a/packages/rest/src/helpers/misc/getUser.ts b/packages/rest/src/helpers/misc/getUser.ts new file mode 100644 index 000000000..d1b13227e --- /dev/null +++ b/packages/rest/src/helpers/misc/getUser.ts @@ -0,0 +1,17 @@ +import { BigString, DiscordUser } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' +import { User } from '../../transformers/member.js' + +/** This function will return the raw user payload in the rare cases you need to fetch a user directly from the API. */ +export async function getUser ( + rest: RestManager, + userId: BigString +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.USER(userId) + ) + + return rest.transformers.user(rest, result) +} diff --git a/packages/rest/src/helpers/misc/index.ts b/packages/rest/src/helpers/misc/index.ts new file mode 100644 index 000000000..f7ba30124 --- /dev/null +++ b/packages/rest/src/helpers/misc/index.ts @@ -0,0 +1,5 @@ +export * from './editBotProfile.js' +export * from './getApplicationInfo.js' +export * from './getGatewayBot.js' +export * from './getNitroStickerPacks.js' +export * from './getUser.js' diff --git a/packages/rest/src/helpers/roles/addRole.ts b/packages/rest/src/helpers/roles/addRole.ts new file mode 100644 index 000000000..0896a7372 --- /dev/null +++ b/packages/rest/src/helpers/roles/addRole.ts @@ -0,0 +1,32 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Adds a role to a member. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild the member to add the role to is in. + * @param userId - The user ID of the member to add the role to. + * @param roleId - The ID of the role to add to the member. + * + * @remarks + * Requires the `MANAGE_ROLES` permission. + * + * Fires a _Guild Member Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#add-guild-member-role} + */ +export async function addRole ( + rest: RestManager, + guildId: BigString, + userId: BigString, + roleId: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'PUT', + rest.constants.routes.GUILD_MEMBER_ROLE(guildId, userId, roleId), + { reason } + ) +} diff --git a/packages/rest/src/helpers/roles/createRole.ts b/packages/rest/src/helpers/roles/createRole.ts new file mode 100644 index 000000000..907cf70ab --- /dev/null +++ b/packages/rest/src/helpers/roles/createRole.ts @@ -0,0 +1,64 @@ +import { BigString, DiscordRole, PermissionStrings } from '@discordeno/types' +import { calculateBits } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Role } from '../../transformers/role.js' + +/** + * Creates a role in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to create the role in. + * @param options - The parameters for the creation of the role. + * @returns An instance of the created {@link Role}. + * + * @remarks + * Requires the `MANAGE_ROLES` permission. + * + * Fires a _Guild Role Create_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#create-guild-role} + */ +export async function createRole ( + rest: RestManager, + guildId: BigString, + options: CreateGuildRole, + reason?: string +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.GUILD_ROLES(guildId), + { + name: options.name, + color: options.color, + hoist: options.hoist, + mentionable: options.mentionable, + permissions: calculateBits(options?.permissions ?? []), + icon: options.icon, + unicode_emoji: options.unicodeEmoji, + reason + } + ) + + return rest.transformers.role(rest, { + role: result, + guildId: rest.transformers.snowflake(guildId) + }) +} + +export interface CreateGuildRole { + /** Name of the role, max 100 characters, default: "new role" */ + name?: string + /** Bitwise value of the enabled/disabled permissions, default: everyone permissions in guild */ + permissions?: PermissionStrings[] + /** RGB color value, default: 0 */ + color?: number + /** Whether the role should be displayed separately in the sidebar, default: false */ + hoist?: boolean + /** Whether the role should be mentionable, default: false */ + mentionable?: boolean + /** The role's unicode emoji (if the guild has the `ROLE_ICONS` feature) */ + unicodeEmoji?: string + /** the role's icon image (if the guild has the `ROLE_ICONS` feature) */ + icon?: string +} diff --git a/packages/rest/src/helpers/roles/deleteRole.ts b/packages/rest/src/helpers/roles/deleteRole.ts new file mode 100644 index 000000000..3c4a06081 --- /dev/null +++ b/packages/rest/src/helpers/roles/deleteRole.ts @@ -0,0 +1,28 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Deletes a role from a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to delete the role from. + * @param roleId - The ID of the role to delete. + * + * @remarks + * Requires the `MANAGE_ROLES` permission. + * + * Fires a _Guild Role Delete_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#delete-guild-role} + */ +export async function deleteRole ( + rest: RestManager, + guildId: BigString, + roleId: BigString +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD_ROLE(guildId, roleId) + ) +} diff --git a/packages/rest/src/helpers/roles/editRole.ts b/packages/rest/src/helpers/roles/editRole.ts new file mode 100644 index 000000000..3aea493f7 --- /dev/null +++ b/packages/rest/src/helpers/roles/editRole.ts @@ -0,0 +1,64 @@ +import { BigString, DiscordRole, PermissionStrings } from '@discordeno/types' +import { calculateBits } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Role } from '../../transformers/role.js' + +/** + * Edits a role in a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to edit the role in. + * @param roleId - The ID of the role to edit. + * @param options - The parameters for the edit of the role. + * @returns An instance of the edited {@link Role}. + * + * @remarks + * Requires the `MANAGE_ROLES` permission. + * + * Fires a _Guild Role Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-guild-role} + */ +export async function editRole ( + rest: RestManager, + guildId: BigString, + roleId: BigString, + options: EditGuildRole +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD_ROLE(guildId, roleId), + { + name: options.name, + color: options.color, + hoist: options.hoist, + mentionable: options.mentionable, + permissions: calculateBits(options?.permissions ?? []), + icon: options.icon, + unicode_emoji: options.unicodeEmoji + } + ) + + return rest.transformers.role(rest, { + role: result, + guildId: rest.transformers.snowflake(guildId) + }) +} + +export interface EditGuildRole { + /** Name of the role, max 100 characters, default: "new role" */ + name?: string + /** Bitwise value of the enabled/disabled permissions, default: everyone permissions in guild */ + permissions?: PermissionStrings[] + /** RGB color value, default: 0 */ + color?: number + /** Whether the role should be displayed separately in the sidebar, default: false */ + hoist?: boolean + /** Whether the role should be mentionable, default: false */ + mentionable?: boolean + /** The role's unicode emoji (if the guild has the `ROLE_ICONS` feature) */ + unicodeEmoji?: string + /** the role's icon image (if the guild has the `ROLE_ICONS` feature) */ + icon?: string +} diff --git a/packages/rest/src/helpers/roles/editRolePositions.ts b/packages/rest/src/helpers/roles/editRolePositions.ts new file mode 100644 index 000000000..d426fc229 --- /dev/null +++ b/packages/rest/src/helpers/roles/editRolePositions.ts @@ -0,0 +1,48 @@ +import { BigString, DiscordRole } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import { RestManager } from '../../restManager.js' +import { Role } from '../../transformers/role.js' + +/** + * Edits the positions of a set of roles. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to edit the role positions in. + * @param options - The parameters for the edit of the role positions. + * @returns A collection of {@link Role} objects assorted by role ID. + * + * @remarks + * Requires the `MANAGE_ROLES` permission. + * + * Fires a _Guild Role Update_ gateway event for every role impacted in this change. + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-guild-role-positions} + */ +export async function modifyRolePositions ( + rest: RestManager, + guildId: BigString, + options: ModifyRolePositions[] +): Promise> { + const results = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD_ROLES(guildId), + options + ) + + const id = rest.transformers.snowflake(guildId) + + return new Collection( + results.map((result) => { + const role = rest.transformers.role(rest, { role: result, guildId: id }) + return [role.id, role] + }) + ) +} + +export interface ModifyRolePositions { + /** The role id */ + id: BigString + /** The sorting position for the role. */ + position?: number | null +} diff --git a/packages/rest/src/helpers/roles/getRoles.ts b/packages/rest/src/helpers/roles/getRoles.ts new file mode 100644 index 000000000..fb9fa966c --- /dev/null +++ b/packages/rest/src/helpers/roles/getRoles.ts @@ -0,0 +1,35 @@ +import { BigString, DiscordRole } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type { RestManager } from '../../restManager.js' +import { Role } from '../../transformers/role.js' + +/** + * Gets the list of roles for a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to get the list of roles for. + * @returns A collection of {@link Role} objects assorted by role ID. + * + * @remarks + * ⚠️ This endpoint should be used sparingly due to {@link User} objects already being included in guild payloads. + * + * @see {@link https://discord.com/developers/docs/resources/guild#get-guild-roles} + */ +export async function getRoles ( + rest: RestManager, + guildId: BigString +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_ROLES(guildId) + ) + const id = rest.transformers.snowflake(guildId) + + return new Collection( + results.map((result) => { + const role = rest.transformers.role(rest, { role: result, guildId: id }) + return [role.id, role] + }) + ) +} diff --git a/packages/rest/src/helpers/roles/index.ts b/packages/rest/src/helpers/roles/index.ts new file mode 100644 index 000000000..2c10976d6 --- /dev/null +++ b/packages/rest/src/helpers/roles/index.ts @@ -0,0 +1,7 @@ +export * from './addRole.js' +export * from './createRole.js' +export * from './deleteRole.js' +export * from './editRole.js' +export * from './editRolePositions.js' +export * from './getRoles.js' +export * from './removeRole.js' diff --git a/packages/rest/src/helpers/roles/removeRole.ts b/packages/rest/src/helpers/roles/removeRole.ts new file mode 100644 index 000000000..dddc7c7a8 --- /dev/null +++ b/packages/rest/src/helpers/roles/removeRole.ts @@ -0,0 +1,32 @@ +import { BigString } from '@discordeno/types' +import type { RestManager } from '../../restManager.js' + +/** + * Removes a role from a member. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild the member to remove the role from is in. + * @param userId - The user ID of the member to remove the role from. + * @param roleId - The ID of the role to remove from the member. + * + * @remarks + * Requires the `MANAGE_ROLES` permission. + * + * Fires a _Guild Member Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild#remove-guild-member-role} + */ +export async function removeRole ( + rest: RestManager, + guildId: BigString, + userId: BigString, + roleId: BigString, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD_MEMBER_ROLE(guildId, userId, roleId), + { reason } + ) +} diff --git a/packages/rest/src/helpers/stickers/createGuildSticker.ts b/packages/rest/src/helpers/stickers/createGuildSticker.ts new file mode 100644 index 000000000..77e19cde6 --- /dev/null +++ b/packages/rest/src/helpers/stickers/createGuildSticker.ts @@ -0,0 +1,49 @@ +import { FileContent, WithReason } from '@discordeno/types' +import { RestManager } from '../../restManager.js' +import { Sticker } from '../../transformers/sticker.js' + +/** + * Create a new sticker for the guild. + * + * @param bot The bot instance to use to make the request. + * @param guildId The ID of the guild to get + * @return A {@link Sticker} + * + * @remarks + * Requires the `MANAGE_EMOJIS_AND_STICKERS` permission. + * Fires a Guild Stickers Update Gateway event. + * Every guilds has five free sticker slots by default, and each Boost level will grant access to more slots. + * Lottie stickers can only be uploaded on guilds that have either the `VERIFIED` and/or the `PARTNERED` guild feature. + * + * @see {@link https://discord.com/developers/docs/resources/sticker#create-guild-sticker} + */ +export async function createGuildSticker ( + rest: RestManager, + guildId: bigint, + options: CreateGuildStickerOptions +): Promise { + const result = await rest.runMethod( + rest, + 'POST', + rest.constants.routes.GUILD_STICKERS(guildId), + { + name: options.name, + description: options.description, + tags: options.tags, + file: options.file, + reason: options.reason + } + ) + return rest.transformers.sticker(rest, result) +} + +export interface CreateGuildStickerOptions extends WithReason { + /** Name of the sticker (2-30 characters) */ + name: string + /** Description of the sticker (empty or 2-100 characters) */ + description: string + /** Autocomplete/suggestion tags for the sticker (max 200 characters) */ + tags: string + /** The sticker file to upload, must be a PNG, APNG, or Lottie JSON file, max 500 KB */ + file: FileContent +} diff --git a/packages/rest/src/helpers/stickers/deleteGuildSticker.ts b/packages/rest/src/helpers/stickers/deleteGuildSticker.ts new file mode 100644 index 000000000..172e8c679 --- /dev/null +++ b/packages/rest/src/helpers/stickers/deleteGuildSticker.ts @@ -0,0 +1,32 @@ +import { RestManager } from '../../restManager.js' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Sticker } from '../../transformers/sticker.js' + +/** + * Delete a new sticker for the guild. + * + * @param bot The bot instance to use to make the request. + * @param guildId The ID of the guild to get + * @return A {@link Sticker} + * + * @remarks + * Requires the `MANAGE_EMOJIS_AND_STICKERS` permission. + * Fires a Guild Stickers Update Gateway event. + * Every guilds has five free sticker slots by default, and each Boost level will grant access to more slots. + * Lottie stickers can only be uploaded on guilds that have either the `VERIFIED` and/or the `PARTNERED` guild feature. + * + * @see {@link https://discord.com/developers/docs/resources/sticker#delete-guild-sticker} + */ +export async function deleteGuildSticker ( + rest: RestManager, + guildId: bigint, + stickerId: bigint, + reason?: string +): Promise { + return await rest.runMethod( + rest, + 'DELETE', + rest.constants.routes.GUILD_STICKER(guildId, stickerId), + reason ? { reason } : undefined + ) +} diff --git a/packages/rest/src/helpers/stickers/editGuildSticker.ts b/packages/rest/src/helpers/stickers/editGuildSticker.ts new file mode 100644 index 000000000..d8bf0e3d8 --- /dev/null +++ b/packages/rest/src/helpers/stickers/editGuildSticker.ts @@ -0,0 +1,45 @@ +import { AtLeastOne, WithReason } from '@discordeno/types' +import { RestManager } from '../../restManager.js' +import { Sticker } from '../../transformers/sticker.js' + +/** + * Edit the given sticker. + * + * @param bot The bot instance to use to make the request. + * @param guildId The ID of the guild to get + * @return A {@link Sticker} + * + * @remarks + * Requires the `MANAGE_EMOJIS_AND_STICKERS` permission. + * Fires a Guild Stickers Update Gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/sticker#modify-guild-sticker} + */ +export async function editGuildSticker ( + rest: RestManager, + guildId: bigint, + stickerId: bigint, + options: AtLeastOne +): Promise { + const result = await rest.runMethod( + rest, + 'PATCH', + rest.constants.routes.GUILD_STICKER(guildId, stickerId), + { + name: options.name, + description: options.description, + tags: options.tags, + reason: options.reason + } + ) + return rest.transformers.sticker(rest, result) +} + +export interface EditGuildStickerOptions extends WithReason { + /** Name of the sticker (2-30 characters) */ + name?: string + /** Description of the sticker (empty or 2-100 characters) */ + description?: string | null + /** Autocomplete/suggestion tags for the sticker (max 200 characters) */ + tags?: string +} diff --git a/packages/rest/src/helpers/stickers/getGuildSticker.ts b/packages/rest/src/helpers/stickers/getGuildSticker.ts new file mode 100644 index 000000000..44d82b56a --- /dev/null +++ b/packages/rest/src/helpers/stickers/getGuildSticker.ts @@ -0,0 +1,27 @@ +import { RestManager } from '../../index.js' +import { Sticker } from '../../transformers/sticker.js' + +/** + * Returns a sticker object for the given guild and sticker IDs. + * + * @param bot The bot instance to use to make the request. + * @param guildId The ID of the guild to get + * @param stickerId The ID of the sticker to get + * @return A {@link Sticker} + * + * @remarks Includes the user field if the bot has the `MANAGE_EMOJIS_AND_STICKERS` permission. + * + * @see {@link https://discord.com/developers/docs/resources/sticker#get-guild-sticker} + */ +export async function getGuildSticker ( + rest: RestManager, + guildId: bigint, + stickerId: bigint +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_STICKER(guildId, stickerId) + ) + return rest.transformers.sticker(rest, result) +} diff --git a/packages/rest/src/helpers/stickers/getGuildStickers.ts b/packages/rest/src/helpers/stickers/getGuildStickers.ts new file mode 100644 index 000000000..eec23a501 --- /dev/null +++ b/packages/rest/src/helpers/stickers/getGuildStickers.ts @@ -0,0 +1,33 @@ +import { DiscordSticker } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import { RestManager } from '../../restManager.js' +import { Sticker } from '../../transformers/sticker.js' + +/** + * Returns an array of sticker objects for the given guild. + * + * @param bot The bot instance to use to make the request. + * @param guildId The ID of the guild to get + * @returns A collection of {@link Sticker} objects assorted by sticker ID. + * + * @remarks Includes user fields if the bot has the `MANAGE_EMOJIS_AND_STICKERS` permission. + * + * @see {@link https://discord.com/developers/docs/resources/sticker#list-guild-stickers} + */ +export async function getGuildStickers ( + rest: RestManager, + guildId: bigint +): Promise> { + const results = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.GUILD_STICKERS(guildId) + ) + + return new Collection( + results.map((result) => { + const pack = rest.transformers.sticker(rest, result) + return [pack.id, pack] + }) + ) +} diff --git a/packages/rest/src/helpers/stickers/getSticker.ts b/packages/rest/src/helpers/stickers/getSticker.ts new file mode 100644 index 000000000..c13552620 --- /dev/null +++ b/packages/rest/src/helpers/stickers/getSticker.ts @@ -0,0 +1,25 @@ +import { DiscordSticker } from '@discordeno/types' +import { RestManager } from '../../restManager.js' +import { Sticker } from '../../transformers/sticker.js' + +/** + * Returns a sticker object for the given sticker ID. + * + * @param bot The bot instance to use to make the request. + * @param stickerId The ID of the sticker to get + * @returns A {@link Sticker} + * + * @see {@link https://discord.com/developers/docs/resources/sticker#get-sticker} + */ +export async function getSticker ( + rest: RestManager, + stickerId: bigint +): Promise { + const result = await rest.runMethod( + rest, + 'GET', + rest.constants.routes.STICKER(stickerId) + ) + + return rest.transformers.sticker(rest, result) +} diff --git a/packages/rest/src/helpers/stickers/index.ts b/packages/rest/src/helpers/stickers/index.ts new file mode 100644 index 000000000..0121522dc --- /dev/null +++ b/packages/rest/src/helpers/stickers/index.ts @@ -0,0 +1,6 @@ +export * from './createGuildSticker.js' +export * from './deleteGuildSticker.js' +export * from './editGuildSticker.js' +export * from './getGuildSticker.js' +export * from './getGuildStickers.js' +export * from './getSticker.js' diff --git a/packages/rest/src/helpers/templates/createGuildTemplate.ts b/packages/rest/src/helpers/templates/createGuildTemplate.ts new file mode 100644 index 000000000..3902759f2 --- /dev/null +++ b/packages/rest/src/helpers/templates/createGuildTemplate.ts @@ -0,0 +1,48 @@ +import type { RestManager } from '../../restManager.js' +import { Template } from '../../transformers/template.js' +import { DiscordTemplate, BigString } from '@discordeno/types' + +/** + * Creates a template from a guild. + * + * @param bot - The bot instance to use to make the request. + * @param guildId - The ID of the guild to create the template from. + * @param options - The parameters for the creation of the template. + * @returns An instance of the created {@link Template}. + * + * @remarks + * Requires the `MANAGE_GUILD` permission. + * + * Fires a _Guild Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/resources/guild-template#create-guild-template} + */ +export async function createGuildTemplate ( + rest: RestManager, + guildId: BigString, + options: CreateTemplate +): Promise