diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba38355..10eabb9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.10.1 hooks: - id: isort diff --git a/alembic/versions/954ad804f057_add_events_table.py b/alembic/versions/954ad804f057_add_events_table.py deleted file mode 100644 index c066446..0000000 --- a/alembic/versions/954ad804f057_add_events_table.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Add events table - -Revision ID: 954ad804f057 -Revises: 9fb84b4d9f0b -Create Date: 2023-02-02 22:20:23.107931 - -""" -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision = "954ad804f057" -down_revision = "9fb84b4d9f0b" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "events", - sa.Column("event_id", sa.Integer(), nullable=False), - sa.Column("name", sa.Text(), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("notification_channel", sa.BigInteger(), nullable=False), - sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("event_id"), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("events") - # ### end Alembic commands ### diff --git a/database/crud/events.py b/database/crud/events.py deleted file mode 100644 index 19887e6..0000000 --- a/database/crud/events.py +++ /dev/null @@ -1,50 +0,0 @@ -import datetime -from typing import Optional -from zoneinfo import ZoneInfo - -from dateutil.parser import parse -from sqlalchemy import delete, select -from sqlalchemy.ext.asyncio import AsyncSession - -from database.schemas import Event - -__all__ = ["add_event", "delete_event_by_id", "get_event_by_id", "get_events", "get_next_event"] - - -async def add_event( - session: AsyncSession, *, name: str, description: Optional[str], date_str: str, channel_id: int -) -> Event: - """Create a new event""" - date_dt = parse(date_str, dayfirst=True).replace(tzinfo=ZoneInfo("Europe/Brussels")) - - event = Event(name=name, description=description, timestamp=date_dt, notification_channel=channel_id) - session.add(event) - await session.commit() - await session.refresh(event) - - return event - - -async def delete_event_by_id(session: AsyncSession, event_id: int): - """Delete an event by its id""" - statement = delete(Event).where(Event.event_id == event_id) - await session.execute(statement) - await session.commit() - - -async def get_event_by_id(session: AsyncSession, event_id: int) -> Optional[Event]: - """Get an event by its id""" - statement = select(Event).where(Event.event_id == event_id) - return (await session.execute(statement)).scalar_one_or_none() - - -async def get_events(session: AsyncSession, *, now: datetime.datetime) -> list[Event]: - """Get a list of all upcoming events""" - statement = select(Event).where(Event.timestamp > now) - return (await session.execute(statement)).scalars().all() - - -async def get_next_event(session: AsyncSession, *, now: datetime.datetime) -> Optional[Event]: - """Get the first upcoming event""" - statement = select(Event).where(Event.timestamp > now).order_by(Event.timestamp) - return (await session.execute(statement)).scalars().first() diff --git a/database/schemas.py b/database/schemas.py index 3300e6a..ffda6ba 100644 --- a/database/schemas.py +++ b/database/schemas.py @@ -33,7 +33,6 @@ __all__ = [ "DadJoke", "Deadline", "EasterEgg", - "Event", "FreeGame", "GitHubLink", "Link", @@ -176,18 +175,6 @@ class EasterEgg(Base): startswith: bool = Column(Boolean, nullable=False, server_default="1") -class Event(Base): - """A scheduled event""" - - __tablename__ = "events" - - event_id: int = Column(Integer, primary_key=True) - name: str = Column(Text, nullable=False) - description: Optional[str] = Column(Text, nullable=True) - notification_channel: int = Column(BigInteger, nullable=False) - timestamp: datetime = Column(DateTime(timezone=True), nullable=False) - - class FreeGame(Base): """A temporarily free game""" diff --git a/didier/cogs/discord.py b/didier/cogs/discord.py index 4d9b423..df91673 100644 --- a/didier/cogs/discord.py +++ b/didier/cogs/discord.py @@ -4,22 +4,20 @@ import discord from discord import app_commands from discord.ext import commands -from database.crud import birthdays, bookmarks, events, github +from database.crud import birthdays, bookmarks, github from database.exceptions import ( DuplicateInsertException, Forbidden, ForbiddenNameException, NoResultFoundException, ) -from database.schemas import Event from didier import Didier from didier.exceptions import expect from didier.menus.bookmarks import BookmarkSource from didier.utils.discord import colours from didier.utils.discord.assets import get_author_avatar, get_user_avatar from didier.utils.discord.constants import Limits -from didier.utils.timer import Timer -from didier.utils.types.datetime import localize, str_to_date, tz_aware_now +from didier.utils.types.datetime import str_to_date from didier.utils.types.string import abbreviate, leading from didier.views.modals import CreateBookmark @@ -28,7 +26,6 @@ class Discord(commands.Cog): """Commands related to Discord itself, which work with resources like servers and members.""" client: Didier - timer: Timer # Context-menu references _bookmark_ctx_menu: app_commands.ContextMenu @@ -41,45 +38,12 @@ class Discord(commands.Cog): self._pin_ctx_menu = app_commands.ContextMenu(name="Pin", callback=self._pin_ctx) self.client.tree.add_command(self._bookmark_ctx_menu) self.client.tree.add_command(self._pin_ctx_menu) - self.timer = Timer(self.client) async def cog_unload(self) -> None: """Remove the commands when the cog is unloaded""" self.client.tree.remove_command(self._bookmark_ctx_menu.name, type=self._bookmark_ctx_menu.type) self.client.tree.remove_command(self._pin_ctx_menu.name, type=self._pin_ctx_menu.type) - @commands.Cog.listener() - async def on_event_create(self, event: Event): - """Custom listener called when an event is created""" - self.timer.maybe_replace_task(event) - - @commands.Cog.listener() - async def on_timer_end(self, event_id: int): - """Custom listener called when an event timer ends""" - async with self.client.postgres_session as session: - event = await events.get_event_by_id(session, event_id) - - if event is None: - return await self.client.log_error(f"Unable to find event with id {event_id}", log_to_discord=True) - - channel = self.client.get_channel(event.notification_channel) - human_readable_time = localize(event.timestamp).strftime("%A, %B %d %Y - %H:%M") - - embed = discord.Embed(title=event.name, colour=discord.Colour.blue()) - embed.set_author(name="Upcoming Event") - embed.description = event.description - embed.add_field( - name="Time", value=f"{human_readable_time} ()", inline=False - ) - - await channel.send(embed=embed) - - # Remove the database entry - await events.delete_event_by_id(session, event.event_id) - - # Set the next timer - self.client.loop.create_task(self.timer.update()) - @commands.group(name="birthday", aliases=["bd", "birthdays"], case_insensitive=True, invoke_without_command=True) async def birthday(self, ctx: commands.Context, user: discord.User = None): """Command to check the birthday of `user`. @@ -236,54 +200,6 @@ class Discord(commands.Cog): modal = CreateBookmark(self.client, message.jump_url) await interaction.response.send_modal(modal) - @commands.hybrid_command(name="events") - @app_commands.rename(event_id="id") - @app_commands.describe(event_id="The id of the event to fetch. If not passed, all events are fetched instead.") - async def events(self, ctx: commands.Context, event_id: Optional[int] = None): - """Show information about the event with id `event_id`. - - If no value for `event_id` is supplied, this shows all upcoming events instead. - """ - async with ctx.typing(): - async with self.client.postgres_session as session: - if event_id is None: - upcoming = await events.get_events(session, now=tz_aware_now()) - - embed = discord.Embed(title="Upcoming Events", colour=discord.Colour.blue()) - if not upcoming: - embed.colour = discord.Colour.red() - embed.description = "There are currently no upcoming events scheduled." - return await ctx.reply(embed=embed, mention_author=False) - - upcoming.sort(key=lambda e: e.timestamp.timestamp()) - description_items = [] - - for event in upcoming: - description_items.append( - f"`{event.event_id}`: {event.name} ({discord.utils.format_dt(event.timestamp, style='R')})" - ) - - embed.description = "\n".join(description_items) - return await ctx.reply(embed=embed, mention_author=False) - else: - result_event = await events.get_event_by_id(session, event_id) - if result_event is None: - return await ctx.reply(f"Found no event with id `{event_id}`.", mention_author=False) - - embed = discord.Embed(title="Upcoming Events", colour=discord.Colour.blue()) - embed.add_field(name="Name", value=result_event.name, inline=True) - embed.add_field(name="Id", value=result_event.event_id, inline=True) - embed.add_field( - name="Timer", value=discord.utils.format_dt(result_event.timestamp, style="R"), inline=True - ) - embed.add_field( - name="Channel", - value=self.client.get_channel(result_event.notification_channel).mention, - inline=False, - ) - embed.description = result_event.description - return await ctx.reply(embed=embed, mention_author=False) - @commands.group(name="github", aliases=["gh", "git"], case_insensitive=True, invoke_without_command=True) async def github_group(self, ctx: commands.Context, user: Optional[discord.User] = None): """Show a user's GitHub links. diff --git a/didier/cogs/owner.py b/didier/cogs/owner.py index 139f02c..1c2e76b 100644 --- a/didier/cogs/owner.py +++ b/didier/cogs/owner.py @@ -13,7 +13,6 @@ from didier.utils.discord.flags.owner import EditCustomFlags, SyncOptionFlags from didier.views.modals import ( AddDadJoke, AddDeadline, - AddEvent, AddLink, CreateCustomCommand, EditCustomCommand, @@ -174,15 +173,6 @@ class Owner(commands.Cog): """Autocompletion for the 'course'-parameter""" return self.client.database_caches.ufora_courses.get_autocomplete_suggestions(current) - @add_slash.command(name="event", description="Add a new event") - async def add_event_slash(self, interaction: discord.Interaction): - """Slash command to add new events""" - if not await self.client.is_owner(interaction.user): - return interaction.response.send_message("You don't have permission to run this command.", ephemeral=True) - - modal = AddEvent(self.client) - await interaction.response.send_modal(modal) - @add_slash.command(name="link", description="Add a new link") async def add_link_slash(self, interaction: discord.Interaction): """Slash command to add new links""" diff --git a/didier/data/embeds/deadlines.py b/didier/data/embeds/deadlines.py index ade4421..fe3c988 100644 --- a/didier/data/embeds/deadlines.py +++ b/didier/data/embeds/deadlines.py @@ -51,7 +51,7 @@ class Deadlines(EmbedBaseModel): embed.set_image(url="https://c.tenor.com/RUzJ3lDGQUsAAAAC/iron-man-you-can-rest-now.gif") return embed - for i in range(1, 7): + for i in range(1, 6): if i not in deadlines_grouped: continue diff --git a/didier/utils/timer.py b/didier/utils/timer.py deleted file mode 100644 index fda2cf7..0000000 --- a/didier/utils/timer.py +++ /dev/null @@ -1,68 +0,0 @@ -import asyncio -from datetime import datetime, timedelta -from typing import Optional - -import discord.utils - -import settings -from database.crud.events import get_next_event -from database.schemas import Event -from didier import Didier -from didier.utils.types.datetime import tz_aware_now - -__all__ = ["Timer"] - - -REMINDER_PREDELAY = timedelta(minutes=settings.REMINDER_PRE) - - -class Timer: - """Class for scheduled timers""" - - client: Didier - upcoming_timer: Optional[datetime] - upcoming_event_id: Optional[int] - _task: Optional[asyncio.Task] - - def __init__(self, client: Didier): - self.client = client - - self.upcoming_timer = None - self.upcoming_event_id = None - self._task = None - - self.client.loop.create_task(self.update()) - - async def update(self): - """Get & schedule the closest reminder""" - async with self.client.postgres_session as session: - event = await get_next_event(session, now=tz_aware_now()) - - # No upcoming events - if event is None: - return - - self.maybe_replace_task(event) - - def maybe_replace_task(self, event: Event): - """Replace the current task if necessary""" - # If there is a current (pending) task, and the new timer is sooner than the - # pending one, cancel it - if self._task is not None and not self._task.done(): - # The upcoming timer will never be None at this point, but Mypy is mad - if self.upcoming_timer is not None and self.upcoming_timer > event.timestamp: - self._task.cancel() - else: - # The new task happens after the existing task, it has to wait for its turn - return - - self.upcoming_timer = event.timestamp - self.upcoming_event_id = event.event_id - self._task = self.client.loop.create_task(self.end_timer(endtime=event.timestamp, event_id=event.event_id)) - - async def end_timer(self, *, endtime: datetime, event_id: int): - """Wait until a timer runs out, and then trigger an event to send the message""" - await discord.utils.sleep_until(endtime - REMINDER_PREDELAY) - self.upcoming_timer = None - self.upcoming_event_id = None - self.client.dispatch("timer_end", event_id) diff --git a/didier/utils/types/datetime.py b/didier/utils/types/datetime.py index 23ad285..ed80c7a 100644 --- a/didier/utils/types/datetime.py +++ b/didier/utils/types/datetime.py @@ -7,7 +7,6 @@ __all__ = [ "LOCAL_TIMEZONE", "forward_to_next_weekday", "int_to_weekday", - "localize", "parse_dm_string", "skip_weekends", "str_to_date", @@ -43,14 +42,6 @@ def int_to_weekday(number: int) -> str: # pragma: no cover # it's useless to wr return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"][number] -def localize(dt_instance: datetime.datetime, *, default_timezone="UTC") -> datetime.datetime: - """Localize a datetime instance to my local timezone""" - if dt_instance.tzinfo is None: - dt_instance = dt_instance.replace(tzinfo=zoneinfo.ZoneInfo(default_timezone)) - - return dt_instance.astimezone(LOCAL_TIMEZONE) - - def parse_dm_string(argument: str) -> datetime.date: """Parse a string to [day]/[month] diff --git a/didier/utils/types/string.py b/didier/utils/types/string.py index 6f4a0cc..aec639b 100644 --- a/didier/utils/types/string.py +++ b/didier/utils/types/string.py @@ -94,6 +94,6 @@ def re_replace_with_list(pattern: str, string: str, replacements: list[str]) -> def get_edu_year_name(year: int) -> str: # pragma: no cover """Get the string representation of a university year""" - years = ["1st Bachelor", "2nd Bachelor", "3rd Bachelor", "1st Master", "2nd Master", "Elective Courses (Master)"] + years = ["1st Bachelor", "2nd Bachelor", "3rd Bachelor", "1st Master", "2nd Master"] return years[year] diff --git a/didier/views/modals/__init__.py b/didier/views/modals/__init__.py index fe3c352..e9f92f0 100644 --- a/didier/views/modals/__init__.py +++ b/didier/views/modals/__init__.py @@ -2,7 +2,6 @@ from .bookmarks import CreateBookmark from .custom_commands import CreateCustomCommand, EditCustomCommand from .dad_jokes import AddDadJoke from .deadlines import AddDeadline -from .events import AddEvent from .links import AddLink from .memes import GenerateMeme @@ -10,7 +9,6 @@ __all__ = [ "CreateBookmark", "AddDadJoke", "AddDeadline", - "AddEvent", "CreateCustomCommand", "EditCustomCommand", "AddLink", diff --git a/didier/views/modals/events.py b/didier/views/modals/events.py deleted file mode 100644 index e7b92b4..0000000 --- a/didier/views/modals/events.py +++ /dev/null @@ -1,61 +0,0 @@ -import traceback -from zoneinfo import ZoneInfo - -import discord -from dateutil.parser import ParserError, parse -from overrides import overrides - -from database.crud.events import add_event -from didier import Didier - -__all__ = ["AddEvent"] - - -class AddEvent(discord.ui.Modal, title="Add Event"): - """Modal to add a new event""" - - name: discord.ui.TextInput = discord.ui.TextInput(label="Name", style=discord.TextStyle.short, required=True) - description: discord.ui.TextInput = discord.ui.TextInput( - label="Description", style=discord.TextStyle.paragraph, required=False, default=None - ) - channel: discord.ui.TextInput = discord.ui.TextInput( - label="Channel id", style=discord.TextStyle.short, required=True, placeholder="676713433567199232" - ) - timestamp: discord.ui.TextInput = discord.ui.TextInput( - label="Date", style=discord.TextStyle.short, required=True, placeholder="21/02/2020 21:21:00" - ) - - client: Didier - - def __init__(self, client: Didier, *args, **kwargs): - super().__init__(*args, **kwargs) - self.client = client - - @overrides - async def on_submit(self, interaction: discord.Interaction) -> None: - try: - parse(self.timestamp.value, dayfirst=True).replace(tzinfo=ZoneInfo("Europe/Brussels")) - except ParserError: - return await interaction.response.send_message("Unable to parse date argument.", ephemeral=True) - - if self.client.get_channel(int(self.channel.value)) is None: - return await interaction.response.send_message( - f"Unable to find channel `{self.channel.value}`", ephemeral=True - ) - - async with self.client.postgres_session as session: - event = await add_event( - session, - name=self.name.value, - description=self.description.value, - date_str=self.timestamp.value, - channel_id=int(self.channel.value), - ) - - await interaction.response.send_message(f"Successfully added event `{event.event_id}`.", ephemeral=True) - self.client.dispatch("event_create", event) - - @overrides - async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore - await interaction.response.send_message("Something went wrong.", ephemeral=True) - traceback.print_tb(error.__traceback__) diff --git a/requirements-dev.txt b/requirements-dev.txt index a9f7109..091b275 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,6 @@ black==22.3.0 coverage[toml]==6.4.1 freezegun==1.2.1 -isort==5.12.0 mypy==0.961 pre-commit==2.20.0 pytest==7.1.2 diff --git a/settings.py b/settings.py index 32bd5e0..3342ea0 100644 --- a/settings.py +++ b/settings.py @@ -16,7 +16,6 @@ __all__ = [ "YEAR", "MENU_TIMEOUT", "EASTER_EGG_CHANCE", - "REMINDER_PRE", "POSTGRES_DB", "POSTGRES_USER", "POSTGRES_PASS", @@ -48,7 +47,6 @@ SEMESTER: int = env.int("SEMESTER", 2) YEAR: int = env.int("YEAR", 3) MENU_TIMEOUT: int = env.int("MENU_TIMEOUT", 30) EASTER_EGG_CHANCE: int = env.int("EASTER_EGG_CHANCE", 15) -REMINDER_PRE: int = env.int("REMINDER_PRE", 15) """Database""" # PostgreSQL