from typing import Optional import datetime as dt import pytz import discord import logging from meta import LionBot from utils.lib import Timezoned from settings.groups import ModelConfig, SettingDotDict from babel.translator import SOURCE_LOCALE from .data import CoreData from .lion_user import LionUser from .lion_guild import LionGuild logger = logging.getLogger(__name__) class MemberConfig(ModelConfig): settings = SettingDotDict() _model_settings = set() model = CoreData.Member class LionMember(Timezoned): """ Represents a member in the LionBot paradigm. Acts as a central interface to the member, user, and guild configurations. No guarantee is made that any corresponding Discord objects are accessible (or exist). """ __slots__ = ('bot', 'data', 'userid', 'guildid', 'config', 'luser', 'lguild', '_member', '__weakref__') Config = MemberConfig settings = Config.settings def __init__( self, bot: LionBot, data: CoreData.Member, lguild: LionGuild, luser: LionUser, member: Optional[discord.Member] = None ): self.bot = bot self.data = data self.userid = data.userid self.guildid = data.guildid self.lguild = lguild self.luser = luser self._member = member @property def member(self): """ The associated Discord member, if accessible. """ if self._member is None: if (guild := self.lguild.guild) is not None: self._member = guild.get_member(self.userid) return self._member @property def timezone(self) -> pytz.timezone: user_timezone = self.luser.config.timezone guild_timezone = self.lguild.config.timezone return user_timezone.value if user_timezone._data is not None else guild_timezone.value def private_locale(self, interaction=None) -> str: """ Appropriate locale to use in private communication with this member. Does not take into account guild force_locale. """ user_locale = self.luser.config.get('user_locale').value interaction_locale = interaction.locale.value if interaction else None guild_locale = self.lguild.config.get('guild_locale').value locale = user_locale or interaction_locale locale = locale or guild_locale locale = locale or SOURCE_LOCALE return locale async def touch_discord_model(self, member: discord.Member): """ Update saved Discord model attributes for this member. """ if member.display_name != self.data.display_name: await self.data.update(display_name=member.display_name) else: await self.data.refresh() async def fetch_member(self) -> Optional[discord.Member]: """ Fetches the associated member through the API. Respects cache. """ if (member := self.member) is None: if (guild := self.lguild.guild) is not None: try: member = await guild.fetch_member(self.userid) self._member = member except discord.HTTPException: pass return member async def remove_role(self, role: discord.Role): member = await self.fetch_member() if member is not None: try: await member.remove_roles(role) except discord.HTTPException as e: # TODO: Logging, audit logging logger.warning( "Lion role removal failed for " f", , . " f"Error: {repr(e)}", ) else: # Remove the role from persistent role storage cog = self.bot.get_cog('MemberAdminCog') if cog: await cog.absent_remove_role(self.guildid, self.userid, role.id)