diff --git a/discord/__init__.py b/discord/__init__.py index c206f650f66f..16c700489fc0 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -72,6 +72,8 @@ from .poll import * from .soundboard import * from .subscription import * +from .clan import * +from .member_verification import * class VersionInfo(NamedTuple): diff --git a/discord/asset.py b/discord/asset.py index e3422f3110d4..84717e07e988 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -346,6 +346,24 @@ def _from_user_banner(cls, state: _State, user_id: int, banner_hash: str) -> Sel animated=animated, ) + @classmethod + def _from_clan_badge(cls, state: _State, guild_id: int, badge_hash: str) -> Self: + return cls( + state, + url=f'{cls.BASE}/clan-badges/{guild_id}/{badge_hash}.png', + key=badge_hash, + animated=False, + ) + + @classmethod + def _from_clan_banner(cls, state: _State, guild_id: int, banner_hash: str) -> Self: + return cls( + state, + url=f'{cls.BASE}/clan-banners/{guild_id}/{banner_hash}.png', + key=banner_hash, + animated=False, + ) + def __str__(self) -> str: return self._url diff --git a/discord/clan.py b/discord/clan.py new file mode 100644 index 000000000000..14c1f9a7699a --- /dev/null +++ b/discord/clan.py @@ -0,0 +1,389 @@ +""" +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 TYPE_CHECKING, Dict, List, Optional + +from . import utils +from .enums import ClanBannerStyle, ClanPlayStyle, try_enum, ClanBadgeType +from .mixins import Hashable +from .object import Object +from .colour import Colour +from .asset import Asset + +if TYPE_CHECKING: + + from .guild import Guild + from .state import ConnectionState + + from .types.clan import ( + PartialClan as PartialClanPayload, + Clan as ClanPayload, + UserClan as UserClanPayload, + ) + +__all__ = ('UserClan', 'PartialClan', 'Clan') + + +class UserClan: + """Represents a partial clan accessible via a user. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two user clans are equal. + + .. describe:: x != y + + Checks if two user clans are not equal. + + .. versionadded:: 2.5 + + Attributes + ---------- + enabled: :class:`bool` + Whether the user is displaying their clan tag. + guild_id: Optional[:class:`int`] + The guild ID the clan is from. + + .. note:: + + This will be ``None`` if :attr:`.enabled` is ``False``. + tag: Optional[:class:`str`] + The clan tag. + + .. note:: + + This will be ``None`` if :attr:`.enabled` is ``False``. + """ + + __slots__ = ( + '_state', + 'guild_id', + 'enabled', + 'tag', + '_badge_hash', + ) + + def __init__(self, *, data: UserClanPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'identity_guild_id') + self.enabled: bool = data['identity_enabled'] + self.tag: Optional[str] = data.get('tag') + self._badge_hash: Optional[str] = data.get('badge') + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and self.tag == other.tag and self.guild_id == other.guild_id + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def __repr__(self) -> str: + return f'' + + @property + def guild(self) -> Optional[Guild]: + """Optional[:class:`Guild`]: Returns the cached guild this clan is from.""" + return self._state._get_guild(self.guild_id) + + @property + def badge(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the clan badge asset. + + .. note:: + + This will be ``None`` if :attr:`.enabled` is ``False``. + """ + if self._badge_hash is None or self.guild_id is None: + return None + return Asset._from_clan_badge(self._state, self.guild_id, self._badge_hash) + + +class PartialClan: + """Represents a partial clan. + + .. versionadded:: 2.5 + + Attributes + ---------- + tag: :class:`str` + The clan tag. + badge_type: Optional[:class:`ClanBadgeType`] + The clan badge type, or ``None``. + """ + + __slots__ = ( + '_state', + 'tag', + 'badge_type', + ) + + def __init__(self, *, data: PartialClanPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + self.tag: str = data['tag'] + try: + self.badge_type: Optional[ClanBadgeType] = try_enum(ClanBadgeType, data['badge']) + except KeyError: + self.badge_type = None + + +class Clan(Hashable, PartialClan): + """Represents a clan. + + .. container:: operations + + .. describe:: x == y + + Checks if two clans are equal. + + .. describe:: x != y + + Checks if two clans are not equal. + + .. describe:: hash(x) + + Returns the clan's hash. + + .. describe:: str(x) + + Returns the clan's name. + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`int` + The guild ID. + name: :class:`str` + The guild name. + tag: :class:`str` + The clan tag. + description: :class:`str` + The clan description. + + .. note:: + + This can be different than the guild's description. + member_count: Optional[:class:`int`] + An approximate count of the total members in the guild, or ``None``. + play_style: :class:`ClanPlayStyle` + The clan play style. + badge_type: Optional[:class:`ClanBadgeType`] + The clan badge type, or ``None``. + banner_style: Optional[:class:`ClanBannerStyle`] + The clan banner type, or ``None``. + """ + + __slots__ = ( + 'id', + 'name', + 'description', + 'play_style', + 'member_count', + 'banner_style', + '_games', + '_search_terms', + '_badge_hash', + '_banner_hash', + '_badge_primary_colour', + '_badge_secondary_colour', + '_banner_primary_colour', + '_banner_secondary_colour', + '_wildcard_descriptors', + '_verification_form', + ) + + if TYPE_CHECKING: + id: int + name: str + description: str + play_style: ClanPlayStyle + member_count: Optional[int] + banner_style: Optional[ClanBannerStyle] + _games: Dict[int, Object] + _search_terms: List[str] + _badge_hash: Optional[str] + _banner_hash: Optional[str] + _badge_primary_colour: Optional[str] + _badge_secondary_colour: Optional[str] + _banner_primary_colour: Optional[str] + _banner_secondary_colour: Optional[str] + + def __init__(self, *, data: ClanPayload, state: ConnectionState) -> None: + super().__init__(data=data, state=state) # type: ignore + + self.id: int = int(data['id']) + self._update(data) + + def _update(self, data: ClanPayload) -> None: + self.name: str = data['name'] + self.description: str = data['description'] + self.member_count: Optional[int] = data.get('member_count') + self.play_style: ClanPlayStyle = try_enum(ClanPlayStyle, data.get('play_style', 0)) + + try: + self.banner_style: Optional[ClanBannerStyle] = try_enum(ClanBannerStyle, data['banner']) + except KeyError: + self.banner_style = None + + self._games: Dict[int, Object] = {int(g): Object(int(g)) for g in data.get('game_application_ids', [])} + self._search_terms: List[str] = data.get('search_terms', []) + self._badge_hash: Optional[str] = data.get('badge_hash') + self._banner_hash: Optional[str] = data.get('banner_hash') + self._badge_primary_colour: Optional[str] = data.get('badge_color_primary') + self._badge_secondary_colour: Optional[str] = data.get('badge_color_secondary') + self._banner_primary_colour: Optional[str] = data.get('brand_color_primary') + self._banner_secondary_colour: Optional[str] = data.get('brand_color_secondary') + self._wildcard_descriptors: List[str] = data.get('wildcard_descriptors', []) + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and self.id == other.id + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return f'' + + @property + def guild(self) -> Optional[Guild]: + """Optional[:class:`Guild`]: Returns the respective guild of this clan.""" + return self._state._get_guild(self.id) + + @property + def games(self) -> List[Object]: + """List[:class:`Object`]: Returns a list of objects that represent the games + the clan plays. + """ + return list(self._games.values()) + + @property + def search_terms(self) -> List[str]: + """List[:class:`str`]: Returns a read-only list of the interests, topics, + or traits for the clan. + """ + return self._search_terms.copy() + + @property + def wildcard_descriptors(self) -> List[str]: + """List[:class:`str`]: Returns a read-only list of the terms that describe the + clan. + """ + return self._wildcard_descriptors.copy() + + @property + def badge_primary_colour(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: A property that returns the clan badge primary colour. + + There is an alias for this named :attr:`badge_primary_color`. + """ + + if self._badge_primary_colour is None: + return None + return Colour.from_str(self._badge_primary_colour) + + @property + def badge_secondary_colour(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: A property that returns the clan badge secondary colour. + + There is an alias for this named :attr:`badge_secondary_color`. + """ + + if self._badge_secondary_colour is None: + return None + return Colour.from_str(self._badge_secondary_colour) + + @property + def badge_primary_color(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: A property that returns the clan badge primary color. + + There is an alias for this named :attr:`badge_primary_colour`. + """ + return self.badge_primary_colour + + @property + def badge_secondary_color(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: A property that returns the clan badge secondary color. + + There is an alias for this named :attr:`badge_secondary_colour`. + """ + return self.badge_secondary_colour + + @property + def banner_primary_colour(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: A property that returns the clan banner primary colour. + + There is an alias for this named :attr:`banner_primary_color`. + """ + + if self._banner_primary_colour is None: + return None + return Colour.from_str(self._banner_primary_colour) + + @property + def banner_secondary_colour(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: A property that returns the clan banner secondary colour. + + There is an alias for this named :attr:`banner_secondary_color`. + """ + + if self._banner_secondary_colour is None: + return None + return Colour.from_str(self._banner_secondary_colour) + + @property + def banner_primary_color(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: A property that returns the clan banner primary color. + + There is an alias for this named :attr:`banner_primary_colour`. + """ + return self.banner_primary_colour + + @property + def banner_secondary_color(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: A property that returns the clan banner secondary color. + + There is an alias for this named :attr:`banner_secondary_colour`. + """ + return self.banner_secondary_colour + + @property + def badge(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the badge asset, or ``None``.""" + if self._badge_hash is None: + return None + return Asset._from_clan_badge(self._state, self.id, self._badge_hash) + + @property + def banner(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the banner asset, or ``None``.""" + if self._banner_hash is None: + return None + return Asset._from_clan_banner(self._state, self.id, self._banner_hash) diff --git a/discord/enums.py b/discord/enums.py index 4fe5f3ffae1e..2f8e0312f82e 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -77,6 +77,10 @@ 'VoiceChannelEffectAnimationType', 'SubscriptionStatus', 'MessageReferenceType', + 'ClanPlayStyle', + 'ClanBadgeType', + 'ClanBannerStyle', + 'MemberVerificationFieldType', ) @@ -862,6 +866,59 @@ class SubscriptionStatus(Enum): inactive = 2 +class ClanPlayStyle(Enum): + none = 0 + social = 1 + casual = 2 + competitive = 3 + creative = 4 + very_competitive = 5 + + +class ClanBadgeType(Enum): + sword = 0 + water_drop = 1 + skull = 2 + toadstool = 3 + moon = 4 + lightning = 5 + leaf = 6 + heart = 7 + fire = 8 + compass = 9 + crosshairs = 10 + flower = 11 + force = 12 + gem = 13 + lava = 14 + psychic = 15 + smoke = 16 + snow = 17 + sound = 18 + sun = 19 + wind = 20 + + +class ClanBannerStyle(Enum): + night_sky = 0 + castle = 1 + world_map = 2 + sea_foam = 3 + warp_tunnel = 4 + house = 5 + height_map = 6 + mesh = 7 + spatter = 8 + + +class MemberVerificationFieldType(Enum): + terms = "TERMS" + text_input = "TEXT_INPUT" + paragraph = "PARAGRAPH" + multiple_choice = "MULTIPLE_CHOICE" + # verification = "VERIFICATION" (deprecated) + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/guild.py b/discord/guild.py index fc39179abeb2..1c1f3d01b424 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -95,6 +95,8 @@ from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji from .soundboard import SoundboardSound +from .clan import Clan +from .member_verification import MemberVerificationForm __all__ = ( @@ -4670,3 +4672,72 @@ async def create_soundboard_sound( data = await self._state.http.create_soundboard_sound(self.id, reason=reason, **payload) return SoundboardSound(guild=self, state=self._state, data=data) + + def has_clan(self) -> bool: + """:class:`bool`: Whether there is a clan available for this guild. + + .. versionadded:: 2.5 + """ + return 'CLAN' in self.features + + async def fetch_clan(self) -> Clan: + """|coro| + + Fetches this guild's clan. + + .. versionadded:: 2.5 + + Raises + ------ + ClientException + Guild does not have a clan. + HTTPException + An error occurred while fetching the clan info. + + Returns + ------- + :class:`Clan` + This guild's clan. + """ + + if not self.has_clan(): + raise ClientException('Guild does not have a clan') + + data = await self._state.http.get_clan(self.id) + + return Clan(data=data, state=self._state) + + async def fetch_member_verification_form( + self, *, with_guild: bool = MISSING, invite: Union[str, Invite] = MISSING + ) -> Optional[MemberVerificationForm]: + """|coro| + + Fetches the current guild member verification form. + + .. versionadded:: 2.5 + + Parameters + ---------- + with_guild: :class:`bool` + Whether to include a guild snapshot on the member verification form. + invite: Union[:class:`str`, :class:`Invite`] + The invite code the member verification form is fetched from. + + Raises + ------ + HTTPException + Fetching the member verification form failed. + + Returns + ------- + Optional[:class:`MemberVerificationForm`] + The member verification form, or ``None``. + """ + + invite_code = MISSING + + if invite is not MISSING: + invite_code = invite.code if isinstance(invite, Invite) else invite + + form = await self._state.http.get_guild_member_verification(self.id, with_guild=with_guild, invite_code=invite_code) + return MemberVerificationForm._from_data(data=form, state=self._state, guild=self) diff --git a/discord/http.py b/discord/http.py index c66132055413..a51c57c27851 100644 --- a/discord/http.py +++ b/discord/http.py @@ -95,6 +95,8 @@ voice, soundboard, subscription, + clan, + member_verification, ) from .types.snowflake import Snowflake, SnowflakeList @@ -1812,6 +1814,18 @@ def edit_widget( def edit_incident_actions(self, guild_id: Snowflake, payload: guild.IncidentData) -> Response[guild.IncidentData]: return self.request(Route('PUT', '/guilds/{guild_id}/incident-actions', guild_id=guild_id), json=payload) + def get_guild_member_verification( + self, guild_id: Snowflake, *, with_guild: bool = MISSING, invite_code: str = MISSING + ) -> Response[member_verification.MemberVerificationForm]: + params = {} + + if with_guild is not MISSING: + params['with_guild'] = with_guild + if invite_code is not MISSING: + params['invite_code'] = invite_code + + return self.request(Route('GET', '/guilds/{guild_id}/member-verification', guild_id=guild_id), params=params) + # Invite management def create_invite( @@ -2526,6 +2540,17 @@ def delete_entitlement(self, application_id: Snowflake, entitlement_id: Snowflak ), ) + # Clans + + def get_clan(self, guild_id: int) -> Response[clan.Clan]: + return self.request( + Route( + 'GET', + '/discovery/{guild_id}/clan', + guild_id=guild_id, + ), + ) + # Soundboard def get_soundboard_default_sounds(self) -> Response[List[soundboard.SoundboardDefaultSound]]: diff --git a/discord/member_verification.py b/discord/member_verification.py new file mode 100644 index 000000000000..543256bd5db9 --- /dev/null +++ b/discord/member_verification.py @@ -0,0 +1,302 @@ +""" +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 datetime import datetime +from typing import TYPE_CHECKING, List, Optional, Union + +from .enums import try_enum, MemberVerificationFieldType +from .utils import MISSING, parse_time + +if TYPE_CHECKING: + from typing_extensions import Self + + from .guild import Guild + from .state import ConnectionState + + from .types.member_verification import ( + MemberVerificationField as MemberVerificationFieldPayload, + MemberVerificationForm as MemberVerificationFormPayload, + ) + +__all__ = ( + 'MemberVerificationForm', + 'MemberVerificationField', + 'PartialMemberVerificationField', +) + + +class PartialMemberVerificationField: + """Represents a partial member verification form field. + + Parameters + ---------- + type: :class:`MemberVerificationFieldType` + The type of field. + label: :class:`str` + The field label. Can be up to 300 characters. + choices: List[:class:`str`] + The choices the user has available. Can have up to 8 items, and each one + can be up to 150 characters. + + Must be passed if ``type`` is :attr:`MemberVerificationFieldType.multiple_choice`. + values: Optional[List[:class:`str`]] + The rules that the user must agree to. Can have up to 16 items, and each one + can be up to 300 characters. + + Must be passed if ``type`` is :attr:`MemberVerificationFieldType.terms`. + required: :class:`bool` + Whether this field is required. + description: Optional[:class:`str`] + The field description. + automations: Optional[List[:class:`str`]] + ... + placeholder: Optional[:class:`str`] + The field placeholder. + """ + + if TYPE_CHECKING: + type: MemberVerificationFieldType + label: str + required: bool + _choices: List[str] + _values: Optional[List[str]] + description: Optional[str] + automations: Optional[List[str]] + _placeholder: Optional[str] + + __slots__ = ( + 'type', + 'label', + 'required', + '_choices', + '_values', + 'description', + 'automations', + '_placeholder', + ) + + def __init__( + self, + *, + type: MemberVerificationFieldType, + label: str, + required: bool, + choices: List[str] = MISSING, + values: Optional[List[str]] = MISSING, + description: Optional[str] = None, + automations: Optional[List[str]] = None, + placeholder: Optional[str] = MISSING, + ) -> None: + self.type: MemberVerificationFieldType = type + self.label: str = label + self.required: bool = required + self._choices: List[str] = choices + self._values: Optional[List[str]] = values + self.description: Optional[str] = description + self.automations: Optional[List[str]] = automations + self._placeholder: Optional[str] = placeholder + + @property + def choices(self) -> List[str]: + """List[:class:`str`]: The choices the user has available.""" + if self._choices is MISSING: + return [] + return self._choices + + @choices.setter + def choices(self, value: List[str]) -> None: + self._choices = value + + @property + def values(self) -> Optional[List[str]]: + """Optional[List[:class:`str`]]: The rules the user must agree to, or ``None``.""" + if self._values is MISSING: + return None + return self._values + + @values.setter + def values(self, value: Optional[List[str]]) -> None: + self._values = value + + @property + def placeholder(self) -> Optional[str]: + """Optional[:class:`str`]: Returns the field placeholder, or ``None``.""" + if self._placeholder is MISSING: + return None + return self._placeholder + + @placeholder.setter + def placeholder(self, value: Optional[str]) -> None: + self._placeholder = value + + def to_dict(self) -> MemberVerificationFieldPayload: + payload: MemberVerificationFieldPayload = { + 'field_type': self.type.value, + 'label': self.label, + 'required': self.required, + 'description': self.description, + 'automations': self.automations, + } + + if self._choices is not MISSING: + payload['choices'] = self._choices + + if self._values is not MISSING: + payload['values'] = self._values + + if self._placeholder is not MISSING: + payload['placeholder'] = self._placeholder + return payload + + +class MemberVerificationField(PartialMemberVerificationField): + """Represents a member verification form field. + + .. versionadded:: 2.5 + + Attributes + ---------- + type: :class:`MemberVerificationFieldType` + The type of field. + label: :class:`str` + The field label. Can be up to 300 characters. + response: Union[:class:`str`, :class:`int`, :class:`bool`] + The user input on the field. + + If ``type`` is :attr:`MemberVerificationFieldType.terms` then this should + be ``True``. + + If ``type`` is :attr:`MemberVerificationFieldType.multiple_choice` then this + represents the index of the selected choice. + required: :class:`bool` + Whether this field is required for a successful application. + description: Optional[:class:`str`] + The field description. + automations: List[:class:`str`] + ... + """ + + __slots__ = ( + '_state', + 'response', + ) + + if TYPE_CHECKING: + _state: ConnectionState + response: Optional[Union[str, int, bool]] + + def __init__(self, *, data: MemberVerificationFieldPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + self._update(data) + + def _update(self, data: MemberVerificationFieldPayload) -> None: + super().__init__( + type=try_enum(MemberVerificationFieldType, data['field_type']), + label=data['label'], + choices=data.get('choices', MISSING), + values=data.get('values', MISSING), + required=data['required'], + description=data['description'], + automations=data['automations'], + placeholder=data.get('placeholder', MISSING), + ) + try: + self.response: Optional[Union[str, int, bool]] = data['response'] + except KeyError: + self.response = None + + +class MemberVerificationForm: + """Represents a member verification form. + + Parameters + ---------- + fields: Sequence[:class:`PartialMemberVerificationField`] + The fields this form has. Can be up to 5 items. + description: Optional[:class:`str`] + A description of what the clan is about. Can be different + from guild description. Can be up to 300 characters. Defaults + to ``None``. + """ + + __slots__ = ( + '_guild', + '_last_modified', + 'fields', + 'description', + ) + + def __init__( + self, + *, + fields: List[PartialMemberVerificationField], + description: Optional[str] = None, + ) -> None: + self.fields: List[PartialMemberVerificationField] = fields + self.description: Optional[str] = description + + self._guild: Optional[Guild] = None + self._last_modified: Optional[datetime] = None + + @classmethod + def _from_data(cls, *, data: MemberVerificationFormPayload, state: ConnectionState, guild: Optional[Guild]) -> Self: + self = cls( + fields=[MemberVerificationField(data=f, state=state) for f in data['form_fields']], + description=data.get('description'), + ) + if guild: + self._guild = guild + else: + # If guild is misteriously None then we use the guild preview + # the data offers us. + guild_data = data.get('guild') + + if guild_data is not None: + from .guild import Guild # circular import + + self._guild = Guild(data=guild_data, state=state) # type: ignore + + self._last_modified = parse_time(data.get('version')) + + return self + + @property + def guild(self) -> Optional[Guild]: + """Optional[:class:`Guild`]: The guild this member verification is for.""" + return self._guild + + @property + def last_modified_at(self) -> Optional[datetime]: + """Optional[:class:`datetime.datetime`]: The timestamp at which the verification + has been latest modified, or ``None``. + """ + return self._last_modified + + def to_dict(self) -> MemberVerificationFieldPayload: + return { + 'form_fields': [f.to_dict() for f in self.fields], # type:ignore + 'description': self.description, + } diff --git a/discord/types/clan.py b/discord/types/clan.py new file mode 100644 index 000000000000..99a91fea0a2d --- /dev/null +++ b/discord/types/clan.py @@ -0,0 +1,112 @@ +""" +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 TYPE_CHECKING, List, Literal, Optional, TypedDict + +from .snowflake import Snowflake, SnowflakeList + +if TYPE_CHECKING: + from typing_extensions import NotRequired + +ClanBadge = Literal[ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, +] +ClanBanner = Literal[ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, +] +ClanPlayStyle = Literal[ + 0, + 1, + 2, + 3, + 4, + 5, +] + + +class PartialClan(TypedDict): + tag: str + badge: ClanBadge + + +# We override almost everything because some may be missing on +# full clan objects. +class Clan(PartialClan): + id: Snowflake + name: str + icon_hash: Optional[str] + member_count: int + description: str + play_style: NotRequired[ClanPlayStyle] + search_terms: NotRequired[List[str]] + game_application_ids: NotRequired[SnowflakeList] + badge: NotRequired[ClanBadge] + badge_hash: NotRequired[str] + badge_color_primary: NotRequired[str] + badge_color_secondary: NotRequired[str] + banner: NotRequired[ClanBanner] + banner_hash: NotRequired[str] + brand_color_primary: NotRequired[str] + brand_color_seconday: NotRequired[str] + wildcard_descriptors: NotRequired[List[str]] + + +# This is not the same as a partial clan as +# the badge the PartialClan provides is one +# of the enum members, but here is the hash +# pretty weird though lol +class UserClan(TypedDict): + tag: Optional[str] + badge: Optional[str] + identity_guild_id: Optional[Snowflake] + identity_enabled: bool diff --git a/discord/types/member_verification.py b/discord/types/member_verification.py new file mode 100644 index 000000000000..7253265f1ee0 --- /dev/null +++ b/discord/types/member_verification.py @@ -0,0 +1,58 @@ +""" +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 TYPE_CHECKING, List, Literal, Optional, TypedDict, Union + +from .guild import GuildPreview + +if TYPE_CHECKING: + from typing_extensions import NotRequired + +MemberVerificationFieldType = Literal[ + 'TERMS', + 'MULTIPLE_CHOICE', + 'TEXT_INPUT', + 'PARAGRAPH', + # 'VERIFICATION', +] + + +class MemberVerificationField(TypedDict): + field_type: MemberVerificationFieldType + label: str + choices: NotRequired[List[str]] + values: NotRequired[Optional[List[str]]] + response: NotRequired[Union[str, int, bool]] + required: bool + description: Optional[str] + automations: Optional[List[str]] + placeholder: NotRequired[Optional[str]] + + +class MemberVerificationForm(TypedDict): + version: str + form_fields: List[MemberVerificationField] + description: Optional[str] + guild: Optional[GuildPreview] diff --git a/discord/types/user.py b/discord/types/user.py index 1f027ce9d9ac..afe96d558a94 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -23,6 +23,7 @@ """ from .snowflake import Snowflake +from .clan import UserClan from typing import Literal, Optional, TypedDict from typing_extensions import NotRequired @@ -54,3 +55,4 @@ class User(PartialUser, total=False): flags: int premium_type: PremiumType public_flags: int + clan: NotRequired[Optional[UserClan]] diff --git a/discord/user.py b/discord/user.py index c5391372aa58..ea9801ba2a70 100644 --- a/discord/user.py +++ b/discord/user.py @@ -32,6 +32,7 @@ from .enums import DefaultAvatar from .flags import PublicUserFlags from .utils import snowflake_time, _bytes_to_base64_data, MISSING, _get_as_snowflake +from .clan import UserClan if TYPE_CHECKING: from typing_extensions import Self @@ -71,6 +72,7 @@ class BaseUser(_UserTag): '_public_flags', '_state', '_avatar_decoration_data', + 'clan', ) if TYPE_CHECKING: @@ -86,6 +88,7 @@ class BaseUser(_UserTag): _accent_colour: Optional[int] _public_flags: int _avatar_decoration_data: Optional[AvatarDecorationData] + clan: Optional[UserClan] def __init__(self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload]) -> None: self._state = state @@ -124,6 +127,12 @@ def _update(self, data: Union[UserPayload, PartialUserPayload]) -> None: self.system = data.get('system', False) self._avatar_decoration_data = data.get('avatar_decoration_data') + clan = data.get('clan') + self.clan = None + + if clan: + self.clan = UserClan(data=clan, state=self._state) + @classmethod def _copy(cls, user: Self) -> Self: self = cls.__new__(cls) # bypass __init__ @@ -516,6 +525,8 @@ class User(BaseUser, discord.abc.Messageable): Specifies if the user is a bot account. system: :class:`bool` Specifies if the user is a system user (i.e. represents Discord officially). + clan: Optional[:class:`UserClan`] + The clan the user belongs to. """ __slots__ = ('__weakref__',) diff --git a/docs/api.rst b/docs/api.rst index 338368910145..df9f5a4259df 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3828,6 +3828,194 @@ of :class:`enum.Enum`. An alias for :attr:`.reply`. + +.. class:: ClanPlayStyle + + Represents a clan play style. + + .. versionadded:: 2.5 + + .. attribute:: none + + The clan has no play style. + + .. attribute:: social + + A social clan. Also described as "Very Casual" on the UI. + + .. attribute:: casual + + A casual clan. + + .. attribute:: competitive + + A competitive clan. + + .. attribute:: creative + + A creative clan. + + .. attribute:: very_competitive + + A very competitive clan. + + +.. class:: ClanBadgeType + + Represents a clan badge type. + + .. attribute:: sword + + A sword icon badge. + + .. attribute:: water_drop + + A water drop icon badge. + + .. attribute:: skull + + A skull icon badge. + + .. attribute:: toadstool + + A toadstool icon badge. + + .. attribute:: moon + + A moon icon badge. + + .. attribute:: lightning + + A lightning icon badge. + + .. attribute:: leaf + + A leaf icon badge. + + .. attribute:: heart + + A heart icon badge. + + .. attribute:: fire + + A fire icon badge. + + .. attribute:: compass + + A compass icon badge. + + .. attribute:: crosshairs + + A crosshair icon badge. + + .. attribute:: flower + + A flower icon badge. + + .. attribute:: force + + A force icon badge. + + .. attribute:: gem + + A gem icon badge. + + .. attribute:: lava + + A lava icon badge. + + .. attribute:: psychic + + A psychic icon badge. + + .. attribute:: smoke + + A smoke icon badge. + + .. attribute:: snow + + A snow icon badge. + + .. attribute:: sound + + A sound icon badge. + + .. attribute:: sun + + A sun icon badge. + + .. attribute:: wind + + A wind icon badge. + + +.. class:: ClanBannerStyle + + Represents a clan banner style. + + .. attribute:: night_sky + + A night sky icon banner. + + .. attribute:: castle + + A castle icon banner. + + .. attribute:: world_map + + A world map icon banner. + + .. attribute:: sea_foam + + A sea foam icon banner. + + .. attribute:: warp_tunnel + + A warp tunnel icon banner. + + .. attribute:: house + + A house icon banner. + + .. attribute:: height_map + + A height map icon banner. + + .. attribute:: mesh + + A mesh icon banner. + + .. attribute:: spatter + + A spatter icon banner. + + +.. class:: MemberVerificationFieldType + + Represents a member verification field type. + + .. attribute:: terms + + The field has a set of guidelines the user must agree to. Fields with this type must have + :attr:`PartialMemberVerificationField.values` filled. This field's :attr:`MemberVerificationField.response` + will be a :class:`bool` representing whether the user has accepted or not to the terms. + + .. attribute:: text_input + + The field asks for a short response from the user. This field's :attr:`MemberVerificationField.response` + will be a :class:`str` representing the user input value. + + .. attribute:: paragraph + + The field asks for a long response from the user. This field's :attr:`MemberVerificationField.response` + will be a :class:`str` representing the user input value. + + .. attribute:: multiple_choice + + The user must choose an option from a list. Fields with this type must have + :attr:`PartialMemberVerificationField.choices` filled. This field's :attr:`MemberVerificationField.response` + will be a :class:`int` representing the index of the selected option. + .. _discord-api-audit-logs: Audit Log Data @@ -4871,6 +5059,24 @@ Member .. automethod:: typing :async-with: +MemberVerificationField +~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: MemberVerificationField + +.. autoclass:: MemberVerificationField() + :members: + :inherited-members: + + +MemberVerificationForm +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: MemberVerificationForm + +.. autoclass:: MemberVerificationForm() + :members: + Spotify ~~~~~~~~ @@ -5038,6 +5244,15 @@ CategoryChannel :members: :inherited-members: +Clan +~~~~ + +.. attributetable:: Clan + +.. autoclass:: Clan() + :members: + :inherited-members: + DMChannel ~~~~~~~~~ @@ -5064,6 +5279,14 @@ GroupChannel .. automethod:: typing :async-with: +PartialClan +~~~~~~~~~~~ + +.. attributetable:: PartialClan + +.. autoclass:: PartialClan() + :members: + PartialInviteGuild ~~~~~~~~~~~~~~~~~~~ @@ -5369,16 +5592,24 @@ PollAnswer .. autoclass:: PollAnswer() :members: -.. _discord_api_data: - MessageSnapshot ~~~~~~~~~~~~~~~~~ .. attributetable:: MessageSnapshot -.. autoclass:: MessageSnapshot +.. autoclass:: MessageSnapshot() + :members: + +UserClan +~~~~~~~~ + +.. attributetable:: UserClan + +.. autoclass:: UserClan() :members: +.. _discord_api_data: + Data Classes -------------- @@ -5683,6 +5914,15 @@ CallMessage :members: +PartialMemberVerificationField +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: PartialMemberVerificationField + +.. autoclass:: PartialMemberVerificationField + :members: + + Exceptions ------------