Repository: Pycord-Development/pycord

Issue:
discord.User is not a valid input type for discord.commands.Option()
### Summary

 Statement from @Lulalaby:
> Libs should be able to parse Option(discord.User) and make it Castable as member within the context of the current guild, If applicable.

### Reproduction Steps

- Pass a `discord.User` to the type argument in a `discord.commands.Option()`.

### Minimal Reproducible Code

```python
import discord
...
discord.commands.Option(discord.User)
```


### Expected Results

Get a user object similar to the `discord.Member` class.

### Actual Results

```
Traceback (most recent call last):
  File "C:\Dev\Projects\bot.py", line 75, in <module>
    discord.commands.Option(discord.User)
  File "C:\Users\mendo\AppData\Local\Programs\Python\Python310\lib\site-packages\discord\commands\commands.py", line 564, in __init__
    _type = SlashCommandOptionType.from_datatype(input_type)
  File "C:\Users\mendo\AppData\Local\Programs\Python\Python310\lib\site-packages\discord\enums.py", line 659, in from_datatype
    raise TypeError(f'Invalid class {datatype} used as an input type for an Option')
TypeError: Invalid class <class 'discord.user.User'> used as an input type for an Option
```

### Intents

Im not using intents.

### System Information

```
- Python v3.10.0-final
- py-cord v2.0.0-alpha
    - py-cord pkg_resources: v2.0.0a4214+g5a23a46
- aiohttp v3.7.4.post0
- system info: Windows 10 10.0.22000
```
(Actually, Windows 11 10.0.22000)


### Checklist

- [X] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

I was trying to make a Slash command with an `Option(discord.Member)` argument and it worked perfectly, except when someone called the command from the bot DM's. 

Since it is a DM and there is no guild, the `Option()` class can not use the `get_member()` method from a NoneType. I tried to implement the @Lulalaby idea of using a `discord.User` instead and if the slash command was called from a guild, then get the discord.Member trough the user ID.


