Source code for curious.dataclasses.member

# This file is part of curious.
#
# curious is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# curious is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with curious.  If not, see <http://www.gnu.org/licenses/>.

"""
Wrappers for Member objects (Users with guilds).

.. currentmodule:: curious.dataclasses.member
"""
import collections
import datetime
from typing import List

from curious.dataclasses import guild as dt_guild, role as dt_role, user as dt_user, \
    voice_state as dt_vs
from curious.dataclasses.bases import Dataclass
from curious.dataclasses.permissions import Permissions
from curious.dataclasses.presence import Game, Presence, Status
from curious.exc import HierarchyError, PermissionsError
from curious.util import subclass_builtin, to_datetime


[docs]@subclass_builtin(str) class Nickname(str): """ Represents the nickname of a :class:`.Member`. :cvar NONE: A singleton :class:`.Nickname` representing an empty nickname. Equal to the empty \ string. """ NONE: 'Nickname' def __new__(cls, value: str): if value is None or value == "": try: return cls.NONE except AttributeError: cls.NONE = super().__new__(cls, "") return cls.NONE return super().__new__(cls, value) def __eq__(self, other): if other is None and self == self.NONE: return True return super().__eq__(other) def __repr__(self) -> str: return f"<Nickname value={super().__repr__()}>"
[docs] async def set(self, new_nickname: str) -> 'Nickname': """ Sets the nickname of the username. :param new_nickname: The new nickname of this user. If None, will reset the nickname. """ parent: Member = self.__dict__['parent'] # Ensure we don't try and set a bad nickname, which makes an empty listener. if new_nickname == self: return self guild: dt_guild.Guild = parent.guild me = False if parent == parent.guild.me: me = True if not guild.me.guild_permissions.change_nickname: raise PermissionsError("change_nickname") else: if not guild.me.guild_permissions.manage_nicknames: raise PermissionsError("manage_nicknames") if parent.top_role >= guild.me.top_role and parent != guild.me: raise HierarchyError("Top role is equal to or lower than victim's top role") if new_nickname is not None and len(new_nickname) > 32: raise ValueError("Nicknames cannot be longer than 32 characters") async def _listener(before, after): return after.guild == guild and after.id == parent.id async with parent._bot.events.wait_for_manager("guild_member_update", _listener): await parent._bot.http.change_nickname(guild.id, new_nickname, member_id=parent.id, me=me) # the wait_for means at this point the nickname has been changed return parent.nickname
[docs] async def reset(self) -> 'Nickname': """ Resets a member's nickname. """ return await self.set(self.NONE)
[docs]class MemberRoleContainer(collections.Sequence): """ Represents the roles of a :class:`.Member`. """ def __init__(self, member: 'Member'): self._member = member def _sorted_roles(self) -> 'List[dt_role.Role]': if not self._member.guild: return [] roles = filter( lambda r: r is not None, map(self._member.guild.roles.get, self._member.role_ids) ) return sorted(roles) # opt: the default Sequence makes us re-create the sorted role list constantly # we don't wanna cache it, without introducing invalidation hell # so we just put `__iter__` on `_sorted_roles` def __iter__(self) -> type(iter([])): return iter(self._sorted_roles()) def __len__(self) -> int: return len(self._member.role_ids) def __getitem__(self, item: int): return self._sorted_roles()[item] @property def top_role(self) -> 'dt_role.Role': """ :return: The top :class:`.Role` for this member. """ roles = self._sorted_roles() if len(roles) <= 0: return self._member.guild.default_role return self[0]
[docs] async def add(self, *roles: 'dt_role.Role'): """ Adds roles to this member. :param roles: The :class:`.Role` objects to add to this member's role list. """ if not self._member.guild.me.guild_permissions.manage_roles: raise PermissionsError("manage_roles") # Ensure we can add all of these roles. for _r in roles: if _r >= self._member.guild.me.top_role: msg = "Cannot add role {} - it has a higher or equal position to our top role" \ .format(_r.name) raise HierarchyError(msg) async def _listener(before, after: Member): if after.id != self._member.id: return False if not all(role in after.roles for role in roles): return False return True async with self._member._bot.events.wait_for_manager("guild_member_update", _listener): role_ids = set([_r.id for _r in self._member.roles] + [_r.id for _r in roles]) await self._member._bot.http.edit_member_roles( self._member.guild_id, self._member.id, role_ids )
[docs] async def remove(self, *roles: 'dt_role.Role'): """ Removes roles from this member. :param roles: The roles to remove. """ if not self._member.guild.me.guild_permissions.manage_roles: raise PermissionsError("manage_roles") for _r in roles: if _r >= self._member.guild.me.top_role: msg = "Cannot remove role {} - it has a higher or equal position to our top role" \ .format(_r.name) raise HierarchyError(msg) async def _listener(before, after: Member): if after.id != self._member.id: return False if not all(role not in after.roles for role in roles): return False return True # Calculate the roles to keep. to_keep = set(self._member.roles) - set(roles) async with self._member._bot.events.wait_for_manager("guild_member_update", _listener): role_ids = set([_r.id for _r in to_keep]) await self._member._bot.http.edit_member_roles(self._member.guild_id, self._member.id, role_ids)
[docs]class Member(Dataclass): """ A member represents somebody who is inside a guild. """ __slots__ = ("_user_data", "role_ids", "joined_at", "_nickname", "guild_id", "presence", "roles") def __init__(self, client, **kwargs): super().__init__(kwargs["user"]["id"], client) # copy user data for when the user is decached self._user_data = kwargs["user"] self._bot.state.make_user(self._user_data) #: An iterable of role IDs this member has. self.role_ids = [int(rid) for rid in kwargs.get("roles", [])] #: A :class:`._MemberRoleContainer` that represents the roles of this member. self.roles = MemberRoleContainer(self) #: The date the user joined the guild. self.joined_at = to_datetime(kwargs.get("joined_at", None)) # type: datetime.datetime nick = kwargs.get("nick") #: The member's current :class:`.Nickname`. self._nickname = Nickname(nick) # type: Nickname #: The ID of the guild that this member is in. self.guild_id = None # type: int #: The current :class:`.Presence` of this member. self.presence = Presence(status=kwargs.get("status", Status.OFFLINE), game=kwargs.get("game", None)) @property def guild(self) -> 'dt_guild.Guild': """ :return: The :class:`.Guild` associated with this member. """ return self._bot.guilds.get(self.guild_id) @property def voice(self) -> 'dt_vs.VoiceState': """ :return: The :class:`.VoiceState` associated with this member. """ try: return self.guild._voice_states[self.id] except (AttributeError, KeyError): return None @property def nickname(self) -> Nickname: """ Represents a member's nickname. :getter: A :class:`._Nickname` for this member. :setter: Coerces a string nickname into a :class:`._Nickname`. Do not use. """ return self._nickname @nickname.setter def nickname(self, value: str): if not value: self._nickname = Nickname.NONE return self._nickname = Nickname(value) self._nickname.__dict__['parent'] = self def __hash__(self) -> int: return hash(self.guild_id) + hash(self.user.id) def __eq__(self, other) -> bool: if not isinstance(other, Member): return NotImplemented return other.guild == self.guild and other.user == self.user
[docs] def _copy(self): """ Copies a member object. """ new_object = object.__new__(self.__class__) # type: Member new_object._bot = self._bot new_object.id = self.id new_object.role_ids = self.role_ids.copy() new_object.joined_at = self.joined_at new_object.guild_id = self.guild_id new_object.presence = self.presence new_object.nickname = self.nickname return new_object
def __del__(self): try: self._bot.state._check_decache_user(self.id) except AttributeError: # during shutdown pass @property def user(self) -> 'dt_user.User': """ :return: The underlying :class:`.User` for this member. """ try: return self._bot.state._users[self.id] except KeyError: # don't go through make_user as it'll cache it return dt_user.User(self._bot, **self._user_data) @property def name(self) -> str: """ :return: The computed display name of this user. """ return self.nickname if self.nickname != Nickname.NONE else self.user.username @property def mention(self) -> str: """ :return: A string that mentions this member. """ if self.nickname: return "<@!{}>".format(self.id) return self.user.mention @property def status(self) -> Status: """ :return: The current :class:`.Status` of this member. """ return self.presence.status if self.presence else Status.OFFLINE @property def game(self) -> Game: """ :return: The current :class:`.Game` this member is playing. """ if not self.presence: return None if self.presence.status == Status.OFFLINE: return None return self.presence.game @property def colour(self) -> int: """ :return: The computed colour of this user. """ roles = reversed(self.roles) # NB: you can abuse discord and edit the defualt role's colour # so explicitly check that it isn't the default role, and make sure it has a colour # in order to get the correct calculated colour roles = filter(lambda role: not role.is_default_role and role.colour, roles) try: return next(roles).colour except StopIteration: return 0 @property def top_role(self) -> 'dt_role.Role': """ :return: This member's top-most :class:`.Role`. """ return self.roles.top_role @property def guild_permissions(self) -> Permissions: """ :return: The calculated guild permissions for a member. """ if self == self.guild.owner: return Permissions.all() bitfield = 0 # add the default roles bitfield |= self.guild.default_role.permissions.bitfield for role in self.roles: bitfield |= role.permissions.bitfield permissions = Permissions(bitfield) if permissions.administrator: return Permissions.all() return permissions # Member methods.
[docs] async def send(self, content: str, *args, **kwargs): """ Sends a message to a member in DM. This is a shortcut for :meth:`.User.send`. """ return await self.user.send(content, *args, **kwargs)
[docs] async def ban(self, delete_message_days: int = 7): """ Bans this member from the guild. :param delete_message_days: The number of days of messages to delete. """ return await self.guild.bans.add(self, delete_message_days=delete_message_days)
[docs] async def kick(self): """ Kicks this member from the guild. """ return await self.guild.kick(self)