Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite the /remindme command #541

Merged
merged 8 commits into from
Sep 21, 2024
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ model Reminder {
reminder_expires_at DateTime
reminder_channel_id BigInt
reminder_user_id BigInt
reminder_sent Boolean @default(false)
guild_id BigInt
guild Guild @relation(fields: [guild_id], references: [guild_id])
Expand Down
143 changes: 30 additions & 113 deletions tux/cogs/utility/remindme.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import asyncio
import contextlib
import datetime

import discord
from discord import app_commands
from discord.ext import commands
from discord.ext import commands, tasks
from loguru import logger

from prisma.models import Reminder
Expand All @@ -14,40 +13,26 @@
from tux.utils.functions import convert_to_seconds


def get_closest_reminder(reminders: list[Reminder]) -> Reminder | None:
"""
Check if there are any reminders and return the closest one.
Parameters
----------
reminders : list[Reminder]
A list of reminders to check.
Returns
-------
Reminder | None
The closest reminder or None if there are no reminders.
"""
return min(reminders, key=lambda x: x.reminder_expires_at) if reminders else None


class RemindMe(commands.Cog):
def __init__(self, bot: Tux) -> None:
self.bot = bot
self.db = DatabaseController().reminder
self.bot.loop.create_task(self.update())
self.check_reminders.start()

@tasks.loop(seconds=120)
async def check_reminders(self):
reminders = await self.db.get_unsent_reminders()

async def send_reminders(self, reminder: Reminder) -> None:
"""
Send the reminder to the user.
try:
for reminder in reminders:
await self.send_reminder(reminder)
await self.db.update_reminder_status(reminder.reminder_id, sent=True)
logger.debug(f'Status of reminder {reminder.reminder_id} updated to "sent".')

Parameters
----------
reminder : Reminder
The reminder object.
"""
except Exception as e:
logger.error(f"Error sending reminders: {e}")

async def send_reminder(self, reminder: Reminder) -> None:
user = self.bot.get_user(reminder.reminder_user_id)

if user is not None:
Expand All @@ -64,103 +49,37 @@ async def send_reminders(self, reminder: Reminder) -> None:
await user.send(embed=embed)

except discord.Forbidden:
# Send a message in the channel if the user has DMs closed
channel: discord.abc.GuildChannel | discord.Thread | discord.abc.PrivateChannel | None = (
self.bot.get_channel(reminder.reminder_channel_id)
)

if channel is not None and isinstance(
channel,
discord.TextChannel | discord.Thread | discord.VoiceChannel,
):
channel = self.bot.get_channel(reminder.reminder_channel_id)

if isinstance(channel, discord.TextChannel | discord.Thread | discord.VoiceChannel):
with contextlib.suppress(discord.Forbidden):
await channel.send(
content=f"{user.mention} Failed to DM you, sending in channel",
embed=embed,
)
return

else:
logger.error(
f"Failed to send reminder to {user.id}, DMs closed and channel not found.",
f"Failed to send reminder {reminder.reminder_id}, DMs closed and channel not found.",
)

else:
logger.error(f"Failed to send reminder to {reminder.reminder_user_id}, user not found.")

# Delete the reminder after sending
await self.db.delete_reminder_by_id(reminder.reminder_id)

# wait for a second so that the reminder is deleted before checking for more reminders
# who knows if this works, it seems to
await asyncio.sleep(1)

# Run update again to check if there are any more reminders
await self.update()

async def end_timer(self, reminder: Reminder) -> None:
"""
End the timer for the reminder.
Parameters
----------
reminder : Reminder
The reminder object.
"""

# Wait until the reminder expires
await discord.utils.sleep_until(reminder.reminder_expires_at)
await self.send_reminders(reminder)

async def update(self) -> None:
"""
Update the reminders
Check if there are any reminders and send the closest one.
"""

try:
# Get all reminders
reminders = await self.db.get_all_reminders()
# Get the closest reminder
closest_reminder = get_closest_reminder(reminders)

except Exception as e:
logger.error(f"Error getting reminders: {e}")
return

# If there are no reminders, return
if closest_reminder is None:
return

# Check if it's expired
if closest_reminder.reminder_expires_at < datetime.datetime.now(datetime.UTC):
await self.send_reminders(closest_reminder)
return
logger.error(
f"Failed to send reminder {reminder.reminder_id}, user with ID {reminder.reminder_user_id} not found.",
)

# Create a task to wait until the reminder expires
self.bot.loop.create_task(self.end_timer(closest_reminder))
@check_reminders.before_loop
async def before_check_reminders(self):
await self.bot.wait_until_ready()

@app_commands.command(
name="remindme",
description="Reminds you after a certain amount of time.",
)
async def remindme(self, interaction: discord.Interaction, time: str, *, reminder: str) -> None:
"""
Set a reminder for a certain amount of time.
Parameters
----------
interaction : discord.Interaction
The discord interaction object.
time : str
Time in the format `[number][M/w/d/h/m/s]`.
reminder : str
Reminder content.
"""

seconds = convert_to_seconds(time)

# Check if the time is valid (this is set to 0 if the time is invalid via convert_to_seconds)
if seconds == 0:
await interaction.response.send_message(
"Invalid time format. Please use the format `[number][M/w/d/h/m/s]`.",
Expand All @@ -169,13 +88,13 @@ async def remindme(self, interaction: discord.Interaction, time: str, *, reminde
)
return

seconds = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=seconds)
expires_at = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=seconds)