Discussion:
![image](https://user-images.githubusercontent.com/36114463/139741658-75735c1a-4613-4551-b733-dcb59c56b327.png)

It appears it does support users
Seems like someone forgot the case for user, because the rest appear to have cases but can't find one for user
https://github.com/Pycord-Development/pycord/commit/e01739d597eda10556c71f4653021c707e3549d3

Could be the required change for this to be fixed
If this helps, here is an example of the data retrieved by the slash interaction from a DM call.
```json
{
    "id": "904771131771351040",
    "name": "example",
    "options": [
        {
            "name": "member",
            "type": 6,
            "value": "229764530354388992"
        }
    ],
    "resolved": {
        "users": {
            "229764530354388992": {
                "avatar": "f51f64e4d2e9af8f3164ccafdfe48b78",
                "discriminator": "8282",
                "id": "229764530354388992",
                "public_flags": 128,
                "username": "Apocryphon"
            }
        }
    },
    "type": 1
}
```
Full traceback in Verbose Mode of another execution [here](https://www.toptal.com/developers/hastebin/okoxexixep.coffeescript).
Fixed
Why was this reopened?
Because it's kinda not resolved
> Why was this reopened?

Because `discord.commands.Option()` is actually not able to parse a `discord.User` argument - i.e: [Slash invocation from a DM](https://www.toptal.com/developers/hastebin/okoxexixep.coffeescript) with `Option(discord.User)` as required option.
According to the traceback, the exception is being launched by this [line](https://github.com/Pycord-Development/pycord/blob/master/discord/commands/commands.py#L508).

```py
508                arg = await get_or_fetch(ctx.guild, name, int(arg), default=int(arg))
-------------------------------------------------^
When invoked from a DM, ctx.guild is `None`
```
Seems we need a null check here.
Would you like to open a PR and fix that @Apocryphon-X 
Sure, let me try 
More detailed:

It needs to be implemented that if guild is null, that a user fetch will be made instead

Resolving patch:
diff --git a/discord/commands/commands.py b/discord/commands/commands.py
--- a/discord/commands/commands.py
+++ b/discord/commands/commands.py
@@ -62,7 +62,7 @@
 )
 
 if TYPE_CHECKING:
-    pass
+    from ..interactions import Interaction
 
 def wrap_callback(coro):
     @functools.wraps(coro)
@@ -99,7 +99,7 @@ class _BaseCommand:
 
 class ApplicationCommand(_BaseCommand):
     cog = None
-    
+
     def __repr__(self):
         return f"<discord.commands.{self.__class__.__name__} name={self.name}>"
 
@@ -143,8 +143,8 @@ async def can_run(self, ctx: ApplicationContext) -> bool:
             # since we have no checks, then we just return True.
             return True
 
-        return await async_all(predicate(ctx) for predicate in predicates) # type: ignore    
-    
+        return await async_all(predicate(ctx) for predicate in predicates) # type: ignore
+
     async def dispatch_error(self, ctx: ApplicationContext, error: Exception) -> None:
         ctx.command_failed = True
         cog = self.cog
@@ -346,7 +346,7 @@ class SlashCommand(ApplicationCommand):
 
         .. note::
 
-            If this is not empty then default_permissions will be set to False.    
+            If this is not empty then default_permissions will be set to False.
 
     cog: Optional[:class:`Cog`]
         The cog that this command belongs to. ``None`` if there isn't one.
@@ -508,8 +508,13 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
                 <= op.input_type.value
                 <= SlashCommandOptionType.role.value
             ):
-                name = "member" if op.input_type.name == "user" else op.input_type.name
-                arg = await get_or_fetch(ctx.guild, name, int(arg), default=int(arg))
+                if ctx.guild is None and op.input_type.name == "user":
+                    _data = ctx.interaction.data["resolved"]["users"][arg]
+                    _data["id"] = int(arg)
+                    arg = User(state=ctx.interaction._state, data=_data)
+                else:
+                    name = "member" if op.input_type.name == "user" else op.input_type.name
+                    arg = await get_or_fetch(ctx.guild, name, int(arg), default=int(arg))
 
             elif op.input_type == SlashCommandOptionType.mentionable:
                 arg_id = int(arg)
@@ -525,7 +530,7 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
         for o in self.options:
             if o._parameter_name not in kwargs:
                 kwargs[o._parameter_name] = o.default
-        
+
         if self.cog is not None:
             await self.callback(self.cog, ctx, **kwargs)
         else:
@@ -533,12 +538,12 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
 
     async def invoke_autocomplete_callback(self, ctx: AutocompleteContext):
         values = { i.name: i.default for i in self.options }
-        
+
         for op in ctx.interaction.data.get("options", []):
             if op.get("focused", False):
                 option = find(lambda o: o.name == op["name"], self.options)
                 values.update({
-                    i["name"]:i["value"] 
+                    i["name"]:i["value"]
                     for i in ctx.interaction.data["options"]
                 })
                 ctx.command = self
@@ -648,7 +653,7 @@ def __init__(
 
         self.min_value: minmax_typehint = kwargs.pop("min_value", None)
         self.max_value: minmax_typehint = kwargs.pop("max_value", None)
-        
+
         if not (isinstance(self.min_value, minmax_types) or self.min_value is None):
             raise TypeError(f"Expected {minmax_typehint} for min_value, got \"{type(self.min_value).__name__}\"")
         if not (isinstance(self.max_value, minmax_types) or self.min_value is None):
@@ -860,7 +865,7 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:
         self.checks = checks
         self._before_invoke = None
         self._after_invoke = None
-        
+
         self.validate_parameters()
 
         # Context Menu commands don't have permissions
@@ -902,7 +907,7 @@ def validate_parameters(self):
             )
         except StopIteration:
             pass
-    
+
     def qualified_name(self):
         return self.name
 
@@ -941,7 +946,7 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
                 guild=ctx.interaction._state._get_guild(ctx.interaction.guild_id),
                 state=ctx.interaction._state,
             )
-        
+
         if self.cog is not None:
             await self.callback(self.cog, ctx, target)
         else:   
@@ -1007,12 +1012,12 @@ async def _invoke(self, ctx: ApplicationContext):
             channel = ctx.interaction._state.add_dm_channel(data)
 
         target = Message(state=ctx.interaction._state, channel=channel, data=message)
-        
+
         if self.cog is not None:
             await self.callback(self.cog, ctx, target)
         else:
             await self.callback(ctx, target)
-    
+
     def copy(self):
         """Creates a copy of this command.
 


Test patch: