From ce2272e359c621c4d785d1d425221dce84665cb7 Mon Sep 17 00:00:00 2001 From: Fleny Date: Wed, 29 Jul 2026 13:43:11 +0200 Subject: [PATCH] feat: Add lobby messages and lobby invites (#5232) * feat: Add lobby messages and lobby invites * Update packages/bot/src/transformers/lobby.ts Co-authored-by: Awesome Stickz <38146668+AwesomeStickz@users.noreply.github.com> --- packages/bot/src/desiredProperties.ts | 21 ++++++++ packages/bot/src/helpers.ts | 33 ++++++++++++ packages/bot/src/transformers.ts | 12 ++++- packages/bot/src/transformers/lobby.ts | 31 ++++++++++- packages/bot/src/transformers/types.ts | 34 ++++++++++++ packages/rest/src/manager.ts | 40 ++++++++++++++ packages/rest/src/routes.ts | 18 +++++++ packages/rest/src/types.ts | 73 ++++++++++++++++++++++++++ packages/rest/src/typings/routes.ts | 7 +++ packages/types/src/discord/lobby.ts | 36 +++++++++++++ packages/types/src/discordeno/lobby.ts | 73 ++++++++++++++++++++++++++ 11 files changed, 375 insertions(+), 3 deletions(-) diff --git a/packages/bot/src/desiredProperties.ts b/packages/bot/src/desiredProperties.ts index ea7229477..ce26ea5d1 100644 --- a/packages/bot/src/desiredProperties.ts +++ b/packages/bot/src/desiredProperties.ts @@ -26,7 +26,9 @@ import type { Invite, InviteStageInstance, Lobby, + LobbyInvite, LobbyMember, + LobbyMessage, MediaGalleryItem, Member, Message, @@ -88,6 +90,8 @@ export interface TransformersObjects { inviteStageInstance: InviteStageInstance; lobby: Lobby; lobbyMember: LobbyMember; + lobbyMessage: LobbyMessage; + lobbyInvite: LobbyInvite; mediaGalleryItem: MediaGalleryItem; member: Member; message: Message; @@ -874,6 +878,23 @@ export function createDesiredPropertiesObject; } diff --git a/packages/bot/src/helpers.ts b/packages/bot/src/helpers.ts index 3d08a59cc..7df2468c4 100644 --- a/packages/bot/src/helpers.ts +++ b/packages/bot/src/helpers.ts @@ -24,6 +24,7 @@ import type { CreateGuildStickerOptions, CreateLobby, CreateMessageOptions, + CreateOrJoinLobby, CreateScheduledEvent, CreateStageInstance, CreateTemplate, @@ -70,6 +71,7 @@ import type { GetGuildAuditLog, GetGuildPruneCountQuery, GetInvite, + GetLobbyMessages, GetMessagesOptions, GetReactions, GetScheduledEvents, @@ -99,6 +101,7 @@ import type { ModifyRolePositions, ModifyWebhook, SearchMembers, + SendLobbyMessage, SendSoundboardSound, StartThreadWithMessage, StartThreadWithoutMessage, @@ -126,7 +129,9 @@ import type { InteractionCallbackResponse, Invite, Lobby, + LobbyInvite, LobbyMember, + LobbyMessage, Member, Message, MessagePin, @@ -611,6 +616,9 @@ export function createBotHelpers { return bot.transformers.lobby(bot, snakelize(await bot.rest.createLobby(options))); }, + createOrJoinLobby: async (options) => { + return bot.transformers.lobby(bot, snakelize(await bot.rest.createOrJoinLobby(options))); + }, getLobby: async (lobbyId) => { return bot.transformers.lobby(bot, snakelize(await bot.rest.getLobby(lobbyId))); }, @@ -632,6 +640,18 @@ export function createBotHelpers { return await bot.rest.updateLobbyMessageModerationMetadata(lobbyId, messageId, options); }, + sendLobbyMessage: async (bearerToken, lobbyId, options) => { + return bot.transformers.lobbyMessage(bot, snakelize(await bot.rest.sendLobbyMessage(bearerToken, lobbyId, options))); + }, + getLobbyMessages: async (bearerToken, lobbyId, options) => { + return (await bot.rest.getLobbyMessages(bearerToken, lobbyId, options)).map((res) => bot.transformers.lobbyMessage(bot, snakelize(res))); + }, + createLobbyChannelInviteForSelf: async (bearerToken, lobbyId) => { + return bot.transformers.lobbyInvite(bot, snakelize(await bot.rest.createLobbyChannelInviteForSelf(bearerToken, lobbyId))); + }, + createLobbyChannelInviteForUser: async (lobbyId, userId) => { + return bot.transformers.lobbyInvite(bot, snakelize(await bot.rest.createLobbyChannelInviteForUser(lobbyId, userId))); + }, getTargetUsers: async (inviteCode) => { return await bot.rest.getTargetUsers(inviteCode); }, @@ -1170,6 +1190,7 @@ export type BotHelpers[], ) => Promise[]>; createLobby: (options: CreateLobby) => Promise>; + createOrJoinLobby: (options: CreateOrJoinLobby) => Promise>; getLobby: (lobbyId: BigString) => Promise>; modifyLobby: (lobbyId: BigString, options: ModifyLobby) => Promise>; addMemberToLobby: (lobbyId: BigString, userId: BigString, options: AddLobbyMember) => Promise>; @@ -1180,6 +1201,18 @@ export type BotHelpers Promise; updateTargetUsers: (inviteCode: string, targetUsersFile: Blob) => Promise; getTargetUsersJobStatus: (inviteCode: string) => Promise>; + sendLobbyMessage: ( + bearerToken: string, + lobbyId: BigString, + options: SendLobbyMessage, + ) => Promise>; + getLobbyMessages: ( + bearerToken: string, + lobbyId: BigString, + options?: GetLobbyMessages, + ) => Promise[]>; + createLobbyChannelInviteForSelf: (bearerToken: string, lobbyId: BigString) => Promise>; + createLobbyChannelInviteForUser: (lobbyId: BigString, userId: BigString) => 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; diff --git a/packages/bot/src/transformers.ts b/packages/bot/src/transformers.ts index 1a873dbf0..623db1ee4 100644 --- a/packages/bot/src/transformers.ts +++ b/packages/bot/src/transformers.ts @@ -42,7 +42,9 @@ import type { DiscordInviteMetadata, DiscordInviteStageInstance, DiscordLobby, + DiscordLobbyInvite, DiscordLobbyMember, + DiscordLobbyMessage, DiscordMediaGalleryItem, DiscordMember, DiscordMessage, @@ -116,7 +118,7 @@ import { transformInteractionResource, } from './transformers/interaction.js'; import { transformInvite } from './transformers/invite.js'; -import { transformLobby, transformLobbyMember } from './transformers/lobby.js'; +import { transformLobby, transformLobbyInvite, transformLobbyMember, transformLobbyMessage } from './transformers/lobby.js'; import { transformMember } from './transformers/member.js'; import { transformMessage, @@ -195,7 +197,9 @@ import type { Invite, InviteStageInstance, Lobby, + LobbyInvite, LobbyMember, + LobbyMessage, MediaGalleryItem, Member, Message, @@ -289,6 +293,8 @@ export type TransformerFunctions; lobby: TransformerFunction; lobbyMember: TransformerFunction; + lobbyMessage: TransformerFunction; + lobbyInvite: TransformerFunction; mediaGalleryItem: TransformerFunction; member: TransformerFunction; message: TransformerFunction; @@ -400,6 +406,8 @@ export function createTransformers; + + if (props.id && payload.id) lobbyMessage.id = bot.transformers.snowflake(payload.id); + if (props.type) lobbyMessage.type = payload.type; + if (props.content && payload.content) lobbyMessage.content = payload.content; + if (props.lobbyId && payload.lobby_id) lobbyMessage.lobbyId = bot.transformers.snowflake(payload.lobby_id); + if (props.channelId && payload.channel_id) lobbyMessage.channelId = bot.transformers.snowflake(payload.channel_id); + if (props.author && payload.author) lobbyMessage.author = bot.transformers.user(bot, payload.author); + if (props.metadata && payload.metadata) lobbyMessage.metadata = payload.metadata; + if (props.moderationMetadata && payload.moderation_metadata) lobbyMessage.moderationMetadata = payload.moderation_metadata; + if (props.flags && payload.flags) lobbyMessage.flags = new ToggleBitfield(payload.flags); + if (props.applicationId && payload.application_id) lobbyMessage.applicationId = bot.transformers.snowflake(payload.application_id); + + return bot.transformers.customizers.lobbyMessage(bot, payload, lobbyMessage); +} + +export function transformLobbyInvite(bot: Bot, payload: DiscordLobbyInvite): LobbyInvite { + const props = bot.transformers.desiredProperties.lobbyInvite; + const lobbyInvite = {} as SetupDesiredProps; + + if (props.code && payload.code) lobbyInvite.code = payload.code; + + return bot.transformers.customizers.lobbyInvite(bot, payload, lobbyInvite); +} diff --git a/packages/bot/src/transformers/types.ts b/packages/bot/src/transformers/types.ts index d46ff13e3..d6ba2574e 100644 --- a/packages/bot/src/transformers/types.ts +++ b/packages/bot/src/transformers/types.ts @@ -2091,3 +2091,37 @@ export interface LobbyMember { /** lobby member flags combined as as bitfield */ flags?: ToggleBitfield; } + +/** https://docs.discord.com/developers/resources/lobby#lobby-message-object */ +export interface LobbyMessage { + /** id of the message */ + id: bigint; + /** Message type */ + type: MessageTypes; + /** Message content */ + content: string; + /** id of the lobby this message was sent to */ + lobbyId: bigint; + /** Included for compatibility with the messages interface; equal to lobby_id */ + channelId: bigint; + /** The user who sent the message */ + author: User; + /** Dispatch-only metadata sent with the message */ + metadata?: Record; + /** Moderation metadata set via Update Lobby Message Moderation Metadata */ + moderationMetadata?: Record; + /** + * Message flags bitfield + * + * @see {@link MessageFlags} + */ + flags: ToggleBitfield; + /** The application that sent the message */ + applicationId: bigint; +} + +/** https://docs.discord.com/developers/resources/lobby#lobby-invite-object */ +export interface LobbyInvite { + /** The invite code for the lobby's linked channel */ + code: string; +} diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index 588f4a76d..7faeca4a8 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -35,7 +35,9 @@ import type { DiscordListActiveThreads, DiscordListArchivedThreads, DiscordLobby, + DiscordLobbyInvite, DiscordLobbyMember, + DiscordLobbyMessage, DiscordMember, DiscordMemberWithUser, DiscordMessage, @@ -1948,6 +1950,44 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }); }, + createOrJoinLobby(options) { + return rest.put(rest.routes.lobby.create(), { + body: options, + }); + }, + + sendLobbyMessage(bearerToken, lobbyId, options) { + return rest.post(rest.routes.lobby.messages(lobbyId), { + body: options, + headers: { + authorization: `Bearer ${bearerToken}`, + }, + unauthorized: true, + }); + }, + + getLobbyMessages(bearerToken, lobbyId, options) { + return rest.get(rest.routes.lobby.messages(lobbyId, options), { + headers: { + authorization: `Bearer ${bearerToken}`, + }, + unauthorized: true, + }); + }, + + createLobbyChannelInviteForSelf(bearerToken, lobbyId) { + return rest.post(rest.routes.lobby.inviteSelf(lobbyId), { + headers: { + authorization: `Bearer ${bearerToken}`, + }, + unauthorized: true, + }); + }, + + createLobbyChannelInviteForUser(lobbyId, userId) { + return rest.post(rest.routes.lobby.inviteUser(lobbyId, userId)); + }, + 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 328108a25..b02110f20 100644 --- a/packages/rest/src/routes.ts +++ b/packages/rest/src/routes.ts @@ -702,6 +702,24 @@ export function createRoutes(disableURIEncode: boolean = false): RestRoutes { moderationMetadata: (lobbyId, messageId) => { return `/lobbies/${encode(lobbyId)}/messages/${encode(messageId)}/moderation-metadata`; }, + + messages: (lobbyId, options) => { + let url = `/lobbies/${encode(lobbyId)}/messages?`; + + if (options) { + if (options.limit) url += `limit=${encode(options.limit)}`; + } + + return url; + }, + + inviteSelf: (lobbyId) => { + return `/lobbies/${encode(lobbyId)}/members/@me/invites`; + }, + + inviteUser: (lobbyId, userId) => { + return `/lobbies/${encode(lobbyId)}/members/${encode(userId)}/invites`; + }, }, applicationEmoji(applicationId, emojiId) { diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index 10b84cc30..ee333d16d 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -25,6 +25,7 @@ import type { CreateGuildStickerOptions, CreateLobby, CreateMessageOptions, + CreateOrJoinLobby, CreateScheduledEvent, CreateStageInstance, CreateTemplate, @@ -67,7 +68,9 @@ import type { DiscordListActiveThreads, DiscordListArchivedThreads, DiscordLobby, + DiscordLobbyInvite, DiscordLobbyMember, + DiscordLobbyMessage, DiscordMember, DiscordMemberWithUser, DiscordMessage, @@ -113,6 +116,7 @@ import type { GetGuildAuditLog, GetGuildPruneCountQuery, GetInvite, + GetLobbyMessages, GetMessagesOptions, GetPollAnswerVotes, GetReactions, @@ -147,6 +151,7 @@ import type { ScheduledEventEntityType, ScheduledEventStatus, SearchMembers, + SendLobbyMessage, SendSoundboardSound, StartThreadWithMessage, StartThreadWithoutMessage, @@ -3225,6 +3230,18 @@ export interface RestManager { * @returns The created lobby */ createLobby: (options: CreateLobby) => Promise>; + /** + * Creates a new lobby for the application identified by a `secret`, or joins the calling user to the existing lobby with that secret if one already exists. + * + * Updates lobby metadata and the calling member's metadata on join. + * + * @param options - The options to create or join the lobby + * @returns - The created or joined lobby + * + * @remarks + * Uses `Bearer` token for authorization with the `sdk.social_layer` scope. + */ + createOrJoinLobby: (options: CreateOrJoinLobby) => Promise>; /** * Returns a lobby object for the specified lobby id, if it exists. * @@ -3331,6 +3348,62 @@ export interface RestManager { * @see {@link https://discord.com/developers/docs/game-sdk/social-sdk/chat-moderation#server-side-chat-moderation} for the full moderation flow. */ updateLobbyMessageModerationMetadata: (lobbyId: BigString, messageId: BigString, options: Record) => Promise; + /** + * Sends a message to the specified lobby. The calling user must be a member of the lobby. + * + * @param bearerToken - The access token of the user + * @param lobbyId - The ID of the lobby to send the message to + * @param options - The options to send the message + * @returns Returns the created lobby message object. + * + * @remarks + * Uses `Bearer` token for authorization with the `sdk.social_layer` scope. + * + * If the lobby has a linked channel, the message is also forwarded to that channel. + * If forwarding fails (for example, due to AutoMod), the lobby message is still delivered to other lobby members. + */ + sendLobbyMessage: (bearerToken: string, lobbyId: BigString, options: SendLobbyMessage) => Promise>; + /** + * Returns the most recent messages in the specified lobby. The calling user must be a member of the lobby. + * + * @param bearerToken - The access token of the user + * @param lobbyId - The ID of the lobby to get the messages from + * @param options - The options to get the messages + * @returns Returns an array of lobby message objects + * + * @remarks + * Uses `Bearer` token for authorization with the `sdk.social_layer` scope. + */ + getLobbyMessages: (bearerToken: string, lobbyId: BigString, options?: GetLobbyMessages) => Promise[]>; + /** + * Creates a single-use guild invite to the lobby's linked channel, targeted at the calling user. + * + * + * @param bearerToken - The access token of the user + * @param lobbyId - The ID of the lobby to create the invite for + * @returns The created invite object + * + * @remarks + * Uses `Bearer` token for authorization with the `sdk.social_layer` scope. + * + * The lobby must have a linked channel and the caller must be a member of the lobby. + * The invite expires after one hour. + */ + createLobbyChannelInviteForSelf: (bearerToken: string, lobbyId: BigString) => Promise>; + /** + * Creates a single-use guild invite to the lobby's linked channel on behalf of an application, targeted at the specified user. + * + * @param lobbyId - The ID of the lobby to create the invite for + * @param userId - The ID of the user to create the invite for + * @returns The created invite object + * + * @remarks + * Uses `Bot` token for authorization + * + * The lobby must have a linked channel. + * The invite expires after one hour. + */ + createLobbyChannelInviteForUser: (lobbyId: BigString, userId: BigString) => 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 a76af3c95..ed066ce74 100644 --- a/packages/rest/src/typings/routes.ts +++ b/packages/rest/src/typings/routes.ts @@ -6,6 +6,7 @@ import type { GetGuildAuditLog, GetGuildPruneCountQuery, GetInvite, + GetLobbyMessages, GetMessagesOptions, GetPollAnswerVotes, GetReactions, @@ -320,6 +321,12 @@ export interface RestRoutes { membersBulk: (lobbyId: BigString) => string; /** Route to set the lobby moderation metadata */ moderationMetadata: (lobbyId: BigString, messageId: BigString) => string; + /** Route to lobby messages */ + messages: (lobbyId: BigString, options?: GetLobbyMessages) => string; + /** Route to invite self to the lobby's linked channel */ + inviteSelf: (lobbyId: BigString) => string; + /** Route to invite a specific user to the lobby's linked channel */ + inviteUser: (lobbyId: BigString, userId: BigString) => string; }; /** Route to list / create an application emoji */ applicationEmojis: (applicationId: BigString) => string; diff --git a/packages/types/src/discord/lobby.ts b/packages/types/src/discord/lobby.ts index 8e772ecab..88b89eb46 100644 --- a/packages/types/src/discord/lobby.ts +++ b/packages/types/src/discord/lobby.ts @@ -1,6 +1,8 @@ /** Types for: https://docs.discord.com/developers/resources/lobby */ import type { DiscordChannel } from './channel.js'; +import type { MessageFlags, MessageTypes } from './message.js'; +import type { DiscordUser } from './user.js'; /** https://docs.discord.com/developers/resources/lobby#lobby-object-lobby-structure */ export interface DiscordLobby { @@ -31,3 +33,37 @@ export enum DiscordLobbyMemberFlags { /** User can link a text channel to a lobby */ CanLinkLobby = 1 << 0, } + +/** https://docs.discord.com/developers/resources/lobby#lobby-message-object */ +export interface DiscordLobbyMessage { + /** id of the message */ + id: string; + /** Message type */ + type: MessageTypes; + /** Message content */ + content: string; + /** id of the lobby this message was sent to */ + lobby_id: string; + /** Included for compatibility with the messages interface; equal to lobby_id */ + channel_id: string; + /** The user who sent the message */ + author: DiscordUser; + /** Dispatch-only metadata sent with the message */ + metadata?: Record | null; + /** Moderation metadata set via Update Lobby Message Moderation Metadata */ + moderation_metadata?: Record | null; + /** + * Message flags bitfield + * + * @see {@link MessageFlags} + */ + flags: number; + /** The application that sent the message */ + application_id: string; +} + +/** https://docs.discord.com/developers/resources/lobby#lobby-invite-object */ +export interface DiscordLobbyInvite { + /** The invite code for the lobby's linked channel */ + code: string; +} diff --git a/packages/types/src/discordeno/lobby.ts b/packages/types/src/discordeno/lobby.ts index 38dbd348d..8a0b49454 100644 --- a/packages/types/src/discordeno/lobby.ts +++ b/packages/types/src/discordeno/lobby.ts @@ -13,6 +13,41 @@ export interface CreateLobby { idleTimeoutSeconds?: number; } +/** https://docs.discord.com/developers/resources/lobby#create-or-join-lobby */ +export interface CreateOrJoinLobby { + /** + * Secret used to identify the lobby. + * If a lobby for this application already exists with this secret, the caller joins it; otherwise a new lobby is created. + * + * @remarks + * Max 250 characters + */ + secret: string; + /** + * Seconds to wait before shutting down a lobby after it becomes idle. + * + * @remarks + * Value can be between 5 and 604800 (7 days). + */ + idleTimeoutSeconds?: number; + /** + * Optional dictionary of string key/value pairs. + * + * @remarks + * The max total length is 1000. + * + * Overwrites any existing lobby metadata. + */ + lobbyMetadata?: Record | null; + /** + * Optional dictionary of string key/value pairs to set on the calling user’s lobby member + * + * @remarks + * The max total length is 1000. + */ + memberMetadata?: Record | null; +} + /** https://docs.discord.com/developers/resources/lobby#create-lobby */ export interface CreateLobbyMember { /** Discord user id of the user to add to the lobby */ @@ -70,3 +105,41 @@ 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; } + +/** https://docs.discord.com/developers/resources/lobby#send-lobby-message */ +export interface SendLobbyMessage { + /** + * Message content + * + * @remarks + * Must be non-empty + */ + content: string; + /** + * Optional dictionary of string key/value pairs delivered alongside the message to active clients via the Social SDK. + * + * @remarks + * Not persisted on the linked channel message. + */ + metadata?: Record | null; + /** + * Optional message flags combined as a bitfield. + * + * @remarks + * Only flags creatable by the Social SDK are accepted. + */ + flags?: number; +} + +/** https://docs.discord.com/developers/resources/lobby#get-lobby-messages */ +export interface GetLobbyMessages { + /** + * Max number of messages to return + * + * @remarks + * Must be between 1 and 200 + * + * @default 50 + */ + limit?: number; +}