diff --git a/src/core/lion_guild.py b/src/core/lion_guild.py index 218e38c3..716afaa0 100644 --- a/src/core/lion_guild.py +++ b/src/core/lion_guild.py @@ -80,7 +80,7 @@ class LionGuild(Timezoned): return GuildMode.StudyGuild @property - def timezone(self) -> pytz.timezone: + def timezone(self) -> str: return self.config.timezone.value @property @@ -94,11 +94,3 @@ 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/cog.py b/src/modules/config/cog.py index 02850d8c..307b1df0 100644 --- a/src/modules/config/cog.py +++ b/src/modules/config/cog.py @@ -1,3 +1,5 @@ +from typing import Optional + import discord from discord import app_commands as appcmds from discord.ext import commands as cmds @@ -22,7 +24,7 @@ class GuildConfigCog(LionCog): 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) + self.bot.core.guild_config.register_model_setting(GeneralSettings.EventLog) configcog = self.bot.get_cog('ConfigCog') if configcog is None: @@ -36,43 +38,13 @@ class GuildConfigCog(LionCog): @appcmds.guild_only @appcmds.default_permissions(manage_guild=True) async def dashboard_cmd(self, ctx: LionContext): + if not ctx.guild or not ctx.interaction: + return + 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) @@ -90,7 +62,7 @@ class GuildConfigCog(LionCog): ) @appcmds.describe( timezone=GeneralSettings.Timezone._desc, - event_log=GeneralSettings.EventLog._display_name, + event_log=GeneralSettings.EventLog._desc, ) @appcmds.guild_only() @appcmds.default_permissions(manage_guild=True) @@ -107,4 +79,34 @@ class GuildConfigCog(LionCog): if not ctx.interaction: return await ctx.interaction.response.defer(thinking=True) - # TODO + + modified = [] + + if timezone is not None: + setting = self.settings.Timezone + instance = await setting.from_string(ctx.guild.id, timezone) + modified.append(instance) + + if event_log is not None: + setting = self.settings.EventLog + instance = await setting.from_value(ctx.guild.id, event_log) + modified.append(instance) + + if modified: + ack_lines = [] + for instance in modified: + await instance.write() + ack_lines.append(instance.update_message) + + tick = self.bot.config.emojis.tick + embed = discord.Embed( + colour=discord.Colour.brand_green(), + description='\n'.join(f"{tick} {line}" for line in ack_lines) + ) + await ctx.reply(embed=embed) + + if ctx.channel.id not in GeneralSettingUI._listening or not modified: + ui = GeneralSettingUI(self.bot, ctx.guild.id, ctx.channel.id) + await ui.run(ctx.interaction) + await ui.wait() + diff --git a/src/modules/config/general.py b/src/modules/config/general.py index d729fc07..c3fd1163 100644 --- a/src/modules/config/general.py +++ b/src/modules/config/general.py @@ -48,88 +48,88 @@ class GeneralSettingsCog(LionCog): # 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 + @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(), - 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(), + 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|success', - "Settings Updated!" + 'cmd:configure_general|parse_failure:timezone', + "Could not set the timezone!" )), - description='\n'.join( - f"{self.bot.config.emojis.tick} {line}" for line in results + description=err.msg ) - ) - await ctx.reply(embed=success_embed) - # TODO: Trigger configuration panel update if listening UI. - else: - # Show general configuration panel UI - # TODO Interactive UI - embed = discord.Embed( - colour=discord.Colour.orange(), + + 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|panel|title', - "General Configuration Panel" - )) + 'cmd:configure_general|success', + "Settings Updated!" + )), + description='\n'.join( + f"{self.bot.config.emojis.tick} {line}" for line in results ) - embed.add_field( - **ctx.lguild.config.timezone.embed_field - ) - await ctx.reply(embed=embed) + ) + await ctx.reply(embed=success_embed) + # TODO: Trigger configuration panel update if listening UI. + else: + # Show general configuration panel UI + # TODO Interactive UI + embed = discord.Embed( + colour=discord.Colour.orange(), + title=t(_p( + 'cmd:configure_general|panel|title', + "General Configuration Panel" + )) + ) + embed.add_field( + **ctx.lguild.config.timezone.embed_field + ) + await ctx.reply(embed=embed) cmd_configure_general.autocomplete('timezone')(TimezoneSetting.parse_acmpl) diff --git a/src/modules/config/settings.py b/src/modules/config/settings.py index 1e62bf31..87c5f0d4 100644 --- a/src/modules/config/settings.py +++ b/src/modules/config/settings.py @@ -1,7 +1,12 @@ +from typing import Optional +import discord + from settings import ModelData from settings.setting_types import TimezoneSetting, ChannelSetting from settings.groups import SettingGroup +from meta.context import ctx_bot +from meta.errors import UserInputError from core.data import CoreData from babel.translator import ctx_translator @@ -20,7 +25,8 @@ class GeneralSettings(SettingGroup): and the timezone used to display guild-wide statistics. """ setting_id = 'timezone' - _event = 'guild_setting_update_timezone' + _event = 'guildset_timezone' + _set_cmd = 'configure general' _display_name = _p('guildset:timezone', "timezone") _desc = _p( @@ -46,29 +52,24 @@ class GeneralSettings(SettingGroup): "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' + _set_cmd = 'configure general' _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." + "My audit log channel where I send server actions and events (e.g. rankgs and expiring 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" + "If configured, I will log most significant actions taken " + "or events which occur through my interface, into this channel. " + "Logged events include, for example:\n" "- Member voice activity\n" "- Roles equipped and expiring from rolemenus\n" "- Privated rooms rented and expiring\n" @@ -76,4 +77,34 @@ class GeneralSettings(SettingGroup): "I must have the 'Manage Webhooks' permission in this channel." ) - # TODO: Updatestr + _model = CoreData.Guild + _column = CoreData.Guild.event_log_channel.name + + + @classmethod + async def _check_value(cls, parent_id: int, value: Optional[discord.abc.GuildChannel], **kwargs): + if value is not None: + t = ctx_translator.get().t + if not value.permissions_for(value.guild.me).manage_webhooks: + raise UserInputError( + t(_p( + 'guildset:eventlog|check_value|error:perms|perm:manage_webhooks', + "Cannot set {channel} as an event log! I lack the 'Manage Webhooks' permission there." + )).format(channel=value) + ) + + @property + def update_message(self): + t = ctx_translator.get().t + channel = self.value + if channel is not None: + response = t(_p( + 'guildset:eventlog|response|set', + "Events will now be logged to {channel}" + )).format(channel=channel.mention) + else: + response = t(_p( + 'guildset:eventlog|response|unset', + "Guild events will no longer be logged." + )) + return response diff --git a/src/modules/config/settingsui.py b/src/modules/config/settingsui.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/modules/config/settingui.py b/src/modules/config/settingui.py index d01b6478..7c5ea28a 100644 --- a/src/modules/config/settingui.py +++ b/src/modules/config/settingui.py @@ -4,6 +4,7 @@ import discord from discord.ui.select import select, ChannelSelect from meta import LionBot +from meta.errors import UserInputError from utils.ui import ConfigUI, DashboardSection from utils.lib import MessageArgs @@ -18,11 +19,11 @@ _p = babel._p class GeneralSettingUI(ConfigUI): setting_classes = ( GeneralSettings.Timezone, - GeneralSettings.Eventlog, + GeneralSettings.EventLog, ) def __init__(self, bot: LionBot, guildid: int, channelid: int, **kwargs): - self.settings = bot.get_cog('GeneralSettingsCog').settings + self.settings = bot.get_cog('GuildConfigCog').settings super().__init__(bot, guildid, channelid, **kwargs) # ----- UI Components ----- @@ -39,13 +40,10 @@ class GeneralSettingUI(ConfigUI): """ await selection.response.defer(thinking=True, ephemeral=True) - setting = self.get_instance(GeneralSettings.Eventlog) + 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 + value = selected.values[0].resolve() if selected.values else None + setting = await setting.from_value(self.guildid, value) await setting.write() await selection.delete_original_response() @@ -103,5 +101,5 @@ class GeneralDashboard(DashboardSection): "dash:general|option|name", "General Configuration Panel" ) - configui = GeneralSettingsUI + configui = GeneralSettingUI setting_classes = configui.setting_classes diff --git a/src/modules/pomodoro/options.py b/src/modules/pomodoro/options.py index 76f9e10d..a88211f6 100644 --- a/src/modules/pomodoro/options.py +++ b/src/modules/pomodoro/options.py @@ -57,7 +57,7 @@ class TimerOptions(SettingGroup): _allow_object = False @classmethod - async def _check_value(cls, parent_id: int, value: Optional[discord.abc.GuildChannel], **kwargs): + async def _check_value(cls, parent_id: int, value, **kwargs): if value is not None: # TODO: Check we either have or can create a webhook # TODO: Check we can send messages, embeds, and files diff --git a/src/modules/pomodoro/ui/config.py b/src/modules/pomodoro/ui/config.py index b4badbb6..2fd92b89 100644 --- a/src/modules/pomodoro/ui/config.py +++ b/src/modules/pomodoro/ui/config.py @@ -145,13 +145,11 @@ class TimerOptionsUI(MessageUI): value = selected.values[0] if selected.values else None setting = self.timer.config.get('notification_channel') - if issue := await setting._check_value(self.timer.data.channelid, value): - await selection.edit_original_response(embed=error_embed(issue)) - else: - setting.value = value - await setting.write() - await self.timer.send_status() - await self.refresh(thinking=selection) + await setting._check_value(self.timer.data.channelid, value) + setting.value = value + await setting.write() + await self.timer.send_status() + await self.refresh(thinking=selection) async def refresh_notification_menu(self): self.notification_menu.placeholder = self.bot.translator.t(_p( diff --git a/src/settings/ui.py b/src/settings/ui.py index e53a874c..5fff6e32 100644 --- a/src/settings/ui.py +++ b/src/settings/ui.py @@ -453,6 +453,12 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]): data = await cls._parse_string(parent_id, userstr, **kwargs) return cls(parent_id, data, **kwargs) + @classmethod + async def from_value(cls, parent_id, value, **kwargs): + await cls._check_value(parent_id, value, **kwargs) + data = cls._data_from_value(parent_id, value, **kwargs) + return cls(parent_id, data, **kwargs) + @classmethod async def _parse_string(cls, parent_id, string: str, **kwargs) -> Optional[SettingData]: """ @@ -471,15 +477,14 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]): raise NotImplementedError @classmethod - async def _check_value(cls, parent_id, value, **kwargs) -> Optional[str]: + async def _check_value(cls, parent_id, value, **kwargs): """ Check the provided value is valid. Many setting update methods now provide Discord objects instead of raw data or user strings. This method may be used for value-checking such a value. - Returns `None` if there are no issues, otherwise an error message. - Subclasses should override this to implement a value checker. + Raises UserInputError if the value fails validation. """ pass