From 3961e7ef6dc05925927dbd2f899661a2058fd070 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 21 Jun 2019 17:09:15 +0800 Subject: [PATCH] Support team members data in application info --- discord/appinfo.py | 48 ++++++++++++++++- discord/asset.py | 8 +++ discord/enums.py | 5 ++ discord/ext/commands/bot.py | 31 +++++++++-- discord/team.py | 100 ++++++++++++++++++++++++++++++++++++ docs/api.rst | 21 ++++++++ 6 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 discord/team.py diff --git a/discord/appinfo.py b/discord/appinfo.py index f38f40b91..ddf5ae05c 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -24,8 +24,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from . import utils from .user import User from .asset import Asset +from .team import Team class AppInfo: @@ -40,6 +42,8 @@ class AppInfo: The application name. owner: :class:`User` The application owner. + team: Optional[:class:`Team`] + The application's team. icon: Optional[:class:`str`] The icon hash, if it exists. description: Optional[:class:`str`] @@ -52,9 +56,28 @@ class AppInfo: grant flow to join. rpc_origins: Optional[List[:class:`str`]] A list of RPC origin URLs, if RPC is enabled. + summary: :class:`str` + If this application is a game sold on Discord, + this field will be the summary field for the store page of its primary SKU + verify_key: :class:`str` + The base64 encoded key for the GameSDK's GetTicket + guild_id: Optional[:class:`int`] + If this application is a game sold on Discord, + this field will be the guild to which it has been linked + primary_sku_id: Optional[:class:`int`] + If this application is a game sold on Discord, + this field will be the id of the "Game SKU" that is created, if exists + slug: Optional[:class:`str`] + If this application is a game sold on Discord, + this field will be the URL slug that links to the store page + cover_image: Optional[:class:`str`] + If this application is a game sold on Discord, + this field will be the hash of the image on store embeds """ __slots__ = ('_state', 'description', 'id', 'name', 'rpc_origins', - 'bot_public', 'bot_require_code_grant', 'owner', 'icon') + 'bot_public', 'bot_require_code_grant', 'owner', 'icon', + 'summary', 'verify_key', 'team', 'guild_id', 'primary_sku_id', + 'slug', 'cover_image') def __init__(self, state, data): self._state = state @@ -68,6 +91,18 @@ class AppInfo: self.bot_require_code_grant = data['bot_require_code_grant'] self.owner = User(state=self._state, data=data['owner']) + team = data.get('team') + self.team = Team(state, team) if team else None + + self.summary = data['summary'] + self.verify_key = data['verify_key'] + + self.guild_id = utils._get_as_snowflake(data, 'guild_id') + + self.primary_sku_id = utils._get_as_snowflake(data, 'primary_sku_id') + self.slug = data.get('slug') + self.cover_image = data.get('cover_image') + def __repr__(self): return '<{0.__class__.__name__} id={0.id} name={0.name!r} description={0.description!r} public={0.bot_public} ' \ 'owner={0.owner!r}>'.format(self) @@ -76,3 +111,14 @@ class AppInfo: def icon_url(self): """:class:`.Asset`: Retrieves the application's icon asset.""" return Asset._from_icon(self._state, self, 'app') + + @property + def cover_image_url(self): + """:class:`.Asset`: Retrieves the cover image on a store embed.""" + return Asset._from_cover_image(self._state, self) + + @property + def guild(self): + """Optional[:class:`Guild`]: If this application is a game sold on Discord, + this field will be the guild to which it has been linked""" + return self._state._get_guild(int(self.guild_id)) diff --git a/discord/asset.py b/discord/asset.py index 1d2ebbaa5..1e1c8a581 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -94,6 +94,14 @@ class Asset: url = 'https://cdn.discordapp.com/{0}-icons/{1.id}/{1.icon}.jpg'.format(path, object) return cls(state, url) + @classmethod + def _from_cover_image(cls, state, obj): + if obj.cover_image is None: + return cls(state) + + url = 'https://cdn.discordapp.com/app-assets/{0.id}/store/{0.cover_image}.jpg'.format(obj) + return cls(state, url) + @classmethod def _from_guild_image(cls, state, id, hash, key, *, format='webp', size=1024): if not utils.valid_icon_size(size): diff --git a/discord/enums.py b/discord/enums.py index 5fec3a02a..a7868bebc 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -47,6 +47,7 @@ __all__ = ( 'PremiumType', 'UserContentFilter', 'FriendFlags', + 'TeamMembershipState', 'Theme', ) @@ -392,6 +393,10 @@ class PremiumType(Enum): nitro_classic = 1 nitro = 2 +class TeamMembershipState(Enum): + invited = 1 + accepted = 2 + def try_enum(cls, val): """A function that tries to turn the value into enum ``cls``. diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 66e245dc3..b45f828bf 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -108,6 +108,16 @@ class BotBase(GroupMixin): self._help_command = None self.description = inspect.cleandoc(description) if description else '' self.owner_id = options.get('owner_id') + self.owner_ids = options.get('owner_ids', {}) + + if self.owner_id and self.owner_ids: + raise ValueError('Both owner_id and owner_ids are set.') + elif not isinstance(self.owner_id, int): + raise ValueError('owner_id is not an int.') + elif not isinstance(self.owner_ids, (set, list, tuple)): + raise ValueError('owner_ids is not a set, list or tuple.') + elif not all(isinstance(i, int) for i in self.owner_ids): + raise ValueError('owner_ids has to be an iterable of int.') if options.pop('self_bot', False): self._skip_check = lambda x, y: x != y @@ -284,6 +294,9 @@ class BotBase(GroupMixin): If an :attr:`owner_id` is not set, it is fetched automatically through the use of :meth:`~.Bot.application_info`. + The function also checks if the application is team-owned if + :attr:`owner_id` is not set. + Parameters ----------- user: :class:`.abc.User` @@ -295,11 +308,18 @@ class BotBase(GroupMixin): Whether the user is the owner. """ - if self.owner_id is None: + if self.owner_id: + return user.id == self.owner_id + elif self.owner_ids: + return user.id in self.owner_ids + else: app = await self.application_info() - self.owner_id = owner_id = app.owner.id - return user.id == owner_id - return user.id == self.owner_id + if app.team: + self.owner_ids = {m.id for m in app.team.members} + return user.id in self.owner_ids + else: + self.owner_id = owner_id = app.owner.id + return user.id == owner_id def before_invoke(self, coro): """A decorator that registers a coroutine as a pre-invoke hook. @@ -959,6 +979,9 @@ class Bot(BotBase, discord.Client): The ID that owns the bot. If this is not set and is then queried via :meth:`.is_owner` then it is fetched automatically using :meth:`~.Bot.application_info`. + owner_ids: Optional[:class:`set`] + The IDs that owns the bot. This is similar to `owner_id`. + If both `owner_id` and `owner_ids` are set, ValueError would be raised. """ pass diff --git a/discord/team.py b/discord/team.py new file mode 100644 index 000000000..5a7877969 --- /dev/null +++ b/discord/team.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +""" +The MIT License (MIT) + +Copyright (c) 2015-2019 Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from . import utils +from .user import User +from .asset import Asset +from .enums import TeamMembershipState, try_enum + + +class Team: + """Represents an application team for a bot provided by Discord. + + + Attributes + ------------- + id: :class:`int` + The team ID. + name: :class:`str` + The team name + icon: Optional[:class:`str`] + The icon hash, if it exists. + owner_id: :class:`int` + The team's owner ID. + members: List[:class:`TeamMember`] + A list of the members in the team + """ + __slots__ = ('_state', 'id', 'name', 'icon', 'owner_id', 'members') + + def __init__(self, state, data): + self._state = state + + self.id = utils._get_as_snowflake(data, 'id') + self.name = data['name'] + self.icon = data['icon'] + self.owner_id = utils._get_as_snowflake(data, 'owner_user_id') + self.members = [TeamMember(self, self._state, member) for member in data['members']] + + def __repr__(self): + return '<{0.__class__.__name__} id={0.id} name={0.name}>'.format(self) + + @property + def icon_url(self): + """:class:`.Asset`: Retrieves the team's icon asset.""" + return Asset._from_icon(self._state, self, 'team') + + @property + def owner(self): + """Optional[:class:`User`]: The team's owner, if available from the cache.""" + return self._state.get_user(self.owner_id) + + +class TeamMember: + """Represents a team member in a team. + + + Attributes + ------------- + team: :class:`team` + The team that the member is from. + membership_state: :class:`TeamMembershipState` + The membership state of the member (e.g. invited or accepted) + user: :class:`User` + The team member + """ + __slots__ = ('_state', 'team', 'membership_state', + 'permissions', 'user') + + def __init__(self, team, state, data): + self._state = state + self.team = team + + self.membership_state = try_enum(TeamMembershipState, data['membership_state']) + self.permissions = data['permissions'] + self.user = User(state=self._state, data=data['user']) + + def __repr__(self): + return '<{0.__class__.__name__} id={0.user.id} name={0.user.name!r}>'.format(self) diff --git a/docs/api.rst b/docs/api.rst index c80bc3044..e2a831000 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -42,6 +42,15 @@ Client .. autoclass:: AppInfo :members: +.. autoclass:: GameInfo + :members: + +.. autoclass:: Team + :members: + +.. autoclass:: TeamMember + :members: + Voice ------ @@ -1537,6 +1546,18 @@ of :class:`enum.Enum`. Represents the Dark theme on Discord. +.. class:: TeamMembershipState + + Represents the membership state of a team member retrieved through :func:Bot.application_info. + + .. attribue:: invited + + Represents an invited member. + + .. attribute:: accepted + + Represents a member currently in the team. + Async Iterator ----------------