diff --git a/packages/bot/src/desiredProperties.ts b/packages/bot/src/desiredProperties.ts index d09b224ad..199c68d2b 100644 --- a/packages/bot/src/desiredProperties.ts +++ b/packages/bot/src/desiredProperties.ts @@ -22,6 +22,8 @@ import type { InteractionResource, Invite, InviteStageInstance, + Lobby, + LobbyMember, Member, Message, MessageCall, @@ -96,6 +98,8 @@ export interface TransformersObjects { webhook: Webhook subscription: Subscription soundboardSound: SoundboardSound + lobby: Lobby + lobbyMember: LobbyMember } // NOTE: the top-level objects need both the dependencies and alwaysPresents even if empty when the key is specified, this is due the extends & nullability on DesiredPropertiesMetadata @@ -730,6 +734,20 @@ export function createDesiredPropertiesObject } @@ -828,28 +846,31 @@ type GetErrorWhenUndesired< type IsObject = T extends object ? (T extends Function ? false : true) : false // If the object is a transformed object, a collection of transformed object or an array of transformed objects we need to apply the desired props to them as well -export type TransformProperty< - T, - TProps extends TransformersDesiredProperties, - TBehavior extends DesiredPropertiesBehavior, -> = T extends TransformersObjects[keyof TransformersObjects] // is T a transformed object? +// NOTE: changing the order of these ternaries can cause bugs, for this reason we check in this order: +// - Is it an array? +// - Is it a collection? +// - Is it a bot? +// - Is it a transformed object? +// - Is it an object? +// - It's not an object +export type TransformProperty = T extends Array // is it an array? ? // Yes, apply the desired props - SetupDesiredProps + TransformProperty[] : // No, is it a collection? T extends Collection ? // Yes, check for nested proprieties Collection> - : // No, is it an array? - T extends Array - ? // Yes, apply the desired props - TransformProperty[] - : // No, is it a Bot? - T extends Bot - ? // Yes, return a bot with the correct set of props & behavior - Bot - : // No, is this a generic object? If so we need to ensure nested inside there aren't transformed objects + : // No, is it a Bot? + T extends Bot + ? // Yes, return a bot with the correct set of props & behavior + Bot + : // No, is it a transformed object? + T extends TransformersObjects[keyof TransformersObjects] + ? // Yes, apply the desired props + SetupDesiredProps + : // Is it an object? IsObject extends true - ? // Yes, check of nested proprieties + ? // Yes, we need to ensure nested inside there aren't transformed objects { [K in keyof T]: TransformProperty } : // No, this is a normal value such as string / bigint / number T diff --git a/packages/bot/src/helpers.ts b/packages/bot/src/helpers.ts index a1171dd88..837b48c78 100644 --- a/packages/bot/src/helpers.ts +++ b/packages/bot/src/helpers.ts @@ -1,6 +1,7 @@ import type { AddDmRecipientOptions, AddGuildMemberOptions, + AddLobbyMember, AtLeastOne, BeginGuildPrune, BigString, @@ -22,6 +23,7 @@ import type { CreateGuildRole, CreateGuildSoundboardSound, CreateGuildStickerOptions, + CreateLobby, CreateMessageOptions, CreateScheduledEvent, CreateStageInstance, @@ -80,6 +82,7 @@ import type { InteractionCallbackData, InteractionCallbackOptions, InteractionResponse, + LinkChannelToLobby, ListArchivedThreads, ListGuildMembers, ListSkuSubscriptionsOptions, @@ -93,6 +96,7 @@ import type { ModifyGuildMember, ModifyGuildSoundboardSound, ModifyGuildTemplate, + ModifyLobby, ModifyRolePositions, ModifyWebhook, SearchMembers, @@ -120,6 +124,8 @@ import type { Integration, InteractionCallbackResponse, Invite, + Lobby, + LobbyMember, Member, Message, Role, @@ -609,6 +615,24 @@ export function createBotHelpers { return await bot.rest.updateApplicationRoleConnectionsMetadataRecords(applicationId, options) }, + createLobby: async (options) => { + return bot.transformers.lobby(bot, snakelize(await bot.rest.createLobby(options))) + }, + getLobby: async (lobbyId) => { + return bot.transformers.lobby(bot, snakelize(await bot.rest.getLobby(lobbyId))) + }, + modifyLobby: async (lobbyId, options) => { + return bot.transformers.lobby(bot, snakelize(await bot.rest.modifyLobby(lobbyId, options))) + }, + addMemberToLobby: async (lobbyId, userId, options) => { + return bot.transformers.lobbyMember(bot, snakelize(await bot.rest.addMemberToLobby(lobbyId, userId, options))) + }, + linkChannelToLobby: async (lobbyId, bearerToken, options) => { + return bot.transformers.lobby(bot, snakelize(await bot.rest.linkChannelToLobby(lobbyId, bearerToken, options))) + }, + unlinkChannelToLobby: async (lobbyId, bearerToken) => { + return bot.transformers.lobby(bot, snakelize(await bot.rest.unlinkChannelToLobby(lobbyId, bearerToken))) + }, // All useless void return functions here addReaction: async (channelId, messageId, reaction) => { return await bot.rest.addReaction(channelId, messageId, reaction) @@ -823,6 +847,15 @@ export function createBotHelpers { await bot.rest.deleteGuildSoundboardSound(guildId, soundId, reason) }, + deleteLobby: async (lobbyId) => { + await bot.rest.deleteLobby(lobbyId) + }, + removeMemberFromLobby: async (lobbyId, userId) => { + await bot.rest.removeMemberFromLobby(lobbyId, userId) + }, + leaveLobby: async (lobbyId, bearerToken) => { + await bot.rest.leaveLobby(lobbyId, bearerToken) + }, } } @@ -1079,6 +1112,12 @@ export type BotHelpers[], ) => Promise[]> + createLobby: (options: CreateLobby) => Promise> + getLobby: (lobbyId: BigString) => Promise> + modifyLobby: (lobbyId: BigString, options: ModifyLobby) => Promise> + addMemberToLobby: (lobbyId: BigString, userId: BigString, options: AddLobbyMember) => Promise> + linkChannelToLobby: (lobbyId: BigString, bearerToken: string, options: LinkChannelToLobby) => Promise> + unlinkChannelToLobby: (lobbyId: BigString, bearerToken: string) => Promise> // functions return Void so dont need any special handling addReaction: (channelId: BigString, messageId: BigString, reaction: string) => Promise addReactions: (channelId: BigString, messageId: BigString, reactions: string[], ordered?: boolean) => Promise @@ -1164,4 +1203,7 @@ export type BotHelpers Promise> deleteGuildSoundboardSound: (guildId: BigString, soundId: BigString, reason?: string) => Promise + deleteLobby: (lobbyId: BigString) => Promise + removeMemberFromLobby: (lobbyId: BigString, userId: BigString) => Promise + leaveLobby: (lobbyId: BigString, bearerToken: string) => Promise } diff --git a/packages/bot/src/transformers.ts b/packages/bot/src/transformers.ts index 41c1d26d2..7636ba718 100644 --- a/packages/bot/src/transformers.ts +++ b/packages/bot/src/transformers.ts @@ -41,6 +41,8 @@ import type { DiscordInviteCreate, DiscordInviteMetadata, DiscordInviteStageInstance, + DiscordLobby, + DiscordLobbyMember, DiscordMember, DiscordMessage, DiscordMessageCall, @@ -116,6 +118,8 @@ import { type InteractionResource, type Invite, type InviteStageInstance, + type Lobby, + type LobbyMember, type Member, type Message, type MessageCall, @@ -184,6 +188,8 @@ import { transformInteractionResource, transformInvite, transformInviteStageInstance, + transformLobby, + transformLobbyMember, transformMember, transformMemberToDiscordMember, transformMessage, @@ -331,6 +337,8 @@ export type Transformers, ) => any + lobby: (bot: Bot, payload: DiscordLobby, lobby: SetupDesiredProps) => any + lobbyMember: (bot: Bot, payload: DiscordLobbyMember, lobbyMember: SetupDesiredProps) => any member: (bot: Bot, payload: DiscordMember, member: SetupDesiredProps) => any message: (bot: Bot, payload: DiscordMessage, message: SetupDesiredProps) => any messageCall: (bot: Bot, payload: DiscordMessageCall, call: SetupDesiredProps) => any @@ -470,6 +478,8 @@ export type Transformers, payload: DiscordInviteStageInstance & { guildId: BigString }, ) => SetupDesiredProps + lobby: (bot: Bot, payload: DiscordLobby) => SetupDesiredProps + lobbyMember: (bot: Bot, payload: DiscordLobbyMember) => SetupDesiredProps member: ( bot: Bot, payload: DiscordMember, @@ -560,6 +570,8 @@ export function createTransformers bot.transformers.lobbyMember(bot, member)) + if (props.linkedChannel && payload.linked_channel) lobby.linkedChannel = bot.transformers.channel(bot, { channel: payload.linked_channel }) + + return bot.transformers.customizers.lobby(bot, payload, lobby) +} + +export function transformLobbyMember(bot: InternalBot, payload: DiscordLobbyMember): LobbyMember { + const props = bot.transformers.desiredProperties.lobbyMember + const lobbyMember = {} as LobbyMember + + if (props.id && payload.id) lobbyMember.id = bot.transformers.snowflake(payload.id) + if (props.metadata && payload.metadata) lobbyMember.metadata = payload.metadata + if (props.flags && payload.flags) lobbyMember.flags = new ToggleBitfield(payload.flags) + + return bot.transformers.customizers.lobbyMember(bot, payload, lobbyMember) +} diff --git a/packages/bot/src/transformers/types.ts b/packages/bot/src/transformers/types.ts index b9f7cbf24..b6461a76a 100644 --- a/packages/bot/src/transformers/types.ts +++ b/packages/bot/src/transformers/types.ts @@ -1826,3 +1826,27 @@ export interface SoundboardSound { /** The user who created this sound */ user?: User } + +/** https://discord.com/developers/docs/resources/lobby#lobby-object-lobby-structure */ +export interface Lobby { + /** The id of this channel */ + id: bigint + /** application that created the lobby */ + applicationId: bigint + /** dictionary of string key/value pairs. The max total length is 1000. */ + metadata?: Record + /** members of the lobby */ + members: LobbyMember[] + /** the guild channel linked to the lobby */ + linkedChannel?: Channel +} + +/** https://discord.com/developers/docs/resources/lobby#lobby-member-object-lobby-member-structure */ +export interface LobbyMember { + /** The id of the user */ + id: bigint + /** dictionary of string key/value pairs. The max total length is 1000. */ + metadata?: Record + /** lobby member flags combined as as bitfield */ + flags?: ToggleBitfield +} diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index 1a6f4e17b..a70b62337 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -33,6 +33,8 @@ import { type DiscordInviteMetadata, type DiscordListActiveThreads, type DiscordListArchivedThreads, + type DiscordLobby, + type DiscordLobbyMember, type DiscordMember, type DiscordMemberWithUser, type DiscordMessage, @@ -1750,6 +1752,64 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }) }, + async createLobby(options) { + return await rest.post(rest.routes.lobby.create(), { + body: options, + }) + }, + + async getLobby(lobbyId) { + return await rest.get(rest.routes.lobby.lobby(lobbyId)) + }, + + async modifyLobby(lobbyId, options) { + return await rest.patch(rest.routes.lobby.lobby(lobbyId), { + body: options, + }) + }, + + async deleteLobby(lobbyId) { + return await rest.delete(rest.routes.lobby.lobby(lobbyId)) + }, + + async addMemberToLobby(lobbyId, userId, options) { + return await rest.put(rest.routes.lobby.member(lobbyId, userId), { + body: options, + }) + }, + + async removeMemberFromLobby(lobbyId, userId) { + return await rest.delete(rest.routes.lobby.member(lobbyId, userId)) + }, + + async leaveLobby(lobbyId, bearerToken) { + return await rest.delete(rest.routes.lobby.leave(lobbyId), { + headers: { + authorization: `Bearer ${bearerToken}`, + }, + unauthorized: true, + }) + }, + + async linkChannelToLobby(lobbyId, bearerToken, options) { + return await rest.patch(rest.routes.lobby.link(lobbyId), { + body: options, + headers: { + authorization: `Bearer ${bearerToken}`, + }, + unauthorized: true, + }) + }, + + async unlinkChannelToLobby(lobbyId, bearerToken) { + return await rest.patch(rest.routes.lobby.link(lobbyId), { + headers: { + authorization: `Bearer ${bearerToken}`, + }, + unauthorized: true, + }) + }, + preferSnakeCase(enabled: boolean) { const camelizer = enabled ? (x: any) => x : camelize diff --git a/packages/rest/src/routes.ts b/packages/rest/src/routes.ts index f3fd594ca..fa2d11e0c 100644 --- a/packages/rest/src/routes.ts +++ b/packages/rest/src/routes.ts @@ -647,6 +647,28 @@ export function createRoutes(): RestRoutes { }, }, + lobby: { + create: () => { + return '/lobbies' + }, + + lobby: (lobbyId) => { + return `/lobbies/${lobbyId}` + }, + + member: (lobbyId, userId) => { + return `/lobbies/${lobbyId}/members/${userId}` + }, + + leave: (lobbyId) => { + return `/lobbies/${lobbyId}/members/@me` + }, + + link: (lobbyId) => { + return `/lobbies/${lobbyId}/channel-linking` + }, + }, + applicationEmoji(applicationId, emojiId) { return `/applications/${applicationId}/emojis/${emojiId}` }, diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index 83b773b5c..9174e74a8 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -1,6 +1,7 @@ import type { AddDmRecipientOptions, AddGuildMemberOptions, + AddLobbyMember, AtLeastOne, BeginGuildPrune, BigString, @@ -22,6 +23,7 @@ import type { CreateGuildRole, CreateGuildSoundboardSound, CreateGuildStickerOptions, + CreateLobby, CreateMessageOptions, CreateScheduledEvent, CreateStageInstance, @@ -60,6 +62,8 @@ import type { DiscordInteractionCallbackResponse, DiscordInvite, DiscordInviteMetadata, + DiscordLobby, + DiscordLobbyMember, DiscordMember, DiscordMemberWithUser, DiscordMessage, @@ -114,6 +118,7 @@ import type { InteractionCallbackData, InteractionCallbackOptions, InteractionResponse, + LinkChannelToLobby, ListArchivedThreads, ListGuildMembers, ListSkuSubscriptionsOptions, @@ -128,6 +133,7 @@ import type { ModifyGuildMember, ModifyGuildSoundboardSound, ModifyGuildTemplate, + ModifyLobby, ModifyRolePositions, ModifyWebhook, SearchMembers, @@ -3174,6 +3180,88 @@ export interface RestManager { applicationId: BigString, options: Camelize[], ) => Promise[]> + /** + * Creates a new lobby, adding any of the specified members to it, if provided. + * + * @param options - The options to create the lobby + * @returns The created lobby + */ + createLobby: (options: CreateLobby) => Promise> + /** + * Returns a lobby object for the specified lobby id, if it exists. + * + * @param lobbyId - The ID of the lobby to get + * @returns The lobby object + */ + getLobby: (lobbyId: BigString) => Promise> + /** + * Modifies the specified lobby with new values, if provided. + * + * @param lobbyId - The ID of the lobby to modify + * @param options - The options to modify the lobby + * @returns The modified lobby + */ + modifyLobby: (lobbyId: BigString, options: ModifyLobby) => Promise> + /** + * Deletes the specified lobby if it exists. + * + * It is safe to call even if the lobby is already deleted as well. + * + * @param lobbyId - The ID of the lobby to delete + * @returns Nothing + */ + deleteLobby: (lobbyId: BigString) => Promise + /** + * Adds the provided user to the specified lobby. If called when the user is already a member of the lobby will update fields such as metadata on that user instead. + * + * @param lobbyId - The ID of the lobby to add the user to + * @param userId - The ID of the user to add to the lobby + * @param options - The options to add the user to the lobby + * @returns The lobby member object + */ + addMemberToLobby: (lobbyId: BigString, userId: BigString, options: AddLobbyMember) => Promise> + /** + * Removes the provided user from the specified lobby. It is safe to call this even if the user is no longer a member of the lobby, but will fail if the lobby does not exist. + * + * @param lobbyId - The ID of the lobby to remove the user from + * @param userId - The ID of the user to remove from the lobby + * @returns Nothing + */ + removeMemberFromLobby: (lobbyId: BigString, userId: BigString) => Promise + /** + * Removes the current user from the specified lobby. It is safe to call this even if the user is no longer a member of the lobby, but will fail if the lobby does not exist. + * + * @param lobbyId - The ID of the lobby to remove the user from + * @param bearerToken - The access token of the user + * @returns Nothing + * + * @remarks + * This requires a bearer token for authorization + */ + leaveLobby: (lobbyId: BigString, bearerToken: string) => Promise + /** + * Links an existing text channel to a lobby. + * + * @param lobbyId - The ID of the lobby to link the channel to + * @param bearerToken - The access token of the user + * @param options - The options to link the channel to the lobby + * @returns The updated lobby object + * + * @remarks + * Uses bearer token for authorization and the user must be a lobby member with the CanLinkLobby lobby member flag. + */ + linkChannelToLobby: (lobbyId: BigString, bearerToken: string, options: LinkChannelToLobby) => Promise> + /** + * Unlinks any currently linked channels from the specified lobby. + * + * @param lobbyId - The ID of the lobby to unlink the channel from + * @param bearerToken - The access token of the user + * @returns The updated lobby object + * + * @remarks + * Uses bearer token for authorization and the user must be a lobby member with the CanLinkLobby lobby member flag. + */ + unlinkChannelToLobby: (lobbyId: BigString, bearerToken: string) => Promise> } export type RequestMethods = 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT' diff --git a/packages/rest/src/typings/routes.ts b/packages/rest/src/typings/routes.ts index 2f0ce595d..8dbb6df76 100644 --- a/packages/rest/src/typings/routes.ts +++ b/packages/rest/src/typings/routes.ts @@ -297,6 +297,19 @@ export interface RestRoutes { /** Route for get/edit/delete of a guild sound */ guildSound: (guildId: BigString, soundId: BigString) => string } + /** Routes realted to lobbies */ + lobby: { + /** Route to create a lobby */ + create: () => string + /** Route to get a specific lobby */ + lobby: (lobbyId: BigString) => string + /** Route to add/remove a member from a lobby */ + member: (lobbyId: BigString, userId: BigString) => string + /** Route to leave a lobby */ + leave: (lobbyId: BigString) => string + /** Route to link a lobby */ + link: (lobbyId: BigString) => string + } /** Route to list / create an application emoji */ applicationEmojis: (applicationId: BigString) => string /** Route to list / update application role connection metadata records */ diff --git a/packages/types/src/discord/lobby.ts b/packages/types/src/discord/lobby.ts new file mode 100644 index 000000000..ffa47444f --- /dev/null +++ b/packages/types/src/discord/lobby.ts @@ -0,0 +1,33 @@ +/** Types for: https://discord.com/developers/docs/resources/lobby */ + +import type { DiscordChannel } from './channel.js' + +/** https://discord.com/developers/docs/resources/lobby#lobby-object-lobby-structure */ +export interface DiscordLobby { + /** The id of this channel */ + id: string + /** application that created the lobby */ + application_id: string + /** dictionary of string key/value pairs. The max total length is 1000. */ + metadata?: Record + /** members of the lobby */ + members: DiscordLobbyMember[] + /** the guild channel linked to the lobby */ + linked_channel?: DiscordChannel +} + +/** https://discord.com/developers/docs/resources/lobby#lobby-member-object-lobby-member-structure */ +export interface DiscordLobbyMember { + /** The id of the user */ + id: string + /** dictionary of string key/value pairs. The max total length is 1000. */ + metadata?: Record + /** lobby member flags combined as as bitfield */ + flags?: number +} + +/** https://discord.com/developers/docs/resources/lobby#lobby-member-object-lobby-member-flags */ +export enum DiscordLobbyMemberFlags { + /** User can link a text channel to a lobby */ + CanLinkLobby = 1 << 0, +} diff --git a/packages/types/src/discordeno.ts b/packages/types/src/discordeno.ts index d34fc9088..9964e9ba6 100644 --- a/packages/types/src/discordeno.ts +++ b/packages/types/src/discordeno.ts @@ -1690,3 +1690,47 @@ export interface ModifyGuildIncidentActions { */ dms_disabled_until?: string | null } + +/** https://discord.com/developers/docs/resources/lobby#create-lobby */ +export interface CreateLobby { + /** Optional dictionary of string key/value pairs. The max total length is 1000. */ + metadata?: Record | null + /** Optional array of up to 25 users to be added to the lobby */ + members?: CreateLobbyMember[] + /** Seconds to wait before shutting down a lobby after it becomes idle. Value can be between 5 and 604800 (7 days). */ + idleTimeoutSeconds?: number +} + +/** https://discord.com/developers/docs/resources/lobby#create-lobby */ +export interface CreateLobbyMember { + /** Discord user id of the user to add to the lobby */ + id: BigString + /** Optional dictionary of string key/value pairs. The max total length is 1000. */ + metadata?: Record | null + /** Lobby member flags combined as a bitfield */ + flags?: number +} + +/** https://discord.com/developers/docs/resources/lobby#add-a-member-to-a-lobby */ +export interface ModifyLobby { + /** Optional dictionary of string key/value pairs. The max total length is 1000. Overwrites any existing metadata. */ + metadata?: Record | null + /** Optional array of up to 25 users to replace the lobby members with. If provided, lobby members not in this list will be removed from the lobby. */ + members?: CreateLobbyMember[] + /** Seconds to wait before shutting down a lobby after it becomes idle. Value can be between 5 and 604800 (7 days). */ + idleTimeoutSeconds?: number +} + +/** https://discord.com/developers/docs/resources/lobby#add-a-member-to-a-lobby */ +export interface AddLobbyMember { + /** Optional dictionary of string key/value pairs. The max total length is 1000. */ + metadata?: Record | null + /** Lobby member flags combined as a bitfield */ + flags?: number +} + +/** https://discord.com/developers/docs/resources/lobby#link-channel-to-lobby */ +export interface LinkChannelToLobby { + /** The id of the channel to link to the lobby. If not provided, will unlink any currently linked channels from the lobby. */ + channelId?: BigString +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 9c861c31e..f21b1e48a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -11,6 +11,7 @@ export * from './discord/guildScheduledEvent.js' export * from './discord/guildTemplate.js' export * from './discord/interactions.js' export * from './discord/invite.js' +export * from './discord/lobby.js' export * from './discord/message.js' export * from './discord/oauth2.js' export * from './discord/opcodes.js'