diff --git a/bot/cogs/config.py b/bot/cogs/config.py index e9292e5..e55e36e 100644 --- a/bot/cogs/config.py +++ b/bot/cogs/config.py @@ -2,6 +2,7 @@ import datetime import difflib +from enum import Enum from typing import ( TYPE_CHECKING, Annotated, @@ -31,6 +32,7 @@ if TYPE_CHECKING: from cogs.tickets import Tickets + from rodhaj import Rodhaj @@ -107,6 +109,11 @@ def replace(self, blocklist: dict[int, BlocklistEntity]) -> None: self._blocklist = blocklist +class ConfigType(Enum): + TOGGLE = 0 + SET = 1 + + # Msgspec Structs are usually extremely fast compared to slotted classes class GuildConfig(msgspec.Struct): bot: Rodhaj @@ -146,6 +153,7 @@ def to_dict(self): class PartialGuildSettings(msgspec.Struct, frozen=True): + mention: str = "@here" anon_replies: bool = False anon_reply_without_command: bool = False anon_snippets: bool = False @@ -317,6 +325,11 @@ def __init__(self, bot: Rodhaj) -> None: "anon_reply_without_command", "anon_snippets", ] + self.settings_keys = [ + "anon_replies", + "anon_reply_without_command", + "anon_snippets", + ] @property def display_emoji(self) -> discord.PartialEmoji: @@ -340,7 +353,9 @@ async def get_guild_config(self, guild_id: int) -> Optional[GuildConfig]: @alru_cache() async def get_guild_settings(self, guild_id: int) -> Optional[GuildSettings]: - query = "SELECT account_age, guild_age, mention, settings FROM guild_config WHERE id = $1;" + query = ( + "SELECT account_age, guild_age, settings FROM guild_config WHERE id = $1;" + ) rows = await self.pool.fetchrow(query, guild_id) if rows is None: self.get_guild_settings.cache_invalidate(guild_id) @@ -348,7 +363,6 @@ async def get_guild_settings(self, guild_id: int) -> Optional[GuildSettings]: return GuildSettings( account_age=rows["account_age"], guild_age=rows["guild_age"], - mention=rows["mention"], **rows["settings"], ) @@ -365,6 +379,48 @@ async def get_partial_guild_settings( **rows["settings"], ) + async def set_guild_settings( + self, + key: str, + value: Union[str, bool], + *, + config_type: ConfigType, + ctx: GuildContext, + ): + current_guild_settings = await self.get_partial_guild_settings(ctx.guild.id) + + # If there are no guild configurations, then we have an issue here + # we will denote this with an error + if not current_guild_settings: + raise RuntimeError("Guild settings could not be found") + + # There is technically an faster method + # of directly modifying the subscripted path... + # But for the reason of autonomic guarantees, the whole entire dict should be modified + query = """ + UPDATE guild_config + SET settings = $2::jsonb + WHERE id = $1; + """ + guild_dict = current_guild_settings.to_dict() + original_value = guild_dict.get(key) + if original_value and original_value is value: + await ctx.send(f"`{key}` is already set to `{value}`!") + return + + guild_dict[key] = value + await self.bot.pool.execute(query, ctx.guild.id, guild_dict) + self.get_partial_guild_settings.cache_invalidate(ctx.guild.id) + + command_type = "Toggled" if config_type == ConfigType.TOGGLE else "Set" + await ctx.send(f"{command_type} `{key}` from `{original_value}` to `{value}`") + + async def _handle_config_error( + self, error: commands.CommandError, ctx: GuildContext + ) -> None: + if isinstance(error, commands.BadArgument): + await ctx.send(str(error)) + ### Blocklist utilities async def can_be_blocked(self, ctx: GuildContext, entity: discord.Member) -> bool: @@ -692,7 +748,6 @@ async def config_set_age( clause = "SET guild_age = $2" else: clause = "SET account_age = $2" - query = f""" UPDATE guild_config {clause} @@ -717,27 +772,18 @@ async def config_set( If you are looking to toggle an option within the configuration, then please use `config toggle` instead. """ - if key not in ["account_age", "guild_age"]: + if key in ["account_age", "guild_age"]: await ctx.send( - f"Please use `{ctx.prefix or 'r>'}config toggle` for setting configuration values that are boolean" + "Please use `config set-age` for setting configuration values that are related with ages" + ) + return + elif key not in "mention": + await ctx.send( + "Please use `config toggle` for setting configuration values that are boolean" ) return - # I'm not joking but this is the only cleanest way I can think of doing this - # Noelle 2024 - if key in "account_age": - clause = "SET account_age = $2" - else: - clause = "SET guild_age = $2" - - query = f""" - UPDATE guild_config - {clause} - WHERE id = $1; - """ - await self.bot.pool.execute(query, ctx.guild.id, value) - self.get_guild_settings.cache_invalidate(ctx.guild.id) - await ctx.send(f"Set `{key}` to `{value}`") + await self.set_guild_settings(key, value, config_type=ConfigType.SET, ctx=ctx) @check_permissions(manage_guild=True) @commands.guild_only() @@ -751,52 +797,38 @@ async def config_toggle( If you are looking to set an option within the configuration, then please use `config set` instead. """ - if key in ["account_age", "guild_age", "mention"]: + if key in ["account_age", "guild_age"]: await ctx.send( - f"Please use `{ctx.prefix or 'r>'}config set` for setting configuration values that are fixed values" + f"Please use `{ctx.prefix or 'r>'}config set-age` for setting configuration values that are fixed values" ) return - - current_guild_settings = await self.get_partial_guild_settings(ctx.guild.id) - - # If there are no guild configurations, then we have an issue here - # we will denote this with an error - if not current_guild_settings: - raise RuntimeError("Guild settings could not be found") - - # There is technically an faster method - # of directly modifying the subscripted path... - # But for the reason of autonomic guarantees, the whole entire dict should be modified - query = """ - UPDATE guild_config - SET settings = $2::jsonb - WHERE id = $1; - """ - guild_dict = current_guild_settings.to_dict() - original_value = guild_dict.get(key) - if original_value and original_value is value: - await ctx.send(f"`{key}` is already set to `{value}`!") + elif key in "mention": + await ctx.send( + "Please use `config set` for setting configuration values that require a set value" + ) return - guild_dict[key] = value - await self.bot.pool.execute(query, ctx.guild.id, guild_dict) - self.get_guild_settings.cache_invalidate(ctx.guild.id) + await self.set_guild_settings( + key, value, config_type=ConfigType.TOGGLE, ctx=ctx + ) - await ctx.send(f"Toggled `{key}` from `{original_value}` to `{value}`") + @config_set_age.error + async def on_config_set_age_error( + self, ctx: GuildContext, error: commands.CommandError + ): + await self._handle_config_error(error, ctx) @config_set.error async def on_config_set_error( self, ctx: GuildContext, error: commands.CommandError ): - if isinstance(error, commands.BadArgument): - await ctx.send(str(error)) + await self._handle_config_error(error, ctx) @config_toggle.error async def on_config_toggle_error( self, ctx: GuildContext, error: commands.CommandError ): - if isinstance(error, commands.BadArgument): - await ctx.send(str(error)) + await self._handle_config_error(error, ctx) @check_permissions(manage_guild=True) @commands.guild_only() diff --git a/bot/libs/utils/time.py b/bot/libs/utils/time.py index 627ab9a..a274e0e 100644 --- a/bot/libs/utils/time.py +++ b/bot/libs/utils/time.py @@ -1,13 +1,11 @@ from __future__ import annotations import datetime -import logging import re from typing import TYPE_CHECKING, Any, Optional, Sequence, Union import parsedatetime as pdt from dateutil.relativedelta import relativedelta -from discord import app_commands from discord.ext import commands # Monkey patch mins and secs into the units @@ -171,29 +169,6 @@ async def convert(cls, ctx: RoboContext, argument: str) -> Self: return cls(argument, now=ctx.message.created_at, tzinfo=datetime.timezone.utc) -class RelativeDelta(app_commands.Transformer, commands.Converter): - @classmethod - def __do_conversion(cls, argument: str) -> relativedelta: - match = ShortTime.compiled.fullmatch(argument) - if match is None or not match.group(0): - raise ValueError("invalid time provided") - - data = {k: int(v) for k, v in match.groupdict(default=0).items()} - return relativedelta(**data) # type: ignore - - async def convert(self, ctx: RoboContext, argument: str) -> relativedelta: - try: - return self.__do_conversion(argument) - except ValueError as e: - raise commands.BadArgument(str(e)) from None - - async def transform(self, interaction, value: str) -> relativedelta: - try: - return self.__do_conversion(value) - except ValueError as e: - raise app_commands.AppCommandError(str(e)) from None - - class HumanTime: calendar = pdt.Calendar(version=pdt.VERSION_CONTEXT_STYLE) @@ -206,12 +181,12 @@ def __init__( ): now = now or datetime.datetime.now(tzinfo) dt, status = self.calendar.parseDT(argument, sourceTime=now, tzinfo=None) - if not status.hasDateOrTime: + if not status.hasDateOrTime: # type: ignore (not much I could do here...) raise commands.BadArgument( 'invalid time provided, try e.g. "tomorrow" or "3 days"' ) - if not status.hasTime: + if not status.hasTime: # type: ignore # replace it with the current time dt = dt.replace( hour=now.hour, @@ -261,38 +236,19 @@ def __init__( raise commands.BadArgument("this time is in the past") -class BadTimeTransform(app_commands.AppCommandError): - pass - - -class TimeTransformer(app_commands.Transformer): - async def transform(self, interaction, value: str) -> datetime.datetime: - tzinfo = datetime.timezone.utc - now = interaction.created_at.astimezone(tzinfo) - try: - short = ShortTime(value, now=now, tzinfo=tzinfo) - except commands.BadArgument: - try: - human = FutureTime(value, now=now, tzinfo=tzinfo) - except commands.BadArgument as e: - raise BadTimeTransform(str(e)) from None - else: - return human.dt - else: - return short.dt - - class FriendlyTimeResult: dt: datetime.datetime + td: datetime.timedelta arg: str - __slots__ = ("dt", "td", "arg") + __slots__ = ("dt", "now", "td", "arg") - def __init__(self, dt: datetime.datetime, td: Optional[datetime.timedelta] = None): + def __init__(self, dt: datetime.datetime, now: datetime.datetime): self.dt = dt - self.td = td self.arg = "" + self.td = dt.replace(microsecond=0) - now.replace(microsecond=0) + async def ensure_constraints( self, ctx: RoboContext, @@ -340,13 +296,11 @@ async def convert(self, ctx: RoboContext, argument: str) -> FriendlyTimeResult: tzinfo = datetime.timezone.utc match = regex.match(argument) - logger = logging.getLogger("rodhaj") - logger.info(f"Current Match: {match}") if match is not None and match.group(0): data = {k: int(v) for k, v in match.groupdict(default=0).items()} remaining = argument[match.end() :].strip() dt = now + relativedelta(**data) # type: ignore - result = FriendlyTimeResult(dt.astimezone(tzinfo)) + result = FriendlyTimeResult(dt.astimezone(tzinfo), now) await result.ensure_constraints(ctx, self, now, remaining) return result @@ -356,7 +310,8 @@ async def convert(self, ctx: RoboContext, argument: str) -> FriendlyTimeResult: result = FriendlyTimeResult( datetime.datetime.fromtimestamp( int(match.group("ts")), tz=datetime.timezone.utc - ).astimezone(tzinfo) + ).astimezone(tzinfo), + now, ) remaining = argument[match.end() :].strip() await result.ensure_constraints(ctx, self, now, remaining) @@ -381,7 +336,7 @@ async def convert(self, ctx: RoboContext, argument: str) -> FriendlyTimeResult: # foo date time # first the first two cases: - dt, status, begin, end, dt_string = elements[0] + dt, status, begin, end, _ = elements[0] if not status.hasDateOrTime: raise commands.BadArgument( @@ -414,7 +369,7 @@ async def convert(self, ctx: RoboContext, argument: str) -> FriendlyTimeResult: if status.accuracy == pdt.pdtContext.ACU_HALFDAY: dt = dt + datetime.timedelta(days=1) - result = FriendlyTimeResult(dt) + result = FriendlyTimeResult(dt, now) remaining = "" if begin in (0, 1):