Repository: Rapptz/discord.py
Pull request: [commands] Add a converter for discord.Object
### Summary

Closes #2562. Also parses basic mentions for their IDs.

```py
@bot.command()
async def hackban(ctx, member: discord.Object):
    await ctx.guild.ban(member)
```

### Checklist

- [x] If code changes were made then they have been tested.
    - [x] I have updated the documentation to reflect the changes.
- [x] This PR fixes an issue.
- [x] This PR adds something new (e.g. new method or parameters).
- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed)
- [ ] This PR is **not** a code change (e.g. documentation, README, ...)


Problem statement:
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`.

Hints from issue discussion:

Commit message: do the needful

Patch to review:
diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py
index 82ea36fd9f75..7479e9e344ae 100644
--- a/discord/ext/commands/converter.py
+++ b/discord/ext/commands/converter.py
@@ -122,6 +122,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 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
index 98154d108193..bfbe55fc7489 100644
--- 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.


Code review comments:
diff_hunk: @@ -122,6 +122,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 mention.
path: discord/ext/commands/converter.py
text: @user1:
```suggestion
    2. Lookup by member, role, or channel mention.
```