Skip to content

Commit

Permalink
finish up code for guild configs
Browse files Browse the repository at this point in the history
  • Loading branch information
No767 committed Jul 17, 2024
1 parent c0d6bee commit ab0ad18
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 107 deletions.
132 changes: 82 additions & 50 deletions bot/cogs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import datetime
import difflib
from enum import Enum
from typing import (
TYPE_CHECKING,
Annotated,
Expand Down Expand Up @@ -31,6 +32,7 @@

if TYPE_CHECKING:
from cogs.tickets import Tickets

from rodhaj import Rodhaj


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -340,15 +353,16 @@ 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)
return None
return GuildSettings(
account_age=rows["account_age"],
guild_age=rows["guild_age"],
mention=rows["mention"],
**rows["settings"],
)

Expand All @@ -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:
Expand Down Expand Up @@ -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}
Expand All @@ -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()
Expand All @@ -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()
Expand Down
69 changes: 12 additions & 57 deletions bot/libs/utils/time.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit ab0ad18

Please sign in to comment.