Source code for interactions.api.models.member

from datetime import datetime
from typing import TYPE_CHECKING, Any, List, Optional, Union

from ...utils.attrs_utils import ClientSerializerMixin, convert_int, convert_list, define, field
from ...utils.missing import MISSING
from ...utils.utils import search_iterable
from ..error import LibraryException
from .channel import Channel
from .flags import Permissions
from .misc import AllowedMentions, File, IDMixin, Snowflake
from .role import Role
from .user import User

if TYPE_CHECKING:
    from ...client.models.component import ActionRow, Button, SelectMenu
    from .guild import Guild
    from .gw import VoiceState
    from .message import Attachment, Embed, Message

__all__ = ("Member",)


[docs]@define() class Member(ClientSerializerMixin, IDMixin): """ A class object representing the user of a guild, known as a "member." .. note:: ``pending`` and ``permissions`` only apply for members retroactively requiring to verify rules via. membership screening or lack permissions to speak. :ivar User user: The user of the guild. :ivar str nick: The nickname of the member. :ivar Optional[str] avatar: The hash containing the user's guild avatar, if applicable. :ivar List[int] roles: The list of roles of the member. :ivar datetime joined_at: The timestamp the member joined the guild at. :ivar datetime premium_since: The timestamp the member has been a server booster since. :ivar bool deaf: Whether the member is deafened. :ivar bool mute: Whether the member is muted. :ivar Optional[bool] pending: Whether the member is pending to pass membership screening. :ivar Optional[Permissions] permissions: Whether the member has permissions. :ivar Optional[str] communication_disabled_until: How long until they're unmuted, if any. """ user: Optional[User] = field(converter=User, default=None, add_client=True) nick: Optional[str] = field(default=None) _avatar: Optional[str] = field(default=None, discord_name="avatar", repr=False) roles: List[int] = field(converter=convert_list(int)) joined_at: datetime = field(converter=datetime.fromisoformat, repr=False) premium_since: Optional[datetime] = field( converter=datetime.fromisoformat, default=None, repr=False ) deaf: bool = field() mute: bool = field() is_pending: Optional[bool] = field(default=None, repr=False) pending: Optional[bool] = field(default=None, repr=False) permissions: Optional[Permissions] = field( converter=convert_int(Permissions), default=None, repr=False ) communication_disabled_until: Optional[datetime.isoformat] = field( converter=datetime.fromisoformat, default=None ) hoisted_role: Optional[Any] = field( default=None, repr=False ) # TODO: Investigate what this is for when documented by Discord. flags: int = field(repr=False) # TODO: Investigate what this is for when documented by Discord. def __getattr__(self, name): # Forward any attributes the user has to make it easier for devs try: return getattr(self.user, name) except AttributeError as e: raise AttributeError(f"Neither `User` nor `Member` have attribute {name}") from e @property def voice_state(self) -> Optional["VoiceState"]: """ .. versionadded:: 4.4.0 Returns the current voice state of the member, if any. :rtype: VoiceState """ if not self._client: raise LibraryException(code=13) from .gw import VoiceState return self._client.cache[VoiceState].get(self.id) @property def guild(self) -> Optional["Guild"]: """ .. versionadded:: 4.4.0 Attempts to get the guild the member is in. Only works then roles or nick or joined at is present and the guild is cached. :return: The guild this member belongs to. :rtype: Guild """ _id = self.guild_id from .guild import Guild if not _id or isinstance(_id, LibraryException): return return self._client.cache[Guild].get(_id, None) @property def guild_id(self) -> Optional[Union[Snowflake, LibraryException]]: """ .. versionadded:: 4.3.2 Attempts to get the guild ID the member is in. Only works when roles or nick or joined at is present and the guild is cached. :return: The ID of the guild this member belongs to. :rtype: Optional[Union[Snowflake, LibraryException]] """ if hasattr(self, "_guild_id"): return self._guild_id elif _id := self._extras.get("guild_id"): return Snowflake(_id) if not self._client: raise LibraryException(code=13) from .guild import Guild if self.roles: for guild in self._client.cache[Guild].values.values(): for role in guild.roles: if role.id in self.roles: self._extras["guild_id"] = int(guild.id) return guild.id possible_guilds: List[Guild] = [] if self.nick: def check(_member: Member): return _member.nick == self.nick and _member.id == self.id for guild in self._client.cache[Guild].values.values(): if len(search_iterable(guild.members, check=check)) > 0: possible_guilds.append(guild) if len(possible_guilds) == 1: self._extras["guild_id"] = int(possible_guilds[0].id) return possible_guilds[0].id elif not possible_guilds: possible_guilds = list(self._client.cache[Guild].values.values()) for guild in possible_guilds: for member in guild.members: if member.joined_at == self.joined_at: self._extras["guild_id"] = int(guild.id) return guild.id else: return LibraryException( code=12, message="the guild_id could not be retrieved, please insert one!" ) @property def avatar(self) -> Optional[str]: """Returns the user's avatar hash, if any.""" return self._avatar or getattr(self.user, "avatar", None) @property def id(self) -> Snowflake: """ .. versionadded:: 4.1.0 Returns the ID of the user. :return: The ID of the user :rtype: Snowflake """ return self.user.id if self.user else None @property def mention(self) -> str: """ .. versionadded:: 4.1.0 Returns a string that allows you to mention the given member. :return: The string of the mentioned member. :rtype: str """ return f"<@{'!' if self.nick else ''}{self.id}>" @property def name(self) -> str: """ .. versionadded:: 4.2.0 Returns the string of either the user's nickname or username. :return: The name of the member :rtype: str """ return self.nick or (self.user.username if self.user else None)
[docs] async def ban( self, guild_id: Optional[Union[int, Snowflake, "Guild"]] = MISSING, seconds: Optional[int] = 0, minutes: Optional[int] = MISSING, hours: Optional[int] = MISSING, days: Optional[int] = MISSING, reason: Optional[str] = None, ) -> None: """ .. versionadded:: 4.0.2 .. versionchanged:: 4.3.2 Method has been aligned to changes in the Discord API. You can now input days, hours, minutes and seconds, as long as it doesn't exceed 604800 seconds in total for deleting messages, instead of only days. .. versionchanged:: 4.3.2 ``guild_id`` is no longer required Bans the member from a guild. :param Optional[Union[int, Snowflake, Guild]] guild_id: The id of the guild to ban the member from :param Optional[int] seconds: Number of seconds to delete messages, from 0 to 604800. Defaults to 0 :param Optional[int] minutes: Number of minutes to delete messages, from 0 to 10080 :param Optional[int] hours: Number of hours to delete messages, from 0 to 168 :param Optional[int] days: Number of days to delete messages, from 0 to 7 :param Optional[str] reason: The reason of the ban """ if guild_id is MISSING: _guild_id = self.guild_id if isinstance(_guild_id, LibraryException): raise _guild_id else: _guild_id = ( int(guild_id) if isinstance(guild_id, (Snowflake, int)) else int(guild_id.id) ) if days is not MISSING: seconds += days * 24 * 3600 if hours is not MISSING: seconds += hours * 3600 if minutes is not MISSING: seconds += minutes * 60 if seconds > 604800: raise LibraryException( code=12, message="The amount of total seconds to delete messages exceeds the limit Discord provides (604800)", ) await self._client.create_guild_ban( guild_id=_guild_id, user_id=int(self.user.id), reason=reason, delete_message_seconds=seconds, )
[docs] async def kick( self, guild_id: Optional[Union[int, Snowflake, "Guild"]] = MISSING, reason: Optional[str] = None, ) -> None: """ .. versionadded:: 4.0.2 .. versionchanged:: 4.3.2 ``guild_id`` is no longer required. Kicks the member from a guild. :param Optional[Union[int, Snowflake, Guild]] guild_id: The id of the guild to kick the member from :param Optional[str] reason: The reason for the kick """ if not self._client: raise LibraryException(code=13) if guild_id is MISSING: _guild_id = self.guild_id if isinstance(_guild_id, LibraryException): raise _guild_id else: _guild_id = ( int(guild_id) if isinstance(guild_id, (Snowflake, int)) else int(guild_id.id) ) await self._client.create_guild_kick( guild_id=_guild_id, user_id=int(self.user.id), reason=reason, )
[docs] async def add_role( self, role: Union[Role, int, Snowflake], guild_id: Optional[Union[int, Snowflake, "Guild"]] = MISSING, reason: Optional[str] = None, ) -> None: """ .. versionadded:: 4.0.2 .. versionchanged:: 4.3.2 ``guild_id`` is no longer required. This method adds a role to a member. :param Union[Role, int, Snowflake] role: The role to add. Either ``Role`` object or role_id :param Optional[Union[int, Snowflake, Guild]] guild_id: The id of the guild to add the roles to the member :param Optional[str] reason: The reason why the roles are added """ if not self._client: raise LibraryException(code=13) _role = int(role.id) if isinstance(role, Role) else int(role) if guild_id is MISSING: _guild_id = self.guild_id if isinstance(_guild_id, LibraryException): raise _guild_id else: _guild_id = ( int(guild_id) if isinstance(guild_id, (Snowflake, int)) else int(guild_id.id) ) await self._client.add_member_role( guild_id=_guild_id, user_id=int(self.user.id), role_id=_role, reason=reason, )
[docs] async def remove_role( self, role: Union[Role, int], guild_id: Optional[Union[int, Snowflake, "Guild"]] = MISSING, reason: Optional[str] = None, ) -> None: """ .. versionadded:: 4.0.2 .. versionchanged:: 4.3.2 ``guild_id`` is no longer required. This method removes a role from a member. :param Union[Role, int] role: The role to remove. Either ``Role`` object or role_id :param Optional[Union[int, Snowflake, Guild]] guild_id: The id of the guild to remove the roles of the member :param Optional[str] reason: The reason why the roles are removed """ if not self._client: raise LibraryException(code=13) if guild_id is MISSING: _guild_id = self.guild_id if isinstance(_guild_id, LibraryException): raise _guild_id else: _guild_id = ( int(guild_id) if isinstance(guild_id, (Snowflake, int)) else int(guild_id.id) ) _role = int(role.id) if isinstance(role, Role) else int(role) await self._client.remove_member_role( guild_id=_guild_id, user_id=int(self.user.id), role_id=_role, reason=reason, )
[docs] async def send( self, content: Optional[str] = MISSING, *, components: Optional[ Union[ "ActionRow", "Button", "SelectMenu", List["ActionRow"], List["Button"], List["SelectMenu"], ] ] = MISSING, tts: Optional[bool] = MISSING, attachments: Optional[List["Attachment"]] = MISSING, files: Optional[Union[File, List[File]]] = MISSING, embeds: Optional[Union["Embed", List["Embed"]]] = MISSING, allowed_mentions: Optional[Union[AllowedMentions, dict]] = MISSING, ) -> "Message": """ .. versionadded:: 4.0.2 Sends a DM to the member. :param Optional[str] content: The contents of the message as a string or string-converted value. :param Optional[Union[ActionRow, Button, SelectMenu, List[ActionRow], List[Button], List[SelectMenu]]] components: A component, or list of components for the message. :param Optional[bool] tts: Whether the message utilizes the text-to-speech Discord programme or not. :param Optional[List[Attachment]] attachments: .. versionadded:: 4.3.0 The attachments to attach to the message. Needs to be uploaded to the CDN first :param Optional[Union[File, List[File]]] files: .. versionadded:: 4.2.0 A file or list of files to be attached to the message. :param Optional[Union[Embed, List[Embed]]] embeds: An embed, or list of embeds for the message. :param Optional[Union[AllowedMentions, dict]] allowed_mentions: The allowed mentions for the message. :return: The sent message as an object. :rtype: Message """ if not self._client: raise LibraryException(code=13) from ...client.models.component import _build_components from .message import Message _content: str = "" if content is MISSING else content _tts: bool = False if tts is MISSING else tts _attachments = [] if attachments is MISSING else [a._json for a in attachments] _embeds: list = ( [] if not embeds or embeds is MISSING else ([embed._json for embed in embeds] if isinstance(embeds, list) else [embeds._json]) ) _allowed_mentions: dict = ( {} if allowed_mentions is MISSING else allowed_mentions._json if isinstance(allowed_mentions, AllowedMentions) else allowed_mentions ) if not components or components is MISSING: _components = [] else: _components = _build_components(components=components) if not files or files is MISSING: _files = [] elif isinstance(files, list): _files = [file._json_payload(id) for id, file in enumerate(files)] else: _files = [files._json_payload(0)] files = [files] _files.extend(_attachments) payload = dict( content=_content, tts=_tts, attachments=_files, embeds=_embeds, components=_components, allowed_mentions=_allowed_mentions, ) channel = Channel(**await self._client.create_dm(recipient_id=int(self.user.id))) res = await self._client.create_message( channel_id=int(channel.id), payload=payload, files=files ) return Message(**res, _client=self._client)
[docs] async def modify( self, guild_id: Optional[Union[int, Snowflake, "Guild"]] = MISSING, nick: Optional[str] = MISSING, roles: Optional[List[int]] = MISSING, mute: Optional[bool] = MISSING, deaf: Optional[bool] = MISSING, channel_id: Optional[Union[Channel, int, Snowflake]] = MISSING, communication_disabled_until: Optional[datetime.isoformat] = MISSING, reason: Optional[str] = None, ) -> "Member": """ .. versionadded:: 4.0.2 .. versionchanged:: 4.3.2 ``guild_id`` is no longer required. Modifies the member of a guild. :param Optional[Union[int, Snowflake, Guild]] guild_id: The id of the guild to modify the member on :param Optional[str] nick: The nickname of the member :param Optional[List[int]] roles: A list of all role ids the member has :param Optional[bool] mute: whether the user is muted in voice channels :param Optional[bool] deaf: whether the user is deafened in voice channels :param Optional[Union[Channel, int, Snowflake]] channel_id: id of channel to move user to (if they are connected to voice) :param Optional[datetime.isoformat] communication_disabled_until: when the user's timeout will expire and the user will be able to communicate in the guild again (up to 28 days in the future) :param Optional[str] reason: The reason of the modifying :return: The modified member object :rtype: Member """ if not self._client: raise LibraryException(code=13) if guild_id is MISSING: _guild_id = self.guild_id if isinstance(_guild_id, LibraryException): raise _guild_id else: _guild_id = ( int(guild_id) if isinstance(guild_id, (int, Snowflake)) else int(guild_id.id) ) payload = {} if nick is not MISSING: payload["nick"] = nick if roles is not MISSING: payload["roles"] = roles if channel_id is not MISSING: payload["channel_id"] = ( int(channel_id.id) if isinstance(channel_id, Channel) else int(channel_id) ) if mute is not MISSING: payload["mute"] = mute if deaf is not MISSING: payload["deaf"] = deaf if communication_disabled_until is not MISSING: payload["communication_disabled_until"] = communication_disabled_until res = await self._client.modify_member( user_id=int(self.user.id), guild_id=_guild_id, payload=payload, reason=reason, ) self.update(res) return self
[docs] async def add_to_thread( self, thread_id: Union[int, Snowflake, Channel], ) -> None: """ .. versionadded:: 4.0.2 Adds the member to a thread. :param Union[int, Snowflake, Channel] thread_id: The id of the thread to add the member to """ if not self._client: raise LibraryException(code=13) _thread_id = int(thread_id.id) if isinstance(thread_id, Channel) else int(thread_id) await self._client.add_member_to_thread( user_id=int(self.user.id), thread_id=_thread_id, )
[docs] def get_avatar_url( self, guild_id: Optional[Union[int, Snowflake, "Guild"]] = MISSING ) -> Optional[str]: """ .. versionadded:: 4.2.0 .. versionchanged:: 4.3.0 Has been renamed to `get_avatar_url` instead of `get_member_avatar_url` .. versionchanged:: 4.3.2 ``guild_id`` is no longer required. Returns the URL of the member's avatar for the specified guild. :param Optional[Union[int, Snowflake, Guild]] guild_id: The id of the guild to get the member's avatar from :return: URL of the members's avatar (None will be returned if no avatar is set) :rtype: str """ if not self.avatar or self.avatar == self.user.avatar: return None if guild_id is MISSING: _guild_id = self.guild_id if isinstance(_guild_id, LibraryException): raise _guild_id else: _guild_id = ( int(guild_id) if isinstance(guild_id, (int, Snowflake)) else int(guild_id.id) ) url = f"https://cdn.discordapp.com/guilds/{_guild_id}/users/{int(self.user.id)}/avatars/{self.avatar}" url += ".gif" if self.avatar.startswith("a_") else ".png" return url
[docs] async def get_guild_permissions( self, guild_id: Optional[Union[int, Snowflake, "Guild"]] = MISSING ) -> Permissions: """ .. versionadded:: 4.3.2 Returns the permissions of the member for the specified guild. .. note:: The permissions returned by this function will not take into account role and user overwrites that can be assigned to channels or categories. If you need these overwrites, look into :meth:`.Channel.get_permissions_for`. :param Guild guild: The guild of the member :return: Base permissions of the member in the specified guild :rtype: Permissions """ from .guild import Guild if guild_id is MISSING: _guild_id = self.guild_id if isinstance(_guild_id, LibraryException): raise _guild_id else: _guild_id = int(guild_id.id) if isinstance(guild_id, Guild) else int(guild_id) guild = Guild(**await self._client.get_guild(int(_guild_id)), _client=self._client) if int(guild.owner_id) == int(self.id): return Permissions.ALL role_everyone = await guild.get_role(int(guild.id)) permissions = int(role_everyone.permissions) for role_id in self.roles: role = await guild.get_role(role_id) permissions |= int(role.permissions) if Permissions.ADMINISTRATOR in Permissions(permissions): return Permissions.ALL return Permissions(permissions)
[docs] async def has_permissions( self, *permissions: Union[int, Permissions], channel: Optional[Channel] = MISSING, guild_id: Optional[Union[int, Snowflake, "Guild"]] = MISSING, operator: str = "and", ) -> bool: r""" .. versionadded:: 4.3.2 Returns whether the member has the permissions passed. .. note:: If the channel argument is present, the function will look if the member has the permissions in the specified channel. If the argument is missing, then it will only consider the member's guild permissions. :param Union[int, Permissions] \*permissions: The list of permissions :param Channel channel: The channel where to check for permissions :param Optional[Union[int, Snowflake, Guild]] guild_id: The id of the guild :param Optional[str] operator: The operator to use to calculate permissions. Possible values: `and`, `or`. Defaults to `and`. :return: Whether the member has the permissions :rtype: bool """ perms = ( await self.get_guild_permissions(guild_id) if channel is MISSING else await channel.get_permissions_for(self) ) if operator == "and": return all(perm in perms for perm in permissions) else: return any(perm in perms for perm in permissions)