Browse Source

Add Object to permission overwrites, channel jump urls, command iterators

pull/10109/head
dolfies 4 years ago
parent
commit
da57fd0e68
  1. 134
      discord/abc.py
  2. 20
      discord/channel.py
  3. 78
      discord/member.py
  4. 79
      discord/message.py
  5. 75
      discord/user.py

134
discord/abc.py

@ -43,9 +43,10 @@ from typing import (
runtime_checkable,
)
from .iterators import HistoryIterator
from .object import Object
from .iterators import CommandIterator, HistoryIterator
from .context_managers import Typing
from .enums import ChannelType
from .enums import CommandType, ChannelType
from .errors import InvalidArgument, ClientException
from .mentions import AllowedMentions
from .permissions import PermissionOverwrite, Permissions
@ -69,8 +70,7 @@ __all__ = (
T = TypeVar('T', bound=VoiceProtocol)
if TYPE_CHECKING:
from datetime import datetime
from .abc import PrivateChannel
from .client import Client
from .user import ClientUser, User
from .asset import Asset
@ -80,7 +80,7 @@ if TYPE_CHECKING:
from .channel import CategoryChannel
from .embeds import Embed
from .message import Message, MessageReference, PartialMessage
from .channel import DMChannel, GroupChannel, PartialMessageable, PrivateChannel, TextChannel, VocalGuildChannel
from .channel import DMChannel, GroupChannel, PartialMessageable, TextChannel, VocalGuildChannel
from .threads import Thread
from .enums import InviteTarget
from .types.channel import (
@ -347,7 +347,7 @@ class GuildChannel:
options['permission_overwrites'] = [c._asdict() for c in category._overwrites]
options['parent_id'] = parent_id
elif lock_permissions and self.category_id is not None:
# if we're syncing permissions on a pre-existing channel category without changing it
# If we're syncing permissions on a pre-existing channel category without changing it
# we need to update the permissions to point to the pre-existing category
category = self.guild.get_channel(self.category_id)
if category:
@ -447,6 +447,14 @@ class GuildChannel:
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
return utils.snowflake_time(self.id)
@property
def jump_url(self) -> str:
""":class:`str`: Returns a URL that allows the client to jump to the channel.
.. versionadded:: 2.0
"""
return f'https://discord.com/channels/{self.guild.id}/{self.id}'
def overwrites_for(self, obj: Union[Role, User]) -> PermissionOverwrite:
"""Returns the channel-specific overwrites for a member or a role.
@ -478,7 +486,7 @@ class GuildChannel:
return PermissionOverwrite()
@property
def overwrites(self) -> Dict[Union[Role, Member], PermissionOverwrite]:
def overwrites(self) -> Dict[Union[Object, Role, Member], PermissionOverwrite]:
"""Returns all of the channel's overwrites.
This is returned as a dictionary where the key contains the target which
@ -487,7 +495,7 @@ class GuildChannel:
Returns
--------
Dict[Union[:class:`~discord.Role`, :class:`~discord.Member`], :class:`~discord.PermissionOverwrite`]
Dict[Union[:class:`~discord.Object`, :class:`~discord.Role`, :class:`~discord.Member`], :class:`~discord.PermissionOverwrite`]
The channel's permission overwrites.
"""
ret = {}
@ -502,13 +510,10 @@ class GuildChannel:
elif ow.is_member():
target = self.guild.get_member(ow.id)
# TODO: There is potential data loss here in the non-chunked
# case, i.e. target is None because get_member returned nothing.
# This can be fixed with a slight breaking change to the return type,
# i.e. adding discord.Object to the list of it
# However, for now this is an acceptable compromise.
if target is not None:
ret[target] = overwrite
if target is None:
target = Object(ow.id)
ret[target] = overwrite
return ret
@property
@ -570,18 +575,18 @@ class GuildChannel:
"""
# The current cases can be explained as:
# Guild owner get all permissions -- no questions asked. Otherwise...
# The @everyone role gets the first application.
# Guild owner get all permissions -- no questions asked
# The @everyone role gets the first application
# After that, the applied roles that the user has in the channel
# (or otherwise) are then OR'd together.
# (or otherwise) are then OR'd together
# After the role permissions are resolved, the member permissions
# have to take into effect.
# After all that is done.. you have to do the following:
# have to take into effect
# After all that is done, you have to do the following:
# If manage permissions is True, then all permissions are set to True.
# If manage permissions is True, then all permissions are set to True
# The operation first takes into consideration the denied
# and then the allowed.
# and then the allowed
if self.guild.owner_id == obj.id:
return Permissions.all()
@ -828,7 +833,7 @@ class GuildChannel:
data = await self._state.http.create_channel(guild_id, self.type.value, reason=reason, **base_attrs)
obj = cls(state=self._state, guild=self.guild, data=data)
# temporarily add it to the cache
# Temporarily add it to the cache
self.guild._channels[obj.id] = obj # type: ignore
return obj
@ -1232,7 +1237,7 @@ class Messageable:
self,
content=None,
*,
tts=None,
tts=False,
embed=None,
embeds=None,
file=None,
@ -1336,7 +1341,7 @@ class Messageable:
content = str(content) if content is not None else None
if embed is not None and embeds is not None:
raise InvalidArgument('Cannot pass both embed and embeds parameter to send()')
raise InvalidArgument('Cannot pass both embed and embeds')
if embed is not None:
embed = embed.to_dict()
@ -1368,10 +1373,10 @@ class Messageable:
raise InvalidArgument('reference parameter must be Message, MessageReference, or PartialMessage') from None
if nonce is MISSING:
nonce = utils.time_snowflake(datetime.utcnow())
nonce = str(utils.time_snowflake(datetime.utcnow()))
if file is not None and files is not None:
raise InvalidArgument('Cannot pass both file and files parameter to send()')
raise InvalidArgument('Cannot pass both file and files')
if file is not None:
if not isinstance(file, File):
@ -1590,6 +1595,81 @@ class Messageable:
"""
return HistoryIterator(self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first)
def slash_commands(
self,
query: Optional[str] = None,
*,
limit: Optional[int] = None,
command_ids: Optional[List[int]] = None,
applications: bool = True,
application: Optional[Snowflake] = None,
):
"""Returns an iterator that allows you to see what slash commands are available to use.
.. note::
If this is a DM context, all parameters here are faked, as the only way to get commands is to fetch them all at once.
Because of this, all except ``query``, ``limit``, and ``command_ids`` are ignored.
It is recommended to not pass any parameters in that case.
Examples
---------
Usage ::
async for command in channel.slash_commands():
print(command.name)
Flattening into a list ::
commands = await channel.slash_commands().flatten()
# commands is now a list of SlashCommand...
All parameters are optional.
Parameters
----------
query: Optional[:class:`str`]
The query to search for.
limit: Optional[:class:`int`]
The maximum number of commands to send back.
cache: :class:`bool`
Whether to cache the commands internally.
command_ids: Optional[List[:class:`int`]]
List of command IDs to search for. If the command doesn't exist it won't be returned.
applications: :class:`bool`
Whether to include applications in the response. This defaults to ``False``.
application: Optional[:class:`Snowflake`]
Query commands only for this application.
Raises
------
:exc:`.InvalidArgument`
The user is not a bot.
The limit was not > 0.
Both query and command_ids were passed.
:exc:`.HTTPException`
Getting the commands failed.
Yields
-------
:class:`.SlashCommand`
A slash command.
"""
if query and command_ids:
raise InvalidArgument('Cannot specify both query and command_ids')
if limit is not None and limit <= 0:
raise InvalidArgument('limit must be > 0')
return CommandIterator(
self,
CommandType.chat_input,
query,
limit,
command_ids,
applications=applications,
application=application,
)
class Connectable(Protocol):
"""An ABC that details the common operations on a channel that can

20
discord/channel.py

@ -24,7 +24,6 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import asyncio
from typing import (
Any,
Callable,
@ -51,7 +50,7 @@ from .object import Object
from . import utils
from .utils import MISSING
from .asset import Asset
from .errors import ClientException, InvalidArgument, NotFound
from .errors import ClientException, InvalidArgument
from .stage_instance import StageInstance
from .threads import Thread
from .iterators import ArchivedThreadIterator
@ -86,7 +85,6 @@ if TYPE_CHECKING:
StoreChannel as StoreChannelPayload,
GroupDMChannel as GroupChannelPayload,
)
from .types.snowflake import SnowflakeList
class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
@ -1751,6 +1749,14 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
""":class:`datetime.datetime`: Returns the direct message channel's creation time in UTC."""
return utils.snowflake_time(self.id)
@property
def jump_url(self) -> str:
""":class:`str`: Returns a URL that allows the client to jump to the channel.
.. versionadded:: 2.0
"""
return f'https://discord.com/channels/@me/{self.id}'
def permissions_for(self, obj: Any = None, /) -> Permissions:
"""Handles permission resolution for a :class:`User`.
@ -1936,6 +1942,14 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
return utils.snowflake_time(self.id)
@property
def jump_url(self) -> str:
""":class:`str`: Returns a URL that allows the client to jump to the channel.
.. versionadded:: 2.0
"""
return f'https://discord.com/channels/@me/{self.id}'
def permissions_for(self, obj: Snowflake, /) -> Permissions:
"""Handles permission resolution for a :class:`User`.

78
discord/member.py

@ -39,9 +39,11 @@ from .utils import MISSING
from .user import BaseUser, User, _UserTag
from .activity import create_activity, ActivityTypes
from .permissions import Permissions
from .enums import RelationshipAction, Status, try_enum
from .enums import CommandType, RelationshipAction, Status, try_enum
from .errors import InvalidArgument
from .colour import Colour
from .object import Object
from .iterators import CommandIterator
__all__ = (
'VoiceState',
@ -720,12 +722,13 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
guild_id = self.guild.id
me = self._state.self_id == self.id
payload: Dict[str, Any] = {}
data = None
if nick is not MISSING:
payload['nick'] = nick
if avatar is not MISSING:
payload['avatar'] = utils._bytes_to_base64_data(avatar)
payload['avatar'] = utils._bytes_to_base64_data(avatar) # type: ignore
if me and payload:
data = await http.edit_me(**payload)
@ -763,7 +766,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
data = await http.edit_member(guild_id, self.id, reason=reason, **payload)
if data:
return Member(data=data, guild=self.guild, state=self._state)
return Member(data=data, guild=self.guild, state=self._state) # type: ignore
async def request_to_speak(self) -> None:
"""|coro|
@ -934,3 +937,72 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
Sending the friend request failed.
"""
await self._state.http.add_relationship(self._user.id, action=RelationshipAction.send_friend_request)
def user_commands(
self,
query: Optional[str] = None,
*,
limit: Optional[int] = None,
command_ids: Optional[List[int]] = None,
applications: bool = True,
application: Optional[Snowflake] = None,
):
"""Returns an iterator that allows you to see what user commands are available to use.
Examples
---------
Usage ::
async for command in member.user_commands():
print(command.name)
Flattening into a list ::
commands = await member.user_commands().flatten()
# commands is now a list of UserCommand...
All parameters are optional.
Parameters
----------
query: Optional[:class:`str`]
The query to search for.
limit: Optional[:class:`int`]
The maximum number of commands to send back.
cache: :class:`bool`
Whether to cache the commands internally.
command_ids: Optional[List[:class:`int`]]
List of command IDs to search for. If the command doesn't exist it won't be returned.
applications: :class:`bool`
Whether to include applications in the response. This defaults to ``False``.
application: Optional[:class:`Snowflake`]
Query commands only for this application.
Raises
------
:exc:`.InvalidArgument`
The limit was not > 0.
Both query and command_ids were passed.
:exc:`.HTTPException`
Getting the commands failed.
Yields
-------
:class:`.UserCommand`
A user command.
"""
if query and command_ids:
raise InvalidArgument('Cannot specify both query and command_ids')
if limit is not None and limit <= 0:
raise InvalidArgument('limit must be > 0')
return CommandIterator(
self,
CommandType.user,
query,
limit,
command_ids,
applications=applications,
application=application,
)

79
discord/message.py

@ -36,9 +36,9 @@ from .reaction import Reaction
from .emoji import Emoji
from .partial_emoji import PartialEmoji
from .calls import CallMessage
from .enums import MessageType, ChannelType, try_enum
from .enums import MessageType, ChannelType, CommandType, try_enum
from .errors import InvalidArgument, HTTPException
from .components import _component_factory, Interaction
from .components import _component_factory
from .embeds import Embed
from .member import Member
from .flags import MessageFlags
@ -48,6 +48,8 @@ from .guild import Guild
from .mixins import Hashable
from .sticker import StickerItem
from .threads import Thread
from .iterators import CommandIterator
from .interactions import Interaction
if TYPE_CHECKING:
from .types.message import (
@ -1602,6 +1604,79 @@ class Message(Hashable):
return await self.channel.send(content, reference=self, **kwargs)
def message_commands(
self,
query: Optional[str] = None,
*,
limit: Optional[int] = None,
command_ids: Optional[List[int]] = None,
applications: bool = True,
application: Optional[Snowflake] = None,
):
"""Returns an iterator that allows you to see what message commands are available to use.
.. note::
If this is a DM context, all parameters here are faked, as the only way to get commands is to fetch them all at once.
Because of this, all except ``query``, ``limit``, and ``command_ids`` are ignored.
It is recommended to not pass any parameters in that case.
Examples
---------
Usage ::
async for command in message.message_commands():
print(command.name)
Flattening into a list ::
commands = await message.message_commands().flatten()
# commands is now a list of SlashCommand...
All parameters are optional.
Parameters
----------
query: Optional[:class:`str`]
The query to search for.
limit: Optional[:class:`int`]
The maximum number of commands to send back.
command_ids: Optional[List[:class:`int`]]
List of command IDs to search for. If the command doesn't exist it won't be returned.
applications: :class:`bool`
Whether to include applications in the response. This defaults to ``False``.
application: Optional[:class:`Snowflake`]
Query commands only for this application.
Raises
------
:exc:`.InvalidArgument`
The user is not a bot.
The limit was not > 0.
Both query and command_ids were passed.
:exc:`.HTTPException`
Getting the commands failed.
Yields
-------
:class:`.MessageCommand`
A message command.
"""
if query and command_ids:
raise InvalidArgument('Cannot specify both query and command_ids')
if limit is not None and limit <= 0:
raise InvalidArgument('limit must be > 0')
return CommandIterator(
self,
CommandType.message,
query,
limit,
command_ids,
applications=applications,
application=application,
)
def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference:
"""Creates a :class:`~discord.MessageReference` from the current message.

75
discord/user.py

@ -24,15 +24,15 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from copy import copy
from typing import Any, Dict, List, Optional, Type, TypeVar, TYPE_CHECKING, Union
import discord.abc
from .asset import Asset
from .colour import Colour
from .enums import DefaultAvatar, HypeSquadHouse, PremiumType, RelationshipAction, RelationshipType, try_enum, UserFlags
from .errors import ClientException, NotFound
from .enums import CommandType, DefaultAvatar, HypeSquadHouse, PremiumType, RelationshipAction, RelationshipType, try_enum, UserFlags
from .errors import ClientException, InvalidArgument, NotFound
from .flags import PublicUserFlags
from .iterators import FakeCommandIterator
from .object import Object
from .relationship import Relationship
from .settings import UserSettings
@ -531,7 +531,6 @@ class BaseUser(_UserTag):
:class:`bool`
Indicates if the user is mentioned in the message.
"""
if message.mention_everyone:
return True
@ -1099,6 +1098,69 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable):
data: DMChannelPayload = await state.http.start_private_message(self.id)
return state.add_dm_channel(data)
def user_commands(
self,
query: Optional[str] = None,
*,
limit: Optional[int] = None,
command_ids: Optional[List[int]] = [],
**kwargs,
):
"""Returns an iterator that allows you to see what user commands are available to use on this user.
Only available on bots.
.. note::
All parameters here are faked, as the only way to get commands in a DM is to fetch them all at once.
Because of this, some are silently ignored. The ones below currently work.
It is recommended to not pass any parameters to this iterator.
Examples
---------
Usage ::
async for command in user.user_commands():
print(command.name)
Flattening into a list ::
commands = await user.user_commands().flatten()
# commands is now a list of UserCommand...
All parameters are optional.
Parameters
----------
query: Optional[:class:`str`]
The query to search for.
limit: Optional[:class:`int`]
The maximum number of commands to send back. Defaults to ``None`` to iterate over all results. Must be at least 1.
command_ids: Optional[List[:class:`int`]]
List of command IDs to search for. If the command doesn't exist it won't be returned.
Raises
------
:exc:`.InvalidArgument`
The user is not a bot.
The limit was not > 0.
Both query and command_ids were passed.
:exc:`.HTTPException`
Getting the commands failed.
Yields
-------
:class:`.UserCommand`
A user command.
"""
if query and command_ids:
raise InvalidArgument('Cannot specify both query and command_ids')
if limit is not None and limit <= 0:
raise InvalidArgument('limit must be > 0')
return FakeCommandIterator(self, CommandType.user, query, limit, command_ids)
def is_friend(self) -> bool:
""":class:`bool`: Checks if the user is your friend."""
r = self.relationship
@ -1201,7 +1263,10 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable):
data = await state.http.get_user_profile(user_id, with_mutual_guilds=with_mutuals)
if with_mutuals:
data['mutual_friends'] = await self.http.get_mutual_friends(user_id)
if not data['user'].get('bot', False):
data['mutual_friends'] = await self.http.get_mutual_friends(user_id)
else:
data['mutual_friends'] = []
profile = Profile(state, data)

Loading…
Cancel
Save