Use modern Discord Ui stuff, fix time zones and, fix Dockerfile
Changes: Use buttons, pagination and modals instead of old text-based stuff Fix time zones (Closes #23) Fix Dockerfile Replace Discord.py with discord-py-interactions Default time zone is now UTC instead of Europe/Stockholm Replace /remind resume /remind pause /remind remove /remind modify with /remind list
This commit is contained in:
26
CHANGELOG.md
26
CHANGELOG.md
@ -1,9 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to discord-reminder-bot will be documented in this file.
|
||||
## [1.0.0] - 2022-09-17
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
### Added
|
||||
|
||||
- Added tests for checking if timezones are working correctly
|
||||
|
||||
### Changed
|
||||
|
||||
- Bot is now using [discord-py-interactions](https://github.com/interactions-py/library) instead of
|
||||
[discord-py-slash-command](https://github.com/interactions-py/library/tree/legacy-v3) and [discord.py]
|
||||
(https://github.com/Rapptz/discord.py).
|
||||
- `/remind pause`, `/remind resume`, `/remind delete`, and `/remind modify` are now buttons under `/remind list`.
|
||||
- `/remind list` uses pagination now.
|
||||
- `/remind modify` uses a modal now.
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Dockerfile](/Dockerfile) was broken, now it is fixed. (Thanks to [FirosStuart](https://github.com/FirosStuart)
|
||||
for the fix)
|
||||
- Timezones are now handled correctly. (Thanks to [FirosStuart](https://github.com/FirosStuart))
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- You will need the latest version of Poetry to install the dependencies. (Or have Git installed)
|
||||
|
||||
## [0.3.0] - 2022-02-19
|
||||
|
||||
|
@ -38,9 +38,9 @@ COPY pyproject.toml poetry.lock README.md /home/botuser/
|
||||
# Change directory to where we will run the bot.
|
||||
WORKDIR /home/botuser
|
||||
|
||||
RUN poetry install --no-interaction --no-ansi --no-dev
|
||||
RUN poetry install --no-interaction --no-ansi --only main
|
||||
|
||||
COPY discord_reminder_bot/main.py discord_reminder_bot/settings.py discord_reminder_bot/countdown.py /home/botuser/discord_reminder_bot/
|
||||
COPY discord_reminder_bot /home/botuser/discord_reminder_bot/
|
||||
|
||||
VOLUME ["/home/botuser/data/"]
|
||||
|
||||
|
@ -1 +1 @@
|
||||
__version__ = "0.3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
197
discord_reminder_bot/create_pages.py
Normal file
197
discord_reminder_bot/create_pages.py
Normal file
@ -0,0 +1,197 @@
|
||||
"""This module creates the pages for the paginator."""
|
||||
from typing import List
|
||||
|
||||
import interactions
|
||||
from apscheduler.job import Job
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
from interactions import ActionRow, ComponentContext
|
||||
from interactions.ext.paginator import Page, Paginator, RowPosition
|
||||
|
||||
from discord_reminder_bot.countdown import calculate
|
||||
from discord_reminder_bot.settings import scheduler
|
||||
|
||||
|
||||
def create_pages(ctx) -> list[Page]:
|
||||
"""Create pages for the paginator.
|
||||
|
||||
Args:
|
||||
ctx (interactions.Context): The context of the command.
|
||||
|
||||
Returns:
|
||||
list[Page]: A list of pages.
|
||||
"""
|
||||
pages = []
|
||||
|
||||
jobs: List[Job] = scheduler.get_jobs()
|
||||
for job in jobs:
|
||||
channel_id = job.kwargs.get("channel_id")
|
||||
# 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:
|
||||
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
|
||||
|
||||
# Paused reminders returns None
|
||||
if trigger_time is None:
|
||||
trigger_value = None
|
||||
trigger_text = "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")
|
||||
|
||||
edit_button = interactions.Button(
|
||||
label="Edit",
|
||||
style=interactions.ButtonStyle.PRIMARY,
|
||||
custom_id="edit",
|
||||
)
|
||||
pause_button = interactions.Button(
|
||||
label="Pause",
|
||||
style=interactions.ButtonStyle.PRIMARY,
|
||||
custom_id="pause",
|
||||
)
|
||||
unpause_button = interactions.Button(
|
||||
label="Unpause",
|
||||
style=interactions.ButtonStyle.PRIMARY,
|
||||
custom_id="unpause",
|
||||
)
|
||||
remove_button = interactions.Button(
|
||||
label="Remove",
|
||||
style=interactions.ButtonStyle.DANGER,
|
||||
custom_id="remove",
|
||||
)
|
||||
|
||||
embed = interactions.Embed(
|
||||
title=f"{job.id}",
|
||||
fields=[
|
||||
interactions.EmbedField(
|
||||
name=f"**Channel:**",
|
||||
value=f"#{channel.name}",
|
||||
),
|
||||
interactions.EmbedField(
|
||||
name=f"**Message:**",
|
||||
value=f"{message}",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
if trigger_value is not None:
|
||||
embed.add_field(
|
||||
name=f"**Trigger:**",
|
||||
value=f"{trigger_text}",
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name=f"**Trigger:**",
|
||||
value=f"_Paused_",
|
||||
)
|
||||
|
||||
components = [
|
||||
edit_button,
|
||||
remove_button,
|
||||
]
|
||||
|
||||
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
|
||||
components.insert(1, pause_or_unpause_button)
|
||||
|
||||
# Add a page to pages list
|
||||
pages.append(
|
||||
Page(
|
||||
embeds=embed,
|
||||
title=message,
|
||||
components=ActionRow(components=components), # type: ignore
|
||||
callback=callback,
|
||||
position=RowPosition.BOTTOM,
|
||||
)
|
||||
)
|
||||
|
||||
return pages
|
||||
|
||||
|
||||
async def callback(self: Paginator, ctx: ComponentContext):
|
||||
"""Callback for the paginator."""
|
||||
job_id = self.component_ctx.message.embeds[0].title
|
||||
job = scheduler.get_job(job_id)
|
||||
|
||||
if job is None:
|
||||
await ctx.send("Job not found.")
|
||||
return
|
||||
|
||||
channel_id = job.kwargs.get("channel_id")
|
||||
old_message = job.kwargs.get("message")
|
||||
|
||||
components = [
|
||||
interactions.TextInput( # type: ignore
|
||||
style=interactions.TextStyleType.PARAGRAPH,
|
||||
placeholder=old_message,
|
||||
label="New message",
|
||||
custom_id="new_message",
|
||||
value=old_message,
|
||||
required=False,
|
||||
),
|
||||
]
|
||||
|
||||
if type(job.trigger) is DateTrigger:
|
||||
# Get trigger time for normal reminders
|
||||
trigger_time = job.trigger.run_date
|
||||
job_type = "normal"
|
||||
components.append(
|
||||
interactions.TextInput( # type: ignore
|
||||
style=interactions.TextStyleType.SHORT,
|
||||
placeholder=str(trigger_time),
|
||||
label="New date, Can be human readable or ISO8601",
|
||||
custom_id="new_date",
|
||||
value=str(trigger_time),
|
||||
required=False,
|
||||
),
|
||||
)
|
||||
|
||||
else:
|
||||
# Get trigger time for cron and interval jobs
|
||||
trigger_time = job.next_run_time
|
||||
job_type = "cron/interval"
|
||||
|
||||
if ctx.custom_id == "edit":
|
||||
await self.end_paginator()
|
||||
modal = interactions.Modal(
|
||||
title=f"Edit {job_type} reminder.",
|
||||
custom_id="edit_modal",
|
||||
components=components,
|
||||
)
|
||||
await ctx.popup(modal)
|
||||
print(ctx.data)
|
||||
|
||||
elif ctx.custom_id == "pause":
|
||||
await self.end_paginator()
|
||||
# TODO: Add unpause button if user paused the wrong job
|
||||
paused_job = scheduler.pause_job(job_id)
|
||||
print(f"Paused job: {paused_job}")
|
||||
|
||||
await ctx.send(f"Job {job_id} paused.")
|
||||
elif ctx.custom_id == "unpause":
|
||||
await self.end_paginator()
|
||||
# TODO: Add pause button if user unpauses the wrong job
|
||||
scheduler.resume_job(job_id)
|
||||
await ctx.send(f"Job {job_id} unpaused.")
|
||||
elif ctx.custom_id == "remove":
|
||||
await self.end_paginator()
|
||||
# TODO: Add recreate button if user removed the wrong job
|
||||
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}"
|
||||
)
|
@ -1,438 +1,175 @@
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
import dateparser
|
||||
import discord
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
from discord.errors import NotFound
|
||||
from discord.ext import commands
|
||||
from discord_slash import SlashCommand, SlashContext
|
||||
from discord_slash.error import IncorrectFormat, RequestFailure
|
||||
from discord_slash.model import SlashCommandOptionType
|
||||
from discord_slash.utils.manage_commands import create_choice, create_option
|
||||
import interactions
|
||||
from apscheduler.jobstores.base import JobLookupError
|
||||
from interactions import CommandContext, Embed, Option, OptionType
|
||||
from interactions.ext.paginator import Paginator
|
||||
from interactions.ext.wait_for import setup
|
||||
|
||||
from discord_reminder_bot.countdown import calculate
|
||||
from discord_reminder_bot.settings import bot_token, config_timezone, log_level, scheduler, sqlite_location
|
||||
|
||||
bot = commands.Bot(
|
||||
command_prefix="!",
|
||||
description="Reminder bot for Discord by TheLovinator#9276",
|
||||
intents=discord.Intents.all(),
|
||||
from discord_reminder_bot.create_pages import create_pages
|
||||
from discord_reminder_bot.settings import (
|
||||
bot_token,
|
||||
config_timezone,
|
||||
log_level,
|
||||
scheduler,
|
||||
sqlite_location,
|
||||
)
|
||||
slash = SlashCommand(bot, sync_commands=True)
|
||||
|
||||
bot = interactions.Client(token=bot_token)
|
||||
|
||||
# Initialize the wait_for extension.
|
||||
setup(bot)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_slash_command_error(ctx: SlashContext, ex: Exception) -> None:
|
||||
"""Handle errors in slash commands.
|
||||
@bot.command(name="remind")
|
||||
async def base_command(ctx: interactions.CommandContext):
|
||||
"""This description isn't seen in the UI (yet?)
|
||||
|
||||
This is the base command for the reminder bot."""
|
||||
pass
|
||||
|
||||
|
||||
@bot.modal("edit_modal")
|
||||
async def modal_response_edit(ctx: CommandContext, new_date: str, new_message: str):
|
||||
"""Edit a reminder.
|
||||
|
||||
Args:
|
||||
ctx: The context of the command. Used to get the server name and what channel the command was sent in.
|
||||
ex: The exception that was raised.
|
||||
"""
|
||||
logging.error(f"Error occurred during the execution of '/{ctx.name} {ctx.subcommand_name}' by {ctx.author}: {ex}")
|
||||
if ex == RequestFailure:
|
||||
message = f"Request to Discord API failed: {ex}"
|
||||
elif ex == IncorrectFormat:
|
||||
message = f"Incorrect format: {ex}"
|
||||
elif ex == NotFound:
|
||||
message = f"I couldn't find the interaction or it took me longer than 3 seconds to respond: {ex}"
|
||||
else:
|
||||
message = f"Error occurred during the execution of '/{ctx.name} {ctx.subcommand_name}': {ex}"
|
||||
|
||||
await ctx.send(
|
||||
f"{message}\nIf this persists, please make an issue on the"
|
||||
"[GitHub repo](https://github.com/TheLovinator1/discord-reminder-bot/issues) or contact TheLovinator#9276",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
"""Print when the bot is ready."""
|
||||
logging.info(f"Logged in as {bot.user.name}")
|
||||
|
||||
|
||||
@slash.subcommand(
|
||||
base="remind",
|
||||
name="modify",
|
||||
description="Modify a reminder. Does not work with cron or interval.",
|
||||
options=[
|
||||
create_option(
|
||||
name="time_or_message",
|
||||
description="Choose between modifying the date or the message.",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
required=True,
|
||||
choices=[
|
||||
create_choice(name="Date", value="date"),
|
||||
create_choice(name="Message", value="message"),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def command_modify(ctx: SlashContext, time_or_message: str):
|
||||
"""Modify a reminder. You can change time or message.
|
||||
|
||||
Args:
|
||||
ctx: Context of the slash command. Contains the guild, author and message and more.
|
||||
time_or_message: Choose between modifying the message or time.
|
||||
"""
|
||||
# TODO: Reduce complexity.
|
||||
|
||||
# Only make a list with normal reminders.
|
||||
jobs_dict = await send_list(ctx, skip_cron_or_interval=True)
|
||||
|
||||
if time_or_message == "date":
|
||||
date_or_message = "the date"
|
||||
else:
|
||||
date_or_message = "the message"
|
||||
|
||||
await ctx.channel.send(
|
||||
f"Type the corresponding number to the reminder were you wish to change {date_or_message}."
|
||||
" Does not work with cron or interval. Type Exit to exit."
|
||||
)
|
||||
|
||||
def check(m):
|
||||
"""Check if the message is from the original user and in the correct channel."""
|
||||
return m.author == ctx.author and m.channel == ctx.channel
|
||||
|
||||
# TODO: Add timeout
|
||||
response_message = await bot.wait_for("message", check=check)
|
||||
if response_message.clean_content == "Exit":
|
||||
return await ctx.channel.send("Exiting...")
|
||||
|
||||
for num, job_from_dict in jobs_dict.items():
|
||||
if int(response_message.clean_content) == num:
|
||||
|
||||
job = scheduler.get_job(job_from_dict)
|
||||
|
||||
# Get_job() returns None when it can't find a job with that ID.
|
||||
if job is None:
|
||||
await ctx.send(f"No reminder with ID ({job_from_dict}).")
|
||||
return
|
||||
|
||||
message = job.kwargs.get("message")
|
||||
old_time = calculate(job)
|
||||
|
||||
channel_name = bot.get_channel(int(job.kwargs.get("channel_id")))
|
||||
msg = f"**Modified** {job_from_dict} in #{channel_name}\n"
|
||||
if time_or_message == "message":
|
||||
await ctx.channel.send("Type the new message. Type Exit to exit.")
|
||||
|
||||
# TODO: Add timeout
|
||||
response_new_message = await bot.wait_for("message", check=check)
|
||||
|
||||
if response_new_message.clean_content == "Exit":
|
||||
return await ctx.channel.send("Exiting...")
|
||||
|
||||
scheduler.modify_job(
|
||||
job_from_dict,
|
||||
kwargs={
|
||||
"channel_id": job.kwargs.get("channel_id"),
|
||||
"message": f"{response_new_message.clean_content}",
|
||||
"author_id": job.kwargs.get("author_id"),
|
||||
},
|
||||
)
|
||||
msg += f"**Old message**: {message}\n" f"**New message**: {response_new_message.clean_content}\n"
|
||||
|
||||
else:
|
||||
await ctx.channel.send("Type the new date. Type Exit to exit.")
|
||||
|
||||
# TODO: Add timeout
|
||||
response_new_date = await bot.wait_for("message", check=check)
|
||||
if response_new_date.clean_content == "Exit":
|
||||
return await ctx.channel.send("Exiting...")
|
||||
|
||||
parsed_date = dateparser.parse(
|
||||
f"{response_new_date.clean_content}",
|
||||
settings={
|
||||
"PREFER_DATES_FROM": "future",
|
||||
"TIMEZONE": f"{config_timezone}",
|
||||
},
|
||||
)
|
||||
date_new = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
job = scheduler.reschedule_job(job_from_dict, run_date=date_new)
|
||||
|
||||
date_old = job.trigger.run_date.strftime("%Y-%m-%d %H:%M")
|
||||
new_time = calculate(job_from_dict)
|
||||
msg += f"**Old date**: {date_old} (in {old_time})\n**New date**: {date_new} (in {new_time})"
|
||||
|
||||
await ctx.send(msg)
|
||||
|
||||
|
||||
@slash.subcommand(
|
||||
base="remind",
|
||||
name="remove",
|
||||
description="Remove a reminder",
|
||||
)
|
||||
async def remind_remove(ctx: SlashContext):
|
||||
"""Select reminder from list that you want to remove.
|
||||
|
||||
Args:
|
||||
ctx: Context of the slash command. Contains the guild, author and message and more.
|
||||
"""
|
||||
# TODO: Reduce complexity
|
||||
|
||||
jobs_dict = await send_list(ctx)
|
||||
|
||||
await ctx.channel.send("Type the corresponding number to the reminder you wish to remove. Type Exit to exit.")
|
||||
|
||||
def check(m):
|
||||
"""Check if the message is from the original user and in the correct channel."""
|
||||
return m.author == ctx.author and m.channel == ctx.channel
|
||||
|
||||
# TODO: Add timeout
|
||||
response_message = await bot.wait_for("message", check=check)
|
||||
if response_message.clean_content == "Exit":
|
||||
return await ctx.channel.send("Exiting...")
|
||||
|
||||
for num, job_from_dict in jobs_dict.items():
|
||||
if int(response_message.clean_content) == num:
|
||||
job = scheduler.get_job(job_from_dict)
|
||||
if job is None:
|
||||
await ctx.channel.send(f"No reminder with that ID ({job_from_dict}).")
|
||||
return
|
||||
|
||||
channel_id = job.kwargs.get("channel_id")
|
||||
channel_name = bot.get_channel(int(channel_id))
|
||||
message = job.kwargs.get("message")
|
||||
|
||||
# Only normal reminders have trigger.run_date, cron and
|
||||
# interval has next_run_time
|
||||
if type(job.trigger) is DateTrigger:
|
||||
trigger_time = job.trigger.run_date
|
||||
else:
|
||||
trigger_time = job.next_run_time
|
||||
|
||||
# Paused reminders returns None
|
||||
if trigger_time is None:
|
||||
trigger_value = "Paused - can be resumed with '/remind resume'"
|
||||
else:
|
||||
trigger_value = f'{trigger_time.strftime("%Y-%m-%d %H:%M")} (in {calculate(job)})'
|
||||
|
||||
msg = f"**Removed** {message} in #{channel_name}.\n**Time**: {trigger_value}"
|
||||
|
||||
scheduler.remove_job(job_from_dict)
|
||||
|
||||
await ctx.channel.send(msg)
|
||||
|
||||
|
||||
async def send_list(ctx: SlashContext, skip_datetriggers=False, skip_cron_or_interval=False) -> dict:
|
||||
"""Create a list of reminders.
|
||||
|
||||
Args:
|
||||
ctx: The context of the command. Used to get the server name and what channel the command was sent in.
|
||||
skip_datetriggers: Only show cron jobs and interval reminders.
|
||||
skip_cron_or_interval: Only show normal reminders.
|
||||
ctx: The context.
|
||||
new_date: The new date.
|
||||
new_message: The new message.
|
||||
|
||||
Returns:
|
||||
jobs_dict: Dictionary that contains placement in list and job ID.
|
||||
A Discord message with changes.
|
||||
"""
|
||||
# TODO: This will fail if the embed is bigger than 6000 characters.
|
||||
jobs_dict = {}
|
||||
job_number = 0
|
||||
await ctx.defer()
|
||||
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.random(),
|
||||
title="discord-reminder-bot by TheLovinator#9276",
|
||||
description=f"Reminders for {ctx.guild.name}",
|
||||
url="https://github.com/TheLovinator1/discord-reminder-bot",
|
||||
)
|
||||
jobs = scheduler.get_jobs()
|
||||
job_id = ctx.message.embeds[0].title
|
||||
old_date = None
|
||||
old_message = None
|
||||
|
||||
for job in jobs:
|
||||
channel_id = job.kwargs.get("channel_id")
|
||||
channel_name = bot.get_channel(int(channel_id))
|
||||
try:
|
||||
job = scheduler.get_job(job_id)
|
||||
except JobLookupError as e:
|
||||
return await ctx.send(
|
||||
f"Failed to get the job after the modal.\nJob ID: {job_id}\nError: {e}"
|
||||
)
|
||||
|
||||
# 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 channel.id == channel_id:
|
||||
if type(job.trigger) is DateTrigger:
|
||||
# Get trigger time for normal reminders
|
||||
trigger_time = job.trigger.run_date
|
||||
if job is None:
|
||||
return await ctx.send("Job not found.")
|
||||
|
||||
# Don't add normal reminders if true
|
||||
if skip_datetriggers:
|
||||
continue
|
||||
else:
|
||||
# Get trigger time for cron and interval jobs
|
||||
trigger_time = job.next_run_time
|
||||
|
||||
# Don't add cron and interval reminders if true
|
||||
if skip_cron_or_interval:
|
||||
continue
|
||||
|
||||
# Paused reminders returns None
|
||||
if trigger_time is None:
|
||||
trigger_value = "Paused"
|
||||
else:
|
||||
trigger_value = f'{trigger_time.strftime("%Y-%m-%d %H:%M")} (in {calculate(job)})'
|
||||
|
||||
job_number += 1
|
||||
jobs_dict[job_number] = job.id
|
||||
message = job.kwargs.get("message")
|
||||
|
||||
# Truncate message if it is too long
|
||||
field_name = f"{job_number}) {message} in #{channel_name}"
|
||||
field_name = field_name[:253] + (field_name[253:] and "...")
|
||||
|
||||
embed.add_field(
|
||||
name=field_name,
|
||||
value=trigger_value,
|
||||
inline=False,
|
||||
message_embeds: List[Embed] = ctx.message.embeds
|
||||
for embeds in message_embeds:
|
||||
for field in embeds.fields:
|
||||
if field.name == "**Channel:**":
|
||||
continue
|
||||
elif field.name == "**Message:**":
|
||||
old_message = field.value
|
||||
elif field.name == "**Trigger:**":
|
||||
old_date = field.value
|
||||
else:
|
||||
return await ctx.send(
|
||||
f"Unknown field name ({field.name}).", ephemeral=True
|
||||
)
|
||||
|
||||
if job_number == 24:
|
||||
await ctx.send("I haven't added support for showing more than 25 reminders yet 🙃")
|
||||
break
|
||||
msg = f"Modified job {job_id}.\n"
|
||||
if new_date != old_date and old_date is not None:
|
||||
parsed_date = dateparser.parse(
|
||||
f"{new_date}",
|
||||
settings={
|
||||
"PREFER_DATES_FROM": "future",
|
||||
"TIMEZONE": f"{config_timezone}",
|
||||
"TO_TIMEZONE": f"{config_timezone}",
|
||||
},
|
||||
)
|
||||
|
||||
# The empty embed has 76 characters
|
||||
if len(embed) <= 76:
|
||||
await ctx.send(f"{ctx.guild.name} has no reminders.")
|
||||
else:
|
||||
await ctx.send(embed=embed)
|
||||
if not parsed_date:
|
||||
return await ctx.send("Could not parse the date.", ephemeral=True)
|
||||
|
||||
return jobs_dict
|
||||
date_new = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
new_job = scheduler.reschedule_job(job.id, run_date=date_new)
|
||||
new_time = calculate(new_job)
|
||||
|
||||
msg += f"**Old date**: {old_date}\n**New date**: {date_new} (in {new_time})"
|
||||
|
||||
if new_message != old_message and old_message is not None:
|
||||
channel_id = job.kwargs.get("channel_id")
|
||||
job_author_id = job.kwargs.get("author_id")
|
||||
|
||||
scheduler.modify_job(
|
||||
job.id,
|
||||
kwargs={
|
||||
"channel_id": channel_id,
|
||||
"message": f"{new_message}",
|
||||
"author_id": job_author_id,
|
||||
},
|
||||
)
|
||||
|
||||
msg += f"**Old message**: {old_message}\n**New message**: {new_message}"
|
||||
|
||||
return await ctx.send(msg)
|
||||
|
||||
|
||||
@slash.subcommand(base="remind", name="list", description="Show reminders.")
|
||||
async def remind_list(ctx: SlashContext):
|
||||
"""Send a list of reminders to Discord.
|
||||
|
||||
Args:
|
||||
ctx: Context of the slash command. Contains the guild, author and message and more.
|
||||
"""
|
||||
await send_list(ctx)
|
||||
|
||||
|
||||
@slash.subcommand(base="remind", name="pause", description="Pause reminder. For cron or interval.")
|
||||
async def remind_pause(ctx: SlashContext):
|
||||
"""Get a list of reminders that you can pause."""
|
||||
jobs_dict = await send_list(ctx, skip_datetriggers=True)
|
||||
|
||||
await ctx.channel.send("Type the corresponding number to the reminder you wish to pause. Type Exit to exit.")
|
||||
|
||||
def check(m):
|
||||
"""Check if the message is from the original user and in the correct channel."""
|
||||
return m.author == ctx.author and m.channel == ctx.channel
|
||||
|
||||
# TODO: Add timeout
|
||||
response_reminder = await bot.wait_for("message", check=check)
|
||||
if response_reminder.clean_content == "Exit":
|
||||
return await ctx.channel.send("Exiting...")
|
||||
|
||||
# Pair a number with the job ID
|
||||
for num, job_from_dict in jobs_dict.items():
|
||||
# Check if the response is a number and if it is in the list.
|
||||
if int(response_reminder.clean_content) == num:
|
||||
job = scheduler.get_job(job_from_dict)
|
||||
channel_id = job.kwargs.get("channel_id")
|
||||
channel_name = bot.get_channel(int(channel_id))
|
||||
message = job.kwargs.get("message")
|
||||
|
||||
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
|
||||
|
||||
# Tell user if he tries to pause a paused reminder
|
||||
if trigger_time is None:
|
||||
return await ctx.channel.send(f"{message} in #{channel_name} is already paused.")
|
||||
|
||||
trigger_value = f'{trigger_time.strftime("%Y-%m-%d %H:%M")} (in {calculate(job)})'
|
||||
|
||||
msg = f"**Paused** {message} in #{channel_name}.\n**Time**: {trigger_value}"
|
||||
|
||||
scheduler.pause_job(job_from_dict)
|
||||
print(f"Paused {job_from_dict} in #{channel_name}")
|
||||
await ctx.channel.send(msg)
|
||||
|
||||
|
||||
@slash.subcommand(
|
||||
base="remind",
|
||||
name="resume",
|
||||
description="Resume paused reminder. For cron or interval.",
|
||||
@base_command.subcommand(
|
||||
name="list", description="List, pause, unpause, and remove reminders."
|
||||
)
|
||||
async def remind_resume(ctx: SlashContext):
|
||||
"""Send a list of reminders to pause to Discord."""
|
||||
# TODO: Reduce the complexity of this function
|
||||
jobs_dict = await send_list(ctx, skip_datetriggers=True)
|
||||
async def list_command(ctx: interactions.CommandContext):
|
||||
"""List, pause, unpause, and remove reminders."""
|
||||
|
||||
await ctx.channel.send("Type the corresponding number to the reminder you wish to pause. Type Exit to exit.")
|
||||
pages = create_pages(ctx)
|
||||
if not pages:
|
||||
await ctx.send("No reminders found.")
|
||||
return
|
||||
|
||||
def check(m):
|
||||
"""Check if the message is from the original user and in the correct channel."""
|
||||
return m.author == ctx.author and m.channel == ctx.channel
|
||||
if len(pages) == 1:
|
||||
await ctx.send("a")
|
||||
return
|
||||
|
||||
# TODO: Add timeout
|
||||
response_message = await bot.wait_for("message", check=check)
|
||||
if response_message.clean_content == "Exit":
|
||||
return await ctx.channel.send("Exiting...")
|
||||
paginator: Paginator = Paginator(
|
||||
client=bot,
|
||||
ctx=ctx,
|
||||
pages=pages,
|
||||
remove_after_timeout=True,
|
||||
author_only=True,
|
||||
extended_buttons=False,
|
||||
use_buttons=False,
|
||||
)
|
||||
|
||||
for num, job_from_dict in jobs_dict.items():
|
||||
if int(response_message.clean_content) == num:
|
||||
job = scheduler.get_job(job_from_dict)
|
||||
if job is None:
|
||||
await ctx.send(f"No reminder with that ID ({job_from_dict}).")
|
||||
return
|
||||
|
||||
channel_id = job.kwargs.get("channel_id")
|
||||
channel_name = bot.get_channel(int(channel_id))
|
||||
message = job.kwargs.get("message")
|
||||
|
||||
scheduler.resume_job(job_from_dict)
|
||||
|
||||
# Only normal reminders have trigger.run_date
|
||||
# Cron and interval has next_run_time
|
||||
if type(job.trigger) is DateTrigger:
|
||||
trigger_time = job.trigger.run_date
|
||||
else:
|
||||
trigger_time = job.next_run_time
|
||||
|
||||
# Paused reminders returns None
|
||||
if trigger_time is None:
|
||||
trigger_value = "Paused - can be resumed with '/remind resume'"
|
||||
else:
|
||||
trigger_value = f'{trigger_time.strftime("%Y-%m-%d %H:%M")} (in {calculate(job)})'
|
||||
|
||||
msg = f"**Resumed** {message} in #{channel_name}.\n**Time**: {trigger_value}\n"
|
||||
|
||||
await ctx.send(msg)
|
||||
await paginator.run()
|
||||
|
||||
|
||||
@slash.subcommand(
|
||||
base="remind",
|
||||
@base_command.subcommand(
|
||||
name="add",
|
||||
description="Set a reminder.",
|
||||
options=[
|
||||
create_option(
|
||||
Option(
|
||||
name="message_reason",
|
||||
description="The message I'm going to send you.",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
description="The message to send.",
|
||||
type=OptionType.STRING,
|
||||
required=True,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="message_date",
|
||||
description="Time and/or date when you want to get reminded.",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
description="The date to send the message.",
|
||||
type=OptionType.STRING,
|
||||
required=True,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="different_channel",
|
||||
description="Send the message to a different channel.",
|
||||
option_type=SlashCommandOptionType.CHANNEL,
|
||||
description="The channel to send the message to.",
|
||||
type=OptionType.CHANNEL,
|
||||
required=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def remind_add(
|
||||
ctx: SlashContext,
|
||||
message_date: str,
|
||||
async def command_add(
|
||||
ctx: interactions.CommandContext,
|
||||
message_reason: str,
|
||||
different_channel: discord.TextChannel = None,
|
||||
message_date: str,
|
||||
different_channel: interactions.Channel | None = None,
|
||||
):
|
||||
"""Add a new reminder. You can add a date and message.
|
||||
|
||||
@ -442,19 +179,25 @@ async def remind_add(
|
||||
message_reason: The message the bot should write when the reminder is triggered.
|
||||
different_channel: The channel the reminder should be sent to.
|
||||
"""
|
||||
await ctx.defer()
|
||||
|
||||
parsed_date = dateparser.parse(
|
||||
f"{message_date}",
|
||||
settings={
|
||||
"PREFER_DATES_FROM": "future",
|
||||
"TIMEZONE": f"{config_timezone}",
|
||||
"TO_TIMEZONE": f"{config_timezone}",
|
||||
},
|
||||
)
|
||||
|
||||
channel_id = ctx.channel.id
|
||||
if not parsed_date:
|
||||
await ctx.send("Could not parse the date.")
|
||||
return
|
||||
|
||||
channel_id = int(ctx.channel_id)
|
||||
|
||||
# If we should send the message to a different channel
|
||||
if different_channel:
|
||||
channel_id = different_channel.id
|
||||
channel_id = int(different_channel.id)
|
||||
|
||||
run_date = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
reminder = scheduler.add_job(
|
||||
@ -463,12 +206,12 @@ async def remind_add(
|
||||
kwargs={
|
||||
"channel_id": channel_id,
|
||||
"message": message_reason,
|
||||
"author_id": ctx.author_id,
|
||||
"author_id": ctx.member.id,
|
||||
},
|
||||
)
|
||||
|
||||
message = (
|
||||
f"Hello {ctx.author.display_name},"
|
||||
f"Hello {ctx.member.name},"
|
||||
f" I will notify you in <#{channel_id}> at:\n"
|
||||
f"**{run_date}** (in {calculate(reminder)})\n"
|
||||
f"With the message:\n**{message_reason}**."
|
||||
@ -477,113 +220,112 @@ async def remind_add(
|
||||
await ctx.send(message)
|
||||
|
||||
|
||||
@slash.subcommand(
|
||||
base="remind",
|
||||
@base_command.subcommand(
|
||||
name="cron",
|
||||
description="Triggers when current time matches all specified time constraints, similarly to the UNIX cron.",
|
||||
options=[
|
||||
create_option(
|
||||
Option(
|
||||
name="message_reason",
|
||||
description="The message I'm going to send you.",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=True,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="year",
|
||||
description="4-digit year. (Example: 2042)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="month",
|
||||
description="Month (1-12)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="day",
|
||||
description="Day of month (1-31)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="week",
|
||||
description="ISO week (1-53)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="day_of_week",
|
||||
description="Number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun). The first weekday is monday.",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="hour",
|
||||
description="Hour (0-23)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="minute",
|
||||
description="Minute (0-59)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="second",
|
||||
description="Second (0-59)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="start_date",
|
||||
description="Earliest possible time to trigger on, in the ISO 8601 format. (Example: 2010-10-10 09:30:00)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="end_date",
|
||||
description="Latest possible time to trigger on, in the ISO 8601 format. (Example: 2010-10-10 09:30:00)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="timezone",
|
||||
description="Time zone to use for the date/time calculations (defaults to scheduler timezone)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="jitter",
|
||||
description="Delay the job execution by x seconds at most. Adds a random component to the execution time.",
|
||||
option_type=SlashCommandOptionType.INTEGER,
|
||||
type=OptionType.INTEGER,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="different_channel",
|
||||
description="Send the messages to a different channel.",
|
||||
option_type=SlashCommandOptionType.CHANNEL,
|
||||
type=OptionType.CHANNEL,
|
||||
required=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def remind_cron(
|
||||
ctx: SlashContext,
|
||||
ctx: interactions.CommandContext,
|
||||
message_reason: str,
|
||||
year: int = None,
|
||||
month: int = None,
|
||||
day: int = None,
|
||||
week: int = None,
|
||||
day_of_week: str = None,
|
||||
hour: int = None,
|
||||
minute: int = None,
|
||||
second: int = None,
|
||||
start_date: str = None,
|
||||
end_date: str = None,
|
||||
timezone: str = None,
|
||||
jitter: int = None,
|
||||
different_channel: discord.TextChannel = None,
|
||||
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,
|
||||
):
|
||||
"""Create new cron job. Works like UNIX cron.
|
||||
|
||||
@ -607,11 +349,13 @@ async def remind_cron(
|
||||
jitter: Delay the job execution by jitter seconds at most.
|
||||
different_channel: Send the messages to a different channel.
|
||||
"""
|
||||
channel_id = ctx.channel.id
|
||||
await ctx.defer()
|
||||
|
||||
channel_id = int(ctx.channel_id)
|
||||
|
||||
# If we should send the message to a different channel
|
||||
if different_channel:
|
||||
channel_id = different_channel.id
|
||||
channel_id = int(different_channel.id)
|
||||
|
||||
job = scheduler.add_job(
|
||||
send_to_discord,
|
||||
@ -631,13 +375,13 @@ async def remind_cron(
|
||||
kwargs={
|
||||
"channel_id": channel_id,
|
||||
"message": message_reason,
|
||||
"author_id": ctx.author_id,
|
||||
"author_id": ctx.member.id,
|
||||
},
|
||||
)
|
||||
|
||||
# TODO: Add what arguments we used in the job to the message
|
||||
message = (
|
||||
f"Hello {ctx.author.display_name},"
|
||||
f"Hello {ctx.member.name},"
|
||||
f" I will send messages to <#{channel_id}>.\n"
|
||||
f"First run in {calculate(job)} with the message:\n"
|
||||
f"**{message_reason}**."
|
||||
@ -645,92 +389,91 @@ async def remind_cron(
|
||||
await ctx.send(message)
|
||||
|
||||
|
||||
@slash.subcommand(
|
||||
base="remind",
|
||||
@base_command.subcommand(
|
||||
name="interval",
|
||||
description="Schedules messages to be run periodically, on selected intervals.",
|
||||
options=[
|
||||
create_option(
|
||||
Option(
|
||||
name="message_reason",
|
||||
description="The message I'm going to send you.",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=True,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="weeks",
|
||||
description="Number of weeks to wait",
|
||||
option_type=SlashCommandOptionType.INTEGER,
|
||||
type=OptionType.INTEGER,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="days",
|
||||
description="Number of days to wait",
|
||||
option_type=SlashCommandOptionType.INTEGER,
|
||||
type=OptionType.INTEGER,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="hours",
|
||||
description="Number of hours to wait",
|
||||
option_type=SlashCommandOptionType.INTEGER,
|
||||
type=OptionType.INTEGER,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="minutes",
|
||||
description="Number of minutes to wait",
|
||||
option_type=SlashCommandOptionType.INTEGER,
|
||||
type=OptionType.INTEGER,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="seconds",
|
||||
description="Number of seconds to wait.",
|
||||
option_type=SlashCommandOptionType.INTEGER,
|
||||
type=OptionType.INTEGER,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="start_date",
|
||||
description="When to start, in the ISO 8601 format. (Example: 2010-10-10 09:30:00)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="end_date",
|
||||
description="When to stop, in the ISO 8601 format. (Example: 2014-06-15 11:00:00)",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="timezone",
|
||||
description="Time zone to use for the date/time calculations",
|
||||
option_type=SlashCommandOptionType.STRING,
|
||||
type=OptionType.STRING,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="jitter",
|
||||
description="Delay the job execution by x seconds at most. Adds a random component to the execution time.",
|
||||
option_type=SlashCommandOptionType.INTEGER,
|
||||
type=OptionType.INTEGER,
|
||||
required=False,
|
||||
),
|
||||
create_option(
|
||||
Option(
|
||||
name="different_channel",
|
||||
description="Send the messages to a different channel.",
|
||||
option_type=SlashCommandOptionType.CHANNEL,
|
||||
type=OptionType.CHANNEL,
|
||||
required=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def remind_interval(
|
||||
ctx: SlashContext,
|
||||
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,
|
||||
end_date: str = None,
|
||||
timezone: str = None,
|
||||
jitter: int = None,
|
||||
different_channel: discord.TextChannel = 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,
|
||||
):
|
||||
"""Create a new reminder that triggers based on an interval.
|
||||
|
||||
@ -748,8 +491,13 @@ async def remind_interval(
|
||||
jitter: Delay the job execution by jitter seconds at most.
|
||||
different_channel: Send the messages to a different channel.
|
||||
"""
|
||||
await ctx.defer()
|
||||
|
||||
channel_id = different_channel.id if different_channel else ctx.channel.id
|
||||
channel_id = int(ctx.channel_id)
|
||||
|
||||
# If we should send the message to a different channel
|
||||
if different_channel:
|
||||
channel_id = int(different_channel.id)
|
||||
|
||||
job = scheduler.add_job(
|
||||
send_to_discord,
|
||||
@ -766,13 +514,13 @@ async def remind_interval(
|
||||
kwargs={
|
||||
"channel_id": channel_id,
|
||||
"message": message_reason,
|
||||
"author_id": ctx.author_id,
|
||||
"author_id": ctx.member.id,
|
||||
},
|
||||
)
|
||||
|
||||
# TODO: Add what arguments we used in the job to the message
|
||||
message = (
|
||||
f"Hello {ctx.author.display_name}, I will send messages to <#{channel_id}>.\n"
|
||||
f"Hello {ctx.member.name}, I will send messages to <#{channel_id}>.\n"
|
||||
f"First run in {calculate(job)} with the message:\n"
|
||||
f"**{message_reason}**."
|
||||
)
|
||||
@ -789,7 +537,14 @@ async def send_to_discord(channel_id: int, message: str, author_id: int):
|
||||
author_id: User we should ping.
|
||||
"""
|
||||
# TODO: Check if channel exists.
|
||||
channel = bot.get_channel(int(channel_id))
|
||||
# TODO: Send message to webhook if channel is not found.
|
||||
channel = await interactions.get( # type: ignore
|
||||
bot,
|
||||
interactions.Channel,
|
||||
object_id=int(channel_id),
|
||||
force=interactions.Force.HTTP, # type: ignore
|
||||
)
|
||||
|
||||
await channel.send(f"<@{author_id}>\n{message}")
|
||||
|
||||
|
||||
@ -806,7 +561,7 @@ def start():
|
||||
)
|
||||
|
||||
scheduler.start()
|
||||
bot.run(bot_token)
|
||||
bot.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -7,7 +7,7 @@ from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(verbose=True)
|
||||
sqlite_location = os.getenv("SQLITE_LOCATION", default="/jobs.sqlite")
|
||||
config_timezone = os.getenv("TIMEZONE", default="Europe/Stockholm")
|
||||
config_timezone = os.getenv("TIMEZONE", default="UTC")
|
||||
bot_token = os.getenv("BOT_TOKEN", default="")
|
||||
log_level = os.getenv(key="LOG_LEVEL", default="INFO")
|
||||
|
||||
|
1221
poetry.lock
generated
1221
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "discord-reminder-bot"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
description = "Discord bot that allows you to set date, cron and interval reminders."
|
||||
authors = ["Joakim Hellsén <tlovinator@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@ -22,19 +22,24 @@ bot = "discord_reminder_bot.main:start"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
"discord.py" = "^1.7.3"
|
||||
python-dotenv = "^0.20.0"
|
||||
discord-py-slash-command = "^3.0.3"
|
||||
python-dotenv = "^0.21.0"
|
||||
APScheduler = "^3.9.1"
|
||||
dateparser = "^1.1.1"
|
||||
SQLAlchemy = "^1.4.32"
|
||||
SQLAlchemy = "^1.4.41"
|
||||
discord-py-interactions = { git = "https://github.com/interactions-py/library.git", rev = "unstable" }
|
||||
interactions-wait-for = "^1.0.6"
|
||||
dinteractions-paginator = { git = "https://github.com/interactions-py/paginator.git", rev = "unstable" }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.1.2"
|
||||
mypy = "^0.971"
|
||||
types-dateparser = "^1.1.4"
|
||||
types-pytz = "^2022.1.2"
|
||||
black = "^22.8.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
@ -3,6 +3,7 @@
|
||||
Jobs are stored in memory.
|
||||
"""
|
||||
import re
|
||||
from sched import scheduler
|
||||
|
||||
import dateparser
|
||||
import pytz
|
||||
@ -17,7 +18,8 @@ class TestCountdown:
|
||||
"""This tests everything.
|
||||
|
||||
This sets up sqlite database in memory, changes scheduler timezone
|
||||
to Europe/Stockholm and creates job that runs January 18 2040.
|
||||
to Europe/Stockholm and creates job that runs January 18 2040 and one that
|
||||
runs at 00:00.
|
||||
"""
|
||||
|
||||
jobstores = {"default": SQLAlchemyJobStore(url="sqlite:///:memory")}
|
||||
@ -35,8 +37,7 @@ class TestCountdown:
|
||||
"TO_TIMEZONE": "Europe/Stockholm",
|
||||
},
|
||||
)
|
||||
|
||||
run_date = parsed_date.strftime("%Y-%m-%d %H:%M:%S") # type: ignore
|
||||
run_date = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
job = scheduler.add_job(
|
||||
send_to_discord,
|
||||
run_date=run_date,
|
||||
@ -47,9 +48,57 @@ class TestCountdown:
|
||||
},
|
||||
)
|
||||
|
||||
timezone_date = dateparser.parse(
|
||||
"00:00",
|
||||
settings={
|
||||
"PREFER_DATES_FROM": "future",
|
||||
"TO_TIMEZONE": "Europe/Stockholm",
|
||||
},
|
||||
)
|
||||
timezone_run_date = timezone_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
timezone_job = scheduler.add_job(
|
||||
send_to_discord,
|
||||
run_date=timezone_run_date,
|
||||
kwargs={
|
||||
"channel_id": 865712621109772329,
|
||||
"message": "Running PyTest at 00:00",
|
||||
"author_id": 126462229892694018,
|
||||
},
|
||||
)
|
||||
|
||||
timezone_date2 = dateparser.parse(
|
||||
"13:37",
|
||||
settings={
|
||||
"PREFER_DATES_FROM": "future",
|
||||
"TO_TIMEZONE": "Europe/Stockholm",
|
||||
},
|
||||
)
|
||||
timezone_run_date2 = timezone_date2.strftime("%Y-%m-%d %H:%M:%S")
|
||||
timezone_job2 = scheduler.add_job(
|
||||
send_to_discord,
|
||||
run_date=timezone_run_date2,
|
||||
kwargs={
|
||||
"channel_id": 865712621109772329,
|
||||
"message": "Running PyTest at 13:37",
|
||||
"author_id": 126462229892694018,
|
||||
},
|
||||
)
|
||||
|
||||
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):
|
||||
"""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
|
||||
assert time_job2.trigger.run_date.second == 0
|
||||
|
Reference in New Issue
Block a user