diff --git a/src/core/lion_guild.py b/src/core/lion_guild.py index b5141ca9..218e38c3 100644 --- a/src/core/lion_guild.py +++ b/src/core/lion_guild.py @@ -93,3 +93,12 @@ class LionGuild(Timezoned): """ if self.data.name != guild.name: await self.data.update(name=guild.name) + + async def _event_log(self, ...): + ... + + def event_log(self, **kwargs): + asyncio.create_task(self._event_log(**kwargs), name='event-log') + + def error_log(self, ...): + ... diff --git a/src/modules/config/__init__.py b/src/modules/config/__init__.py index 616c68a6..f4a849d1 100644 --- a/src/modules/config/__init__.py +++ b/src/modules/config/__init__.py @@ -6,8 +6,6 @@ babel = LocalBabel('config') async def setup(bot): - from .general import GeneralSettingsCog - from .cog import DashCog + from .cog import GuildConfigCog - await bot.add_cog(GeneralSettingsCog(bot)) - await bot.add_cog(DashCog(bot)) + await bot.add_cog(GuildConfigCog(bot)) diff --git a/src/modules/config/cog.py b/src/modules/config/cog.py index 56926d4a..02850d8c 100644 --- a/src/modules/config/cog.py +++ b/src/modules/config/cog.py @@ -3,22 +3,31 @@ from discord import app_commands as appcmds from discord.ext import commands as cmds from meta import LionBot, LionContext, LionCog +from wards import low_management_ward from . import babel from .dashboard import GuildDashboard +from .settings import GeneralSettings +from .settingui import GeneralSettingUI _p = babel._p -class DashCog(LionCog): +class GuildConfigCog(LionCog): + depends = {'CoreCog'} + def __init__(self, bot: LionBot): self.bot = bot + self.settings = GeneralSettings() async def cog_load(self): - ... + self.bot.core.guild_config.register_model_setting(GeneralSettings.Timezone) + self.bot.core.guild_config.register_model_setting(GeneralSettings.Eventlog) - async def cog_unload(self): - ... + configcog = self.bot.get_cog('ConfigCog') + if configcog is None: + raise ValueError("Cannot load GuildConfigCog without ConfigCog") + self.crossload_group(self.configure_group, configcog.configure_group) @cmds.hybrid_command( name="dashboard", @@ -30,3 +39,72 @@ class DashCog(LionCog): ui = GuildDashboard(self.bot, ctx.guild, ctx.author.id, ctx.channel.id) await ui.run(ctx.interaction) await ui.wait() + + @cmds.hybrid_group("configure", with_app_command=False) + async def configure_group(self, ctx: LionContext): + # Placeholder configure group command. + ... + + @configure_group.command( + name=_p('cmd:configure_general', "general"), + description=_p('cmd:configure_general|desc', "General configuration panel") + ) + @appcmds.rename( + timezone=GeneralSettings.Timezone._display_name, + event_log=GeneralSettings.EventLog._display_name, + ) + @appcmds.describe( + timezone=GeneralSettings.Timezone._desc, + event_log=GeneralSettings.EventLog._display_name, + ) + @appcmds.guild_only() + @appcmds.default_permissions(manage_guild=True) + @low_management_ward + async def cmd_configure_general(self, ctx: LionContext, + timezone: Optional[str] = None, + event_log: Optional[discord.TextChannel] = None, + ): + t = self.bot.translator.t + + # Typechecker guards because they don't understand the check ward + if not ctx.guild: + return + if not ctx.interaction: + return + await ctx.interaction.response.defer(thinking=True) + + # ----- Configuration ----- + @LionCog.placeholder_group + @cmds.hybrid_group("configure", with_app_command=False) + async def configure_group(self, ctx: LionContext): + # Placeholder configure group command. + ... + + @configure_group.command( + name=_p('cmd:configure_general', "general"), + description=_p('cmd:configure_general|desc', "General configuration panel") + ) + @appcmds.rename( + timezone=GeneralSettings.Timezone._display_name, + event_log=GeneralSettings.EventLog._display_name, + ) + @appcmds.describe( + timezone=GeneralSettings.Timezone._desc, + event_log=GeneralSettings.EventLog._display_name, + ) + @appcmds.guild_only() + @appcmds.default_permissions(manage_guild=True) + @low_management_ward + async def cmd_configure_general(self, ctx: LionContext, + timezone: Optional[str] = None, + event_log: Optional[discord.TextChannel] = None, + ): + t = self.bot.translator.t + + # Typechecker guards because they don't understand the check ward + if not ctx.guild: + return + if not ctx.interaction: + return + await ctx.interaction.response.defer(thinking=True) + # TODO diff --git a/src/modules/config/general.py b/src/modules/config/general.py index 4b57c844..d729fc07 100644 --- a/src/modules/config/general.py +++ b/src/modules/config/general.py @@ -26,48 +26,6 @@ from . import babel _p = babel._p -class GeneralSettings(SettingGroup): - class Timezone(ModelData, TimezoneSetting): - """ - Guild timezone configuration. - - Exposed via `/configure general timezone:`, and the standard interface. - The `timezone` setting acts as the default timezone for all members, - and the timezone used to display guild-wide statistics. - """ - setting_id = 'timezone' - _event = 'guild_setting_update_timezone' - - _display_name = _p('guildset:timezone', "timezone") - _desc = _p( - 'guildset:timezone|desc', - "Guild timezone for statistics display." - ) - _long_desc = _p( - 'guildset:timezone|long_desc', - "Guild-wide timezone. " - "Used to determine start of the day for the leaderboards, " - "and as the default statistics timezone for members who have not set one." - ) - _default = 'UTC' - - _model = CoreData.Guild - _column = CoreData.Guild.timezone.name - - @property - def update_message(self): - t = ctx_translator.get().t - return t(_p( - 'guildset:timezone|response', - "The guild timezone has been set to `{timezone}`." - )).format(timezone=self.data) - - @property - def set_str(self): - bot = ctx_bot.get() - return bot.core.mention_cmd('configure general') if bot else None - - class GeneralSettingsCog(LionCog): depends = {'CoreCog'} @@ -87,68 +45,72 @@ class GeneralSettingsCog(LionCog): @LionCog.placeholder_group @cmds.hybrid_group("configure", with_app_command=False) async def configure_group(self, ctx: LionContext): - # Placeholder configure group command. - ... + # Placeholder configure group command. + ... - @configure_group.command( - name=_p('cmd:configure_general', "general"), - description=_p('cmd:configure_general|desc', "General configuration panel") - ) - @appcmds.rename( - timezone=GeneralSettings.Timezone._display_name - ) - @appcmds.describe( - timezone=GeneralSettings.Timezone._desc - ) - @appcmds.guild_only() - @appcmds.default_permissions(manage_guild=True) - @low_management_ward - async def cmd_configure_general(self, ctx: LionContext, - timezone: Optional[str] = None): - t = self.bot.translator.t + @configure_group.command( + name=_p('cmd:configure_general', "general"), + description=_p('cmd:configure_general|desc', "General configuration panel") + ) + @appcmds.rename( + timezone=GeneralSettings.Timezone._display_name, + event_log=GeneralSettings.EventLog._display_name, + ) + @appcmds.describe( + timezone=GeneralSettings.Timezone._desc, + event_log=GeneralSettings.EventLog._display_name, + ) + @appcmds.guild_only() + @appcmds.default_permissions(manage_guild=True) + @low_management_ward + async def cmd_configure_general(self, ctx: LionContext, + timezone: Optional[str] = None, + event_log: Optional[discord.TextChannel] = None, + ): + t = self.bot.translator.t - # Typechecker guards because they don't understand the check ward - if not ctx.guild: - return - if not ctx.interaction: - return - await ctx.interaction.response.defer(thinking=True) + # Typechecker guards because they don't understand the check ward + if not ctx.guild: + return + if not ctx.interaction: + return + await ctx.interaction.response.defer(thinking=True) - updated = [] # Possibly empty list of setting instances which were updated, with new data stored - error_embed = None + updated = [] # Possibly empty list of setting instances which were updated, with new data stored + error_embed = None - if timezone is not None: - try: - timezone_setting = await self.settings.Timezone.from_string(ctx.guild.id, timezone) - updated.append(timezone_setting) - except UserInputError as err: - error_embed = discord.Embed( - colour=discord.Colour.brand_red(), + if timezone is not None: + try: + timezone_setting = await self.settings.Timezone.from_string(ctx.guild.id, timezone) + updated.append(timezone_setting) + except UserInputError as err: + error_embed = discord.Embed( + colour=discord.Colour.brand_red(), + title=t(_p( + 'cmd:configure_general|parse_failure:timezone', + "Could not set the timezone!" + )), + description=err.msg + ) + + if error_embed is not None: + # User requested configuration updated, but we couldn't parse input + await ctx.reply(embed=error_embed) + elif updated: + # Save requested configuration updates + results = [] # List of "success" update responses for each updated setting + for to_update in updated: + # TODO: Again need a better way of batch writing + # Especially since most of these are on one model... + await to_update.write() + results.append(to_update.update_message) + # Post aggregated success message + success_embed = discord.Embed( + colour=discord.Colour.brand_green(), title=t(_p( - 'cmd:configure_general|parse_failure:timezone', - "Could not set the timezone!" + 'cmd:configure_general|success', + "Settings Updated!" )), - description=err.msg - ) - - if error_embed is not None: - # User requested configuration updated, but we couldn't parse input - await ctx.reply(embed=error_embed) - elif updated: - # Save requested configuration updates - results = [] # List of "success" update responses for each updated setting - for to_update in updated: - # TODO: Again need a better way of batch writing - # Especially since most of these are on one model... - await to_update.write() - results.append(to_update.update_message) - # Post aggregated success message - success_embed = discord.Embed( - colour=discord.Colour.brand_green(), - title=t(_p( - 'cmd:configure_general|success', - "Settings Updated!" - )), description='\n'.join( f"{self.bot.config.emojis.tick} {line}" for line in results ) diff --git a/src/modules/config/settings.py b/src/modules/config/settings.py new file mode 100644 index 00000000..1e62bf31 --- /dev/null +++ b/src/modules/config/settings.py @@ -0,0 +1,79 @@ +from settings import ModelData +from settings.setting_types import TimezoneSetting, ChannelSetting +from settings.groups import SettingGroup + +from core.data import CoreData +from babel.translator import ctx_translator + +from . import babel + +_p = babel._p + + +class GeneralSettings(SettingGroup): + class Timezone(ModelData, TimezoneSetting): + """ + Guild timezone configuration. + + Exposed via `/configure general timezone:`, and the standard interface. + The `timezone` setting acts as the default timezone for all members, + and the timezone used to display guild-wide statistics. + """ + setting_id = 'timezone' + _event = 'guild_setting_update_timezone' + + _display_name = _p('guildset:timezone', "timezone") + _desc = _p( + 'guildset:timezone|desc', + "Guild timezone for statistics display." + ) + _long_desc = _p( + 'guildset:timezone|long_desc', + "Guild-wide timezone. " + "Used to determine start of the day for the leaderboards, " + "and as the default statistics timezone for members who have not set one." + ) + _default = 'UTC' + + _model = CoreData.Guild + _column = CoreData.Guild.timezone.name + + @property + def update_message(self): + t = ctx_translator.get().t + return t(_p( + 'guildset:timezone|response', + "The guild timezone has been set to `{timezone}`." + )).format(timezone=self.data) + + @property + def set_str(self): + bot = ctx_bot.get() + return bot.core.mention_cmd('configure general') if bot else None + + class EventLog(ModelData, ChannelSetting): + """ + Guild event log channel. + """ + setting_id = 'eventlog' + _event = 'guildset_eventlog' + + _display_name = _p('guildset:eventlog', "event_log") + _desc = _p( + 'guildset:eventlog|desc', + "Channel to which to log server events, such as voice sessions and equipped roles." + ) + # TODO: Reword + _long_desc = _p( + 'guildset:eventlog|long_desc', + "An audit log for my own systems, " + "I will send most significant actions and events that occur through my interface " + "to this channel. For example, this includes:\n" + "- Member voice activity\n" + "- Roles equipped and expiring from rolemenus\n" + "- Privated rooms rented and expiring\n" + "- Activity ranks earned\n" + "I must have the 'Manage Webhooks' permission in this channel." + ) + + # TODO: Updatestr diff --git a/src/modules/config/settingsui.py b/src/modules/config/settingsui.py new file mode 100644 index 00000000..e69de29b diff --git a/src/modules/config/settingui.py b/src/modules/config/settingui.py new file mode 100644 index 00000000..d01b6478 --- /dev/null +++ b/src/modules/config/settingui.py @@ -0,0 +1,107 @@ +import asyncio + +import discord +from discord.ui.select import select, ChannelSelect + +from meta import LionBot + +from utils.ui import ConfigUI, DashboardSection +from utils.lib import MessageArgs + +from . import babel +from .settings import GeneralSettings + + +_p = babel._p + + +class GeneralSettingUI(ConfigUI): + setting_classes = ( + GeneralSettings.Timezone, + GeneralSettings.Eventlog, + ) + + def __init__(self, bot: LionBot, guildid: int, channelid: int, **kwargs): + self.settings = bot.get_cog('GeneralSettingsCog').settings + super().__init__(bot, guildid, channelid, **kwargs) + + # ----- UI Components ----- + # Event log + @select( + cls=ChannelSelect, + channel_types=[discord.ChannelType.text, discord.ChannelType.voice], + placeholder='EVENT_LOG_PLACEHOLDER', + min_values=0, max_values=1, + ) + async def eventlog_menu(self, selection: discord.Interaction, selected: ChannelSelect): + """ + Single channel selector for the event log. + """ + await selection.response.defer(thinking=True, ephemeral=True) + + setting = self.get_instance(GeneralSettings.Eventlog) + + value = selected.values[0] if selected.values else None + if issue := (await setting.check_value(value)): + raise UserInputError(issue) + + setting.value = value + await setting.write() + await selection.delete_original_response() + + async def eventlog_menu_refresh(self): + menu = self.eventlog_menu + t = self.bot.translator.t + menu.placeholder = t(_p( + 'ui:general_config|menu:event_log|placeholder', + "Select Event Log" + )) + + # ----- UI Flow ----- + async def make_message(self) -> MessageArgs: + t = self.bot.translator.t + title = t(_p( + 'ui:general_config|embed:title', + "General Configuration" + )) + embed = discord.Embed( + title=title, + colour=discord.Colour.orange() + ) + for setting in self.instances: + embed.add_field(**setting.embed_field, inline=False) + + return MessageArgs(embed=embed) + + async def reload(self): + self.instances = [ + await setting.get(self.guildid) + for setting in self.setting_classes + ] + + async def refresh_components(self): + to_refresh = ( + self.edit_button_refresh(), + self.close_button_refresh(), + self.reset_button_refresh(), + self.eventlog_menu_refresh(), + ) + await asyncio.gather(*to_refresh) + + self.set_layout( + (self.eventlog_menu,), + (self.edit_button, self.reset_button, self.close_button,), + ) + + +class GeneralDashboard(DashboardSection): + section_name = _p( + "dash:general|title", + "General Dashboard Settings ({commands[configure general]})" + ) + _option_name = _p( + "dash:general|option|name", + "General Configuration Panel" + ) + configui = GeneralSettingsUI + setting_classes = configui.setting_classes