From 9d9490f07a72b5c72365d211925f27d6faa941f5 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Sat, 24 Jul 2021 18:51:12 -0600 Subject: [PATCH] Add RemindmeCog, closes #26 --- jarvis/__init__.py | 4 +- jarvis/cogs/remindme.py | 365 ++++++++++++++++++++++++++++++++++++++++ jarvis/db/types.py | 25 ++- 3 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 jarvis/cogs/remindme.py diff --git a/jarvis/__init__.py b/jarvis/__init__.py index c552525..65539e5 100644 --- a/jarvis/__init__.py +++ b/jarvis/__init__.py @@ -15,13 +15,13 @@ from jarvis import logo, utils from jarvis.config import get_config from jarvis.db import DBManager from jarvis.db.types import ( + Autopurge, Autoreact, Ban, Lock, Mute, Setting, Warning, - Autopurge, ) from jarvis.utils import build_embed from jarvis.utils.field import Field @@ -44,7 +44,7 @@ jarvis = commands.Bot( ) slash = SlashCommand(jarvis, sync_commands=True, sync_on_cog_reload=True) jarvis_self = Process() -__version__ = "1.4.1" +__version__ = "1.5.0" db = DBManager(get_config().mongo).mongo diff --git a/jarvis/cogs/remindme.py b/jarvis/cogs/remindme.py new file mode 100644 index 0000000..df0cc58 --- /dev/null +++ b/jarvis/cogs/remindme.py @@ -0,0 +1,365 @@ +import asyncio +import re +from datetime import datetime, timedelta +from typing import Optional + +from bson import ObjectId +from ButtonPaginator import Paginator +from discord.ext import commands +from discord.ext.tasks import loop +from discord.utils import find +from discord_slash import SlashContext, cog_ext +from discord_slash.model import ButtonStyle +from discord_slash.utils.manage_commands import create_option +from discord_slash.utils.manage_components import ( + create_actionrow, + create_select, + create_select_option, + wait_for_component, +) + +from jarvis.db.types import Reminder +from jarvis.utils import build_embed +from jarvis.utils.field import Field + +valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*") +invites = re.compile( + r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", + flags=re.IGNORECASE, +) + + +class RemindmeCog(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.cache = {} + self._remind.start() + + def check_cache(self, ctx: SlashContext, **kwargs): + if not kwargs: + kwargs = {} + return find( + lambda x: x["command"] == ctx.subcommand_name + and x["user"] == ctx.author.id + and x["guild"] == ctx.guild.id + and all(x[k] == v for k, v in kwargs.items()), + self.cache.values(), + ) + + @cog_ext.cog_slash( + name="remindme", + description="Set a reminder", + options=[ + create_option( + name="message", + description="What to remind you of", + option_type=3, + required=True, + ), + create_option( + name="weeks", + description="Number of weeks?", + option_type=4, + required=False, + ), + create_option( + name="days", + description="Number of days?", + option_type=4, + required=False, + ), + create_option( + name="hours", + description="Number of hours?", + option_type=4, + required=False, + ), + create_option( + name="minutes", + description="Number of minutes?", + option_type=4, + required=False, + ), + ], + ) + async def _remindme( + self, + ctx: SlashContext, + message: Optional[str] = None, + weeks: Optional[int] = 0, + days: Optional[int] = 0, + hours: Optional[int] = 0, + minutes: Optional[int] = 0, + ): + if len(message) > 100: + await ctx.send("Reminder cannot be > 100 characters.", hidden=True) + return + elif invites.search(message): + await ctx.send( + "Listen, don't use this to try and bypass the rules", + hidden=True, + ) + return + elif not valid.fullmatch(message): + await ctx.send( + "Hey, you should probably make this readable", hidden=True + ) + return + + if not any([weeks, days, hours, minutes]): + await ctx.send("At least one time period is required", hidden=True) + return + + weeks = abs(weeks) + days = abs(days) + hours = abs(hours) + minutes = abs(minutes) + + if weeks and weeks > 4: + await ctx.send("Cannot be farther than 4 weeks out!", hidden=True) + return + + elif days and days > 6: + await ctx.send( + "Use weeks instead of 7+ days, please.", hidden=True + ) + return + + elif hours and hours > 23: + await ctx.send( + "Use days instead of 24+ hours, please.", hidden=True + ) + return + + elif minutes and minutes > 59: + await ctx.send( + "Use hours instead of 59+ minutes, please.", hidden=True + ) + return + + reminders = Reminder.get_active(user=ctx.author.id) + if len(reminders) >= 5: + await ctx.send( + "You already have 5 (or more) active reminders. " + + "Please either remove an old one, or wait for one to pass", + hidden=True, + ) + return + + remind_at = datetime.utcnow() + timedelta( + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + ) + + _ = Reminder( + user=ctx.author_id, + channel=ctx.channel.id, + guild=ctx.guild.id, + message=message, + remind_at=remind_at, + active=True, + ).insert() + + embed = build_embed( + title="Reminder Set", + description=f"{ctx.author.mention} set a reminder", + fields=[ + Field(name="Message", value=message), + Field( + name="When", + value=remind_at.strftime("%Y-%m-%d %H:%M UTC"), + inline=False, + ), + ], + ) + + embed.set_author( + name=ctx.author.name + "#" + ctx.author.discriminator, + icon_url=ctx.author.avatar_url, + ) + embed.set_thumbnail(url=ctx.author.avatar_url) + + await ctx.send(embed=embed) + + async def get_reminders_embed(self, ctx, reminders): + fields = [] + for reminder in reminders: + fields.append( + Field( + name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), + value=f"{reminder.message}\n\u200b", + inline=False, + ) + ) + + embed = build_embed( + title=f"{len(reminders)} Active Reminder(s)", + description="All active reminders for " + f"{ctx.author.mention}", + fields=fields, + ) + + embed.set_author( + name=ctx.author.name + "#" + ctx.author.discriminator, + icon_url=ctx.author.avatar_url, + ) + embed.set_thumbnail(url=ctx.author.avatar_url) + + return embed + + @cog_ext.cog_subcommand( + base="reminders", + name="list", + description="List reminders for a user", + ) + async def _list(self, ctx: SlashContext): + exists = self.check_cache(ctx) + if exists: + await ctx.defer(hidden=True) + await ctx.send( + "Please use existing interaction: " + + f"{exists['paginator']._message.jump_url}", + hidden=True, + ) + return + reminders = Reminder.get_active(user=ctx.author.id) + if not reminders: + await ctx.send("You have no reminders set.", hidden=True) + return + + embed = await self.get_reminders_embed(ctx, reminders) + + await ctx.send(embed=embed) + + @cog_ext.cog_subcommand( + base="reminders", + name="delete", + description="Delete a reminder", + ) + async def _delete(self, ctx: SlashContext): + reminders = Reminder.get_active(user=ctx.author.id) + if not reminders: + await ctx.send("You have no reminders set", hidden=True) + return + + options = [] + for reminder in reminders: + option = create_select_option( + label=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), + value=str(reminder._id), + emoji="⏰", + ) + options.append(option) + + select = create_select( + options=options, + custom_id="to_delete", + placeholder="Select reminders to delete", + min_values=1, + max_values=len(reminders), + ) + + components = [create_actionrow(select)] + embed = await self.get_reminders_embed(ctx, reminders) + message = await ctx.send( + content=f"You have {len(reminders)} reminder(s) set:", + embed=embed, + components=components, + ) + + try: + context = await wait_for_component( + self.bot, + check=lambda x: ctx.author.id == x.author_id, + messages=message, + timeout=60 * 5, + ) + for to_delete in context.selected_options: + _ = Reminder.get( + user=ctx.author.id, _id=ObjectId(to_delete) + ).delete() + + for row in components: + for component in row["components"]: + component["disabled"] = True + + fields = [] + for reminder in filter( + lambda x: str(x._id) in context.selected_options, reminders + ): + fields.append( + Field( + name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), + value=reminder.message, + inline=False, + ) + ) + embed = build_embed( + title="Deleted Reminder(s)", + description="", + fields=fields, + ) + + embed.set_author( + name=ctx.author.name + "#" + ctx.author.discriminator, + icon_url=ctx.author.avatar_url, + ) + embed.set_thumbnail(url=ctx.author.avatar_url) + + await context.edit_origin( + content=f"Deleted {len(context.selected_options)} reminder(s)", + components=components, + embed=embed, + ) + except asyncio.TimeoutError: + for row in components: + for component in row["components"]: + component["disabled"] = True + await message.edit(components=components) + + @loop(minutes=1) + async def _expire_interaction(self): + keys = list(self.cache.keys()) + for key in keys: + if self.cache[key]["timeout"] <= datetime.utcnow() + timedelta( + minutes=1 + ): + del self.cache[key] + + @loop(seconds=15) + async def _remind(self): + reminders = Reminder.get_active( + remind_at={"$lt": datetime.utcnow() + timedelta(seconds=30)} + ) + for reminder in reminders: + if reminder.remind_at <= datetime.utcnow(): + user = await self.bot.fetch_user(reminder.user) + if not user: + reminder.delete() + continue + embed = build_embed( + title="You have a reminder", + description=reminder.message, + fields=[], + ) + embed.set_author( + name=user.name + "#" + user.discriminator, + icon_url=user.avatar_url, + ) + embed.set_thumbnail(url=user.avatar_url) + try: + await user.send(embed=embed) + except: + guild = self.bot.fetch_guild(reminder.guild) + channel = ( + guild.get_channel(reminder.channel) if guild else None + ) + if channel: + await channel.send(f"{user.mention}", embed=embed) + finally: + reminder.delete() + + +def setup(bot): + bot.add_cog(RemindmeCog(bot)) diff --git a/jarvis/db/types.py b/jarvis/db/types.py index 965055a..d70094e 100644 --- a/jarvis/db/types.py +++ b/jarvis/db/types.py @@ -5,7 +5,6 @@ from typing import Any, Optional from bson import ObjectId from pymongo import ASCENDING, DESCENDING -from pymongo.collection import Collection from jarvis.config import get_config from jarvis.db import DBManager @@ -28,6 +27,7 @@ coll_lookup = { "Kick": "kicks", "Mute": "mutes", "Purge": "purges", + "Reminder": "reminders", "Setting": "settings", "Starboard": "starboard", "Star": "stars", @@ -437,6 +437,29 @@ class Purge(MongoObject): created_at: datetime = field(default_factory=datetime.utcnow) +@dataclass +class Reminder(MongoObject, ActiveObject): + """ + User Reminder object + + :param _id: MongoDB ID + :param user: User who wanted reminded + :param channel: ID of origin channel + :param guild: ID of origin guild + :param remind_at: Datetime delay to send message + :param active: If reminder is active + :param message: Reminder message + :param created_at: Time the reminder was created + """ + + user: int + channel: int + guild: int + remind_at: datetime + message: str + created_at: datetime = field(default_factory=datetime.utcnow) + + @dataclass class Setting(MongoObject): """