try:
await self.db.insert_reminder(
reminder_user_id=interaction.user.id,
reminder_content=reminder,
reminder_expires_at=seconds,
reminder_expires_at=expires_at,
reminder_channel_id=interaction.channel_id or 0,
guild_id=interaction.guild_id or 0,
)
Expand All @@ -186,12 +105,13 @@ async def remindme(self, interaction: discord.Interaction, time: str, *, reminde
user_name=interaction.user.name,
user_display_avatar=interaction.user.display_avatar.url,
title="Reminder Set",
description=f"Reminder set for <t:{int(seconds.timestamp())}:f>.",
description=f"Reminder set for <t:{int(expires_at.timestamp())}:f>.",
)

embed.add_field(
name="Note",
value="If you have DMs closed, the reminder may not reach you. We will attempt to send it in this channel instead, however it is not guaranteed.",
value="- If you have DMs closed, we will attempt to send it in this channel instead.\n"
"- The reminder may be delayed by up to 120 seconds due to the way Tux works.",
)

except Exception as e:
Expand All @@ -207,9 +127,6 @@ async def remindme(self, interaction: discord.Interaction, time: str, *, reminde

await interaction.response.send_message(embed=embed, ephemeral=True)

# Run update again to check if this reminder is the closest
await self.update()


async def setup(bot: Tux) -> None:
await bot.add_cog(RemindMe(bot))
23 changes: 22 additions & 1 deletion tux/database/controllers/reminder.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import UTC, datetime

from prisma.models import Guild, Reminder
from tux.database.client import db
Expand All @@ -21,6 +21,10 @@ async def get_all_reminders(self) -> list[Reminder]:
async def get_reminder_by_id(self, reminder_id: int) -> Reminder | None:
return await self.table.find_first(where={"reminder_id": reminder_id})

async def get_unsent_reminders(self) -> list[Reminder]:
wlinator marked this conversation as resolved.
Show resolved Hide resolved
now = datetime.now(UTC)
return await self.table.find_many(where={"reminder_sent": False, "reminder_expires_at": {"lte": now}})

async def insert_reminder(
self,
reminder_user_id: int,
Expand All @@ -38,6 +42,7 @@ async def insert_reminder(
"reminder_expires_at": reminder_expires_at,
"reminder_channel_id": reminder_channel_id,
"guild_id": guild_id,
"reminder_sent": False,
},
)

Expand All @@ -53,3 +58,19 @@ async def update_reminder_by_id(
where={"reminder_id": reminder_id},
data={"reminder_content": reminder_content},
)

async def update_reminder_status(self, reminder_id: int, sent: bool = True) -> None:
"""
Update the status of a reminder. This sets the value "reminder_sent" to True by default.
Parameters
----------
reminder_id : int
The ID of the reminder to update.
sent : bool
The new status of the reminder.
"""
await self.table.update(
where={"reminder_id": reminder_id},
data={"reminder_sent": sent},
)