diff --git a/README.md b/README.md index 8022f62..a2903b8 100755 --- a/README.md +++ b/README.md @@ -72,10 +72,10 @@ Main goal of this project is to get Robocop functionality done, secondary goal i - [ ] New feature: Highlights (problematic words automatically get posted to modmail channel) - [ ] New feature: Modmail - [ ] New feature: Submiterr -- [ ] New moderation feature: Display of mutes on listwarns -- [ ] New moderation feature: User notes - [ ] New moderation feature: mutetime (mute with time) - [ ] New moderation feature: timelock (channel lockdown with time) +- [x] New moderation feature: Display of mutes, bans and kicks on listwarns (.userlog now) +- [x] New moderation feature: User notes - [x] New moderation feature: Reaction removing features (thanks misson20000!) - [x] New moderation feature: User nickname change - [x] New self-moderation feature: .mywarns diff --git a/Robocop.py b/Robocop.py index 2ef0c2e..085191e 100755 --- a/Robocop.py +++ b/Robocop.py @@ -47,6 +47,9 @@ initial_extensions = ['cogs.common', 'cogs.legacy', 'cogs.links', 'cogs.mod', + 'cogs.mod_note', + 'cogs.mod_reacts', + 'cogs.mod_userlog', 'cogs.meme'] bot = commands.Bot(command_prefix=get_prefix, diff --git a/cogs/mod.py b/cogs/mod.py index e7126cc..e81b3d6 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -1,10 +1,9 @@ -import asyncio import discord from discord.ext import commands import config -import json -import time from helpers.checks import check_if_staff +from helpers.userlogs import userlog +from helpers.restrictions import add_restriction, remove_restriction class Mod: @@ -14,30 +13,7 @@ class Mod: def check_if_target_is_staff(self, target): return any(r.id in config.staff_role_ids for r in target.roles) - async def add_restriction(self, member, rst): - # from kurisu source, credits go to ihaveamac - with open("data/restrictions.json", "r") as f: - rsts = json.load(f) - if str(member.id) not in rsts: - rsts[str(member.id)] = [] - if rst not in rsts[str(member.id)]: - rsts[str(member.id)].append(rst) - with open("data/restrictions.json", "w") as f: - json.dump(rsts, f) - - async def remove_restriction(self, member, rst): - # from kurisu source, credits go to ihaveamac - with open("data/restrictions.json", "r") as f: - rsts = json.load(f) - if str(member.id) not in rsts: - rsts[str(member.id)] = [] - if rst in rsts[str(member.id)]: - rsts[str(member.id)].remove(rst) - with open("data/restrictions.json", "w") as f: - json.dump(rsts, f) - @commands.guild_only() - @commands.bot_has_permissions(kick_members=True) @commands.check(check_if_staff) @commands.command() async def mute(self, ctx, target: discord.Member, *, reason: str = ""): @@ -49,6 +25,8 @@ class Mod: return await ctx.send("I can't mute this user as " "they're a member of staff.") + userlog(target.id, ctx.author, reason, "mutes", target.name) + safe_name = self.bot.escape_message(str(target)) dm_message = f"You were muted!" @@ -79,10 +57,9 @@ class Mod: log_channel = self.bot.get_channel(config.log_channel) await log_channel.send(chan_message) await ctx.send(f"{target.mention} can no longer speak.") - await self.add_restriction(target, config.mute_role) + await add_restriction(target, config.mute_role) @commands.guild_only() - @commands.bot_has_permissions(kick_members=True) @commands.check(check_if_staff) @commands.command() async def unmute(self, ctx, target: discord.Member): @@ -99,7 +76,7 @@ class Mod: log_channel = self.bot.get_channel(config.log_channel) await log_channel.send(chan_message) await ctx.send(f"{target.mention} can now speak again.") - await self.remove_restriction(target, config.mute_role) + await remove_restriction(target, config.mute_role) @commands.guild_only() @commands.bot_has_permissions(kick_members=True) @@ -114,6 +91,8 @@ class Mod: return await ctx.send("I can't kick this user as " "they're a member of staff.") + userlog(target.id, ctx.author, reason, "kicks", target.name) + safe_name = self.bot.escape_message(str(target)) dm_message = f"You were kicked from {ctx.guild.name}." @@ -156,6 +135,8 @@ class Mod: return await ctx.send("I can't ban this user as " "they're a member of staff.") + userlog(target.id, ctx.author, reason, "bans", target.name) + safe_name = self.bot.escape_message(str(target)) dm_message = f"You were banned from {ctx.guild.name}." @@ -201,6 +182,8 @@ class Mod: return await ctx.send("I can't ban this user as " "they're a member of staff.") + userlog(target, ctx.author, reason, "bans", target_user.name) + safe_name = self.bot.escape_message(str(target_user)) await ctx.guild.ban(target_user, @@ -220,20 +203,6 @@ class Mod: await log_channel.send(chan_message) await ctx.send(f"{safe_name} is now b&. 👍") - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command(aliases=['echo']) - async def say(self, ctx, *, the_text: str): - """Repeats a given text, staff only.""" - await ctx.send(the_text) - - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command() - async def speak(self, ctx, channel: discord.TextChannel, *, the_text: str): - """Repeats a given text in a given channel, staff only.""" - await channel.send(the_text) - @commands.guild_only() @commands.bot_has_permissions(ban_members=True) @commands.check(check_if_staff) @@ -247,6 +216,8 @@ class Mod: return await ctx.send("I can't ban this user as " "they're a member of staff.") + userlog(target.id, ctx.author, reason, "bans", target.name) + safe_name = self.bot.escape_message(str(target)) await target.ban(reason=f"{ctx.author}, reason: {reason}", @@ -264,25 +235,6 @@ class Mod: log_channel = self.bot.get_channel(config.log_channel) await log_channel.send(chan_message) - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command() - async def userinfo(self, ctx, *, user: discord.Member): - """Gets user info, staff only.""" - role = user.top_role.name - if role == "@everyone": - role = "@ everyone" - await ctx.send(f"user = {user}\n" - f"id = {user.id}\n" - f"avatar = {user.avatar_url}\n" - f"bot = {user.bot}\n" - f"created_at = {user.created_at}\n" - f"display_name = {user.display_name}\n" - f"joined_at = {user.joined_at}\n" - f"activities = `{user.activities}`\n" - f"color = {user.colour}\n" - f"top_role = {role}\n") - @commands.guild_only() @commands.check(check_if_staff) @commands.command() @@ -329,50 +281,6 @@ class Mod: await log_channel.send(f"❌ Un-approved: {ctx.author.mention} removed" f" {role} from {target.mention}") - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command(aliases=["setplaying", "setgame"]) - async def playing(self, ctx, *, game: str = ""): - """Sets the bot's currently played game name, staff only. - - Just send .playing to wipe the playing state.""" - if game: - await self.bot.change_presence(activity=discord.Game(name=game)) - else: - await self.bot.change_presence(activity=None) - - await ctx.send("Successfully set game.") - - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command(aliases=["setbotnick", "botnick", "robotnick"]) - async def botnickname(self, ctx, *, nick: str = ""): - """Sets the bot's nickname, staff only. - - Just send .botnickname to wipe the nickname.""" - - if nick: - await ctx.guild.me.edit(nick=nick, reason=str(ctx.author)) - else: - await ctx.guild.me.edit(nick=None, reason=str(ctx.author)) - - await ctx.send("Successfully set nickname.") - - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command(aliases=["setnick", "nick"]) - async def nickname(self, ctx, target: discord.Member, *, nick: str = ""): - """Sets a user's nickname, staff only. - - Just send .nickname to wipe the nickname.""" - - if nick: - await target.edit(nick=nick, reason=str(ctx.author)) - else: - await target.edit(nick=None, reason=str(ctx.author)) - - await ctx.send("Successfully set nickname.") - @commands.guild_only() @commands.check(check_if_staff) @commands.command(aliases=["clear"]) @@ -399,21 +307,8 @@ class Mod: "they're a member of staff.") log_channel = self.bot.get_channel(config.log_channel) - with open("data/warnsv2.json", "r") as f: - warns = json.load(f) - if str(target.id) not in warns: - warns[str(target.id)] = {"warns": []} - warns[str(target.id)]["name"] = str(target) - timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - warn_data = {"issuer_id": ctx.author.id, - "issuer_name": ctx.author.name, - "reason": reason, - "timestamp": timestamp} - warns[str(target.id)]["warns"].append(warn_data) - with open("data/warnsv2.json", "w") as f: - json.dump(warns, f) - - warn_count = len(warns[str(target.id)]["warns"]) + warn_count = userlog(target.id, ctx.author, reason, + "warns", target.name) msg = f"You were warned on {ctx.guild.name}." if reason: @@ -457,254 +352,83 @@ class Mod: " as the reason is automatically sent to the user." await log_channel.send(msg) - def get_warns_embed_for_id(self, uid: str, name: str): - embed = discord.Embed(color=discord.Color.dark_red()) - embed.set_author(name=f"Warns for {name}") - with open("data/warnsv2.json", "r") as f: - warns = json.load(f) - try: - if len(warns[uid]["warns"]): - for idx, warn in enumerate(warns[uid]["warns"]): - embed.add_field(name=f"{idx + 1}: {warn['timestamp']}", - value=f"Issuer: {warn['issuer_name']}\n" - f"Reason: {warn['reason']}") - else: - embed.description = "There are none!" - embed.color = discord.Color.green() - except KeyError: # if the user is not in the file - embed.description = "ID doesn't exist in saved "\ - "warns (there likely aren't any warns)." - embed.color = discord.Color.green() - return embed - - def clear_warns_from_id(self, uid: str): - with open("data/warnsv2.json", "r") as f: - warns = json.load(f) - if uid not in warns: - return f"<@{uid}> has no warns!" - warn_count = len(warns[uid]["warns"]) - if not warn_count: - return f"<@{uid}> has no warns!" - warns[uid]["warns"] = [] - with open("data/warnsv2.json", "w") as f: - json.dump(warns, f) - return f"<@{uid}> no longer has any warns!" - - def delete_warns_from_id(self, uid: str, idx: int): - with open("data/warnsv2.json", "r") as f: - warns = json.load(f) - if uid not in warns: - return f"<@{uid}> has no warns!" - warn_count = len(warns[uid]["warns"]) - if not warn_count: - return f"<@{uid}> has no warns!" - if idx > warn_count: - return "Warn index is higher than "\ - f"warn count ({warn_count})!" - if idx < 1: - return "Warn index is below 1!" - warn = warns[uid]["warns"][idx - 1] - embed = discord.Embed(color=discord.Color.dark_red(), - title=f"Warn {idx} on {warn['timestamp']}", - description=f"Issuer: {warn['issuer_name']}\n" - f"Reason: {warn['reason']}") - del warns[uid]["warns"][idx - 1] - with open("data/warnsv2.json", "w") as f: - json.dump(warns, f) - return embed - @commands.guild_only() @commands.check(check_if_staff) - @commands.command() - async def listwarns(self, ctx, target: discord.Member): - """Lists warns for a user, staff only.""" - embed = self.get_warns_embed_for_id(str(target.id), str(target)) - await ctx.send(embed=embed) + @commands.command(aliases=["setnick", "nick"]) + async def nickname(self, ctx, target: discord.Member, *, nick: str = ""): + """Sets a user's nickname, staff only. - @commands.guild_only() - @commands.command() - async def mywarns(self, ctx): - """Lists your warns.""" - embed = discord.Embed(color=discord.Color.dark_red()) - uid = str(ctx.author.id) - embed.set_author(name=f"{ctx.author.name}'s warns") - with open("data/warnsv2.json", "r") as f: - warns = json.load(f) - try: - if len(warns[uid]["warns"]): - for idx, warn in enumerate(warns[uid]["warns"]): - embed.add_field(name=f"{idx + 1}: {warn['timestamp']}", - value=f"Reason: {warn['reason']}") - else: - embed.description = "There are none! Good for you." - embed.color = discord.Color.green() - except KeyError: # if the user is not in the file - embed.description = "ID doesn't exist in saved "\ - "warns (there likely aren't any warns)." - embed.color = discord.Color.green() - await ctx.send(embed=embed) + Just send .nickname to wipe the nickname.""" - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command() - async def listwarnsid(self, ctx, target: int): - """Lists warns for a user by ID, staff only.""" - embed = self.get_warns_embed_for_id(str(target), str(target)) - await ctx.send(embed=embed) - - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command() - async def clearwarns(self, ctx, target: discord.Member): - """Clears all warns for a user, staff only.""" - log_channel = self.bot.get_channel(config.log_channel) - msg = self.clear_warns_from_id(str(target.id)) - await ctx.send(msg) - msg = f"🗑 **Cleared warns**: {ctx.member.mention} cleared"\ - f" warns of {target.mention} | "\ - f"{self.bot.escape_message(target)}" - await log_channel.send(msg) - - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command() - async def clearwarnsid(self, ctx, target: int): - """Clears all warns for a user from their userid, staff only.""" - log_channel = self.bot.get_channel(config.log_channel) - msg = self.clear_warns_from_id(str(target)) - await ctx.send(msg) - msg = f"🗑 **Cleared warns**: {ctx.member.mention} cleared"\ - f" warns of <@{target}> " - await log_channel.send(msg) - - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command() - async def delwarn(self, ctx, target: discord.Member, idx: int): - """Removes a specific warn from a user, staff only.""" - log_channel = self.bot.get_channel(config.log_channel) - del_warn = self.delete_warns_from_id(str(target.id), idx) - # This is hell. - if isinstance(del_warn, discord.Embed): - await ctx.send(f"{target.mention} has a warning removed!") - msg = f"🗑 **Deleted warn**: {ctx.author.mention} removed "\ - f"warn {idx} from {target.mention} | "\ - f"{self.bot.escape_message(target)}" - await log_channel.send(msg, embed=del_warn) + if nick: + await target.edit(nick=nick, reason=str(ctx.author)) else: - await ctx.send(del_warn) + await target.edit(nick=None, reason=str(ctx.author)) + + await ctx.send("Successfully set nickname.") @commands.guild_only() @commands.check(check_if_staff) @commands.command() - async def delwarnid(self, ctx, target: int, idx: int): - """Removes a specific warn from a user, staff only.""" - log_channel = self.bot.get_channel(config.log_channel) - del_warn = self.delete_warns_from_id(str(target), idx) - # This is hell. - if isinstance(del_warn, discord.Embed): - await ctx.send(f"<@{target}> has a warning removed!") - msg = f"🗑 **Deleted warn**: {ctx.author.mention} removed "\ - f"warn {idx} from <@{target}> " - await log_channel.send(msg, embed=del_warn) + async def userinfo(self, ctx, *, user: discord.Member): + """Gets user info, staff only.""" + role = user.top_role.name + if role == "@everyone": + role = "@ everyone" + await ctx.send(f"user = {user}\n" + f"id = {user.id}\n" + f"avatar = {user.avatar_url}\n" + f"bot = {user.bot}\n" + f"created_at = {user.created_at}\n" + f"display_name = {user.display_name}\n" + f"joined_at = {user.joined_at}\n" + f"activities = `{user.activities}`\n" + f"color = {user.colour}\n" + f"top_role = {role}\n") + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=['echo']) + async def say(self, ctx, *, the_text: str): + """Repeats a given text, staff only.""" + await ctx.send(the_text) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command() + async def speak(self, ctx, channel: discord.TextChannel, *, the_text: str): + """Repeats a given text in a given channel, staff only.""" + await channel.send(the_text) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=["setplaying", "setgame"]) + async def playing(self, ctx, *, game: str = ""): + """Sets the bot's currently played game name, staff only. + + Just send .playing to wipe the playing state.""" + if game: + await self.bot.change_presence(activity=discord.Game(name=game)) else: - await ctx.send(del_warn) + await self.bot.change_presence(activity=None) + + await ctx.send("Successfully set game.") @commands.guild_only() @commands.check(check_if_staff) - @commands.command() - async def clearreactsbyuser(self, ctx, user: discord.Member, *, - channel: discord.TextChannel = None, - limit: int = 50): - """Clears reacts from a given user in the given channel, staff only.""" - log_channel = self.bot.get_channel(config.log_channel) - if not channel: - channel = ctx.channel - count = 0 - async for msg in channel.history(limit=limit): - for react in msg.reactions: - if await react.users().find(lambda u: u == user): - count += 1 - async for u in react.users(): - await msg.remove_reaction(react, u) - msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared "\ - f"{user.mention}'s reacts from the last {limit} messages "\ - f"in {channel.mention}." - await ctx.channel.send(f"Cleared {count} unique reactions") - await log_channel.send(msg) + @commands.command(aliases=["setbotnick", "botnick", "robotnick"]) + async def botnickname(self, ctx, *, nick: str = ""): + """Sets the bot's nickname, staff only. - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command() - async def clearallreacts(self, ctx, *, - limit: int = 50, - channel: discord.TextChannel = None): - """Clears all reacts in a given channel, staff only. Use with care.""" - log_channel = self.bot.get_channel(config.log_channel) - if not channel: - channel = ctx.channel - count = 0 - async for msg in channel.history(limit=limit): - if msg.reactions: - count += 1 - await msg.clear_reactions() - msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared all "\ - f"reacts from the last {limit} messages in {channel.mention}." - await ctx.channel.send(f"Cleared reacts from {count} messages!") - await log_channel.send(msg) - - @commands.guild_only() - @commands.check(check_if_staff) - @commands.command() - async def clearreactsinteractive(self, ctx): - """Clears reacts interactively, staff only. Use with care.""" - log_channel = self.bot.get_channel(config.log_channel) + Just send .botnickname to wipe the nickname.""" - msg_text = f"{ctx.author.mention}, react to the reactions you want "\ - f"to remove. React to this message when you're done." - msg = await ctx.channel.send(msg_text) - - tasks = [] - - def check(event): - # we only care about the user who is clearing reactions - if event.user_id != ctx.author.id: - return False - # this is how the user finishes - if event.message_id == msg.id: - return True - else: - # remove a reaction - async def impl(): - msg = await self.bot \ - .get_guild(event.guild_id) \ - .get_channel(event.channel_id) \ - .get_message(event.message_id) - def check_emoji(r): - if event.emoji.is_custom_emoji() == r.custom_emoji: - if event.emoji.is_custom_emoji(): - return event.emoji.id == r.emoji.id - else: - # gotta love consistent APIs - return event.emoji.name == r.emoji - else: - return False - for reaction in filter(check_emoji, msg.reactions): - async for u in reaction.users(): - await reaction.message.remove_reaction(reaction, u) - # schedule immediately - tasks.append(asyncio.create_task(impl())) - return False - - try: - await self.bot.wait_for("raw_reaction_add", - timeout=120.0, - check=check) - except asyncio.TimeoutError: - await msg.edit(content = f"{msg_text} Timed out.") + if nick: + await ctx.guild.me.edit(nick=nick, reason=str(ctx.author)) else: - await asyncio.gather(*tasks) - await msg.edit(content = f"{msg_text} Done!") + await ctx.guild.me.edit(nick=None, reason=str(ctx.author)) + + await ctx.send("Successfully set bot nickname.") + def setup(bot): bot.add_cog(Mod(bot)) diff --git a/cogs/mod_note.py b/cogs/mod_note.py new file mode 100644 index 0000000..81b9515 --- /dev/null +++ b/cogs/mod_note.py @@ -0,0 +1,31 @@ +import discord +from discord.ext import commands +from helpers.checks import check_if_staff +from helpers.userlogs import userlog + + +class ModNote: + def __init__(self, bot): + self.bot = bot + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=["addnote"]) + async def note(self, ctx, target: discord.Member, *, note: str = ""): + """Adds a note to a user, staff only.""" + userlog(target.id, ctx.author, note, + "notes", target.name) + await ctx.send(f"{target.mention}: noted!") + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=["addnoteid"]) + async def noteid(self, ctx, target: int, *, note: str = ""): + """Adds a note to a user by userid, staff only.""" + userlog(target, ctx.author, note, + "notes") + await ctx.send(f"{target.mention}: noted!") + + +def setup(bot): + bot.add_cog(ModNote(bot)) diff --git a/cogs/mod_reacts.py b/cogs/mod_reacts.py new file mode 100644 index 0000000..fb81ec4 --- /dev/null +++ b/cogs/mod_reacts.py @@ -0,0 +1,109 @@ +import asyncio +import discord +from discord.ext import commands +import config +from helpers.checks import check_if_staff + + +class ModReact: + def __init__(self, bot): + self.bot = bot + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command() + async def clearreactsbyuser(self, ctx, user: discord.Member, *, + channel: discord.TextChannel = None, + limit: int = 50): + """Clears reacts from a given user in the given channel, staff only.""" + log_channel = self.bot.get_channel(config.log_channel) + if not channel: + channel = ctx.channel + count = 0 + async for msg in channel.history(limit=limit): + for react in msg.reactions: + if await react.users().find(lambda u: u == user): + count += 1 + async for u in react.users(): + await msg.remove_reaction(react, u) + msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared "\ + f"{user.mention}'s reacts from the last {limit} messages "\ + f"in {channel.mention}." + await ctx.channel.send(f"Cleared {count} unique reactions") + await log_channel.send(msg) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command() + async def clearallreacts(self, ctx, *, + limit: int = 50, + channel: discord.TextChannel = None): + """Clears all reacts in a given channel, staff only. Use with care.""" + log_channel = self.bot.get_channel(config.log_channel) + if not channel: + channel = ctx.channel + count = 0 + async for msg in channel.history(limit=limit): + if msg.reactions: + count += 1 + await msg.clear_reactions() + msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared all "\ + f"reacts from the last {limit} messages in {channel.mention}." + await ctx.channel.send(f"Cleared reacts from {count} messages!") + await log_channel.send(msg) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command() + async def clearreactsinteractive(self, ctx): + """Clears reacts interactively, staff only. Use with care.""" + msg_text = f"{ctx.author.mention}, react to the reactions you want "\ + f"to remove. React to this message when you're done." + msg = await ctx.channel.send(msg_text) + + tasks = [] + + def check(event): + # we only care about the user who is clearing reactions + if event.user_id != ctx.author.id: + return False + # this is how the user finishes + if event.message_id == msg.id: + return True + else: + # remove a reaction + async def impl(): + msg = await self.bot \ + .get_guild(event.guild_id) \ + .get_channel(event.channel_id) \ + .get_message(event.message_id) + + def check_emoji(r): + if event.emoji.is_custom_emoji() == r.custom_emoji: + if event.emoji.is_custom_emoji(): + return event.emoji.id == r.emoji.id + else: + # gotta love consistent APIs + return event.emoji.name == r.emoji + else: + return False + for reaction in filter(check_emoji, msg.reactions): + async for u in reaction.users(): + await reaction.message.remove_reaction(reaction, u) + # schedule immediately + tasks.append(asyncio.create_task(impl())) + return False + + try: + await self.bot.wait_for("raw_reaction_add", + timeout=120.0, + check=check) + except asyncio.TimeoutError: + await msg.edit(content=f"{msg_text} Timed out.") + else: + await asyncio.gather(*tasks) + await msg.edit(content=f"{msg_text} Done!") + + +def setup(bot): + bot.add_cog(ModReact(bot)) diff --git a/cogs/mod_userlog.py b/cogs/mod_userlog.py new file mode 100644 index 0000000..d7941fd --- /dev/null +++ b/cogs/mod_userlog.py @@ -0,0 +1,181 @@ +import discord +from discord.ext import commands +import config +import json +from helpers.checks import check_if_staff +from helpers.userlogs import get_userlog, set_userlog, userlog_event_types + + +class ModUserlog: + def __init__(self, bot): + self.bot = bot + + def get_userlog_embed_for_id(self, uid: str, name: str, own: bool = False, + event=""): + own_note = " Good for you!" if own else "" + wanted_events = ["warns", "bans", "kicks", "mutes"] + if event: + wanted_events = [event] + embed = discord.Embed(color=discord.Color.dark_red()) + embed.set_author(name=f"Userlog for {name}") + userlog = get_userlog() + + if uid not in userlog: + embed.description = f"There are none!{own_note} (no entry)" + embed.color = discord.Color.green() + return embed + + for event_type in wanted_events: + if event_type in userlog[uid] and userlog[uid][event_type]: + event_name = userlog_event_types[event_type] + for idx, event in enumerate(userlog[uid][event_type]): + issuer = "" if own else f"Issuer: {event['issuer_name']} "\ + f"({event['issuer_id']})\n" + embed.add_field(name=f"{event_name} {idx + 1}: " + f"{event['timestamp']}", + value=issuer + f"Reason: {event['reason']}", + inline=False) + + if not own and "watch" in userlog[uid]: + watch_state = "" if userlog[uid]["watch"] else "NOT " + embed.set_footer(text=f"User is {watch_state}under watch.") + + if not embed.fields: + embed.description = f"There are none!{own_note}" + embed.color = discord.Color.green() + return embed + + def clear_event_from_id(self, uid: str, event_type): + userlog = get_userlog() + if uid not in userlog: + return f"<@{uid}> has no {event_type}!" + event_count = len(userlog[uid][event_type]) + if not event_count: + return f"<@{uid}> has no {event_type}!" + userlog[uid][event_type] = [] + set_userlog(json.dumps(userlog)) + return f"<@{uid}> no longer has any {event_type}!" + + def delete_event_from_id(self, uid: str, idx: int, event_type): + userlog = get_userlog() + if uid not in userlog: + return f"<@{uid}> has no {event_type}!" + event_count = len(userlog[uid][event_type]) + if not event_count: + return f"<@{uid}> has no {event_type}!" + if idx > event_count: + return "Index is higher than "\ + f"count ({event_count})!" + if idx < 1: + return "Index is below 1!" + event = userlog[uid][event_type][idx - 1] + embed = discord.Embed(color=discord.Color.dark_red(), + title=f"{event_type} {idx} on " + f"{event['timestamp']}", + description=f"Issuer: {event['issuer_name']}\n" + f"Reason: {event['reason']}") + del userlog[uid][event_type][idx - 1] + set_userlog(json.dumps(userlog)) + return embed + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(name="userlog", + aliases=["listwarns", "getuserlog", "listuserlog"]) + async def userlog_cmd(self, ctx, target: discord.Member): + """Lists the userlog events for a user, staff only.""" + embed = self.get_userlog_embed_for_id(str(target.id), str(target)) + await ctx.send(embed=embed) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=["listnotes", "usernotes"]) + async def notes(self, ctx, target: discord.Member): + """Lists the notes for a user, staff only.""" + embed = self.get_userlog_embed_for_id(str(target.id), str(target), + event="notes") + await ctx.send(embed=embed) + + @commands.guild_only() + @commands.command(aliases=["mywarns"]) + async def myuserlog(self, ctx): + """Lists your userlog events (warns etc).""" + embed = self.get_userlog_embed_for_id(str(ctx.author.id), + str(ctx.author), True) + await ctx.send(embed=embed) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=["listwarnsid"]) + async def userlogid(self, ctx, target: int): + """Lists the userlog events for a user by ID, staff only.""" + embed = self.get_userlog_embed_for_id(str(target), str(target)) + await ctx.send(embed=embed) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=["clearwarns"]) + async def clearevent(self, ctx, target: discord.Member, + event="warns"): + """Clears all events of given type for a user, staff only.""" + log_channel = self.bot.get_channel(config.log_channel) + msg = self.clear_event_from_id(str(target.id), event) + await ctx.send(msg) + msg = f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"\ + f" all {event} events of {target.mention} | "\ + f"{self.bot.escape_message(target)}" + await log_channel.send(msg) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=["clearwarnsid"]) + async def cleareventid(self, ctx, target: int, event="warns"): + """Clears all events of given type for a userid, staff only.""" + log_channel = self.bot.get_channel(config.log_channel) + msg = self.clear_event_from_id(str(target), event) + await ctx.send(msg) + msg = f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"\ + f" all {event} events of <@{target}> " + await log_channel.send(msg) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=["delwarn"]) + async def delevent(self, ctx, target: discord.Member, idx: int, + event="warns"): + """Removes a specific event from a user, staff only.""" + log_channel = self.bot.get_channel(config.log_channel) + del_event = self.delete_event_from_id(str(target.id), idx, event) + event_name = userlog_event_types[event].lower() + # This is hell. + if isinstance(del_event, discord.Embed): + await ctx.send(f"{target.mention} has a {event_name} removed!") + msg = f"🗑 **Deleted {event_name}**: "\ + f"{ctx.author.mention} removed "\ + f"{event_name} {idx} from {target.mention} | "\ + f"{self.bot.escape_message(target)}" + await log_channel.send(msg, embed=del_event) + else: + await ctx.send(del_event) + + @commands.guild_only() + @commands.check(check_if_staff) + @commands.command(aliases=["delwarnid"]) + async def deleventid(self, ctx, target: int, idx: int, event="warns"): + """Removes a specific event from a userid, staff only.""" + log_channel = self.bot.get_channel(config.log_channel) + del_event = self.delete_event_from_id(str(target), idx, event) + event_name = userlog_event_types[event].lower() + # This is hell. + if isinstance(del_event, discord.Embed): + await ctx.send(f"<@{target}> has a {event_name} removed!") + msg = f"🗑 **Deleted {event_name}**: "\ + f"{ctx.author.mention} removed "\ + f"{event_name} {idx} from <@{target}> " + await log_channel.send(msg, embed=del_event) + else: + await ctx.send(del_event) + + +def setup(bot): + bot.add_cog(ModUserlog(bot)) diff --git a/data/warnsv2.json b/data/warnsv2.json index 0967ef4..9e26dfe 100644 --- a/data/warnsv2.json +++ b/data/warnsv2.json @@ -1 +1 @@ -{} +{} \ No newline at end of file diff --git a/helpers/restrictions.py b/helpers/restrictions.py new file mode 100644 index 0000000..4e5af9c --- /dev/null +++ b/helpers/restrictions.py @@ -0,0 +1,25 @@ +import json + + +def add_restriction(self, member, rst): + # from kurisu source, credits go to ihaveamac + with open("data/restrictions.json", "r") as f: + rsts = json.load(f) + if str(member.id) not in rsts: + rsts[str(member.id)] = [] + if rst not in rsts[str(member.id)]: + rsts[str(member.id)].append(rst) + with open("data/restrictions.json", "w") as f: + json.dump(rsts, f) + + +def remove_restriction(self, member, rst): + # from kurisu source, credits go to ihaveamac + with open("data/restrictions.json", "r") as f: + rsts = json.load(f) + if str(member.id) not in rsts: + rsts[str(member.id)] = [] + if rst in rsts[str(member.id)]: + rsts[str(member.id)].remove(rst) + with open("data/restrictions.json", "w") as f: + json.dump(rsts, f) diff --git a/helpers/userlogs.py b/helpers/userlogs.py new file mode 100644 index 0000000..ee34132 --- /dev/null +++ b/helpers/userlogs.py @@ -0,0 +1,45 @@ +import json +import time + +userlog_event_types = {"warns": "Warn", + "bans": "Ban", + "kicks": "Kick", + "mutes": "Mute", + "notes": "Note"} + + +def get_userlog(): + with open("data/warnsv2.json", "r") as f: + return json.load(f) + + +def set_userlog(contents): + with open("data/warnsv2.json", "w") as f: + f.write(contents) + + +def userlog(uid, issuer, reason, event_type, uname: str = ""): + with open("data/warnsv2.json", "r") as f: + userlogs = json.load(f) + uid = str(uid) + if uid not in userlogs: + userlogs[uid] = {"warns": [], + "mutes": [], + "kicks": [], + "bans": [], + "notes": [], + "watch": False, + "name": "n/a"} + if uname: + userlogs[uid]["name"] = uname + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + log_data = {"issuer_id": issuer.id, + "issuer_name": f"{issuer}", + "reason": reason, + "timestamp": timestamp} + if event_type not in userlogs[uid]: + userlogs[uid][event_type] = [] + userlogs[uid][event_type].append(log_data) + with open("data/warnsv2.json", "w") as f: + json.dump(userlogs, f) + return len(userlogs[uid][event_type])