diff --git a/didier/cogs/discord.py b/didier/cogs/discord.py index df91673..7418994 100644 --- a/didier/cogs/discord.py +++ b/didier/cogs/discord.py @@ -63,20 +63,12 @@ class Discord(commands.Cog): return await ctx.reply(f"{name or 'Your'} birthday is set to **{day}/{month}**.", mention_author=False) @birthday.command(name="set", aliases=["config"]) - async def birthday_set(self, ctx: commands.Context, day: str, user: Optional[discord.User] = None): + async def birthday_set(self, ctx: commands.Context, day: str): """Set your birthday to `day`. Parsing of the `day`-argument happens in the following order: `DD/MM/YYYY`, `DD/MM/YY`, `DD/MM`. Other formats will not be accepted. """ - # Let owners set other people's birthdays - if user is not None and not await self.client.is_owner(ctx.author): - return await ctx.reply("You don't have permission to set other people's birthdays.", mention_author=False) - - # For regular users: default to your own birthday - if user is None: - user = ctx.author - try: default_year = 2001 date = str_to_date(day, formats=["%d/%m/%Y", "%d/%m/%y", "%d/%m"]) @@ -89,7 +81,7 @@ class Discord(commands.Cog): return await ctx.reply(f"`{day}` is not a valid date.", mention_author=False) async with self.client.postgres_session as session: - await birthdays.add_birthday(session, user.id, date) + await birthdays.add_birthday(session, ctx.author.id, date) await self.client.confirm_message(ctx.message) @commands.group(name="bookmark", aliases=["bm", "bookmarks"], case_insensitive=True, invoke_without_command=True) diff --git a/didier/cogs/fun.py b/didier/cogs/fun.py index 6a89833..98a86d6 100644 --- a/didier/cogs/fun.py +++ b/didier/cogs/fun.py @@ -102,7 +102,8 @@ class Fun(commands.Cog): async def memegen_preview_msg(self, ctx: commands.Context, template: str): """Generate a preview for the meme template `template`, to see how the fields are structured.""" async with ctx.typing(): - meme = await self._do_generate_meme(template, []) + fields = [f"Field #{i + 1}" for i in range(20)] + meme = await self._do_generate_meme(template, fields) return await ctx.reply(meme, mention_author=False) @memes_slash.command(name="generate") @@ -118,9 +119,10 @@ class Fun(commands.Cog): @app_commands.describe(template="The meme template to use in the preview.") async def memegen_preview_slash(self, interaction: discord.Interaction, template: str): """Generate a preview for a meme, to see how the fields are structured.""" - await interaction.response.defer(ephemeral=True) + await interaction.response.defer() - meme_url = await self._do_generate_meme(template, []) + fields = [f"Field #{i + 1}" for i in range(20)] + meme_url = await self._do_generate_meme(template, fields) await interaction.followup.send(meme_url, ephemeral=True) diff --git a/didier/data/apis/imgflip.py b/didier/data/apis/imgflip.py index 416e22c..c44eea6 100644 --- a/didier/data/apis/imgflip.py +++ b/didier/data/apis/imgflip.py @@ -10,10 +10,6 @@ __all__ = ["generate_meme"] def generate_boxes(meme: MemeTemplate, fields: list[str]) -> list[str]: """Generate the template boxes for Imgflip""" - # If no fields were passed, generate a template instead - if not fields: - fields = [f"Field #{i + 1}" for i in range(meme.field_count)] - # If a meme only has 1 field, join all the arguments together into one string if meme.field_count == 1: fields = [" ".join(fields)] diff --git a/didier/data/embeds/error_embed.py b/didier/data/embeds/error_embed.py index ea03bfe..70bc2d3 100644 --- a/didier/data/embeds/error_embed.py +++ b/didier/data/embeds/error_embed.py @@ -20,7 +20,7 @@ def _get_traceback(exception: Exception) -> str: break # Escape Discord Markdown formatting - error_string += line + error_string += line.replace(r"*", r"\*").replace(r"_", r"\_") if line.strip(): error_string += "\n" @@ -45,12 +45,7 @@ def create_error_embed(ctx: Optional[commands.Context], exception: Exception) -> invocation = f"{ctx.author.display_name} in {origin}" - if ctx.interaction is not None and ctx.interaction.command is not None: - command = ctx.interaction.command.name - else: - command = ctx.message.content or "N/A" - - embed.add_field(name="Command", value=command, inline=True) + embed.add_field(name="Command", value=f"{ctx.message.content}", inline=True) embed.add_field(name="Context", value=invocation, inline=True) if message: diff --git a/didier/data/embeds/schedules.py b/didier/data/embeds/schedules.py index ef829cf..98a7a51 100644 --- a/didier/data/embeds/schedules.py +++ b/didier/data/embeds/schedules.py @@ -52,7 +52,7 @@ class Schedule(EmbedBaseModel): if not self.slots: return Schedule() - personal_slots = [] + personal_slots = set() for slot in self.slots: alt_id = slot.alternative_overarching_role_id @@ -65,9 +65,24 @@ class Schedule(EmbedBaseModel): alt_id is not None and alt_id in roles ) if role_found or overarching_role_found: - personal_slots.append(slot) + personal_slots.add(slot) - return Schedule(set(personal_slots)) + return Schedule(personal_slots) + + def simplify(self): + """Merge sequential slots in the same location into one + + Note: this is done in-place instead of returning a new schedule! + (The operation is O(n^2)) + + Example: + 13:00 - 14:30: AD3 in S9 + 14:30 - 1600: AD3 in S9 + """ + for first in self.slots: + for second in self.slots: + if first == second: + continue @overrides def to_embed(self, **kwargs) -> discord.Embed: @@ -116,11 +131,9 @@ class ScheduleSlot: def __post_init__(self): """Fix some properties to display more nicely""" # Re-format the location data - match = re.search(r"(.*)\. (?:Gebouw )?(.*)\. (?:Campus )?(.*)\. ", self.location) - if match is not None: - room, building, campus = match.groups() - room = room.replace("PC / laptoplokaal ", "PC-lokaal") - self.location = f"{campus} {building} {room}" + room, building, campus = re.search(r"(.*)\. (?:Gebouw )?(.*)\. (?:Campus )?(.*)\. ", self.location).groups() + room = room.replace("PC / laptoplokaal ", "PC-lokaal") + self.location = f"{campus} {building} {room}" # The same course can only start once at the same moment, # so this is guaranteed to be unique @@ -164,8 +177,7 @@ class ScheduleSlot: return False if self.start_time == other.end_time: - self.start_time = other.start_time - self._hash = hash(f"{self.course.course_id} {str(self.start_time)}") + other.end_time = self.end_time return True elif self.end_time == other.start_time: self.end_time = other.end_time @@ -222,7 +234,7 @@ async def parse_schedule_from_content(content: str, *, database_session: AsyncSe calendar = Calendar(content) events = list(calendar.events) course_codes: dict[str, UforaCourse] = {} - slots: list[ScheduleSlot] = [] + slots: set[ScheduleSlot] = set() for event in events: code = parse_course_code(event.name) @@ -248,10 +260,9 @@ async def parse_schedule_from_content(content: str, *, database_session: AsyncSe if any(s.could_merge_with(slot) for s in slots): continue - slots.append(slot) + slots.add(slot) - # Cast to set at the END because the __hash__ can change while merging with others - return Schedule(slots=set(slots)) + return Schedule(slots=slots) async def parse_schedule(name: ScheduleType, *, database_session: AsyncSession) -> Optional[Schedule]: diff --git a/didier/didier.py b/didier/didier.py index 06db727..0d1671c 100644 --- a/didier/didier.py +++ b/didier/didier.py @@ -215,14 +215,9 @@ class Didier(commands.Bot): if not message.content.startswith(settings.DISCORD_CUSTOM_COMMAND_PREFIX): return False - # Remove the prefix - content = message.content[len(settings.DISCORD_CUSTOM_COMMAND_PREFIX) :].strip() - - # Message was just "?" (or whatever the prefix was configured to) - if not content: - return False - async with self.postgres_session as session: + # Remove the prefix + content = message.content[len(settings.DISCORD_CUSTOM_COMMAND_PREFIX) :] command = await custom_commands.get_command(session, content) # Command found diff --git a/didier/menus/common.py b/didier/menus/common.py index 3b893da..45b0d95 100644 --- a/didier/menus/common.py +++ b/didier/menus/common.py @@ -169,7 +169,7 @@ class PageSource(ABC, Generic[T]): def get_page_data(self, page: int) -> list[T]: """Get the chunk of the dataset for page [page]""" - return self.dataset[page * self.per_page : (page + 1) * self.per_page] + return self.dataset[page : page + self.per_page] async def start(self, *, ephemeral: bool = False, timeout: Optional[int] = None) -> Menu: """Shortcut to creating (and starting) a Menu with this source diff --git a/settings.py b/settings.py index aa0f6fe..9e36f1f 100644 --- a/settings.py +++ b/settings.py @@ -29,6 +29,7 @@ __all__ = [ "DISCORD_CUSTOM_COMMAND_PREFIX", "UFORA_ANNOUNCEMENTS_CHANNEL", "UFORA_RSS_TOKEN", + "URBAN_DICTIONARY_TOKEN", "IMGFLIP_NAME", "IMGFLIP_PASSWORD", "ScheduleType", @@ -76,6 +77,7 @@ MA_CS_ENG_2_ROLE: Optional[int] = env.int("MA_CS_ENG_2_ROLE", 102330043480016491 """API Keys""" UFORA_RSS_TOKEN: Optional[str] = env.str("UFORA_RSS_TOKEN", None) +URBAN_DICTIONARY_TOKEN: Optional[str] = env.str("URBAN_DICTIONARY_TOKEN", None) IMGFLIP_NAME: Optional[str] = env.str("IMGFLIP_NAME", None) IMGFLIP_PASSWORD: Optional[str] = env.str("IMGFLIP_PASSWORD", None)