From 4ad2527a59556de588b5453e3892b3abe0e09ae7 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Sun, 2 Apr 2023 13:56:49 +0200 Subject: [PATCH] Use reply message to target a specific member & Add aliases for macros (#26) * Remove command message when using macros * macro: Reply to the same message as the author invoking the command * logfilereader: Increase size of empty logs * meme: Add reply targets * mod: Add reply targets * mod_timed: Add reply targets * macro: Add aliases for macros * macro: Fix list_macros not listing any macros * Apply black formatting --- robocop_ng/cogs/logfilereader.py | 2 +- robocop_ng/cogs/macro.py | 80 ++++++++++++++++++-- robocop_ng/cogs/meme.py | 121 ++++++++++++++++++++++--------- robocop_ng/cogs/mod.py | 95 ++++++++++++++++++++++-- robocop_ng/cogs/mod_timed.py | 23 +++++- robocop_ng/helpers/macros.py | 117 +++++++++++++++++++++++++----- 6 files changed, 371 insertions(+), 67 deletions(-) diff --git a/robocop_ng/cogs/logfilereader.py b/robocop_ng/cogs/logfilereader.py index fb383b4..64e3458 100644 --- a/robocop_ng/cogs/logfilereader.py +++ b/robocop_ng/cogs/logfilereader.py @@ -227,7 +227,7 @@ class LogFileReader(Cog): 2) Ensure the following default logs are enabled: `Info`, `Warning`, `Error`, `Guest` and `Stub`. 3) Start a game up. 4) Play until your issue occurs. -5) Upload the latest log file which is larger than 2KB.""", +5) Upload the latest log file which is larger than 3KB.""", inline=False, ) else: diff --git a/robocop_ng/cogs/macro.py b/robocop_ng/cogs/macro.py index 32eb9c6..3de1b2f 100644 --- a/robocop_ng/cogs/macro.py +++ b/robocop_ng/cogs/macro.py @@ -10,7 +10,10 @@ from robocop_ng.helpers.macros import ( add_macro, edit_macro, remove_macro, - get_macros, + get_macros_dict, + add_aliases, + remove_aliases, + clear_aliases, ) @@ -18,13 +21,19 @@ class Macro(Cog): @commands.cooldown(3, 30, BucketType.member) @commands.command(aliases=["m"]) async def macro(self, ctx: Context, target: Optional[discord.Member], key: str): + await ctx.message.delete() if len(key) > 0: text = get_macro(key) if text is not None: if target is not None: await ctx.send(f"{target.mention}:\n{text}") else: - await ctx.send(text) + if ctx.message.reference is not None: + await ctx.send( + text, reference=ctx.message.reference, mention_author=True + ) + else: + await ctx.send(text) else: await ctx.send( f"{ctx.author.mention}: The macro '{key}' doesn't exist." @@ -38,6 +47,19 @@ class Macro(Cog): else: await ctx.send(f"Error: Macro '{key}' already exists.") + @commands.check(check_if_staff) + @commands.command(name="aliasadd", aliases=["addalias", "add_alias"]) + async def add_alias_macro(self, ctx: Context, existing_key: str, *new_keys: str): + if len(new_keys) == 0: + await ctx.send("Error: You need to add at least one alias.") + else: + if add_aliases(existing_key, list(new_keys)): + await ctx.send( + f"Added {len(new_keys)} aliases to macro '{existing_key}'!" + ) + else: + await ctx.send(f"Error: No new and unique aliases found.") + @commands.check(check_if_staff) @commands.command(name="macroedit", aliases=["me", "editmacro", "edit_macro"]) async def edit_macro(self, ctx: Context, key: str, *, text: str): @@ -46,6 +68,33 @@ class Macro(Cog): else: await ctx.send(f"Error: Macro '{key}' not found.") + @commands.check(check_if_staff) + @commands.command( + name="aliasremove", + aliases=[ + "aliasdelete", + "delalias", + "aliasdel", + "removealias", + "remove_alias", + "delete_alias", + ], + ) + async def remove_alias_macro( + self, ctx: Context, existing_key: str, *remove_keys: str + ): + if len(remove_keys) == 0: + await ctx.send("Error: You need to remove at least one alias.") + else: + if remove_aliases(existing_key, list(remove_keys)): + await ctx.send( + f"Removed {len(remove_keys)} aliases from macro '{existing_key}'!" + ) + else: + await ctx.send( + f"Error: None of the specified aliases were found for macro '{existing_key}'." + ) + @commands.check(check_if_staff) @commands.command( name="macroremove", @@ -65,12 +114,20 @@ class Macro(Cog): else: await ctx.send(f"Error: Macro '{key}' not found.") + @commands.check(check_if_staff) + @commands.command(name="aliasclear", aliases=["clearalias", "clear_alias"]) + async def clear_alias_macro(self, ctx: Context, existing_key: str): + if clear_aliases(existing_key): + await ctx.send(f"Removed all aliases of macro '{existing_key}'!") + else: + await ctx.send(f"Error: No aliases found for macro '{existing_key}'.") + @commands.cooldown(3, 30, BucketType.channel) @commands.command(name="macros", aliases=["ml", "listmacros", "list_macros"]) async def list_macros(self, ctx: Context): - macros = get_macros() - if len(macros) > 0: - macros = [f"- {key}\n" for key in sorted(macros.keys())] + macros = get_macros_dict() + if len(macros["macros"]) > 0: + macros = [f"- {key}\n" for key in sorted(macros["macros"].keys())] message = "📝 **Macros**:\n" for macro_key in macros: message += macro_key @@ -78,6 +135,19 @@ class Macro(Cog): else: await ctx.send("Couldn't find any macros.") + @commands.cooldown(3, 30, BucketType.channel) + @commands.command(name="aliases", aliases=["listaliases", "list_aliases"]) + async def list_aliases(self, ctx: Context, existing_key: str): + macros = get_macros_dict() + existing_key = existing_key.lower() + if existing_key in macros["aliases"].keys(): + message = f"📝 **Aliases for '{existing_key}'**:\n" + for alias in sorted(macros["aliases"][existing_key]): + message += f"- {alias}\n" + await ctx.send(message) + else: + await ctx.send(f"Couldn't find any aliases for macro '{existing_key}'.") + async def setup(bot): await bot.add_cog(Macro(bot)) diff --git a/robocop_ng/cogs/meme.py b/robocop_ng/cogs/meme.py index f7d8453..ffa2c9a 100644 --- a/robocop_ng/cogs/meme.py +++ b/robocop_ng/cogs/meme.py @@ -2,6 +2,7 @@ import datetime import math import platform import random +from typing import Optional import discord from discord.ext import commands @@ -28,42 +29,87 @@ class Meme(Cog): @commands.check(check_if_staff_or_ot) @commands.command(hidden=True, name="warm") - async def warm_member(self, ctx, user: discord.Member): + async def warm_member(self, ctx, user: Optional[discord.Member]): """Warms a user :3""" - celsius = random.randint(15, 100) - fahrenheit = self.c_to_f(celsius) - kelvin = self.c_to_k(celsius) - await ctx.send( - f"{user.mention} warmed." - f" User is now {celsius}°C " - f"({fahrenheit}°F, {kelvin}K)." - ) + if user is None and ctx.message.reference is None: + celsius = random.randint(15, 20) + fahrenheit = self.c_to_f(celsius) + kelvin = self.c_to_k(celsius) + await ctx.send( + f"{ctx.author.mention} tries to warm themself." + f" User is now {celsius}°C " + f"({fahrenheit}°F, {kelvin}K).\n" + "You might have more success warming someone else :3" + ) + else: + if user is None: + user = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author + + celsius = random.randint(15, 100) + fahrenheit = self.c_to_f(celsius) + kelvin = self.c_to_k(celsius) + await ctx.send( + f"{user.mention} warmed." + f" User is now {celsius}°C " + f"({fahrenheit}°F, {kelvin}K)." + ) @commands.check(check_if_staff_or_ot) @commands.command(hidden=True, name="chill", aliases=["cold"]) - async def chill_member(self, ctx, user: discord.Member): + async def chill_member(self, ctx, user: Optional[discord.Member]): """Chills a user >:3""" - celsius = random.randint(-50, 15) - fahrenheit = self.c_to_f(celsius) - kelvin = self.c_to_k(celsius) - await ctx.send( - f"{user.mention} chilled." - f" User is now {celsius}°C " - f"({fahrenheit}°F, {kelvin}K)." - ) + if user is None and ctx.message.reference is None: + celsius = random.randint(-75, 10) + fahrenheit = self.c_to_f(celsius) + kelvin = self.c_to_k(celsius) + await ctx.send( + f"{ctx.author.mention} chills themself." + f" User is now {celsius}°C " + f"({fahrenheit}°F, {kelvin}K).\n" + "🧊 Don't be so hard on yourself. 😔" + ) + else: + if user is None: + user = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author + celsius = random.randint(-50, 15) + fahrenheit = self.c_to_f(celsius) + kelvin = self.c_to_k(celsius) + await ctx.send( + f"{user.mention} chilled." + f" User is now {celsius}°C " + f"({fahrenheit}°F, {kelvin}K)." + ) @commands.check(check_if_staff_or_ot) @commands.command(hidden=True, aliases=["thank", "reswitchedgold"]) - async def gild(self, ctx, user: discord.Member): + async def gild(self, ctx, user: Optional[discord.Member]): """Gives a star to a user""" - await ctx.send(f"{user.mention} gets a :star:, yay!") + if user is None and ctx.message.reference is None: + await ctx.send(f"No stars for you, {ctx.author.mention}!") + else: + if user is None: + user = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author + await ctx.send(f"{user.mention} gets a :star:, yay!") @commands.check(check_if_staff_or_ot) @commands.command( hidden=True, aliases=["reswitchedsilver", "silv3r", "reswitchedsilv3r"] ) - async def silver(self, ctx, user: discord.Member): + async def silver(self, ctx, user: Optional[discord.Member]): """Gives a user ReSwitched Silver™""" + if user is None and ctx.message.reference is None: + await ctx.send(f"{ctx.author.mention}, you can't reward yourself.") + else: + if user is None: + user = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author embed = discord.Embed( title="ReSwitched Silver™!", description=f"Here's your ReSwitched Silver™," f"{user.mention}!", @@ -131,23 +177,30 @@ class Meme(Cog): @commands.check(check_if_staff_or_ot) @commands.command(hidden=True, name="bam") - async def bam_member(self, ctx, target: discord.Member): + async def bam_member(self, ctx, target: Optional[discord.Member]): """Bams a user owo""" - if target == ctx.author: - if target.id == 181627658520625152: + if target is None and ctx.message.reference is None: + await ctx.reply("https://tenor.com/view/bonk-gif-26414884") + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author + if target == ctx.author: + if target.id == 181627658520625152: + return await ctx.send( + "https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG" + ) + return await ctx.send("hedgeberg#7337 is ̶n͢ow b̕&̡.̷ 👍̡") + elif target == self.bot.user: return await ctx.send( - "https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG" + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." ) - return await ctx.send("hedgeberg#7337 is ̶n͢ow b̕&̡.̷ 👍̡") - elif target == self.bot.user: - return await ctx.send( - f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." - ) - safe_name = await commands.clean_content(escape_markdown=True).convert( - ctx, str(target) - ) - await ctx.send(f"{safe_name} is ̶n͢ow b̕&̡.̷ 👍̡") + safe_name = await commands.clean_content(escape_markdown=True).convert( + ctx, str(target) + ) + await ctx.send(f"{safe_name} is ̶n͢ow b̕&̡.̷ 👍̡") @commands.command(hidden=True) async def memebercount(self, ctx): diff --git a/robocop_ng/cogs/mod.py b/robocop_ng/cogs/mod.py index e73b628..986b313 100644 --- a/robocop_ng/cogs/mod.py +++ b/robocop_ng/cogs/mod.py @@ -1,4 +1,5 @@ import io +from typing import Optional import discord from discord.ext import commands @@ -38,8 +39,17 @@ class Mod(Cog): @commands.guild_only() @commands.check(check_if_staff) @commands.command() - async def mute(self, ctx, target: discord.Member, *, reason: str = ""): + async def mute(self, ctx, target: Optional[discord.Member], *, reason: str = ""): """Mutes a user, staff only.""" + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author # Hedge-proofing the code if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") @@ -123,8 +133,17 @@ class Mod(Cog): @commands.bot_has_permissions(kick_members=True) @commands.check(check_if_staff) @commands.command() - async def kick(self, ctx, target: discord.Member, *, reason: str = ""): + async def kick(self, ctx, target: Optional[discord.Member], *, reason: str = ""): """Kicks a user, staff only.""" + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author # Hedge-proofing the code if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") @@ -184,8 +203,17 @@ class Mod(Cog): @commands.bot_has_permissions(ban_members=True) @commands.check(check_if_staff) @commands.command(aliases=["yeet"]) - async def ban(self, ctx, target: discord.Member, *, reason: str = ""): + async def ban(self, ctx, target: Optional[discord.Member], *, reason: str = ""): """Bans a user, staff only.""" + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author # Hedge-proofing the code if target == ctx.author: if target.id == 181627658520625152: @@ -246,9 +274,18 @@ class Mod(Cog): @commands.check(check_if_staff) @commands.command() async def bandel( - self, ctx, day_count: int, target: discord.Member, *, reason: str = "" + self, ctx, day_count: int, target: Optional[discord.Member], *, reason: str = "" ): """Bans a user for a given number of days, staff only.""" + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author # Hedge-proofing the code if target == ctx.author: if target.id == 181627658520625152: @@ -488,13 +525,25 @@ class Mod(Cog): @commands.guild_only() @commands.check(check_if_staff) @commands.command() - async def approve(self, ctx, target: discord.Member, role: str = "community"): + async def approve( + self, ctx, target: Optional[discord.Member], role: str = "community" + ): """Add a role to a user (default: community), staff only.""" if role not in config.named_roles: return await ctx.send( "No such role! Available roles: " + ",".join(config.named_roles) ) + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author + log_channel = self.bot.get_channel(config.modlog_channel) target_role = ctx.guild.get_role(config.named_roles[role]) @@ -514,13 +563,25 @@ class Mod(Cog): @commands.guild_only() @commands.check(check_if_staff) @commands.command(aliases=["unapprove"]) - async def revoke(self, ctx, target: discord.Member, role: str = "community"): + async def revoke( + self, ctx, target: Optional[discord.Member], role: str = "community" + ): """Remove a role from a user (default: community), staff only.""" if role not in config.named_roles: return await ctx.send( "No such role! Available roles: " + ",".join(config.named_roles) ) + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author + log_channel = self.bot.get_channel(config.modlog_channel) target_role = ctx.guild.get_role(config.named_roles[role]) @@ -555,8 +616,17 @@ class Mod(Cog): @commands.guild_only() @commands.check(check_if_staff) @commands.command() - async def warn(self, ctx, target: discord.Member, *, reason: str = ""): + async def warn(self, ctx, target: Optional[discord.Member], *, reason: str = ""): """Warns a user, staff only.""" + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author # Hedge-proofing the code if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") @@ -631,10 +701,19 @@ class Mod(Cog): @commands.guild_only() @commands.check(check_if_staff) @commands.command(aliases=["setnick", "nick"]) - async def nickname(self, ctx, target: discord.Member, *, nick: str = ""): + async def nickname(self, ctx, target: Optional[discord.Member], *, nick: str = ""): """Sets a user's nickname, staff only. Just send .nickname to wipe the nickname.""" + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author try: if nick: diff --git a/robocop_ng/cogs/mod_timed.py b/robocop_ng/cogs/mod_timed.py index 745e0d8..b3658e7 100644 --- a/robocop_ng/cogs/mod_timed.py +++ b/robocop_ng/cogs/mod_timed.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Optional import discord from discord.ext import commands @@ -23,9 +24,18 @@ class ModTimed(Cog): @commands.check(check_if_staff) @commands.command() async def timeban( - self, ctx, target: discord.Member, duration: str, *, reason: str = "" + self, ctx, target: Optional[discord.Member], duration: str, *, reason: str = "" ): """Bans a user for a specified amount of time, staff only.""" + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author # Hedge-proofing the code if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") @@ -89,9 +99,18 @@ class ModTimed(Cog): @commands.check(check_if_staff) @commands.command() async def timemute( - self, ctx, target: discord.Member, duration: str, *, reason: str = "" + self, ctx, target: Optional[discord.Member], duration: str, *, reason: str = "" ): """Mutes a user for a specified amount of time, staff only.""" + if target is None and ctx.message.reference is None: + return await ctx.send( + f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that." + ) + else: + if target is None: + target = ctx.channel.fetch_message( + ctx.message.reference.message_id + ).author # Hedge-proofing the code if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") diff --git a/robocop_ng/helpers/macros.py b/robocop_ng/helpers/macros.py index ec77e57..d7bfc96 100644 --- a/robocop_ng/helpers/macros.py +++ b/robocop_ng/helpers/macros.py @@ -1,55 +1,138 @@ import json import os -from typing import Optional +from typing import Optional, Union MACROS_FILE = "data/macros.json" -def get_macros() -> dict[str, str]: +def get_macros_dict() -> dict[str, dict[str, Union[list[str], str]]]: if os.path.isfile(MACROS_FILE): with open(MACROS_FILE, "r") as f: - return json.load(f) - return {} + macros = json.load(f) + + # Migration code + if "aliases" not in macros.keys(): + new_macros = {"macros": macros, "aliases": {}} + unique_macros = set(new_macros["macros"].values()) + for macro_text in unique_macros: + first_macro_key = "" + duplicate_num = 0 + for key, macro in new_macros["macros"].copy().items(): + if macro == macro_text and duplicate_num == 0: + first_macro_key = key + duplicate_num += 1 + continue + elif macro == macro_text: + if first_macro_key not in new_macros["aliases"].keys(): + new_macros["aliases"][first_macro_key] = [] + new_macros["aliases"][first_macro_key].append(key) + del new_macros["macros"][key] + duplicate_num += 1 + + set_macros(new_macros) + return new_macros + + return macros + return {"macros": {}, "aliases": {}} -def set_macros(contents: dict[str, str]): +def is_macro_key_available( + key: str, macros: dict[str, dict[str, Union[list[str], str]]] = None +) -> bool: + if macros is None: + macros = get_macros_dict() + if key in macros["macros"].keys(): + return False + for aliases in macros["aliases"].values(): + if key in aliases: + return False + return True + + +def set_macros(contents: dict[str, dict[str, Union[list[str], str]]]): with open(MACROS_FILE, "w") as f: json.dump(contents, f) def get_macro(key: str) -> Optional[str]: - macros = get_macros() + macros = get_macros_dict() key = key.lower() - if key in macros.keys(): - return macros[key] + if key in macros["macros"].keys(): + return macros["macros"][key] + for main_key, aliases in macros["aliases"].items(): + if key in aliases: + return macros["macros"][main_key] return None def add_macro(key: str, message: str) -> bool: - macros = get_macros() + macros = get_macros_dict() key = key.lower() - if key not in macros.keys(): - macros[key] = message + if is_macro_key_available(key, macros): + macros["macros"][key] = message set_macros(macros) return True return False +def add_aliases(key: str, aliases: list[str]) -> bool: + macros = get_macros_dict() + key = key.lower() + success = False + if key in macros["macros"].keys(): + for alias in aliases: + alias = alias.lower() + if is_macro_key_available(alias, macros): + macros["aliases"][key].append(alias) + success = True + if success: + set_macros(macros) + return success + + def edit_macro(key: str, message: str) -> bool: - macros = get_macros() + macros = get_macros_dict() key = key.lower() - if key in macros.keys(): - macros[key] = message + if key in macros["macros"].keys(): + macros["macros"][key] = message set_macros(macros) return True return False +def remove_aliases(key: str, aliases: list[str]) -> bool: + macros = get_macros_dict() + key = key.lower() + success = False + if key not in macros["aliases"].keys(): + return False + for alias in aliases: + alias = alias.lower() + if alias in macros["aliases"][key]: + macros["aliases"][key].remove(alias) + if len(macros["aliases"][key]) == 0: + del macros["aliases"][key] + success = True + if success: + set_macros(macros) + return success + + def remove_macro(key: str) -> bool: - macros = get_macros() + macros = get_macros_dict() key = key.lower() - if key in macros.keys(): - del macros[key] + if key in macros["macros"].keys(): + del macros["macros"][key] + set_macros(macros) + return True + return False + + +def clear_aliases(key: str) -> bool: + macros = get_macros_dict() + key = key.lower() + if key in macros["macros"].keys() and key in macros["aliases"].keys(): + del macros["aliases"][key] set_macros(macros) return True return False