diff --git a/README.md b/README.md
index 84f0328..cba3a07 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Type `/remind` in a Discord server where this bot exists to get a list of slash
## 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).
### 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.
- Copy the bot token and paste it into the `BOT_TOKEN` environment variable.
- Go to the OAuth2 page -> URL Generator
- - 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` and `applications.commands` scope.
+ - Select the bot permissions that you want the bot to have. Select `Administrator`. (TODO: Add a list of permissions
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 the latest version of needed software:
- - [Python](https://www.python.org/)
- - You should use the latest version.
- - You want to add Python to your PATH.
- - Windows: Find `App execution aliases` and disable python.exe and python3.exe
- - [Poetry](https://python-poetry.org/docs/master/#installation)
- - Windows: You have to add `%appdata%\Python\Scripts` to your PATH for Poetry to work.
+ - [Python](https://www.python.org/)
+ - You should use the latest version.
+ - You want to add Python to your PATH.
+ - Windows: Find `App execution aliases` and disable python.exe and python3.exe
+ - [Poetry](https://python-poetry.org/docs/master/#installation)
+ - Windows: You have to add `%appdata%\Python\Scripts` to your PATH for Poetry to work.
- Download project from GitHub with Git or download
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).
- - 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 11: Click View -> Show -> File name extensions.
+ - 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 11: Click View -> Show -> File name extensions.
- Open a terminal in the repository folder.
- - Windows 10: Shift + right-click in the folder and select `Open PowerShell window here`
- - Windows 11: Shift + right-click in the folder and Show more options
+ - Windows 10: Shift + right-click in the folder and select `Open PowerShell window here`
+ - Windows 11: Shift + right-click in the folder and Show more options
and `Open PowerShell window here`
- 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.
- - 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.
- Start the bot:
- - Type `poetry run bot` into the PowerShell window.
- - You can stop the bot with Ctrl + c.
+ - Type `poetry run bot` into the PowerShell window.
+ - You can stop the bot with Ctrl + c.
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)
- 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:
- - 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.
+ - 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 11: Click View -> Show -> File name extensions.
- Open a terminal in the extras folder.
- - Windows 10: Shift + right-click in the folder and select `Open PowerShell window here`
- - Windows 11: Shift + right-click in the folder and Show more options
+ - Windows 10: Shift + right-click in the folder and select `Open PowerShell window here`
+ - Windows 11: Shift + right-click in the folder and Show more options
and `Open PowerShell window here`
- Run the Docker Compose file:
- - `docker-compose up`
- - You can stop the bot with Ctrl + c.
- - If you want to run the bot in the background, you can run `docker-compose up -d`.
+ - `docker-compose up`
+ - You can stop the bot with Ctrl + c.
+ - If you want to run the bot in the background, you can run `docker-compose up -d`.
## Help
diff --git a/discord_reminder_bot/countdown.py b/discord_reminder_bot/countdown.py
index bfe96b2..a8949a7 100644
--- a/discord_reminder_bot/countdown.py
+++ b/discord_reminder_bot/countdown.py
@@ -1,7 +1,4 @@
-"""
-calculate(job) - Calculates how many days, hours and minutes till trigger.
-"""
-import datetime
+from datetime import datetime, timedelta
import pytz
from apscheduler.job import Job
@@ -11,8 +8,7 @@ from discord_reminder_bot.settings import config_timezone
def calculate(job: Job) -> str:
- """Get trigger time from a reminder and calculate how many days,
- hours and minutes till trigger.
+ """Get trigger time from a reminder and calculate how many days, hours and minutes till trigger.
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.
# If we use (in {calc_countdown(job)}) it will show (in )
- if type(job.trigger) is DateTrigger:
- trigger_time = job.trigger.run_date
- else:
- trigger_time = job.next_run_time
+ trigger_time: datetime | None = job.trigger.run_date if type(job.trigger) is DateTrigger else job.next_run_time
# Get_job() returns None when it can't find a job with that ID.
if trigger_time is None:
@@ -41,8 +34,7 @@ def calculate(job: Job) -> str:
def countdown(trigger_time: datetime) -> str:
- """
- Calculate days, hours and minutes to a date.
+ """Calculate days, hours and minutes to a date.
Args:
trigger_time: The date.
@@ -50,7 +42,7 @@ def countdown(trigger_time: datetime) -> str:
Returns:
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 = (
countdown_time.days,
@@ -60,7 +52,7 @@ def countdown(trigger_time: datetime) -> str:
# Return seconds if only seconds are left.
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 "")
# TODO: Explain this.
diff --git a/discord_reminder_bot/create_pages.py b/discord_reminder_bot/create_pages.py
index 90620d3..8a0d1d6 100644
--- a/discord_reminder_bot/create_pages.py
+++ b/discord_reminder_bot/create_pages.py
@@ -1,17 +1,23 @@
-"""This module creates the pages for the paginator."""
-from typing import List
+from typing import TYPE_CHECKING, Literal
import interactions
-from apscheduler.job import Job
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 discord_reminder_bot.countdown import calculate
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.
Args:
@@ -20,65 +26,69 @@ def create_pages(ctx: CommandContext) -> list[Page]:
Returns:
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:
- channel_id = job.kwargs.get("channel_id")
- guild_id = job.kwargs.get("guild_id")
+ channel_id: int = job.kwargs.get("channel_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
# Check if channel is in the Discord server, if not, skip it.
for channel in ctx.guild.channels:
if int(channel.id) == channel_id or ctx.guild_id == guild_id:
- if type(job.trigger) is DateTrigger:
- # Get trigger time for normal reminders
- trigger_time = job.trigger.run_date
- else:
- # Get trigger time for cron and interval jobs
- trigger_time = job.next_run_time
+ trigger_time: datetime | None = (
+ job.trigger.run_date if type(job.trigger) is DateTrigger else job.next_run_time
+ )
# Paused reminders returns None
if trigger_time is None:
- trigger_value = None
- trigger_text = "Paused"
+ trigger_value: str | None = None
+ trigger_text: str = "Paused"
else:
trigger_value = f'{trigger_time.strftime("%Y-%m-%d %H:%M")} (in {calculate(job)})'
trigger_text = trigger_value
- message = job.kwargs.get("message")
- message = f"{message[:1000]}..." if len(message) > 1010 else message
+ message: str = job.kwargs.get("message")
+ message = f"{message[:1000]}..." if len(message) > max_message_length else message
- edit_button = interactions.Button(
+ edit_button: Button = interactions.Button(
label="Edit",
style=interactions.ButtonStyle.PRIMARY,
custom_id="edit",
)
- pause_button = interactions.Button(
+ pause_button: Button = interactions.Button(
label="Pause",
style=interactions.ButtonStyle.PRIMARY,
custom_id="pause",
)
- unpause_button = interactions.Button(
+ unpause_button: Button = interactions.Button(
label="Unpause",
style=interactions.ButtonStyle.PRIMARY,
custom_id="unpause",
)
- remove_button = interactions.Button(
+ remove_button: Button = interactions.Button(
label="Remove",
style=interactions.ButtonStyle.DANGER,
custom_id="remove",
)
- embed = interactions.Embed(
+ embed: Embed = interactions.Embed(
title=f"{job.id}",
fields=[
interactions.EmbedField(
- name=f"**Channel:**",
+ name="**Channel:**",
value=f"#{channel.name}",
),
interactions.EmbedField(
- name=f"**Message:**",
+ name="**Message:**",
value=f"{message}",
),
],
@@ -86,16 +96,16 @@ def create_pages(ctx: CommandContext) -> list[Page]:
if trigger_value is not None:
embed.add_field(
- name=f"**Trigger:**",
+ name="**Trigger:**",
value=f"{trigger_text}",
)
else:
embed.add_field(
- name=f"**Trigger:**",
- value=f"_Paused_",
+ name="**Trigger:**",
+ value="_Paused_",
)
- components = [
+ components: list[Button] = [
edit_button,
remove_button,
]
@@ -103,39 +113,42 @@ def create_pages(ctx: CommandContext) -> list[Page]:
if type(job.trigger) is not DateTrigger:
# Get trigger time for cron and interval jobs
trigger_time = job.next_run_time
- if trigger_time is None:
- pause_or_unpause_button = unpause_button
- else:
- pause_or_unpause_button = pause_button
+ pause_or_unpause_button: Button = unpause_button if trigger_time is None else pause_button
components.insert(1, pause_or_unpause_button)
# 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(
Page(
embeds=embed,
title=title,
- components=ActionRow(components=components),
+ components=ActionRow(components=components), # type: ignore # noqa: PGH003
callback=callback,
position=RowPosition.BOTTOM,
- )
+ ),
)
return pages
-async def callback(self: Paginator, ctx: ComponentContext):
+async def callback(self: Paginator, ctx: ComponentContext) -> Message | None: # noqa: PLR0911
"""Callback for the paginator."""
- job_id = self.component_ctx.message.embeds[0].title
- job = scheduler.get_job(job_id)
+ if self.component_ctx is None:
+ 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:
return await ctx.send("Job not found.", ephemeral=True)
- channel_id = job.kwargs.get("channel_id")
- old_message = job.kwargs.get("message")
+ channel_id: int = job.kwargs.get("channel_id")
+ old_message: str = job.kwargs.get("message")
- components = [
+ components: list[TextInput] = [
interactions.TextInput(
style=interactions.TextStyleType.PARAGRAPH,
label="New message",
@@ -147,8 +160,8 @@ async def callback(self: Paginator, ctx: ComponentContext):
if type(job.trigger) is DateTrigger:
# Get trigger time for normal reminders
- trigger_time = job.trigger.run_date
- job_type = "normal"
+ trigger_time: datetime | None = job.trigger.run_date
+ job_type: str = "normal"
components.append(
interactions.TextInput(
style=interactions.TextStyleType.SHORT,
@@ -165,29 +178,31 @@ async def callback(self: Paginator, ctx: ComponentContext):
job_type = "cron/interval"
if ctx.custom_id == "edit":
- modal = interactions.Modal(
+ modal: Modal = interactions.Modal(
title=f"Edit {job_type} reminder.",
custom_id="edit_modal",
- components=components,
+ components=components, # type: ignore # noqa: PGH003
)
await ctx.popup(modal)
+ return None
- elif ctx.custom_id == "pause":
- # TODO: Add unpause button if user paused the wrong job
+ if ctx.custom_id == "pause":
scheduler.pause_job(job_id)
await ctx.send(f"Job {job_id} paused.")
+ return None
- elif ctx.custom_id == "unpause":
- # TODO: Add pause button if user unpauses the wrong job
+ if ctx.custom_id == "unpause":
scheduler.resume_job(job_id)
await ctx.send(f"Job {job_id} unpaused.")
+ return None
- elif ctx.custom_id == "remove":
- # TODO: Add recreate button if user removed the wrong job
+ if ctx.custom_id == "remove":
scheduler.remove_job(job_id)
await ctx.send(
f"Job {job_id} removed.\n"
f"**Message:** {old_message}\n"
f"**Channel:** {channel_id}\n"
- f"**Time:** {trigger_time}"
+ f"**Time:** {trigger_time}",
)
+ return None
+ return None
diff --git a/discord_reminder_bot/main.py b/discord_reminder_bot/main.py
index 7225126..e3a9e6f 100644
--- a/discord_reminder_bot/main.py
+++ b/discord_reminder_bot/main.py
@@ -1,71 +1,83 @@
import logging
-from typing import List
+from typing import TYPE_CHECKING
import interactions
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.triggers.date import DateTrigger
from discord_webhook import DiscordWebhook
-from interactions import CommandContext, Embed, OptionType, autodefer
-from interactions.ext.paginator import Paginator
+from interactions import Channel, Client, CommandContext, Embed, Member, Message, OptionType, autodefer
+from interactions.ext.paginator import Page, Paginator
+
from discord_reminder_bot.countdown import calculate
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 (
bot_token,
config_timezone,
log_level,
scheduler,
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."):
- """
- Send a webhook to Discord.
+def send_webhook(
+ url: str = webhook_url,
+ message: str = "discord-reminder-bot: Empty message.",
+) -> None:
+ """Send a webhook to Discord.
Args:
url: Our webhook url, defaults to the one from settings.
message: The message that will be sent to Discord.
"""
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
- webhook = DiscordWebhook(url=url, content=message, rate_limit_retry=True)
+ webhook: DiscordWebhook = DiscordWebhook(url=url, content=message, rate_limit_retry=True)
webhook.execute()
@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."""
- pass
@bot.modal("edit_modal")
-async def modal_response_edit(ctx: CommandContext, *response: str):
- """This is what gets triggerd when the user clicks the Edit button in /reminder list.
+async def modal_response_edit(ctx: CommandContext, *response: str) -> Message: # noqa: C901, PLR0912, PLR0911
+ """This is what gets triggered when the user clicks the Edit button in /reminder list.
Args:
ctx: Context of the slash command. Contains the guild, author and message and more.
+ response: The response from the modal.
Returns:
A Discord message with changes.
"""
- job_id = ctx.message.embeds[0].title
- old_date = None
- old_message = None
+ if not ctx.message:
+ return await ctx.send(
+ "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:
- job = scheduler.get_job(job_id)
+ job: Job | None = scheduler.get_job(job_id)
except JobLookupError as e:
return await ctx.send(
- f"Failed to get the job after the modal.\n"
- f"Job ID: {job_id}\n"
- f"Error: {e}",
+ f"Failed to get the job after the modal.\nJob ID: {job_id}\nError: {e}",
ephemeral=True,
)
@@ -76,50 +88,54 @@ async def modal_response_edit(ctx: CommandContext, *response: str):
return await ctx.send("No changes made.", ephemeral=True)
if type(job.trigger) is DateTrigger:
- new_message = response[0]
- new_date = response[1]
+ new_message: str | None = response[0]
+ new_date: str | None = response[1]
else:
new_message = response[0]
new_date = None
- message_embeds: List[Embed] = ctx.message.embeds
+ message_embeds: list[Embed] = ctx.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:
if field.name == "**Channel:**":
continue
- elif field.name == "**Message:**":
+ if field.name == "**Message:**":
old_message = field.value
- elif field.name == "**Trigger:**":
+ if field.name == "**Trigger:**":
old_date = field.value
else:
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"
- if old_date is not None:
- if new_date:
- # Parse the time/date we got from the command.
- parsed = parse_time(date_to_parse=new_date)
- if parsed.err:
- return await ctx.send(parsed.err_msg)
- parsed_date = parsed.parsed_time
- date_new = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
+ msg: str = f"Modified job {job_id}.\n"
+ if old_date is not None and new_date:
+ # Parse the time/date we got from the command.
+ parsed: ParsedTime = parse_time(date_to_parse=new_date)
+ if parsed.err:
+ return await ctx.send(parsed.err_msg)
+ parsed_date: datetime | None = parsed.parsed_time
- new_job = scheduler.reschedule_job(job.id, run_date=date_new)
- new_time = calculate(new_job)
+ if parsed_date is None:
+ return await ctx.send(f"Failed to parse the date. ({new_date})")
- # 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"
- f"**New date**: {date_new} (in {new_time})\n"
- )
+ date_new: str = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
+
+ new_job: Job = scheduler.reschedule_job(job.id, run_date=date_new)
+ new_time: str = calculate(new_job)
+
+ # 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:
- channel_id = job.kwargs.get("channel_id")
- job_author_id = job.kwargs.get("author_id")
+ channel_id: int = job.kwargs.get("channel_id")
+ job_author_id: int = job.kwargs.get("author_id")
try:
scheduler.modify_job(
job.id,
@@ -131,9 +147,7 @@ async def modal_response_edit(ctx: CommandContext, *response: str):
)
except JobLookupError as e:
return await ctx.send(
- f"Failed to modify the job.\n"
- f"Job ID: {job_id}\n"
- f"Error: {e}",
+ f"Failed to modify the job.\nJob ID: {job_id}\nError: {e}",
ephemeral=True,
)
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()
-@bot.command(name="parse", description="Parse the time from a string")
-@interactions.option(name="time_to_parse", description="The string you want to parse.", type=OptionType.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):
- """
- Find the date and 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(
+ 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:
ctx: Context of the slash command. Contains the guild, author and message and more.
time_to_parse: The string you want to parse.
optional_timezone: Optional time zone, for example Europe/Stockholm.
"""
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:
parsed = parse_time(date_to_parse=time_to_parse)
if parsed.err:
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.
- locale_time = parsed_date.strftime("%c")
- run_date = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
- 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")
+ if parsed_date is None:
+ return await ctx.send(f"Failed to parse the date. ({time_to_parse})")
+
+ # Locale`s appropriate date and time representation.
+ locale_time: str = parsed_date.strftime("%c")
+ run_date: str = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
+ 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()
-@base_command.subcommand(name="list", description="List, pause, unpause, and remove reminders.")
-async def list_command(ctx: interactions.CommandContext):
+@base_command.subcommand(
+ name="list",
+ description="List, pause, unpause, and remove reminders.",
+)
+async def list_command(ctx: interactions.CommandContext) -> Message | None:
"""List, pause, unpause, and remove reminders.
Args:
ctx: Context of the slash command. Contains the guild, author and message and more.
"""
-
- pages = create_pages(ctx)
+ pages: list[Page] = await create_pages(ctx)
if not pages:
return await ctx.send("No reminders found.", ephemeral=True)
@@ -187,7 +222,7 @@ async def list_command(ctx: interactions.CommandContext):
for page in pages:
return await ctx.send(
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,
)
@@ -202,23 +237,49 @@ async def list_command(ctx: interactions.CommandContext):
)
await paginator.run()
+ return None
@autodefer()
@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(name="message_date", description="The date to send the message.", type=OptionType.STRING, required=True)
-@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) # noqa
-@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
-async def command_add(
- 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,
-):
+@interactions.option(
+ name="message_reason",
+ description="The message I'm going to send you.",
+ type=OptionType.STRING,
+ required=True,
+)
+@interactions.option(
+ name="message_date",
+ description="The date to send the message.",
+ type=OptionType.STRING,
+ required=True,
+)
+@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.
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.
"""
# 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:
return await ctx.send(parsed.err_msg)
- parsed_date = parsed.parsed_time
- run_date = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
+ parsed_date: datetime | None = parsed.parsed_time
+
+ 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
channel_id = int(ctx.channel_id)
if different_channel:
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. :-)"
should_send_channel_reminder = True
try:
if send_dm_to_user:
- dm_reminder = scheduler.add_job(
+ dm_reminder: Job = scheduler.add_job(
send_to_user,
run_date=run_date,
kwargs={
@@ -259,11 +324,15 @@ async def command_add(
if not both_dm_and_channel:
# If we should send the message to the channel too instead of just a DM.
should_send_channel_reminder = False
- where_and_when = (f"I will send a DM to {send_dm_to_user.username} at:\n"
- f"**{run_date}** (in {calculate(dm_reminder)})\n")
+ where_and_when: str = (
+ 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:
- reminder = scheduler.add_job(
+ reminder: Job = scheduler.add_job(
send_to_discord,
run_date=run_date,
kwargs={
@@ -272,23 +341,20 @@ async def command_add(
"author_id": ctx.member.id,
},
)
- where_and_when = (f"I will notify you in <#{channel_id}> {dm_message}at:\n"
- f"**{run_date}** (in {calculate(reminder)})\n")
+ where_and_when = (
+ f"I will notify you in <#{channel_id}> {dm_message}at:\n**{run_date}** (in {calculate(reminder)})\n"
+ )
except ValueError as e:
await ctx.send(str(e), ephemeral=True)
- return
+ return None
- message = (
- f"Hello {ctx.member.name}, "
- f"{where_and_when}"
- f"With the message:\n"
- f"**{message_reason}**."
- )
+ message: str = f"Hello {ctx.member.name}, {where_and_when}With the message:\n**{message_reason}**."
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.
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.
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)
@@ -305,41 +377,121 @@ async def send_to_user(user_id: int, guild_id: int, message: str):
name="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(name="year", description="4-digit year. (Example: 2042)", type=OptionType.STRING, required=False)
-@interactions.option(name="month", description="Month. (1-12)", type=OptionType.STRING, required=False)
-@interactions.option(name="day", description="Day of month (1-31)", type=OptionType.STRING, required=False)
-@interactions.option(name="week", description="ISO week (1-53)", type=OptionType.STRING, required=False)
-@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(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) # noqa
-@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
-@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(name="different_channel", description="Send the messages to a different channel.", type=OptionType.CHANNEL, required=False) # noqa
-@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
-@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
-async def remind_cron(
- 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,
-):
+@interactions.option(
+ name="message_reason",
+ description="The message I'm going to send you.",
+ type=OptionType.STRING,
+ required=True,
+)
+@interactions.option(
+ name="year",
+ description="4-digit year. (Example: 2042)",
+ type=OptionType.STRING,
+ required=False,
+)
+@interactions.option(
+ name="month",
+ description="Month. (1-12)",
+ type=OptionType.STRING,
+ required=False,
+)
+@interactions.option(
+ name="day",
+ description="Day of month (1-31)",
+ type=OptionType.STRING,
+ required=False,
+)
+@interactions.option(
+ name="week",
+ description="ISO week (1-53)",
+ type=OptionType.STRING,
+ required=False,
+)
+@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,
+)
+@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.
https://en.wikipedia.org/wiki/Cron
@@ -369,12 +521,12 @@ async def remind_cron(
if different_channel:
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. :-)"
should_send_channel_reminder = True
try:
if send_dm_to_user:
- dm_reminder = scheduler.add_job(
+ dm_reminder: Job = scheduler.add_job(
send_to_user,
"cron",
year=year,
@@ -399,11 +551,15 @@ async def remind_cron(
if not both_dm_and_channel:
# If we should send the message to the channel too instead of just a DM.
should_send_channel_reminder = False
- where_and_when = (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")
-
+ where_and_when: str = (
+ 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:
- job = scheduler.add_job(
+ job: Job = scheduler.add_job(
send_to_discord,
"cron",
year=year,
@@ -424,52 +580,118 @@ async def remind_cron(
"author_id": ctx.member.id,
},
)
- where_and_when = (f" I will send messages to <#{channel_id}>{dm_message}.\n"
- f"First run in {calculate(job)} with the message:\n")
+ where_and_when = (
+ 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:
await ctx.send(str(e), ephemeral=True)
return
# TODO: Add what arguments we used in the job to the message
- message = (
- f"Hello {ctx.member.name}, "
- f"{where_and_when}"
- f"**{message_reason}**."
- )
+ message: str = f"Hello {ctx.member.name}, {where_and_when} **{message_reason}**."
await ctx.send(message)
-@base_command.subcommand(name="interval", description="Schedules messages to be run periodically, on selected intervals.")
-@interactions.option(name="message_reason", description="The message I'm going to send you.", type=OptionType.STRING, required=True)
-@interactions.option(name="weeks", description="Number of weeks to wait", type=OptionType.INTEGER, required=False)
-@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(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) # noqa
-@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
-@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(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) # noqa
-@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
-async def remind_interval(
- 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,
-):
+@base_command.subcommand(
+ name="interval",
+ description="Schedules messages to be run periodically, on selected intervals.",
+)
+@interactions.option(
+ name="message_reason",
+ description="The message I'm going to send you.",
+ type=OptionType.STRING,
+ required=True,
+)
+@interactions.option(
+ name="weeks",
+ description="Number of weeks to wait",
+ type=OptionType.INTEGER,
+ required=False,
+)
+@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(
+ 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.
Args:
@@ -493,12 +715,12 @@ async def remind_interval(
if different_channel:
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. :-)"
should_send_channel_reminder = True
try:
if send_dm_to_user:
- dm_reminder = scheduler.add_job(
+ dm_reminder: Job = scheduler.add_job(
send_to_user,
"interval",
weeks=weeks,
@@ -520,10 +742,16 @@ async def remind_interval(
if not both_dm_and_channel:
# If we should send the message to the channel too instead of just a DM.
should_send_channel_reminder = False
- where_and_when = (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")
+ where_and_when: str = (
+ 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:
- job = scheduler.add_job(
+ job: Job = scheduler.add_job(
send_to_discord,
"interval",
weeks=weeks,
@@ -541,37 +769,36 @@ async def remind_interval(
"author_id": ctx.member.id,
},
)
- where_and_when = (f" I will send messages to <#{channel_id}>{dm_message}.\n"
- f"First run in {calculate(job)} with the message:\n")
+ where_and_when = (
+ f" I will send messages to <#{channel_id}>{dm_message}.\n"
+ f"First run in {calculate(job)} with the message:"
+ )
except ValueError as e:
await ctx.send(str(e), ephemeral=True)
return
# TODO: Add what arguments we used in the job to the message
- message = (
- f"Hello {ctx.member.name}\n"
- f"{where_and_when}"
- f"**{message_reason}**."
- )
+ message: str = f"Hello {ctx.member.name}\n{where_and_when}\n**{message_reason}**."
await ctx.send(message)
-def my_listener(event):
+def my_listener(event: JobExecutionEvent) -> None:
"""This gets called when something in APScheduler happens."""
if event.code == events.EVENT_JOB_MISSED:
# TODO: Is it possible to get the message?
- scheduled_time = event.scheduled_run_time.strftime("%Y-%m-%d %H:%M:%S")
- msg = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}"
+ scheduled_time: str = event.scheduled_run_time.strftime("%Y-%m-%d %H:%M:%S")
+ msg: str = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}"
send_webhook(message=msg)
if event.exception:
- send_webhook(f"discord-reminder-bot failed to send message to Discord\n"
- f"{event}")
+ send_webhook(
+ 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.
Args:
@@ -579,26 +806,23 @@ async def send_to_discord(channel_id: int, message: str, author_id: int):
message: The message.
author_id: User we should ping.
"""
-
- channel = await interactions.get(
+ channel: Channel = await interactions.get(
bot,
interactions.Channel,
- object_id=int(channel_id),
+ object_id=channel_id,
force=interactions.Force.HTTP,
)
await channel.send(f"<@{author_id}>\n{message}")
-def start():
+def start() -> None:
"""Start scheduler and log in to Discord."""
# TODO: Add how many reminders are scheduled.
# TODO: Make backup of jobs.sqlite before running the bot.
logging.basicConfig(level=logging.getLevelName(log_level))
logging.info(
- f"\nsqlite_location = {sqlite_location}\n"
- f"config_timezone = {config_timezone}\n"
- f"log_level = {log_level}"
+ "\nsqlite_location = %s\nconfig_timezone = %s\nlog_level = %s" % (sqlite_location, config_timezone, log_level),
)
scheduler.start()
scheduler.add_listener(my_listener, EVENT_JOB_MISSED | EVENT_JOB_ERROR)
diff --git a/discord_reminder_bot/parse.py b/discord_reminder_bot/parse.py
index f337882..f92fe48 100644
--- a/discord_reminder_bot/parse.py
+++ b/discord_reminder_bot/parse.py
@@ -9,8 +9,7 @@ from discord_reminder_bot.settings import config_timezone
@dataclasses.dataclass
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.
@@ -20,10 +19,11 @@ class ParsedTime:
err_msg: The error message.
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_msg: str = ""
- parsed_time: datetime = None
+ parsed_time: datetime | None = None
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
"""
try:
- parsed_date = dateparser.parse(
+ parsed_date: datetime | None = dateparser.parse(
f"{date_to_parse}",
settings={
"PREFER_DATES_FROM": "future",
@@ -46,12 +46,25 @@ def parse_time(date_to_parse: str, timezone: str = config_timezone) -> ParsedTim
},
)
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:
- 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:
return ParsedTime(err=True, err_msg=f"{e}", date_to_parse=date_to_parse)
- if not parsed_date:
- return ParsedTime(err=True, err_msg=f"Could not parse the date.", date_to_parse=date_to_parse)
-
- return ParsedTime(parsed_time=parsed_date, date_to_parse=date_to_parse)
+ return (
+ ParsedTime(parsed_time=parsed_date, date_to_parse=date_to_parse)
+ if parsed_date
+ else ParsedTime(
+ err=True,
+ err_msg="Could not parse the date.",
+ date_to_parse=date_to_parse,
+ )
+ )
diff --git a/discord_reminder_bot/settings.py b/discord_reminder_bot/settings.py
index 503f3a9..6b506c6 100644
--- a/discord_reminder_bot/settings.py
+++ b/discord_reminder_bot/settings.py
@@ -6,18 +6,19 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
from dotenv import load_dotenv
load_dotenv(verbose=True)
-sqlite_location = os.getenv("SQLITE_LOCATION", default="/jobs.sqlite")
-config_timezone = os.getenv("TIMEZONE", default="UTC")
-bot_token = os.getenv("BOT_TOKEN", default="")
-log_level = os.getenv("LOG_LEVEL", default="INFO")
-webhook_url = os.getenv("WEBHOOK_URL", default="")
+sqlite_location: str = os.getenv("SQLITE_LOCATION", default="/jobs.sqlite")
+config_timezone: str = os.getenv("TIMEZONE", default="UTC")
+bot_token: str = os.getenv("BOT_TOKEN", default="")
+log_level: str = os.getenv("LOG_LEVEL", default="INFO")
+webhook_url: str = os.getenv("WEBHOOK_URL", default="")
if not bot_token:
- raise ValueError("Missing bot token")
+ err_msg = "Missing bot token"
+ raise ValueError(err_msg)
# Advanced Python Scheduler
-jobstores = {"default": SQLAlchemyJobStore(url=f"sqlite://{sqlite_location}")}
-job_defaults = {"coalesce": True}
+jobstores: dict[str, SQLAlchemyJobStore] = {"default": SQLAlchemyJobStore(url=f"sqlite://{sqlite_location}")}
+job_defaults: dict[str, bool] = {"coalesce": True}
scheduler = AsyncIOScheduler(
jobstores=jobstores,
timezone=pytz.timezone(config_timezone),
diff --git a/poetry.lock b/poetry.lock
index 60d7dd4..6ee8355 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -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]]
name = "aiohttp"
@@ -318,9 +318,9 @@ fasttext = ["fasttext"]
langdetect = ["langdetect"]
[[package]]
-name = "dinteractions_Paginator"
+name = "dinteractions-Paginator"
version = "2.1.0"
-description = ""
+description = "Official interactions.py paginator"
category = "main"
optional = false
python-versions = "*"
@@ -951,7 +951,7 @@ files = [
]
[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]
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
diff --git a/pyproject.toml b/pyproject.toml
index 8b685df..17c8c6c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,4 +39,81 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.isort]
-profile = "black"
\ No newline at end of file
+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"]
diff --git a/tests/test_countdown.py b/tests/test_countdown.py
index 64f7a4e..c7e5d21 100644
--- a/tests/test_countdown.py
+++ b/tests/test_countdown.py
@@ -1,10 +1,8 @@
-"""Test discord-reminder-bot.
-
-Jobs are stored in memory.
-"""
+from datetime import datetime
import dateparser
import pytz
+from apscheduler.job import Job
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.schedulers.asyncio import AsyncIOScheduler
@@ -19,23 +17,25 @@ class TestCountdown:
runs at 00:00.
"""
- jobstores = {"default": SQLAlchemyJobStore(url="sqlite:///:memory")}
- job_defaults = {"coalesce": True}
+ jobstores: dict[str, SQLAlchemyJobStore] = {"default": SQLAlchemyJobStore(url="sqlite:///:memory")}
+ job_defaults: dict[str, bool] = {"coalesce": True}
scheduler = AsyncIOScheduler(
jobstores=jobstores,
timezone=pytz.timezone("Europe/Stockholm"),
job_defaults=job_defaults,
)
- parsed_date = dateparser.parse(
+ parsed_date: datetime | None = dateparser.parse(
"18 January 2040",
settings={
"PREFER_DATES_FROM": "future",
"TO_TIMEZONE": "Europe/Stockholm",
},
)
- run_date = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
- job = scheduler.add_job(
+ assert parsed_date
+
+ run_date: str = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
+ job: Job = scheduler.add_job(
send_to_discord,
run_date=run_date,
kwargs={
@@ -45,7 +45,7 @@ class TestCountdown:
},
)
- timezone_date = dateparser.parse(
+ timezone_date: datetime | None = dateparser.parse(
"00:00",
settings={
"PREFER_DATES_FROM": "future",
@@ -53,8 +53,10 @@ class TestCountdown:
"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,
run_date=timezone_run_date,
kwargs={
@@ -64,7 +66,7 @@ class TestCountdown:
},
)
- timezone_date2 = dateparser.parse(
+ timezone_date2: datetime | None = dateparser.parse(
"13:37",
settings={
"PREFER_DATES_FROM": "future",
@@ -72,8 +74,10 @@ class TestCountdown:
"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,
run_date=timezone_run_date2,
kwargs={
@@ -83,21 +87,22 @@ class TestCountdown:
},
)
- # def test_countdown(self):
- # """Check if calc_countdown returns days, hours and minutes."""
- # # 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)")
- # countdown = calculate(self.job)
- # assert pattern.match(countdown)
+ def test_if_timezones_are_working(self) -> None: # noqa: ANN101
+ """Check if timezones are working.
+
+ Args:
+ self: TestCountdown
+ """
+ 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.minute == 0
assert time_job.trigger.run_date.second == 0
- time_job2 = self.scheduler.get_job(self.timezone_job2.id)
- assert time_job2.trigger.run_date.hour == 13
- assert time_job2.trigger.run_date.minute == 37
+ time_job2: Job | None = self.scheduler.get_job(self.timezone_job2.id)
+ assert time_job2
+
+ 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
diff --git a/tests/test_main.py b/tests/test_main.py
index df21a39..27c186b 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,14 +1,11 @@
from discord_reminder_bot import main
-def test_if_send_to_discord_is_in_main():
- """
- send_to_discords needs to be in main for this program to work.
- """
+def test_if_send_to_discord_is_in_main() -> None:
+ """send_to_discords needs to be in main for this program to work."""
assert hasattr(main, "send_to_discord")
-def test_if_send_to_user_is_in_main():
- """
- send_to_user needs to be in main for this program to work.
- """
- assert hasattr(main, "send_to_user")
\ No newline at end of file
+
+def test_if_send_to_user_is_in_main() -> None:
+ """send_to_user needs to be in main for this program to work."""
+ assert hasattr(main, "send_to_user")