Add WIP for /remind list, and make /remind add work

This commit is contained in:
2025-01-03 21:38:44 +01:00
parent 6a6b7bf7d6
commit 8337a20b50
2 changed files with 200 additions and 10 deletions

View File

@ -2,15 +2,22 @@ from __future__ import annotations
import datetime import datetime
import logging import logging
from typing import TYPE_CHECKING
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import dateparser import dateparser
import discord import discord
from apscheduler.job import Job
from discord.abc import PrivateChannel from discord.abc import PrivateChannel
from discord.ui import Button, Select
from discord_webhook import DiscordWebhook from discord_webhook import DiscordWebhook
from discord_reminder_bot import settings from discord_reminder_bot import settings
if TYPE_CHECKING:
from apscheduler.job import Job
from apscheduler.schedulers.asyncio import AsyncIOScheduler
logger: logging.Logger = logging.getLogger(__name__) logger: logging.Logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@ -35,6 +42,8 @@ class RemindBotClient(discord.Client):
async def setup_hook(self) -> None: async def setup_hook(self) -> None:
"""Setup the bot.""" """Setup the bot."""
settings.scheduler.start()
try: try:
self.tree.copy_global_to(guild=GUILD_ID) self.tree.copy_global_to(guild=GUILD_ID)
await self.tree.sync(guild=GUILD_ID) await self.tree.sync(guild=GUILD_ID)
@ -110,17 +119,83 @@ class RemindGroup(discord.app_commands.Group):
user (discord.User, optional): Send reminder as a DM to this user. Defaults to None. user (discord.User, optional): Send reminder as a DM to this user. Defaults to None.
dm_and_current_channel (bool, optional): Send reminder as a DM to the user and in this channel. Defaults to False. dm_and_current_channel (bool, optional): Send reminder as a DM to the user and in this channel. Defaults to False.
""" # noqa: E501 """ # noqa: E501
should_send_channel_reminder = True
await interaction.response.defer() await interaction.response.defer()
self.log_reminder_details(interaction, message, time, channel, user, dm_and_current_channel) self.log_reminder_details(interaction, message, time, channel, user, dm_and_current_channel)
return await self.parse_reminder_time(interaction, time) parsed_time: datetime.datetime | None = await self.parse_reminder_time(interaction, time)
if not parsed_time:
return
run_date: str = parsed_time.strftime("%Y-%m-%d %H:%M:%S")
guild: discord.Guild | None = interaction.guild or None
if not guild:
await interaction.followup.send("Failed to get guild.")
return
dm_message: str = ""
where_and_when = ""
channel_id: int | None = self.get_channel_id(interaction, channel)
if user:
_user_reminder: Job = settings.scheduler.add_job(
send_to_user,
run_date=run_date,
kwargs={
"user_id": user.id,
"guild_id": guild.id,
"message": message,
},
)
dm_message = f"and a DM to {user.display_name} "
if not dm_and_current_channel:
should_send_channel_reminder = False
where_and_when: str = f"I will send a DM to {user.display_name} at:\n**{run_date}** (in )\n"
if should_send_channel_reminder:
_reminder: Job = settings.scheduler.add_job(
send_to_discord,
run_date=run_date,
kwargs={
"channel_id": channel_id,
"message": message,
"author_id": interaction.user.id,
},
)
where_and_when = f"I will notify you in <#{channel_id}> {dm_message}at:\n**{run_date}** (in)\n"
final_message: str = f"Hello {interaction.user.display_name}, {where_and_when}With the message:\n**{message}**."
await interaction.followup.send(final_message)
@staticmethod @staticmethod
async def parse_reminder_time(interaction: discord.Interaction, time: str) -> None: def get_channel_id(interaction: discord.Interaction, channel: discord.TextChannel | None) -> int | None:
"""Get the channel ID to send the reminder to.
Args:
interaction: The interaction object for the command.
channel: The channel to send the reminder to.
Returns:
int: The channel ID to send the reminder to.
"""
channel_id: int | None = None
if interaction.channel:
channel_id = interaction.channel.id
if channel:
logger.info("Channel provided: %s (%s) so using that instead of current channel.", channel, channel.id)
channel_id = channel.id
logger.info("Will send reminder to channel: %s (%s)", channel, channel_id)
return channel_id
@staticmethod
async def parse_reminder_time(interaction: discord.Interaction, time: str) -> datetime.datetime | None:
"""Parse the reminder time. """Parse the reminder time.
Args: Args:
interaction: The interaction object for the command. interaction: The interaction object for the command.
time: The time of the reminder. time: The time of the reminder.
Returns:
datetime.datetime: The parsed time.
""" """
parsed = None parsed = None
error_during_parsing: ValueError | TypeError | None = None error_during_parsing: ValueError | TypeError | None = None
@ -131,8 +206,8 @@ class RemindGroup(discord.app_commands.Group):
error_during_parsing = e error_during_parsing = e
if not parsed: if not parsed:
await interaction.followup.send(f"Failed to parse time. Error: {error_during_parsing}") await interaction.followup.send(f"Failed to parse time. Error: {error_during_parsing}")
return return None
await interaction.followup.send(f"Reminder set for {parsed}") return parsed
@staticmethod @staticmethod
def log_reminder_details( # noqa: PLR0913, PLR0917 def log_reminder_details( # noqa: PLR0913, PLR0917
@ -158,16 +233,130 @@ class RemindGroup(discord.app_commands.Group):
logger.info("Channel: %s User: %s", channel, user) logger.info("Channel: %s User: %s", channel, user)
logger.info("DM and current channel: %s", dm_and_current_channel) logger.info("DM and current channel: %s", dm_and_current_channel)
@discord.app_commands.command(name="list", description="List all reminders") @discord.app_commands.command(name="list", description="List, pause, unpause, and remove reminders.")
async def list(self, interaction: discord.Interaction) -> None: # noqa: PLR6301 async def list(self, interaction: discord.Interaction) -> None: # noqa: PLR6301
"""List all reminders. """List all reminders with pagination and buttons for deleting and modifying jobs.
Args: Args:
interaction: The interaction. interaction(discord.Interaction): The interaction object for the command.
""" """
reminders: list[str] = ["Meeting at 10 AM", "Lunch at 12 PM"] await interaction.response.defer()
reminder_text: str = "\n".join(reminders)
await interaction.response.send_message(f"Your reminders:\n{reminder_text}") jobs: list[Job] = settings.scheduler.get_jobs()
if not jobs:
await interaction.followup.send("No jobs available.")
return
first_job: Job | None = jobs[0] if jobs else None
if not first_job:
await interaction.followup.send("No jobs available.")
return
embed: discord.Embed = create_job_embed(first_job)
view = JobManagementView(first_job, settings.scheduler)
await interaction.followup.send(embed=embed, view=view)
def create_job_embed(job: Job) -> discord.Embed:
"""Create an embed for a job.
Args:
job: The job to create the embed for.
Returns:
discord.Embed: The embed for the job.
"""
return discord.Embed(
title=f"Job: {job.name}",
description=f"Next run: {job.next_run_time.strftime('%Y-%m-%d %H:%M:%S') if job.next_run_time else 'Paused'}",
color=discord.Color.blue(),
)
class JobSelector(Select):
"""Select menu for selecting a job to manage."""
def __init__(self, scheduler: AsyncIOScheduler) -> None:
"""Initialize the job selector.
Args:
scheduler: The scheduler to get the jobs from.
"""
self.scheduler: settings.AsyncIOScheduler = scheduler
options: list[discord.SelectOption] = [
discord.SelectOption(label=job.name, value=job.id) for job in settings.scheduler.get_jobs()
]
super().__init__(placeholder="Select a job...", options=options)
async def callback(self, interaction: discord.Interaction) -> None:
"""Callback for the job selector.
Args:
interaction: The interaction object for the command.
"""
job: Job | None = self.scheduler.get_job(self.values[0])
if job:
embed = create_job_embed(job)
view = JobManagementView(job, self.scheduler)
await interaction.response.edit_message(embed=embed, view=view)
class JobManagementView(discord.ui.View):
"""View for managing jobs."""
def __init__(self, job: Job, scheduler: AsyncIOScheduler) -> None:
"""Initialize the job management view.
Args:
job: The job to manage.
scheduler: The scheduler to manage the job with.
"""
super().__init__(timeout=None)
self.job: Job = job
self.scheduler: settings.AsyncIOScheduler = scheduler
self.add_item(JobSelector(scheduler))
@discord.ui.button(label="Delete", style=discord.ButtonStyle.danger)
async def delete_button(self, interaction: discord.Interaction, button: Button) -> None: # noqa: ARG002
"""Delete the job.
Args:
interaction: The interaction object for the command.
button: The button that was clicked.
"""
self.job.remove()
await interaction.response.send_message(f"Job '{self.job.name}' has been deleted.", ephemeral=True)
self.stop()
@discord.ui.button(label="Modify", style=discord.ButtonStyle.primary)
async def modify_button(self, interaction: discord.Interaction, button: Button) -> None: # noqa: ARG002
"""Modify the job.
Args:
interaction: The interaction object for the command.
button: The button that was clicked.
"""
next_run = self.job.next_run_time.strftime("%Y-%m-%d %H:%M:%S")
await interaction.response.send_message(
f"Current schedule: {next_run}\nPlease use /modify_job command to update the schedule.",
ephemeral=True,
)
@discord.ui.button(label="Pause/Resume", style=discord.ButtonStyle.secondary)
async def pause_button(self, interaction: discord.Interaction, button: Button) -> None: # noqa: ARG002
"""Pause or resume the job.
Args:
interaction: The interaction object for the command.
button: The button that was clicked.
"""
if self.job.next_run_time is None:
self.job.resume()
status = "resumed"
else:
self.job.pause()
status = "paused"
await interaction.response.send_message(f"Job '{self.job.name}' has been {status}.", ephemeral=True)
intents: discord.Intents = discord.Intents.default() intents: discord.Intents = discord.Intents.default()

View File

@ -13,6 +13,7 @@ dependencies = [
"dateparser", "dateparser",
"discord-py", "discord-py",
"discord-webhook", "discord-webhook",
"legacy-cgi",
"python-dotenv", "python-dotenv",
"sqlalchemy", "sqlalchemy",
] ]