Repository: Rapptz/discord.py

Issue:
Use discord.Object annotation as an ID converter
### The Problem

It's quite common to have a command accept an arbitrary Discord ID value, in cases where obtaining such value (or it's respective model) via other converters would not be possible. The most iconic example of this would be a `hackban` command, where normally it's possible for the bot to not have any access to the member nor the user object for the ID provided. Similarly, an `unban` command would face the same issue. One could always write either their own custom ID converter, or accept an integer, check basic validity and wrap it in an object themselves, but I figured it'd be cool to have an already pre-made converter for this purpose.

### The Ideal Solution

Make it so that annotating an input argument in a command with `discord.Object` would use the already existing `IDConverter` to construct a `discord.Object` and return it as said argument's value. This has the benefit of already having basic input validation regarding the ID length, as well as obtaining an object that already meets the `Snowflake` abstract base class (some library methods can utilize this).

### The Current Solution

As explained, one has to make the argument accept an integer (possibly as a fallback at the end of a Union), and process the result themselves. Most of the time it's just boilerplate code, having this in the library itself would simplify this a lot, and I figured it wouldn't be too hard to implement given the already-existing `IDConverter`.



Discussion:

Resolving patch:
diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py
--- a/discord/ext/commands/converter.py
+++ b/discord/ext/commands/converter.py
@@ -37,6 +37,7 @@
 
 __all__ = (
     'Converter',
+    'ObjectConverter',
     'MemberConverter',
     'UserConverter',
     'MessageConverter',
@@ -122,6 +123,30 @@ def _get_id_match(argument):
         return _ID_REGEX.match(argument)
 
 
+class ObjectConverter(IDConverter[discord.Object]):
+    """Converts to a :class:`~discord.Object`.
+
+    The argument must follow the valid ID or mention formats (e.g. `<@80088516616269824>`).
+
+    .. versionadded:: 2.0
+
+    The lookup strategy is as follows (in order):
+
+    1. Lookup by ID.
+    2. Lookup by member, role, or channel mention.
+    """
+
+    async def convert(self, ctx: Context, argument: str) -> discord.Object:
+        match = self._get_id_match(argument) or re.match(r'<(?:@(?:!|&)?|#)([0-9]{15,20})>$', argument)
+
+        if match is None:
+            raise ObjectNotFound(argument)
+
+        result = int(match.group(1))
+
+        return discord.Object(id=result)
+
+
 class MemberConverter(IDConverter[discord.Member]):
     """Converts to a :class:`~discord.Member`.
 
diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py
--- a/discord/ext/commands/errors.py
+++ b/discord/ext/commands/errors.py
@@ -42,6 +42,7 @@
     'MaxConcurrencyReached',
     'NotOwner',
     'MessageNotFound',
+    'ObjectNotFound',
     'MemberNotFound',
     'GuildNotFound',
     'UserNotFound',
@@ -213,6 +214,23 @@ class NotOwner(CheckFailure):
     """
     pass
 
+class ObjectNotFound(BadArgument):
+    """Exception raised when the argument provided did not match the format
+    of an ID or a mention.
+
+    This inherits from :exc:`BadArgument`
+
+    .. versionadded:: 2.0
+
+    Attributes
+    -----------
+    argument: :class:`str`
+        The argument supplied by the caller that was not matched
+    """
+    def __init__(self, argument):
+        self.argument = argument
+        super().__init__(f'{argument!r} does not follow a valid ID or mention format.')
+
 class MemberNotFound(BadArgument):
     """Exception raised when the member provided was not found in the bot's
     cache.


Test patch: