Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
aad1630
base message search routes
NeloBlivion Mar 19, 2026
4d7419c
main implementation
NeloBlivion Mar 19, 2026
fa3e0ec
items
NeloBlivion Mar 19, 2026
4d671fe
Merge branch 'master' into search
NeloBlivion Mar 19, 2026
7c755b8
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 19, 2026
e8d340d
:
NeloBlivion Mar 19, 2026
93f6dbd
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 19, 2026
7ad0198
slop
NeloBlivion Mar 19, 2026
32b65bc
fixes
NeloBlivion Mar 19, 2026
0f5d110
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 19, 2026
4c22f00
bool to int
NeloBlivion Mar 19, 2026
875b1be
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 19, 2026
f901164
i can read
NeloBlivion Mar 20, 2026
93bb7fe
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 20, 2026
db40d7f
handle large limit
NeloBlivion Mar 20, 2026
05d5ece
all
NeloBlivion Mar 20, 2026
7188ef9
params
NeloBlivion Mar 20, 2026
355a7d8
s
NeloBlivion Mar 20, 2026
0cb6f67
Merge branch 'master' into search
NeloBlivion Mar 20, 2026
05aa66b
Merge branch 'master' into search
NeloBlivion Mar 20, 2026
58438ea
Merge branch 'master' into search
NeloBlivion Mar 21, 2026
b730d65
Merge branch 'master' into search
NeloBlivion Mar 21, 2026
ceb227a
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 21, 2026
0405f8b
Merge branch 'master' into search
NeloBlivion Mar 21, 2026
9dcba5a
Merge branch 'master' into search
NeloBlivion Mar 24, 2026
81f6b82
Merge branch 'master' into search
NeloBlivion Mar 27, 2026
6ec6936
Merge branch 'master' into search
NeloBlivion Mar 31, 2026
e342ca1
Merge branch 'master' into search
NeloBlivion Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,9 @@ 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.
Expand Down
36 changes: 36 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@
"SubscriptionStatus",
"SeparatorSpacingSize",
"SelectDefaultValueType",
"SearchEmbedType",
"SearchSortMode",
"SearchSortOrder",
"ApplicationEventWebhookStatus",
"InviteTargetUsersJobStatusCode",
)
Expand Down Expand Up @@ -1136,6 +1139,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


class RoleType(IntEnum):
"""Represents the type of role.

Expand Down
218 changes: 217 additions & 1 deletion discord/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
RoleType,
ScheduledEventLocationType,
ScheduledEventPrivacyLevel,
SearchEmbedType,
SearchSortMode,
SearchSortOrder,
SortOrder,
VerificationLevel,
VideoQualityMode,
Expand All @@ -81,6 +84,7 @@
BanIterator,
EntitlementIterator,
MemberIterator,
MessageSearchIterator,
)
from .member import Member, VoiceState
from .mixins import Hashable
Expand All @@ -98,7 +102,7 @@
from .welcome_screen import WelcomeScreen, WelcomeScreenChannel
from .widget import Widget

__all__ = ("BanEntry", "Guild", "GuildRoleCounts")
__all__ = ("BanEntry", "Guild", "GuildRoleCounts", "SearchHas", "SearchAuthors")

MISSING = utils.MISSING

Expand Down Expand Up @@ -152,6 +156,64 @@ class _GuildLimit(NamedTuple):
filesize: int


class Parsable:
# idk this kinda sucks lmao
def __init__(self, **values):
self._values = values
for k, v in values.items():
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.

Expand Down Expand Up @@ -4724,3 +4786,157 @@ 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 slop > 100 or slop < 0:
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:
params["author_type"] = author_types.parse()

if authors:
if len(authors) > 100:
raise ValueError("can only specify up to 100 authors")
params["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_extension"] = 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)
64 changes: 64 additions & 0 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,70 @@ 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,
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[message.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,
) -> Response[message.MessageSearchResults]:

p = {
"limit": limit,
"offset": offset,
"min_id": min_id,
"max_id": max_id,
"slop": slop,
"content": content,
"channel_id": channel_id,
"author_type": author_type,
"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
),
"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,
"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": 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(
Route("GET", "/guilds/{guild_id}/messages/search", guild_id=guild_id),
params=params,
)

# Member management

def kick(
Expand Down
Loading
Loading