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