From aad1630dbfcb2fe698ff25cce967aa27b1cb9fe9 Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:49:53 -0400 Subject: [PATCH 01/18] base message search routes --- discord/http.py | 70 ++++++++++++++++++++++++++++++++++++++++ discord/types/message.py | 50 +++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 2ea80b292b..b6d1ccc7fa 100644 --- a/discord/http.py +++ b/discord/http.py @@ -952,6 +952,76 @@ def legacy_pins_from( Route("GET", "/channels/{channel_id}/pins", channel_id=channel_id) ) + def message_search( + self, + guild_id: Snowflake, + *, + limit: int | None = None, + offset: int | None = None, + min_id: Snowflake | None = None, + max_id: Snowflake | None = None, + slop: int | None = None, + content: str | None = None, + contents: list[str] | None = None, + channel_id: SnowflakeList | None = None, + author_type: message.SearchAuthorTypes | None = None, + author_id: SnowflakeList | None = None, + mentions: SnowflakeList | None = None, + mentions_role_id: SnowflakeList | None = None, + mention_everyone: bool | None = None, + replied_to_user_id: SnowflakeList | None = None, + replied_to_message_id: SnowflakeList | None = None, + pinned: bool | None = None, + has: list[message.SearchHasTypes] | None = None, + embed_type: list[SearchEmbedTypes] | None = None, + embed_provider: list[str] | None = None, + link_hostname: list[str] | None = None, + attachment_filename: list[str] | None = None, + attachment_extension: list[str] | None = None, + sort_by: message.SearchSortModes | None = None, + sort_order: message.SearchSortOrders | None = None, + include_nsfw: bool | None = None, + cursor: dict | None = None, + command_id: Snowflake | None = None, + command_name: str | None = None, + ) -> Response[message.MessageSearchResults]: + + p = { + "limit": limit, + "offset": offset, + "min_id": min_id, + "max_id": max_id, + "slop": slop, + "content": content, + "contents": contents, + "channel_id": channel_id, + "author_type": author_type, + "author_id": author_id, + "mentions": mentions, + "mentions_role_id": mentions_role_id, + "mention_everyone": mention_everyone, + "replied_to_user_id": replied_to_user_id, + "replied_to_message_id": replied_to_message_id, + "pinned": pinned, + "has": has, + "embed_type": embed_type, + "embed_provider": embed_provider, + "link_hostname": link_hostname, + "attachment_filename": attachment_filename, + "attachment_extension": attachment_extension, + "sort_by": sort_by, + "sort_order": sort_order, + "include_nsfw": include_nsfw, + "cursor": cursor, + "command_id": command_id, + "command_name": command_name, + } + params = {k:v for k, v in p.items() if v is not None} + return self.request( + Route("GET", "/guilds/{guild_id}/messages/search", guild_id=guild_id), + params=params + ) + # Member management def kick( diff --git a/discord/types/message.py b/discord/types/message.py index c6a48881c7..dbcc8ca50d 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -35,7 +35,7 @@ from .poll import Poll from .snowflake import Snowflake, SnowflakeList from .sticker import StickerItem -from .threads import Thread +from .threads import Thread, ThreadMember from .user import User if TYPE_CHECKING: @@ -194,3 +194,51 @@ class AllowedMentions(TypedDict): roles: SnowflakeList users: SnowflakeList replied_user: bool + + +SearchAuthorTypes = Literal["user", "bot", "webhook", "-user", "-bot", "-webhook"] +SearchHasTypes = Literal["image", "sound", "video", "file", "sticker", "embed", "link", "poll", "snapshot", "-image", "-sound", "-video", "-file", "-sticker", "-embed", "-link", "-poll", "-snapshot"] +SearchEmbedTypes = Literal["image", "video", "gif", "sound", "article"] +SearchSortModes = Literal["relevance", "timestamp"] +SearchSortOrders = Literal["asc", "desc"] + + +class MessageSearch(TypedDict): + limit: NotRequired[int] + offset: NotRequired[int] + max_id: NotRequired[Snowflake] + min_id: NotRequired[Snowflake] + slop: NotRequired[int] + content: NotRequired[str] + contents: NotRequired[List[str]] + channel_id: NotRequired[SnowflakeList] + author_type: NotRequired[List[SearchAuthorTypes]] + author_id: NotRequired[SnowflakeList] + mentions: NotRequired[SnowflakeList] + mentions_role_id: NotRequired[SnowflakeList] + mention_everyone: NotRequired[bool] + replied_to_user_id: NotRequired[SnowflakeList] + replied_to_message_id: NotRequired[SnowflakeList] + pinned: NotRequired[bool] + has: NotRequired[List[SearchHasTypes]] + embed_type: NotRequired[List[SearchEmbedTypes]] + embed_provider: NotRequired[List[str]] + link_hostname: NotRequired[List[str]] + attachment_filename: NotRequired[List[str]] + attachment_extension: NotRequired[List[str]] + sort_by: NotRequired[SearchSortModes] + sort_order: NotRequired[SearchSortOrders] + include_nsfw: NotRequired[bool] + cursor: NotRequired[dict] + command_id: NotRequired[Snowflake] + command_name: NotRequired[str] + + +class MessageSearchResults(TypedDict): + doing_deep_historical_index: bool + documents_indexed: NotRequired[int] + total_results: int + messages: List[List[Message]] # ????? + threads: NotRequired[List[Thread]] + members: NotRequired[List[ThreadMember]] + \ No newline at end of file From 4d7419c6d2de0906a306a26b64c02beba008cbe0 Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:14:15 -0400 Subject: [PATCH 02/18] main implementation --- discord/abc.py | 2 + discord/enums.py | 36 ++++++++ discord/guild.py | 180 +++++++++++++++++++++++++++++++++++++++ discord/http.py | 8 -- discord/iterators.py | 72 ++++++++++++++++ discord/member.py | 9 ++ discord/message.py | 3 + discord/role.py | 3 + discord/types/message.py | 3 +- 9 files changed, 306 insertions(+), 10 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 8185154475..6b578eeed3 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1316,6 +1316,8 @@ async def invites(self) -> list[Invite]: for invite in data ] + def search(self, **params): + return self.guild.search(channels=[self], **params) class Messageable: """An ABC that details the common operations on a model that can send messages. diff --git a/discord/enums.py b/discord/enums.py index 63557c853b..61fdba2c05 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -84,6 +84,9 @@ "SubscriptionStatus", "SeparatorSpacingSize", "SelectDefaultValueType", + "SearchEmbedType", + "SearchSortMode", + "SearchSortOrder", ) @@ -1131,6 +1134,39 @@ class SelectDefaultValueType(Enum): user = "user" +class SearchEmbedType(Enum): + """The types of media embedded on a message.""" + + image = "image" + video = "video" + gif = "gif" + sound = "sound" + article = "article" + + def __str__(self): + return self.value + + +class SearchSortMode(Enum): + """The sorting algorithm used for message searches.""" + + timestamp = "timestamp" + relevance = "relevance" + + def __str__(self): + return self.value + + +class SearchSortOrder(Enum): + """The order to sort message searches.""" + + asc = "asc" + desc = "desc" + + def __str__(self): + return self.value + + T = TypeVar("T") diff --git a/discord/guild.py b/discord/guild.py index dd6c91fdc1..3fbf645ae9 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -63,6 +63,9 @@ OnboardingMode, ScheduledEventLocationType, ScheduledEventPrivacyLevel, + SearchEmbedType, + SearchSortMode, + SearchSortOrder, SortOrder, VerificationLevel, VideoQualityMode, @@ -80,6 +83,7 @@ BanIterator, EntitlementIterator, MemberIterator, + MessageSearchIterator, ) from .member import Member, VoiceState from .mixins import Hashable @@ -151,6 +155,36 @@ class _GuildLimit(NamedTuple): filesize: int +class Parsable: + # idk this kinda sucks lmao + def __init__(self, **values): + self._values = values + for k, v in values: + setattr(self, k, v) + + def parse(self) -> list[str]: + true, false = [], [] + for k, v in self._values.items(): + if v: + true.append(k) + elif v is False: + false.append(f"-{k}") + return true + false + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} resolved={self.parse()!r}>" + + +class SearchAuthors(Parsable): + def __init__(self, *, user: bool | None = None, bot: bool | None = None, webhook: bool | None = None): + super().__init__(user=user, bot=bot, webhook=webhook) + + +class SearchHas(Parsable): + def __init__(self, *, image: bool | None = None, sound: bool | None = None, video: bool | None = None, file: bool | None = None, sticker: bool | None = None, embed: bool | None = None, link: bool | None = None, poll: bool | None = None, snapshot: bool | None = None): + super().__init__(image=image, sound=sound, video=video, file=file, sticker=sticker, embed=embed, link=link, poll=poll, snapshot=snapshot) + + class GuildRoleCounts(dict[int, int]): """A dictionary subclass that maps role IDs to their member counts. @@ -4723,3 +4757,149 @@ def get_sound(self, sound_id: int) -> SoundboardSound | None: The sound or ``None`` if not found. """ return self._sounds.get(sound_id) + + def search( + self, + *, + limit: int | None = 25, + offset: int | None = None, + after: Snowflake | None = None, + before: Snowflake | None = None, + slop: int | None = 2, + content: str | None = None, + channels: list[Snowflake] | None = None, + author_types: SearchAuthors | None = None, + authors: list[Snowflake] | None = None, + mentions: list[Snowflake] | None = None, + mentions_roles: list[Snowflake] | None = None, + mention_everyone: bool | None = None, + replied_to_users: list[Snowflake] | None = None, + replied_to_messages: list[Snowflake] | None = None, + pinned: bool | None = None, + has: SearchHas | None = None, + embed_types: list[SearchEmbedType] | None = None, + embed_providers: list[str] | None = None, + link_hostnames: list[str] | None = None, + attachment_filenames: list[str] | None = None, + attachment_extensions: list[str] | None = None, + sort_by: SearchSortMode | None = None, + sort_order: SearchSortOrder | None = SearchSortOrder.desc, + include_nsfw: bool | None = False, + ) -> list[Message]: + """etc...""" + + params = {} + + if limit: + if limit <= 0: + raise ValueError("limit must be above 1") + params["limit"] = limit if limit <= 25 else limit + + if offset is not None: + if offset > 9975 or offset < 0: + raise ValueError("offset must be between 0 and 9975") + params["offset"] = offset + + if after: + params["min_id"] = after.id + + if before: + params["max_id"] = before.id + + if slop is not None: + if not 0 >= slop >= 100: + raise ValueError("slop must be between 0 and 100") + params["slop"] = int(slop) + + if content: + if len(content) > 1024: + raise ValueError("content must be under 1024 characters") + params["content"] = content + + if channels: + if len(channels) > 500: + raise ValueError("can only specify up to 500 channels") + params["channel_id"] = [c.id for c in channels] + + if author_types: + param["author_type"] = author_types.parse() + + if authors: + if len(authors) > 100: + raise ValueError("can only specify up to 100 authors") + param["author_id"] = [a.id for a in authors] + + if mentions: + if len(mentions) > 100: + raise ValueError("can only specify up to 100 mentions") + params["mentions"] = [m.id for m in mentions] + + if mentions_roles: + if len(mentions_roles) > 100: + raise ValueError("can only specify up to 100 mentions_roles") + params["mentions_role_id"] = [m.id for m in mentions_roles] + + if mention_everyone is not None: + params["mention_everyone"] = mention_everyone + + if replied_to_users: + if len(replied_to_users) > 100: + raise ValueError("can only specify up to 100 replied_to_users") + params["replied_to_user_id"] = [u.id for u in replied_to_users] + + if replied_to_messages: + if len(replied_to_messages) > 100: + raise ValueError("can only specify up to 100 replied_to_messages") + params["replied_to_message_id"] = [m.id for u in replied_to_messages] + + if pinned is not None: + params["pinned"] = pinned + + if has: + params["has"] = has.parse() + + if embed_types: + params["embed_type"] = [str(t) for t in embed_types] + + if embed_providers: + if len(embed_providers) > 100: + raise ValueError("can only specify up to 100 embed_providers") + for e in embed_providers: + if len(e) > 256: + raise ValueError(f"embed_provider {e!r} must be up to 256 characters.") + params["embed_provider"] = embed_providers + + if link_hostnames: + if len(link_hostnames) > 100: + raise ValueError("can only specify up to 100 link_hostnames") + for l in link_hostnames: + if len(l) > 256: + raise ValueError(f"link_hostname {l!r} must be up to 256 characters.") + params["link_hostname"] = link_hostnames + + if attachment_filenames: + if len(attachment_filenames) > 100: + raise ValueError("can only specify up to 100 attachment_filenames") + for a in attachment_filenames: + if len(a) > 1024: + raise ValueError(f"attachment_filename {a!r} must be up to 1024 characters.") + params["attachment_filename"] = attachment_filenames + + if attachment_extensions: + if len(attachment_extensions) > 100: + raise ValueError("can only specify up to 100 attachment_extensions") + for a in attachment_extensions: + if len(a) > 256: + raise ValueError(f"attachment_extension {a!r} must be up to 256 characters.") + params["attachment_extensions"] = attachment_extensions + + if sort_by: + params["sort_by"] = str(sort_by) + + if sort_order: + params["sort_order"] = str(sort_order) + + if include_nsfw is not None: + params["include_nsfw"] = include_nsfw + + return MessageSearchIterator(self, limit, params) \ No newline at end of file diff --git a/discord/http.py b/discord/http.py index b6d1ccc7fa..a7c0e32b22 100644 --- a/discord/http.py +++ b/discord/http.py @@ -962,7 +962,6 @@ def message_search( max_id: Snowflake | None = None, slop: int | None = None, content: str | None = None, - contents: list[str] | None = None, channel_id: SnowflakeList | None = None, author_type: message.SearchAuthorTypes | None = None, author_id: SnowflakeList | None = None, @@ -981,9 +980,6 @@ def message_search( sort_by: message.SearchSortModes | None = None, sort_order: message.SearchSortOrders | None = None, include_nsfw: bool | None = None, - cursor: dict | None = None, - command_id: Snowflake | None = None, - command_name: str | None = None, ) -> Response[message.MessageSearchResults]: p = { @@ -993,7 +989,6 @@ def message_search( "max_id": max_id, "slop": slop, "content": content, - "contents": contents, "channel_id": channel_id, "author_type": author_type, "author_id": author_id, @@ -1012,9 +1007,6 @@ def message_search( "sort_by": sort_by, "sort_order": sort_order, "include_nsfw": include_nsfw, - "cursor": cursor, - "command_id": command_id, - "command_name": command_name, } params = {k:v for k, v in p.items() if v is not None} return self.request( diff --git a/discord/iterators.py b/discord/iterators.py index b074aefdc4..3ab2e433cb 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -50,6 +50,7 @@ "AuditLogIterator", "GuildIterator", "MemberIterator", + "MessageSearchIterator", "ScheduledEventSubscribersIterator", "EntitlementIterator", "SubscriptionIterator", @@ -68,6 +69,7 @@ from .types.guild import Guild as GuildPayload from .types.message import Message as MessagePayload from .types.message import MessagePin as MessagePinPayload + from .types.message import MessageSearch as MessageSearchPayload from .types.monetization import Entitlement as EntitlementPayload from .types.monetization import Subscription as SubscriptionPayload from .types.threads import Thread as ThreadPayload @@ -1283,3 +1285,73 @@ def __await__(self) -> Generator[Any, Any, MessagePin]: reference="The documentation of pins()", ) return self.retrieve_inner().__await__() + + +class MessageSearchIterator(_AsyncIterator["Message"]): + """Iterator for receiving a guild's search results. + """ + + def __init__( + self, + guild, + limit, + params, + ): + self.guild = guild + self.limit = limit + self.params = params + + self.state = self.guild._state + self.search = self.state.http.message_search + self.messages = asyncio.Queue() + self.message_ids = [] + + async def next(self) -> Message: + if self.messages.empty(): + await self.fill_messages() + + try: + return self.messages.get_nowait() + except asyncio.QueueEmpty: + raise NoMoreItems() + + def _get_retrieve(self) -> bool: + l = self.limit + if l is None or l > 25: + r = 25 + else: + r = l + self.retrieve = r + return r > 0 + + async def fill_messages(self): + + if self._get_retrieve(): + data = await self._retrieve_messages(self.retrieve) + if not data["messages"] + # "Clients should not rely on the length of the `messages` array to paginate results" + self.limit = 0 # terminate the infinite loop + + threads = data["threads"] + members = data["members"] # do something here + + for element in data["messages"]: + if int(element["id"]) not in self.message_ids: + ch = self.guild.get_channel(int(element["channel_id"])) + channel = await ch._get_channel() + await self.messages.put( + self.state.create_message(channel=channel, data=element) + ) + self.message_ids.append(int(element["id"])) + + async def _retrieve_messages( + self, retrieve: int + ) -> list[MessagePayload]: + data: list[MessageSearchPayload] = await self.search( + self.guild.id, **self.params + ) + self.params["offset"] = self.params.get("offset", 0) + retrieve + if data["messages"]: + if self.limit is not None: + self.limit -= retrieve + return data diff --git a/discord/member.py b/discord/member.py index 88e045b54a..5bdc0128f6 100644 --- a/discord/member.py +++ b/discord/member.py @@ -1261,3 +1261,12 @@ def get_role(self, role_id: int, /) -> Role | None: The role or ``None`` if not found in the member's roles. """ return self.guild.get_role(role_id) if self._roles.has(role_id) else None + + def search_messages(self, **params): + return self.guild.search(authors=[self], **params) + + def search_replies(self, **params): + return self.guild.search(replied_to_users=[self], **params) + + def search_mentions(self, **params): + return self.guild.search(mentions=[self], **params) diff --git a/discord/message.py b/discord/message.py index b59d78c89a..389b7f04b6 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2259,6 +2259,9 @@ async def end_poll(self) -> Message: return message + def search_replies(self, **params): + return self.guild.search(replied_to_messages=[self], **params) + def to_reference( self, *, fail_if_not_exists: bool = True, type: MessageReferenceType = None ) -> MessageReference: diff --git a/discord/role.py b/discord/role.py index 44f4d1d1d5..562b0e9f61 100644 --- a/discord/role.py +++ b/discord/role.py @@ -728,3 +728,6 @@ async def delete(self, *, reason: str | None = None) -> None: """ await self._state.http.delete_role(self.guild.id, self.id, reason=reason) + + def search_mentions(self, **params): + return self.guild.search(mentions_roles=[self], **params) diff --git a/discord/types/message.py b/discord/types/message.py index dbcc8ca50d..6b0da61baa 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -210,7 +210,6 @@ class MessageSearch(TypedDict): min_id: NotRequired[Snowflake] slop: NotRequired[int] content: NotRequired[str] - contents: NotRequired[List[str]] channel_id: NotRequired[SnowflakeList] author_type: NotRequired[List[SearchAuthorTypes]] author_id: NotRequired[SnowflakeList] @@ -232,6 +231,7 @@ class MessageSearch(TypedDict): cursor: NotRequired[dict] command_id: NotRequired[Snowflake] command_name: NotRequired[str] + contents: NotRequired[List[str]] class MessageSearchResults(TypedDict): @@ -241,4 +241,3 @@ class MessageSearchResults(TypedDict): messages: List[List[Message]] # ????? threads: NotRequired[List[Thread]] members: NotRequired[List[ThreadMember]] - \ No newline at end of file From fa3e0ec05cf5189a1b3c0caccff709e42ee7252b Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:25:27 -0400 Subject: [PATCH 03/18] items --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 3fbf645ae9..6d708a80b3 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -159,7 +159,7 @@ class Parsable: # idk this kinda sucks lmao def __init__(self, **values): self._values = values - for k, v in values: + for k, v in values.items(): setattr(self, k, v) def parse(self) -> list[str]: From 7c755b8ad55ce6e9a35210e94ad1323c0522f76f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:32:50 +0000 Subject: [PATCH 04/18] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/abc.py | 1 + discord/enums.py | 4 +-- discord/guild.py | 70 ++++++++++++++++++++++++++++++---------- discord/http.py | 10 +++--- discord/iterators.py | 5 ++- discord/types/message.py | 21 +++++++++++- 6 files changed, 83 insertions(+), 28 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 213458b022..dbe5ffa07b 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1341,6 +1341,7 @@ async def invites(self) -> list[Invite]: def search(self, **params): return self.guild.search(channels=[self], **params) + class Messageable: """An ABC that details the common operations on a model that can send messages. diff --git a/discord/enums.py b/discord/enums.py index 181c0b92b9..dd11ce7a4a 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1170,8 +1170,8 @@ class SearchSortOrder(Enum): def __str__(self): return self.value - - + + class RoleType(IntEnum): """Represents the type of role. diff --git a/discord/guild.py b/discord/guild.py index b3c11761bf..46899ce0cf 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -176,13 +176,41 @@ def __repr__(self) -> str: class SearchAuthors(Parsable): - def __init__(self, *, user: bool | None = None, bot: bool | None = None, webhook: bool | None = None): + def __init__( + self, + *, + user: bool | None = None, + bot: bool | None = None, + webhook: bool | None = None, + ): super().__init__(user=user, bot=bot, webhook=webhook) class SearchHas(Parsable): - def __init__(self, *, image: bool | None = None, sound: bool | None = None, video: bool | None = None, file: bool | None = None, sticker: bool | None = None, embed: bool | None = None, link: bool | None = None, poll: bool | None = None, snapshot: bool | None = None): - super().__init__(image=image, sound=sound, video=video, file=file, sticker=sticker, embed=embed, link=link, poll=poll, snapshot=snapshot) + def __init__( + self, + *, + image: bool | None = None, + sound: bool | None = None, + video: bool | None = None, + file: bool | None = None, + sticker: bool | None = None, + embed: bool | None = None, + link: bool | None = None, + poll: bool | None = None, + snapshot: bool | None = None, + ): + super().__init__( + image=image, + sound=sound, + video=video, + file=file, + sticker=sticker, + embed=embed, + link=link, + poll=poll, + snapshot=snapshot, + ) class GuildRoleCounts(dict[int, int]): @@ -4760,10 +4788,10 @@ def get_sound(self, sound_id: int) -> SoundboardSound | None: def search( self, - *, + *, limit: int | None = 25, offset: int | None = None, - after: Snowflake | None = None, + after: Snowflake | None = None, before: Snowflake | None = None, slop: int | None = 2, content: str | None = None, @@ -4782,27 +4810,27 @@ def search( link_hostnames: list[str] | None = None, attachment_filenames: list[str] | None = None, attachment_extensions: list[str] | None = None, - sort_by: SearchSortMode | None = None, + sort_by: SearchSortMode | None = None, sort_order: SearchSortOrder | None = SearchSortOrder.desc, include_nsfw: bool | None = False, ) -> list[Message]: """etc...""" - + params = {} - + if limit: if limit <= 0: raise ValueError("limit must be above 1") - params["limit"] = limit if limit <= 25 else limit + params["limit"] = limit if limit <= 25 else limit if offset is not None: if offset > 9975 or offset < 0: raise ValueError("offset must be between 0 and 9975") params["offset"] = offset - + if after: params["min_id"] = after.id - + if before: params["max_id"] = before.id @@ -4815,7 +4843,7 @@ def search( if len(content) > 1024: raise ValueError("content must be under 1024 characters") params["content"] = content - + if channels: if len(channels) > 500: raise ValueError("can only specify up to 500 channels") @@ -4866,7 +4894,9 @@ def search( raise ValueError("can only specify up to 100 embed_providers") for e in embed_providers: if len(e) > 256: - raise ValueError(f"embed_provider {e!r} must be up to 256 characters.") + raise ValueError( + f"embed_provider {e!r} must be up to 256 characters." + ) params["embed_provider"] = embed_providers if link_hostnames: @@ -4874,7 +4904,9 @@ def search( raise ValueError("can only specify up to 100 link_hostnames") for l in link_hostnames: if len(l) > 256: - raise ValueError(f"link_hostname {l!r} must be up to 256 characters.") + raise ValueError( + f"link_hostname {l!r} must be up to 256 characters." + ) params["link_hostname"] = link_hostnames if attachment_filenames: @@ -4882,7 +4914,9 @@ def search( raise ValueError("can only specify up to 100 attachment_filenames") for a in attachment_filenames: if len(a) > 1024: - raise ValueError(f"attachment_filename {a!r} must be up to 1024 characters.") + raise ValueError( + f"attachment_filename {a!r} must be up to 1024 characters." + ) params["attachment_filename"] = attachment_filenames if attachment_extensions: @@ -4890,7 +4924,9 @@ def search( raise ValueError("can only specify up to 100 attachment_extensions") for a in attachment_extensions: if len(a) > 256: - raise ValueError(f"attachment_extension {a!r} must be up to 256 characters.") + raise ValueError( + f"attachment_extension {a!r} must be up to 256 characters." + ) params["attachment_extensions"] = attachment_extensions if sort_by: @@ -4902,4 +4938,4 @@ def search( if include_nsfw is not None: params["include_nsfw"] = include_nsfw - return MessageSearchIterator(self, limit, params) \ No newline at end of file + return MessageSearchIterator(self, limit, params) diff --git a/discord/http.py b/discord/http.py index 29727168e0..bf2d066c0f 100644 --- a/discord/http.py +++ b/discord/http.py @@ -961,10 +961,10 @@ def legacy_pins_from( def message_search( self, guild_id: Snowflake, - *, + *, limit: int | None = None, offset: int | None = None, - min_id: Snowflake | None = None, + min_id: Snowflake | None = None, max_id: Snowflake | None = None, slop: int | None = None, content: str | None = None, @@ -983,7 +983,7 @@ def message_search( link_hostname: list[str] | None = None, attachment_filename: list[str] | None = None, attachment_extension: list[str] | None = None, - sort_by: message.SearchSortModes | None = None, + sort_by: message.SearchSortModes | None = None, sort_order: message.SearchSortOrders | None = None, include_nsfw: bool | None = None, ) -> Response[message.MessageSearchResults]: @@ -1014,10 +1014,10 @@ def message_search( "sort_order": sort_order, "include_nsfw": include_nsfw, } - params = {k:v for k, v in p.items() if v is not None} + params = {k: v for k, v in p.items() if v is not None} return self.request( Route("GET", "/guilds/{guild_id}/messages/search", guild_id=guild_id), - params=params + params=params, ) # Member management diff --git a/discord/iterators.py b/discord/iterators.py index 3ab2e433cb..7cf3ef8b53 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -1288,8 +1288,7 @@ def __await__(self) -> Generator[Any, Any, MessagePin]: class MessageSearchIterator(_AsyncIterator["Message"]): - """Iterator for receiving a guild's search results. - """ + """Iterator for receiving a guild's search results.""" def __init__( self, @@ -1322,7 +1321,7 @@ def _get_retrieve(self) -> bool: else: r = l self.retrieve = r - return r > 0 + return r > 0 async def fill_messages(self): diff --git a/discord/types/message.py b/discord/types/message.py index 6b0da61baa..968c9ebee3 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -197,7 +197,26 @@ class AllowedMentions(TypedDict): SearchAuthorTypes = Literal["user", "bot", "webhook", "-user", "-bot", "-webhook"] -SearchHasTypes = Literal["image", "sound", "video", "file", "sticker", "embed", "link", "poll", "snapshot", "-image", "-sound", "-video", "-file", "-sticker", "-embed", "-link", "-poll", "-snapshot"] +SearchHasTypes = Literal[ + "image", + "sound", + "video", + "file", + "sticker", + "embed", + "link", + "poll", + "snapshot", + "-image", + "-sound", + "-video", + "-file", + "-sticker", + "-embed", + "-link", + "-poll", + "-snapshot", +] SearchEmbedTypes = Literal["image", "video", "gif", "sound", "article"] SearchSortModes = Literal["relevance", "timestamp"] SearchSortOrders = Literal["asc", "desc"] From e8d340d16be11dcf59342fb2d3985a36c844dc7b Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:35:09 -0400 Subject: [PATCH 05/18] : --- discord/iterators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/iterators.py b/discord/iterators.py index 7cf3ef8b53..a8c4f111d0 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -1327,7 +1327,7 @@ async def fill_messages(self): if self._get_retrieve(): data = await self._retrieve_messages(self.retrieve) - if not data["messages"] + if not data["messages"]: # "Clients should not rely on the length of the `messages` array to paginate results" self.limit = 0 # terminate the infinite loop From 93f6dbd00b3213282707286c9e97a7b7974ea86e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:35:49 +0000 Subject: [PATCH 06/18] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/iterators.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/discord/iterators.py b/discord/iterators.py index a8c4f111d0..60731dfb34 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -1328,10 +1328,10 @@ async def fill_messages(self): if self._get_retrieve(): data = await self._retrieve_messages(self.retrieve) if not data["messages"]: - # "Clients should not rely on the length of the `messages` array to paginate results" + # "Clients should not rely on the length of the `messages` array to paginate results" self.limit = 0 # terminate the infinite loop - threads = data["threads"] + data["threads"] members = data["members"] # do something here for element in data["messages"]: @@ -1343,9 +1343,7 @@ async def fill_messages(self): ) self.message_ids.append(int(element["id"])) - async def _retrieve_messages( - self, retrieve: int - ) -> list[MessagePayload]: + async def _retrieve_messages(self, retrieve: int) -> list[MessagePayload]: data: list[MessageSearchPayload] = await self.search( self.guild.id, **self.params ) From 7ad01985b495d938293dc866f52509304e582d95 Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:39:10 -0400 Subject: [PATCH 07/18] slop --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 46899ce0cf..10d4dfa8ab 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4835,7 +4835,7 @@ def search( params["max_id"] = before.id if slop is not None: - if not 0 >= slop >= 100: + if slop > 100 or slop < 0: raise ValueError("slop must be between 0 and 100") params["slop"] = int(slop) From 32b65bc0619748b8ec77f7826ef0bcab5324a6c5 Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:46:48 -0400 Subject: [PATCH 08/18] fixes --- discord/http.py | 2 +- discord/iterators.py | 9 +++++---- discord/types/message.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/discord/http.py b/discord/http.py index bf2d066c0f..c19c9d1a3b 100644 --- a/discord/http.py +++ b/discord/http.py @@ -978,7 +978,7 @@ def message_search( replied_to_message_id: SnowflakeList | None = None, pinned: bool | None = None, has: list[message.SearchHasTypes] | None = None, - embed_type: list[SearchEmbedTypes] | None = None, + embed_type: list[message.SearchEmbedTypes] | None = None, embed_provider: list[str] | None = None, link_hostname: list[str] | None = None, attachment_filename: list[str] | None = None, diff --git a/discord/iterators.py b/discord/iterators.py index 60731dfb34..a9b3b8a5d4 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -1335,13 +1335,14 @@ async def fill_messages(self): members = data["members"] # do something here for element in data["messages"]: - if int(element["id"]) not in self.message_ids: - ch = self.guild.get_channel(int(element["channel_id"])) + message = element[0] + if int(message["id"]) not in self.message_ids: + ch = self.guild.get_channel(int(message["channel_id"])) channel = await ch._get_channel() await self.messages.put( - self.state.create_message(channel=channel, data=element) + self.state.create_message(channel=channel, data=message) ) - self.message_ids.append(int(element["id"])) + self.message_ids.append(int(message["id"])) async def _retrieve_messages(self, retrieve: int) -> list[MessagePayload]: data: list[MessageSearchPayload] = await self.search( diff --git a/discord/types/message.py b/discord/types/message.py index 968c9ebee3..fccf11a4bf 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -25,7 +25,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, List from .channel import ChannelType from .components import Component From 0f5d1102a4c2f3e5a39dca5ff0fda4231f20c53d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:47:18 +0000 Subject: [PATCH 09/18] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/types/message.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/discord/types/message.py b/discord/types/message.py index fccf11a4bf..b99e20043a 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -25,7 +25,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, List +from typing import TYPE_CHECKING, List, Literal from .channel import ChannelType from .components import Component @@ -230,7 +230,7 @@ class MessageSearch(TypedDict): slop: NotRequired[int] content: NotRequired[str] channel_id: NotRequired[SnowflakeList] - author_type: NotRequired[List[SearchAuthorTypes]] + author_type: NotRequired[list[SearchAuthorTypes]] author_id: NotRequired[SnowflakeList] mentions: NotRequired[SnowflakeList] mentions_role_id: NotRequired[SnowflakeList] @@ -238,25 +238,25 @@ class MessageSearch(TypedDict): replied_to_user_id: NotRequired[SnowflakeList] replied_to_message_id: NotRequired[SnowflakeList] pinned: NotRequired[bool] - has: NotRequired[List[SearchHasTypes]] - embed_type: NotRequired[List[SearchEmbedTypes]] - embed_provider: NotRequired[List[str]] - link_hostname: NotRequired[List[str]] - attachment_filename: NotRequired[List[str]] - attachment_extension: NotRequired[List[str]] + has: NotRequired[list[SearchHasTypes]] + embed_type: NotRequired[list[SearchEmbedTypes]] + embed_provider: NotRequired[list[str]] + link_hostname: NotRequired[list[str]] + attachment_filename: NotRequired[list[str]] + attachment_extension: NotRequired[list[str]] sort_by: NotRequired[SearchSortModes] sort_order: NotRequired[SearchSortOrders] include_nsfw: NotRequired[bool] cursor: NotRequired[dict] command_id: NotRequired[Snowflake] command_name: NotRequired[str] - contents: NotRequired[List[str]] + contents: NotRequired[list[str]] class MessageSearchResults(TypedDict): doing_deep_historical_index: bool documents_indexed: NotRequired[int] total_results: int - messages: List[List[Message]] # ????? - threads: NotRequired[List[Thread]] - members: NotRequired[List[ThreadMember]] + messages: list[list[Message]] # ????? + threads: NotRequired[list[Thread]] + members: NotRequired[list[ThreadMember]] From 4c22f000405bc766fcf21596f88fbe6aa457328d Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:56:16 -0400 Subject: [PATCH 10/18] bool to int --- discord/http.py | 6 +++--- discord/iterators.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/discord/http.py b/discord/http.py index c19c9d1a3b..5d8499ae56 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1000,10 +1000,10 @@ def message_search( "author_id": author_id, "mentions": mentions, "mentions_role_id": mentions_role_id, - "mention_everyone": mention_everyone, + "mention_everyone": bool(mention_everyone), "replied_to_user_id": replied_to_user_id, "replied_to_message_id": replied_to_message_id, - "pinned": pinned, + "pinned": bool(pinned), "has": has, "embed_type": embed_type, "embed_provider": embed_provider, @@ -1012,7 +1012,7 @@ def message_search( "attachment_extension": attachment_extension, "sort_by": sort_by, "sort_order": sort_order, - "include_nsfw": include_nsfw, + "include_nsfw": bool(include_nsfw), } params = {k: v for k, v in p.items() if v is not None} return self.request( diff --git a/discord/iterators.py b/discord/iterators.py index a9b3b8a5d4..f73c8bd5dd 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -1331,8 +1331,8 @@ async def fill_messages(self): # "Clients should not rely on the length of the `messages` array to paginate results" self.limit = 0 # terminate the infinite loop - data["threads"] - members = data["members"] # do something here + threads = data.get("threads", []) + members = data.get("members", []) # do something here for element in data["messages"]: message = element[0] From 875b1be9a27ffb23c0afc4c7dbd73edc355f9158 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:56:47 +0000 Subject: [PATCH 11/18] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/iterators.py | 2 +- discord/types/message.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/iterators.py b/discord/iterators.py index f73c8bd5dd..8142d7ae5f 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -1331,7 +1331,7 @@ async def fill_messages(self): # "Clients should not rely on the length of the `messages` array to paginate results" self.limit = 0 # terminate the infinite loop - threads = data.get("threads", []) + data.get("threads", []) members = data.get("members", []) # do something here for element in data["messages"]: diff --git a/discord/types/message.py b/discord/types/message.py index b99e20043a..33365fbaef 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -25,7 +25,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Literal +from typing import TYPE_CHECKING, Literal from .channel import ChannelType from .components import Component From f9011642bb13d98d776e2397d36908c6a5625e9c Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:00:21 -0400 Subject: [PATCH 12/18] i can read --- discord/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/http.py b/discord/http.py index 5d8499ae56..8cd1e285a4 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1000,10 +1000,10 @@ def message_search( "author_id": author_id, "mentions": mentions, "mentions_role_id": mentions_role_id, - "mention_everyone": bool(mention_everyone), + "mention_everyone": int(mention_everyone) if mention_everyone is not None else None, "replied_to_user_id": replied_to_user_id, "replied_to_message_id": replied_to_message_id, - "pinned": bool(pinned), + "pinned": int(pinned) if pinned is not None else None, "has": has, "embed_type": embed_type, "embed_provider": embed_provider, @@ -1012,7 +1012,7 @@ def message_search( "attachment_extension": attachment_extension, "sort_by": sort_by, "sort_order": sort_order, - "include_nsfw": bool(include_nsfw), + "include_nsfw": int(include_nsfw) if include_nsfw is not None else None, } params = {k: v for k, v in p.items() if v is not None} return self.request( From 93bb7feb47e0a6372c21249b2f6a5d121406b54b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:02:20 +0000 Subject: [PATCH 13/18] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 8cd1e285a4..68b5d733c0 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1000,7 +1000,9 @@ def message_search( "author_id": author_id, "mentions": mentions, "mentions_role_id": mentions_role_id, - "mention_everyone": int(mention_everyone) if mention_everyone is not None else None, + "mention_everyone": ( + int(mention_everyone) if mention_everyone is not None else None + ), "replied_to_user_id": replied_to_user_id, "replied_to_message_id": replied_to_message_id, "pinned": int(pinned) if pinned is not None else None, From db40d7f00d1c2904b1dbe2951b216bdfe5546f92 Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:04:31 -0400 Subject: [PATCH 14/18] handle large limit --- discord/iterators.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/iterators.py b/discord/iterators.py index 8142d7ae5f..2959dd407b 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -1299,6 +1299,9 @@ def __init__( self.guild = guild self.limit = limit self.params = params + if "limit" in params: + if params["limit"] is None or params["limit"] > 25: + params["limit"] = 25 self.state = self.guild._state self.search = self.state.http.message_search From 05d5ece1172028b02f1ee45db326eca814b9a086 Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:11:47 -0400 Subject: [PATCH 15/18] all --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 10d4dfa8ab..1c3333fb1a 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -101,7 +101,7 @@ from .welcome_screen import WelcomeScreen, WelcomeScreenChannel from .widget import Widget -__all__ = ("BanEntry", "Guild", "GuildRoleCounts") +__all__ = ("BanEntry", "Guild", "GuildRoleCounts", "SearchHas", "SearchAuthors") MISSING = utils.MISSING From 7188ef95a8ef1caf458c04aaaf85f656c95135c3 Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:12:45 -0400 Subject: [PATCH 16/18] params --- discord/guild.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 1c3333fb1a..0a816445b8 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4850,12 +4850,12 @@ def search( params["channel_id"] = [c.id for c in channels] if author_types: - param["author_type"] = author_types.parse() + params["author_type"] = author_types.parse() if authors: if len(authors) > 100: raise ValueError("can only specify up to 100 authors") - param["author_id"] = [a.id for a in authors] + params["author_id"] = [a.id for a in authors] if mentions: if len(mentions) > 100: From 355a7d8d5100eff251b20ad96de612d152235ff2 Mon Sep 17 00:00:00 2001 From: Nelo <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:14:50 -0400 Subject: [PATCH 17/18] s --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 0a816445b8..1530193b02 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4927,7 +4927,7 @@ def search( raise ValueError( f"attachment_extension {a!r} must be up to 256 characters." ) - params["attachment_extensions"] = attachment_extensions + params["attachment_extension"] = attachment_extensions if sort_by: params["sort_by"] = str(sort_by) From ceb227aa605b418dce9eb3da5207e4aa3f8e54bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:39:52 +0000 Subject: [PATCH 18/18] style(pre-commit): auto fixes from pre-commit.com hooks --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f67a32e018..70d7eff6a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ These changes are available on the `master` branch, but have not yet been releas ### Removed ## [2.8.0rc1] - 2026-03-21 + ### Added - Added support for community invites. @@ -88,6 +89,7 @@ These changes are available on the `master` branch, but have not yet been releas restrictions. ([#3056](https://github.com/Pycord-Development/pycord/pull/3056)) - Removed the following methods: `Guild.set_mfa_required`, `Guild.delete`, `Template.create_guild`, and `Client.create_guild`. + ## [2.7.1] - 2026-02-09 ### Added