diff --git a/src/core/__init__.py b/src/core/__init__.py index 6217ee8f..0a7fa849 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -1,5 +1,7 @@ from .cog import CoreCog +from .config import ConfigCog async def setup(bot): await bot.add_cog(CoreCog(bot)) + await bot.add_cog(ConfigCog(bot)) diff --git a/src/core/config.py b/src/core/config.py new file mode 100644 index 00000000..e0bcfd9a --- /dev/null +++ b/src/core/config.py @@ -0,0 +1,35 @@ +from discord import app_commands as appcmds +from discord.ext import commands as cmds + +from meta import LionBot, LionContext, LionCog +from babel.translator import LocalBabel + +babel = LocalBabel('core_config') + +_p = babel._p + + +class ConfigCog(LionCog): + """ + Core guild config cog. + + Primarily used to expose the `configure` base command group at a high level. + """ + def __init__(self, bot: LionBot): + self.bot = bot + + async def cog_load(self): + ... + + async def cog_unload(self): + ... + + @cmds.hybrid_group( + name=_p('group:configure', "configure"), + ) + @appcmds.guild_only + async def configure_group(self, ctx: LionContext): + """ + Bare command group, has no function. + """ + return diff --git a/src/core/data.py b/src/core/data.py index bb44b548..1085beb5 100644 --- a/src/core/data.py +++ b/src/core/data.py @@ -67,6 +67,7 @@ class CoreData(Registry, name="core"): CREATE TABLE user_config( userid BIGINT PRIMARY KEY, timezone TEXT, + show_global_stats BOOLEAN, topgg_vote_reminder BOOLEAN, avatar_hash TEXT, name TEXT, @@ -84,6 +85,7 @@ class CoreData(Registry, name="core"): userid = Integer(primary=True) timezone = String() + show_global_stats = Bool() topgg_vote_reminder = Bool() avatar_hash = String() name = String() diff --git a/src/core/lion_guild.py b/src/core/lion_guild.py new file mode 100644 index 00000000..d7db44a6 --- /dev/null +++ b/src/core/lion_guild.py @@ -0,0 +1,85 @@ +from typing import Optional, TYPE_CHECKING +from enum import Enum +import pytz +import discord + +from meta import LionBot +from utils.lib import Timezoned +from settings.groups import ModelConfig, SettingDotDict + +from .data import CoreData + +if TYPE_CHECKING: + # TODO: Import Settings for Config type hinting + pass + + +class VoiceMode(Enum): + STUDY = 0 + VOICE = 1 + + +class GuildMode(Enum): + StudyGuild = (VoiceMode.STUDY,) + VoiceGuild = (VoiceMode.VOICE,) + TextGuild = (VoiceMode.VOICE,) + + @property + def voice(self): + return self.value[0] + + +class GuildConfig(ModelConfig): + settings = SettingDotDict() + _model_settings = set() + model = CoreData.Guild + + @property + def timezone(self): + return self.get('timezone') + + +class LionGuild(Timezoned): + """ + Represents a Guild in the LionBot paradigm. + + Provides central access to cached data and configuration for a Guild. + + No guarantee is made that the client is in the corresponding Guild, + or that the corresponding Guild even exists. + """ + __slots__ = ('bot', 'data', 'guildid', 'config', '_guild', '__weakref__') + + Config = GuildConfig + settings = Config.settings + + def __init__(self, bot: LionBot, data: CoreData.Guild, guild: Optional[discord.Guild] = None): + self.bot = bot + self.data = data + self.guildid = data.guildid + + self._guild = guild + + self.config = self.Config(self.guildid, data) + + @property + def guild(self): + if self._guild is None: + self._guild = self.bot.get_guild(self.guildid) + return self._guild + + @property + def guild_mode(self): + # TODO: Configuration, data, and settings for this... + return GuildMode.StudyGuild + + @property + def timezone(self) -> pytz.timezone: + return self.config.timezone.value + + async def touch_discord_model(self, guild: discord.Guild): + """ + Update saved Discord model attributes for this guild. + """ + if self.data.name != guild.name: + await self.data.update(name=guild.name) diff --git a/src/core/lion_member.py b/src/core/lion_member.py new file mode 100644 index 00000000..7245d433 --- /dev/null +++ b/src/core/lion_member.py @@ -0,0 +1,84 @@ +from typing import Optional +import datetime as dt +import pytz +import discord + +from meta import LionBot +from utils.lib import Timezoned +from settings.groups import ModelConfig, SettingDotDict + +from .data import CoreData +from .lion_user import LionUser +from .lion_guild import LionGuild + + +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 + + 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) + + 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 diff --git a/src/core/lion_user.py b/src/core/lion_user.py new file mode 100644 index 00000000..4d6c3b6b --- /dev/null +++ b/src/core/lion_user.py @@ -0,0 +1,71 @@ +from typing import Optional +import discord +import pytz + +from meta import LionBot +from utils.lib import utc_now, Timezoned +from settings.groups import ModelConfig, SettingDotDict + +from .data import CoreData + + +class UserConfig(ModelConfig): + settings = SettingDotDict() + _model_settings = set() + model = CoreData.User + + @property + def timezone(self) -> pytz.timezone: + return self.get('timezone') + + +class LionUser(Timezoned): + """ + Represents a User in the LionBot paradigm. + + Provides central access to cached data and configuration for a User. + + No guarantee is made that the client has access to this User. + """ + __slots__ = ('bot', 'data', 'userid', '_user', 'config', '__weakref__') + + Config = UserConfig + settings = Config.settings + + def __init__(self, bot: LionBot, data: CoreData.User, user: Optional[discord.User] = None): + self.bot = bot + self.data = data + self.userid = data.userid + + self._user = user + + self.config = self.Config(self.userid, data) + + @property + def user(self): + if self._user is None: + self._user = self.bot.get_user(self.userid) + return self._user + + @property + def timezone(self) -> pytz.timezone: + return self.config.timezone.value + + async def touch_discord_model(self, user: discord.User, seen=True): + """ + Updated stored Discord model attributes for this user. + """ + to_update = {} + + avatar_key = user.avatar.key if user.avatar else None + if self.data.avatar_hash != avatar_key: + to_update['avatar_hash'] = avatar_key + + if self.data.name != user.name: + to_update['name'] = user.name + + if seen: + to_update['last_seen'] = utc_now() + + if to_update: + await self.data.update(**to_update) diff --git a/src/core/settings.py b/src/core/settings.py new file mode 100644 index 00000000..53ae1c6c --- /dev/null +++ b/src/core/settings.py @@ -0,0 +1,15 @@ +from settings.groups import ModelConfig, SettingDotDict + +from .data import CoreData + + +class GuildConfig(ModelConfig): + settings = SettingDotDict() + _model_settings = set() + model = CoreData.Guild + + +class UserConfig(ModelConfig): + settings = SettingDotDict() + _model_settings = set() + model = CoreData.User