diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml index 508f34218f..6f4ba8a5b6 100644 --- a/.github/workflows/todo.yml +++ b/.github/workflows/todo.yml @@ -6,7 +6,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run tdg-github-action - uses: ribtoks/tdg-github-action@v0.4.7-beta + uses: ribtoks/tdg-github-action@v0.4.10-beta with: TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81e9a64c72..9719862ee2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -19,7 +19,7 @@ repos: # - --remove-duplicate-keys # - --remove-unused-variables - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 + rev: v3.15.2 hooks: - id: pyupgrade args: [--py38-plus] @@ -28,7 +28,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.4.0 hooks: - id: black args: [--safe, --quiet] diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b3a52e2c6..b5ba3d0323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ These changes are available on the `master` branch, but have not yet been releas ([#2396](https://github.com/Pycord-Development/pycord/pull/2396)) - Added `user` argument to `Paginator.edit`. ([#2390](https://github.com/Pycord-Development/pycord/pull/2390)) +- Added `bridge_option` decorator. Required for `bridge.Bot` in 2.7. + ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) +- Added `Guild.search_members`. + ([#2418](https://github.com/Pycord-Development/pycord/pull/2418)) +- Added `member` data to the `raw_reaction_remove` event. + ([#2412](https://github.com/Pycord-Development/pycord/pull/2412)) - Added support for user-installable applications. ([#2409](https://github.com/Pycord-Development/pycord/pull/2409)) @@ -34,6 +40,10 @@ These changes are available on the `master` branch, but have not yet been releas ([#2400](https://github.com/Pycord-Development/pycord/pull/2400)) - Fixed `ScheduledEvent.subscribers` behavior with `limit=None`. ([#2407](https://github.com/Pycord-Development/pycord/pull/2407)) +- Fixed invalid data being passed to `Interaction._guild` in certain cases. + ([#2411](https://github.com/Pycord-Development/pycord/pull/2411)) +- Fixed option typehints being ignored when using `parameter_name`. + ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) - Fixed an issue with `Interaction` that would cause bots to crash if a guild was not cached. ([#2409](https://github.com/Pycord-Development/pycord/pull/2409)) @@ -43,6 +53,12 @@ These changes are available on the `master` branch, but have not yet been releas ([#2387](https://github.com/Pycord-Development/pycord/pull/2387)) - HTTP requests that fail with a 503 status are now re-tried. ([#2395](https://github.com/Pycord-Development/pycord/pull/2395)) +- `option` decorator now accepts `input_type`. + ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) +- `Option` may be used instead of `BridgeOption` until 2.7. + ([#2417](https://github.com/Pycord-Development/pycord/pull/2417)) +- `Guild.query_members` now accepts `limit=None` to retrieve all members. + ([#2419](https://github.com/Pycord-Development/pycord/pull/2419)) - `ApplicationCommand.guild_only` is now deprecated in favor of `ApplicationCommand.contexts`. ([#2409](https://github.com/Pycord-Development/pycord/pull/2409)) diff --git a/discord/cog.py b/discord/cog.py index 24a5b58e70..4f064edb26 100644 --- a/discord/cog.py +++ b/discord/cog.py @@ -379,7 +379,9 @@ def _get_overridden_method(cls, method: FuncT) -> FuncT | None: ) @classmethod - def listener(cls, name: str = MISSING) -> Callable[[FuncT], FuncT]: + def listener( + cls, name: str = MISSING, once: bool = False + ) -> Callable[[FuncT], FuncT]: """A decorator that marks a function as a listener. This is the cog equivalent of :meth:`.Bot.listen`. @@ -389,6 +391,9 @@ def listener(cls, name: str = MISSING) -> Callable[[FuncT], FuncT]: name: :class:`str` The name of the event being listened to. If not provided, it defaults to the function's name. + once: :class:`bool` + If this listener should only be called once after each cog load. + Defaults to false. Raises ------ @@ -411,6 +416,7 @@ def decorator(func: FuncT) -> FuncT: raise TypeError("Listener function must be a coroutine function.") actual.__cog_listener__ = True to_assign = name or actual.__name__ + actual._once = once try: actual.__cog_listener_names__.append(to_assign) except AttributeError: diff --git a/discord/commands/options.py b/discord/commands/options.py index 80d525c65b..38cac6b539 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -395,7 +395,7 @@ def to_dict(self) -> dict[str, str | int | float]: return as_dict -def option(name, type=None, **kwargs): +def option(name, input_type=None, **kwargs): """A decorator that can be used instead of typehinting :class:`.Option`. .. versionadded:: 2.0 @@ -408,12 +408,13 @@ def option(name, type=None, **kwargs): """ def decorator(func): - nonlocal type - type = type or func.__annotations__.get(name, str) - if parameter := kwargs.get("parameter_name"): - func.__annotations__[parameter] = Option(type, name=name, **kwargs) - else: - func.__annotations__[name] = Option(type, **kwargs) + resolved_name = kwargs.pop("parameter_name", None) or name + itype = ( + kwargs.pop("type", None) + or input_type + or func.__annotations__.get(resolved_name, str) + ) + func.__annotations__[resolved_name] = Option(itype, name=name, **kwargs) return func return decorator diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index b1a37b3980..c14d9db95a 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.py @@ -40,7 +40,7 @@ SlashCommandOptionType, ) -from ...utils import MISSING, find, get +from ...utils import MISSING, find, get, warn_deprecated from ..commands import BadArgument from ..commands import Bot as ExtBot from ..commands import ( @@ -63,6 +63,7 @@ "BridgeCommandGroup", "bridge_command", "bridge_group", + "bridge_option", "BridgeExtCommand", "BridgeSlashCommand", "BridgeExtGroup", @@ -627,3 +628,38 @@ async def convert(self, ctx, argument: str) -> Any: return converted except ValueError as exc: raise BadArgument() from exc + + +def bridge_option(name, input_type=None, **kwargs): + """A decorator that can be used instead of typehinting :class:`.BridgeOption`. + + .. versionadded:: 2.6 + + Attributes + ---------- + parameter_name: :class:`str` + The name of the target parameter this option is mapped to. + This allows you to have a separate UI ``name`` and parameter name. + """ + + def decorator(func): + resolved_name = kwargs.pop("parameter_name", None) or name + itype = ( + kwargs.pop("type", None) + or input_type + or func.__annotations__.get(resolved_name, str) + ) + func.__annotations__[resolved_name] = BridgeOption(itype, name=name, **kwargs) + return func + + return decorator + + +discord.commands.options.Option = BridgeOption +discord.Option = BridgeOption +warn_deprecated( + "Option", + "BridgeOption", + "2.5", + reference="https://github.com/Pycord-Development/pycord/pull/2417", +) diff --git a/discord/guild.py b/discord/guild.py index 489b825b73..a2ece66b68 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2027,6 +2027,36 @@ def fetch_members( return MemberIterator(self, limit=limit, after=after) + async def search_members(self, query: str, *, limit: int = 1000) -> list[Member]: + """Search for guild members whose usernames or nicknames start with the query string. Unlike :meth:`fetch_members`, this does not require :meth:`Intents.members`. + + .. note:: + + This method is an API call. For general usage, consider filtering :attr:`members` instead. + + .. versionadded:: 2.6 + + Parameters + ---------- + query: :class:`str` + Searches for usernames and nicknames that start with this string, case-insensitive. + limit: :class:`int` + The maximum number of members to retrieve, up to 1000. + + Returns + ------- + List[:class:`Member`] + The list of members that have matched the query. + + Raises + ------ + HTTPException + Getting the members failed. + """ + + data = await self._state.http.search_members(self.id, query, limit) + return [Member(data=m, guild=self, state=self._state) for m in data] + async def fetch_member(self, member_id: int, /) -> Member: """|coro| @@ -3354,7 +3384,7 @@ async def query_members( self, query: str | None = None, *, - limit: int = 5, + limit: int | None = 5, user_ids: list[int] | None = None, presences: bool = False, cache: bool = True, @@ -3372,10 +3402,14 @@ async def query_members( ---------- query: Optional[:class:`str`] The string that the username's start with. - limit: :class:`int` - The maximum number of members to send back. This must be - a number between 5 and 100. - presences: :class:`bool` + user_ids: Optional[List[:class:`int`]] + List of user IDs to search for. If the user ID is not in the guild then it won't be returned. + + .. versionadded:: 1.4 + limit: Optional[:class:`int`] + The maximum number of members to send back. If no query is passed, passing ``None`` returns all members. + If a ``query`` or ``user_ids`` is passed, must be between 1 and 100. Defaults to 5. + presences: Optional[:class:`bool`] Whether to request for presences to be provided. This defaults to ``False``. @@ -3383,11 +3417,7 @@ async def query_members( cache: :class:`bool` Whether to cache the members internally. This makes operations - such as :meth:`get_member` work for those that matched. - user_ids: Optional[List[:class:`int`]] - List of user IDs to search for. If the user ID is not in the guild then it won't be returned. - - .. versionadded:: 1.4 + such as :meth:`get_member` work for those that matched. Defaults to ``True``. Returns ------- @@ -3407,12 +3437,18 @@ async def query_members( if presences and not self._state._intents.presences: raise ClientException("Intents.presences must be enabled to use this.") - if query is None: - if query == "": - raise ValueError("Cannot pass empty query string.") + if not limit or limit > 100 or limit < 1: + if query or user_ids: + raise ValueError( + "limit must be between 1 and 100 when using query or user_ids" + ) + if not limit: + query = "" + limit = 0 + if query is None: if user_ids is None: - raise ValueError("Must pass either query or user_ids") + raise ValueError("Must pass query or user_ids, or set limit to None") if user_ids is not None and query is not None: raise ValueError("Cannot pass both query and user_ids") @@ -3420,7 +3456,6 @@ async def query_members( if user_ids is not None and not user_ids: raise ValueError("user_ids must contain at least 1 value") - limit = min(100, limit or 5) return await self._state.query_members( self, query=query, diff --git a/discord/http.py b/discord/http.py index a8833d17af..e79d6120b7 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1636,6 +1636,20 @@ def get_members( r = Route("GET", "/guilds/{guild_id}/members", guild_id=guild_id) return self.request(r, params=params) + def search_members( + self, + guild_id: Snowflake, + query: str, + limit: int, + ) -> Response[list[member.MemberWithUser]]: + params: dict[str, Any] = { + "query": query, + "limit": limit, + } + + r = Route("GET", "/guilds/{guild_id}/members/search", guild_id=guild_id) + return self.request(r, params=params) + def get_member( self, guild_id: Snowflake, member_id: Snowflake ) -> Response[member.MemberWithUser]: diff --git a/discord/raw_models.py b/discord/raw_models.py index 49f5575064..e3d38e558d 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -205,8 +205,7 @@ class RawReactionActionEvent(_RawReprMixin): emoji: :class:`PartialEmoji` The custom or unicode emoji being used. member: Optional[:class:`Member`] - The member who added the reaction. Only available if `event_type` is `REACTION_ADD` - and the reaction is inside a guild. + The member who added the reaction. Only available if the reaction occurs within a guild. .. versionadded:: 1.3 diff --git a/discord/state.py b/discord/state.py index 04cf5bae60..eff078e4e2 100644 --- a/discord/state.py +++ b/discord/state.py @@ -781,6 +781,17 @@ def parse_message_reaction_remove(self, data) -> None: emoji_id = utils._get_as_snowflake(emoji, "id") emoji = PartialEmoji.with_state(self, id=emoji_id, name=emoji["name"]) raw = RawReactionActionEvent(data, emoji, "REACTION_REMOVE") + + member_data = data.get("member") + if member_data: + guild = self._get_guild(raw.guild_id) + if guild is not None: + raw.member = Member(data=member_data, guild=guild, state=self) + else: + raw.member = None + else: + raw.member = None + self.dispatch("raw_reaction_remove", raw) message = self._get_message(raw.message_id)