Enhance /remind list and add calculate()

This commit is contained in:
2025-01-03 23:09:47 +01:00
parent 8337a20b50
commit 649cf1b4a3

View File

@ -2,12 +2,16 @@ from __future__ import annotations
import datetime import datetime
import logging import logging
import textwrap
from typing import TYPE_CHECKING 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 apscheduler.job import Job
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger
from discord.abc import PrivateChannel from discord.abc import PrivateChannel
from discord.ui import Button, Select from discord.ui import Button, Select
from discord_webhook import DiscordWebhook from discord_webhook import DiscordWebhook
@ -137,7 +141,7 @@ class RemindGroup(discord.app_commands.Group):
where_and_when = "" where_and_when = ""
channel_id: int | None = self.get_channel_id(interaction, channel) channel_id: int | None = self.get_channel_id(interaction, channel)
if user: if user:
_user_reminder: Job = settings.scheduler.add_job( user_reminder: Job = settings.scheduler.add_job(
send_to_user, send_to_user,
run_date=run_date, run_date=run_date,
kwargs={ kwargs={
@ -150,9 +154,11 @@ class RemindGroup(discord.app_commands.Group):
dm_message = f"and a DM to {user.display_name} " dm_message = f"and a DM to {user.display_name} "
if not dm_and_current_channel: if not dm_and_current_channel:
should_send_channel_reminder = False 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" where_and_when: str = (
f"I will send a DM to {user.display_name} at:\n**{run_date}** (in {calculate(user_reminder)})\n"
)
if should_send_channel_reminder: if should_send_channel_reminder:
_reminder: Job = settings.scheduler.add_job( reminder: Job = settings.scheduler.add_job(
send_to_discord, send_to_discord,
run_date=run_date, run_date=run_date,
kwargs={ kwargs={
@ -161,7 +167,9 @@ class RemindGroup(discord.app_commands.Group):
"author_id": interaction.user.id, "author_id": interaction.user.id,
}, },
) )
where_and_when = f"I will notify you in <#{channel_id}> {dm_message}at:\n**{run_date}** (in)\n" where_and_when = (
f"I will notify you in <#{channel_id}> {dm_message}at:\n**{run_date}** (in {calculate(reminder)})\n"
)
final_message: str = f"Hello {interaction.user.display_name}, {where_and_when}With the message:\n**{message}**." final_message: str = f"Hello {interaction.user.display_name}, {where_and_when}With the message:\n**{message}**."
await interaction.followup.send(final_message) await interaction.followup.send(final_message)
@ -257,6 +265,48 @@ class RemindGroup(discord.app_commands.Group):
await interaction.followup.send(embed=embed, view=view) await interaction.followup.send(embed=embed, view=view)
def calculate(job: Job) -> str:
"""Get trigger time from a reminder and calculate how many days, hours and minutes till trigger.
Days/Minutes will not be included if 0.
Args:
job: The job. Can be cron, interval or normal.
Returns:
Returns days, hours and minutes till the reminder. Returns "Couldn't calculate time" if no job is found.
"""
trigger_time: datetime.datetime | None = (
job.trigger.run_date if isinstance(job.trigger, DateTrigger) else job.next_run_time
)
if trigger_time is None:
logger.error("Couldn't calculate time for job: %s: %s", job.id, job.name)
return "Couldn't calculate time"
countdown_time: datetime.timedelta = trigger_time - datetime.datetime.now(tz=ZoneInfo(settings.config_timezone))
days, hours, minutes = (
countdown_time.days,
countdown_time.seconds // 3600,
countdown_time.seconds // 60 % 60,
)
# Return seconds if only seconds are left.
if days == 0 and hours == 0 and minutes == 0:
seconds: int = countdown_time.seconds % 60
return f"{seconds} second" + ("s" if seconds != 1 else "")
return ", ".join(
f"{x} {y}{'s' * (x != 1)}"
for x, y in (
(days, "day"),
(hours, "hour"),
(minutes, "minute"),
)
if x
)
def create_job_embed(job: Job) -> discord.Embed: def create_job_embed(job: Job) -> discord.Embed:
"""Create an embed for a job. """Create an embed for a job.
@ -266,9 +316,18 @@ def create_job_embed(job: Job) -> discord.Embed:
Returns: Returns:
discord.Embed: The embed for the job. discord.Embed: The embed for the job.
""" """
next_run_time: datetime.datetime | str = (
job.next_run_time.strftime("%Y-%m-%d %H:%M:%S") if job.next_run_time else "Paused"
)
job_kwargs: dict = job.kwargs or {}
channel_id: int = job_kwargs.get("channel_id", 0)
message: str = job_kwargs.get("message", "N/A")
author_id: int = job_kwargs.get("author_id", 0)
embed_title: str = textwrap.shorten(f"{message}", width=256, placeholder="...")
return discord.Embed( return discord.Embed(
title=f"Job: {job.name}", title=embed_title,
description=f"Next run: {job.next_run_time.strftime('%Y-%m-%d %H:%M:%S') if job.next_run_time else 'Paused'}", description=f"ID: {job.id}\nNext run: {next_run_time}\nTime left: {calculate(job)}\nChannel: <#{channel_id}>\nAuthor: <@{author_id}>", # noqa: E501
color=discord.Color.blue(), color=discord.Color.blue(),
) )
@ -283,9 +342,32 @@ class JobSelector(Select):
scheduler: The scheduler to get the jobs from. scheduler: The scheduler to get the jobs from.
""" """
self.scheduler: settings.AsyncIOScheduler = scheduler self.scheduler: settings.AsyncIOScheduler = scheduler
options: list[discord.SelectOption] = [ options: list[discord.SelectOption] = []
discord.SelectOption(label=job.name, value=job.id) for job in settings.scheduler.get_jobs() jobs: list[Job] = scheduler.get_jobs()
]
# Only 25 options are allowed in a select menu.
# TODO(TheLovinator): Add pagination for more than 25 jobs. # noqa: TD003
max_jobs: int = 25
if len(jobs) > max_jobs:
jobs = jobs[:max_jobs]
for job in jobs:
job_kwargs: dict = job.kwargs or {}
label_prefix: str = ""
if job.next_run_time is None:
label_prefix = "Paused: "
# Cron job
elif isinstance(job.trigger, CronTrigger):
label_prefix = "Cron: "
# Interval job
elif isinstance(job.trigger, IntervalTrigger):
label_prefix = "Interval: "
message: str = job_kwargs.get("message", f"{job.id}")
message: str = textwrap.shorten(f"{label_prefix}{message}", width=100, placeholder="...")
options.append(discord.SelectOption(label=message, value=job.id))
super().__init__(placeholder="Select a job...", options=options) super().__init__(placeholder="Select a job...", options=options)
async def callback(self, interaction: discord.Interaction) -> None: async def callback(self, interaction: discord.Interaction) -> None:
@ -296,7 +378,7 @@ class JobSelector(Select):
""" """
job: Job | None = self.scheduler.get_job(self.values[0]) job: Job | None = self.scheduler.get_job(self.values[0])
if job: if job:
embed = create_job_embed(job) embed: discord.Embed = create_job_embed(job)
view = JobManagementView(job, self.scheduler) view = JobManagementView(job, self.scheduler)
await interaction.response.edit_message(embed=embed, view=view) await interaction.response.edit_message(embed=embed, view=view)