diff --git a/packages/bot/src/helpers.ts b/packages/bot/src/helpers.ts index bfe80b06a..ca9098483 100644 --- a/packages/bot/src/helpers.ts +++ b/packages/bot/src/helpers.ts @@ -46,6 +46,8 @@ import type { DiscordInvite, DiscordListArchivedThreads, DiscordPrunedCount, + DiscordSearchGuildMessages, + DiscordSearchGuildMessagesIndexing, DiscordTargetUsersJobStatus, DiscordTokenExchange, DiscordTokenRevocation, @@ -100,6 +102,7 @@ import type { ModifyLobby, ModifyRolePositions, ModifyWebhook, + SearchGuildMessagesOptions, SearchMembers, SendLobbyMessage, SendSoundboardSound, @@ -542,6 +545,16 @@ export function createBotHelpers { return bot.transformers.message(bot, snakelize(await bot.rest.publishMessage(channelId, messageId))); }, + searchGuildMessages: async (guildId, options) => { + const result = await bot.rest.searchGuildMessages(guildId, options); + if ('code' in result) return result; + return { + ...result, + messages: result.messages.map((messages) => messages.map((message) => bot.transformers.message(bot, snakelize(message)))), + threads: result.threads?.map((thread) => bot.transformers.channel(bot, snakelize(thread), { guildId })), + members: result.members?.map((member) => bot.transformers.threadMember(bot, snakelize(member), { guildId })), + }; + }, sendMessage: async (channelId, options) => { return bot.transformers.message(bot, snakelize(await bot.rest.sendMessage(channelId, options))); }, @@ -1071,6 +1084,17 @@ export type BotHelpers Promise[]>; getMessage: (channelId: BigString, messageId: BigString) => Promise>; getMessages: (channelId: BigString, options?: GetMessagesOptions) => Promise[]>; + searchGuildMessages: ( + guildId: BigString, + options?: SearchGuildMessagesOptions, + ) => Promise< + | Camelize + | (Omit, 'messages' | 'threads' | 'members'> & { + messages: SetupDesiredProps[][]; + threads?: SetupDesiredProps[]; + members?: SetupDesiredProps[]; + }) + >; getStickerPack: (stickerPackId: BigString) => Promise>; getStickerPacks: () => Promise[]>; getOriginalInteractionResponse: (token: string) => Promise>; diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index 7faeca4a8..0b0c3fa43 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -44,6 +44,8 @@ import type { DiscordPrunedCount, DiscordRole, DiscordScheduledEvent, + DiscordSearchGuildMessages, + DiscordSearchGuildMessagesIndexing, DiscordSku, DiscordSoundboardSound, DiscordStageInstance, @@ -1433,6 +1435,9 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage return await rest.get(rest.routes.channels.messages(channelId, options)); }, + async searchGuildMessages(guildId, options) { + return await rest.get(rest.routes.guilds.messagesSearch(guildId, options)); + }, async getStickerPack(stickerPackId) { return await rest.get(rest.routes.stickerPack(stickerPackId)); }, diff --git a/packages/rest/src/routes.ts b/packages/rest/src/routes.ts index b02110f20..0c303218f 100644 --- a/packages/rest/src/routes.ts +++ b/packages/rest/src/routes.ts @@ -1,5 +1,5 @@ -import type { GetMessagesOptions, GetScheduledEventUsers } from '@discordeno/types'; -import { isGetMessagesAfter, isGetMessagesAround, isGetMessagesBefore, isGetMessagesLimit } from '@discordeno/utils'; +import type { GetMessagesOptions, GetScheduledEventUsers, SearchGuildMessagesOptions } from '@discordeno/types'; +import { camelToSnakeCase, isGetMessagesAfter, isGetMessagesAround, isGetMessagesBefore, isGetMessagesLimit } from '@discordeno/utils'; import type { RestRoutes } from './typings/routes.js'; /** @@ -447,6 +447,19 @@ export function createRoutes(disableURIEncode: boolean = false): RestRoutes { return url; }, }, + messagesSearch: (guildId, options?: SearchGuildMessagesOptions) => { + let url = `/guilds/${encode(guildId)}/messages/search?`; + + if (options) { + for (const [key, value] of Object.entries(options)) { + if (value === undefined) continue; + + for (const item of Array.isArray(value) ? value : [value]) url += `&${camelToSnakeCase(key)}=${encode(item)}`; + } + } + + return url; + }, preview: (guildId) => { return `/guilds/${encode(guildId)}/preview`; }, diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index ee333d16d..b2b0eeae2 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -77,6 +77,8 @@ import type { DiscordPrunedCount, DiscordRole, DiscordScheduledEvent, + DiscordSearchGuildMessages, + DiscordSearchGuildMessagesIndexing, DiscordSku, DiscordSoundboardSound, DiscordStageInstance, @@ -150,6 +152,7 @@ import type { ModifyWebhook, ScheduledEventEntityType, ScheduledEventStatus, + SearchGuildMessagesOptions, SearchMembers, SendLobbyMessage, SendSoundboardSound, @@ -2094,6 +2097,18 @@ export interface RestManager { * @see {@link https://docs.discord.com/developers/resources/channel#get-channel-messages} */ getMessages: (channelId: BigString, options?: GetMessagesOptions) => Promise[]>; + /** + * Search messages in a guild. + * + * @remarks + * Requires `READ_MESSAGE_HISTORY` and is restricted by the `MESSAGE_CONTENT` intent. + * + * @see {@link https://docs.discord.com/developers/resources/message#search-guild-messages} + */ + searchGuildMessages: ( + guildId: BigString, + options?: SearchGuildMessagesOptions, + ) => Promise>; /** * Returns a sticker pack for the given ID. * diff --git a/packages/rest/src/typings/routes.ts b/packages/rest/src/typings/routes.ts index ed066ce74..c97727c66 100644 --- a/packages/rest/src/typings/routes.ts +++ b/packages/rest/src/typings/routes.ts @@ -18,6 +18,7 @@ import type { ListGuildMembers, ListSkuSubscriptionsOptions, ListThreadMembers, + SearchGuildMessagesOptions, } from '@discordeno/types'; export interface RestRoutes { @@ -166,6 +167,8 @@ export interface RestRoutes { invites: (guildId: BigString) => string; /** Route for handling a bot leaving a guild. */ leave: (guildId: BigString) => string; + /** Route for searching a guild's messages. */ + messagesSearch: (guildId: BigString, options?: SearchGuildMessagesOptions) => string; /** Route for handling a guild's preview. */ preview: (guildId: BigString) => string; /** Route for handling pruning of a guild. */ diff --git a/packages/types/src/discord/message.ts b/packages/types/src/discord/message.ts index 294b1bdcf..cc9a1557b 100644 --- a/packages/types/src/discord/message.ts +++ b/packages/types/src/discord/message.ts @@ -11,6 +11,7 @@ import type { DiscordMessageInteraction, InteractionTypes, } from './interactions.js'; +import type { HTTPJsonErrorCodes } from './opcodes.js'; import type { DiscordPoll } from './poll.js'; import type { DiscordSticker, DiscordStickerItem } from './sticker.js'; import type { DiscordUser } from './user.js'; @@ -115,6 +116,30 @@ export interface DiscordMessage extends Partial { shared_client_theme?: DiscordSharedClientTheme; } +/** https://docs.discord.com/developers/resources/message#search-guild-messages-response-body */ +export interface DiscordSearchGuildMessages { + /** Whether the guild is undergoing a deep historical indexing operation */ + doing_deep_historical_index: boolean; + /** The number of documents indexed during the current index operation, if any */ + documents_indexed?: number; + /** The total number of results that match the query */ + total_results: number; + /** Nested messages that match the query; surrounding context is no longer returned */ + messages: DiscordMessage[][]; + /** The threads that contain the returned messages */ + threads?: DiscordChannel[]; + /** A thread member object for each returned thread the current user has joined */ + members?: DiscordThreadMember[]; +} + +/** https://docs.discord.com/developers/resources/message#search-guild-messages */ +export interface DiscordSearchGuildMessagesIndexing { + message: string; + code: HTTPJsonErrorCodes.IndexNotAvailable; + documents_indexed: number; + retry_after: number; +} + /** https://docs.discord.com/developers/resources/message#message-object-message-types */ export enum MessageTypes { Default, diff --git a/packages/types/src/discord/opcodes.ts b/packages/types/src/discord/opcodes.ts index 4e754e2f2..e08f78d8f 100644 --- a/packages/types/src/discord/opcodes.ts +++ b/packages/types/src/discord/opcodes.ts @@ -467,6 +467,8 @@ export enum HTTPJsonErrorCodes { /** User cannot use burst reactions */ NoBurstReactions = 90002, + /** Index not yet available. Try again later */ + IndexNotAvailable = 110000, /** Application not yet available. Try again later */ AppNotAvailable = 110001, diff --git a/packages/types/src/discordeno/message.ts b/packages/types/src/discordeno/message.ts index 7d19a6827..434fa93a6 100644 --- a/packages/types/src/discordeno/message.ts +++ b/packages/types/src/discordeno/message.ts @@ -117,6 +117,136 @@ export interface GetMessagesAfter extends GetMessagesLimit { /** https://docs.discord.com/developers/resources/channel#get-channel-messages-query-string-params */ export type GetMessagesOptions = GetMessagesAfter | GetMessagesBefore | GetMessagesAround | GetMessagesLimit; +/** https://docs.discord.com/developers/resources/message#search-guild-messages-query-string-params */ +export enum SearchGuildMessagesAuthorType { + /** Return messages sent by user accounts */ + User = 'user', + /** Return messages sent by bot accounts */ + Bot = 'bot', + /** Return messages sent by webhooks */ + Webhook = 'webhook', + /** Exclude messages sent by user accounts */ + ExcludeUser = '-user', + /** Exclude messages sent by bot accounts */ + ExcludeBot = '-bot', + /** Exclude messages sent by webhooks */ + ExcludeWebhook = '-webhook', +} + +/** https://docs.discord.com/developers/resources/message#search-guild-messages-query-string-params */ +export enum SearchGuildMessagesHas { + /** Return messages that have an image */ + Image = 'image', + /** Return messages that have a sound attachment */ + Sound = 'sound', + /** Return messages that have a video */ + Video = 'video', + /** Return messages that have an attachment */ + File = 'file', + /** Return messages that have a sent sticker */ + Sticker = 'sticker', + /** Return messages that have an embed */ + Embed = 'embed', + /** Return messages that have a link */ + Link = 'link', + /** Return messages that have a poll */ + Poll = 'poll', + /** Return messages that have a forwarded message */ + Snapshot = 'snapshot', + /** Exclude messages that have an image */ + ExcludeImage = '-image', + /** Exclude messages that have a sound attachment */ + ExcludeSound = '-sound', + /** Exclude messages that have a video */ + ExcludeVideo = '-video', + /** Exclude messages that have an attachment */ + ExcludeFile = '-file', + /** Exclude messages that have a sent sticker */ + ExcludeSticker = '-sticker', + /** Exclude messages that have an embed */ + ExcludeEmbed = '-embed', + /** Exclude messages that have a link */ + ExcludeLink = '-link', + /** Exclude messages that have a poll */ + ExcludePoll = '-poll', + /** Exclude messages that have a forwarded message */ + ExcludeSnapshot = '-snapshot', +} + +/** https://docs.discord.com/developers/resources/message#search-guild-messages-query-string-params */ +export enum SearchGuildMessagesEmbedType { + /** Return messages that have an image embed */ + Image = 'image', + /** Return messages that have a video embed */ + Video = 'video', + /** Return messages that have a gifv embed */ + Gif = 'gif', + /** Return messages that have a sound embed */ + Sound = 'sound', + /** Return messages that have an article embed */ + Article = 'article', +} + +/** https://docs.discord.com/developers/resources/message#search-guild-messages-query-string-params */ +export enum SearchGuildMessagesSortBy { + /** Sort by message creation time (default) */ + Timestamp = 'timestamp', + /** Sort by relevance to the search query */ + Relevance = 'relevance', +} + +/** https://docs.discord.com/developers/resources/message#search-guild-messages-query-string-params */ +export interface SearchGuildMessagesOptions { + /** Max number of messages to return (1-25, default 25) */ + limit?: number; + /** Number to offset the returned messages by (max 9975) */ + offset?: number; + /** Get messages before this message ID */ + maxId?: BigString; + /** Get messages after this message ID */ + minId?: BigString; + /** Max number of words to skip between matching tokens in the search content (max 100, default 2) */ + slop?: number; + /** Filter messages by content (max 1024 characters) */ + content?: string; + /** Filter messages by these channels (max 500) */ + channelId?: BigString[]; + /** Filter messages by author type */ + authorType?: SearchGuildMessagesAuthorType[]; + /** Filter messages by these authors (max 100) */ + authorId?: BigString[]; + /** Filter messages that mention these users (max 100) */ + mentions?: BigString[]; + /** Filter messages that mention these roles (max 100) */ + mentionsRoleId?: BigString[]; + /** Filter messages by whether they do or do not mention @everyone */ + mentionEveryone?: boolean; + /** Filter messages that reply to these users (max 100) */ + repliedToUserId?: BigString[]; + /** Filter messages that reply to these messages (max 100) */ + repliedToMessageId?: BigString[]; + /** Filter messages by whether they are or are not pinned */ + pinned?: boolean; + /** Filter messages by whether they have or do not have specific things */ + has?: SearchGuildMessagesHas[]; + /** Filter messages by embed type */ + embedType?: SearchGuildMessagesEmbedType[]; + /** Filter messages by embed provider (case-sensitive, max 256 characters, max 100) */ + embedProvider?: string[]; + /** Filter messages by link hostname (max 256 characters, max 100) */ + linkHostname?: string[]; + /** Filter messages by attachment filename (max 1024 characters, max 100) */ + attachmentFilename?: string[]; + /** Filter messages by attachment extension (max 256 characters, max 100) */ + attachmentExtension?: string[]; + /** The sorting algorithm to use */ + sortBy?: SearchGuildMessagesSortBy; + /** The direction to sort (default desc); ignored when sorting by relevance */ + sortOrder?: 'asc' | 'desc'; + /** Whether to include results from age-restricted channels (default false) */ + includeNsfw?: boolean; +} + /** https://docs.discord.com/developers/resources/message#create-message-json/form-params */ export interface CreateMessageOptions { /** The message contents (up to 2000 characters) */