mirror of
				https://github.com/TheLovinator1/discord-reminder-bot.git
				synced 2025-10-31 08:39:48 +01:00 
			
		
		
		
	Add WIP for /remind list, and make /remind add work
This commit is contained in:
		| @@ -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() | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ dependencies = [ | |||||||
|     "dateparser", |     "dateparser", | ||||||
|     "discord-py", |     "discord-py", | ||||||
|     "discord-webhook", |     "discord-webhook", | ||||||
|  |     "legacy-cgi", | ||||||
|     "python-dotenv", |     "python-dotenv", | ||||||
|     "sqlalchemy", |     "sqlalchemy", | ||||||
| ] | ] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user