aboutsummaryrefslogtreecommitdiffstats
path: root/bot/cogs/reminders.py
diff options
context:
space:
mode:
authorGravatar Matteo <[email protected]>2020-03-01 15:44:08 +0100
committerGravatar Matteo <[email protected]>2020-03-01 15:44:08 +0100
commit938b97a037665f871aac33f2e8f8a9a84047e337 (patch)
tree6bb47228977a6c4aa11fba56a39f2582182024e0 /bot/cogs/reminders.py
parentMake sure that the offensive message deletion date returned by the API is naive (diff)
parentMerge pull request #806 from python-discord/feat/frontend/b000/coloured-logs (diff)
Merge branch 'master' into #364-offensive-msg-autodeletion
Diffstat (limited to '')
-rw-r--r--bot/cogs/reminders.py135
1 files changed, 92 insertions, 43 deletions
diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py
index 45bf9a8f4..24c279357 100644
--- a/bot/cogs/reminders.py
+++ b/bot/cogs/reminders.py
@@ -2,16 +2,17 @@ import asyncio
import logging
import random
import textwrap
+import typing as t
from datetime import datetime, timedelta
from operator import itemgetter
-from typing import Optional
+import discord
+from dateutil.parser import isoparse
from dateutil.relativedelta import relativedelta
-from discord import Colour, Embed, Message
from discord.ext.commands import Cog, Context, group
from bot.bot import Bot
-from bot.constants import Channels, Icons, NEGATIVE_REPLIES, POSITIVE_REPLIES, STAFF_ROLES
+from bot.constants import Guild, Icons, NEGATIVE_REPLIES, POSITIVE_REPLIES, STAFF_ROLES
from bot.converters import Duration
from bot.pagination import LinePaginator
from bot.utils.checks import without_role_check
@@ -20,7 +21,7 @@ from bot.utils.time import humanize_delta, wait_until
log = logging.getLogger(__name__)
-WHITELISTED_CHANNELS = (Channels.bot,)
+WHITELISTED_CHANNELS = Guild.reminder_whitelist
MAXIMUM_REMINDERS = 5
@@ -35,39 +36,73 @@ class Reminders(Scheduler, Cog):
async def reschedule_reminders(self) -> None:
"""Get all current reminders from the API and reschedule them."""
- await self.bot.wait_until_ready()
+ await self.bot.wait_until_guild_available()
response = await self.bot.api_client.get(
'bot/reminders',
params={'active': 'true'}
)
now = datetime.utcnow()
- loop = asyncio.get_event_loop()
for reminder in response:
- remind_at = datetime.fromisoformat(reminder['expiration'][:-1])
+ is_valid, *_ = self.ensure_valid_reminder(reminder, cancel_task=False)
+ if not is_valid:
+ continue
+
+ remind_at = isoparse(reminder['expiration']).replace(tzinfo=None)
# If the reminder is already overdue ...
if remind_at < now:
late = relativedelta(now, remind_at)
await self.send_reminder(reminder, late)
-
else:
- self.schedule_task(loop, reminder["id"], reminder)
+ self.schedule_task(reminder["id"], reminder)
+
+ def ensure_valid_reminder(
+ self,
+ reminder: dict,
+ cancel_task: bool = True
+ ) -> t.Tuple[bool, discord.User, discord.TextChannel]:
+ """Ensure reminder author and channel can be fetched otherwise delete the reminder."""
+ user = self.bot.get_user(reminder['author'])
+ channel = self.bot.get_channel(reminder['channel_id'])
+ is_valid = True
+ if not user or not channel:
+ is_valid = False
+ log.info(
+ f"Reminder {reminder['id']} invalid: "
+ f"User {reminder['author']}={user}, Channel {reminder['channel_id']}={channel}."
+ )
+ asyncio.create_task(self._delete_reminder(reminder['id'], cancel_task))
+
+ return is_valid, user, channel
@staticmethod
- async def _send_confirmation(ctx: Context, on_success: str) -> None:
+ async def _send_confirmation(
+ ctx: Context,
+ on_success: str,
+ reminder_id: str,
+ delivery_dt: t.Optional[datetime],
+ ) -> None:
"""Send an embed confirming the reminder change was made successfully."""
- embed = Embed()
- embed.colour = Colour.green()
+ embed = discord.Embed()
+ embed.colour = discord.Colour.green()
embed.title = random.choice(POSITIVE_REPLIES)
embed.description = on_success
+
+ footer_str = f"ID: {reminder_id}"
+ if delivery_dt:
+ # Reminder deletion will have a `None` `delivery_dt`
+ footer_str = f"{footer_str}, Due: {delivery_dt.strftime('%Y-%m-%dT%H:%M:%S')}"
+
+ embed.set_footer(text=footer_str)
+
await ctx.send(embed=embed)
async def _scheduled_task(self, reminder: dict) -> None:
"""A coroutine which sends the reminder once the time is reached, and cancels the running task."""
reminder_id = reminder["id"]
- reminder_datetime = datetime.fromisoformat(reminder['expiration'][:-1])
+ reminder_datetime = isoparse(reminder['expiration']).replace(tzinfo=None)
# Send the reminder message once the desired duration has passed
await wait_until(reminder_datetime)
@@ -76,30 +111,30 @@ class Reminders(Scheduler, Cog):
log.debug(f"Deleting reminder {reminder_id} (the user has been reminded).")
await self._delete_reminder(reminder_id)
- # Now we can begone with it from our schedule list.
- self.cancel_task(reminder_id)
-
- async def _delete_reminder(self, reminder_id: str) -> None:
+ async def _delete_reminder(self, reminder_id: str, cancel_task: bool = True) -> None:
"""Delete a reminder from the database, given its ID, and cancel the running task."""
await self.bot.api_client.delete('bot/reminders/' + str(reminder_id))
- # Now we can remove it from the schedule list
- self.cancel_task(reminder_id)
+ if cancel_task:
+ # Now we can remove it from the schedule list
+ self.cancel_task(reminder_id)
async def _reschedule_reminder(self, reminder: dict) -> None:
"""Reschedule a reminder object."""
- loop = asyncio.get_event_loop()
-
+ log.trace(f"Cancelling old task #{reminder['id']}")
self.cancel_task(reminder["id"])
- self.schedule_task(loop, reminder["id"], reminder)
+
+ log.trace(f"Scheduling new task #{reminder['id']}")
+ self.schedule_task(reminder["id"], reminder)
async def send_reminder(self, reminder: dict, late: relativedelta = None) -> None:
"""Send the reminder."""
- channel = self.bot.get_channel(reminder["channel_id"])
- user = self.bot.get_user(reminder["author"])
+ is_valid, user, channel = self.ensure_valid_reminder(reminder)
+ if not is_valid:
+ return
- embed = Embed()
- embed.colour = Colour.blurple()
+ embed = discord.Embed()
+ embed.colour = discord.Colour.blurple()
embed.set_author(
icon_url=Icons.remind_blurple,
name="It has arrived!"
@@ -111,7 +146,7 @@ class Reminders(Scheduler, Cog):
embed.description += f"\n[Jump back to when you created the reminder]({reminder['jump_url']})"
if late:
- embed.colour = Colour.red()
+ embed.colour = discord.Colour.red()
embed.set_author(
icon_url=Icons.remind_red,
name=f"Sorry it arrived {humanize_delta(late, max_units=2)} late!"
@@ -129,20 +164,20 @@ class Reminders(Scheduler, Cog):
await ctx.invoke(self.new_reminder, expiration=expiration, content=content)
@remind_group.command(name="new", aliases=("add", "create"))
- async def new_reminder(self, ctx: Context, expiration: Duration, *, content: str) -> Optional[Message]:
+ async def new_reminder(self, ctx: Context, expiration: Duration, *, content: str) -> t.Optional[discord.Message]:
"""
Set yourself a simple reminder.
Expiration is parsed per: http://strftime.org/
"""
- embed = Embed()
+ embed = discord.Embed()
# If the user is not staff, we need to verify whether or not to make a reminder at all.
if without_role_check(ctx, *STAFF_ROLES):
# If they don't have permission to set a reminder in this channel
if ctx.channel.id not in WHITELISTED_CHANNELS:
- embed.colour = Colour.red()
+ embed.colour = discord.Colour.red()
embed.title = random.choice(NEGATIVE_REPLIES)
embed.description = "Sorry, you can't do that here!"
@@ -159,7 +194,7 @@ class Reminders(Scheduler, Cog):
# Let's limit this, so we don't get 10 000
# reminders from kip or something like that :P
if len(active_reminders) > MAXIMUM_REMINDERS:
- embed.colour = Colour.red()
+ embed.colour = discord.Colour.red()
embed.title = random.choice(NEGATIVE_REPLIES)
embed.description = "You have too many active reminders!"
@@ -178,18 +213,20 @@ class Reminders(Scheduler, Cog):
)
now = datetime.utcnow() - timedelta(seconds=1)
+ humanized_delta = humanize_delta(relativedelta(expiration, now))
# Confirm to the user that it worked.
await self._send_confirmation(
ctx,
- on_success=f"Your reminder will arrive in {humanize_delta(relativedelta(expiration, now))}!"
+ on_success=f"Your reminder will arrive in {humanized_delta}!",
+ reminder_id=reminder["id"],
+ delivery_dt=expiration,
)
- loop = asyncio.get_event_loop()
- self.schedule_task(loop, reminder["id"], reminder)
+ self.schedule_task(reminder["id"], reminder)
@remind_group.command(name="list")
- async def list_reminders(self, ctx: Context) -> Optional[Message]:
+ async def list_reminders(self, ctx: Context) -> t.Optional[discord.Message]:
"""View a paginated embed of all reminders for your user."""
# Get all the user's reminders from the database.
data = await self.bot.api_client.get(
@@ -212,7 +249,7 @@ class Reminders(Scheduler, Cog):
for content, remind_at, id_ in reminders:
# Parse and humanize the time, make it pretty :D
- remind_datetime = datetime.fromisoformat(remind_at[:-1])
+ remind_datetime = isoparse(remind_at).replace(tzinfo=None)
time = humanize_delta(relativedelta(remind_datetime, now))
text = textwrap.dedent(f"""
@@ -222,8 +259,8 @@ class Reminders(Scheduler, Cog):
lines.append(text)
- embed = Embed()
- embed.colour = Colour.blurple()
+ embed = discord.Embed()
+ embed.colour = discord.Colour.blurple()
embed.title = f"Reminders for {ctx.author}"
# Remind the user that they have no reminders :^)
@@ -232,7 +269,7 @@ class Reminders(Scheduler, Cog):
return await ctx.send(embed=embed)
# Construct the embed and paginate it.
- embed.colour = Colour.blurple()
+ embed.colour = discord.Colour.blurple()
await LinePaginator.paginate(
lines,
@@ -261,7 +298,10 @@ class Reminders(Scheduler, Cog):
# Send a confirmation message to the channel
await self._send_confirmation(
- ctx, on_success="That reminder has been edited successfully!"
+ ctx,
+ on_success="That reminder has been edited successfully!",
+ reminder_id=id_,
+ delivery_dt=expiration,
)
await self._reschedule_reminder(reminder)
@@ -275,18 +315,27 @@ class Reminders(Scheduler, Cog):
json={'content': content}
)
+ # Parse the reminder expiration back into a datetime for the confirmation message
+ expiration = isoparse(reminder['expiration']).replace(tzinfo=None)
+
# Send a confirmation message to the channel
await self._send_confirmation(
- ctx, on_success="That reminder has been edited successfully!"
+ ctx,
+ on_success="That reminder has been edited successfully!",
+ reminder_id=id_,
+ delivery_dt=expiration,
)
await self._reschedule_reminder(reminder)
- @remind_group.command("delete", aliases=("remove",))
+ @remind_group.command("delete", aliases=("remove", "cancel"))
async def delete_reminder(self, ctx: Context, id_: int) -> None:
"""Delete one of your active reminders."""
await self._delete_reminder(id_)
await self._send_confirmation(
- ctx, on_success="That reminder has been deleted successfully!"
+ ctx,
+ on_success="That reminder has been deleted successfully!",
+ reminder_id=id_,
+ delivery_dt=None,
)