From eaa19de0502b227f31ffcf239ca7dc0546c4ba00 Mon Sep 17 00:00:00 2001 From: zevaryx Date: Sat, 17 Sep 2022 23:53:47 +0000 Subject: [PATCH 01/23] Fix breaking bug in event logging on message edits --- jarvis/client/events/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jarvis/client/events/message.py b/jarvis/client/events/message.py index c268a9c..477ba60 100644 --- a/jarvis/client/events/message.py +++ b/jarvis/client/events/message.py @@ -317,7 +317,7 @@ class MessageEventMixin: if not after.author.bot: modlog = await Setting.find_one(q(guild=after.guild.id, setting="activitylog")) ignore = await Setting.find_one(q(guild=after.guild.id, setting="log_ignore")) - if modlog and (ignore and after.channel.id not in ignore.value): + if modlog and (not ignore or (ignore and after.channel.id not in ignore.value)): if not before or before.content == after.content or before.content is None: return try: From c1806dffcfdf45ea81d50acce23dafc5c0ae7028 Mon Sep 17 00:00:00 2001 From: zevaryx Date: Sat, 17 Sep 2022 23:57:42 +0000 Subject: [PATCH 02/23] Propogate fix to message delete --- jarvis/client/events/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jarvis/client/events/message.py b/jarvis/client/events/message.py index 477ba60..7fa5a9b 100644 --- a/jarvis/client/events/message.py +++ b/jarvis/client/events/message.py @@ -370,7 +370,7 @@ class MessageEventMixin: message = event.message modlog = await Setting.find_one(q(guild=message.guild.id, setting="activitylog")) ignore = await Setting.find_one(q(guild=message.guild.id, setting="log_ignore")) - if modlog and (ignore and message.channel.id not in ignore.value): + if modlog and (not ignore or (ignore and message.channel.id not in ignore.value)): try: content = message.content or "N/A" except AttributeError: From 46d42114ba818b499b1cbccb0df8f216bf8c6a7e Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Mon, 3 Oct 2022 20:04:30 -0600 Subject: [PATCH 03/23] Move to static embeds in own package --- jarvis/client/events/message.py | 57 +++------ jarvis/cogs/admin/ban.py | 66 ++++------ jarvis/cogs/admin/kick.py | 32 +---- jarvis/cogs/admin/mute.py | 35 ++---- jarvis/cogs/admin/warning.py | 4 +- jarvis/cogs/reddit.py | 13 +- jarvis/cogs/remindme.py | 12 +- jarvis/cogs/rolegiver.py | 6 - jarvis/embeds/admin.py | 208 ++++++++++++++++++++++++++++++++ jarvis/utils/embeds.py | 26 ---- 10 files changed, 272 insertions(+), 187 deletions(-) create mode 100644 jarvis/embeds/admin.py delete mode 100644 jarvis/utils/embeds.py diff --git a/jarvis/client/events/message.py b/jarvis/client/events/message.py index 7fa5a9b..01b04df 100644 --- a/jarvis/client/events/message.py +++ b/jarvis/client/events/message.py @@ -26,9 +26,7 @@ class MessageEventMixin: autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id)) if autopurge: if not message.author.has_permission(Permissions.ADMINISTRATOR): - self.logger.debug( - f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}" - ) + self.logger.debug(f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}") await message.delete(delay=autopurge.delay) async def autoreact(self, message: Message) -> None: @@ -40,9 +38,7 @@ class MessageEventMixin: ) ) if autoreact: - self.logger.debug( - f"Autoreacting to message {message.guild.id}/{message.channel.id}/{message.id}" - ) + self.logger.debug(f"Autoreacting to message {message.guild.id}/{message.channel.id}/{message.id}") for reaction in autoreact.reactions: await message.add_reaction(reaction) if autoreact.thread: @@ -92,9 +88,7 @@ class MessageEventMixin: reason="Sent an invite link", user=message.author.id, ).commit() - tracker = warnings_tracker.labels( - guild_id=message.guild.id, guild_name=message.guild.name - ) + tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() embed = warning_embed(message.author, "Sent an invite link") try: @@ -118,9 +112,7 @@ class MessageEventMixin: - (1 if message.author.id in message._mention_ids else 0) # noqa: W503 > massmention.value # noqa: W503 ): - self.logger.debug( - f"Massmention threshold on {message.guild.id}/{message.channel.id}/{message.id}" - ) + self.logger.debug(f"Massmention threshold on {message.guild.id}/{message.channel.id}/{message.id}") expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) await Warning( active=True, @@ -131,9 +123,7 @@ class MessageEventMixin: reason="Mass Mention", user=message.author.id, ).commit() - tracker = warnings_tracker.labels( - guild_id=message.guild.id, guild_name=message.guild.name - ) + tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() embed = warning_embed(message.author, "Mass Mention") try: @@ -186,9 +176,7 @@ class MessageEventMixin: break if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass: - self.logger.debug( - f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}" - ) + self.logger.debug(f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}") expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) await Warning( active=True, @@ -199,9 +187,7 @@ class MessageEventMixin: reason="Pinged a blocked role/user with a blocked role", user=message.author.id, ).commit() - tracker = warnings_tracker.labels( - guild_id=message.guild.id, guild_name=message.guild.name - ) + tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role") try: @@ -226,9 +212,7 @@ class MessageEventMixin: reason="Phishing URL", user=message.author.id, ).commit() - tracker = warnings_tracker.labels( - guild_id=message.guild.id, guild_name=message.guild.name - ) + tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() embed = warning_embed(message.author, "Phishing URL") try: @@ -239,9 +223,7 @@ class MessageEventMixin: await message.delete() except Exception: self.logger.warn("Failed to delete malicious message") - tracker = malicious_tracker.labels( - guild_id=message.guild.id, guild_name=message.guild.name - ) + tracker = malicious_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() return True return False @@ -275,9 +257,7 @@ class MessageEventMixin: reason="Unsafe URL", user=message.author.id, ).commit() - tracker = warnings_tracker.labels( - guild_id=message.guild.id, guild_name=message.guild.name - ) + tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() reasons = ", ".join(f"{m['source']}: {m['type']}" for m in data["matches"]) embed = warning_embed(message.author, reasons) @@ -289,9 +269,7 @@ class MessageEventMixin: await message.delete() except Exception: self.logger.warn("Failed to delete malicious message") - tracker = malicious_tracker.labels( - guild_id=message.guild.id, guild_name=message.guild.name - ) + tracker = malicious_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() return True return False @@ -347,9 +325,7 @@ class MessageEventMixin: icon_url=after.author.display_avatar.url, url=after.jump_url, ) - embed.set_footer( - text=f"{after.author.username}#{after.author.discriminator} | {after.author.id}" - ) + embed.set_footer(text=f"{after.author.username}#{after.author.discriminator} | {after.author.id}") await channel.send(embeds=embed) except Exception as e: self.logger.warning( @@ -422,13 +398,8 @@ class MessageEventMixin: url=message.jump_url, ) embed.set_footer( - text=( - f"{message.author.username}#{message.author.discriminator} | " - f"{message.author.id}" - ) + text=(f"{message.author.username}#{message.author.discriminator} | " f"{message.author.id}") ) await channel.send(embeds=embed) except Exception as e: - self.logger.warning( - f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}" - ) + self.logger.warning(f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}") diff --git a/jarvis/cogs/admin/ban.py b/jarvis/cogs/admin/ban.py index 9d71422..4ea06d5 100644 --- a/jarvis/cogs/admin/ban.py +++ b/jarvis/cogs/admin/ban.py @@ -18,6 +18,7 @@ from naff.models.naff.application_commands import ( from naff.models.naff.command import check from jarvis.branding import get_command_color +from jarvis.embeds.admin import ban_embed, unban_embed from jarvis.utils import build_embed from jarvis.utils.cogs import ModcaseCog from jarvis.utils.permissions import admin_or_permissions @@ -33,7 +34,6 @@ class BanCog(ModcaseCog): user: User, duration: int, active: bool, - fields: list, mtype: str, ) -> None: """Apply a Discord ban.""" @@ -51,20 +51,15 @@ class BanCog(ModcaseCog): ) await b.commit() - embed = build_embed( - title="User Banned", - description=f"Reason: {reason}", - fields=fields, - color=get_command_color("ban"), + embed = ban_embed( + user=user, + admin=ctx.author, + reason=reason, + guild=ctx.guild, + duration=duration, + type=mtype, ) - embed.set_author( - name=user.display_name, - icon_url=user.avatar.url, - ) - embed.set_thumbnail(url=user.avatar.url) - embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") - await ctx.send(embeds=embed) async def discord_apply_unban(self, ctx: InteractionContext, user: User, reason: str) -> None: @@ -80,18 +75,8 @@ class BanCog(ModcaseCog): ) await u.commit() - embed = build_embed( - title="User Unbanned", - description=f"<@{user.id}> was unbanned", - fields=[EmbedField(name="Reason", value=reason)], - color=get_command_color("unban"), - ) - embed.set_author( - name=user.username, - icon_url=user.avatar.url, - ) - embed.set_thumbnail(url=user.avatar.url) - embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + embed = unban_embed(user=user, admin=ctx.author, reason=reason) + await ctx.send(embeds=embed) @slash_command(name="ban", description="Ban a user") @@ -152,45 +137,36 @@ class BanCog(ModcaseCog): if mtype == "temp": user_message += f"\nDuration: {duration} hours" - fields = [EmbedField(name="Type", value=mtype)] - - if mtype == "temp": - fields.append(EmbedField(name="Duration", value=f"{duration} hour(s)")) - - user_embed = build_embed( - title=f"You have been banned from {ctx.guild.name}", - description=f"Reason: {reason}", - fields=fields, - color=get_command_color("ban"), + user_embed = ban_embed( + user=user, + admin=ctx.author, + reason=reason, + type=mtype, + guild=ctx.guild, + duration=duration, + dm=True, ) - user_embed.set_author( - name=ctx.author.username + "#" + ctx.author.discriminator, - icon_url=ctx.author.avatar, - ) - user_embed.set_thumbnail(url=ctx.guild.icon.url) - try: await user.send(embed=user_embed) except Exception: - send_failed = True + self.logger.warn(f"Failed to send ban embed to {user.id}") try: await ctx.guild.ban(user, reason=reason) except Exception as e: await ctx.send(f"Failed to ban user:\n```\n{e}\n```", ephemeral=True) return - send_failed = False + if mtype == "soft": await ctx.guild.unban(user, reason="Ban was softban") - fields.append(EmbedField(name="DM Sent?", value=str(not send_failed))) if btype != "temp": duration = None active = True if btype == "soft": active = False - await self.discord_apply_ban(ctx, reason, user, duration, active, fields, mtype) + await self.discord_apply_ban(ctx, reason, user, duration, active, mtype) @slash_command(name="unban", description="Unban a user") @slash_option( diff --git a/jarvis/cogs/admin/kick.py b/jarvis/cogs/admin/kick.py index 1c53304..7dfe089 100644 --- a/jarvis/cogs/admin/kick.py +++ b/jarvis/cogs/admin/kick.py @@ -1,7 +1,6 @@ """JARVIS KickCog.""" from jarvis_core.db.models import Kick from naff import InteractionContext, Permissions -from naff.models.discord.embed import EmbedField from naff.models.discord.user import User from naff.models.naff.application_commands import ( OptionTypes, @@ -10,8 +9,7 @@ from naff.models.naff.application_commands import ( ) from naff.models.naff.command import check -from jarvis.branding import get_command_color -from jarvis.utils import build_embed +from jarvis.embeds.admin import kick_embed from jarvis.utils.cogs import ModcaseCog from jarvis.utils.permissions import admin_or_permissions @@ -39,41 +37,19 @@ class KickCog(ModcaseCog): await ctx.send("User must be in guild", ephemeral=True) return - guild_name = ctx.guild.name - embed = build_embed( - title=f"You have been kicked from {guild_name}", - description=f"Reason: {reason}", - fields=[], - color=get_command_color("kick"), - ) + embed = kick_embed(user=user, admin=ctx.author, reason=reason, guild=ctx.guild, dm=True) - embed.set_author( - name=ctx.author.username + "#" + ctx.author.discriminator, - icon_url=ctx.author.display_avatar.url, - ) - embed.set_thumbnail(url=ctx.guild.icon.url) - - send_failed = False try: await user.send(embeds=embed) except Exception: - send_failed = True + self.logger.warn(f"Failed to send kick message to {user.id}") try: await ctx.guild.kick(user, reason=reason) except Exception as e: await ctx.send(f"Failed to kick user:\n```\n{e}\n```", ephemeral=True) return - fields = [EmbedField(name="DM Sent?", value=str(not send_failed))] - embed = build_embed( - title="User Kicked", - description=f"Reason: {reason}", - fields=fields, - ) - - embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) - embed.set_thumbnail(url=user.display_avatar.url) - embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + embed = kick_embed(user=user, admin=ctx.author, reason=reason, guild=ctx.guild) k = Kick( user=user.id, diff --git a/jarvis/cogs/admin/mute.py b/jarvis/cogs/admin/mute.py index 0cf301c..c0ec9ae 100644 --- a/jarvis/cogs/admin/mute.py +++ b/jarvis/cogs/admin/mute.py @@ -7,7 +7,6 @@ from dateparser_data.settings import default_parsers from jarvis_core.db.models import Mute from naff import InteractionContext, Permissions from naff.client.errors import Forbidden -from naff.models.discord.embed import EmbedField from naff.models.discord.modal import InputText, Modal, TextStyles from naff.models.discord.user import Member from naff.models.naff.application_commands import ( @@ -20,8 +19,7 @@ from naff.models.naff.application_commands import ( ) from naff.models.naff.command import check -from jarvis.branding import get_command_color -from jarvis.utils import build_embed +from jarvis.embeds.admin import mute_embed, unmute_embed from jarvis.utils.cogs import ModcaseCog from jarvis.utils.permissions import admin_or_permissions @@ -42,21 +40,8 @@ class MuteCog(ModcaseCog): duration=duration, active=True, ).commit() - ts = int(until.timestamp()) - embed = build_embed( - title="User Muted", - description=f"{user.mention} has been muted", - fields=[ - EmbedField(name="Reason", value=reason), - EmbedField(name="Until", value=f" "), - ], - color=get_command_color("mute"), - ) - embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) - embed.set_thumbnail(url=user.display_avatar.url) - embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") - return embed + return mute_embed(user=user, admin=ctx.author, reason=reason, guild=ctx.guild) @context_menu(name="Mute User", context_type=CommandTypes.USER) @check( @@ -193,12 +178,15 @@ class MuteCog(ModcaseCog): @slash_option( name="user", description="User to unmute", opt_type=OptionTypes.USER, required=True ) + @slash_option( + name="reason", description="Reason for unmute", opt_type=OptionTypes.STRING, required=True + ) @check( admin_or_permissions( Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS ) ) - async def _unmute(self, ctx: InteractionContext, user: Member) -> None: + async def _unmute(self, ctx: InteractionContext, user: Member, reason: str) -> None: if ( not user.communication_disabled_until or user.communication_disabled_until.timestamp() @@ -213,13 +201,6 @@ class MuteCog(ModcaseCog): await user.timeout(communication_disabled_until=datetime.now(tz=timezone.utc)) - embed = build_embed( - title="User Unmuted", - description=f"{user.mention} has been unmuted", - fields=[], - color=get_command_color("unmute"), - ) - embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) - embed.set_thumbnail(url=user.display_avatar.url) - embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + embed = unmute_embed(user=user, admin=ctx.author, reason=reason, guild=ctx.guild) + await ctx.send(embeds=embed) diff --git a/jarvis/cogs/admin/warning.py b/jarvis/cogs/admin/warning.py index 4b67104..79b0b97 100644 --- a/jarvis/cogs/admin/warning.py +++ b/jarvis/cogs/admin/warning.py @@ -15,9 +15,9 @@ from naff.models.naff.application_commands import ( ) from naff.models.naff.command import check +from jarvis.embeds.admin import warning_embed from jarvis.utils import build_embed from jarvis.utils.cogs import ModcaseCog -from jarvis.utils.embeds import warning_embed from jarvis.utils.permissions import admin_or_permissions @@ -65,7 +65,7 @@ class WarningCog(ModcaseCog): expires_at=expires_at, active=True, ).commit() - embed = warning_embed(user, reason) + embed = warning_embed(user, reason, ctx.author) await ctx.send(embeds=embed) @slash_command(name="warnings", description="Get count of user warnings") diff --git a/jarvis/cogs/reddit.py b/jarvis/cogs/reddit.py index c8b8333..a6f3d40 100644 --- a/jarvis/cogs/reddit.py +++ b/jarvis/cogs/reddit.py @@ -16,11 +16,6 @@ from jarvis_core.db.models import ( SubredditFollow, UserSetting, ) - -from jarvis import const -from jarvis.config import JarvisConfig -from jarvis.utils import build_embed -from jarvis.utils.permissions import admin_or_permissions from naff import Client, Extension, InteractionContext, Permissions from naff.client.utils.misc_utils import get from naff.models.discord.channel import ChannelTypes, GuildText @@ -34,6 +29,11 @@ from naff.models.naff.application_commands import ( ) from naff.models.naff.command import check +from jarvis import const +from jarvis.config import JarvisConfig +from jarvis.utils import build_embed +from jarvis.utils.permissions import admin_or_permissions + DEFAULT_USER_AGENT = f"python:JARVIS:{const.__version__} (by u/zevaryx)" sub_name = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_]{2,20}\Z") user_name = re.compile(r"[A-Za-z0-9_-]+") @@ -116,7 +116,8 @@ class RedditCog(Extension): ) base_embed.set_author(name="u/" + post.author.name, url=author_url, icon_url=author_icon) base_embed.set_footer( - text="Reddit", icon_url="https://www.redditinc.com/assets/images/site/reddit-logo.png" + text=f"r/{sub.display_name}", + icon_url="https://www.redditinc.com/assets/images/site/reddit-logo.png", ) embeds = [base_embed] diff --git a/jarvis/cogs/remindme.py b/jarvis/cogs/remindme.py index a18dfb6..b247bc4 100644 --- a/jarvis/cogs/remindme.py +++ b/jarvis/cogs/remindme.py @@ -153,7 +153,7 @@ class RemindmeCog(Extension): EmbedField(name="Message", value=message), EmbedField( name="When", - value=f"", + value=f" ()", inline=False, ), ], @@ -182,7 +182,7 @@ class RemindmeCog(Extension): if reminder.private and isinstance(ctx.channel, GuildChannel): fields.embed( EmbedField( - name=f"", + name=f" ()", value="Please DM me this command to view the content of this reminder", inline=False, ) @@ -190,7 +190,7 @@ class RemindmeCog(Extension): else: fields.append( EmbedField( - name=f"", + name=f" ()", value=f"{reminder.message}\n\u200b", inline=False, ) @@ -282,8 +282,12 @@ class RemindmeCog(Extension): return ts = int(reminder.remind_at.timestamp()) + cts = int(reminder.created_at.timestamp()) - fields = [EmbedField(name="Remind At", value=f" ()")] + fields = [ + EmbedField(name="Remind At", value=f" ()"), + EmbedField(name="Created At", value=f" ()"), + ] embed = build_embed( title="You have a reminder!", description=reminder.message, fields=fields diff --git a/jarvis/cogs/rolegiver.py b/jarvis/cogs/rolegiver.py index 6627a39..1cc1c45 100644 --- a/jarvis/cogs/rolegiver.py +++ b/jarvis/cogs/rolegiver.py @@ -90,7 +90,6 @@ class RolegiverCog(Extension): ) embed.set_thumbnail(url=ctx.guild.icon.url) - embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url) embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}") components = Button( @@ -164,7 +163,6 @@ class RolegiverCog(Extension): ) embed.set_thumbnail(url=ctx.guild.icon.url) - embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url) embed.set_footer( text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}" @@ -207,10 +205,6 @@ class RolegiverCog(Extension): ) embed.set_thumbnail(url=ctx.guild.icon.url) - embed.set_author( - name=ctx.author.display_name, - icon_url=ctx.author.display_avatar.url, - ) embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}") components = Button( diff --git a/jarvis/embeds/admin.py b/jarvis/embeds/admin.py new file mode 100644 index 0000000..ced7c29 --- /dev/null +++ b/jarvis/embeds/admin.py @@ -0,0 +1,208 @@ +"""JARVIS bot-specific embeds.""" +from typing import Optional + +from naff.models.discord.embed import Embed, EmbedField +from naff.models.discord.guild import Guild +from naff.models.discord.user import Member, User + +from jarvis.branding import get_command_color +from jarvis.utils import build_embed + + +def ban_embed( + user: User, + admin: User, + reason: str, + type: str, + guild: Guild, + duration: Optional[int] = None, + dm: bool = False, +) -> Embed: + """ + Generate a ban embed. + + Args: + user: User to ban + admin: Admin to execute ban + reason: Reason for ban + type: Ban type + guild: Guild to ban from + duration: Optional temporary ban duration + dm: If the embed should be a user embed + """ + fields = [EmbedField(name="Reason", value=reason), EmbedField(name="Type", value=type)] + if duration: + fields.append(EmbedField(name="Duration", value=f"{duration} hours")) + fields.append( + EmbedField(name="Admin", value=f"{admin.username}#{admin.discriminator} ({admin.mention})") + ) + if dm: + embed = build_embed( + title=f"You have been banned from {guild.name}", + fields=fields, + color=get_command_color("ban"), + ) + embed.set_thumbnail(url=guild.icon.url) + else: + embed = build_embed( + title="User Banned", + description=f"{user.mention} has been banned", + fields=fields, + color=get_command_color("ban"), + ) + embed.set_thumbnail(url=user.display_avatar.url) + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + + return embed + + +def unban_embed(user: User, admin: User, reason: str) -> Embed: + """ + Generate an unban embed. + + Args: + user: User to unban + admin: Admin to execute unban + reason: Reason for unban + """ + fields = ( + EmbedField(name="Reason", value=reason), + EmbedField(name="Admin", value=f"{admin.username}#{admin.discriminator} ({admin.mention})"), + ) + embed = build_embed( + title="User Unbanned", + description=f"{user.mention} was unbanned", + fields=fields, + color=get_command_color("unban"), + ) + embed.set_thumbnail(url=user.display_avatar.url) + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + + return embed + + +def kick_embed(user: Member, admin: Member, reason: str, guild: Guild, dm: bool = False) -> Embed: + """ + Generate a kick embed. + + Args: + user: User to kick + admin: Admin to execute kick + reason: Reason for kick + guild: Guild to kick from + dm: If the embed should be a user embed + """ + fields = [ + EmbedField(name="Reason", value="A valid reason"), + EmbedField( + name="Admin", + value=f"{admin.username}#{admin.discriminator} ({admin.mention})", + ), + ] + if dm: + embed = build_embed( + title=f"You have been kicked from {guild.name}", + color=get_command_color("kick"), + fields=fields, + ) + embed.set_thumbnail(url=guild.icon.url) + else: + embed = build_embed( + title="User Kicked", + description=f"{user.mention} has been kicked", + fields=fields, + color=get_command_color("kick"), + ) + + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + + return embed + + +def mute_embed(user: Member, admin: Member, reason: str, guild: Guild) -> Embed: + """ + Generate a mute embed. + + Args: + user: User to mute + admin: Admin to execute mute + reason: Reason for mute + """ + until = int(user.communication_disabled_until.timestamp()) + fields = ( + EmbedField(name="Reason", value=reason), + EmbedField(name="Until", value=f" ()"), + EmbedField( + name="Admin", + value=f"{admin.username}#{admin.discriminator} ({admin.mention})", + ), + ) + embed = build_embed( + title="User Muted", + description=f"{user.mention} has been muted", + fields=fields, + color=get_command_color("mute"), + ) + + embed.set_thumbnail(url=user.display_avatar.url) + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + + return embed + + +def unmute_embed(user: Member, admin: Member, reason: str, guild: Guild) -> Embed: + """ + Generate an umute embed. + + Args: + user: User to unmute + admin: Admin to execute unmute + reason: Reason for unmute + """ + fields = ( + EmbedField(name="Reason", value=reason), + EmbedField( + name="Admin", + value=f"{admin.username}#{admin.discriminator} ({admin.mention})", + ), + ) + embed = build_embed( + title="User Unmuted", + description=f"{user.mention} has been unmuted", + fields=fields, + color=get_command_color("mute"), + ) + + embed.set_thumbnail(url=user.display_avatar.url) + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + + return embed + + +def warning_embed(user: Member, reason: str, admin: Member) -> Embed: + """ + Generate a warning embed. + + Args: + user: User to warn + reason: Warning reason + admin: Admin who sent the warning + """ + fields = ( + EmbedField(name="Reason", value=reason, inline=False), + EmbedField(name="Admin", value=f"{admin.username}#{admin.discriminator} ({admin.mention})"), + ) + embed = build_embed( + title="Warning", + description=f"{user.mention} has been warned", + fields=fields, + color=get_command_color("warning"), + ) + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + return embed diff --git a/jarvis/utils/embeds.py b/jarvis/utils/embeds.py deleted file mode 100644 index f972aed..0000000 --- a/jarvis/utils/embeds.py +++ /dev/null @@ -1,26 +0,0 @@ -"""JARVIS bot-specific embeds.""" -from naff.models.discord.embed import Embed, EmbedField -from naff.models.discord.user import Member - -from jarvis.branding import get_command_color -from jarvis.utils import build_embed - - -def warning_embed(user: Member, reason: str) -> Embed: - """ - Generate a warning embed. - - Args: - user: User to warn - reason: Warning reason - """ - fields = (EmbedField(name="Reason", value=reason, inline=False),) - embed = build_embed( - title="Warning", - description=f"{user.mention} has been warned", - fields=fields, - color=get_command_color("warning"), - ) - embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) - embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") - return embed From 6f2fdc55d7025360a491cf5e89f73150c386a0ab Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 9 Sep 2022 22:23:13 -0600 Subject: [PATCH 04/23] Condense slash commands --- jarvis/client/events/__init__.py | 30 +++++++++++++++++++ jarvis/cogs/dev.py | 38 ++++++++++++------------ jarvis/cogs/image.py | 6 ++-- jarvis/cogs/remindme.py | 7 ++--- jarvis/cogs/rolegiver.py | 6 ++-- jarvis/cogs/starboard.py | 50 -------------------------------- jarvis/cogs/util.py | 29 +++++++++++------- 7 files changed, 77 insertions(+), 89 deletions(-) diff --git a/jarvis/client/events/__init__.py b/jarvis/client/events/__init__.py index f6f2b34..4c68ce7 100644 --- a/jarvis/client/events/__init__.py +++ b/jarvis/client/events/__init__.py @@ -8,6 +8,7 @@ from jarvis_core.util.ansi import RESET, Fore, Format, fmt from naff import listen from naff.models.discord.channel import DMChannel from naff.models.discord.embed import EmbedField +from naff.models.naff.application_commands import ContextMenu from naff.models.naff.context import Context, InteractionContext, PrefixedContext from jarvis import const @@ -61,6 +62,35 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin): "{}&permissions=8&scope=bot%20applications.commands".format(self.user.id) ) + global_base_commands = {} + guild_base_commands = {} + global_context_menus = [] + guild_context_menus = [] + for cid in self.interactions: + commands = self.interactions[cid] + to_update = global_base_commands if cid == 0 else guild_base_commands + for command in commands: + if isinstance(commands[command], ContextMenu): + if cid == 0: + global_context_menus.append(command) + else: + guild_context_menus.append(command) + continue + full = command.split(" ") + base = full[0] + if base not in to_update: + to_update[base] = {} + if len(full) == 3: + to_update[base][full[1]] = full[2] + elif len(full) == 2: + to_update[base][full[1]] = None + + self.logger.info( + "Loaded {:>2} global base slash commands".format(len(global_base_commands)) + ) + self.logger.info("Loaded {:>2} global context menus".format(len(global_context_menus))) + self.logger.info("Loaded {:>2} guild base slash commands".format(len(guild_base_commands))) + self.logger.info("Loaded {:>2} guild context menus".format(len(guild_context_menus))) self.logger.debug("Hitting Reminders for faster loads") _ = await Reminder.find().to_list(None) diff --git a/jarvis/cogs/dev.py b/jarvis/cogs/dev.py index cad6dab..561dee0 100644 --- a/jarvis/cogs/dev.py +++ b/jarvis/cogs/dev.py @@ -23,8 +23,8 @@ from naff.models.discord.file import File from naff.models.discord.message import Attachment from naff.models.naff.application_commands import ( OptionTypes, + SlashCommand, SlashCommandChoice, - slash_command, slash_option, ) from naff.models.naff.command import cooldown @@ -60,7 +60,9 @@ class DevCog(Extension): self.bot = bot self.logger = logging.getLogger(__name__) - @slash_command(name="hash", description="Hash some data") + dev = SlashCommand(name="dev", description="Developer utilities") + + @dev.subcommand(sub_cmd_name="hash", sub_cmd_description="Hash some data") @slash_option( name="method", description="Hash method", @@ -126,7 +128,7 @@ class DevCog(Extension): ) await ctx.send(embeds=embed, components=components) - @slash_command(name="uuid", description="Generate a UUID") + @dev.subcommand(sub_cmd_name="uuid", sub_cmd_description="Generate a UUID") @slash_option( name="version", description="UUID version", @@ -159,25 +161,25 @@ class DevCog(Extension): to_send = UUID_GET[version](uuidpy.NAMESPACE_DNS, data) await ctx.send(f"UUID{version}: `{to_send}`") - @slash_command( - name="objectid", - description="Generate an ObjectID", + @dev.subcommand( + sub_cmd_name="objectid", + sub_cmd_description="Generate an ObjectID", ) @cooldown(bucket=Buckets.USER, rate=1, interval=2) async def _objectid(self, ctx: InteractionContext) -> None: await ctx.send(f"ObjectId: `{str(ObjectId())}`") - @slash_command( - name="ulid", - description="Generate a ULID", + @dev.subcommand( + sub_cmd_name="ulid", + sub_cmd_description="Generate a ULID", ) @cooldown(bucket=Buckets.USER, rate=1, interval=2) async def _ulid(self, ctx: InteractionContext) -> None: await ctx.send(f"ULID: `{ulidpy.new().str}`") - @slash_command( - name="uuid2ulid", - description="Convert a UUID to a ULID", + @dev.subcommand( + sub_cmd_name="uuid2ulid", + sub_cmd_description="Convert a UUID to a ULID", ) @slash_option( name="uuid", description="UUID to convert", opt_type=OptionTypes.STRING, required=True @@ -190,9 +192,9 @@ class DevCog(Extension): else: await ctx.send("Invalid UUID") - @slash_command( - name="ulid2uuid", - description="Convert a ULID to a UUID", + @dev.subcommand( + sub_cmd_name="ulid2uuid", + sub_cmd_description="Convert a ULID to a UUID", ) @slash_option( name="ulid", description="ULID to convert", opt_type=OptionTypes.STRING, required=True @@ -207,7 +209,7 @@ class DevCog(Extension): base64_methods = ["b64", "b16", "b32", "a85", "b85"] - @slash_command(name="encode", description="Encode some data") + @dev.subcommand(sub_cmd_name="encode", sub_cmd_description="Encode some data") @slash_option( name="method", description="Encode method", @@ -245,7 +247,7 @@ class DevCog(Extension): ) await ctx.send(embeds=embed, components=components) - @slash_command(name="decode", description="Decode some data") + @dev.subcommand(sub_cmd_name="decode", sub_cmd_description="Decode some data") @slash_option( name="method", description="Decode method", @@ -283,7 +285,7 @@ class DevCog(Extension): ) await ctx.send(embeds=embed, components=components) - @slash_command(name="cloc", description="Get JARVIS lines of code") + @dev.subcommand(sub_cmd_name="cloc", sub_cmd_description="Get JARVIS lines of code") @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30) async def _cloc(self, ctx: InteractionContext) -> None: await ctx.defer() diff --git a/jarvis/cogs/image.py b/jarvis/cogs/image.py index 93f7a12..83e385b 100644 --- a/jarvis/cogs/image.py +++ b/jarvis/cogs/image.py @@ -15,7 +15,7 @@ from naff.models.discord.file import File from naff.models.discord.message import Attachment from naff.models.naff.application_commands import ( OptionTypes, - slash_command, + SlashCommand, slash_option, ) @@ -40,7 +40,9 @@ class ImageCog(Extension): def __del__(self): self._session.close() - @slash_command(name="resize", description="Resize an image") + image = SlashCommand(name="image", description="Manipulate images") + + @image.subcommand(sub_cmd_name="shrink", sub_cmd_description="Shrink an image") @slash_option( name="target", description="Target size, i.e. 200KB", diff --git a/jarvis/cogs/remindme.py b/jarvis/cogs/remindme.py index b247bc4..9d2f0ce 100644 --- a/jarvis/cogs/remindme.py +++ b/jarvis/cogs/remindme.py @@ -18,7 +18,6 @@ from naff.models.discord.modal import InputText, Modal, TextStyles from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, - slash_command, slash_option, ) from thefuzz import process @@ -40,7 +39,9 @@ class RemindmeCog(Extension): self.bot = bot self.logger = logging.getLogger(__name__) - @slash_command(name="remindme", description="Set a reminder") + reminders = SlashCommand(name="reminders", description="Manage reminders") + + @reminders.subcommand(sub_cmd_name="set", sub_cmd_description="Set a reminder") @slash_option( name="private", description="Send as DM?", @@ -210,8 +211,6 @@ class RemindmeCog(Extension): return embed - reminders = SlashCommand(name="reminders", description="Manage reminders") - @reminders.subcommand(sub_cmd_name="list", sub_cmd_description="List reminders") async def _list(self, ctx: InteractionContext) -> None: reminders = await Reminder.find(q(user=ctx.author.id, active=True)).to_list(None) diff --git a/jarvis/cogs/rolegiver.py b/jarvis/cogs/rolegiver.py index 1cc1c45..b4727c8 100644 --- a/jarvis/cogs/rolegiver.py +++ b/jarvis/cogs/rolegiver.py @@ -212,9 +212,7 @@ class RolegiverCog(Extension): ) await ctx.send(embeds=embed, components=components) - role = SlashCommand(name="role", description="Get/Remove Rolegiver roles") - - @role.subcommand(sub_cmd_name="get", sub_cmd_description="Get a role") + @rolegiver.subcommand(sub_cmd_name="get", sub_cmd_description="Get a role") @cooldown(bucket=Buckets.USER, rate=1, interval=10) async def _role_get(self, ctx: InteractionContext) -> None: setting = await Rolegiver.find_one(q(guild=ctx.guild.id)) @@ -290,7 +288,7 @@ class RolegiverCog(Extension): component.disabled = True await message.edit(components=components) - @role.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role") + @rolegiver.subcommand(sub_cmd_name="forfeit", sub_cmd_description="Forfeit a role") @cooldown(bucket=Buckets.USER, rate=1, interval=10) async def _role_remove(self, ctx: InteractionContext) -> None: user_roles = ctx.author.roles diff --git a/jarvis/cogs/starboard.py b/jarvis/cogs/starboard.py index 7e62058..018480c 100644 --- a/jarvis/cogs/starboard.py +++ b/jarvis/cogs/starboard.py @@ -265,56 +265,6 @@ class StarboardCog(Extension): description="Manage stars", ) - @star.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a starred message") - @slash_option( - name="id", description="Star ID to delete", opt_type=OptionTypes.INTEGER, required=True - ) - @slash_option( - name="starboard", - description="Starboard to delete star from", - opt_type=OptionTypes.CHANNEL, - required=True, - ) - @check(admin_or_permissions(Permissions.MANAGE_GUILD)) - async def _star_delete( - self, - ctx: InteractionContext, - id: int, - starboard: GuildText, - ) -> None: - if not isinstance(starboard, GuildText): - await ctx.send("Channel must be a GuildText channel", ephemeral=True) - return - - exists = await Starboard.find_one(q(channel=starboard.id, guild=ctx.guild.id)) - if not exists: - # TODO: automagically create starboard - await ctx.send( - f"Starboard does not exist in {starboard.mention}. Please create it first", - ephemeral=True, - ) - return - - star = await Star.find_one( - q( - starboard=starboard.id, - index=id, - guild=ctx.guild.id, - active=True, - ) - ) - if not star: - await ctx.send(f"No star exists with id {id}", ephemeral=True) - return - - message = await starboard.fetch_message(star.star) - if message: - await message.delete() - - await star.delete() - - await ctx.send(f"Star {id} deleted from {starboard.mention}") - def setup(bot: Client) -> None: """Add StarboardCog to JARVIS""" diff --git a/jarvis/cogs/util.py b/jarvis/cogs/util.py index 1134a81..f9ac647 100644 --- a/jarvis/cogs/util.py +++ b/jarvis/cogs/util.py @@ -20,6 +20,7 @@ from naff.models.discord.user import User from naff.models.naff.application_commands import ( CommandTypes, OptionTypes, + SlashCommand, SlashCommandChoice, context_menu, slash_command, @@ -49,7 +50,9 @@ class UtilCog(Extension): self.bot = bot self.logger = logging.getLogger(__name__) - @slash_command(name="status", description="Retrieve JARVIS status") + bot = SlashCommand(name="bot", description="Bot commands") + + @bot.subcommand(sub_cmd_name="status", sub_cmd_description="Retrieve JARVIS status") @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30) async def _status(self, ctx: InteractionContext) -> None: title = "JARVIS Status" @@ -74,9 +77,9 @@ class UtilCog(Extension): ) await ctx.send(embeds=embed, components=components) - @slash_command( - name="logo", - description="Get the current logo", + @bot.subcommand( + sub_cmd_name="logo", + sub_cmd_description="Get the current logo", ) @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30) async def _logo(self, ctx: InteractionContext) -> None: @@ -89,13 +92,15 @@ class UtilCog(Extension): ) await ctx.send(file=logo, components=components) - @slash_command(name="rchk", description="Robot Camo HK416") + rc = SlashCommand(name="rc", description="Robot Camo emoji commands") + + @rc.subcommand(sub_cmd_name="hk", sub_cmd_description="Robot Camo HK416") async def _rchk(self, ctx: InteractionContext) -> None: await ctx.send(content=hk, ephemeral=True) - @slash_command( - name="rcauto", - description="Automates robot camo letters", + @rc.subcommand( + sub_cmd_name="auto", + sub_cmd_description="Automates robot camo letters", ) @slash_option( name="text", @@ -176,7 +181,7 @@ class UtilCog(Extension): embed.set_thumbnail(url="attachment://color_show.png") data = np.array(JARVIS_LOGO) - r, g, b, a = data.T + *_, a = data.T fill = a > 0 @@ -397,7 +402,7 @@ class UtilCog(Extension): ) await ctx.send(embeds=embed, ephemeral=private, components=components) - @slash_command(name="support", description="Got issues?") + @bot.subcommand(sub_cmd_name="support", sub_cmd_description="Got issues?") async def _support(self, ctx: InteractionContext) -> None: await ctx.send( f""" @@ -409,7 +414,9 @@ We'll help as best we can with whatever issues you encounter. """ ) - @slash_command(name="privacy_terms", description="View Privacy and Terms of Use") + @bot.subcommand( + sub_cmd_name="privacy_terms", sub_cmd_description="View Privacy and Terms of Use" + ) async def _privacy_terms(self, ctx: InteractionContext) -> None: await ctx.send( """ From 63594fc50fd7989c403fd81fc6d0ffeadb750153 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 9 Sep 2022 22:26:58 -0600 Subject: [PATCH 05/23] Increase lock limits to 7 days from 12 hours --- jarvis/cogs/admin/lock.py | 4 ++-- jarvis/cogs/admin/lockdown.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jarvis/cogs/admin/lock.py b/jarvis/cogs/admin/lock.py index 55a99c4..faad57e 100644 --- a/jarvis/cogs/admin/lock.py +++ b/jarvis/cogs/admin/lock.py @@ -57,8 +57,8 @@ class LockCog(Extension): await ctx.send("Duration must be > 0", ephemeral=True) return - elif duration > 60 * 12: - await ctx.send("Duration must be <= 12 hours", ephemeral=True) + elif duration > 60 * 24 * 7: + await ctx.send("Duration must be <= 7 days", ephemeral=True) return if len(reason) > 100: diff --git a/jarvis/cogs/admin/lockdown.py b/jarvis/cogs/admin/lockdown.py index b1c3e86..15c75ad 100644 --- a/jarvis/cogs/admin/lockdown.py +++ b/jarvis/cogs/admin/lockdown.py @@ -130,8 +130,8 @@ class LockdownCog(Extension): if duration <= 0: await ctx.send("Duration must be > 0", ephemeral=True) return - elif duration >= 300: - await ctx.send("Duration must be < 5 hours", ephemeral=True) + elif duration > 60 * 24 * 7: + await ctx.send("Duration must be <= 7 days", ephemeral=True) return exists = await Lockdown.find_one(q(guild=ctx.guild.id, active=True)) From cf7e54f67b774cd6433c0ed3179340a688e66bad Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 9 Sep 2022 22:27:37 -0600 Subject: [PATCH 06/23] Remove star command group as it is no longer needed --- jarvis/cogs/starboard.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/jarvis/cogs/starboard.py b/jarvis/cogs/starboard.py index 018480c..38c823a 100644 --- a/jarvis/cogs/starboard.py +++ b/jarvis/cogs/starboard.py @@ -260,11 +260,6 @@ class StarboardCog(Extension): async def _star_message(self, ctx: InteractionContext) -> None: await self._star_add(ctx, message=str(ctx.target_id)) - star = SlashCommand( - name="star", - description="Manage stars", - ) - def setup(bot: Client) -> None: """Add StarboardCog to JARVIS""" From b683e2807555acf71cff6a175064517151ddd473 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 9 Sep 2022 22:32:54 -0600 Subject: [PATCH 07/23] Add Avatar context menu --- jarvis/cogs/util.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/jarvis/cogs/util.py b/jarvis/cogs/util.py index f9ac647..43b547a 100644 --- a/jarvis/cogs/util.py +++ b/jarvis/cogs/util.py @@ -126,14 +126,6 @@ class UtilCog(Extension): else: await ctx.send(to_send, ephemeral=True) - @slash_command(name="avatar", description="Get a user avatar") - @slash_option( - name="user", - description="User to view avatar of", - opt_type=OptionTypes.USER, - required=False, - ) - @cooldown(bucket=Buckets.USER, rate=1, interval=5) async def _avatar(self, ctx: InteractionContext, user: User = None) -> None: if not user: user = ctx.author @@ -198,6 +190,21 @@ class UtilCog(Extension): ) await ctx.send(embeds=embed, file=color_show, components=components) + @slash_command(name="avatar", description="Get a user avatar") + @slash_option( + name="user", + description="User to view avatar of", + opt_type=OptionTypes.USER, + required=False, + ) + @cooldown(bucket=Buckets.USER, rate=1, interval=5) + async def _avatar_slash(self, ctx: InteractionContext, user: User = None) -> None: + await self._userinfo(ctx, user) + + @context_menu(name="Avatar", context_type=CommandTypes.USER) + async def _avatar_menu(self, ctx: InteractionContext) -> None: + await self._avatar(ctx, ctx.target) + async def _userinfo(self, ctx: InteractionContext, user: User = None) -> None: await ctx.defer() if not user: From 16190d48387d68a95f3fa531d6844609c6ccb382 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 9 Sep 2022 22:36:26 -0600 Subject: [PATCH 08/23] Move all startup logic to on_startup --- jarvis/client/events/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/jarvis/client/events/__init__.py b/jarvis/client/events/__init__.py index 4c68ce7..ddb423c 100644 --- a/jarvis/client/events/__init__.py +++ b/jarvis/client/events/__init__.py @@ -42,10 +42,6 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin): async def on_startup(self) -> None: """NAFF on_startup override. Prometheus info generated here.""" jarvis_info.info({"version": const.__version__}) - - @listen() - async def on_ready(self) -> None: - """NAFF on_ready override.""" try: if not self.synced: await self._sync_domains() From aa9d4da1c26600f83dc347f6e0f39afa239044d6 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 9 Sep 2022 22:37:29 -0600 Subject: [PATCH 09/23] Change domain update task to not log if no changes --- jarvis/client/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jarvis/client/tasks.py b/jarvis/client/tasks.py index f8fe609..08b19f2 100644 --- a/jarvis/client/tasks.py +++ b/jarvis/client/tasks.py @@ -13,9 +13,9 @@ class TaskMixin: response.raise_for_status() data = await response.json() - self.logger.debug(f"Found {len(data)} changes to phishing domains") if len(data) == 0: return + self.logger.debug(f"Found {len(data)} changes to phishing domains") add = 0 sub = 0 From c70c7e20bebea8cec5354d29e2346c34d73c0bd6 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 9 Sep 2022 22:39:35 -0600 Subject: [PATCH 10/23] Remove unnecessary logging line --- jarvis/client/tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jarvis/client/tasks.py b/jarvis/client/tasks.py index 08b19f2..ff28c44 100644 --- a/jarvis/client/tasks.py +++ b/jarvis/client/tasks.py @@ -7,7 +7,6 @@ from naff.models.naff.tasks.triggers import IntervalTrigger class TaskMixin: @Task.create(IntervalTrigger(minutes=1)) async def _update_domains(self) -> None: - self.logger.debug("Updating phishing domains") async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session: response = await session.get("https://phish.sinking.yachts/v2/recent/60") response.raise_for_status() From d3fe0a0744727dc699aa9805c7081a4fdfc8479e Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Sat, 10 Sep 2022 17:40:02 -0600 Subject: [PATCH 11/23] Utilize Client.interaction_tree --- jarvis/client/events/__init__.py | 59 +++++++++++++++++--------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/jarvis/client/events/__init__.py b/jarvis/client/events/__init__.py index ddb423c..d6d14c2 100644 --- a/jarvis/client/events/__init__.py +++ b/jarvis/client/events/__init__.py @@ -58,35 +58,38 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin): "{}&permissions=8&scope=bot%20applications.commands".format(self.user.id) ) - global_base_commands = {} - guild_base_commands = {} - global_context_menus = [] - guild_context_menus = [] - for cid in self.interactions: - commands = self.interactions[cid] - to_update = global_base_commands if cid == 0 else guild_base_commands - for command in commands: - if isinstance(commands[command], ContextMenu): - if cid == 0: - global_context_menus.append(command) - else: - guild_context_menus.append(command) - continue - full = command.split(" ") - base = full[0] - if base not in to_update: - to_update[base] = {} - if len(full) == 3: - to_update[base][full[1]] = full[2] - elif len(full) == 2: - to_update[base][full[1]] = None + global_base_commands = 0 + guild_base_commands = 0 + global_context_menus = 0 + guild_context_menus = 0 + for cid in self.interaction_tree: + if cid == 0: + global_base_commands = sum( + 1 + for _ in self.interaction_tree[cid] + if not isinstance(self.interaction_tree[cid][_], ContextMenu) + ) + global_context_menus = sum( + 1 + for _ in self.interaction_tree[cid] + if isinstance(self.interaction_tree[cid][_], ContextMenu) + ) + else: + guild_base_commands += sum( + 1 + for _ in self.interaction_tree[cid] + if not isinstance(self.interaction_tree[cid][_], ContextMenu) + ) + guild_context_menus += sum( + 1 + for _ in self.interaction_tree[cid] + if isinstance(self.interaction_tree[cid][_], ContextMenu) + ) - self.logger.info( - "Loaded {:>2} global base slash commands".format(len(global_base_commands)) - ) - self.logger.info("Loaded {:>2} global context menus".format(len(global_context_menus))) - self.logger.info("Loaded {:>2} guild base slash commands".format(len(guild_base_commands))) - self.logger.info("Loaded {:>2} guild context menus".format(len(guild_context_menus))) + self.logger.info("Loaded {:>3} global base slash commands".format(global_base_commands)) + self.logger.info("Loaded {:>3} global context menus".format(global_context_menus)) + self.logger.info("Loaded {:>3} guild base slash commands".format(guild_base_commands)) + self.logger.info("Loaded {:>3} guild context menus".format(guild_context_menus)) self.logger.debug("Hitting Reminders for faster loads") _ = await Reminder.find().to_list(None) From 9c0bf5f7a57501d90e3522f4e15b44db7b5fde1f Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Sat, 10 Sep 2022 17:48:46 -0600 Subject: [PATCH 12/23] Add total number of commands to status --- jarvis/cogs/util.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/jarvis/cogs/util.py b/jarvis/cogs/util.py index 43b547a..4b0160d 100644 --- a/jarvis/cogs/util.py +++ b/jarvis/cogs/util.py @@ -56,14 +56,31 @@ class UtilCog(Extension): @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30) async def _status(self, ctx: InteractionContext) -> None: title = "JARVIS Status" - desc = f"All systems online\nConnected to **{len(self.bot.guilds)}** guilds" + desc = ( + f"All systems online" + f"\nConnected to **{len(self.bot.guilds)}** guilds" + f"\nListening for **{len(self.bot.application_commands)}** commands" + ) color = "#3498db" fields = [] uptime = int(self.bot.start_time.timestamp()) - fields.append(EmbedField(name="Version", value=jconst.__version__, inline=True)) - fields.append(EmbedField(name="naff", value=const.__version__, inline=True)) - fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=True)) + fields.append( + EmbedField( + name="Version", + value=f"[{jconst.__version__}](https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot)", + inline=True, + ) + ) + fields.append( + EmbedField(name="NAFF", value=f"[{const.__version__}](https://naff.info)", inline=True) + ) + repo_url = ( + f"https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/-/tree/{get_repo_hash()}" + ) + fields.append( + EmbedField(name="Git Hash", value=f"[{get_repo_hash()[:7]}]({repo_url})", inline=True) + ) fields.append(EmbedField(name="Online Since", value=f"", inline=False)) num_domains = len(self.bot.phishing_domains) fields.append( From 99c521fa76e28b09f8ce39647119dc0b523c17ba Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Mon, 12 Sep 2022 11:17:46 -0600 Subject: [PATCH 13/23] Auto mute on harmful link --- jarvis/client/events/message.py | 49 +++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/jarvis/client/events/message.py b/jarvis/client/events/message.py index 01b04df..a08a7f8 100644 --- a/jarvis/client/events/message.py +++ b/jarvis/client/events/message.py @@ -4,16 +4,18 @@ from datetime import datetime, timedelta, timezone from aiohttp import ClientSession from jarvis_core.db import q -from jarvis_core.db.models import Autopurge, Autoreact, Roleping, Setting, Warning +from jarvis_core.db.models import Autopurge, Autoreact, Mute, Roleping, Setting, Warning from jarvis_core.filters import invites, url from naff import listen from naff.api.events.discord import MessageCreate, MessageDelete, MessageUpdate from naff.client.utils.misc_utils import find_all -from naff.models.discord.channel import DMChannel +from naff.models.discord.channel import DMChannel, GuildText from naff.models.discord.embed import EmbedField from naff.models.discord.enums import Permissions from naff.models.discord.message import Message +from naff.models.discord.user import Member +from jarvis.branding import get_command_color from jarvis.tracking import malicious_tracker, warnings_tracker from jarvis.utils import build_embed from jarvis.utils.embeds import warning_embed @@ -274,6 +276,37 @@ class MessageEventMixin: return True return False + async def timeout_user(self, user: Member, channel: GuildText) -> None: + """Timeout a user.""" + expires_at = datetime.now(tz=timezone.utc) + timedelta(minutes=30) + try: + await user.timeout(communication_disabled_until=expires_at, reason="Phishing link") + await Mute( + user=user.id, + reason="Auto mute for harmful link", + admin=self.user.id, + guild=user.guild.id, + duration=30, + active=True, + ).commit() + ts = int(expires_at.timestamp()) + embed = build_embed( + title="User Muted", + description=f"{user.mention} has been muted", + fields=[ + EmbedField(name="Reason", value="Auto mute for harmful link"), + EmbedField(name="Until", value=f" "), + ], + color=get_command_color("mute"), + ) + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + embed.set_thumbnail(url=user.display_avatar.url) + embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") + await channel.send(embeds=embed) + + except Exception: + self.logger.warn("Failed to timeout user for phishing") + @listen() async def on_message(self, event: MessageCreate) -> None: """Handle on_message event. Calls other event handlers.""" @@ -284,8 +317,10 @@ class MessageEventMixin: await self.roleping(message) await self.autopurge(message) await self.checks(message) - if not await self.phishing(message): - await self.malicious_url(message) + if not (phish := await self.phishing(message)): + malicious = await self.malicious_url(message) + if phish or malicious: + await self.timeout_user(message.author, message.channel) @listen() async def on_message_edit(self, event: MessageUpdate) -> None: @@ -337,8 +372,10 @@ class MessageEventMixin: await self.checks(after) await self.roleping(after) await self.checks(after) - if not await self.phishing(after): - await self.malicious_url(after) + if not (phish := await self.phishing(after)): + malicious = await self.malicious_url(after) + if phish or malicious: + await self.timeout_user(after.author, after.channel) @listen() async def on_message_delete(self, event: MessageDelete) -> None: From a8b0c52e97904eda60424f356b646ff8a887616d Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Sat, 17 Sep 2022 18:04:23 -0600 Subject: [PATCH 14/23] Fix typos in dev embeds --- jarvis/cogs/dev.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jarvis/cogs/dev.py b/jarvis/cogs/dev.py index 561dee0..af8ff09 100644 --- a/jarvis/cogs/dev.py +++ b/jarvis/cogs/dev.py @@ -241,7 +241,7 @@ class DevCog(Extension): EmbedField(name="Plaintext", value=f"`{data}`", inline=False), EmbedField(name=mstr, value=f"`{encoded}`", inline=False), ] - embed = build_embed(title="Decoded Data", description="", fields=fields) + embed = build_embed(title="Encoded Data", description="", fields=fields) components = Button( style=ButtonStyles.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}" ) @@ -257,7 +257,7 @@ class DevCog(Extension): ) @slash_option( name="data", - description="Data to encode", + description="Data to decode", opt_type=OptionTypes.STRING, required=True, ) @@ -276,7 +276,7 @@ class DevCog(Extension): ) return fields = [ - EmbedField(name="Plaintext", value=f"`{data}`", inline=False), + EmbedField(name="Encoded Text", value=f"`{data}`", inline=False), EmbedField(name=mstr, value=f"`{decoded}`", inline=False), ] embed = build_embed(title="Decoded Data", description="", fields=fields) From bfdb61dc9d78f410abbc747c53ef9ff9aea55eea Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Sat, 1 Oct 2022 22:49:27 -0600 Subject: [PATCH 15/23] Some visual changes --- jarvis/__init__.py | 43 +++++++++++++++++++++++-- jarvis/branding.py | 7 ++++- jarvis/cogs/dbrand.py | 73 +++++++++++++++++++++++++++++++------------ poetry.lock | 59 ++++++++++++++++++++++------------ 4 files changed, 139 insertions(+), 43 deletions(-) diff --git a/jarvis/__init__.py b/jarvis/__init__.py index acb5cd0..7eba22d 100644 --- a/jarvis/__init__.py +++ b/jarvis/__init__.py @@ -1,5 +1,7 @@ """Main JARVIS package.""" import logging +from functools import partial +from typing import Any import aioredis import jurigged @@ -17,11 +19,47 @@ from jarvis.utils import get_extensions __version__ = const.__version__ +def jlogger(logger: logging.Logger, event: Any) -> None: + """ + Logging for jurigged + + Args: + logger: Logger to use + event: Event to parse + """ + jlog = partial(logger.log, 11) + if isinstance(event, jurigged.live.WatchOperation): + jlog(f"[bold]Watch[/] {event.filename}", extra={"markup": True}) + elif isinstance(event, jurigged.codetools.AddOperation): + event_str = f"{event.defn.parent.dotpath()}:{event.defn.stashed.lineno}" + if isinstance(event.defn, jurigged.codetools.LineDefinition): + event_str += f" | {event.defn.text}" + jlog( + f"[bold green]Run[/] {event_str}", + extra={"markup": True}, + ) + else: + jlog(f"[bold green]Add[/] {event_str}", extra={"markup": True}) + elif isinstance(event, jurigged.codetools.UpdateOperation): + if isinstance(event.defn, jurigged.codetools.FunctionDefinition): + event_str = f"{event.defn.parent.dotpath()}:{event.defn.stashed.lineno}" + jlog(f"[bold yellow]Update[/] {event_str}", extra={"markup": True}) + elif isinstance(event, jurigged.codetools.DeleteOperation): + event_str = f"{event.defn.parent.dotpath()}:{event.defn.stashed.lineno}" + if isinstance(event.defn, jurigged.codetools.LineDefinition): + event_str += f" | {event.defn.text}" + jlog(f"[bold red]Delete[/] {event_str}", extra={"markup": True}) + elif isinstance(event, (Exception, SyntaxError)): + logger.exception("Jurigged encountered error", exc_info=True) + else: + jlog(event) + + async def run() -> None: """Run JARVIS""" # Configure logger jconfig = JarvisConfig.from_yaml() - logger = get_logger("jarvis", show_locals=jconfig.log_level == "DEBUG") + logger = get_logger("jarvis", show_locals=False) # jconfig.log_level == "DEBUG") logger.setLevel(jconfig.log_level) file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w") file_handler.setFormatter( @@ -49,7 +87,8 @@ async def run() -> None: # External modules if jconfig.log_level == "DEBUG": - jurigged.watch(pattern="jarvis/*.py") + logging.addLevelName(11, "\033[35mJURIG\033[0m ") + jurigged.watch(pattern="jarvis/*.py", logger=partial(jlogger, logger)) if jconfig.rook_token: rook.start(token=jconfig.rook_token, labels={"env": "dev"}) diff --git a/jarvis/branding.py b/jarvis/branding.py index 33b1352..e7d2a02 100644 --- a/jarvis/branding.py +++ b/jarvis/branding.py @@ -11,8 +11,13 @@ COMMAND_TYPES = { "SOFT": ["warning"], "GOOD": ["unban", "unmute"], } - CUSTOM_COMMANDS = {} +CUSTOM_EMOJIS = { + "ico_clock_green": "<:ico_clock_green:1019710693206933605>", + "ico_clock_yellow": "<:ico_clock_yellow:1019710734340472834>", + "ico_clock_red": "<:ico_clock_red:1019710735896551534>", + "ico_check_green": "<:ico_check_green:1019725504120639549>", +} def get_command_color(command: str) -> str: diff --git a/jarvis/cogs/dbrand.py b/jarvis/cogs/dbrand.py index 4900e0f..859220f 100644 --- a/jarvis/cogs/dbrand.py +++ b/jarvis/cogs/dbrand.py @@ -1,11 +1,12 @@ """JARVIS dbrand cog.""" import logging import re -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import aiohttp from bs4 import BeautifulSoup from naff import Client, Extension, InteractionContext +from naff.client.utils import find from naff.models.discord.embed import EmbedField from naff.models.naff.application_commands import ( OptionTypes, @@ -16,6 +17,7 @@ from naff.models.naff.command import cooldown from naff.models.naff.cooldowns import Buckets from thefuzz import process +from jarvis.branding import CUSTOM_EMOJIS from jarvis.config import JarvisConfig from jarvis.data.dbrand import shipping_lookup from jarvis.utils import build_embed @@ -51,11 +53,11 @@ async def parse_db_status() -> dict: elif "column--status" in cell["class"]: info = cell.find("span")["class"] if any("green" in x for x in info): - cell = "🟢" + cell = CUSTOM_EMOJIS.get("ico_clock_green", "🟢") elif any("yellow" in x for x in info): - cell = "🟡" + cell = CUSTOM_EMOJIS.get("ico_clock_yellow", "🟡") elif any("red" in x for x in info): - cell = "🔴" + cell = CUSTOM_EMOJIS.get("ico_clock_red", "🔴") elif any("black" in x for x in info): cell = "⚫" else: @@ -91,20 +93,26 @@ class DbrandCog(Extension): @db.subcommand(sub_cmd_name="status", sub_cmd_description="Get dbrand operational status") async def _status(self, ctx: InteractionContext) -> None: status = self.cache.get("status") - if not status or status["cache_expiry"] <= datetime.utcnow(): + if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc): status = await parse_db_status() - status["cache_expiry"] = datetime.utcnow() + timedelta(hours=2) + status["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=2) self.cache["status"] = status status = status.get("operations") + emojies = [x["Status"] for x in status] fields = [ - EmbedField(name=f'{x["Status"]} {x["Service"]}', value=x["Detail"]) for x in status + EmbedField(name=f'{x["Status"]} {x["Service"]}', value=x["Detail"]) for x in status ] + color = "#FBBD1E" + if all("green" in x for x in emojies): + color = "#38F657" + elif all("red" in x for x in emojies): + color = "#F12D20" embed = build_embed( title="Operational Status", description="Current dbrand operational status.\n[View online](https://dbrand.com/status)", fields=fields, url="https://dbrand.com/status", - color="#FFBB00", + color=color, ) embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png") @@ -112,6 +120,7 @@ class DbrandCog(Extension): text="dbrand.com", icon_url="https://dev.zevaryx.com/db_logo.png", ) + await ctx.send(embeds=embed) @db.subcommand(sub_cmd_name="gripcheck", sub_cmd_description="Watch a dbrand grip get thrown") @@ -206,12 +215,12 @@ class DbrandCog(Extension): await ctx.defer() dest = search.lower() data = self.cache.get(dest, None) - if not data or data["cache_expiry"] < datetime.utcnow(): + if not data or data["cache_expiry"] < datetime.now(tz=timezone.utc): api_link = self.api_url + dest data = await self._session.get(api_link) if 200 <= data.status < 400: data = await data.json() - data["cache_expiry"] = datetime.utcnow() + timedelta(hours=24) + data["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=24) self.cache[dest] = data else: data = None @@ -220,32 +229,56 @@ class DbrandCog(Extension): fields = [] for service in data["shipping_services_available"]: service_data = self.cache.get(f"{dest}-{service}") - if not service_data or service_data["cache_expiry"] < datetime.utcnow(): + if not service_data or service_data["cache_expiry"] < datetime.now(tz=timezone.utc): service_data = await self._session.get( self.api_url + dest + "/" + service["url"] ) if service_data.status > 400: continue service_data = await service_data.json() - service_data["cache_expiry"] = datetime.utcnow() + timedelta(hours=24) + service_data["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta( + hours=24 + ) self.cache[f"{dest}-{service}"] = service_data title = f'{service_data["carrier"]} {service_data["tier-title"]} | {service_data["costs-min"]}' message = service_data["time-title"] if service_data["free_threshold_available"]: title += " | Free over " + service_data["free-threshold"] fields.append(EmbedField(title, message)) + + status = self.cache.get("status") + if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc): + status = await parse_db_status() + status["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=2) + self.cache["status"] = status + status = status["countries"] + + country = data["country"] + if country.startswith("the"): + country = country.replace("the", "").strip() + shipping_info = find(lambda x: x["Country"] == country, status) + country = "-".join(x for x in data["country"].split(" ") if x != "the") - country_urlsafe = country.replace("-", "%20") - description = ( - f"Click the link above to see shipping time to {data['country']}." - "\n[View all shipping destinations](https://dbrand.com/shipping)" - " | [Check shipping status]" - f"(https://dbrand.com/status#main-content:~:text={country_urlsafe})" - ) + description = "" + color = "#FFBB00" + if shipping_info: + description = f'{shipping_info["Status"]}\u200b \u200b {shipping_info["Est. Delivery Time"].split(":")[0]}' + created = self.cache.get("status").get("cache_expiry") - timedelta(hours=2) + ts = int(created.timestamp()) + description += f" \u200b | \u200b Last updated: \n\u200b" + if "green" in shipping_info["Status"]: + color = "#38F657" + elif "yellow" in shipping_info["Status"]: + color = "#FBBD1E" + elif "red" in shipping_info["Status"]: + color = "#F12D20" + else: + color = "#FFFFFF" + embed = build_embed( title="Shipping to {}".format(data["country"]), description=description, - color="#FFBB00", + color=color, fields=fields, url=self.base_url + "shipping/" + country, ) diff --git a/poetry.lock b/poetry.lock index c1ce6e3..df79f1b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -172,10 +172,10 @@ aiohttp = "*" yarl = "*" [package.extras] +test = ["vcrpy (==4.0.2)", "testfixtures (>4.13.2,<7)", "pytest-vcr", "pytest", "mock (>=0.8)", "asynctest (>=0.13.0)"] +lint = ["pydocstyle", "pre-commit", "flynt", "flake8", "black"] +dev = ["vcrpy (==4.0.2)", "testfixtures (>4.13.2,<7)", "pytest-vcr", "pytest", "mock (>=0.8)", "asynctest (>=0.13.0)", "pydocstyle", "pre-commit", "flynt", "flake8", "black"] ci = ["coveralls"] -dev = ["black", "flake8", "flynt", "pre-commit", "pydocstyle", "asynctest (>=0.13.0)", "mock (>=0.8)", "pytest", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.0.2)"] -lint = ["black", "flake8", "flynt", "pre-commit", "pydocstyle"] -test = ["asynctest (>=0.13.0)", "mock (>=0.8)", "pytest", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.0.2)"] [[package]] name = "attrs" @@ -306,7 +306,7 @@ optional = false python-versions = "*" [package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] +test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"] [[package]] name = "dateparser" @@ -323,9 +323,9 @@ regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27,<2022.3.15" tzlocal = "*" [package.extras] -calendars = ["convertdate", "hijri-converter", "convertdate"] -fasttext = ["fasttext"] langdetect = ["langdetect"] +fasttext = ["fasttext"] +calendars = ["convertdate", "hijri-converter", "convertdate"] [[package]] name = "discord-typings" @@ -454,7 +454,7 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "jurigged" -version = "0.5.2" +version = "0.5.3" description = "Live update of Python functions" category = "main" optional = false @@ -467,7 +467,7 @@ ovld = ">=0.3.1,<0.4.0" watchdog = ">=1.0.2" [package.extras] -develoop = ["giving (>=0.3.6,<0.4.0)", "rich (>=10.13.0,<11.0.0)", "hrepr (>=0.4.0,<0.5.0)"] +develoop = ["hrepr (>=0.4.0,<0.5.0)", "rich (>=10.13.0,<11.0.0)", "giving (>=0.3.6,<0.4.0)"] [[package]] name = "marshmallow" @@ -529,7 +529,7 @@ python-versions = "*" [[package]] name = "naff" -version = "1.9.0" +version = "1.10.0" description = "Not another freaking fork" category = "main" optional = false @@ -784,7 +784,7 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyppeteer" @@ -1163,9 +1163,9 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] -optional = ["python-socks", "wsaccel"] test = ["websockets"] +optional = ["wsaccel", "python-socks"] +docs = ["sphinx-rtd-theme (>=0.5)", "Sphinx (>=3.4)"] [[package]] name = "websockets" @@ -1413,7 +1413,10 @@ dateparser = [ {file = "dateparser-1.1.1-py2.py3-none-any.whl", hash = "sha256:9600874312ff28a41f96ec7ccdc73be1d1c44435719da47fea3339d55ff5a628"}, {file = "dateparser-1.1.1.tar.gz", hash = "sha256:038196b1f12c7397e38aad3d61588833257f6f552baa63a1499e6987fa8d42d9"}, ] -discord-typings = [] +discord-typings = [ + {file = "discord-typings-0.5.1.tar.gz", hash = "sha256:1a4fb1e00201416ae94ca64ca5935d447c005e0475b1ec274c1a6e09072db70e"}, + {file = "discord_typings-0.5.1-py3-none-any.whl", hash = "sha256:55ebdb6d6f0f47df774a0c31193ba6a45de14625fab9c6fbd43bfe87bb8c0128"}, +] distro = [ {file = "distro-1.7.0-py3-none-any.whl", hash = "sha256:d596311d707e692c2160c37807f83e3820c5d539d5a83e87cfb6babd8ba3a06b"}, {file = "distro-1.7.0.tar.gz", hash = "sha256:151aeccf60c216402932b52e40ee477a939f8d58898927378a02abbe852c1c39"}, @@ -1509,8 +1512,8 @@ jinxed = [ {file = "jinxed-1.2.0.tar.gz", hash = "sha256:032acda92d5c57cd216033cbbd53de731e6ed50deb63eb4781336ca55f72cda5"}, ] jurigged = [ - {file = "jurigged-0.5.2-py3-none-any.whl", hash = "sha256:410ff6199c659108dace9179507342883fe2fffec1966fd19709f9d59fd69e24"}, - {file = "jurigged-0.5.2.tar.gz", hash = "sha256:de1d4daeb99c0299eaa86f691d35cb1eab3bfa836cfe9a3551a56f3829479e3b"}, + {file = "jurigged-0.5.3-py3-none-any.whl", hash = "sha256:355a9bddf42cae541e862796fb125827fc35573a982c6f35d3dc5621e59c91e3"}, + {file = "jurigged-0.5.3.tar.gz", hash = "sha256:47cf4e9f10455a39602caa447888c06adda962699c65f19d8c37509817341b5e"}, ] marshmallow = [ {file = "marshmallow-3.16.0-py3-none-any.whl", hash = "sha256:53a1e0ee69f79e1f3e80d17393b25cfc917eda52f859e8183b4af72c3390c1f1"}, @@ -1590,8 +1593,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] naff = [ - {file = "naff-1.9.0-py3-none-any.whl", hash = "sha256:20144495aed9452d9d2e713eb6ade9636601457ca3de255684b2186068505bcd"}, - {file = "naff-1.9.0.tar.gz", hash = "sha256:f4870ea304747368d6d750f3d52fcbc96017bd7afaa7ec06a3e9a68ff301997d"}, + {file = "naff-1.10.0-py3-none-any.whl", hash = "sha256:bb28ef19efb3f8e04f3569a3aac6b3e2738cf5747dea0bed483c458588933682"}, + {file = "naff-1.10.0.tar.gz", hash = "sha256:d0ab71c39ea5bf352228f0bc3d3dfe3610122cb01733bca4565497078de95650"}, ] nafftrack = [] nanoid = [ @@ -1850,6 +1853,7 @@ pymongo = [ {file = "pymongo-3.12.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:71c0db2c313ea8a80825fb61b7826b8015874aec29ee6364ade5cb774fe4511b"}, {file = "pymongo-3.12.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b779e87300635b8075e8d5cfd4fdf7f46078cd7610c381d956bca5556bb8f97"}, {file = "pymongo-3.12.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:351a2efe1c9566c348ad0076f4bf541f4905a0ebe2d271f112f60852575f3c16"}, + {file = "pymongo-3.12.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:858af7c2ab98f21ed06b642578b769ecfcabe4754648b033168a91536f7beef9"}, {file = "pymongo-3.12.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0a02313e71b7c370c43056f6b16c45effbb2d29a44d24403a3d5ba6ed322fa3f"}, {file = "pymongo-3.12.3-cp310-cp310-manylinux1_i686.whl", hash = "sha256:d3082e5c4d7b388792124f5e805b469109e58f1ab1eb1fbd8b998e8ab766ffb7"}, {file = "pymongo-3.12.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:514e78d20d8382d5b97f32b20c83d1d0452c302c9a135f0a9022236eb9940fda"}, @@ -1963,7 +1967,9 @@ python-gitlab = [ {file = "python-gitlab-3.5.0.tar.gz", hash = "sha256:29ae7fb9b8c9aeb2e6e19bd2fd04867e93ecd7af719978ce68fac0cf116ab30d"}, {file = "python_gitlab-3.5.0-py3-none-any.whl", hash = "sha256:73b5aa6502efa557ee1a51227cceb0243fac5529627da34f08c5f265bf50417c"}, ] -python-levenshtein = [] +python-levenshtein = [ + {file = "python-Levenshtein-0.12.2.tar.gz", hash = "sha256:dc2395fbd148a1ab31090dd113c366695934b9e85fe5a4b2a032745efd0346f6"}, +] pytz = [ {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, @@ -1980,6 +1986,13 @@ pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -2133,7 +2146,10 @@ soupsieve = [ {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, ] -thefuzz = [] +thefuzz = [ + {file = "thefuzz-0.19.0-py2.py3-none-any.whl", hash = "sha256:4fcdde8e40f5ca5e8106bc7665181f9598a9c8b18b0a4d38c41a095ba6788972"}, + {file = "thefuzz-0.19.0.tar.gz", hash = "sha256:6f7126db2f2c8a54212b05e3a740e45f4291c497d75d20751728f635bb74aa3d"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -2146,7 +2162,10 @@ tweepy = [ {file = "tweepy-4.10.0-py3-none-any.whl", hash = "sha256:f0abbd234a588e572f880f99a094ac321217ff3eade6c0eca118ed6db8e2cf0a"}, {file = "tweepy-4.10.0.tar.gz", hash = "sha256:7f92574920c2f233663fff154745fc2bb0d10aedc23617379a912d8e4fefa399"}, ] -typing-extensions = [] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, +] tzdata = [ {file = "tzdata-2022.1-py2.py3-none-any.whl", hash = "sha256:238e70234214138ed7b4e8a0fab0e5e13872edab3be586ab8198c407620e2ab9"}, {file = "tzdata-2022.1.tar.gz", hash = "sha256:8b536a8ec63dc0751342b3984193a3118f8fca2afe25752bb9b7fffd398552d3"}, From fef96fed3269124581b31189c399ba87658764d9 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Sun, 2 Oct 2022 01:20:02 -0600 Subject: [PATCH 16/23] Add regex filtering --- jarvis/client/events/message.py | 63 ++++++++++--- jarvis/cogs/admin/__init__.py | 3 + jarvis/cogs/admin/filters.py | 151 ++++++++++++++++++++++++++++++++ jarvis/cogs/tags.py | 2 +- 4 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 jarvis/cogs/admin/filters.py diff --git a/jarvis/client/events/message.py b/jarvis/client/events/message.py index a08a7f8..289f054 100644 --- a/jarvis/client/events/message.py +++ b/jarvis/client/events/message.py @@ -4,7 +4,15 @@ from datetime import datetime, timedelta, timezone from aiohttp import ClientSession from jarvis_core.db import q -from jarvis_core.db.models import Autopurge, Autoreact, Mute, Roleping, Setting, Warning +from jarvis_core.db.models import ( + Autopurge, + Autoreact, + Filter, + Mute, + Roleping, + Setting, + Warning, +) from jarvis_core.filters import invites, url from naff import listen from naff.api.events.discord import MessageCreate, MessageDelete, MessageUpdate @@ -75,10 +83,6 @@ class MessageEventMixin: ] if (m := match.group(1)) not in allowed and setting.value: self.logger.debug(f"Removing non-allowed invite `{m}` from {message.guild.id}") - try: - await message.delete() - except Exception: - self.logger.debug("Message deleted before action taken") expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) await Warning( @@ -94,10 +98,47 @@ class MessageEventMixin: tracker.inc() embed = warning_embed(message.author, "Sent an invite link") try: - await message.channel.send(embeds=embed) + await message.reply(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") + try: + await message.delete() + except Exception: + self.logger.debug("Message deleted before action taken") + + async def filters(self, message: Message) -> None: + """Handle filter evennts.""" + filters = await Filter.find(q(guild=message.guild.id)).to_list(None) + for item in filters: + for f in item.filters: + if re.search(f, message.content, re.IGNORECASE): + expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) + await Warning( + active=True, + admin=self.user.id, + duration=24, + expires_at=expires_at, + guild=message.guild.id, + reason="Sent a message with a filtered word", + user=message.author.id, + ).commit() + tracker = warnings_tracker.labels( + guild_id=message.guild.id, guild_name=message.guild.name + ) + tracker.inc() + embed = warning_embed(message.author, "Sent a message with a filtered word") + try: + await message.reply(embeds=embed) + except Exception: + self.logger.warn("Failed to send warning embed") + + try: + await message.delete() + except Exception: + self.logger.debug("Message deleted before action taken") + return + async def massmention(self, message: Message) -> None: """Handle massmention events.""" massmention = await Setting.find_one( @@ -129,7 +170,7 @@ class MessageEventMixin: tracker.inc() embed = warning_embed(message.author, "Mass Mention") try: - await message.channel.send(embeds=embed) + await message.reply(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") @@ -193,7 +234,7 @@ class MessageEventMixin: tracker.inc() embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role") try: - await message.channel.send(embeds=embed) + await message.reply(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") @@ -218,7 +259,7 @@ class MessageEventMixin: tracker.inc() embed = warning_embed(message.author, "Phishing URL") try: - await message.channel.send(embeds=embed) + await message.reply(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") try: @@ -264,7 +305,7 @@ class MessageEventMixin: reasons = ", ".join(f"{m['source']}: {m['type']}" for m in data["matches"]) embed = warning_embed(message.author, reasons) try: - await message.channel.send(embeds=embed) + await message.reply(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") try: @@ -321,6 +362,7 @@ class MessageEventMixin: malicious = await self.malicious_url(message) if phish or malicious: await self.timeout_user(message.author, message.channel) + await self.filters(message) @listen() async def on_message_edit(self, event: MessageUpdate) -> None: @@ -376,6 +418,7 @@ class MessageEventMixin: malicious = await self.malicious_url(after) if phish or malicious: await self.timeout_user(after.author, after.channel) + await self.filters(after) @listen() async def on_message_delete(self, event: MessageDelete) -> None: diff --git a/jarvis/cogs/admin/__init__.py b/jarvis/cogs/admin/__init__.py index e524d89..0a3304b 100644 --- a/jarvis/cogs/admin/__init__.py +++ b/jarvis/cogs/admin/__init__.py @@ -5,6 +5,7 @@ from naff import Client from jarvis.cogs.admin import ( ban, + filters, kick, lock, lockdown, @@ -38,3 +39,5 @@ def setup(bot: Client) -> None: logger.debug(msg.format("roleping")) warning.WarningCog(bot) logger.debug(msg.format("warning")) + filters.FilterCog(bot) + logger.debug(msg.format("filters")) diff --git a/jarvis/cogs/admin/filters.py b/jarvis/cogs/admin/filters.py new file mode 100644 index 0000000..02b6c52 --- /dev/null +++ b/jarvis/cogs/admin/filters.py @@ -0,0 +1,151 @@ +"""Filters cog.""" +import asyncio +import difflib +from typing import Dict, List + +from jarvis_core.db import q +from jarvis_core.db.models import Filter +from naff import AutocompleteContext, Client, Extension, InteractionContext, Permissions +from naff.models.discord.modal import InputText, Modal, TextStyles +from naff.models.naff.application_commands import ( + OptionTypes, + SlashCommand, + slash_option, +) +from naff.models.naff.command import check +from thefuzz import process + +from jarvis.utils.permissions import admin_or_permissions + + +class FilterCog(Extension): + """JARVIS Filter cog.""" + + def __init__(self, bot: Client): + self.bot = bot + self.cache: Dict[int, List[str]] = {} + + async def _edit_filter(self, ctx: InteractionContext, name: str, search: bool = False) -> None: + content = "" + f: Filter = None + if search: + if f := await Filter.find_one(q(name=name, guild=ctx.guild.id)): + content = "\n".join(f.filters) + + modal = Modal( + title=f"Creating filter `{name}`", + components=[ + InputText( + label="Filter (one statement per line)", + placeholder="" if content else "i.e. $bad_word^", + custom_id="filters", + max_length=3000, + value=content, + style=TextStyles.PARAGRAPH, + ) + ], + ) + await ctx.send_modal(modal) + try: + data = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5) + filters = data.responses.get("filters").split("\n") + except asyncio.TimeoutError: + return + try: + if not f: + f = Filter(name=name, guild=ctx.guild.id, filters=filters) + else: + f.filters = filters + await f.commit() + except Exception as e: + await data.send(f"{e}", ephemeral=True) + return + + content = content.splitlines() + diff = "\n".join(difflib.ndiff(content, filters)) + + await data.send(f"Filter `{name}` has been updated:\n\n```diff\n{diff}\n```") + + if ctx.guild.id not in self.cache: + self.cache[ctx.guild.id] = [] + if name not in self.cache[ctx.guild.id]: + self.cache[ctx.guild.id].append(name) + + filter_ = SlashCommand(name="filter", description="Manage keyword filters") + + @filter_.subcommand(sub_cmd_name="create", sub_cmd_description="Create a new filter") + @slash_option( + name="name", description="Name of new filter", required=True, opt_type=OptionTypes.STRING + ) + @check(admin_or_permissions(Permissions.MANAGE_MESSAGES)) + async def _filter_create(self, ctx: InteractionContext, name: str) -> None: + return await self._edit_filter(ctx, name) + + @filter_.subcommand(sub_cmd_name="edit", sub_cmd_description="Edit a filter") + @slash_option( + name="name", + description="Filter to edit", + autocomplete=True, + opt_type=OptionTypes.STRING, + required=True, + ) + @check(admin_or_permissions(Permissions.MANAGE_MESSAGES)) + async def _filter_edit(self, ctx: InteractionContext, name: str) -> None: + return await self._edit_filter(ctx, name, True) + + @filter_.subcommand(sub_cmd_name="view", sub_cmd_description="View a filter") + @slash_option( + name="name", + description="Filter to view", + autocomplete=True, + opt_type=OptionTypes.STRING, + required=True, + ) + async def _filter_view(self, ctx: InteractionContext, name: str) -> None: + f = await Filter.find_one(q(name=name, guild=ctx.guild.id)) + if not f: + await ctx.send("That filter doesn't exist", ephemeral=True) + return + + filters = "\n".join(f.filters) + + await ctx.send(f"Filter `{name}`:\n\n```\n{filters}\n```") + + @filter_.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a filter") + @slash_option( + name="name", + description="Filter to delete", + autocomplete=True, + opt_type=OptionTypes.STRING, + required=True, + ) + @check(admin_or_permissions(Permissions.MANAGE_MESSAGES)) + async def _filter_delete(self, ctx: InteractionContext, name: str) -> None: + f = await Filter.find_one(q(name=name, guild=ctx.guild.id)) + if not f: + await ctx.send("That filter doesn't exist", ephemeral=True) + return + + try: + await f.delete() + except Exception: + self.bot.logger.debug(f"Failed to delete filter {name} in {ctx.guild.id}") + + await ctx.send(f"Filter `{name}` deleted") + self.cache[ctx.guild.id].remove(f.name) + + @_filter_edit.autocomplete("name") + @_filter_view.autocomplete("name") + @_filter_delete.autocomplete("name") + async def _autocomplete(self, ctx: AutocompleteContext, name: str) -> None: + if not self.cache.get(ctx.guild.id): + filters = await Filter.find(q(guild=ctx.guild.id)).to_list(None) + self.cache[ctx.guild.id] = [f.name for f in filters] + results = process.extract(name, self.cache.get(ctx.guild.id), limit=25) + choices = [{"name": r[0], "value": r[0]} for r in results] + await ctx.send(choices=choices) + + +def setup(bot: Client) -> None: + """Add FilterCog to JARVIS""" + FilterCog(bot) diff --git a/jarvis/cogs/tags.py b/jarvis/cogs/tags.py index 43f9f8b..a06e2ef 100644 --- a/jarvis/cogs/tags.py +++ b/jarvis/cogs/tags.py @@ -30,7 +30,7 @@ tag_name = re.compile(r"^[\w\ \-]{1,40}$") class TagCog(Extension): def __init__(self, bot: Client): self.bot = bot - self.cache: Dict[int, List[int]] = {} + self.cache: Dict[int, List[str]] = {} tag = SlashCommand(name="tag", description="Create and manage custom tags") From b7e0381b8aa22e17555f845c0d49569fa8d44214 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Mon, 3 Oct 2022 18:49:17 -0600 Subject: [PATCH 17/23] Fix some issues around names and diff contents --- jarvis/cogs/admin/filters.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/jarvis/cogs/admin/filters.py b/jarvis/cogs/admin/filters.py index 02b6c52..96b55a9 100644 --- a/jarvis/cogs/admin/filters.py +++ b/jarvis/cogs/admin/filters.py @@ -1,6 +1,7 @@ """Filters cog.""" import asyncio import difflib +import re from typing import Dict, List from jarvis_core.db import q @@ -32,8 +33,10 @@ class FilterCog(Extension): if f := await Filter.find_one(q(name=name, guild=ctx.guild.id)): content = "\n".join(f.filters) + kw = "Updating" if search else "Creating" + modal = Modal( - title=f"Creating filter `{name}`", + title=f'{kw} filter "{name}"', components=[ InputText( label="Filter (one statement per line)", @@ -51,10 +54,13 @@ class FilterCog(Extension): filters = data.responses.get("filters").split("\n") except asyncio.TimeoutError: return + # Thanks, Glitter + new_name = re.sub(r"[^\w-]", "", name) try: if not f: - f = Filter(name=name, guild=ctx.guild.id, filters=filters) + f = Filter(name=new_name, guild=ctx.guild.id, filters=filters) else: + f.name = new_name f.filters = filters await f.commit() except Exception as e: @@ -62,14 +68,16 @@ class FilterCog(Extension): return content = content.splitlines() - diff = "\n".join(difflib.ndiff(content, filters)) + diff = "\n".join(difflib.ndiff(content, filters)).replace("`", "\u200b`") - await data.send(f"Filter `{name}` has been updated:\n\n```diff\n{diff}\n```") + await data.send(f"Filter `{new_name}` has been updated:\n\n```diff\n{diff}\n```") if ctx.guild.id not in self.cache: self.cache[ctx.guild.id] = [] - if name not in self.cache[ctx.guild.id]: - self.cache[ctx.guild.id].append(name) + if new_name not in self.cache[ctx.guild.id]: + self.cache[ctx.guild.id].append(new_name) + if name != new_name: + self.cache[ctx.guild.id].remove(name) filter_ = SlashCommand(name="filter", description="Manage keyword filters") From 1a73d6bbf34c58e453ffcf6674f0ff410d08ccf8 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Tue, 4 Oct 2022 12:44:39 -0600 Subject: [PATCH 18/23] Add phishing validation and Phishlist --- jarvis/client/events/components.py | 33 +++++++- jarvis/client/events/message.py | 129 +++++++++++++++++------------ 2 files changed, 109 insertions(+), 53 deletions(-) diff --git a/jarvis/client/events/components.py b/jarvis/client/events/components.py index 8d33d3b..1eab154 100644 --- a/jarvis/client/events/components.py +++ b/jarvis/client/events/components.py @@ -1,6 +1,6 @@ """JARVIS component event mixin.""" from jarvis_core.db import q -from jarvis_core.db.models import Action, Modlog, Note, Reminder, Star +from jarvis_core.db.models import Action, Modlog, Note, Phishlist, Reminder, Star from naff import listen from naff.api.events.internal import Button from naff.models.discord.embed import EmbedField @@ -136,9 +136,40 @@ class ComponentEventMixin: await context.send("Reminder copied!", ephemeral=True) + async def _handle_phishlist_button(self, event: Button) -> None: + context = event.context + if not context.custom_id.startswith("pl|"): + return + + if not context.deferred and not context.responded: + await context.defer(ephemeral=True) + + _, valid, id_ = context.custom_id.split("|") + valid = valid == "valid" + pl = await Phishlist.find_one(q(_id=id_)) + if not pl: + self.logger.warn(f"Phishlist {id_} does not exist!") + return + + pl.valid = valid + pl.confirmed = True + + await pl.commit() + + for row in context.message.components: + for component in row.components: + component.disabled = True + + embed = context.message.embeds[0] + embed.add_field(name="Valid", value="Yes" if valid else "No") + + await context.message.edit(components=context.message.components, embeds=embed) + await context.send("Confirmed! Thank you for confirming this URL.") + @listen() async def on_button(self, event: Button) -> None: """Process button events.""" await self._handle_modcase_button(event) await self._handle_delete_button(event) await self._handle_copy_button(event) + await self._handle_phishlist_button(event) diff --git a/jarvis/client/events/message.py b/jarvis/client/events/message.py index 289f054..f3a760a 100644 --- a/jarvis/client/events/message.py +++ b/jarvis/client/events/message.py @@ -7,8 +7,8 @@ from jarvis_core.db import q from jarvis_core.db.models import ( Autopurge, Autoreact, - Filter, Mute, + Phishlist, Roleping, Setting, Warning, @@ -18,15 +18,16 @@ from naff import listen from naff.api.events.discord import MessageCreate, MessageDelete, MessageUpdate from naff.client.utils.misc_utils import find_all from naff.models.discord.channel import DMChannel, GuildText +from naff.models.discord.components import ActionRow, Button from naff.models.discord.embed import EmbedField -from naff.models.discord.enums import Permissions +from naff.models.discord.enums import ButtonStyles, Permissions from naff.models.discord.message import Message from naff.models.discord.user import Member from jarvis.branding import get_command_color +from jarvis.embeds.admin import warning_embed from jarvis.tracking import malicious_tracker, warnings_tracker from jarvis.utils import build_embed -from jarvis.utils.embeds import warning_embed class MessageEventMixin: @@ -83,6 +84,10 @@ class MessageEventMixin: ] if (m := match.group(1)) not in allowed and setting.value: self.logger.debug(f"Removing non-allowed invite `{m}` from {message.guild.id}") + try: + await message.delete() + except Exception: + self.logger.debug("Message deleted before action taken") expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) await Warning( @@ -96,49 +101,12 @@ class MessageEventMixin: ).commit() tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() - embed = warning_embed(message.author, "Sent an invite link") + embed = warning_embed(message.author, "Sent an invite link", self.user) try: - await message.reply(embeds=embed) + await message.channel.send(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") - try: - await message.delete() - except Exception: - self.logger.debug("Message deleted before action taken") - - async def filters(self, message: Message) -> None: - """Handle filter evennts.""" - filters = await Filter.find(q(guild=message.guild.id)).to_list(None) - for item in filters: - for f in item.filters: - if re.search(f, message.content, re.IGNORECASE): - expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) - await Warning( - active=True, - admin=self.user.id, - duration=24, - expires_at=expires_at, - guild=message.guild.id, - reason="Sent a message with a filtered word", - user=message.author.id, - ).commit() - tracker = warnings_tracker.labels( - guild_id=message.guild.id, guild_name=message.guild.name - ) - tracker.inc() - embed = warning_embed(message.author, "Sent a message with a filtered word") - try: - await message.reply(embeds=embed) - except Exception: - self.logger.warn("Failed to send warning embed") - - try: - await message.delete() - except Exception: - self.logger.debug("Message deleted before action taken") - return - async def massmention(self, message: Message) -> None: """Handle massmention events.""" massmention = await Setting.find_one( @@ -168,9 +136,9 @@ class MessageEventMixin: ).commit() tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() - embed = warning_embed(message.author, "Mass Mention") + embed = warning_embed(message.author, "Mass Mention", self.user) try: - await message.reply(embeds=embed) + await message.channel.send(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") @@ -232,9 +200,11 @@ class MessageEventMixin: ).commit() tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() - embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role") + embed = warning_embed( + message.author, "Pinged a blocked role/user with a blocked role", self.user + ) try: - await message.reply(embeds=embed) + await message.channel.send(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") @@ -242,6 +212,9 @@ class MessageEventMixin: """Check if the message contains any known phishing domains.""" for match in url.finditer(message.content): if (m := match.group("domain")) in self.phishing_domains: + pl = await Phishlist.find_one(q(url=m)) + if pl and pl.confirmed and not pl.valid: + return False self.logger.debug( f"Phishing url `{m}` detected in {message.guild.id}/{message.channel.id}/{message.id}" ) @@ -257,9 +230,9 @@ class MessageEventMixin: ).commit() tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() - embed = warning_embed(message.author, "Phishing URL") + embed = warning_embed(message.author, "Phishing URL", self.user) try: - await message.reply(embeds=embed) + await message.channel.send(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") try: @@ -268,12 +241,41 @@ class MessageEventMixin: self.logger.warn("Failed to delete malicious message") tracker = malicious_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() + + if not pl or not pl.confirmed: + if not pl: + pl = Phishlist(url=m) + await pl.commit() + + embed = build_embed( + title="Phishing URL detected", + description="Please confirm that this is valid", + fields=[EmbedField(name="URL", value=m)], + ) + + valid_button = Button( + style=ButtonStyles.GREEN, emoji="✔️", custom_id=f"pl|valid|{pl.id}" + ) + invalid_button = Button( + style=ButtonStyles.RED, emoji="✖️", custom_id=f"pl|invalid|{pl.id}" + ) + + channel = await self.fetch_channel(1026918337554423868) + + components = [ActionRow(invalid_button, valid_button)] + + await channel.send(embeds=embed, components=components) + return True return False async def malicious_url(self, message: Message) -> None: """Check if the message contains any known phishing domains.""" for match in url.finditer(message.content): + m = match.group("domain") + pl = await Phishlist.find_one(q(url=m)) + if pl and pl.confirmed and not pl.valid: + return False async with ClientSession() as session: resp = await session.post( "https://anti-fish.bitflow.dev/check", @@ -303,9 +305,9 @@ class MessageEventMixin: tracker = warnings_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() reasons = ", ".join(f"{m['source']}: {m['type']}" for m in data["matches"]) - embed = warning_embed(message.author, reasons) + embed = warning_embed(message.author, reasons, self.user) try: - await message.reply(embeds=embed) + await message.channel.send(embeds=embed) except Exception: self.logger.warn("Failed to send warning embed") try: @@ -314,6 +316,31 @@ class MessageEventMixin: self.logger.warn("Failed to delete malicious message") tracker = malicious_tracker.labels(guild_id=message.guild.id, guild_name=message.guild.name) tracker.inc() + + if not pl or not pl.confirmed: + if not pl: + pl = Phishlist(url=m) + await pl.commit() + + embed = build_embed( + title="Malicious URL detected", + description="Please confirm that this is valid", + fields=[EmbedField(name="URL", value=m)], + ) + + valid_button = Button( + style=ButtonStyles.GREEN, emoji="✔️", custom_id=f"pl|valid|{pl.id}" + ) + invalid_button = Button( + style=ButtonStyles.RED, emoji="✖️", custom_id=f"pl|invalid|{pl.id}" + ) + + channel = await self.fetch_channel(1026918337554423868) + + components = [ActionRow(invalid_button, valid_button)] + + await channel.send(embeds=embed, components=components) + return True return False @@ -362,7 +389,6 @@ class MessageEventMixin: malicious = await self.malicious_url(message) if phish or malicious: await self.timeout_user(message.author, message.channel) - await self.filters(message) @listen() async def on_message_edit(self, event: MessageUpdate) -> None: @@ -418,7 +444,6 @@ class MessageEventMixin: malicious = await self.malicious_url(after) if phish or malicious: await self.timeout_user(after.author, after.channel) - await self.filters(after) @listen() async def on_message_delete(self, event: MessageDelete) -> None: From adef0b4ab27388305383621820a464b58de7aa5c Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Tue, 4 Oct 2022 12:49:44 -0600 Subject: [PATCH 19/23] Update NAFF --- poetry.lock | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index df79f1b..f2e0c0f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -418,7 +418,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [[package]] name = "jarvis-core" -version = "0.12.0" +version = "0.13.1" description = "JARVIS core" category = "main" optional = false @@ -439,7 +439,7 @@ umongo = "^3.1.0" type = "git" url = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git" reference = "main" -resolved_reference = "fe24fce330cfd23a7af3834ef11b675780e6325d" +resolved_reference = "739d07885e161d66adbf946805e70b88360ddce3" [[package]] name = "jinxed" @@ -529,7 +529,7 @@ python-versions = "*" [[package]] name = "naff" -version = "1.10.0" +version = "1.11.1" description = "Not another freaking fork" category = "main" optional = false @@ -542,9 +542,10 @@ discord-typings = ">=0.5.1" tomli = "*" [package.extras] -all = ["PyNaCl (>=1.5.0,<1.6)", "cchardet", "aiodns", "orjson", "brotli", "sentry-sdk"] +all = ["PyNaCl (>=1.5.0,<1.6)", "aiodns", "orjson", "brotli", "sentry-sdk"] +docs = ["PyNaCl (>=1.5.0,<1.6)", "aiodns", "orjson", "brotli", "sentry-sdk", "mkdocs-autorefs", "mkdocs-awesome-pages-plugin", "mkdocs-material", "mkdocstrings-python", "mkdocs-minify-plugin", "mkdocs-git-committers-plugin-2", "mkdocs-git-revision-date-localized-plugin"] sentry = ["sentry-sdk"] -speedup = ["cchardet", "aiodns", "orjson", "brotli"] +speedup = ["aiodns", "orjson", "brotli"] tests = ["pytest", "pytest-recording", "pytest-asyncio", "pytest-cov", "typeguard"] voice = ["PyNaCl (>=1.5.0,<1.6)"] @@ -1593,8 +1594,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] naff = [ - {file = "naff-1.10.0-py3-none-any.whl", hash = "sha256:bb28ef19efb3f8e04f3569a3aac6b3e2738cf5747dea0bed483c458588933682"}, - {file = "naff-1.10.0.tar.gz", hash = "sha256:d0ab71c39ea5bf352228f0bc3d3dfe3610122cb01733bca4565497078de95650"}, + {file = "naff-1.11.1-py3-none-any.whl", hash = "sha256:ea9192556ad162e9990f41b1d4c315255e4354580614bdedc6ac4228c3339004"}, + {file = "naff-1.11.1.tar.gz", hash = "sha256:f1933c218b3a3ac93866245f8d2b034cc8d2b4b4ff48b76cacd37a0c2507abe2"}, ] nafftrack = [] nanoid = [ From a5126482bb9424ca5461a08d5994813693358621 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Tue, 4 Oct 2022 12:51:06 -0600 Subject: [PATCH 20/23] Catch error if interaction_tree doesn't exist --- jarvis/client/events/__init__.py | 57 +++++++++++++++++--------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/jarvis/client/events/__init__.py b/jarvis/client/events/__init__.py index d6d14c2..430419b 100644 --- a/jarvis/client/events/__init__.py +++ b/jarvis/client/events/__init__.py @@ -62,34 +62,37 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin): guild_base_commands = 0 global_context_menus = 0 guild_context_menus = 0 - for cid in self.interaction_tree: - if cid == 0: - global_base_commands = sum( - 1 - for _ in self.interaction_tree[cid] - if not isinstance(self.interaction_tree[cid][_], ContextMenu) - ) - global_context_menus = sum( - 1 - for _ in self.interaction_tree[cid] - if isinstance(self.interaction_tree[cid][_], ContextMenu) - ) - else: - guild_base_commands += sum( - 1 - for _ in self.interaction_tree[cid] - if not isinstance(self.interaction_tree[cid][_], ContextMenu) - ) - guild_context_menus += sum( - 1 - for _ in self.interaction_tree[cid] - if isinstance(self.interaction_tree[cid][_], ContextMenu) - ) + try: + for cid in self.interaction_tree: + if cid == 0: + global_base_commands = sum( + 1 + for _ in self.interaction_tree[cid] + if not isinstance(self.interaction_tree[cid][_], ContextMenu) + ) + global_context_menus = sum( + 1 + for _ in self.interaction_tree[cid] + if isinstance(self.interaction_tree[cid][_], ContextMenu) + ) + else: + guild_base_commands += sum( + 1 + for _ in self.interaction_tree[cid] + if not isinstance(self.interaction_tree[cid][_], ContextMenu) + ) + guild_context_menus += sum( + 1 + for _ in self.interaction_tree[cid] + if isinstance(self.interaction_tree[cid][_], ContextMenu) + ) + self.logger.info("Loaded {:>3} global base slash commands".format(global_base_commands)) + self.logger.info("Loaded {:>3} global context menus".format(global_context_menus)) + self.logger.info("Loaded {:>3} guild base slash commands".format(guild_base_commands)) + self.logger.info("Loaded {:>3} guild context menus".format(guild_context_menus)) + except Exception: + self.logger.error("interaction_tree not found, try updating NAFF") - self.logger.info("Loaded {:>3} global base slash commands".format(global_base_commands)) - self.logger.info("Loaded {:>3} global context menus".format(global_context_menus)) - self.logger.info("Loaded {:>3} guild base slash commands".format(guild_base_commands)) - self.logger.info("Loaded {:>3} guild context menus".format(guild_context_menus)) self.logger.debug("Hitting Reminders for faster loads") _ = await Reminder.find().to_list(None) From 42992e76f8b06ec9591619d53c67a18bb10d5d80 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Tue, 4 Oct 2022 13:01:54 -0600 Subject: [PATCH 21/23] version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c13722a..14f3810 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jarvis" -version = "2.2.3" +version = "2.3.0" description = "JARVIS admin bot" authors = ["Zevaryx "] From 6f4d93a88130b61a6eb73abd33d7c26f893e4bda Mon Sep 17 00:00:00 2001 From: zevaryx Date: Thu, 6 Oct 2022 16:18:45 +0000 Subject: [PATCH 22/23] Fix bug in reminders list --- jarvis/cogs/remindme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jarvis/cogs/remindme.py b/jarvis/cogs/remindme.py index 9d2f0ce..53a4874 100644 --- a/jarvis/cogs/remindme.py +++ b/jarvis/cogs/remindme.py @@ -181,7 +181,7 @@ class RemindmeCog(Extension): fields = [] for reminder in reminders: if reminder.private and isinstance(ctx.channel, GuildChannel): - fields.embed( + fields.append( EmbedField( name=f" ()", value="Please DM me this command to view the content of this reminder", From 5466dca31e6002c0353608550a77b27ca3140f72 Mon Sep 17 00:00:00 2001 From: zevaryx Date: Thu, 6 Oct 2022 17:45:54 +0000 Subject: [PATCH 23/23] Update userinfo embed with muted until --- jarvis/cogs/util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jarvis/cogs/util.py b/jarvis/cogs/util.py index 4b0160d..37adfa1 100644 --- a/jarvis/cogs/util.py +++ b/jarvis/cogs/util.py @@ -253,6 +253,10 @@ class UtilCog(Extension): ), ] + if muted: + ts = int(user.communication_disabled_until.timestamp()) + fields.append(EmbedField(name="Muted Until", value=f" ()")) + embed = build_embed( title="", description=user.mention,