mirror of https://github.com/stijndcl/didier
Compare commits
No commits in common. "105cee7e6e7c71bafd02adfc5c8c1830dabab860" and "98b18f7ee3f4099e459d51d0b48096f93faff50a" have entirely different histories.
105cee7e6e
...
98b18f7ee3
|
|
@ -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)
|
return await ctx.reply(f"{name or 'Your'} birthday is set to **{day}/{month}**.", mention_author=False)
|
||||||
|
|
||||||
@birthday.command(name="set", aliases=["config"])
|
@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`.
|
"""Set your birthday to `day`.
|
||||||
|
|
||||||
Parsing of the `day`-argument happens in the following order: `DD/MM/YYYY`, `DD/MM/YY`, `DD/MM`.
|
Parsing of the `day`-argument happens in the following order: `DD/MM/YYYY`, `DD/MM/YY`, `DD/MM`.
|
||||||
Other formats will not be accepted.
|
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:
|
try:
|
||||||
default_year = 2001
|
default_year = 2001
|
||||||
date = str_to_date(day, formats=["%d/%m/%Y", "%d/%m/%y", "%d/%m"])
|
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)
|
return await ctx.reply(f"`{day}` is not a valid date.", mention_author=False)
|
||||||
|
|
||||||
async with self.client.postgres_session as session:
|
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)
|
await self.client.confirm_message(ctx.message)
|
||||||
|
|
||||||
@commands.group(name="bookmark", aliases=["bm", "bookmarks"], case_insensitive=True, invoke_without_command=True)
|
@commands.group(name="bookmark", aliases=["bm", "bookmarks"], case_insensitive=True, invoke_without_command=True)
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@ class Fun(commands.Cog):
|
||||||
async def memegen_preview_msg(self, ctx: commands.Context, template: str):
|
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."""
|
"""Generate a preview for the meme template `template`, to see how the fields are structured."""
|
||||||
async with ctx.typing():
|
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)
|
return await ctx.reply(meme, mention_author=False)
|
||||||
|
|
||||||
@memes_slash.command(name="generate")
|
@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.")
|
@app_commands.describe(template="The meme template to use in the preview.")
|
||||||
async def memegen_preview_slash(self, interaction: discord.Interaction, template: str):
|
async def memegen_preview_slash(self, interaction: discord.Interaction, template: str):
|
||||||
"""Generate a preview for a meme, to see how the fields are structured."""
|
"""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)
|
await interaction.followup.send(meme_url, ephemeral=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,6 @@ __all__ = ["generate_meme"]
|
||||||
|
|
||||||
def generate_boxes(meme: MemeTemplate, fields: list[str]) -> list[str]:
|
def generate_boxes(meme: MemeTemplate, fields: list[str]) -> list[str]:
|
||||||
"""Generate the template boxes for Imgflip"""
|
"""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 a meme only has 1 field, join all the arguments together into one string
|
||||||
if meme.field_count == 1:
|
if meme.field_count == 1:
|
||||||
fields = [" ".join(fields)]
|
fields = [" ".join(fields)]
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ def _get_traceback(exception: Exception) -> str:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Escape Discord Markdown formatting
|
# Escape Discord Markdown formatting
|
||||||
error_string += line
|
error_string += line.replace(r"*", r"\*").replace(r"_", r"\_")
|
||||||
if line.strip():
|
if line.strip():
|
||||||
error_string += "\n"
|
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}"
|
invocation = f"{ctx.author.display_name} in {origin}"
|
||||||
|
|
||||||
if ctx.interaction is not None and ctx.interaction.command is not None:
|
embed.add_field(name="Command", value=f"{ctx.message.content}", inline=True)
|
||||||
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="Context", value=invocation, inline=True)
|
embed.add_field(name="Context", value=invocation, inline=True)
|
||||||
|
|
||||||
if message:
|
if message:
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class Schedule(EmbedBaseModel):
|
||||||
if not self.slots:
|
if not self.slots:
|
||||||
return Schedule()
|
return Schedule()
|
||||||
|
|
||||||
personal_slots = []
|
personal_slots = set()
|
||||||
for slot in self.slots:
|
for slot in self.slots:
|
||||||
alt_id = slot.alternative_overarching_role_id
|
alt_id = slot.alternative_overarching_role_id
|
||||||
|
|
||||||
|
|
@ -65,9 +65,24 @@ class Schedule(EmbedBaseModel):
|
||||||
alt_id is not None and alt_id in roles
|
alt_id is not None and alt_id in roles
|
||||||
)
|
)
|
||||||
if role_found or overarching_role_found:
|
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
|
@overrides
|
||||||
def to_embed(self, **kwargs) -> discord.Embed:
|
def to_embed(self, **kwargs) -> discord.Embed:
|
||||||
|
|
@ -116,11 +131,9 @@ class ScheduleSlot:
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""Fix some properties to display more nicely"""
|
"""Fix some properties to display more nicely"""
|
||||||
# Re-format the location data
|
# Re-format the location data
|
||||||
match = re.search(r"(.*)\. (?:Gebouw )?(.*)\. (?:Campus )?(.*)\. ", self.location)
|
room, building, campus = re.search(r"(.*)\. (?:Gebouw )?(.*)\. (?:Campus )?(.*)\. ", self.location).groups()
|
||||||
if match is not None:
|
room = room.replace("PC / laptoplokaal ", "PC-lokaal")
|
||||||
room, building, campus = match.groups()
|
self.location = f"{campus} {building} {room}"
|
||||||
room = room.replace("PC / laptoplokaal ", "PC-lokaal")
|
|
||||||
self.location = f"{campus} {building} {room}"
|
|
||||||
|
|
||||||
# The same course can only start once at the same moment,
|
# The same course can only start once at the same moment,
|
||||||
# so this is guaranteed to be unique
|
# so this is guaranteed to be unique
|
||||||
|
|
@ -164,8 +177,7 @@ class ScheduleSlot:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.start_time == other.end_time:
|
if self.start_time == other.end_time:
|
||||||
self.start_time = other.start_time
|
other.end_time = self.end_time
|
||||||
self._hash = hash(f"{self.course.course_id} {str(self.start_time)}")
|
|
||||||
return True
|
return True
|
||||||
elif self.end_time == other.start_time:
|
elif self.end_time == other.start_time:
|
||||||
self.end_time = other.end_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)
|
calendar = Calendar(content)
|
||||||
events = list(calendar.events)
|
events = list(calendar.events)
|
||||||
course_codes: dict[str, UforaCourse] = {}
|
course_codes: dict[str, UforaCourse] = {}
|
||||||
slots: list[ScheduleSlot] = []
|
slots: set[ScheduleSlot] = set()
|
||||||
|
|
||||||
for event in events:
|
for event in events:
|
||||||
code = parse_course_code(event.name)
|
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):
|
if any(s.could_merge_with(slot) for s in slots):
|
||||||
continue
|
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=slots)
|
||||||
return Schedule(slots=set(slots))
|
|
||||||
|
|
||||||
|
|
||||||
async def parse_schedule(name: ScheduleType, *, database_session: AsyncSession) -> Optional[Schedule]:
|
async def parse_schedule(name: ScheduleType, *, database_session: AsyncSession) -> Optional[Schedule]:
|
||||||
|
|
|
||||||
|
|
@ -215,14 +215,9 @@ class Didier(commands.Bot):
|
||||||
if not message.content.startswith(settings.DISCORD_CUSTOM_COMMAND_PREFIX):
|
if not message.content.startswith(settings.DISCORD_CUSTOM_COMMAND_PREFIX):
|
||||||
return False
|
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:
|
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 = await custom_commands.get_command(session, content)
|
||||||
|
|
||||||
# Command found
|
# Command found
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ class PageSource(ABC, Generic[T]):
|
||||||
|
|
||||||
def get_page_data(self, page: int) -> list[T]:
|
def get_page_data(self, page: int) -> list[T]:
|
||||||
"""Get the chunk of the dataset for page [page]"""
|
"""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:
|
async def start(self, *, ephemeral: bool = False, timeout: Optional[int] = None) -> Menu:
|
||||||
"""Shortcut to creating (and starting) a Menu with this source
|
"""Shortcut to creating (and starting) a Menu with this source
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ __all__ = [
|
||||||
"DISCORD_CUSTOM_COMMAND_PREFIX",
|
"DISCORD_CUSTOM_COMMAND_PREFIX",
|
||||||
"UFORA_ANNOUNCEMENTS_CHANNEL",
|
"UFORA_ANNOUNCEMENTS_CHANNEL",
|
||||||
"UFORA_RSS_TOKEN",
|
"UFORA_RSS_TOKEN",
|
||||||
|
"URBAN_DICTIONARY_TOKEN",
|
||||||
"IMGFLIP_NAME",
|
"IMGFLIP_NAME",
|
||||||
"IMGFLIP_PASSWORD",
|
"IMGFLIP_PASSWORD",
|
||||||
"ScheduleType",
|
"ScheduleType",
|
||||||
|
|
@ -76,6 +77,7 @@ MA_CS_ENG_2_ROLE: Optional[int] = env.int("MA_CS_ENG_2_ROLE", 102330043480016491
|
||||||
|
|
||||||
"""API Keys"""
|
"""API Keys"""
|
||||||
UFORA_RSS_TOKEN: Optional[str] = env.str("UFORA_RSS_TOKEN", None)
|
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_NAME: Optional[str] = env.str("IMGFLIP_NAME", None)
|
||||||
IMGFLIP_PASSWORD: Optional[str] = env.str("IMGFLIP_PASSWORD", None)
|
IMGFLIP_PASSWORD: Optional[str] = env.str("IMGFLIP_PASSWORD", None)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue