From b48f510e154e40b8ba3bef58e5c513fccf90c89c Mon Sep 17 00:00:00 2001 From: Zomatree <39768508+Zomatree@users.noreply.github.com> Date: Sat, 29 May 2021 05:15:46 +0100 Subject: [PATCH] Add invite targets for voice channel invites --- discord/abc.py | 21 ++++++++++ discord/appinfo.py | 83 ++++++++++++++++++++++++++++++++++------ discord/enums.py | 2 +- discord/http.py | 12 ++++++ discord/invite.py | 20 ++++++++-- discord/team.py | 22 ++++++++--- discord/types/appinfo.py | 66 ++++++++++++++++++++++++++++++++ discord/types/invite.py | 6 ++- discord/types/team.py | 43 +++++++++++++++++++++ docs/api.rst | 15 ++++++-- 10 files changed, 263 insertions(+), 27 deletions(-) create mode 100644 discord/types/appinfo.py create mode 100644 discord/types/team.py diff --git a/discord/abc.py b/discord/abc.py index 961545ed6..da1636f27 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -62,6 +62,7 @@ if TYPE_CHECKING: from .channel import CategoryChannel from .embeds import Embed from .message import Message, MessageReference + from .enums import InviteTarget SnowflakeTime = Union["Snowflake", datetime] @@ -1013,6 +1014,9 @@ class GuildChannel: max_uses: int = 0, temporary: bool = False, unique: bool = True, + target_type: Optional[InviteTarget] = None, + target_user: Optional[User] = None, + target_application_id: Optional[int] = None ) -> Invite: """|coro| @@ -1038,6 +1042,20 @@ class GuildChannel: invite. reason: Optional[:class:`str`] The reason for creating this invite. Shows up on the audit log. + target_type: Optional[:class:`InviteTarget`] + The type of target for the voice channel invite, if any. + + .. versionadded:: 2.0 + + target_user: Optional[:class:`User`] + The user whose stream to display for this invite, required if `target_type` is `TargetType.stream`. The user must be streaming in the channel. + + .. versionadded:: 2.0 + + target_application_id:: Optional[:class:`int`] + The id of the embedded application for the invite, required if `target_type` is `TargetType.embedded_application`. + + .. versionadded:: 2.0 Raises ------- @@ -1060,6 +1078,9 @@ class GuildChannel: max_uses=max_uses, temporary=temporary, unique=unique, + target_type=target_type.value if target_type else None, + target_user_id=target_user.id if target_user else None, + target_application_id=target_application_id ) return Invite.from_incomplete(data=data, state=self._state) diff --git a/discord/appinfo.py b/discord/appinfo.py index 40a4e4029..a7f8da82f 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -22,13 +22,22 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + from . import utils -from .user import User from .asset import Asset -from .team import Team + +if TYPE_CHECKING: + from .guild import Guild + from .types.appinfo import AppInfo as AppInfoPayload, PartialAppInfo as PartialAppInfoPayload + + from .state import ConnectionState __all__ = ( 'AppInfo', + 'PartialAppInfo', ) @@ -49,7 +58,7 @@ class AppInfo: .. versionadded:: 1.3 - description: Optional[:class:`str`] + description: :class:`str` The application description. bot_public: :class:`bool` Whether the bot can be invited by anyone or if it is locked @@ -122,9 +131,10 @@ class AppInfo: 'privacy_policy_url', ) - def __init__(self, state, data): - self._state = state + def __init__(self, state, data: AppInfoPayload): + from .team import Team + self._state = state self.id = int(data['id']) self.name = data['name'] self.description = data['description'] @@ -132,7 +142,7 @@ class AppInfo: self.rpc_origins = data['rpc_origins'] self.bot_public = data['bot_public'] self.bot_require_code_grant = data['bot_require_code_grant'] - self.owner = User(state=self._state, data=data['owner']) + self.owner = state.store_user(data['owner']) team = data.get('team') self.team = Team(state, team) if team else None @@ -148,7 +158,7 @@ class AppInfo: self.terms_of_service_url = data.get('terms_of_service_url') self.privacy_policy_url = data.get('privacy_policy_url') - def __repr__(self): + def __repr__(self) -> str: return ( f'<{self.__class__.__name__} id={self.id} name={self.name!r} ' f'description={self.description!r} public={self.bot_public} ' @@ -156,14 +166,14 @@ class AppInfo: ) @property - def icon(self): + def icon(self) -> Optional[Asset]: """Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any.""" if self._icon is None: return None return Asset._from_icon(self._state, self.id, self._icon, path='app') @property - def cover_image(self): + def cover_image(self) -> Optional[Asset]: """Optional[:class:`.Asset`]: Retrieves the cover image on a store embed, if any. This is only available if the application is a game sold on Discord. @@ -173,10 +183,61 @@ class AppInfo: return Asset._from_cover_image(self._state, self.id, self._cover_image) @property - def guild(self): + def guild(self) -> Optional[Guild]: """Optional[:class:`Guild`]: If this application is a game sold on Discord, this field will be the guild to which it has been linked .. versionadded:: 1.3 """ - return self._state._get_guild(int(self.guild_id)) + return self._state._get_guild(self.guild_id) + +class PartialAppInfo: + """Represents a partial AppInfo given by :func:`~GuildChannel.create_invite` + + .. versionadded:: 2.0 + + Attributes + ------------- + id: :class:`int` + The application ID. + name: :class:`str` + The application name. + description: :class:`str` + The application description. + 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 hex encoded key for verification in interactions and the + GameSDK's `GetTicket `_. + terms_of_service_url: Optional[:class:`str`] + The application's terms of service URL, if set. + privacy_policy_url: Optional[:class:`str`] + The application's privacy policy URL, if set. + """ + + __slots__ = ('_state', 'id', 'name', 'description', 'rpc_origins', 'summary', 'verify_key', 'terms_of_service_url', 'privacy_policy_url', '_icon') + + def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload): + self._state = state + self.id = int(data['id']) + self.name = data['name'] + self._icon = data.get('icon') + self.description = data['description'] + self.rpc_origins = data.get('rpc_origins') + self.summary = data['summary'] + self.verify_key = data['verify_key'] + self.terms_of_service_url = data.get('terms_of_service_url') + self.privacy_policy_url = data.get('privacy_policy_url') + + def __repr__(self) -> str: + return f'<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r}>' + + @property + def icon(self) -> Optional[Asset]: + """Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any.""" + if self._icon is None: + return None + return Asset._from_icon(self._state, self.id, self._icon, path='app') diff --git a/discord/enums.py b/discord/enums.py index fa86767db..aaa59b25b 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -431,7 +431,7 @@ class StickerType(Enum): class InviteTarget(Enum): unknown = 0 - stream = 1 + stream = 1 embedded_application = 2 class InteractionType(Enum): diff --git a/discord/http.py b/discord/http.py index 67529495b..c14cc8d49 100644 --- a/discord/http.py +++ b/discord/http.py @@ -981,6 +981,9 @@ class HTTPClient: max_uses: int = 0, temporary: bool = False, unique: bool = True, + target_type: Optional[int] = None, + target_user_id: Optional[int] = None, + target_application_id: Optional[int] = None ) -> Response[invite.Invite]: r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id) payload = { @@ -990,6 +993,15 @@ class HTTPClient: 'unique': unique, } + if target_type: + payload['target_type'] = target_type + + if target_user_id: + payload['target_user_id'] = target_user_id + + if target_application_id: + payload['target_application_id'] = str(target_application_id) + return self.request(r, reason=reason, json=payload) def get_invite(self, invite_id, *, with_counts=True, with_expiration=True): diff --git a/discord/invite.py b/discord/invite.py index 213ef5a44..8e60d1e8b 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -30,6 +30,7 @@ from .utils import parse_time, snowflake_time, _get_as_snowflake from .object import Object from .mixins import Hashable from .enums import ChannelType, VerificationLevel, InviteTarget, try_enum +from .appinfo import PartialAppInfo __all__ = ( 'PartialInviteChannel', @@ -277,12 +278,18 @@ class Invite(Hashable): channel: Union[:class:`abc.GuildChannel`, :class:`Object`, :class:`PartialInviteChannel`] The channel the invite is for. + target_type: :class:`InviteTarget` + The type of target for the voice channel invite. + + .. versionadded:: 2.0 + target_user: Optional[:class:`User`] - The target of this invite in the case of stream invites. + The user whose stream to display for this invite, if any. .. versionadded:: 2.0 - target_type: :class:`InviteTarget` - The invite's target type. + + target_application: Optional[:class:`PartialAppInfo`] + The embedded application the invite targets, if any. .. versionadded:: 2.0 """ @@ -303,6 +310,7 @@ class Invite(Hashable): '_state', 'approximate_member_count', 'approximate_presence_count', + 'target_application', 'expires_at', ) @@ -328,7 +336,11 @@ class Invite(Hashable): self.channel = data.get('channel') target_user_data = data.get('target_user') self.target_user = None if target_user_data is None else self._state.store_user(target_user_data) - self.target_type = try_enum(InviteTarget, data.get('target_type', 0)) + + self.target_type = try_enum(InviteTarget, data.get("target_type", 0)) + + application = data.get('target_application') + self.target_application = PartialAppInfo(data=application, state=state) if application else None @classmethod def from_incomplete(cls, *, state, data): diff --git a/discord/team.py b/discord/team.py index 5e47770b0..94cf6e59d 100644 --- a/discord/team.py +++ b/discord/team.py @@ -22,11 +22,21 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations + from . import utils from .user import BaseUser from .asset import Asset from .enums import TeamMembershipState, try_enum +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .types.team import ( + Team as TeamPayload, + TeamMember as TeamMemberPayload, + ) + __all__ = ( 'Team', 'TeamMember', @@ -52,7 +62,7 @@ class Team: __slots__ = ('_state', 'id', 'name', '_icon', 'owner_id', 'members') - def __init__(self, state, data): + def __init__(self, state, data: TeamPayload): self._state = state self.id = int(data['id']) @@ -61,18 +71,18 @@ class Team: 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): + def __repr__(self) -> str: return f'<{self.__class__.__name__} id={self.id} name={self.name}>' @property - def icon(self): + def icon(self) -> Optional[Asset]: """Optional[:class:`.Asset`]: Retrieves the team's icon asset, if any.""" if self._icon is None: return None return Asset._from_icon(self._state, self.id, self._icon, path='team') @property - def owner(self): + def owner(self) -> Optional[TeamMember]: """Optional[:class:`TeamMember`]: The team's owner.""" return utils.get(self.members, id=self.owner_id) @@ -120,13 +130,13 @@ class TeamMember(BaseUser): __slots__ = BaseUser.__slots__ + ('team', 'membership_state', 'permissions') - def __init__(self, team, state, data): + def __init__(self, team: Team, state, data: TeamMemberPayload): self.team = team self.membership_state = try_enum(TeamMembershipState, data['membership_state']) self.permissions = data['permissions'] super().__init__(state=state, data=data['user']) - def __repr__(self): + def __repr__(self) -> str: return ( f'<{self.__class__.__name__} id={self.id} name={self.name!r} ' f'discriminator={self.discriminator!r} membership_state={self.membership_state!r}>' diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py new file mode 100644 index 000000000..d223837fa --- /dev/null +++ b/discord/types/appinfo.py @@ -0,0 +1,66 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present 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 __future__ import annotations + +from typing import TypedDict, List, Optional + +from .user import User +from .team import Team +from .snowflake import Snowflake + +class BaseAppInfo(TypedDict): + id: Snowflake + name: str + verify_key: str + icon: Optional[str] + summary: str + description: str + +class _AppInfoOptional(TypedDict, total=False): + team: Team + guild_id: Snowflake + primary_sku_id: Snowflake + slug: str + terms_of_service_url: str + privacy_policy_url: str + hook: bool + max_participants: int + +class AppInfo(BaseAppInfo, _AppInfoOptional): + rpc_origins: List[str] + owner: User + bot_public: bool + bot_require_code_grant: bool + +class _PartialAppInfoOptional(TypedDict, total=False): + rpc_origins: List[str] + cover_image: str + hook: bool + terms_of_service_url: str + privacy_policy_url: str + max_participants: int + +class PartialAppInfo(_PartialAppInfoOptional, BaseAppInfo): + pass diff --git a/discord/types/invite.py b/discord/types/invite.py index 25466158b..3760f0fab 100644 --- a/discord/types/invite.py +++ b/discord/types/invite.py @@ -29,15 +29,17 @@ from typing import Literal, TypedDict from .guild import InviteGuild, _GuildPreviewUnique from .channel import PartialChannel from .user import PartialUser +from .appinfo import PartialAppInfo -TargetUserType = Literal[1] +InviteTargetType = Literal[1, 2] class _InviteOptional(TypedDict, total=False): guild: InviteGuild inviter: PartialUser target_user: PartialUser - target_user_type: TargetUserType + target_type: InviteTargetType + target_application: PartialAppInfo class _InviteMetadata(TypedDict, total=False): diff --git a/discord/types/team.py b/discord/types/team.py new file mode 100644 index 000000000..e8cd524f8 --- /dev/null +++ b/discord/types/team.py @@ -0,0 +1,43 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present 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 __future__ import annotations + +from typing import TypedDict, List + +from .user import PartialUser +from .snowflake import Snowflake + +class TeamMember(TypedDict): + user: PartialUser + membership_state: int + permissions: List[str] + team_id: Snowflake + +class Team(TypedDict): + id: Snowflake + name: str + owner_id: Snowflake + members: List[TeamMember] + icon: str diff --git a/docs/api.rst b/docs/api.rst index 62764a23d..649e8d339 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -64,6 +64,14 @@ AppInfo .. autoclass:: AppInfo() :members: +PartialAppInfo +~~~~~~~~~~~~~~~ + +.. attributetable:: PartialAppInfo + +.. autoclass:: PartialAppInfo() + :members: + Team ~~~~~ @@ -2076,7 +2084,7 @@ of :class:`enum.Enum`. .. class:: InviteTarget - Represents the type of target an invite contains. + Represents the invite type for voice channel invites. .. versionadded:: 2.0 @@ -2086,11 +2094,11 @@ of :class:`enum.Enum`. .. attribute:: stream - The invite targets a stream. + A stream invite that targets a user. .. attribute:: embedded_application - The invite targets an embedded application activity. + A stream invite that targets an embedded application. .. class:: VideoQualityMode @@ -2106,6 +2114,7 @@ of :class:`enum.Enum`. Represents full camera video quality. + Async Iterator ----------------