diff --git a/src/bot.py b/src/bot.py index 458fb0f..202a010 100644 --- a/src/bot.py +++ b/src/bot.py @@ -195,6 +195,7 @@ async def setup_hook(self) -> None: "src.reports", "src.roles", "src.welcome", + "src.testing", ) for i, extension in enumerate(extensions): try: diff --git a/src/testing.py b/src/testing.py new file mode 100644 index 0000000..64d5fa5 --- /dev/null +++ b/src/testing.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING, Literal + +import discord +from discord import app_commands +from discord.ext import commands + +from src.utils import DateTransformer, EmojiEmbed, TimeTransformer + +from .views import MILBotView + +if TYPE_CHECKING: + from .bot import MILBot + + +class TestingSignUpSelect(discord.ui.Select): + def __init__(self, vehicle: str): + options = [ + discord.SelectOption(label="I cannot come", value="no", emoji="❌"), + discord.SelectOption( + label="I can come, but need a ride", + value="cannotdrive", + emoji="🚶", + ), + discord.SelectOption( + label="I can come, and will bring my car", + value="candrive", + emoji="🚗", + ), + ] + if vehicle == "SubjuGator": + options.append( + discord.SelectOption( + label="I can come, and drive the sub", + value="candrivesub", + emoji="🤿", + ), + ) + super().__init__( + custom_id="testing_signup:select", + placeholder="Please respond with your availability...", + max_values=1, + options=options, + ) + + +class TestingSignUpView(MILBotView): + def __init__(self, vehicle: str): + super().__init__(timeout=None) + self.add_item(TestingSignUpSelect(vehicle)) + + +class TestingCog(commands.Cog): + def __init__(self, bot: MILBot): + self.bot = bot + + @app_commands.command() + @app_commands.checks.has_role("Leaders") + async def testing( + self, + interaction: discord.Interaction, + vehicle: Literal["SubjuGator", "NaviGator", "Other"], + date: app_commands.Transform[datetime.date, DateTransformer], + location: str, + arrive_time: app_commands.Transform[datetime.time, TimeTransformer], + max_people: int = 10, + ): + embed = EmojiEmbed( + title="Upcoming Testing: Are you going?", + color=discord.Color.from_str("0x5BCEFA"), + description="A leader has indicated that a testing is taking place soon. Having members come to testing is super helpful to streamlining the testing process, and for making the testing experience great for everyone. We'd appreciate if you could make it!", + ) + arrive_dt = datetime.datetime.combine(date, arrive_time) + prep_dt = arrive_dt - datetime.timedelta(minutes=60) + date_str = f"{discord.utils.format_dt(arrive_dt, 'D')} ({discord.utils.format_dt(arrive_dt, 'R')})" + embed.add_field(emoji="🚀", name="Vehicle", value=vehicle, inline=True) + embed.add_field(emoji="📍", name="Location", value=location, inline=True) + embed.add_field( + emoji="👥", + name="Max People", + value=f"{max_people} people", + inline=True, + ) + embed.add_field(emoji="📅", name="Date", value=date_str, inline=True) + embed.add_field( + emoji="⏰", + name="Prep Time", + value=discord.utils.format_dt(prep_dt, "t"), + inline=True, + ) + embed.add_field( + emoji="⏰", + name="Testing Starts", + value=discord.utils.format_dt(arrive_dt, "t"), + inline=True, + ) + mention = ( + self.bot.leaders_role.mention + if interaction.channel == self.bot.leaders_channel + else self.bot.egn4912_role + ) + await interaction.response.send_message( + mention, + embed=embed, + view=TestingSignUpView(vehicle), + ) + + +async def setup(bot: MILBot): + await bot.add_cog(TestingCog(bot)) diff --git a/src/utils.py b/src/utils.py index 644b74d..50d4214 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,4 +1,8 @@ import datetime +import re + +import discord +from discord import app_commands from .constants import SEMESTERS @@ -15,6 +19,91 @@ def is_active() -> bool: return False +def emoji_header(emoji: str, title: str) -> str: + return f"{emoji} __{title}__" + + +class EmojiEmbed(discord.Embed): + def add_field( + self, + emoji: str, + name: str, + value: str, + *, + inline: bool = False, + ) -> None: + super().add_field(name=emoji_header(emoji, name), value=value, inline=inline) + + +class DateTransformer(app_commands.Transformer): + async def transform( + self, + interaction: discord.Interaction, + value: str, + ) -> datetime.date: + # Supports the following formats: + # 2021-09-01 + # 9/1/2021 + # 9/1/21 + # 9-1-2021 + # 9-1-21 + # 9/1 + # 9-1 + + value = value.replace(" ", "") + + # Define regex patterns for different date formats + patterns = { + r"^(\d{4})-(\d{2})-(\d{2})$": "%Y-%m-%d", # 2021-09-01 + r"^(\d{1,2})/(\d{1,2})/(\d{4})$": "%m/%d/%Y", # 9/1/2021 + r"^(\d{1,2})/(\d{1,2})/(\d{2})$": "%m/%d/%y", # 9/1/21 + r"^(\d{1,2})-(\d{1,2})-(\d{4})$": "%m-%d-%Y", # 9-1-2021 + r"^(\d{1,2})-(\d{1,2})-(\d{2})$": "%m-%d-%y", # 9-1-21 + r"^(\d{1,2})/(\d{1,2})$": "%m/%d", # 9/1 + r"^(\d{1,2})-(\d{1,2})$": "%m-%d", # 9-1 + } + + for pattern, date_format in patterns.items(): + if re.match(pattern, value): + # Convert matched value to datetime.date object + date_value = datetime.datetime.strptime(value, date_format).date() + return date_value + + # If no pattern matches, you might want to raise an error or handle it gracefully + raise ValueError("Invalid date format. Please enter a valid date.") + + +class TimeTransformer(app_commands.Transformer): + async def transform( + self, + interaction: discord.Interaction, + value: str, + ) -> datetime.time: + # Supports the following formats: + # 09:00 + # 9:00 AM + # 9:00 am + # 9AM + + value = value.replace(" ", "").lower() + + # Define regex patterns for different time formats + patterns = { + r"^(\d{1,2}):(\d{2})$": "%H:%M", # 09:00 + r"^(\d{1,2}):(\d{2})(am|pm)$": "%I:%M%p", # 9:00am + r"^(\d{1,2})(am|pm)$": "%I%p", # 9am + } + + for pattern, time_format in patterns.items(): + if re.match(pattern, value): + # Convert matched value to datetime.time object + time_value = datetime.datetime.strptime(value, time_format).time() + return time_value + + # If no pattern matches, you might want to raise an error or handle it gracefully + raise ValueError("Invalid time format. Please enter a valid time.") + + def capped_str(parts: list[str], cap: int = 1024) -> str: """ Joins the most parts possible with a new line between them. If the resulting