Use Ruff and fix all its warnings and errors
This commit is contained in:
56
README.md
56
README.md
@ -13,7 +13,7 @@ Type `/remind` in a Discord server where this bot exists to get a list of slash
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
You have two choices, [install directly on your computer](#Install-directly-on-your-computer) or
|
You have two choices, [install directly on your computer](#install-directly-on-your-computer) or
|
||||||
using [Docker](https://hub.docker.com/r/thelovinator/discord-reminder-bot).
|
using [Docker](https://hub.docker.com/r/thelovinator/discord-reminder-bot).
|
||||||
|
|
||||||
### Creating a Discord bot token
|
### Creating a Discord bot token
|
||||||
@ -23,39 +23,39 @@ using [Docker](https://hub.docker.com/r/thelovinator/discord-reminder-bot).
|
|||||||
- You can change Icon and Username here.
|
- You can change Icon and Username here.
|
||||||
- Copy the bot token and paste it into the `BOT_TOKEN` environment variable.
|
- Copy the bot token and paste it into the `BOT_TOKEN` environment variable.
|
||||||
- Go to the OAuth2 page -> URL Generator
|
- Go to the OAuth2 page -> URL Generator
|
||||||
- Select the `bot` and `applications.commands` scope.
|
- Select the `bot` and `applications.commands` scope.
|
||||||
- Select the bot permissions that you want the bot to have. Select `Administrator`. (TODO: Add a list of permissions
|
- Select the bot permissions that you want the bot to have. Select `Administrator`. (TODO: Add a list of permissions
|
||||||
that are needed)
|
that are needed)
|
||||||
- Copy the generated URL and open it in your browser. You can now invite the bot to your server.
|
- Copy the generated URL and open it in your browser. You can now invite the bot to your server.
|
||||||
|
|
||||||
### Install directly on your computer
|
### Install directly on your computer
|
||||||
|
|
||||||
- Install the latest version of needed software:
|
- Install the latest version of needed software:
|
||||||
- [Python](https://www.python.org/)
|
- [Python](https://www.python.org/)
|
||||||
- You should use the latest version.
|
- You should use the latest version.
|
||||||
- You want to add Python to your PATH.
|
- You want to add Python to your PATH.
|
||||||
- Windows: Find `App execution aliases` and disable python.exe and python3.exe
|
- Windows: Find `App execution aliases` and disable python.exe and python3.exe
|
||||||
- [Poetry](https://python-poetry.org/docs/master/#installation)
|
- [Poetry](https://python-poetry.org/docs/master/#installation)
|
||||||
- Windows: You have to add `%appdata%\Python\Scripts` to your PATH for Poetry to work.
|
- Windows: You have to add `%appdata%\Python\Scripts` to your PATH for Poetry to work.
|
||||||
- Download project from GitHub with Git or download
|
- Download project from GitHub with Git or download
|
||||||
the [ZIP](https://github.com/TheLovinator1/discord-reminder-bot/archive/refs/heads/master.zip).
|
the [ZIP](https://github.com/TheLovinator1/discord-reminder-bot/archive/refs/heads/master.zip).
|
||||||
- If you want to update the bot, you can run `git pull` in the project folder or download the ZIP again.
|
- If you want to update the bot, you can run `git pull` in the project folder or download the ZIP again.
|
||||||
- Rename .env.example to .env and open it in a text editor (e.g., VSCode, Notepad++, Notepad).
|
- Rename .env.example to .env and open it in a text editor (e.g., VSCode, Notepad++, Notepad).
|
||||||
- If you can't see the file extension:
|
- If you can't see the file extension:
|
||||||
- Windows 10: Click the View Tab in File Explorer and click the box next to File name extensions.
|
- Windows 10: Click the View Tab in File Explorer and click the box next to File name extensions.
|
||||||
- Windows 11: Click View -> Show -> File name extensions.
|
- Windows 11: Click View -> Show -> File name extensions.
|
||||||
- Open a terminal in the repository folder.
|
- Open a terminal in the repository folder.
|
||||||
- Windows 10: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and select `Open PowerShell window here`
|
- Windows 10: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and select `Open PowerShell window here`
|
||||||
- Windows 11: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and Show more options
|
- Windows 11: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and Show more options
|
||||||
and `Open PowerShell window here`
|
and `Open PowerShell window here`
|
||||||
- Install requirements:
|
- Install requirements:
|
||||||
- Type `poetry install` into the PowerShell window. Make sure you are
|
- Type `poetry install` into the PowerShell window. Make sure you are
|
||||||
in the repository folder with the [pyproject.toml](pyproject.toml) file.
|
in the repository folder with the [pyproject.toml](pyproject.toml) file.
|
||||||
- You may have to restart your terminal if it can't find the `poetry` command. Also double check it is in
|
- You may have to restart your terminal if it can't find the `poetry` command. Also double check it is in
|
||||||
your PATH.
|
your PATH.
|
||||||
- Start the bot:
|
- Start the bot:
|
||||||
- Type `poetry run bot` into the PowerShell window.
|
- Type `poetry run bot` into the PowerShell window.
|
||||||
- You can stop the bot with <kbd>Ctrl</kbd> + <kbd>c</kbd>.
|
- You can stop the bot with <kbd>Ctrl</kbd> + <kbd>c</kbd>.
|
||||||
|
|
||||||
Note: You will need to run `poetry install` again if poetry.lock has been modified.
|
Note: You will need to run `poetry install` again if poetry.lock has been modified.
|
||||||
|
|
||||||
@ -66,17 +66,17 @@ Note: It can take up to one hour for the slash commands to be visible in the Dis
|
|||||||
Docker Hub: [thelovinator/discord-reminder-bot](https://hub.docker.com/r/thelovinator/discord-reminder-bot)
|
Docker Hub: [thelovinator/discord-reminder-bot](https://hub.docker.com/r/thelovinator/discord-reminder-bot)
|
||||||
|
|
||||||
- Rename .env.example to .env and open it in a text editor (e.g., VSCode, Notepad++, Notepad).
|
- Rename .env.example to .env and open it in a text editor (e.g., VSCode, Notepad++, Notepad).
|
||||||
- If you can't see the file extension:
|
- If you can't see the file extension:
|
||||||
- Windows 10: Click the View Tab in File Explorer and click the box next to File name extensions.
|
- Windows 10: Click the View Tab in File Explorer and click the box next to File name extensions.
|
||||||
- Windows 11: Click View -> Show -> File name extensions.
|
- Windows 11: Click View -> Show -> File name extensions.
|
||||||
- Open a terminal in the extras folder.
|
- Open a terminal in the extras folder.
|
||||||
- Windows 10: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and select `Open PowerShell window here`
|
- Windows 10: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and select `Open PowerShell window here`
|
||||||
- Windows 11: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and Show more options
|
- Windows 11: <kbd>Shift</kbd> + <kbd>right-click</kbd> in the folder and Show more options
|
||||||
and `Open PowerShell window here`
|
and `Open PowerShell window here`
|
||||||
- Run the Docker Compose file:
|
- Run the Docker Compose file:
|
||||||
- `docker-compose up`
|
- `docker-compose up`
|
||||||
- You can stop the bot with <kbd>Ctrl</kbd> + <kbd>c</kbd>.
|
- You can stop the bot with <kbd>Ctrl</kbd> + <kbd>c</kbd>.
|
||||||
- If you want to run the bot in the background, you can run `docker-compose up -d`.
|
- If you want to run the bot in the background, you can run `docker-compose up -d`.
|
||||||
|
|
||||||
## Help
|
## Help
|
||||||
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
"""
|
from datetime import datetime, timedelta
|
||||||
calculate(job) - Calculates how many days, hours and minutes till trigger.
|
|
||||||
"""
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from apscheduler.job import Job
|
from apscheduler.job import Job
|
||||||
@ -11,8 +8,7 @@ from discord_reminder_bot.settings import config_timezone
|
|||||||
|
|
||||||
|
|
||||||
def calculate(job: Job) -> str:
|
def calculate(job: Job) -> str:
|
||||||
"""Get trigger time from a reminder and calculate how many days,
|
"""Get trigger time from a reminder and calculate how many days, hours and minutes till trigger.
|
||||||
hours and minutes till trigger.
|
|
||||||
|
|
||||||
Days/Minutes will not be included if 0.
|
Days/Minutes will not be included if 0.
|
||||||
|
|
||||||
@ -25,10 +21,7 @@ def calculate(job: Job) -> str:
|
|||||||
# TODO: This "breaks" when only seconds are left.
|
# TODO: This "breaks" when only seconds are left.
|
||||||
# If we use (in {calc_countdown(job)}) it will show (in )
|
# If we use (in {calc_countdown(job)}) it will show (in )
|
||||||
|
|
||||||
if type(job.trigger) is DateTrigger:
|
trigger_time: datetime | None = job.trigger.run_date if type(job.trigger) is DateTrigger else job.next_run_time
|
||||||
trigger_time = job.trigger.run_date
|
|
||||||
else:
|
|
||||||
trigger_time = job.next_run_time
|
|
||||||
|
|
||||||
# Get_job() returns None when it can't find a job with that ID.
|
# Get_job() returns None when it can't find a job with that ID.
|
||||||
if trigger_time is None:
|
if trigger_time is None:
|
||||||
@ -41,8 +34,7 @@ def calculate(job: Job) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def countdown(trigger_time: datetime) -> str:
|
def countdown(trigger_time: datetime) -> str:
|
||||||
"""
|
"""Calculate days, hours and minutes to a date.
|
||||||
Calculate days, hours and minutes to a date.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
trigger_time: The date.
|
trigger_time: The date.
|
||||||
@ -50,7 +42,7 @@ def countdown(trigger_time: datetime) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
A string with the days, hours and minutes.
|
A string with the days, hours and minutes.
|
||||||
"""
|
"""
|
||||||
countdown_time = trigger_time - datetime.datetime.now(tz=pytz.timezone(config_timezone))
|
countdown_time: timedelta = trigger_time - datetime.now(tz=pytz.timezone(config_timezone))
|
||||||
|
|
||||||
days, hours, minutes = (
|
days, hours, minutes = (
|
||||||
countdown_time.days,
|
countdown_time.days,
|
||||||
@ -60,7 +52,7 @@ def countdown(trigger_time: datetime) -> str:
|
|||||||
|
|
||||||
# Return seconds if only seconds are left.
|
# Return seconds if only seconds are left.
|
||||||
if days == 0 and hours == 0 and minutes == 0:
|
if days == 0 and hours == 0 and minutes == 0:
|
||||||
seconds = countdown_time.seconds % 60
|
seconds: int = countdown_time.seconds % 60
|
||||||
return f"{seconds} second" + ("s" if seconds != 1 else "")
|
return f"{seconds} second" + ("s" if seconds != 1 else "")
|
||||||
|
|
||||||
# TODO: Explain this.
|
# TODO: Explain this.
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
"""This module creates the pages for the paginator."""
|
from typing import TYPE_CHECKING, Literal
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import interactions
|
import interactions
|
||||||
from apscheduler.job import Job
|
|
||||||
from apscheduler.triggers.date import DateTrigger
|
from apscheduler.triggers.date import DateTrigger
|
||||||
from interactions import ActionRow, CommandContext, ComponentContext
|
from interactions import ActionRow, Button, CommandContext, ComponentContext, Embed, Message, Modal, TextInput
|
||||||
from interactions.ext.paginator import Page, Paginator, RowPosition
|
from interactions.ext.paginator import Page, Paginator, RowPosition
|
||||||
|
|
||||||
from discord_reminder_bot.countdown import calculate
|
from discord_reminder_bot.countdown import calculate
|
||||||
from discord_reminder_bot.settings import scheduler
|
from discord_reminder_bot.settings import scheduler
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
def create_pages(ctx: CommandContext) -> list[Page]:
|
from apscheduler.job import Job
|
||||||
|
|
||||||
|
max_message_length: Literal[1010] = 1010
|
||||||
|
max_title_length: Literal[90] = 90
|
||||||
|
|
||||||
|
|
||||||
|
async def create_pages(ctx: CommandContext) -> list[Page]:
|
||||||
"""Create pages for the paginator.
|
"""Create pages for the paginator.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -20,65 +26,69 @@ def create_pages(ctx: CommandContext) -> list[Page]:
|
|||||||
Returns:
|
Returns:
|
||||||
list[Page]: A list of pages.
|
list[Page]: A list of pages.
|
||||||
"""
|
"""
|
||||||
pages = []
|
pages: list[Page] = []
|
||||||
|
|
||||||
jobs: List[Job] = scheduler.get_jobs()
|
jobs: list[Job] = scheduler.get_jobs()
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
channel_id = job.kwargs.get("channel_id")
|
channel_id: int = job.kwargs.get("channel_id")
|
||||||
guild_id = job.kwargs.get("guild_id")
|
guild_id: int = job.kwargs.get("guild_id")
|
||||||
|
|
||||||
|
if ctx.guild is None:
|
||||||
|
await ctx.send("I can't find the server you're in. Are you sure you're in a server?", ephemeral=True)
|
||||||
|
return pages
|
||||||
|
if ctx.guild.channels is None:
|
||||||
|
await ctx.send("I can't find the channel you're in.", ephemeral=True)
|
||||||
|
return pages
|
||||||
|
|
||||||
# Only add reminders from channels in the server we run "/reminder list" in
|
# Only add reminders from channels in the server we run "/reminder list" in
|
||||||
# Check if channel is in the Discord server, if not, skip it.
|
# Check if channel is in the Discord server, if not, skip it.
|
||||||
for channel in ctx.guild.channels:
|
for channel in ctx.guild.channels:
|
||||||
if int(channel.id) == channel_id or ctx.guild_id == guild_id:
|
if int(channel.id) == channel_id or ctx.guild_id == guild_id:
|
||||||
if type(job.trigger) is DateTrigger:
|
trigger_time: datetime | None = (
|
||||||
# Get trigger time for normal reminders
|
job.trigger.run_date if type(job.trigger) is DateTrigger else job.next_run_time
|
||||||
trigger_time = job.trigger.run_date
|
)
|
||||||
else:
|
|
||||||
# Get trigger time for cron and interval jobs
|
|
||||||
trigger_time = job.next_run_time
|
|
||||||
|
|
||||||
# Paused reminders returns None
|
# Paused reminders returns None
|
||||||
if trigger_time is None:
|
if trigger_time is None:
|
||||||
trigger_value = None
|
trigger_value: str | None = None
|
||||||
trigger_text = "Paused"
|
trigger_text: str = "Paused"
|
||||||
else:
|
else:
|
||||||
trigger_value = f'{trigger_time.strftime("%Y-%m-%d %H:%M")} (in {calculate(job)})'
|
trigger_value = f'{trigger_time.strftime("%Y-%m-%d %H:%M")} (in {calculate(job)})'
|
||||||
trigger_text = trigger_value
|
trigger_text = trigger_value
|
||||||
|
|
||||||
message = job.kwargs.get("message")
|
message: str = job.kwargs.get("message")
|
||||||
message = f"{message[:1000]}..." if len(message) > 1010 else message
|
message = f"{message[:1000]}..." if len(message) > max_message_length else message
|
||||||
|
|
||||||
edit_button = interactions.Button(
|
edit_button: Button = interactions.Button(
|
||||||
label="Edit",
|
label="Edit",
|
||||||
style=interactions.ButtonStyle.PRIMARY,
|
style=interactions.ButtonStyle.PRIMARY,
|
||||||
custom_id="edit",
|
custom_id="edit",
|
||||||
)
|
)
|
||||||
pause_button = interactions.Button(
|
pause_button: Button = interactions.Button(
|
||||||
label="Pause",
|
label="Pause",
|
||||||
style=interactions.ButtonStyle.PRIMARY,
|
style=interactions.ButtonStyle.PRIMARY,
|
||||||
custom_id="pause",
|
custom_id="pause",
|
||||||
)
|
)
|
||||||
unpause_button = interactions.Button(
|
unpause_button: Button = interactions.Button(
|
||||||
label="Unpause",
|
label="Unpause",
|
||||||
style=interactions.ButtonStyle.PRIMARY,
|
style=interactions.ButtonStyle.PRIMARY,
|
||||||
custom_id="unpause",
|
custom_id="unpause",
|
||||||
)
|
)
|
||||||
remove_button = interactions.Button(
|
remove_button: Button = interactions.Button(
|
||||||
label="Remove",
|
label="Remove",
|
||||||
style=interactions.ButtonStyle.DANGER,
|
style=interactions.ButtonStyle.DANGER,
|
||||||
custom_id="remove",
|
custom_id="remove",
|
||||||
)
|
)
|
||||||
|
|
||||||
embed = interactions.Embed(
|
embed: Embed = interactions.Embed(
|
||||||
title=f"{job.id}",
|
title=f"{job.id}",
|
||||||
fields=[
|
fields=[
|
||||||
interactions.EmbedField(
|
interactions.EmbedField(
|
||||||
name=f"**Channel:**",
|
name="**Channel:**",
|
||||||
value=f"#{channel.name}",
|
value=f"#{channel.name}",
|
||||||
),
|
),
|
||||||
interactions.EmbedField(
|
interactions.EmbedField(
|
||||||
name=f"**Message:**",
|
name="**Message:**",
|
||||||
value=f"{message}",
|
value=f"{message}",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -86,16 +96,16 @@ def create_pages(ctx: CommandContext) -> list[Page]:
|
|||||||
|
|
||||||
if trigger_value is not None:
|
if trigger_value is not None:
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name=f"**Trigger:**",
|
name="**Trigger:**",
|
||||||
value=f"{trigger_text}",
|
value=f"{trigger_text}",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name=f"**Trigger:**",
|
name="**Trigger:**",
|
||||||
value=f"_Paused_",
|
value="_Paused_",
|
||||||
)
|
)
|
||||||
|
|
||||||
components = [
|
components: list[Button] = [
|
||||||
edit_button,
|
edit_button,
|
||||||
remove_button,
|
remove_button,
|
||||||
]
|
]
|
||||||
@ -103,39 +113,42 @@ def create_pages(ctx: CommandContext) -> list[Page]:
|
|||||||
if type(job.trigger) is not DateTrigger:
|
if type(job.trigger) is not DateTrigger:
|
||||||
# Get trigger time for cron and interval jobs
|
# Get trigger time for cron and interval jobs
|
||||||
trigger_time = job.next_run_time
|
trigger_time = job.next_run_time
|
||||||
if trigger_time is None:
|
pause_or_unpause_button: Button = unpause_button if trigger_time is None else pause_button
|
||||||
pause_or_unpause_button = unpause_button
|
|
||||||
else:
|
|
||||||
pause_or_unpause_button = pause_button
|
|
||||||
components.insert(1, pause_or_unpause_button)
|
components.insert(1, pause_or_unpause_button)
|
||||||
|
|
||||||
# Add a page to pages list
|
# Add a page to pages list
|
||||||
title = f"{message[:87]}..." if len(message) > 90 else message
|
title: str = f"{message[:87]}..." if len(message) > max_title_length else message
|
||||||
pages.append(
|
pages.append(
|
||||||
Page(
|
Page(
|
||||||
embeds=embed,
|
embeds=embed,
|
||||||
title=title,
|
title=title,
|
||||||
components=ActionRow(components=components),
|
components=ActionRow(components=components), # type: ignore # noqa: PGH003
|
||||||
callback=callback,
|
callback=callback,
|
||||||
position=RowPosition.BOTTOM,
|
position=RowPosition.BOTTOM,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return pages
|
return pages
|
||||||
|
|
||||||
|
|
||||||
async def callback(self: Paginator, ctx: ComponentContext):
|
async def callback(self: Paginator, ctx: ComponentContext) -> Message | None: # noqa: PLR0911
|
||||||
"""Callback for the paginator."""
|
"""Callback for the paginator."""
|
||||||
job_id = self.component_ctx.message.embeds[0].title
|
if self.component_ctx is None:
|
||||||
job = scheduler.get_job(job_id)
|
return await ctx.send("Something went wrong.", ephemeral=True)
|
||||||
|
|
||||||
|
if self.component_ctx.message is None:
|
||||||
|
return await ctx.send("Something went wrong.", ephemeral=True)
|
||||||
|
|
||||||
|
job_id: str | None = self.component_ctx.message.embeds[0].title
|
||||||
|
job: Job | None = scheduler.get_job(job_id)
|
||||||
|
|
||||||
if job is None:
|
if job is None:
|
||||||
return await ctx.send("Job not found.", ephemeral=True)
|
return await ctx.send("Job not found.", ephemeral=True)
|
||||||
|
|
||||||
channel_id = job.kwargs.get("channel_id")
|
channel_id: int = job.kwargs.get("channel_id")
|
||||||
old_message = job.kwargs.get("message")
|
old_message: str = job.kwargs.get("message")
|
||||||
|
|
||||||
components = [
|
components: list[TextInput] = [
|
||||||
interactions.TextInput(
|
interactions.TextInput(
|
||||||
style=interactions.TextStyleType.PARAGRAPH,
|
style=interactions.TextStyleType.PARAGRAPH,
|
||||||
label="New message",
|
label="New message",
|
||||||
@ -147,8 +160,8 @@ async def callback(self: Paginator, ctx: ComponentContext):
|
|||||||
|
|
||||||
if type(job.trigger) is DateTrigger:
|
if type(job.trigger) is DateTrigger:
|
||||||
# Get trigger time for normal reminders
|
# Get trigger time for normal reminders
|
||||||
trigger_time = job.trigger.run_date
|
trigger_time: datetime | None = job.trigger.run_date
|
||||||
job_type = "normal"
|
job_type: str = "normal"
|
||||||
components.append(
|
components.append(
|
||||||
interactions.TextInput(
|
interactions.TextInput(
|
||||||
style=interactions.TextStyleType.SHORT,
|
style=interactions.TextStyleType.SHORT,
|
||||||
@ -165,29 +178,31 @@ async def callback(self: Paginator, ctx: ComponentContext):
|
|||||||
job_type = "cron/interval"
|
job_type = "cron/interval"
|
||||||
|
|
||||||
if ctx.custom_id == "edit":
|
if ctx.custom_id == "edit":
|
||||||
modal = interactions.Modal(
|
modal: Modal = interactions.Modal(
|
||||||
title=f"Edit {job_type} reminder.",
|
title=f"Edit {job_type} reminder.",
|
||||||
custom_id="edit_modal",
|
custom_id="edit_modal",
|
||||||
components=components,
|
components=components, # type: ignore # noqa: PGH003
|
||||||
)
|
)
|
||||||
await ctx.popup(modal)
|
await ctx.popup(modal)
|
||||||
|
return None
|
||||||
|
|
||||||
elif ctx.custom_id == "pause":
|
if ctx.custom_id == "pause":
|
||||||
# TODO: Add unpause button if user paused the wrong job
|
|
||||||
scheduler.pause_job(job_id)
|
scheduler.pause_job(job_id)
|
||||||
await ctx.send(f"Job {job_id} paused.")
|
await ctx.send(f"Job {job_id} paused.")
|
||||||
|
return None
|
||||||
|
|
||||||
elif ctx.custom_id == "unpause":
|
if ctx.custom_id == "unpause":
|
||||||
# TODO: Add pause button if user unpauses the wrong job
|
|
||||||
scheduler.resume_job(job_id)
|
scheduler.resume_job(job_id)
|
||||||
await ctx.send(f"Job {job_id} unpaused.")
|
await ctx.send(f"Job {job_id} unpaused.")
|
||||||
|
return None
|
||||||
|
|
||||||
elif ctx.custom_id == "remove":
|
if ctx.custom_id == "remove":
|
||||||
# TODO: Add recreate button if user removed the wrong job
|
|
||||||
scheduler.remove_job(job_id)
|
scheduler.remove_job(job_id)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
f"Job {job_id} removed.\n"
|
f"Job {job_id} removed.\n"
|
||||||
f"**Message:** {old_message}\n"
|
f"**Message:** {old_message}\n"
|
||||||
f"**Channel:** {channel_id}\n"
|
f"**Channel:** {channel_id}\n"
|
||||||
f"**Time:** {trigger_time}"
|
f"**Time:** {trigger_time}",
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
@ -1,71 +1,83 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import interactions
|
import interactions
|
||||||
from apscheduler import events
|
from apscheduler import events
|
||||||
from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_MISSED
|
from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_MISSED, JobExecutionEvent
|
||||||
from apscheduler.jobstores.base import JobLookupError
|
from apscheduler.jobstores.base import JobLookupError
|
||||||
from apscheduler.triggers.date import DateTrigger
|
from apscheduler.triggers.date import DateTrigger
|
||||||
from discord_webhook import DiscordWebhook
|
from discord_webhook import DiscordWebhook
|
||||||
from interactions import CommandContext, Embed, OptionType, autodefer
|
from interactions import Channel, Client, CommandContext, Embed, Member, Message, OptionType, autodefer
|
||||||
from interactions.ext.paginator import Paginator
|
from interactions.ext.paginator import Page, Paginator
|
||||||
|
|
||||||
from discord_reminder_bot.countdown import calculate
|
from discord_reminder_bot.countdown import calculate
|
||||||
from discord_reminder_bot.create_pages import create_pages
|
from discord_reminder_bot.create_pages import create_pages
|
||||||
from discord_reminder_bot.parse import parse_time
|
from discord_reminder_bot.parse import ParsedTime, parse_time
|
||||||
from discord_reminder_bot.settings import (
|
from discord_reminder_bot.settings import (
|
||||||
bot_token,
|
bot_token,
|
||||||
config_timezone,
|
config_timezone,
|
||||||
log_level,
|
log_level,
|
||||||
scheduler,
|
scheduler,
|
||||||
sqlite_location,
|
sqlite_location,
|
||||||
webhook_url
|
webhook_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
bot = interactions.Client(token=bot_token)
|
if TYPE_CHECKING:
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from apscheduler.job import Job
|
||||||
|
|
||||||
|
bot: Client = interactions.Client(token=bot_token)
|
||||||
|
|
||||||
|
|
||||||
def send_webhook(url=webhook_url, message: str = "discord-reminder-bot: Empty message."):
|
def send_webhook(
|
||||||
"""
|
url: str = webhook_url,
|
||||||
Send a webhook to Discord.
|
message: str = "discord-reminder-bot: Empty message.",
|
||||||
|
) -> None:
|
||||||
|
"""Send a webhook to Discord.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: Our webhook url, defaults to the one from settings.
|
url: Our webhook url, defaults to the one from settings.
|
||||||
message: The message that will be sent to Discord.
|
message: The message that will be sent to Discord.
|
||||||
"""
|
"""
|
||||||
if not url:
|
if not url:
|
||||||
print("ERROR: Tried to send a webhook but you have no webhook url configured.")
|
logging.error("ERROR: Tried to send a webhook but you have no webhook url configured.")
|
||||||
return
|
return
|
||||||
webhook = DiscordWebhook(url=url, content=message, rate_limit_retry=True)
|
webhook: DiscordWebhook = DiscordWebhook(url=url, content=message, rate_limit_retry=True)
|
||||||
webhook.execute()
|
webhook.execute()
|
||||||
|
|
||||||
|
|
||||||
@bot.command(name="remind")
|
@bot.command(name="remind")
|
||||||
async def base_command(ctx: interactions.CommandContext):
|
async def base_command(ctx: interactions.CommandContext) -> None: # noqa: ARG001
|
||||||
"""This is the base command for the reminder bot."""
|
"""This is the base command for the reminder bot."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@bot.modal("edit_modal")
|
@bot.modal("edit_modal")
|
||||||
async def modal_response_edit(ctx: CommandContext, *response: str):
|
async def modal_response_edit(ctx: CommandContext, *response: str) -> Message: # noqa: C901, PLR0912, PLR0911
|
||||||
"""This is what gets triggerd when the user clicks the Edit button in /reminder list.
|
"""This is what gets triggered when the user clicks the Edit button in /reminder list.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx: Context of the slash command. Contains the guild, author and message and more.
|
ctx: Context of the slash command. Contains the guild, author and message and more.
|
||||||
|
response: The response from the modal.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A Discord message with changes.
|
A Discord message with changes.
|
||||||
"""
|
"""
|
||||||
job_id = ctx.message.embeds[0].title
|
if not ctx.message:
|
||||||
old_date = None
|
return await ctx.send(
|
||||||
old_message = None
|
"The message that triggered this modal is missing. Or something else went wrong.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
job_id: str | None = ctx.message.embeds[0].title
|
||||||
|
old_date: str | None = None
|
||||||
|
old_message: str | None = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
job = scheduler.get_job(job_id)
|
job: Job | None = scheduler.get_job(job_id)
|
||||||
except JobLookupError as e:
|
except JobLookupError as e:
|
||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
f"Failed to get the job after the modal.\n"
|
f"Failed to get the job after the modal.\nJob ID: {job_id}\nError: {e}",
|
||||||
f"Job ID: {job_id}\n"
|
|
||||||
f"Error: {e}",
|
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -76,50 +88,54 @@ async def modal_response_edit(ctx: CommandContext, *response: str):
|
|||||||
return await ctx.send("No changes made.", ephemeral=True)
|
return await ctx.send("No changes made.", ephemeral=True)
|
||||||
|
|
||||||
if type(job.trigger) is DateTrigger:
|
if type(job.trigger) is DateTrigger:
|
||||||
new_message = response[0]
|
new_message: str | None = response[0]
|
||||||
new_date = response[1]
|
new_date: str | None = response[1]
|
||||||
else:
|
else:
|
||||||
new_message = response[0]
|
new_message = response[0]
|
||||||
new_date = None
|
new_date = None
|
||||||
|
|
||||||
message_embeds: List[Embed] = ctx.message.embeds
|
message_embeds: list[Embed] = ctx.message.embeds
|
||||||
for embeds in message_embeds:
|
for embeds in message_embeds:
|
||||||
|
if embeds.fields is None:
|
||||||
|
return await ctx.send("No fields found in the embed.", ephemeral=True)
|
||||||
|
|
||||||
for field in embeds.fields:
|
for field in embeds.fields:
|
||||||
if field.name == "**Channel:**":
|
if field.name == "**Channel:**":
|
||||||
continue
|
continue
|
||||||
elif field.name == "**Message:**":
|
if field.name == "**Message:**":
|
||||||
old_message = field.value
|
old_message = field.value
|
||||||
elif field.name == "**Trigger:**":
|
if field.name == "**Trigger:**":
|
||||||
old_date = field.value
|
old_date = field.value
|
||||||
else:
|
else:
|
||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
f"Unknown field name ({field.name}).", ephemeral=True
|
f"Unknown field name ({field.name}).",
|
||||||
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = f"Modified job {job_id}.\n"
|
msg: str = f"Modified job {job_id}.\n"
|
||||||
if old_date is not None:
|
if old_date is not None and new_date:
|
||||||
if new_date:
|
# Parse the time/date we got from the command.
|
||||||
# Parse the time/date we got from the command.
|
parsed: ParsedTime = parse_time(date_to_parse=new_date)
|
||||||
parsed = parse_time(date_to_parse=new_date)
|
if parsed.err:
|
||||||
if parsed.err:
|
return await ctx.send(parsed.err_msg)
|
||||||
return await ctx.send(parsed.err_msg)
|
parsed_date: datetime | None = parsed.parsed_time
|
||||||
parsed_date = parsed.parsed_time
|
|
||||||
date_new = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
new_job = scheduler.reschedule_job(job.id, run_date=date_new)
|
if parsed_date is None:
|
||||||
new_time = calculate(new_job)
|
return await ctx.send(f"Failed to parse the date. ({new_date})")
|
||||||
|
|
||||||
# TODO: old_date and date_new has different precision.
|
date_new: str = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
# Old date: 2032-09-18 00:07
|
|
||||||
# New date: 2032-09-18 00:07:13
|
new_job: Job = scheduler.reschedule_job(job.id, run_date=date_new)
|
||||||
msg += (
|
new_time: str = calculate(new_job)
|
||||||
f"**Old date**: {old_date}\n"
|
|
||||||
f"**New date**: {date_new} (in {new_time})\n"
|
# TODO: old_date and date_new has different precision.
|
||||||
)
|
# Old date: 2032-09-18 00:07
|
||||||
|
# New date: 2032-09-18 00:07:13
|
||||||
|
msg += f"**Old date**: {old_date}\n**New date**: {date_new} (in {new_time})\n"
|
||||||
|
|
||||||
if old_message is not None:
|
if old_message is not None:
|
||||||
channel_id = job.kwargs.get("channel_id")
|
channel_id: int = job.kwargs.get("channel_id")
|
||||||
job_author_id = job.kwargs.get("author_id")
|
job_author_id: int = job.kwargs.get("author_id")
|
||||||
try:
|
try:
|
||||||
scheduler.modify_job(
|
scheduler.modify_job(
|
||||||
job.id,
|
job.id,
|
||||||
@ -131,9 +147,7 @@ async def modal_response_edit(ctx: CommandContext, *response: str):
|
|||||||
)
|
)
|
||||||
except JobLookupError as e:
|
except JobLookupError as e:
|
||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
f"Failed to modify the job.\n"
|
f"Failed to modify the job.\nJob ID: {job_id}\nError: {e}",
|
||||||
f"Job ID: {job_id}\n"
|
|
||||||
f"Error: {e}",
|
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
msg += f"**Old message**: {old_message}\n**New message**: {new_message}\n"
|
msg += f"**Old message**: {old_message}\n**New message**: {new_message}\n"
|
||||||
@ -142,44 +156,65 @@ async def modal_response_edit(ctx: CommandContext, *response: str):
|
|||||||
|
|
||||||
|
|
||||||
@autodefer()
|
@autodefer()
|
||||||
@bot.command(name="parse", description="Parse the time from a string")
|
@bot.command(name="parse", description="Parse the time from a string") # type: ignore # noqa: PGH003
|
||||||
@interactions.option(name="time_to_parse", description="The string you want to parse.", type=OptionType.STRING, required=True)
|
@interactions.option(
|
||||||
@interactions.option(name="optional_timezone", description="Optional time zone, for example Europe/Stockholm", type=OptionType.STRING, required=False)
|
name="time_to_parse",
|
||||||
async def parse_command(ctx: interactions.CommandContext, time_to_parse: str, optional_timezone: str | None = None):
|
description="The string you want to parse.",
|
||||||
"""
|
type=OptionType.STRING,
|
||||||
Find the date and time from a string.
|
required=True,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="optional_timezone",
|
||||||
|
description="Optional time zone, for example Europe/Stockholm",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
async def parse_command(
|
||||||
|
ctx: interactions.CommandContext,
|
||||||
|
time_to_parse: str,
|
||||||
|
optional_timezone: str | None = None,
|
||||||
|
) -> Message:
|
||||||
|
"""Find the date and time from a string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx: Context of the slash command. Contains the guild, author and message and more.
|
ctx: Context of the slash command. Contains the guild, author and message and more.
|
||||||
time_to_parse: The string you want to parse.
|
time_to_parse: The string you want to parse.
|
||||||
optional_timezone: Optional time zone, for example Europe/Stockholm.
|
optional_timezone: Optional time zone, for example Europe/Stockholm.
|
||||||
"""
|
"""
|
||||||
if optional_timezone:
|
if optional_timezone:
|
||||||
parsed = parse_time(date_to_parse=time_to_parse, timezone=optional_timezone)
|
parsed: ParsedTime = parse_time(date_to_parse=time_to_parse, timezone=optional_timezone)
|
||||||
else:
|
else:
|
||||||
parsed = parse_time(date_to_parse=time_to_parse)
|
parsed = parse_time(date_to_parse=time_to_parse)
|
||||||
if parsed.err:
|
if parsed.err:
|
||||||
return await ctx.send(parsed.err_msg)
|
return await ctx.send(parsed.err_msg)
|
||||||
parsed_date = parsed.parsed_time
|
parsed_date: datetime | None = parsed.parsed_time
|
||||||
|
|
||||||
# Locale’s appropriate date and time representation.
|
if parsed_date is None:
|
||||||
locale_time = parsed_date.strftime("%c")
|
return await ctx.send(f"Failed to parse the date. ({time_to_parse})")
|
||||||
run_date = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
return await ctx.send(f"**String**: {time_to_parse}\n"
|
# Locale`s appropriate date and time representation.
|
||||||
f"**Parsed date**: {parsed_date}\n"
|
locale_time: str = parsed_date.strftime("%c")
|
||||||
f"**Formatted**: {run_date}\n"
|
run_date: str = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
f"**Locale time**: {locale_time}\n")
|
return await ctx.send(
|
||||||
|
f"**String**: {time_to_parse}\n"
|
||||||
|
f"**Parsed date**: {parsed_date}\n"
|
||||||
|
f"**Formatted**: {run_date}\n"
|
||||||
|
f"**Locale time**: {locale_time}\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@autodefer()
|
@autodefer()
|
||||||
@base_command.subcommand(name="list", description="List, pause, unpause, and remove reminders.")
|
@base_command.subcommand(
|
||||||
async def list_command(ctx: interactions.CommandContext):
|
name="list",
|
||||||
|
description="List, pause, unpause, and remove reminders.",
|
||||||
|
)
|
||||||
|
async def list_command(ctx: interactions.CommandContext) -> Message | None:
|
||||||
"""List, pause, unpause, and remove reminders.
|
"""List, pause, unpause, and remove reminders.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ctx: Context of the slash command. Contains the guild, author and message and more.
|
ctx: Context of the slash command. Contains the guild, author and message and more.
|
||||||
"""
|
"""
|
||||||
|
pages: list[Page] = await create_pages(ctx)
|
||||||
pages = create_pages(ctx)
|
|
||||||
if not pages:
|
if not pages:
|
||||||
return await ctx.send("No reminders found.", ephemeral=True)
|
return await ctx.send("No reminders found.", ephemeral=True)
|
||||||
|
|
||||||
@ -187,7 +222,7 @@ async def list_command(ctx: interactions.CommandContext):
|
|||||||
for page in pages:
|
for page in pages:
|
||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
content="I haven't added support for buttons if there is only one reminder, "
|
content="I haven't added support for buttons if there is only one reminder, "
|
||||||
"so you need to add another one to edit/delete this one 🙃",
|
"so you need to add another one to edit/delete this one 🙃",
|
||||||
embeds=page.embeds,
|
embeds=page.embeds,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -202,23 +237,49 @@ async def list_command(ctx: interactions.CommandContext):
|
|||||||
)
|
)
|
||||||
|
|
||||||
await paginator.run()
|
await paginator.run()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@autodefer()
|
@autodefer()
|
||||||
@base_command.subcommand(name="add", description="Set a reminder.")
|
@base_command.subcommand(name="add", description="Set a reminder.")
|
||||||
@interactions.option(name="message_reason", description="The message I'm going to send you.", type=OptionType.STRING, required=True)
|
@interactions.option(
|
||||||
@interactions.option(name="message_date", description="The date to send the message.", type=OptionType.STRING, required=True)
|
name="message_reason",
|
||||||
@interactions.option(name="different_channel", description="The channel to send the message to.", type=OptionType.CHANNEL, required=False)
|
description="The message I'm going to send you.",
|
||||||
@interactions.option(name="send_dm_to_user", description="Send message to a user via DM instead of a channel. Set both_dm_and_channel to send both.", type=OptionType.USER, required=False) # noqa
|
type=OptionType.STRING,
|
||||||
@interactions.option(name="both_dm_and_channel", description="Send both DM and message to the channel, needs send_dm_to_user to be set if you want both.", type=OptionType.BOOLEAN, required=False) # noqa
|
required=True,
|
||||||
async def command_add(
|
)
|
||||||
ctx: interactions.CommandContext,
|
@interactions.option(
|
||||||
message_reason: str,
|
name="message_date",
|
||||||
message_date: str,
|
description="The date to send the message.",
|
||||||
different_channel: interactions.Channel | None = None,
|
type=OptionType.STRING,
|
||||||
send_dm_to_user: interactions.User | None = None,
|
required=True,
|
||||||
both_dm_and_channel: bool | None = None,
|
)
|
||||||
):
|
@interactions.option(
|
||||||
|
name="different_channel",
|
||||||
|
description="The channel to send the message to.",
|
||||||
|
type=OptionType.CHANNEL,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="send_dm_to_user",
|
||||||
|
description="Send message to a user via DM instead of a channel. Set both_dm_and_channel to send both.",
|
||||||
|
type=OptionType.USER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="both_dm_and_channel",
|
||||||
|
description="Send both DM and message to the channel, needs send_dm_to_user to be set if you want both.",
|
||||||
|
type=OptionType.BOOLEAN,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
async def command_add( # noqa: PLR0913
|
||||||
|
ctx: interactions.CommandContext,
|
||||||
|
message_reason: str,
|
||||||
|
message_date: str,
|
||||||
|
different_channel: interactions.Channel | None = None,
|
||||||
|
send_dm_to_user: interactions.User | None = None,
|
||||||
|
both_dm_and_channel: bool | None = None,
|
||||||
|
) -> Message | None:
|
||||||
"""Add a new reminder. You can add a date and message.
|
"""Add a new reminder. You can add a date and message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -230,23 +291,27 @@ async def command_add(
|
|||||||
both_dm_and_channel: If we should send both a DM and a message to the channel. Works with different_channel.
|
both_dm_and_channel: If we should send both a DM and a message to the channel. Works with different_channel.
|
||||||
"""
|
"""
|
||||||
# Parse the time/date we got from the command.
|
# Parse the time/date we got from the command.
|
||||||
parsed = parse_time(date_to_parse=message_date)
|
parsed: ParsedTime = parse_time(date_to_parse=message_date)
|
||||||
if parsed.err:
|
if parsed.err:
|
||||||
return await ctx.send(parsed.err_msg)
|
return await ctx.send(parsed.err_msg)
|
||||||
parsed_date = parsed.parsed_time
|
parsed_date: datetime | None = parsed.parsed_time
|
||||||
run_date = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
if parsed_date is None:
|
||||||
|
return await ctx.send(f"Failed to parse the date. ({message_date})")
|
||||||
|
|
||||||
|
run_date: str = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
# If we should send the message to a different channel
|
# If we should send the message to a different channel
|
||||||
channel_id = int(ctx.channel_id)
|
channel_id = int(ctx.channel_id)
|
||||||
if different_channel:
|
if different_channel:
|
||||||
channel_id = int(different_channel.id)
|
channel_id = int(different_channel.id)
|
||||||
|
|
||||||
dm_message = ""
|
dm_message: str = ""
|
||||||
where_and_when = "You should never see this message. Please report this to the bot owner if you do. :-)"
|
where_and_when = "You should never see this message. Please report this to the bot owner if you do. :-)"
|
||||||
should_send_channel_reminder = True
|
should_send_channel_reminder = True
|
||||||
try:
|
try:
|
||||||
if send_dm_to_user:
|
if send_dm_to_user:
|
||||||
dm_reminder = scheduler.add_job(
|
dm_reminder: Job = scheduler.add_job(
|
||||||
send_to_user,
|
send_to_user,
|
||||||
run_date=run_date,
|
run_date=run_date,
|
||||||
kwargs={
|
kwargs={
|
||||||
@ -259,11 +324,15 @@ async def command_add(
|
|||||||
if not both_dm_and_channel:
|
if not both_dm_and_channel:
|
||||||
# If we should send the message to the channel too instead of just a DM.
|
# If we should send the message to the channel too instead of just a DM.
|
||||||
should_send_channel_reminder = False
|
should_send_channel_reminder = False
|
||||||
where_and_when = (f"I will send a DM to {send_dm_to_user.username} at:\n"
|
where_and_when: str = (
|
||||||
f"**{run_date}** (in {calculate(dm_reminder)})\n")
|
f"I will send a DM to {send_dm_to_user.username} at:\n"
|
||||||
|
f"**{run_date}** (in {calculate(dm_reminder)})\n"
|
||||||
|
)
|
||||||
|
if ctx.member is None:
|
||||||
|
return await ctx.send("Something went wrong when grabbing the member, are you in a guild?", ephemeral=True)
|
||||||
|
|
||||||
if should_send_channel_reminder:
|
if should_send_channel_reminder:
|
||||||
reminder = scheduler.add_job(
|
reminder: Job = scheduler.add_job(
|
||||||
send_to_discord,
|
send_to_discord,
|
||||||
run_date=run_date,
|
run_date=run_date,
|
||||||
kwargs={
|
kwargs={
|
||||||
@ -272,23 +341,20 @@ async def command_add(
|
|||||||
"author_id": ctx.member.id,
|
"author_id": ctx.member.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
where_and_when = (f"I will notify you in <#{channel_id}> {dm_message}at:\n"
|
where_and_when = (
|
||||||
f"**{run_date}** (in {calculate(reminder)})\n")
|
f"I will notify you in <#{channel_id}> {dm_message}at:\n**{run_date}** (in {calculate(reminder)})\n"
|
||||||
|
)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
await ctx.send(str(e), ephemeral=True)
|
await ctx.send(str(e), ephemeral=True)
|
||||||
return
|
return None
|
||||||
|
|
||||||
message = (
|
message: str = f"Hello {ctx.member.name}, {where_and_when}With the message:\n**{message_reason}**."
|
||||||
f"Hello {ctx.member.name}, "
|
|
||||||
f"{where_and_when}"
|
|
||||||
f"With the message:\n"
|
|
||||||
f"**{message_reason}**."
|
|
||||||
)
|
|
||||||
await ctx.send(message)
|
await ctx.send(message)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def send_to_user(user_id: int, guild_id: int, message: str):
|
async def send_to_user(user_id: int, guild_id: int, message: str) -> None:
|
||||||
"""Send a message to a user via DM.
|
"""Send a message to a user via DM.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -296,7 +362,13 @@ async def send_to_user(user_id: int, guild_id: int, message: str):
|
|||||||
guild_id: The guild ID to get the user from.
|
guild_id: The guild ID to get the user from.
|
||||||
message: The message to send.
|
message: The message to send.
|
||||||
"""
|
"""
|
||||||
member = await interactions.get(bot, interactions.Member, parent_id=guild_id, object_id=user_id, force="http")
|
member: Member = await interactions.get(
|
||||||
|
bot,
|
||||||
|
interactions.Member,
|
||||||
|
parent_id=guild_id,
|
||||||
|
object_id=user_id,
|
||||||
|
force="http",
|
||||||
|
)
|
||||||
await member.send(message)
|
await member.send(message)
|
||||||
|
|
||||||
|
|
||||||
@ -305,41 +377,121 @@ async def send_to_user(user_id: int, guild_id: int, message: str):
|
|||||||
name="cron",
|
name="cron",
|
||||||
description="Triggers when current time matches all specified time constraints, similarly to the UNIX cron.",
|
description="Triggers when current time matches all specified time constraints, similarly to the UNIX cron.",
|
||||||
)
|
)
|
||||||
@interactions.option(name="message_reason", description="The message I'm going to send you.", type=OptionType.STRING, required=True)
|
@interactions.option(
|
||||||
@interactions.option(name="year", description="4-digit year. (Example: 2042)", type=OptionType.STRING, required=False)
|
name="message_reason",
|
||||||
@interactions.option(name="month", description="Month. (1-12)", type=OptionType.STRING, required=False)
|
description="The message I'm going to send you.",
|
||||||
@interactions.option(name="day", description="Day of month (1-31)", type=OptionType.STRING, required=False)
|
type=OptionType.STRING,
|
||||||
@interactions.option(name="week", description="ISO week (1-53)", type=OptionType.STRING, required=False)
|
required=True,
|
||||||
@interactions.option(name="day_of_week", description="Number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun).", type=OptionType.STRING, required=False) # noqa
|
)
|
||||||
@interactions.option(name="hour", description="Hour (0-23)", type=OptionType.STRING, required=False)
|
@interactions.option(
|
||||||
@interactions.option(name="minute", description="Minute (0-59)", type=OptionType.STRING, required=False)
|
name="year",
|
||||||
@interactions.option(name="second", description="Second (0-59)", type=OptionType.STRING, required=False)
|
description="4-digit year. (Example: 2042)",
|
||||||
@interactions.option(name="start_date", description="Earliest possible time to trigger on, in the ISO 8601 format. (Example: 2010-10-10 09:30:00)", type=OptionType.STRING, required=False) # noqa
|
type=OptionType.STRING,
|
||||||
@interactions.option(name="end_date", description="Latest possible time to trigger on, in the ISO 8601 format. (Example: 2010-10-10 09:30:00)", type=OptionType.STRING, required=False) # noqa
|
required=False,
|
||||||
@interactions.option(name="timezone", description="Time zone to use for the date/time calculations (defaults to scheduler timezone)", type=OptionType.STRING, required=False) # noqa
|
)
|
||||||
@interactions.option(name="jitter", description="Delay the job execution by x seconds at most. Adds a random component to the execution time.", type=OptionType.INTEGER, required=False) # noqa
|
@interactions.option(
|
||||||
@interactions.option(name="different_channel", description="Send the messages to a different channel.", type=OptionType.CHANNEL, required=False) # noqa
|
name="month",
|
||||||
@interactions.option(name="send_dm_to_user", description="Send message to a user via DM instead of a channel. Set both_dm_and_channel to send both.", type=OptionType.USER, required=False) # noqa
|
description="Month. (1-12)",
|
||||||
@interactions.option(name="both_dm_and_channel", description="Send both DM and message to the channel, needs send_dm_to_user to be set if you want both.", type=OptionType.BOOLEAN, required=False) # noqa
|
type=OptionType.STRING,
|
||||||
async def remind_cron(
|
required=False,
|
||||||
ctx: interactions.CommandContext,
|
)
|
||||||
message_reason: str,
|
@interactions.option(
|
||||||
year: int | None = None,
|
name="day",
|
||||||
month: int | None = None,
|
description="Day of month (1-31)",
|
||||||
day: int | None = None,
|
type=OptionType.STRING,
|
||||||
week: int | None = None,
|
required=False,
|
||||||
day_of_week: str | None = None,
|
)
|
||||||
hour: int | None = None,
|
@interactions.option(
|
||||||
minute: int | None = None,
|
name="week",
|
||||||
second: int | None = None,
|
description="ISO week (1-53)",
|
||||||
start_date: str | None = None,
|
type=OptionType.STRING,
|
||||||
end_date: str | None = None,
|
required=False,
|
||||||
timezone: str | None = None,
|
)
|
||||||
jitter: int | None = None,
|
@interactions.option(
|
||||||
different_channel: interactions.Channel | None = None,
|
name="day_of_week",
|
||||||
send_dm_to_user: interactions.User | None = None,
|
description="Number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun).",
|
||||||
both_dm_and_channel: bool | None = None,
|
type=OptionType.STRING,
|
||||||
):
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="hour",
|
||||||
|
description="Hour (0-23)",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="minute",
|
||||||
|
description="Minute (0-59)",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="second",
|
||||||
|
description="Second (0-59)",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="start_date",
|
||||||
|
description="Earliest possible time to trigger on, in the ISO 8601 format. (Example: 2010-10-10 09:30:00)",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="end_date",
|
||||||
|
description="Latest possible time to trigger on, in the ISO 8601 format. (Example: 2010-10-10 09:30:00)",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="timezone",
|
||||||
|
description="Time zone to use for the date/time calculations (defaults to scheduler timezone)",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="jitter",
|
||||||
|
description="Delay the job execution by x seconds at most. Adds a random component to the execution time.",
|
||||||
|
type=OptionType.INTEGER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="different_channel",
|
||||||
|
description="Send the messages to a different channel.",
|
||||||
|
type=OptionType.CHANNEL,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="send_dm_to_user",
|
||||||
|
description="Send message to a user via DM instead of a channel. Set both_dm_and_channel to send both.",
|
||||||
|
type=OptionType.USER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="both_dm_and_channel",
|
||||||
|
description="Send both DM and message to the channel, needs send_dm_to_user to be set if you want both.",
|
||||||
|
type=OptionType.BOOLEAN,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
async def remind_cron( # noqa: PLR0913
|
||||||
|
ctx: interactions.CommandContext,
|
||||||
|
message_reason: str,
|
||||||
|
year: int | None = None,
|
||||||
|
month: int | None = None,
|
||||||
|
day: int | None = None,
|
||||||
|
week: int | None = None,
|
||||||
|
day_of_week: str | None = None,
|
||||||
|
hour: int | None = None,
|
||||||
|
minute: int | None = None,
|
||||||
|
second: int | None = None,
|
||||||
|
start_date: str | None = None,
|
||||||
|
end_date: str | None = None,
|
||||||
|
timezone: str | None = None,
|
||||||
|
jitter: int | None = None,
|
||||||
|
different_channel: interactions.Channel | None = None,
|
||||||
|
send_dm_to_user: interactions.User | None = None,
|
||||||
|
both_dm_and_channel: bool | None = None,
|
||||||
|
) -> None:
|
||||||
"""Create new cron job. Works like UNIX cron.
|
"""Create new cron job. Works like UNIX cron.
|
||||||
|
|
||||||
https://en.wikipedia.org/wiki/Cron
|
https://en.wikipedia.org/wiki/Cron
|
||||||
@ -369,12 +521,12 @@ async def remind_cron(
|
|||||||
if different_channel:
|
if different_channel:
|
||||||
channel_id = int(different_channel.id)
|
channel_id = int(different_channel.id)
|
||||||
|
|
||||||
dm_message = ""
|
dm_message: str = ""
|
||||||
where_and_when = "You should never see this message. Please report this to the bot owner if you do. :-)"
|
where_and_when = "You should never see this message. Please report this to the bot owner if you do. :-)"
|
||||||
should_send_channel_reminder = True
|
should_send_channel_reminder = True
|
||||||
try:
|
try:
|
||||||
if send_dm_to_user:
|
if send_dm_to_user:
|
||||||
dm_reminder = scheduler.add_job(
|
dm_reminder: Job = scheduler.add_job(
|
||||||
send_to_user,
|
send_to_user,
|
||||||
"cron",
|
"cron",
|
||||||
year=year,
|
year=year,
|
||||||
@ -399,11 +551,15 @@ async def remind_cron(
|
|||||||
if not both_dm_and_channel:
|
if not both_dm_and_channel:
|
||||||
# If we should send the message to the channel too instead of just a DM.
|
# If we should send the message to the channel too instead of just a DM.
|
||||||
should_send_channel_reminder = False
|
should_send_channel_reminder = False
|
||||||
where_and_when = (f"I will send a DM to {send_dm_to_user.username} at:\n"
|
where_and_when: str = (
|
||||||
f"First run in {calculate(dm_reminder)} with the message:\n")
|
f"I will send a DM to {send_dm_to_user.username} at:\n"
|
||||||
|
f"First run in {calculate(dm_reminder)} with the message:\n"
|
||||||
|
)
|
||||||
|
if ctx.member is None:
|
||||||
|
await ctx.send("Failed to get member from context. Are you sure you're in a server?", ephemeral=True)
|
||||||
|
return
|
||||||
if should_send_channel_reminder:
|
if should_send_channel_reminder:
|
||||||
job = scheduler.add_job(
|
job: Job = scheduler.add_job(
|
||||||
send_to_discord,
|
send_to_discord,
|
||||||
"cron",
|
"cron",
|
||||||
year=year,
|
year=year,
|
||||||
@ -424,52 +580,118 @@ async def remind_cron(
|
|||||||
"author_id": ctx.member.id,
|
"author_id": ctx.member.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
where_and_when = (f" I will send messages to <#{channel_id}>{dm_message}.\n"
|
where_and_when = (
|
||||||
f"First run in {calculate(job)} with the message:\n")
|
f" I will send messages to <#{channel_id}>{dm_message}.\n"
|
||||||
|
f"First run in {calculate(job)} with the message:\n"
|
||||||
|
)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
await ctx.send(str(e), ephemeral=True)
|
await ctx.send(str(e), ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO: Add what arguments we used in the job to the message
|
# TODO: Add what arguments we used in the job to the message
|
||||||
message = (
|
message: str = f"Hello {ctx.member.name}, {where_and_when} **{message_reason}**."
|
||||||
f"Hello {ctx.member.name}, "
|
|
||||||
f"{where_and_when}"
|
|
||||||
f"**{message_reason}**."
|
|
||||||
)
|
|
||||||
await ctx.send(message)
|
await ctx.send(message)
|
||||||
|
|
||||||
|
|
||||||
@base_command.subcommand(name="interval", description="Schedules messages to be run periodically, on selected intervals.")
|
@base_command.subcommand(
|
||||||
@interactions.option(name="message_reason", description="The message I'm going to send you.", type=OptionType.STRING, required=True)
|
name="interval",
|
||||||
@interactions.option(name="weeks", description="Number of weeks to wait", type=OptionType.INTEGER, required=False)
|
description="Schedules messages to be run periodically, on selected intervals.",
|
||||||
@interactions.option(name="days", description="Number of days to wait", type=OptionType.INTEGER, required=False)
|
)
|
||||||
@interactions.option(name="hours", description="Number of hours to wait", type=OptionType.INTEGER, required=False)
|
@interactions.option(
|
||||||
@interactions.option(name="minutes", description="Number of minutes to wait", type=OptionType.INTEGER, required=False)
|
name="message_reason",
|
||||||
@interactions.option(name="seconds", description="Number of seconds to wait", type=OptionType.INTEGER, required=False)
|
description="The message I'm going to send you.",
|
||||||
@interactions.option(name="start_date", description="When to start, in the ISO 8601 format. (Example: 2010-10-10 09:30:00)", type=OptionType.STRING, required=False) # noqa
|
type=OptionType.STRING,
|
||||||
@interactions.option(name="end_date", description="When to stop, in the ISO 8601 format. (Example: 2014-06-15 11:00:00)", type=OptionType.STRING, required=False) # noqa
|
required=True,
|
||||||
@interactions.option(name="timezone", description="Time zone to use for the date/time calculations", type=OptionType.STRING, required=False)
|
)
|
||||||
@interactions.option(name="jitter", description="Delay the job execution by x seconds at most. Adds a random component to the execution time.", type=OptionType.INTEGER, required=False) # noqa
|
@interactions.option(
|
||||||
@interactions.option(name="different_channel", description="Send the messages to a different channel.", type=OptionType.CHANNEL, required=False)
|
name="weeks",
|
||||||
@interactions.option(name="send_dm_to_user", description="Send message to a user via DM instead of a channel. Set both_dm_and_channel to send both.", type=OptionType.USER, required=False) # noqa
|
description="Number of weeks to wait",
|
||||||
@interactions.option(name="both_dm_and_channel", description="Send both DM and message to the channel, needs send_dm_to_user to be set if you want both.", type=OptionType.BOOLEAN, required=False) # noqa
|
type=OptionType.INTEGER,
|
||||||
async def remind_interval(
|
required=False,
|
||||||
ctx: interactions.CommandContext,
|
)
|
||||||
message_reason: str,
|
@interactions.option(
|
||||||
weeks: int = 0,
|
name="days",
|
||||||
days: int = 0,
|
description="Number of days to wait",
|
||||||
hours: int = 0,
|
type=OptionType.INTEGER,
|
||||||
minutes: int = 0,
|
required=False,
|
||||||
seconds: int = 0,
|
)
|
||||||
start_date: str | None = None,
|
@interactions.option(
|
||||||
end_date: str | None = None,
|
name="hours",
|
||||||
timezone: str | None = None,
|
description="Number of hours to wait",
|
||||||
jitter: int | None = None,
|
type=OptionType.INTEGER,
|
||||||
different_channel: interactions.Channel | None = None,
|
required=False,
|
||||||
send_dm_to_user: interactions.User | None = None,
|
)
|
||||||
both_dm_and_channel: bool | None = None,
|
@interactions.option(
|
||||||
):
|
name="minutes",
|
||||||
|
description="Number of minutes to wait",
|
||||||
|
type=OptionType.INTEGER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="seconds",
|
||||||
|
description="Number of seconds to wait",
|
||||||
|
type=OptionType.INTEGER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="start_date",
|
||||||
|
description="When to start, in the ISO 8601 format. (Example: 2010-10-10 09:30:00)",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="end_date",
|
||||||
|
description="When to stop, in the ISO 8601 format. (Example: 2014-06-15 11:00:00)",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="timezone",
|
||||||
|
description="Time zone to use for the date/time calculations",
|
||||||
|
type=OptionType.STRING,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="jitter",
|
||||||
|
description="Delay the job execution by x seconds at most. Adds a random component to the execution time.",
|
||||||
|
type=OptionType.INTEGER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="different_channel",
|
||||||
|
description="Send the messages to a different channel.",
|
||||||
|
type=OptionType.CHANNEL,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="send_dm_to_user",
|
||||||
|
description="Send message to a user via DM instead of a channel. Set both_dm_and_channel to send both.",
|
||||||
|
type=OptionType.USER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@interactions.option(
|
||||||
|
name="both_dm_and_channel",
|
||||||
|
description="Send both DM and message to the channel, needs send_dm_to_user to be set if you want both.",
|
||||||
|
type=OptionType.BOOLEAN,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
async def remind_interval( # noqa: PLR0913
|
||||||
|
ctx: interactions.CommandContext,
|
||||||
|
message_reason: str,
|
||||||
|
weeks: int = 0,
|
||||||
|
days: int = 0,
|
||||||
|
hours: int = 0,
|
||||||
|
minutes: int = 0,
|
||||||
|
seconds: int = 0,
|
||||||
|
start_date: str | None = None,
|
||||||
|
end_date: str | None = None,
|
||||||
|
timezone: str | None = None,
|
||||||
|
jitter: int | None = None,
|
||||||
|
different_channel: interactions.Channel | None = None,
|
||||||
|
send_dm_to_user: interactions.User | None = None,
|
||||||
|
both_dm_and_channel: bool | None = None,
|
||||||
|
) -> None:
|
||||||
"""Create a new reminder that triggers based on an interval.
|
"""Create a new reminder that triggers based on an interval.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -493,12 +715,12 @@ async def remind_interval(
|
|||||||
if different_channel:
|
if different_channel:
|
||||||
channel_id = int(different_channel.id)
|
channel_id = int(different_channel.id)
|
||||||
|
|
||||||
dm_message = ""
|
dm_message: str = ""
|
||||||
where_and_when = "You should never see this message. Please report this to the bot owner if you do. :-)"
|
where_and_when = "You should never see this message. Please report this to the bot owner if you do. :-)"
|
||||||
should_send_channel_reminder = True
|
should_send_channel_reminder = True
|
||||||
try:
|
try:
|
||||||
if send_dm_to_user:
|
if send_dm_to_user:
|
||||||
dm_reminder = scheduler.add_job(
|
dm_reminder: Job = scheduler.add_job(
|
||||||
send_to_user,
|
send_to_user,
|
||||||
"interval",
|
"interval",
|
||||||
weeks=weeks,
|
weeks=weeks,
|
||||||
@ -520,10 +742,16 @@ async def remind_interval(
|
|||||||
if not both_dm_and_channel:
|
if not both_dm_and_channel:
|
||||||
# If we should send the message to the channel too instead of just a DM.
|
# If we should send the message to the channel too instead of just a DM.
|
||||||
should_send_channel_reminder = False
|
should_send_channel_reminder = False
|
||||||
where_and_when = (f"I will send a DM to {send_dm_to_user.username} at:\n"
|
where_and_when: str = (
|
||||||
f"First run in {calculate(dm_reminder)} with the message:\n")
|
f"I will send a DM to {send_dm_to_user.username} at:\n"
|
||||||
|
f"First run in {calculate(dm_reminder)} with the message:\n"
|
||||||
|
)
|
||||||
|
if ctx.member is None:
|
||||||
|
await ctx.send("Failed to get the member who sent the command.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if should_send_channel_reminder:
|
if should_send_channel_reminder:
|
||||||
job = scheduler.add_job(
|
job: Job = scheduler.add_job(
|
||||||
send_to_discord,
|
send_to_discord,
|
||||||
"interval",
|
"interval",
|
||||||
weeks=weeks,
|
weeks=weeks,
|
||||||
@ -541,37 +769,36 @@ async def remind_interval(
|
|||||||
"author_id": ctx.member.id,
|
"author_id": ctx.member.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
where_and_when = (f" I will send messages to <#{channel_id}>{dm_message}.\n"
|
where_and_when = (
|
||||||
f"First run in {calculate(job)} with the message:\n")
|
f" I will send messages to <#{channel_id}>{dm_message}.\n"
|
||||||
|
f"First run in {calculate(job)} with the message:"
|
||||||
|
)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
await ctx.send(str(e), ephemeral=True)
|
await ctx.send(str(e), ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO: Add what arguments we used in the job to the message
|
# TODO: Add what arguments we used in the job to the message
|
||||||
message = (
|
message: str = f"Hello {ctx.member.name}\n{where_and_when}\n**{message_reason}**."
|
||||||
f"Hello {ctx.member.name}\n"
|
|
||||||
f"{where_and_when}"
|
|
||||||
f"**{message_reason}**."
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(message)
|
await ctx.send(message)
|
||||||
|
|
||||||
|
|
||||||
def my_listener(event):
|
def my_listener(event: JobExecutionEvent) -> None:
|
||||||
"""This gets called when something in APScheduler happens."""
|
"""This gets called when something in APScheduler happens."""
|
||||||
if event.code == events.EVENT_JOB_MISSED:
|
if event.code == events.EVENT_JOB_MISSED:
|
||||||
# TODO: Is it possible to get the message?
|
# TODO: Is it possible to get the message?
|
||||||
scheduled_time = event.scheduled_run_time.strftime("%Y-%m-%d %H:%M:%S")
|
scheduled_time: str = event.scheduled_run_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
msg = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}"
|
msg: str = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}"
|
||||||
send_webhook(message=msg)
|
send_webhook(message=msg)
|
||||||
|
|
||||||
if event.exception:
|
if event.exception:
|
||||||
send_webhook(f"discord-reminder-bot failed to send message to Discord\n"
|
send_webhook(
|
||||||
f"{event}")
|
f"discord-reminder-bot failed to send message to Discord\n{event}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def send_to_discord(channel_id: int, message: str, author_id: int):
|
async def send_to_discord(channel_id: int, message: str, author_id: int) -> None:
|
||||||
"""Send a message to Discord.
|
"""Send a message to Discord.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -579,26 +806,23 @@ async def send_to_discord(channel_id: int, message: str, author_id: int):
|
|||||||
message: The message.
|
message: The message.
|
||||||
author_id: User we should ping.
|
author_id: User we should ping.
|
||||||
"""
|
"""
|
||||||
|
channel: Channel = await interactions.get(
|
||||||
channel = await interactions.get(
|
|
||||||
bot,
|
bot,
|
||||||
interactions.Channel,
|
interactions.Channel,
|
||||||
object_id=int(channel_id),
|
object_id=channel_id,
|
||||||
force=interactions.Force.HTTP,
|
force=interactions.Force.HTTP,
|
||||||
)
|
)
|
||||||
|
|
||||||
await channel.send(f"<@{author_id}>\n{message}")
|
await channel.send(f"<@{author_id}>\n{message}")
|
||||||
|
|
||||||
|
|
||||||
def start():
|
def start() -> None:
|
||||||
"""Start scheduler and log in to Discord."""
|
"""Start scheduler and log in to Discord."""
|
||||||
# TODO: Add how many reminders are scheduled.
|
# TODO: Add how many reminders are scheduled.
|
||||||
# TODO: Make backup of jobs.sqlite before running the bot.
|
# TODO: Make backup of jobs.sqlite before running the bot.
|
||||||
logging.basicConfig(level=logging.getLevelName(log_level))
|
logging.basicConfig(level=logging.getLevelName(log_level))
|
||||||
logging.info(
|
logging.info(
|
||||||
f"\nsqlite_location = {sqlite_location}\n"
|
"\nsqlite_location = %s\nconfig_timezone = %s\nlog_level = %s" % (sqlite_location, config_timezone, log_level),
|
||||||
f"config_timezone = {config_timezone}\n"
|
|
||||||
f"log_level = {log_level}"
|
|
||||||
)
|
)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
scheduler.add_listener(my_listener, EVENT_JOB_MISSED | EVENT_JOB_ERROR)
|
scheduler.add_listener(my_listener, EVENT_JOB_MISSED | EVENT_JOB_ERROR)
|
||||||
|
@ -9,8 +9,7 @@ from discord_reminder_bot.settings import config_timezone
|
|||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class ParsedTime:
|
class ParsedTime:
|
||||||
"""
|
"""This is used when parsing a time or date from a string.
|
||||||
This is used when parsing a time or date from a string.
|
|
||||||
|
|
||||||
We use this when adding a job with /reminder add.
|
We use this when adding a job with /reminder add.
|
||||||
|
|
||||||
@ -20,10 +19,11 @@ class ParsedTime:
|
|||||||
err_msg: The error message.
|
err_msg: The error message.
|
||||||
parsed_time: The parsed time we got from the string.
|
parsed_time: The parsed time we got from the string.
|
||||||
"""
|
"""
|
||||||
date_to_parse: str = None
|
|
||||||
|
date_to_parse: str | None = None
|
||||||
err: bool = False
|
err: bool = False
|
||||||
err_msg: str = ""
|
err_msg: str = ""
|
||||||
parsed_time: datetime = None
|
parsed_time: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
def parse_time(date_to_parse: str, timezone: str = config_timezone) -> ParsedTime:
|
def parse_time(date_to_parse: str, timezone: str = config_timezone) -> ParsedTime:
|
||||||
@ -37,7 +37,7 @@ def parse_time(date_to_parse: str, timezone: str = config_timezone) -> ParsedTim
|
|||||||
ParsedTime
|
ParsedTime
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
parsed_date = dateparser.parse(
|
parsed_date: datetime | None = dateparser.parse(
|
||||||
f"{date_to_parse}",
|
f"{date_to_parse}",
|
||||||
settings={
|
settings={
|
||||||
"PREFER_DATES_FROM": "future",
|
"PREFER_DATES_FROM": "future",
|
||||||
@ -46,12 +46,25 @@ def parse_time(date_to_parse: str, timezone: str = config_timezone) -> ParsedTim
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except SettingValidationError as e:
|
except SettingValidationError as e:
|
||||||
return ParsedTime(err=True, err_msg=f"Timezone is possible wrong?: {e}", date_to_parse=date_to_parse)
|
return ParsedTime(
|
||||||
|
err=True,
|
||||||
|
err_msg=f"Timezone is possible wrong?: {e}",
|
||||||
|
date_to_parse=date_to_parse,
|
||||||
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return ParsedTime(err=True, err_msg=f"Failed to parse date. Unknown language: {e}", date_to_parse=date_to_parse)
|
return ParsedTime(
|
||||||
|
err=True,
|
||||||
|
err_msg=f"Failed to parse date. Unknown language: {e}",
|
||||||
|
date_to_parse=date_to_parse,
|
||||||
|
)
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
return ParsedTime(err=True, err_msg=f"{e}", date_to_parse=date_to_parse)
|
return ParsedTime(err=True, err_msg=f"{e}", date_to_parse=date_to_parse)
|
||||||
if not parsed_date:
|
return (
|
||||||
return ParsedTime(err=True, err_msg=f"Could not parse the date.", date_to_parse=date_to_parse)
|
ParsedTime(parsed_time=parsed_date, date_to_parse=date_to_parse)
|
||||||
|
if parsed_date
|
||||||
return ParsedTime(parsed_time=parsed_date, date_to_parse=date_to_parse)
|
else ParsedTime(
|
||||||
|
err=True,
|
||||||
|
err_msg="Could not parse the date.",
|
||||||
|
date_to_parse=date_to_parse,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -6,18 +6,19 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv(verbose=True)
|
load_dotenv(verbose=True)
|
||||||
sqlite_location = os.getenv("SQLITE_LOCATION", default="/jobs.sqlite")
|
sqlite_location: str = os.getenv("SQLITE_LOCATION", default="/jobs.sqlite")
|
||||||
config_timezone = os.getenv("TIMEZONE", default="UTC")
|
config_timezone: str = os.getenv("TIMEZONE", default="UTC")
|
||||||
bot_token = os.getenv("BOT_TOKEN", default="")
|
bot_token: str = os.getenv("BOT_TOKEN", default="")
|
||||||
log_level = os.getenv("LOG_LEVEL", default="INFO")
|
log_level: str = os.getenv("LOG_LEVEL", default="INFO")
|
||||||
webhook_url = os.getenv("WEBHOOK_URL", default="")
|
webhook_url: str = os.getenv("WEBHOOK_URL", default="")
|
||||||
|
|
||||||
if not bot_token:
|
if not bot_token:
|
||||||
raise ValueError("Missing bot token")
|
err_msg = "Missing bot token"
|
||||||
|
raise ValueError(err_msg)
|
||||||
|
|
||||||
# Advanced Python Scheduler
|
# Advanced Python Scheduler
|
||||||
jobstores = {"default": SQLAlchemyJobStore(url=f"sqlite://{sqlite_location}")}
|
jobstores: dict[str, SQLAlchemyJobStore] = {"default": SQLAlchemyJobStore(url=f"sqlite://{sqlite_location}")}
|
||||||
job_defaults = {"coalesce": True}
|
job_defaults: dict[str, bool] = {"coalesce": True}
|
||||||
scheduler = AsyncIOScheduler(
|
scheduler = AsyncIOScheduler(
|
||||||
jobstores=jobstores,
|
jobstores=jobstores,
|
||||||
timezone=pytz.timezone(config_timezone),
|
timezone=pytz.timezone(config_timezone),
|
||||||
|
8
poetry.lock
generated
8
poetry.lock
generated
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
|
# This file is automatically @generated by Poetry and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
@ -318,9 +318,9 @@ fasttext = ["fasttext"]
|
|||||||
langdetect = ["langdetect"]
|
langdetect = ["langdetect"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dinteractions_Paginator"
|
name = "dinteractions-Paginator"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
description = ""
|
description = "Official interactions.py paginator"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
@ -951,7 +951,7 @@ files = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""}
|
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
|
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
|
||||||
|
@ -39,4 +39,81 @@ requires = ["poetry-core>=1.0.0"]
|
|||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 120
|
||||||
|
select = [
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"B",
|
||||||
|
"W",
|
||||||
|
"C90",
|
||||||
|
"I",
|
||||||
|
"N",
|
||||||
|
"D",
|
||||||
|
"UP",
|
||||||
|
"YTT",
|
||||||
|
"ANN",
|
||||||
|
"S",
|
||||||
|
"BLE",
|
||||||
|
"FBT",
|
||||||
|
"A",
|
||||||
|
"COM",
|
||||||
|
"C4",
|
||||||
|
"DTZ",
|
||||||
|
"EM",
|
||||||
|
"EXE",
|
||||||
|
"ISC",
|
||||||
|
"ICN",
|
||||||
|
"G",
|
||||||
|
"INP",
|
||||||
|
"PIE",
|
||||||
|
"T20",
|
||||||
|
"PYI",
|
||||||
|
"PT",
|
||||||
|
"Q",
|
||||||
|
"RSE",
|
||||||
|
"RET",
|
||||||
|
"SLF",
|
||||||
|
"SIM",
|
||||||
|
"TID",
|
||||||
|
"TCH",
|
||||||
|
"ARG",
|
||||||
|
"PTH",
|
||||||
|
"ERA",
|
||||||
|
"PGH",
|
||||||
|
"PL",
|
||||||
|
"PLC",
|
||||||
|
"PLE",
|
||||||
|
"PLR",
|
||||||
|
"PLW",
|
||||||
|
"TRY",
|
||||||
|
"RUF",
|
||||||
|
]
|
||||||
|
|
||||||
|
ignore = [
|
||||||
|
"D100", # pydocstyle - missing docstring in public module
|
||||||
|
"D101", # pydocstyle - missing docstring in public class
|
||||||
|
"D102", # pydocstyle - missing docstring in public method
|
||||||
|
"D103", # pydocstyle - missing docstring in public function
|
||||||
|
"D104", # pydocstyle - missing docstring in public package
|
||||||
|
"D105", # pydocstyle - missing docstring in magic method
|
||||||
|
"D106", # pydocstyle - missing docstring in public nested class
|
||||||
|
"D107", # pydocstyle - missing docstring in __init__
|
||||||
|
"G002", # Allow % in logging
|
||||||
|
"UP031", # Allow % in logging
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.per-file-ignores]
|
||||||
|
"tests/*" = ["S101"]
|
||||||
|
|
||||||
|
[tool.ruff.pydocstyle]
|
||||||
|
convention = "google"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-vvvvvv --exitfirst"
|
||||||
|
filterwarnings = ["ignore::DeprecationWarning:pkg_resources:121"]
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
"""Test discord-reminder-bot.
|
from datetime import datetime
|
||||||
|
|
||||||
Jobs are stored in memory.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import dateparser
|
import dateparser
|
||||||
import pytz
|
import pytz
|
||||||
|
from apscheduler.job import Job
|
||||||
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
@ -19,23 +17,25 @@ class TestCountdown:
|
|||||||
runs at 00:00.
|
runs at 00:00.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
jobstores = {"default": SQLAlchemyJobStore(url="sqlite:///:memory")}
|
jobstores: dict[str, SQLAlchemyJobStore] = {"default": SQLAlchemyJobStore(url="sqlite:///:memory")}
|
||||||
job_defaults = {"coalesce": True}
|
job_defaults: dict[str, bool] = {"coalesce": True}
|
||||||
scheduler = AsyncIOScheduler(
|
scheduler = AsyncIOScheduler(
|
||||||
jobstores=jobstores,
|
jobstores=jobstores,
|
||||||
timezone=pytz.timezone("Europe/Stockholm"),
|
timezone=pytz.timezone("Europe/Stockholm"),
|
||||||
job_defaults=job_defaults,
|
job_defaults=job_defaults,
|
||||||
)
|
)
|
||||||
|
|
||||||
parsed_date = dateparser.parse(
|
parsed_date: datetime | None = dateparser.parse(
|
||||||
"18 January 2040",
|
"18 January 2040",
|
||||||
settings={
|
settings={
|
||||||
"PREFER_DATES_FROM": "future",
|
"PREFER_DATES_FROM": "future",
|
||||||
"TO_TIMEZONE": "Europe/Stockholm",
|
"TO_TIMEZONE": "Europe/Stockholm",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
run_date = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
assert parsed_date
|
||||||
job = scheduler.add_job(
|
|
||||||
|
run_date: str = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
job: Job = scheduler.add_job(
|
||||||
send_to_discord,
|
send_to_discord,
|
||||||
run_date=run_date,
|
run_date=run_date,
|
||||||
kwargs={
|
kwargs={
|
||||||
@ -45,7 +45,7 @@ class TestCountdown:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
timezone_date = dateparser.parse(
|
timezone_date: datetime | None = dateparser.parse(
|
||||||
"00:00",
|
"00:00",
|
||||||
settings={
|
settings={
|
||||||
"PREFER_DATES_FROM": "future",
|
"PREFER_DATES_FROM": "future",
|
||||||
@ -53,8 +53,10 @@ class TestCountdown:
|
|||||||
"TO_TIMEZONE": "Europe/Stockholm",
|
"TO_TIMEZONE": "Europe/Stockholm",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
timezone_run_date = timezone_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
timezone_job = scheduler.add_job(
|
assert timezone_date
|
||||||
|
timezone_run_date: str = timezone_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
timezone_job: Job = scheduler.add_job(
|
||||||
send_to_discord,
|
send_to_discord,
|
||||||
run_date=timezone_run_date,
|
run_date=timezone_run_date,
|
||||||
kwargs={
|
kwargs={
|
||||||
@ -64,7 +66,7 @@ class TestCountdown:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
timezone_date2 = dateparser.parse(
|
timezone_date2: datetime | None = dateparser.parse(
|
||||||
"13:37",
|
"13:37",
|
||||||
settings={
|
settings={
|
||||||
"PREFER_DATES_FROM": "future",
|
"PREFER_DATES_FROM": "future",
|
||||||
@ -72,8 +74,10 @@ class TestCountdown:
|
|||||||
"TO_TIMEZONE": "Europe/Stockholm",
|
"TO_TIMEZONE": "Europe/Stockholm",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
timezone_run_date2 = timezone_date2.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
timezone_job2 = scheduler.add_job(
|
assert timezone_date2
|
||||||
|
timezone_run_date2: str = timezone_date2.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
timezone_job2: Job = scheduler.add_job(
|
||||||
send_to_discord,
|
send_to_discord,
|
||||||
run_date=timezone_run_date2,
|
run_date=timezone_run_date2,
|
||||||
kwargs={
|
kwargs={
|
||||||
@ -83,21 +87,22 @@ class TestCountdown:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# def test_countdown(self):
|
def test_if_timezones_are_working(self) -> None: # noqa: ANN101
|
||||||
# """Check if calc_countdown returns days, hours and minutes."""
|
"""Check if timezones are working.
|
||||||
# # FIXME: This will break when there is 0 seconds/hours/days left
|
|
||||||
# pattern = re.compile(r"\d* (day|days), \d* (hour|hours). \d* (minute|minutes)")
|
Args:
|
||||||
# countdown = calculate(self.job)
|
self: TestCountdown
|
||||||
# assert pattern.match(countdown)
|
"""
|
||||||
|
time_job: Job | None = self.scheduler.get_job(self.timezone_job.id)
|
||||||
|
assert time_job
|
||||||
|
|
||||||
def test_if_timezones_are_working(self):
|
|
||||||
"""Check if timezones are working."""
|
|
||||||
time_job = self.scheduler.get_job(self.timezone_job.id)
|
|
||||||
assert time_job.trigger.run_date.hour == 0
|
assert time_job.trigger.run_date.hour == 0
|
||||||
assert time_job.trigger.run_date.minute == 0
|
assert time_job.trigger.run_date.minute == 0
|
||||||
assert time_job.trigger.run_date.second == 0
|
assert time_job.trigger.run_date.second == 0
|
||||||
|
|
||||||
time_job2 = self.scheduler.get_job(self.timezone_job2.id)
|
time_job2: Job | None = self.scheduler.get_job(self.timezone_job2.id)
|
||||||
assert time_job2.trigger.run_date.hour == 13
|
assert time_job2
|
||||||
assert time_job2.trigger.run_date.minute == 37
|
|
||||||
|
assert time_job2.trigger.run_date.hour == 13 # noqa: PLR2004
|
||||||
|
assert time_job2.trigger.run_date.minute == 37 # noqa: PLR2004
|
||||||
assert time_job2.trigger.run_date.second == 0
|
assert time_job2.trigger.run_date.second == 0
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
from discord_reminder_bot import main
|
from discord_reminder_bot import main
|
||||||
|
|
||||||
|
|
||||||
def test_if_send_to_discord_is_in_main():
|
def test_if_send_to_discord_is_in_main() -> None:
|
||||||
"""
|
"""send_to_discords needs to be in main for this program to work."""
|
||||||
send_to_discords needs to be in main for this program to work.
|
|
||||||
"""
|
|
||||||
assert hasattr(main, "send_to_discord")
|
assert hasattr(main, "send_to_discord")
|
||||||
|
|
||||||
def test_if_send_to_user_is_in_main():
|
|
||||||
"""
|
def test_if_send_to_user_is_in_main() -> None:
|
||||||
send_to_user needs to be in main for this program to work.
|
"""send_to_user needs to be in main for this program to work."""
|
||||||
"""
|
assert hasattr(main, "send_to_user")
|
||||||
assert hasattr(main, "send_to_user")
|
|
||||||
|
Reference in New Issue
Block a user