feat(rest): add guild message search support (#5233)

* feat(rest): add guild message search support

* fix(types): address guild message search review feedback

* Update packages/rest/src/routes.ts

Co-authored-by: Awesome Stickz <38146668+AwesomeStickz@users.noreply.github.com>

* Update packages/types/src/discord/message.ts

Co-authored-by: Awesome Stickz <38146668+AwesomeStickz@users.noreply.github.com>

* Update packages/rest/src/routes.ts

Co-authored-by: Awesome Stickz <38146668+AwesomeStickz@users.noreply.github.com>

* apply requested changes

---------

Co-authored-by: Fleny <Fleny113@outlook.com>
Co-authored-by: Awesome Stickz <38146668+AwesomeStickz@users.noreply.github.com>
This commit is contained in:
adam
2026-08-02 10:25:05 +02:00
committed by GitHub
co-authored by Awesome Stickz Fleny
parent 4c9522f14d
commit a6b940677e
8 changed files with 219 additions and 2 deletions
+24
View File
@@ -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<TProps extends TransformersDesiredProperties, T
publishMessage: async (channelId, messageId) => {
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<TProps extends TransformersDesiredProperties, TBehavior e
getInvites: (guildId: BigString) => Promise<SetupDesiredProps<Invite, TProps, TBehavior>[]>;
getMessage: (channelId: BigString, messageId: BigString) => Promise<SetupDesiredProps<Message, TProps, TBehavior>>;
getMessages: (channelId: BigString, options?: GetMessagesOptions) => Promise<SetupDesiredProps<Message, TProps, TBehavior>[]>;
searchGuildMessages: (
guildId: BigString,
options?: SearchGuildMessagesOptions,
) => Promise<
| Camelize<DiscordSearchGuildMessagesIndexing>
| (Omit<Camelize<DiscordSearchGuildMessages>, 'messages' | 'threads' | 'members'> & {
messages: SetupDesiredProps<Message, TProps, TBehavior>[][];
threads?: SetupDesiredProps<Channel, TProps, TBehavior>[];
members?: SetupDesiredProps<ThreadMember, TProps, TBehavior>[];
})
>;
getStickerPack: (stickerPackId: BigString) => Promise<SetupDesiredProps<StickerPack, TProps, TBehavior>>;
getStickerPacks: () => Promise<SetupDesiredProps<StickerPack, TProps, TBehavior>[]>;
getOriginalInteractionResponse: (token: string) => Promise<SetupDesiredProps<Message, TProps, TBehavior>>;
+5
View File
@@ -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<DiscordMessage[]>(rest.routes.channels.messages(channelId, options));
},
async searchGuildMessages(guildId, options) {
return await rest.get<DiscordSearchGuildMessages | DiscordSearchGuildMessagesIndexing>(rest.routes.guilds.messagesSearch(guildId, options));
},
async getStickerPack(stickerPackId) {
return await rest.get<DiscordStickerPack>(rest.routes.stickerPack(stickerPackId));
},
+15 -2
View File
@@ -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`;
},
+15
View File
@@ -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<Camelize<DiscordMessage>[]>;
/**
* 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<Camelize<DiscordSearchGuildMessages | DiscordSearchGuildMessagesIndexing>>;
/**
* Returns a sticker pack for the given ID.
*
+3
View File
@@ -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. */
+25
View File
@@ -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<DiscordMessageCreateExtra> {
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,
+2
View File
@@ -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,
+130
View File
@@ -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) */