From e3c18103b32c3d0e55e2b2a178e66b0e7a49c446 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Wed, 7 Jul 2021 11:08:38 -0600 Subject: [PATCH] Add user/modlog, fix timestamp bug on embeds, add max_message config option. Closes #10, closes #27 --- config.example.yaml | 1 + jarvis/__init__.py | 3 +- jarvis/cogs/modlog.py | 565 ++++++++++++++++++++++++++------------- jarvis/cogs/verify.py | 6 +- jarvis/config.py | 9 +- jarvis/utils/__init__.py | 2 +- 6 files changed, 398 insertions(+), 188 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 7663eee..0083067 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -10,3 +10,4 @@ api_urls: url_name: url url_name2: url2 + max_messages: 1000 diff --git a/jarvis/__init__.py b/jarvis/__init__.py index c49e968..38a07aa 100644 --- a/jarvis/__init__.py +++ b/jarvis/__init__.py @@ -142,7 +142,7 @@ async def unmute(): user = await guild.fetch_member(mute["user"]) if user: if role in user.roles: - await user.remove_roles(role, reason="Unmute") + await user.remove_roles(role, reason="Mute expired") updates.append( pymongo.UpdateOne( {"user": user.id, "guild": guild.id, "time": mute["time"]}, @@ -196,6 +196,7 @@ def run(ctx=None): ) unmute.start() unban.start() + jarvis.max_messages = config.max_messages jarvis.run(config.token, bot=True, reconnect=True) for cog in jarvis.cogs: session = getattr(cog, "_session", None) diff --git a/jarvis/cogs/modlog.py b/jarvis/cogs/modlog.py index 37428f8..e7f4f44 100644 --- a/jarvis/cogs/modlog.py +++ b/jarvis/cogs/modlog.py @@ -2,6 +2,7 @@ import asyncio from datetime import datetime, timedelta import discord +import pymongo from discord.ext import commands from discord.utils import find @@ -13,198 +14,396 @@ from jarvis.utils.field import Field class ModlogCog(commands.Cog): - def __init__(self, bot): + """ + A hybrid user/modlog functionality for J.A.R.V.I.S. + """ + + def __init__(self, bot: discord.ext.commands.Bot): self.bot = bot self.db = DBManager(get_config().mongo).mongo def get_latest_log(self, auditlog, target): before = datetime.utcnow() - timedelta(seconds=10) return find( - lambda x: x.target.id == target.id and x.created_at < before, + lambda x: x.target.id == target.id and x.created_at > before, auditlog, ) - # @commands.Cog.listener() - # async def on_member_ban(self, guild: discord.Guild, user: discord.User): - # modlog = self.db.jarvis.settings.find_one( - # {"guild": guild.id, "setting": "modlog"} - # ) - # if modlog: - # channel = guild.get_channel(modlog["value"]) - # await asyncio.sleep(0.5) # Need to wait for audit log - # auditlog = await guild.audit_logs( - # limit=50, - # action=discord.AuditLogAction.ban, - # after=datetime.utcnow() - timedelta(seconds=15), - # oldest_first=False, - # ) - # log: discord.AuditLogEntry = self.get_latest_log(auditlog, user) - # fields = [ - # Field( - # name="Member", - # value=f"{user.mention} ({user.name}#{user.descriminator})", - # ), - # Field( - # name="Moderator", - # value=f"{user.mention} ({user.name}#{user.descriminator})", - # ), - # Field(name="Reason", value=log.reason, inline=False), - # ] - # embed = build_embed( - # title="User Banned", - # description=f"A user was banned from {guild.name}", - # color="#bf2a3e", - # fields=fields, - # timestamp=log.created_at, - # ) - # embed.set_author( - # name=f"{user.name}#{user.discriminator} | {user.id}", - # icon_url=user.avatar_url, - # ) - # - # await channel.send(embed=embed) - # - # @commands.Cog.listener() - # async def on_member_unban(self, guild: discord.Guild, user: discord.User): - # modlog = self.db.jarvis.settings.find_one( - # {"guild": guild.id, "setting": "modlog"} - # ) - # if modlog: - # channel = guild.get_channel(modlog["value"]) - # await asyncio.sleep(0.5) # Need to wait for audit log - # auditlog = await guild.audit_logs( - # limit=50, - # action=discord.AuditLogAction.unban, - # after=datetime.utcnow() - timedelta(seconds=15), - # oldest_first=False, - # ) - # log: discord.AuditLogEntry = self.get_latest_log(auditlog, user) - # fields = [ - # Field( - # name="Member", - # value=f"{user.mention} ({user.name}#{user.descriminator})", - # ), - # Field( - # name="Moderator", - # value=f"{user.mention} ({user.name}#{user.descriminator})", - # ), - # Field(name="Reason", value=log.reason, inline=False), - # ] - # embed = build_embed( - # title="User Unbanned", - # description=f"A user was unbanned from {guild.name}", - # color="#bf2a3e", - # fields=fields, - # timestamp=log.created_at, - # ) - # embed.set_author( - # name=f"{user.name}#{user.discriminator} | {user.id}", - # icon_url=user.avatar_url, - # ) - # - # await channel.send(embed=embed) - # - # @commands.Cog.listener() - # async def on_member_remove(self, user: discord.User): - # modlog = self.db.jarvis.settings.find_one( - # {"guild": user.guild.id, "setting": "modlog"} - # ) - # if modlog: - # channel = user.guild.get_channel(modlog["value"]) - # await asyncio.sleep(0.5) # Need to wait for audit log - # auditlog = await user.guild.audit_logs( - # limit=50, - # action=discord.AuditLogAction.kick, - # after=datetime.utcnow() - timedelta(seconds=15), - # oldest_first=False, - # ) - # log: discord.AuditLogEntry = self.get_latest_log(auditlog, user) - # fields = [ - # Field( - # name="Member", - # value=f"{user.mention} ({user.name}#{user.descriminator})", - # ), - # Field( - # name="Moderator", - # value=f"{user.mention} ({user.name}#{user.descriminator})", - # ), - # Field(name="Reason", value=log.reason, inline=False), - # ] - # embed = build_embed( - # title="User Kicked", - # description=f"A user was kicked from {guild.name}", - # color="#bf2a3e", - # fields=fields, - # timestamp=log.created_at, - # ) - # embed.set_author( - # name=f"{user.name}#{user.discriminator} | {user.id}", - # icon_url=user.avatar_url, - # ) - # - # await channel.send(embed=embed) - # - # @commands.Cog.listener() - # async def on_member_update( - # self, before: discord.User, after: discord.User - # ): - # modlog = self.db.jarvis.settings.find_one( - # {"guild": user.guild.id, "setting": "modlog"} - # ) - # if modlog: - # channel = user.guild.get_channel(modlog["value"]) - # await asyncio.sleep(0.5) # Need to wait for audit log - # embed = None - # mute = self.db.jarvis.settings.find_one( - # {"guild": before.guild.id, "setting": "mute"} - # ) - # verified = self.db.jarvis.settings.find_one( - # {"guild": before.guild.id, "setting": "verified"} - # ) - # if mute and before.guild.get_role(mute["value"]) in after.roles: - # # TODO: User was muted - # pass - # elif mute and before.guild.get_role(mute["value"]) in before.roles: - # # TODO: User was unmuted - # pass - # elif ( - # verified - # and before.guild.get_role(verified["value"]) in before.roles - # ): - # # TODO: User was verified - # pass - # - # auditlog = await user.guild.audit_logs( - # limit=50, - # action=discord.AuditLogAction.kick, - # after=datetime.utcnow() - timedelta(seconds=15), - # oldest_first=False, - # ) - # log: discord.AuditLogEntry = self.get_latest_log(auditlog, user) - # fields = [ - # Field( - # name="Member", - # value=f"{user.mention} ({user.name}#{user.descriminator})", - # ), - # Field( - # name="Moderator", - # value=f"{user.mention} ({user.name}#{user.descriminator})", - # ), - # Field(name="Reason", value=log.reason, inline=False), - # ] - # embed = build_embed( - # title="User Kicked", - # description=f"A user was kicked from {guild.name}", - # color="#bf2a3e", - # fields=fields, - # timestamp=log.created_at, - # ) - # embed.set_author( - # name=f"{user.name}#{user.discriminator} | {user.id}", - # icon_url=user.avatar_url, - # ) - # - # await channel.send(embed=embed) + async def modlog_embed( + self, + member: discord.Member, + admin: discord.Member, + log: discord.AuditLogEntry, + title: str, + desc: str, + ) -> discord.Embed: + fields = [ + Field( + name="Moderator", + value=f"{admin.mention} ({admin.name}" + + f"#{admin.discriminator})", + ), + Field(name="Reason", value=log.reason, inline=False), + ] + embed = build_embed( + title=title, + description=desc, + color="#fc9e3f", + fields=fields, + timestamp=log.created_at, + ) + embed.set_author( + name=f"{member.name}", + icon_url=member.avatar_url, + ) + embed.set_footer( + text=f"{member.name}#{member.discriminator} | {member.id}" + ) + return embed + + @commands.Cog.listener() + async def on_member_ban(self, guild: discord.Guild, user: discord.User): + modlog = self.db.jarvis.settings.find_one( + {"guild": guild.id, "setting": "modlog"} + ) + if modlog: + channel = guild.get_channel(modlog["value"]) + await asyncio.sleep(0.5) # Need to wait for audit log + auditlog = await guild.audit_logs( + limit=50, + action=discord.AuditLogAction.ban, + after=datetime.utcnow() - timedelta(seconds=15), + oldest_first=False, + ) + log: discord.AuditLogEntry = self.get_latest_log(auditlog, user) + admin: discord.User = log.user + if admin.id == get_config().client_id: + mute = self.db.jarvis.bans.find_one( + { + "guild": guild.id, + "user": user.id, + "active": True, + }, + sort=[("time", pymongo.DESCENDING)], + ) + admin = guild.get_member(mute["admin"]) + embed = await self.modlog_embed( + user, + admin, + log, + "User banned", + f"{user.mention} was banned from {guild.name}", + ) + + await channel.send(embed=embed) + + @commands.Cog.listener() + async def on_member_unban(self, guild: discord.Guild, user: discord.User): + modlog = self.db.jarvis.settings.find_one( + {"guild": guild.id, "setting": "modlog"} + ) + if modlog: + channel = guild.get_channel(modlog["value"]) + await asyncio.sleep(0.5) # Need to wait for audit log + auditlog = await guild.audit_logs( + limit=50, + action=discord.AuditLogAction.unban, + after=datetime.utcnow() - timedelta(seconds=15), + oldest_first=False, + ) + log: discord.AuditLogEntry = self.get_latest_log(auditlog, user) + admin: discord.User = log.user + if admin.id == get_config().client_id: + mute = self.db.jarvis.bans.find_one( + { + "guild": guild.id, + "user": user.id, + "active": True, + }, + sort=[("time", pymongo.DESCENDING)], + ) + admin = guild.get_member(mute["admin"]) + embed = await self.modlog_embed( + user, + admin, + log, + "User unbanned", + f"{user.mention} was unbanned from {guild.name}", + ) + + await channel.send(embed=embed) + + @commands.Cog.listener() + async def on_member_remove(self, user: discord.User): + modlog = self.db.jarvis.settings.find_one( + {"guild": user.guild.id, "setting": "modlog"} + ) + if modlog: + channel = user.guild.get_channel(modlog["value"]) + await asyncio.sleep(0.5) # Need to wait for audit log + auditlog = await user.guild.audit_logs( + limit=50, + action=discord.AuditLogAction.kick, + after=datetime.utcnow() - timedelta(seconds=15), + oldest_first=False, + ) + log: discord.AuditLogEntry = self.get_latest_log(auditlog, user) + admin: discord.User = log.user + if admin.id == get_config().client_id: + mute = self.db.jarvis.kicks.find_one( + { + "guild": user.guild.id, + "user": user.id, + }, + sort=[("time", pymongo.DESCENDING)], + ) + admin = user.guild.get_member(mute["admin"]) + embed = await self.modlog_embed( + user, + admin, + log, + "User Kicked", + f"{user.mention} was kicked from {user.guild.name}", + ) + + await channel.send(embed=embed) + + async def process_mute(self, before, after) -> discord.Embed: + auditlog = await before.guild.audit_logs( + limit=50, + action=discord.AuditLogAction.member_role_update, + after=datetime.utcnow() - timedelta(seconds=15), + oldest_first=False, + ).flatten() + log: discord.AuditLogEntry = self.get_latest_log(auditlog, before) + admin: discord.User = log.user + if admin.id == get_config().client_id: + mute = self.db.jarvis.mutes.find_one( + { + "guild": before.guild.id, + "user": before.id, + "active": True, + }, + sort=[("time", pymongo.DESCENDING)], + ) + admin = before.guild.get_member(mute["admin"]) + return await self.modlog_embed( + member=before, + admin=admin, + log=log, + title="User Muted", + desc=f"{before.mention} was muted", + ) + + async def process_unmute(self, before, after) -> discord.Embed: + auditlog = await before.guild.audit_logs( + limit=50, + action=discord.AuditLogAction.member_role_update, + after=datetime.utcnow() - timedelta(seconds=15), + oldest_first=False, + ).flatten() + log: discord.AuditLogEntry = self.get_latest_log(auditlog, before) + admin: discord.User = log.user + if admin.id == get_config().client_id: + mute = self.db.jarvis.mutes.find_one( + { + "guild": before.guild.id, + "user": before.id, + "active": True, + }, + sort=[("time", pymongo.DESCENDING)], + ) + admin = before.guild.get_member(mute["admin"]) + return await self.modlog_embed( + member=before, + admin=admin, + log=log, + title="User Muted", + desc=f"{before.mention} was muted", + ) + + async def process_verify(self, before, after) -> discord.Embed: + auditlog = await before.guild.audit_logs( + limit=50, + action=discord.AuditLogAction.member_role_update, + after=datetime.utcnow() - timedelta(seconds=15), + oldest_first=False, + ).flatten() + log: discord.AuditLogEntry = self.get_latest_log(auditlog, before) + admin: discord.User = log.user + return await self.modlog_embed( + member=before, + admin=admin, + log=log, + title="User Verified", + desc=f"{before.mention} was verified", + ) + + async def process_rolechange(self, before, after) -> discord.Embed: + auditlog = await before.guild.audit_logs( + limit=50, + action=discord.AuditLogAction.member_role_update, + after=datetime.utcnow() - timedelta(seconds=15), + oldest_first=False, + ).flatten() + log: discord.AuditLogEntry = self.get_latest_log(auditlog, before) + admin: discord.User = log.user + role = None + title = "User Given Role" + verb = "was given" + if len(before.roles) > len(after.roles): + title = "User Forfeited Role" + verb = "forfeited" + role = find(lambda x: x not in after.roles, before.roles) + elif len(before.roles) < len(after.roles): + role = find(lambda x: x not in before.roles, after.roles) + role_text = role.mention if role else "||`[redacted]`||" + return await self.modlog_embed( + member=before, + admin=admin, + log=log, + title=title, + desc=f"{before.mention} {verb} role {role_text}", + ) + + @commands.Cog.listener() + async def on_member_update( + self, before: discord.User, after: discord.User + ): + modlog = self.db.jarvis.settings.find_one( + {"guild": after.guild.id, "setting": "modlog"} + ) + if modlog: + channel = after.guild.get_channel(modlog["value"]) + await asyncio.sleep(0.5) # Need to wait for audit log + embed = None + mute = self.db.jarvis.settings.find_one( + {"guild": before.guild.id, "setting": "mute"} + ) + verified = self.db.jarvis.settings.find_one( + {"guild": before.guild.id, "setting": "verified"} + ) + if mute and before.guild.get_role(mute["value"]) in after.roles: + embed = await self.process_mute(before, after) + elif mute and before.guild.get_role(mute["value"]) in before.roles: + embed = await self.process_unmute(before, after) + elif ( + verified + and before.guild.get_role(verified["value"]) + not in before.roles + ): + embed = await self.process_verify(before, after) + elif before.nick != after.nick: + auditlog = await before.guild.audit_logs( + limit=50, + action=discord.AuditLogAction.member_update, + after=datetime.utcnow() - timedelta(seconds=15), + oldest_first=False, + ).flatten() + log: discord.AuditLogEntry = self.get_latest_log( + auditlog, before + ) + bname = before.nick if before.nick else before.name + aname = after.nick if after.nick else after.name + fields = [ + Field( + name="Before", + value=f"{bname} ({before.name}" + + f"#{before.discriminator})", + ), + Field( + name="After", + value=f"{aname} ({after.name}" + + f"#{after.discriminator})", + ), + ] + if log.user.id != before.id: + fields.append( + Field( + name="Moderator", + value=f"{log.user.mention} ({log.user.name}" + + f"#{log.user.discriminator})", + ) + ) + fields.append( + Field(name="Reason", value=log.reason, inline=False), + ) + embed = build_embed( + title="User Nick Changed", + description=f"{after.mention} changed their nickname", + color="#fc9e3f", + fields=fields, + timestamp=log.created_at, + ) + embed.set_author( + name=f"{after.name}", + icon_url=after.avatar_url, + ) + embed.set_footer( + text=f"{after.name}#{after.discriminator} | {after.id}" + ) + elif len(before.roles) != len(after.roles): + # TODO: User got a new role + embed = await self.process_rolechange(before, after) + if embed: + await channel.send(embed=embed) + + @commands.Cog.listener() + async def on_message_edit( + self, before: discord.Message, after: discord.Message + ): + modlog = self.db.jarvis.settings.find_one( + {"guild": after.guild.id, "setting": "modlog"} + ) + if modlog: + channel = before.guild.get_channel(modlog["value"]) + fields = [ + Field("Original Message", before.content, False), + Field("New Message", after.content, False), + ] + embed = build_embed( + title="Message Edited", + description=f"{before.author.mention} edited a message", + fields=fields, + color="#fc9e3f", + timestamp=after.edited_at, + url=after.jump_url, + ) + embed.set_author( + name=before.author.name, + icon_url=before.author.avatar_url, + url=after.jump_url, + ) + embed.set_footer( + text=f"{before.author.name}#{before.author.discriminator}" + + f" | {before.author.id}" + ) + await channel.send(embed=embed) + + @commands.Cog.listener() + async def on_message_delete(self, message: discord.Message): + modlog = self.db.jarvis.settings.find_one( + {"guild": message.guild.id, "setting": "modlog"} + ) + if modlog: + channel = message.guild.get_channel(modlog["value"]) + fields = [Field("Original Message", message.content, False)] + embed = build_embed( + title="Message Deleted", + description=f"{message.author.mention} deleted a message", + fields=fields, + color="#fc9e3f", + ) + embed.set_author( + name=message.author.name, + icon_url=message.author.avatar_url, + url=message.jump_url, + ) + embed.set_footer( + text=f"{message.author.name}#{message.author.discriminator}" + + f" | {message.author.id}" + ) + await channel.send(embed=embed) def setup(bot): diff --git a/jarvis/cogs/verify.py b/jarvis/cogs/verify.py index 5d280a3..a8879f0 100644 --- a/jarvis/cogs/verify.py +++ b/jarvis/cogs/verify.py @@ -79,13 +79,15 @@ class VerifyCog(commands.Cog): {"setting": "verified", "guild": ctx.guild.id} ) role = ctx.guild.get_role(setting["value"]) - await ctx.author.add_roles(role, reason="Verified") + await ctx.author.add_roles(role, reason="Verification passed") setting = self.db.jarvis.settings.find_one( {"setting": "unverified", "guild": ctx.guild.id} ) if setting: role = ctx.guild.get_role(setting["value"]) - await ctx.author.remove_roles(role, reason="Verified") + await ctx.author.remove_roles( + role, reason="Verification passed" + ) await ctx.edit_origin( content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.", components=manage_components.spread_to_rows( diff --git a/jarvis/config.py b/jarvis/config.py index 9b4e6ee..5ce6e86 100644 --- a/jarvis/config.py +++ b/jarvis/config.py @@ -18,13 +18,20 @@ class Config(object): return it def init( - self, token: str, client_id: str, logo: str, mongo: dict, urls: dict + self, + token: str, + client_id: str, + logo: str, + mongo: dict, + urls: dict, + max_messages: int = 1000, ): self.token = token self.client_id = client_id self.logo = logo self.mongo = mongo self.urls = urls + self.max_messages = max_messages db = DBManager(config=mongo).mongo.jarvis.config db_config = db.find() for item in db_config: diff --git a/jarvis/utils/__init__.py b/jarvis/utils/__init__.py index 39ae0c6..b34862c 100644 --- a/jarvis/utils/__init__.py +++ b/jarvis/utils/__init__.py @@ -58,7 +58,7 @@ def build_embed( **kwargs, ) -> Embed: if not timestamp: - timestamp = datetime.now() + timestamp = datetime.utcnow() embed = Embed( title=title, description=description,