From 6ca37b2335a504e5dd9faf01ab1fa160304bbf01 Mon Sep 17 00:00:00 2001 From: Jan200101 Date: Sat, 21 Sep 2019 19:47:19 +0200 Subject: [PATCH 01/43] escape user and display name escaping the user name might be a bit overkill but its better to be safe than sorry I guess --- cogs/mod_userlog.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cogs/mod_userlog.py b/cogs/mod_userlog.py index 1b55ec6..0ebf583 100644 --- a/cogs/mod_userlog.py +++ b/cogs/mod_userlog.py @@ -205,12 +205,15 @@ class ModUserlog(Cog): embed = self.get_userlog_embed_for_id(str(user.id), str(user), event=event_types) - await ctx.send(f"user = {user}\n" + user_name = await commands.clean_content().convert(ctx, user.name) + display_name = await commands.clean_content().convert(ctx, user.display_name) + + await ctx.send(f"user = {user_name}\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"display_name = {display_name}\n" f"joined_at = {user.joined_at}\n" f"activities = `{user.activities}`\n" f"color = {user.colour}\n" From d97f948e9ba3387450e7a4e2aeb1e0cfcb9e89d7 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Mon, 7 Oct 2019 10:19:01 +0300 Subject: [PATCH 02/43] :) --- cogs/logs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/logs.py b/cogs/logs.py index 6cd498c..aa63fac 100644 --- a/cogs/logs.py +++ b/cogs/logs.py @@ -24,7 +24,7 @@ class Logs(Cog): "tinfoil", "dz", # title managers "goldleaf", "lithium", # title managers "cracked", # older term for pirated games - "xci"] # "backup" format + "xci", "nsz"] # "backup" format susp_hellgex = "|".join([r"\W*".join(list(word)) for word in self.susp_words]) self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE) From 9d652356879c18f003fde6a8081577505df00591 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Wed, 30 Oct 2019 23:04:59 +0300 Subject: [PATCH 03/43] kick: add boot. --- cogs/mod.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/mod.py b/cogs/mod.py index e3b6ee4..db42259 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -147,6 +147,7 @@ class Mod(Cog): log_channel = self.bot.get_channel(config.modlog_channel) await log_channel.send(chan_message) + await ctx.send(f"👢 {safe_name}, 👍.") @commands.guild_only() @commands.bot_has_permissions(ban_members=True) From 3ed010a4ceed47ac4a1db87e336aa248f3a5c76f Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Tue, 5 Nov 2019 12:43:17 +0300 Subject: [PATCH 04/43] blackalabi blackalabi --- cogs/meme.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cogs/meme.py b/cogs/meme.py index 02c7f79..958c15e 100644 --- a/cogs/meme.py +++ b/cogs/meme.py @@ -79,6 +79,12 @@ class Meme(Cog): """secret command""" await ctx.send(f"🍂 you found me 🍂") + @commands.check(check_if_staff_or_ot) + @commands.command(hidden=True) + async def blackalabi(self, ctx): + """secret command""" + await ctx.send("https://elixi.re/i/discord.png") + @commands.check(check_if_staff_or_ot) @commands.command(hidden=True) async def peng(self, ctx): From 527f55e6ddbb6264ac59751d031839baf7e44eb0 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Wed, 6 Nov 2019 01:59:29 +0300 Subject: [PATCH 05/43] Fix usage not showing command name --- Robocop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robocop.py b/Robocop.py index c924d4d..9acd679 100755 --- a/Robocop.py +++ b/Robocop.py @@ -170,7 +170,7 @@ async def on_command_error(ctx, error): # Nothing to do when command is not found. return - help_text = f"Usage of this command is: ```{ctx.prefix}"\ + help_text = f"Usage of this command is: ```{ctx.prefix}{ctx.command.name} "\ f"{ctx.command.signature}```\nPlease see `{ctx.prefix}help "\ f"{ctx.command.name}` for more info about this command." if isinstance(error, commands.BadArgument): From 3e70ede154e248aab5749988c39a7793b83ab7df Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Wed, 6 Nov 2019 02:01:22 +0300 Subject: [PATCH 06/43] Make robocop dm on help --- Robocop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robocop.py b/Robocop.py index 9acd679..be703ab 100755 --- a/Robocop.py +++ b/Robocop.py @@ -65,7 +65,7 @@ initial_extensions = ['cogs.common', 'cogs.invites'] bot = commands.Bot(command_prefix=get_prefix, - description=config.bot_description, pm_help=True) + description=config.bot_description, dm_help=True) bot.log = log bot.config = config From a0fd90fabd67de5fb81ec6ca6fbb61037b81b903 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Wed, 6 Nov 2019 02:06:21 +0300 Subject: [PATCH 07/43] Make robocop *actually* dm on help --- Robocop.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Robocop.py b/Robocop.py index be703ab..43899dc 100755 --- a/Robocop.py +++ b/Robocop.py @@ -65,7 +65,8 @@ initial_extensions = ['cogs.common', 'cogs.invites'] bot = commands.Bot(command_prefix=get_prefix, - description=config.bot_description, dm_help=True) + description=config.bot_description) +bot.help_command = commands.DefaultHelpCommand(dm_help=True) bot.log = log bot.config = config From 11d264cbd8199190a8576aed1613bb8fbe23f143 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Mon, 11 Nov 2019 14:29:30 +0300 Subject: [PATCH 08/43] basic: Add hackercount --- cogs/basic.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cogs/basic.py b/cogs/basic.py index f902302..e00cae6 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -35,6 +35,14 @@ class Basic(Cog): await ctx.send(f"{ctx.guild.name} has " f"{len(community.members)} community members!") + @commands.guild_only() + @commands.command() + async def hackercount(self, ctx): + """Prints the hacker member count of the server.""" + h4x0r = ctx.guild.get_role(config.named_roles["hacker"]) + await ctx.send(f"{ctx.guild.name} has " + f"{len(h4x0r.members)} people with hacker role!") + @commands.guild_only() @commands.command() async def membercount(self, ctx): From 0629a09d69459151f0610f0c5cd4cbb11ce5e480 Mon Sep 17 00:00:00 2001 From: Ave Date: Tue, 26 Nov 2019 08:52:23 +0200 Subject: [PATCH 09/43] Fix my gitlab url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4aa0b4b..491c3a5 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Next-gen rewrite of Kurisu/Robocop bot used on ReSwitched bot with discord.py rewrite, designed to be relatively clean, consistent and un-bloated. -Code is based on https://gitlab.com/ao/dpybotbase and https://github.com/916253/Kurisu-Reswitched. +Code is based on https://gitlab.com/a/dpybotbase and https://github.com/916253/Kurisu-Reswitched. --- From f44e1ea17ba328ec8e91e5396d5ffbb8eb144321 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Sat, 7 Dec 2019 13:31:59 +0300 Subject: [PATCH 10/43] Verif: add rule 11 --- cogs/verification.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cogs/verification.py b/cogs/verification.py index fafea82..b29da3d 100644 --- a/cogs/verification.py +++ b/cogs/verification.py @@ -72,7 +72,14 @@ welcome_rules = ( """, # 10 - 'The first character of your server nickname should be alphanumeric if you wish to talk in chat.' + 'The first character of your server nickname should be alphanumeric if you wish to talk in chat.', + + # 11 + """ + Do not boost the server. + • ReSwitched neither wants nor needs your server boosts, and your money is better off elsewhere. Consider the EFF (or a charity of your choice). + • Boosting the server is liable to get you kicked (to remove the nitro boost role), and/or warned. Roles you possessed prior to the kick may not be restored in a timely fashion. + """ ) welcome_footer = ( From e4fcc81a13903c1e43e905d2e461a3ca2f57601d Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 19 Dec 2019 04:17:36 +0300 Subject: [PATCH 11/43] add .cox Literal bloat --- Robocop.py | 1 + assets/.gitignore | 1 + assets/byjcox.png | Bin 0 -> 6773 bytes assets/motherboardlogo.png | Bin 0 -> 7779 bytes cogs/imagemanip.py | 80 +++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++ 6 files changed, 85 insertions(+) create mode 100644 assets/.gitignore create mode 100644 assets/byjcox.png create mode 100644 assets/motherboardlogo.png create mode 100644 cogs/imagemanip.py diff --git a/Robocop.py b/Robocop.py index 43899dc..cc6877c 100755 --- a/Robocop.py +++ b/Robocop.py @@ -61,6 +61,7 @@ initial_extensions = ['cogs.common', 'cogs.remind', 'cogs.robocronp', 'cogs.meme', + 'cogs.imagemanip', 'cogs.pin', 'cogs.invites'] diff --git a/assets/.gitignore b/assets/.gitignore new file mode 100644 index 0000000..5b0b552 --- /dev/null +++ b/assets/.gitignore @@ -0,0 +1 @@ +*.otf diff --git a/assets/byjcox.png b/assets/byjcox.png new file mode 100644 index 0000000000000000000000000000000000000000..e6fcd515ab5fa808f82594509ffc718483139291 GIT binary patch literal 6773 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 XYo~8>4I{$W00000NkvXXu0mjf9*oh} literal 0 HcmV?d00001 diff --git a/assets/motherboardlogo.png b/assets/motherboardlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..d1a07db7a89c4909ff9e3f227bdbce046bef1602 GIT binary patch literal 7779 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
pt)9?dy{D4^000SaNLh0L01FcU z01FcV0GgZ_00007bV*G`2jdJA04f}yGDe#K000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_C zX>@2HM@dakSAh-}000N>Nkl1>g#BDgaktTtS=) zY*%1X0k{I=3Lq6g2f7dURwM8VgY(|ImpgVwGcibFSNciXf69|mN&x^I4hJ|MkI<*h zW&=`6{d;UZq@pO`csxQ`mQa=@002lSfiVWF)e3?j=+s{<7VuSidwT=V^9K3N^Bj^S zf$Q~pw?Ef)VYl1e;HE;Li^gK`VJ4H?@rEYga>IeD0uK?ht#Br=lvenycHl2D^ zRVgVYS=TH=2z9w!bh~@?$CNzJb$^@9hO9FrOTYNOKZ!TSnA-36cgKt>Z*Om^Pf3!D zYMZ91qG-S0TiZlYbW?wdoTt-ivYd=DwcG8w-^4$J5M(3I^K|8=X{?qWvEU?40iwx&(=^KSyh&}tFuVa)5Cnsucoc|Oa3h&2r{{U9X__t&hZTyGa@3Snk z)7zBaJ^ITmn z7xnh`HmD=zd9Lz2C*QlStKZxDS*=zoO;eSosZ~!MTER9N(v5BW=6Ozj2is9Qb`(V_ ziXyT<&bd0BPU>>GsMG1BIOl`AB6YM=ZSd5)C#MZ7w(<0RU-Rm^t`%P}bbR#wry}?W4EPJcVxC#P)TNA;agp0dc5s&L>-wL*Jqn zw5@-}7`Zs2C?a&K0~S>0O z4jc}LNp0G;h4p%^_3f{WW81c*{}la>lpSExXYa|m+wDfzxA{7sO+#Y{h$fSc<3N%m zq;fu186Kk`e^&~E0Lro?C%mux_no_CS;BI;B#naW^?G9nwrxu~kr`v;yn3DoLI~2h znmQ+O97A2#I(RUK&gb*ZGtN1HP4`nuSruV4UZ{7J4gdfh2WFnCuY#KZ^ zrT=FcE0ny9v5qb`WUMHqtV+Di-GPQh(=;j!!%4@elu}74$?-=Q+#`O#dq;CsRaRbu zAW%U02C=ctkA;p}8&DqA+fb+aDiFWwM?{O;EH-}PI3~ep>RdxWtg4Cx#aG7hE5&B~ z*cPrkN#!|+B|F$3L{;#t=zVsOs2UOEUw$~wLW zmb$J%N(n_#j2cA8ao}`1S%v%##26!ux3+EJ_4W1P$zhB^Q50nK#b!K|Wl4gGbFMRY zuuu`Bp@k!aAbF|ldh+~5Q3PdK>P_u0Tq!mnu2!oK+D#x*pu~NfKBr7B?kH5;z=J$ z28+c4o}QlI<>dvo+iiz8Y7A!jO%%tm@_k>&jXCGa_k9(|v5ukQGc5c4z7wnuf&u9f*{a+$8oHAa~!85 zXa6egOuvqx4+=9qiF($*qG;a3jiTr#MSTnq%}f-t4rUTZQKW?{R<%!W2}>th`S_X z>?TdjOjXU;qIBoI)k_ZYa>*An4220k`3#FTT6eEw$o>d|Z-m7C2*?<9{L&1Oy;b-mF> zoO6Bj;<4OshdGjF=5!c_N=i8&&3^#>H3qE5}-^|JUIXW002ovPDHLkV1hML)&T$j literal 0 HcmV?d00001 diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py new file mode 100644 index 0000000..34713eb --- /dev/null +++ b/cogs/imagemanip.py @@ -0,0 +1,80 @@ +import discord +from discord.ext import commands +from discord.ext.commands import Cog +import textwrap +import PIL.Image +import PIL.ImageFilter +import PIL.ImageOps +import PIL.ImageFont +import PIL.ImageDraw + + +class ImageManip(Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command() + async def cox(self, ctx, *headline: str): + """Gives a cox headline""" + mention = ctx.author.mention + + in_vice = "assets/motherboardlogo.png" + in_byjcox = "assets/byjcox.png" + font_path = "assets/neue-haas-grotesk-display-bold-regular.otf" + + # Settings for image generation, don't touch anything + horipos = 18 + vertpos = 75 + line_spacing = 10 + font_size = 50 + image_width = 750 + font_wrap_count = 30 + sig_height = 15 + + # Wrap into lines + lines = textwrap.wrap(headline, width=font_wrap_count) + # not great, 4am be like + image_height = (len(lines) + 2) * (vertpos + line_spacing) + + # Load font + f = PIL.ImageFont.truetype(font_path, font_size) + + # Create image base, paste mobo logo + im = PIL.Image.new("RGB", (image_width, image_height), color="#FFFFFF") + moboim = PIL.Image.open(in_vice) + im.paste(moboim, (horipos, 17)) + + # Go through all the wrapped text lines + for line in lines: + # Get size of the text by font, create a new image of that size + size = f.getsize(line) + txt = PIL.Image.new('L', size) + + # Draw the text + d = PIL.ImageDraw.Draw(txt) + d.text((0, 0), line, font=f, fill=255) + + # Paste the text into the base image + w = txt.rotate(0, expand=1) + im.paste(PIL.ImageOps.colorize(w, (0, 0, 0), + (0, 0, 0)), (horipos, vertpos), w) + + # Calculate position on next line + vertpos += size[1] + line_spacing + + # Add jcox signature + jcoxim = PIL.Image.open(in_byjcox) + im.paste(jcoxim, (horipos, vertpos + sig_height)) + + # Crop the image to the actual resulting size + im = im.crop((0, 0, image_width, vertpos + (sig_height * 3))) + + # Save image + out_filename = f"/tmp/{ctx.message.id}-out.png" + im.save(out_filename, quality=100, optimize=True) + await ctx.send(content=f"{mention}: Enjoy.", + file=discord.File(out_filename)) + + +def setup(bot): + bot.add_cog(ImageManip(bot)) diff --git a/requirements.txt b/requirements.txt index 60c81ef..b5c9702 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,6 @@ humanize parsedatetime aiohttp gidgethub + +# Only if you'll enable imagemanip, which is only used for meme commands +Pillow From ee2ec67c17b8f319f7f4d82de850a84bc049ec64 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 19 Dec 2019 04:31:48 +0300 Subject: [PATCH 12/43] fix cox --- cogs/imagemanip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index 34713eb..88b78ae 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -14,7 +14,7 @@ class ImageManip(Cog): self.bot = bot @commands.command() - async def cox(self, ctx, *headline: str): + async def cox(self, ctx, *, headline: str): """Gives a cox headline""" mention = ctx.author.mention From 5174da59aa31212fcd002b7e069985df8b8740f6 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 19 Dec 2019 04:35:17 +0300 Subject: [PATCH 13/43] Limit .cox to ot and staff --- cogs/imagemanip.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index 88b78ae..abf4412 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -1,6 +1,7 @@ import discord from discord.ext import commands from discord.ext.commands import Cog +from helpers.checks import check_if_staff_or_ot import textwrap import PIL.Image import PIL.ImageFilter @@ -13,7 +14,8 @@ class ImageManip(Cog): def __init__(self, bot): self.bot = bot - @commands.command() + @commands.check(check_if_staff_or_ot) + @commands.command(hidden=True) async def cox(self, ctx, *, headline: str): """Gives a cox headline""" mention = ctx.author.mention From 1c5ab9c63a1092ec2cc09e7386772021cdb8f300 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 19 Dec 2019 11:37:33 +0300 Subject: [PATCH 14/43] add .ttfs .gitignore --- assets/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/.gitignore b/assets/.gitignore index 5b0b552..70994af 100644 --- a/assets/.gitignore +++ b/assets/.gitignore @@ -1 +1,2 @@ *.otf +*.ttf From fc18f4a83c23cba04ac491c78272ce5c3bbd09f7 Mon Sep 17 00:00:00 2001 From: Lucy <54782568+lucyyyyyyy@users.noreply.github.com> Date: Thu, 19 Dec 2019 23:30:27 +1000 Subject: [PATCH 15/43] im a bitch --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 00623b2..7a84f4e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ PRs to this file to improve wording are welcome. Please do not try to exploit public instances if it's going to cause harm, instead, set up your own instance of robocop-ng. -Breaking "database" files, running arbitrary code, using an unprivileged uesr to do something user can't normally do (editing channels or guild, deleting others' messages, making bot do an @e or @h mention, reading channels that user can't read, writing to channels user can't write to etc) are all considered harmful. +Breaking "database" files, running arbitrary code, using an unprivileged user to do something user can't normally do (editing channels or guild, deleting others' messages, making bot do an @e or @h mention, reading channels that user can't read, writing to channels that user can't write to, etc.) are all considered harmful. ## Supported Versions From e53a6341a5c1e0d66ffd134e4fed0e1cf118720e Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 19 Dec 2019 16:38:24 +0300 Subject: [PATCH 16/43] .trump fml this bot became not only a liberal but also a bloated mess --- assets/trumpfooter.png | Bin 0 -> 6214 bytes assets/trumpheader.png | Bin 0 -> 9133 bytes cogs/imagemanip.py | 63 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 assets/trumpfooter.png create mode 100644 assets/trumpheader.png diff --git a/assets/trumpfooter.png b/assets/trumpfooter.png new file mode 100644 index 0000000000000000000000000000000000000000..4e81e349ffa3556813996d48ae73f5f1ece5dafd GIT binary patch literal 6214 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 literal 0 HcmV?d00001 diff --git a/assets/trumpheader.png b/assets/trumpheader.png new file mode 100644 index 0000000000000000000000000000000000000000..34efe9e4c6a3d39aaf0ca1610673a9c1937d71f8 GIT binary patch literal 9133 zcmYLPcR1Vc*S0BYZ=qTVS_9=7u`9fNvV3ceM^&G%QBV8cpLU zD=a;}3~w(;cr0XAcPmi7B_&s5X`~UK=~e2cjM*fAmC~3~nUaeetev}uh($&8_nmcz zlT%C!FE$PMt1mv4&H*L&{CmUpLWhDrj$Zw-TfU^AfhRP*B^-wfE7-I_qOSl>osN=2 zf()}#A3BocU)9M!jXhEI&&_kq!@;0a)l>{>0Z;t@rUf=k6V0X;eVtPF&$Sl))4K4X z$;WHdd{pw$BG6&HrzrsWz3CCADe&Rdh^dBy&92fkeRb

t$PO#3z@T_o;Jbt&8r+< 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 W__9&gI{qRDfq{;R_FGNInEwOWnUe1S literal 0 HcmV?d00001 diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index abf4412..031e82c 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -77,6 +77,69 @@ class ImageManip(Cog): await ctx.send(content=f"{mention}: Enjoy.", file=discord.File(out_filename)) + @commands.check(check_if_staff_or_ot) + @commands.command(hidden=True) + async def trump(self, ctx, *, headline: str): + """Gives a trump tweet""" + mention = ctx.author.mention + + in_header = "assets/trumpheader.png" + in_footer = "assets/trumpfooter.png" + font_path = "assets/Segoe UI.ttf" + + # Settings for image generation, don't touch anything + horipos = 10 + vertpos = 70 + font_size = 27 + image_width = 590 + font_wrap_count = 45 + sig_height = 49 + + # Wrap into lines + lines = textwrap.wrap(headline, width=font_wrap_count) + # not great, 4am be like + image_height = (len(lines) + 2) * vertpos + + # Load font + f = PIL.ImageFont.truetype(font_path, font_size) + + # Create image base, paste mobo logo + im = PIL.Image.new("RGB", (image_width, image_height), color="#15202B") + headerim = PIL.Image.open(in_header) + im.paste(headerim, (horipos, 15)) + + # Go through all the wrapped text lines + for line in lines: + # Get size of the text by font, create a new image of that size + size = f.getsize(line) + txt = PIL.Image.new('L', size) + + # Draw the text + d = PIL.ImageDraw.Draw(txt) + d.text((0, 0), line, font=f, fill=255) + + # Paste the text into the base image + w = txt.rotate(0, expand=1) + im.paste(PIL.ImageOps.colorize(w, (0, 0, 0), + (255, 255, 255)), (horipos, vertpos), + w) + + # Calculate position on next line + vertpos += size[1] + + # Add jcox signature + jcoxim = PIL.Image.open(in_footer) + im.paste(jcoxim, (horipos, vertpos + int(sig_height / 3))) + + # Crop the image to the actual resulting size + im = im.crop((0, 0, image_width, vertpos + int(sig_height * 2.5))) + + # Save image + out_filename = f"/tmp/{ctx.message.id}-out.png" + im.save(out_filename, quality=100, optimize=True) + await ctx.send(content=f"{mention}: Enjoy.", + file=discord.File(out_filename)) + def setup(bot): bot.add_cog(ImageManip(bot)) From 83b18d7a0d04501d858ac73217cdc86acbc2687c Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 19 Dec 2019 16:43:37 +0300 Subject: [PATCH 17/43] .trump: make image size be... better --- cogs/imagemanip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index 031e82c..8e0abf1 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -98,7 +98,7 @@ class ImageManip(Cog): # Wrap into lines lines = textwrap.wrap(headline, width=font_wrap_count) # not great, 4am be like - image_height = (len(lines) + 2) * vertpos + image_height = (len(lines) + 3) * vertpos # Load font f = PIL.ImageFont.truetype(font_path, font_size) From fac3ac3605317ae1aeed973e1fb7f27267ed4dc6 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Sun, 22 Dec 2019 01:18:59 +0300 Subject: [PATCH 18/43] Revert ".trump: make image size be... better" This reverts commit 83b18d7a0d04501d858ac73217cdc86acbc2687c. --- cogs/imagemanip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index 8e0abf1..031e82c 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -98,7 +98,7 @@ class ImageManip(Cog): # Wrap into lines lines = textwrap.wrap(headline, width=font_wrap_count) # not great, 4am be like - image_height = (len(lines) + 3) * vertpos + image_height = (len(lines) + 2) * vertpos # Load font f = PIL.ImageFont.truetype(font_path, font_size) From b6f6b984800b2133076d696d80ec94bce063e7fd Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Sun, 22 Dec 2019 01:19:28 +0300 Subject: [PATCH 19/43] Revert ".trump" This reverts commit e53a6341a5c1e0d66ffd134e4fed0e1cf118720e. --- assets/trumpfooter.png | Bin 6214 -> 0 bytes assets/trumpheader.png | Bin 9133 -> 0 bytes cogs/imagemanip.py | 63 ----------------------------------------- 3 files changed, 63 deletions(-) delete mode 100644 assets/trumpfooter.png delete mode 100644 assets/trumpheader.png diff --git a/assets/trumpfooter.png b/assets/trumpfooter.png deleted file mode 100644 index 4e81e349ffa3556813996d48ae73f5f1ece5dafd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6214 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 diff --git a/assets/trumpheader.png b/assets/trumpheader.png deleted file mode 100644 index 34efe9e4c6a3d39aaf0ca1610673a9c1937d71f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9133 zcmYLPcR1Vc*S0BYZ=qTVS_9=7u`9fNvV3ceM^&G%QBV8cpLU zD=a;}3~w(;cr0XAcPmi7B_&s5X`~UK=~e2cjM*fAmC~3~nUaeetev}uh($&8_nmcz zlT%C!FE$PMt1mv4&H*L&{CmUpLWhDrj$Zw-TfU^AfhRP*B^-wfE7-I_qOSl>osN=2 zf()}#A3BocU)9M!jXhEI&&_kq!@;0a)l>{>0Z;t@rUf=k6V0X;eVtPF&$Sl))4K4X z$;WHdd{pw$BG6&HrzrsWz3CCADe&Rdh^dBy&92fkeRb

t$PO#3z@T_o;Jbt&8r+< zmxCX<^-SL;kbr=S@#Y~UD1x!!2g!pBjCIIS)Qq=pv7F``b`cQp5Ey7{SU|rWtcP|# zdGzw|2+MK$3=!^|P)w+)PedXqL&(b$DsprHw6t{k z9cQy_UHaJ_~nW32)iDL~U~iNUQr20G(czqE%_n zVDPgi;mtYikxhT>ypz6?53Zev-r`APCa#Io<#*EUoKo*G0_ac>(mdRyhLphOEfGJo z6Wc5keFf1r*GD`QG5Ghv?{>wWd?6Y(BO8TwU0AKQu%>rBSh6kM473IR08RN|-l(uU zo6k0MD~a)N1@Y7e>hpS6n-i<`nvq0HA;U#|IRdisdy}fpQJ`zYE=4p!a8@AH#2mPII$lEL_h?%%(JlHQ%IkapHDQCmXG$ zuqkDOEP^ShJyrIs2~z!)rulz6>)8Kdn#Y^vTQq+5mVJ{)`2~~&X2=yRY~}VX=FPeKIV8e*cvCYKYUVv}1sVMUEN>i_ z$=H?5IS)%KZFO6LTbBPWOZ?jawhUf!VV!(@Qtf>EcrCzql`;gj8aPiIh|pA(<~J-0 z=R$N8!PL26jx@rb#KgIi%pFVmcQcj$pq{pLA-;*f7itx#8}Gy28&g`G?2$|mmGJ1Y z3+iL&n1_YYAT-(|8Nd(|BW`s9K*!tEMAj$E}>Dwzvp+(%;j|8(JSdg89-_Tk@9 zh$7UBZB*GVr@qG1h$RZaTl_~dkUW%{QDGnhQ4#zDL>TXUNT&#cIEUY z@6%%bR_&wZ*57mdYuB4vu%MrXv(OC2W|o48ZHb=mZ?M0PR&vtS@&_;W1q9cxDnCQ1fYXs&EQWq*G;I zm5ty7l{4!D9FG)8c;ZP2XCA=XqqIN(KjWgPWH;WH_kh zIH+ml*gQVhFN=H@26lo#vc~!y+En_eR{^%)agg*dd*SafCo^sbvzCDiM`H;OKc>vx z8iNPej?sShUQ`vt?*;1g@064^;9D{BU&k2ibXOX8KqU_2Sjlys?*FP~@sl{$o4JzW zc!^CXkYu$`y_+cdqg|}DA~@`j#XI1*$+&B=dXc+oky~)K*V;W?x(c?cYRXTi8DsH7 zNJHnAuy}O(oM%k3tZ6dF z{rDL@r~GZ=`+mRZust|hd#O9Q#lAghJ1t7_6*`m9O!&{|@dM?)lAm=+7sD8x>sk7F5XO>)?N>X%zSGM+B_DsUzYf1SqtFI1HTk+SXZk zi{%%%*OO(~8?xbI3@~z9Fp!R+fj5Y$f;Wvdvl#ZgYZ~eu^Sn;Wsf$*PkUKh)0mw8% zr5rP2ONsW6VkDB_Lxuymf#HjYlggn`a?NtotH4<$F8-as(`S)qWiMLqtO0sm{0EnJ z*V<E?cQAJ@R)|CV_bxVW|4s;By7Kvx z>)q6MCO4ykNv#uG&MSD?7Wwa6S+js~HtY5W8JqM@HM{Avmi{IOa{1=ogsnek0gG?E zOW#bE*smU~N&Gu-KR*k^k34TE;KBrtVfG)>_F>F8>zB{l%TBkYBTs1is2`Ms%oNVP z&bXn~{=)=$zugUqL7%SSX&2KIY=HyfaCiD4sayk@m4Gx)cTF55irS2 zNwc0Jaa3uX%CK*-W_|jH3p;`%lYt)CPsfH8f*m|<*c9c?IOE0uW)vwQZA-rQ{M<;8m)RMi8sDn zh=c;x=WYLfin)DC)hnu*2Yrz~0G0dk=P{{RJO``agT|uNL$al8T?`rx<3>-i5 zwTAoZr^FRneEE7PeB2#QWShlK`$T8=Dioxo0=}m?wKyV;3KuQl8XfxFD`6sP-H~m(?3MHq0?n8e1Xv^CV z*{ewD!A2*Ro+R6a{b2aniKP4ZlQZIzan~Bz&{+iTj;Io@{0a9+s9EdoOdp|jAbK4T zG9^+AkXEenL?>SU+F$t)I9B3K*p27<0HmV3$sQaRyxQ&g+Ak7)aS`-ushM{z0K-9m z7_+kwVdj3}!6m;Qz&Up3?32pUN7O@~pR+%UnwHnuz0Tm=(GSDBV z`=2|ADPHcsvWPft7O5kVy&Q78axAIPWo8Fv^YJG+fATB@+mbbeDc(nHz7wy1C6onq z-+M0IyH1T!1`8u}b@w_1TXjsTnUNgA21Zr(sG+|nKz`u5FJh_L?yHg%sBy6dB6^@q z&Tk;%>Nb^`yYTaLu(=J1Y9_>*QaINPhaUS5v-`dH5Xi6s3twUfz`u`QefWJve&?Gj z&coqJ=vUSYjp?0@e{#hMiGNWc_8ns=RtZ~uz}U9VaP8LC6(%sjZLnHxJTNigRn;ry z*^p4|uALtm2eCShyi>p2{S$LNVuQXGhfMpwy+T->Lf2b4{PEF7ifs4apS~M8L<%ma zqHedTrP?L+J0I7Ag>wXeS;=%@NIakPD(h!V2b*8|(RR+mcb<#7ay~0Vycs^5ZDVDQ zK0YvC!4DiMhunT*q4`e9MA1CykV*xCNva|g$|b&HaoQs!u-8VzNjcu14Pz#(3A#1I z`=dF75lFgZ?m{{fW`9v+rH0}Ae&fAqufJZL|CJ0`>&X{+doG@#)^;n(+(fmlj7nNj zxf4P=DqTpe@7w>^@+LqyP%A%kSwFwM9cqH7qQ4#TzlmFjcW2id2fL1c>|B;+;Vr$a zi}uUzme`QFmewK=h5Y8fvbXmObHlyn38e)2T>c7HFE1T#f}j$H-$u8*t{}1wxMQmi z^4fUOL}*)Ozb1B@8PZEm7kicallIJ`*&pc)(-KY^TJSZ_WFJFp3F5KUmB)}OzZJp< zslWnneFswmnVMn3dq7|TI9ox$fT1an&r{P@cu|b{D>P+(UZ6y|N@;%hLG-B4m>+_s{mkyBupD;PYPCAzE7uT; zH9)b&m4Ee`pP_DEo7-m*RmN?4lFL0k?y08Es4| z`QF^G*5si|)U2w}{C*fk$4ze#mea@w(ouj+*kz}S2sIH`_*1~<9D4LkHl2dC18pVD>bE}!nNRqv~AD=sIajn+9 zh}@cVxJ_t$lRe#S1r|(dCslF)7s3iPDRmz_6%hEgBBR5p#m8@EPv^=GN<#Y79P|ZI zNzB_;1jnhHD?Gfbu1{#PNeE;H=}q!C*^7<=_=}V zt!v&&3{gCoqUTL&(tIwxjUyp2V5rK~-kis$PL5pMvGOeLTr*mwoB`fRCY(6C~kn4t$5opALDg^av>3;z0QBlinB ztL49Q$^R8CeiIevcUI5DPP}S+?aR7#H{G0?C~1^UZwv6meGXh1r>Br;1c^I|`th`9 zll1L(AZ&L@^Zm00#BiBplKU%(i9eh9%K&!s~>nb+U5M38G!r z-s1s@sTgRXx&>nI^KLSw#0<3efCe=!sjI82zaUiu_RG&(;=lrgZqI=(&9yw(>V%&uD zN=U)lIuOO$Y~Z~kgVY6%#Id3nWsGyG4k*ns+Y6rDv~1uN+++d0J8UD@a?$c~;BVl} zgR)pwiPGb5CEiPiD2$~FmN|0!m>C8=_QsuCUFXeb(WgHih%tW{xhOt%+571v$aQs5 zQm^^(1hYdQY;oh*uJ1F7%I4~Z@%RBN%I`v{+Ot!M$;lL+EzxDAA1;8cQ;uV-y{peC zx?YmI0gs{rX&}@i;jl-WcQI~351#gSZUQn?jwyIo6RTJN9dr#$;E^9cC=lEFfGmu$ zV4bai0T>ol*=h|rLh{D*BtzoFD>!b0VsHCYt4BR6ou>U4P{zGhS9&Gz>v4O6-^&wB zAIXErpo#FZ(~7GXU#0pBMFyTLmY9%7AvSr?4!GxdZ{YC>bG^QAors2osII>Ey*+G% zubCZ=kjUAMJ@EOK-}V$L4Vk{>T;g29)4Wu##}7Lb!h=yw1mxG$@5l0=Qnp=!W*|ly zOrH2M$^tmaq|dO*ek=?My;OpoMb)jBmRp#x7tIr3gnHTI)3X+0GP^6gmeLrA?(#a9 zI0hR9Kg(QDFEL@@P1SZ{kHB4dt{;x*q$nQAw(NZJdr@*5vwU!R_az>N{+<0m`$I?P zAeXq7ex7hEEmN~)!dh=yk2Lzvd;P&LX?=e!Y;uBsC(m2*uc8dw^u}Rn*QN&0T!Wmc+#1 zJ;cP#1g)SOsfg1Yj~ns+{*x!KupOW6XJ=bSo0hh1?Hw?79>0e4U|9zpKb!|&a%Tn9 zX!d=%*`QrS7~@$Jy!Zo6Bzq#@eQx*?a$U{$;7Q24fAv*_=PF7MXxuyPnt>`RRIFmV zQ|%idrn7)dZu>6Z0`5xpg}tC|O!@-_uN@Gm2CO#sEtKu)m(C4zX=lh;c(sZ0U)g`Z z6Uq+1Eti3q^k#c~9?;}<+KDY8gVFYRk?%ykvAsD~B~S+17P~R7t_{SVpJI;pV4$&D zw+}TUZkaN4WP6am&*9BvLfq{b5s7|A zw8!^DYCu^ZazxK3y}KC{Gg9?pm}}V~Hqf`l@4cjgevR$c`6IPz3*n>u_SvlghpX&6 z4qeIdDJ+5KcBQs#zN<^kfakc$^I@W)V&G6T`RSeh zoZ5;wc}OdRbv82maEQ{ z+cq-_@(1OXXUEk)))XQIIoFHda&rD8uKN1vd$rBMY)6+Cex$1Wj6@ft7V08oY=zC??$x@^6^dZOl_ z#q^PSx`cd4On!X@Ys|*Q?gTw?bDQS=R+o^R6Bmu(xzTn|jnWR**Lv{n#-vQcIF+w8 zyvU?(5&NgXLInStXI`^~u>a>zd28*bzkp-ccvapH{+iOinZD{*e5&Hdj$HFAb5MDv zW|tuU5O|}(a{Qac`xRe%j0y0hc>kX_Exrr}{Jlbu`Y=cldQ|aa1S~f6eY`KT^BYc| zu@0zBk8Fo0YL|EF8m0vf6eL#3B5aLHQPfZob&o@$1{nh|kyNTIG@vPZgrg})yKSVK zh|3^1pMn$tg@JJjmCAu+s_Ar~yCv#SD#Rg9>kNC6wu}irR>oeJU8QT#4mEw%@>%T- z{-pST?7^R~5rL9G0sNGSklHpbFJ*xieCII{L~h>?y~;N{!EngCP^@GM8VMr zPG9tt{FOj}548yC<{`onK2}eW(``?OQbwNDzBB}8filsG>n6o(*568~b7BwaHHG?# z(JGoil1x{2?vzZT1{JWKKC@K5cAGAOQiMd8Du7j^sQiuVi|QZKtmN*1f@|#c7m2X} zyhX^6k$Y0pNGVB;e1fFc;LR?(77zH?cP*Mw#EaK;lN+xAZ`qOkJ!+y-ib1kZZq8`7 zeM$b@yYf#{c2S+(d*xZrgm}7}$cfd^k@#@)C9P@ymPHe0r$--{1$}I0S`TY_^@=g+ zlmV@R{B!IR)?Q6gVv;z&tAJvBnmon(GG<9qQz%u^8m%#B=kbs-jzoewFD#Xn{tu{U z8B!&Kw5?8+6sqiq6Rz3T+=OGEwwrFR1x}_d4t(7*=1lrtU&hq`%WClFS$9+K=>@^# z-!mx}1`Q7ja^`*HG$_b8V*6Blhc&1?I|aH=A6Th1l6m1t5l63gxX2lyS+}?~A@%_f zF-m_K<+FgtIh^azsLscx%g}6F)IMN5*T&^SshSM#G^jjT+b=iA#%Gd^{zqI|4K!m) z91_LbbPdaj0`~7GD+$zZtFoac?R&gw3^8jg+o8IpdTdqxVHbxvfzYs&U&dFTy&rL; zX&iEFK|nQ>Ag|Ote%x;D$^dI_)tA!p#iwT7EHg~nwv66s>ZaDw?L>kQP#SwZxS`9x zmfV{<@_P8pM8p%i;PQ4NNpLVh@6Okuj21gtSIR#@*zLF4b)`O3nDV*0L0UqySe08vz8ZmsigwQ2xklKKei1 z(cn^R&s+j?HlM8@EDUz(4Ki-guZE(f6YL^>t8n*+9H?#_s(54FmiF(8hoL{3*Xln! zP4EBpdGQ8(?4#MS&W)a;z>VjGtb=Elm&=w)rTnew zwc|xjMdz>YUV6ov)P1Oeu8=Q>gptPEsmg=B`lOB_7_Xs3~7np)7EaEJR6q1Ky zvpH&hpNg>!K@Ia=w?h*GS@!mfH!L$D6Ui{;#P|L`hKcKGZr=sD_QvnDh=-Efl|q#} ztZ#b6B#Tr&1adTfTSco>GuHkTE>Gg*;lU0BVj5dn$SwFVp{7@*J_Cj=zE+5`>5mZq z{A!M7jg|iV#rg&ECqg0dPr{T<0IE<3z=gGWFm<>P zKa%<*4z0+xI(fTw@xgCWpPWv19oqqDt|c~@B-GtASir-bODQy3kLOXUHj2v{)%spu z#g6h86t*76u7kfTKv;Tn7vQSsEJ|HJDJS0qS-Uv#`iC_lODOx z3IXRpPdA)Cp-7pQw&;28wm;R??UIr#oqtasYU@7XFL-R-KD|7E<3ILgu?5}$6a%k3 z+vS09d2_S7S$2*}lG9|PNt>S+>l2SabjZgjblfJ$1B7cRH7O2h?h+_3F?%pO!K;eV z&>;oG_H~!Zak+0?8KSeBn;-I@ee4<>yF81psQVB4TKASPpIR8fRC5hLwfsn&eCyRc z<0j^$^!kFv+#D$oh$Kf*jtEU1FNtr@i_U6*0^B{HOWPJ!ctc8=WS3M9ni$*l W__9&gI{qRDfq{;R_FGNInEwOWnUe1S diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index 031e82c..abf4412 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -77,69 +77,6 @@ class ImageManip(Cog): await ctx.send(content=f"{mention}: Enjoy.", file=discord.File(out_filename)) - @commands.check(check_if_staff_or_ot) - @commands.command(hidden=True) - async def trump(self, ctx, *, headline: str): - """Gives a trump tweet""" - mention = ctx.author.mention - - in_header = "assets/trumpheader.png" - in_footer = "assets/trumpfooter.png" - font_path = "assets/Segoe UI.ttf" - - # Settings for image generation, don't touch anything - horipos = 10 - vertpos = 70 - font_size = 27 - image_width = 590 - font_wrap_count = 45 - sig_height = 49 - - # Wrap into lines - lines = textwrap.wrap(headline, width=font_wrap_count) - # not great, 4am be like - image_height = (len(lines) + 2) * vertpos - - # Load font - f = PIL.ImageFont.truetype(font_path, font_size) - - # Create image base, paste mobo logo - im = PIL.Image.new("RGB", (image_width, image_height), color="#15202B") - headerim = PIL.Image.open(in_header) - im.paste(headerim, (horipos, 15)) - - # Go through all the wrapped text lines - for line in lines: - # Get size of the text by font, create a new image of that size - size = f.getsize(line) - txt = PIL.Image.new('L', size) - - # Draw the text - d = PIL.ImageDraw.Draw(txt) - d.text((0, 0), line, font=f, fill=255) - - # Paste the text into the base image - w = txt.rotate(0, expand=1) - im.paste(PIL.ImageOps.colorize(w, (0, 0, 0), - (255, 255, 255)), (horipos, vertpos), - w) - - # Calculate position on next line - vertpos += size[1] - - # Add jcox signature - jcoxim = PIL.Image.open(in_footer) - im.paste(jcoxim, (horipos, vertpos + int(sig_height / 3))) - - # Crop the image to the actual resulting size - im = im.crop((0, 0, image_width, vertpos + int(sig_height * 2.5))) - - # Save image - out_filename = f"/tmp/{ctx.message.id}-out.png" - im.save(out_filename, quality=100, optimize=True) - await ctx.send(content=f"{mention}: Enjoy.", - file=discord.File(out_filename)) - def setup(bot): bot.add_cog(ImageManip(bot)) From eeb2689bae88226d85045cfa00ec37686a856f04 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Sun, 22 Dec 2019 01:20:21 +0300 Subject: [PATCH 20/43] Impose rate limits on .cox Currently 1 per 3 hours: https://discordapp.com/channels/269333940928512010/286612533757083648/658069981698981899 --- cogs/imagemanip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index abf4412..e9db782 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -14,6 +14,7 @@ class ImageManip(Cog): def __init__(self, bot): self.bot = bot + @commands.cooldown(1, 60 * 60 * 3, type=commands.BucketType.user) @commands.check(check_if_staff_or_ot) @commands.command(hidden=True) async def cox(self, ctx, *, headline: str): From d056752e98367aa0adc92dc9a91c11fc79316d8a Mon Sep 17 00:00:00 2001 From: "Ave (High Sec Drive)" Date: Fri, 27 Dec 2019 16:43:47 +0100 Subject: [PATCH 21/43] logs: Only allow events from whitelsited guilds --- cogs/logs.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cogs/logs.py b/cogs/logs.py index aa63fac..7ce2bb1 100644 --- a/cogs/logs.py +++ b/cogs/logs.py @@ -34,6 +34,10 @@ class Logs(Cog): @Cog.listener() async def on_member_join(self, member): await self.bot.wait_until_ready() + + if (member_after.guild.id not in config.guild_whitelist): + return + log_channel = self.bot.get_channel(config.log_channel) # We use this a lot, might as well get it once escaped_name = self.bot.escape_message(member) @@ -246,6 +250,10 @@ class Logs(Cog): @Cog.listener() async def on_member_remove(self, member): await self.bot.wait_until_ready() + + if (member.guild.id not in config.guild_whitelist): + return + log_channel = self.bot.get_channel(config.log_channel) msg = f"⬅️ **Leave**: {member.mention} | "\ f"{self.bot.escape_message(member)}\n"\ @@ -255,6 +263,10 @@ class Logs(Cog): @Cog.listener() async def on_member_ban(self, guild, member): await self.bot.wait_until_ready() + + if (guild.id not in config.guild_whitelist): + return + log_channel = self.bot.get_channel(config.modlog_channel) msg = f"⛔ **Ban**: {member.mention} | "\ f"{self.bot.escape_message(member)}\n"\ @@ -264,6 +276,10 @@ class Logs(Cog): @Cog.listener() async def on_member_unban(self, guild, user): await self.bot.wait_until_ready() + + if (guild.id not in config.guild_whitelist): + return + log_channel = self.bot.get_channel(config.modlog_channel) msg = f"⚠️ **Unban**: {user.mention} | "\ f"{self.bot.escape_message(user)}\n"\ @@ -282,6 +298,10 @@ class Logs(Cog): @Cog.listener() async def on_member_update(self, member_before, member_after): await self.bot.wait_until_ready() + + if (member_after.guild.id not in config.guild_whitelist): + return + msg = "" log_channel = self.bot.get_channel(config.log_channel) if member_before.roles != member_after.roles: From 656dbb1a0eee9a5356b7cb82b417dd21abd505af Mon Sep 17 00:00:00 2001 From: Mz <52611335+Mz49@users.noreply.github.com> Date: Wed, 8 Jan 2020 02:08:38 -0800 Subject: [PATCH 22/43] condensed a few lines to a new function --- helpers/userlogs.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/helpers/userlogs.py b/helpers/userlogs.py index 679a4de..275d6a4 100644 --- a/helpers/userlogs.py +++ b/helpers/userlogs.py @@ -18,9 +18,9 @@ def set_userlog(contents): f.write(contents) -def userlog(uid, issuer, reason, event_type, uname: str = ""): +def fill_userlog(userid, uname): userlogs = get_userlog() - uid = str(uid) + uid = str(userid) if uid not in userlogs: userlogs[uid] = {"warns": [], "mutes": [], @@ -31,6 +31,13 @@ def userlog(uid, issuer, reason, event_type, uname: str = ""): "name": "n/a"} if uname: userlogs[uid]["name"] = uname + + return userlogs, uid + + +def userlog(uid, issuer, reason, event_type, uname: str = ""): + userlogs, uid = fill_userlog(uid, uname) + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) log_data = {"issuer_id": issuer.id, "issuer_name": f"{issuer}", @@ -44,19 +51,7 @@ def userlog(uid, issuer, reason, event_type, uname: str = ""): def setwatch(uid, issuer, watch_state, uname: str = ""): - userlogs = get_userlog() - uid = str(uid) - # Can we reduce code repetition here? - if uid not in userlogs: - userlogs[uid] = {"warns": [], - "mutes": [], - "kicks": [], - "bans": [], - "notes": [], - "watch": False, - "name": "n/a"} - if uname: - userlogs[uid]["name"] = uname + userlogs, uid = fill_userlog(uid, uname) userlogs[uid]["watch"] = watch_state set_userlog(json.dumps(userlogs)) From 40570ee112cffa4397f4b0915fd7b40bb15cf00b Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Mon, 3 Feb 2020 20:18:30 +0300 Subject: [PATCH 23/43] noteid: fix a bug where it wouldn't work --- cogs/mod_note.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/mod_note.py b/cogs/mod_note.py index df943e4..9f407f1 100644 --- a/cogs/mod_note.py +++ b/cogs/mod_note.py @@ -25,7 +25,7 @@ class ModNote(Cog): """Adds a note to a user by userid, staff only.""" userlog(target, ctx.author, note, "notes") - await ctx.send(f"{target.mention}: noted!") + await ctx.send(f"{ctx.author.mention}: noted!") def setup(bot): From 4058612e9669a3eea13d962dfaefae4f6b91ab94 Mon Sep 17 00:00:00 2001 From: Nichole Mattera <697668+NicholeMattera@users.noreply.github.com> Date: Tue, 25 Feb 2020 18:15:49 -0500 Subject: [PATCH 24/43] Added cog to manage channels dedicated towards lists. --- Robocop.py | 3 +- cogs/lists.py | 290 +++++++++++++++++++++++++++++++++++++++++++++ config_template.py | 6 + 3 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 cogs/lists.py diff --git a/Robocop.py b/Robocop.py index cc6877c..1c46a30 100755 --- a/Robocop.py +++ b/Robocop.py @@ -63,7 +63,8 @@ initial_extensions = ['cogs.common', 'cogs.meme', 'cogs.imagemanip', 'cogs.pin', - 'cogs.invites'] + 'cogs.invites', + 'cogs.lists'] bot = commands.Bot(command_prefix=get_prefix, description=config.bot_description) diff --git a/cogs/lists.py b/cogs/lists.py new file mode 100644 index 0000000..5285df6 --- /dev/null +++ b/cogs/lists.py @@ -0,0 +1,290 @@ +import config +import discord +import io +import urllib.parse +from discord.ext import commands +from discord.ext.commands import Cog + +class Lists(Cog): + """ + Manages channels that are dedicated to lists. + """ + + def __init__(self, bot): + self.bot = bot + + # Helpers + + def check_if_target_is_staff(self, target): + return any(r.id in config.staff_role_ids for r in target.roles) + + def is_edit(self, emoji): + return str(emoji)[0] == u"✏" or str(emoji)[0] == u"📝" + + def is_delete(self, emoji): + return str(emoji)[0] == u"❌" or str(emoji)[0] == u"❎" + + def is_recycle(self, emoji): + return str(emoji)[0] == u"♻" + + def is_insert_above(self, emoji): + return str(emoji)[0] == u"⤴️" or str(emoji)[0] == u"⬆" + + def is_insert_below(self, emoji): + return str(emoji)[0] == u"⤵️" or str(emoji)[0] == u"⬇" + + def is_reaction_valid(self, reaction): + allowed_reactions = [ + u"✏", + u"📝", + u"❌", + u"❎", + u"♻", + u"⤴️", + u"⬆", + u"⬇", + u"⤵️", + ] + return str(reaction.emoji)[0] in allowed_reactions + + async def find_reactions(self, user_id, channel_id, limit = None): + reactions = [] + channel = self.bot.get_channel(channel_id) + async for message in channel.history(limit = limit): + if len(message.reactions) == 0: + continue + + for reaction in message.reactions: + users = await reaction.users().flatten() + user_ids = map(lambda user: user.id, users) + if user_id in user_ids: + reactions.append(reaction) + + return reactions + + def create_log_message(self, emoji, action, user, channel, reason = ""): + msg = f"{emoji} **{action}** \n"\ + f"from {self.bot.escape_message(user.name)} ({user.id}), in {channel.mention}" + + if reason != "": + msg += f":\n`{reason}`" + + return msg + + async def clean_up_raw_text_file_message(self, message): + embeds = message.embeds + if len(embeds) == 0: + return + + fields = embeds[0].fields + for field in fields: + if field.name == "Message ID": + files_channel = self.bot.get_channel(config.list_files_channel) + file_message = await files_channel.fetch_message(int(field.value)) + await file_message.delete() + + await message.edit(embed = None) + + # Commands + + @commands.command(aliases = ["list"]) + async def listitem(self, ctx, channel: discord.TextChannel, number: int): + """Link to a specific list item.""" + if number <= 0: + await ctx.send(f"Number must be greater than 0.") + return + + if channel.id not in config.list_channels: + await ctx.send(f"{channel.mention} is not a list channel.") + return + + counter = 0 + async for message in channel.history(limit = None, oldest_first = True): + if message.content.strip(): + counter += 1 + + if counter == number: + embed = discord.Embed( + title = f"Item #{number} in #{channel.name}", + description = message.content, + url = message.jump_url + ) + await ctx.send( + content = "", + embed = embed + ) + return + + await ctx.send(f"Unable to find item #{number} in {channel.mention}.") + + # Listeners + + @Cog.listener() + async def on_raw_reaction_add(self, payload): + await self.bot.wait_until_ready() + + # We only care about reactions in Rules, and Support FAQ + if payload.channel_id not in config.list_channels: + return + + channel = self.bot.get_channel(payload.channel_id) + message = await channel.fetch_message(payload.message_id) + member = channel.guild.get_member(payload.user_id) + user = self.bot.get_user(payload.user_id) + reaction = next((reaction for reaction in message.reactions if str(reaction.emoji) == str(payload.emoji)), None) + if reaction is None: + return + + # Only staff can add reactions in these channels. + if not self.check_if_target_is_staff(member): + await reaction.remove(user) + return + + # Reactions are only allowed on messages from the bot. + if not message.author.bot: + await reaction.remove(user) + return + + # Only certain reactions are allowed. + if not self.is_reaction_valid(reaction): + await reaction.remove(user) + return + + # Remove all other reactions from user in this channel. + for r in await self.find_reactions(payload.user_id, payload.channel_id): + if r.message.id != message.id or (r.message.id == message.id and str(r.emoji) != str(reaction.emoji)): + await r.remove(user) + + # When editing we want to provide the user a copy of the raw text. + if self.is_edit(reaction.emoji) and config.list_files_channel != 0: + files_channel = self.bot.get_channel(config.list_files_channel) + file = discord.File(io.BytesIO(message.content.encode("utf-8")), filename = f"{message.id}.txt") + file_message = await files_channel.send(file = file) + + embed = discord.Embed( + title = "Click here to get the raw text to modify.", + url = f"{file_message.attachments[0].url}?" + ) + embed.add_field(name = "Message ID", value = file_message.id, inline = False) + await message.edit(embed = embed) + + @Cog.listener() + async def on_raw_reaction_remove(self, payload): + await self.bot.wait_until_ready() + + # We only care about reactions in Rules, and Support FAQ + if payload.channel_id not in config.list_channels: + return + + channel = self.bot.get_channel(payload.channel_id) + message = await channel.fetch_message(payload.message_id) + + # Reaction was removed from a message we don"t care about. + if not message.author.bot: + return + + # We want to remove the embed we added. + if self.is_edit(payload.emoji) and config.list_files_channel != 0: + await self.clean_up_raw_text_file_message(message) + + @Cog.listener() + async def on_message(self, message): + await self.bot.wait_until_ready() + + # We only care about messages in Rules, and Support FAQ + if message.channel.id not in config.list_channels: + return + + # We don"t care about messages from bots. + if message.author.bot: + return + + # Only staff can modify lists. + if not self.check_if_target_is_staff(message.author): + await message.delete() + return + + log_channel = self.bot.get_channel(config.log_channel) + channel = message.channel + content = message.content + user = message.author + + attachment_filename = None + attachment_data = None + if len(message.attachments) != 0: + # Lists will only reupload the first image. + attachment = next((a for a in message.attachments if a.filename.endswith(".png") or a.filename.endswith(".jpg") or a.filename.endswith(".jpeg")), None) + if attachment != None: + attachment_filename = attachment.filename + attachment_data = await attachment.read() + + await message.delete() + + reactions = await self.find_reactions(user.id, channel.id) + + # Add to the end of the list if there is no reactions or somehow more than one. + if len(reactions) != 1: + if attachment_filename != None and attachment_data != None: + file = discord.File(io.BytesIO(attachment_data), filename = attachment_filename) + await channel.send(content = content, file = file) + else: + await channel.send(content) + + for reaction in reactions: + await reaction.remove(user) + + await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + return + + targeted_reaction = reactions[0] + targeted_message = targeted_reaction.message + + if self.is_edit(targeted_reaction): + if config.list_files_channel != 0: + await self.clean_up_raw_text_file_message(targeted_message) + await targeted_message.edit(content = content) + await targeted_reaction.remove(user) + + await log_channel.send(self.create_log_message("📝", "List item edited:", user, channel)) + + elif self.is_delete(targeted_reaction): + await targeted_message.delete() + + await log_channel.send(self.create_log_message("❌", "List item deleted:", user, channel, content)) + + elif self.is_recycle(targeted_reaction): + messages = await channel.history(limit = None, after = targeted_message, oldest_first = True).flatten() + await channel.purge(limit = len(messages) + 1, bulk = True) + + await channel.send(targeted_message.content) + for message in messages: + await channel.send(message.content) + + await log_channel.send(self.create_log_message("♻", "List item recycled:", user, channel, content)) + + elif self.is_insert_above(targeted_reaction): + messages = await channel.history(limit = None, after = targeted_message, oldest_first = True).flatten() + await channel.purge(limit = len(messages) + 1, bulk = True) + + await channel.send(content) + await channel.send(targeted_message.content) + for message in messages: + self.bot.log.info(message.content) + await channel.send(message.content) + + await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + + elif self.is_insert_below(targeted_reaction): + messages = await channel.history(limit = None, after = targeted_message, oldest_first = True).flatten() + await channel.purge(limit = len(messages) + 1, bulk = True) + + await channel.send(targeted_message.content) + await channel.send(content) + for message in messages: + self.bot.log.info(message.content) + await channel.send(message.content) + + await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + +def setup(bot): + bot.add_cog(Lists(bot)) diff --git a/config_template.py b/config_template.py index 70f4e29..d9ac323 100644 --- a/config_template.py +++ b/config_template.py @@ -97,3 +97,9 @@ allowed_pin_roles = [] # Used for the pinboard. Leave empty if you don't wish for a gist pinboard. github_oauth_token = "" + +# Channel to upload text files while editing list items. (They are cleaned up.) +list_files_channel = 0 + +# Channels that are lists that are controlled by the lists cog. +list_channels = [] From fd3a26c80e86a0e690138c639796218ceb0e08a4 Mon Sep 17 00:00:00 2001 From: Nichole Mattera <697668+NicholeMattera@users.noreply.github.com> Date: Tue, 25 Feb 2020 20:08:55 -0500 Subject: [PATCH 25/43] Ran code through autopep8. --- cogs/lists.py | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/cogs/lists.py b/cogs/lists.py index 5285df6..72b4c61 100644 --- a/cogs/lists.py +++ b/cogs/lists.py @@ -5,6 +5,7 @@ import urllib.parse from discord.ext import commands from discord.ext.commands import Cog + class Lists(Cog): """ Manages channels that are dedicated to lists. @@ -59,7 +60,7 @@ class Lists(Cog): user_ids = map(lambda user: user.id, users) if user_id in user_ids: reactions.append(reaction) - + return reactions def create_log_message(self, emoji, action, user, channel, reason = ""): @@ -68,7 +69,7 @@ class Lists(Cog): if reason != "": msg += f":\n`{reason}`" - + return msg async def clean_up_raw_text_file_message(self, message): @@ -82,7 +83,7 @@ class Lists(Cog): files_channel = self.bot.get_channel(config.list_files_channel) file_message = await files_channel.fetch_message(int(field.value)) await file_message.delete() - + await message.edit(embed = None) # Commands @@ -131,7 +132,9 @@ class Lists(Cog): message = await channel.fetch_message(payload.message_id) member = channel.guild.get_member(payload.user_id) user = self.bot.get_user(payload.user_id) - reaction = next((reaction for reaction in message.reactions if str(reaction.emoji) == str(payload.emoji)), None) + reaction = next( + (reaction for reaction in message.reactions + if str(reaction.emoji) == str(payload.emoji)), None) if reaction is None: return @@ -152,20 +155,25 @@ class Lists(Cog): # Remove all other reactions from user in this channel. for r in await self.find_reactions(payload.user_id, payload.channel_id): - if r.message.id != message.id or (r.message.id == message.id and str(r.emoji) != str(reaction.emoji)): + if r.message.id != message.id or (r.message.id == message.id and + str(r.emoji) != str(reaction.emoji)): await r.remove(user) # When editing we want to provide the user a copy of the raw text. if self.is_edit(reaction.emoji) and config.list_files_channel != 0: files_channel = self.bot.get_channel(config.list_files_channel) - file = discord.File(io.BytesIO(message.content.encode("utf-8")), filename = f"{message.id}.txt") + file = discord.File( + io.BytesIO(message.content.encode("utf-8")), + filename = f"{message.id}.txt") file_message = await files_channel.send(file = file) embed = discord.Embed( title = "Click here to get the raw text to modify.", - url = f"{file_message.attachments[0].url}?" - ) - embed.add_field(name = "Message ID", value = file_message.id, inline = False) + url = f"{file_message.attachments[0].url}?") + embed.add_field( + name = "Message ID", + value = file_message.id, + inline = False) await message.edit(embed = embed) @Cog.listener() @@ -194,7 +202,7 @@ class Lists(Cog): # We only care about messages in Rules, and Support FAQ if message.channel.id not in config.list_channels: return - + # We don"t care about messages from bots. if message.author.bot: return @@ -213,8 +221,10 @@ class Lists(Cog): attachment_data = None if len(message.attachments) != 0: # Lists will only reupload the first image. - attachment = next((a for a in message.attachments if a.filename.endswith(".png") or a.filename.endswith(".jpg") or a.filename.endswith(".jpeg")), None) - if attachment != None: + attachment = next((a for a in message.attachments if + a.filename.endswith(".png") or a.filename.endswith(".jpg") or + a.filename.endswith(".jpeg")), None) + if attachment is not None: attachment_filename = attachment.filename attachment_data = await attachment.read() @@ -222,10 +232,13 @@ class Lists(Cog): reactions = await self.find_reactions(user.id, channel.id) - # Add to the end of the list if there is no reactions or somehow more than one. + # Add to the end of the list if there is no reactions or somehow more + # than one. if len(reactions) != 1: - if attachment_filename != None and attachment_data != None: - file = discord.File(io.BytesIO(attachment_data), filename = attachment_filename) + if attachment_filename is not None and attachment_data is not None: + file = discord.File( + io.BytesIO(attachment_data), + filename = attachment_filename) await channel.send(content = content, file = file) else: await channel.send(content) @@ -286,5 +299,6 @@ class Lists(Cog): await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + def setup(bot): bot.add_cog(Lists(bot)) From 1fe4dab6cd396b83788362dab2e8cc0999f222f9 Mon Sep 17 00:00:00 2001 From: Nichole Mattera <697668+NicholeMattera@users.noreply.github.com> Date: Wed, 26 Feb 2020 08:46:11 -0500 Subject: [PATCH 26/43] Forgot to remove logs after debugging. --- cogs/lists.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cogs/lists.py b/cogs/lists.py index 72b4c61..4305f54 100644 --- a/cogs/lists.py +++ b/cogs/lists.py @@ -282,7 +282,6 @@ class Lists(Cog): await channel.send(content) await channel.send(targeted_message.content) for message in messages: - self.bot.log.info(message.content) await channel.send(message.content) await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) @@ -294,7 +293,6 @@ class Lists(Cog): await channel.send(targeted_message.content) await channel.send(content) for message in messages: - self.bot.log.info(message.content) await channel.send(message.content) await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) From d04a4e8faa0a94ca6255d7b9cae112091e7d7507 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Sat, 14 Mar 2020 16:26:47 +0300 Subject: [PATCH 27/43] Update list of maintainers --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 491c3a5..0695857 100755 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ the following require me to rethink some of the lockdown code, which I don't fee ## Credits -Robocop-NG is currently developed and maintained by @aveao and @tumGER. The official bot is hosted by @yuukieve. +Robocop-NG was initially developed by @aveao and @tumGER. It is currently maintained by @aveao. Similarly, the official robocop-ng on reswitched discord guild is hosted by @aveao too. I (ave) would like to thank the following, in no particular order: From e2076fd41445adb7a03a3703fc5ade01dce21bef Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 19 Mar 2020 21:33:52 +0300 Subject: [PATCH 28/43] Remove a guide --- cogs/links.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/links.py b/cogs/links.py index e86a19e..1ee3ea7 100644 --- a/cogs/links.py +++ b/cogs/links.py @@ -38,8 +38,8 @@ class Links(Cog): await ctx.send("**Generic starter guides:**\n" "Nintendo Homebrew's Guide: " "\n" - "AtlasNX's Guide: " - "\n" + # "AtlasNX's Guide: " + # "\n" # "Pegaswitch Guide: " # "(outdated for anything but Pegaswitch/3.0.0)\n" "\n**Specific guides:**\n" From 1e0ed86b4c16d73f1db42a8875f70972261ab2c5 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 19 Mar 2020 22:19:38 +0300 Subject: [PATCH 29/43] Add official dev env thing --- cogs/links.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cogs/links.py b/cogs/links.py index 1ee3ea7..f05b746 100644 --- a/cogs/links.py +++ b/cogs/links.py @@ -47,6 +47,8 @@ class Links(Cog): "\n" "Manually Repairing/Downgrading (without HOS): " "\n" + "How to set up a Homebrew development environment: " + "\n" "How to get started developing Homebrew: " "\n" From 0505977cb5c8d683e4d9c641e1401c7c9f940fba Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 19 Mar 2020 22:30:17 +0300 Subject: [PATCH 30/43] Remove an old guide --- cogs/links.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cogs/links.py b/cogs/links.py index f05b746..d82bb96 100644 --- a/cogs/links.py +++ b/cogs/links.py @@ -49,9 +49,6 @@ class Links(Cog): "\n" "How to set up a Homebrew development environment: " "\n" - "How to get started developing Homebrew: " - "\n" "Getting full RAM in homebrew without NSPs: " "as of Atmosphere 0.8.6, hold R while opening any game.\n" "Check if a switch is vulnerable to RCM through serial: " From c05811e8f4d4b53e222dfabddfe98b8f69084162 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Thu, 26 Mar 2020 10:22:27 +0300 Subject: [PATCH 31/43] Add new highlight word --- cogs/logs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/logs.py b/cogs/logs.py index 7ce2bb1..66901f9 100644 --- a/cogs/logs.py +++ b/cogs/logs.py @@ -21,6 +21,7 @@ class Logs(Cog): self.clean_re = re.compile(r'[^a-zA-Z0-9_ ]+', re.UNICODE) # All lower case, no spaces, nothing non-alphanumeric self.susp_words = ["sx", "tx", "reinx", # piracy-enabling cfws + "gomanx", # piracy-enabling cfws "tinfoil", "dz", # title managers "goldleaf", "lithium", # title managers "cracked", # older term for pirated games From 69ecfc5f62a0b76e23187e4a2d8c23bb6764b970 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Sun, 5 Apr 2020 23:50:58 +0300 Subject: [PATCH 32/43] Cox: Clean content before showing --- cogs/imagemanip.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index e9db782..c9aed56 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -21,6 +21,8 @@ class ImageManip(Cog): """Gives a cox headline""" mention = ctx.author.mention + headline = await commands.clean_content().convert(ctx, headline, fix_channel_mentions=True) + in_vice = "assets/motherboardlogo.png" in_byjcox = "assets/byjcox.png" font_path = "assets/neue-haas-grotesk-display-bold-regular.otf" @@ -30,7 +32,7 @@ class ImageManip(Cog): vertpos = 75 line_spacing = 10 font_size = 50 - image_width = 750 + image_width = 800 font_wrap_count = 30 sig_height = 15 From 88f1d5200d3a4bc9885ce533e444071aebc214d3 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Sun, 5 Apr 2020 23:53:20 +0300 Subject: [PATCH 33/43] cox: unfox --- cogs/imagemanip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index c9aed56..4fe59b1 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -21,7 +21,7 @@ class ImageManip(Cog): """Gives a cox headline""" mention = ctx.author.mention - headline = await commands.clean_content().convert(ctx, headline, fix_channel_mentions=True) + headline = await commands.clean_content(fix_channel_mentions=True).convert(ctx, headline) in_vice = "assets/motherboardlogo.png" in_byjcox = "assets/byjcox.png" From 06c5c63a6fb43633b1297dfd184d838f790a8890 Mon Sep 17 00:00:00 2001 From: Thog Date: Wed, 8 Apr 2020 20:14:25 +0200 Subject: [PATCH 34/43] logs: fix on_member_join crash --- cogs/logs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/logs.py b/cogs/logs.py index 66901f9..122c2f2 100644 --- a/cogs/logs.py +++ b/cogs/logs.py @@ -36,7 +36,7 @@ class Logs(Cog): async def on_member_join(self, member): await self.bot.wait_until_ready() - if (member_after.guild.id not in config.guild_whitelist): + if (member.guild.id not in config.guild_whitelist): return log_channel = self.bot.get_channel(config.log_channel) From 95dd28d7a6965c16940645686ed4e9385a2fad20 Mon Sep 17 00:00:00 2001 From: Thog Date: Thu, 9 Apr 2020 20:54:27 +0200 Subject: [PATCH 35/43] logs: Make suspect words configurable This commit move the hardcoded suspect words to the configuration. --- cogs/logs.py | 14 +++----------- config_template.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/cogs/logs.py b/cogs/logs.py index 122c2f2..af07c23 100644 --- a/cogs/logs.py +++ b/cogs/logs.py @@ -20,18 +20,10 @@ class Logs(Cog): self.name_re = re.compile(r"[a-zA-Z0-9].*") self.clean_re = re.compile(r'[^a-zA-Z0-9_ ]+', re.UNICODE) # All lower case, no spaces, nothing non-alphanumeric - self.susp_words = ["sx", "tx", "reinx", # piracy-enabling cfws - "gomanx", # piracy-enabling cfws - "tinfoil", "dz", # title managers - "goldleaf", "lithium", # title managers - "cracked", # older term for pirated games - "xci", "nsz"] # "backup" format susp_hellgex = "|".join([r"\W*".join(list(word)) for - word in self.susp_words]) + word in config.suspect_words]) self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE) - self.ok_words = [] - @Cog.listener() async def on_member_join(self, member): await self.bot.wait_until_ready() @@ -162,9 +154,9 @@ class Logs(Cog): msg += f"\n- Has invite: https://{invite[0]}" alert = True - for susp_word in self.susp_words: + for susp_word in config.suspect_words: if susp_word in cleancont and\ - not any(ok_word in cleancont for ok_word in self.ok_words): + not any(ok_word in cleancont for ok_word in config.suspect_ignored_words): msg += f"\n- Contains suspicious word: `{susp_word}`" alert = True diff --git a/config_template.py b/config_template.py index d9ac323..6ca1949 100644 --- a/config_template.py +++ b/config_template.py @@ -103,3 +103,14 @@ list_files_channel = 0 # Channels that are lists that are controlled by the lists cog. list_channels = [] + +# All lower case, no spaces, nothing non-alphanumeric +suspect_words = ["sx", "tx", "reinx", # piracy-enabling cfws + "gomanx", # piracy-enabling cfws + "tinfoil", "dz", # title managers + "goldleaf", "lithium", # title managers + "cracked", # older term for pirated games + "xci", "nsz"] # "backup" format + +# List of words that will be ignored if they match one of the suspect_words (This is used to remove false positives) +suspect_ignored_words = [] From f454219eccaa1264591bd943815675c8fe659f7b Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Sun, 19 Apr 2020 21:00:41 +0300 Subject: [PATCH 36/43] Replace dkp link as requested --- cogs/links.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/links.py b/cogs/links.py index d82bb96..d3ebe25 100644 --- a/cogs/links.py +++ b/cogs/links.py @@ -48,7 +48,7 @@ class Links(Cog): "Manually Repairing/Downgrading (without HOS): " "\n" "How to set up a Homebrew development environment: " - "\n" + "\n" "Getting full RAM in homebrew without NSPs: " "as of Atmosphere 0.8.6, hold R while opening any game.\n" "Check if a switch is vulnerable to RCM through serial: " From c5c5b457419ee0090dda86cac6e747618ce2cdba Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Tue, 21 Apr 2020 00:42:49 +0300 Subject: [PATCH 37/43] BREAKING: Move initial cogs to config FORK MAINTAINERS: Merging this without updating your config WILL lead to your bot not starting properly. --- Robocop.py | 32 ++++----------------------- config_template.py | 54 ++++++++++++++++++++++++++++++++++++++++------ requirements.txt | 3 --- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/Robocop.py b/Robocop.py index 1c46a30..15face3 100755 --- a/Robocop.py +++ b/Robocop.py @@ -1,5 +1,4 @@ import os -import asyncio import sys import logging import logging.handlers @@ -43,29 +42,6 @@ wanted_jsons = ["data/restrictions.json", "data/userlog.json", "data/invites.json"] -initial_extensions = ['cogs.common', - 'cogs.admin', - 'cogs.verification', - 'cogs.mod', - 'cogs.mod_note', - 'cogs.mod_reacts', - 'cogs.mod_userlog', - 'cogs.mod_timed', - 'cogs.mod_watch', - 'cogs.basic', - 'cogs.logs', - 'cogs.err', - 'cogs.lockdown', - 'cogs.legacy', - 'cogs.links', - 'cogs.remind', - 'cogs.robocronp', - 'cogs.meme', - 'cogs.imagemanip', - 'cogs.pin', - 'cogs.invites', - 'cogs.lists'] - bot = commands.Bot(command_prefix=get_prefix, description=config.bot_description) bot.help_command = commands.DefaultHelpCommand(dm_help=True) @@ -76,11 +52,11 @@ bot.script_name = script_name bot.wanted_jsons = wanted_jsons if __name__ == '__main__': - for extension in initial_extensions: + for cog in config.initial_cogs: try: - bot.load_extension(extension) - except Exception as e: - log.error(f'Failed to load extension {extension}.') + bot.load_extension(cog) + except: + log.error(f'Failed to load cog {cog}.') log.error(traceback.print_exc()) diff --git a/config_template.py b/config_template.py index 6ca1949..b2be450 100644 --- a/config_template.py +++ b/config_template.py @@ -15,6 +15,36 @@ embed_desc = "Robocop-NG is developed by [Ave](https://github.com/aveao)"\ "of Robocop.\nRobocop is based on Kurisu by 916253 and ihaveamac." +# The cogs the bot will load on startup. +initial_cogs = ['cogs.common', + 'cogs.admin', + 'cogs.verification', + 'cogs.mod', + 'cogs.mod_note', + 'cogs.mod_reacts', + 'cogs.mod_userlog', + 'cogs.mod_timed', + 'cogs.mod_watch', + 'cogs.basic', + 'cogs.logs', + 'cogs.err', + 'cogs.lockdown', + 'cogs.legacy', + 'cogs.links', + 'cogs.remind', + 'cogs.robocronp', + 'cogs.meme', + 'cogs.invites'] + +# The following cogs are also available but aren't loaded by default: +# cogs.imagemanip - Adds a meme command called .cox. +# Requires Pillow to be installed with pip. +# cogs.lists - Allows managing list channels (rules, FAQ) easily through the bot +# PR'd in at: https://github.com/reswitched/robocop-ng/pull/65 +# cogs.pin - Lets users pin important messages +# and sends pins above limit to a github gist + + # Minimum account age required to join the guild # If user's account creation is shorter than the time delta given here # then user will be kicked and informed @@ -84,7 +114,9 @@ lockdown_configs = { # As we no longer have mute role on ReSwitched, I set it to 0 here mute_role = 0 # Mute role in ReSwitched -# Channels that will be cleaned every minute/hour +# Channels that will be cleaned every minute/hour. +# This feature isn't very good rn. +# See https://github.com/reswitched/robocop-ng/issues/23 minutely_clean_channels = [] hourly_clean_channels = [] @@ -95,9 +127,6 @@ spy_channels = general_channels allowed_pin_channels = [] allowed_pin_roles = [] -# Used for the pinboard. Leave empty if you don't wish for a gist pinboard. -github_oauth_token = "" - # Channel to upload text files while editing list items. (They are cleaned up.) list_files_channel = 0 @@ -112,5 +141,18 @@ suspect_words = ["sx", "tx", "reinx", # piracy-enabling cfws "cracked", # older term for pirated games "xci", "nsz"] # "backup" format -# List of words that will be ignored if they match one of the suspect_words (This is used to remove false positives) -suspect_ignored_words = [] +# List of words that will be ignored if they match one of the +# suspect_words (This is used to remove false positives) +suspect_ignored_words = ["excit", + "s/x", + "3dsx", + "psx", + "txt", + "s(x", + "txd", + "t=x", + "osx"] + +# == Only if you want to use cogs.pin == +# Used for the pinboard. Leave empty if you don't wish for a gist pinboard. +github_oauth_token = "" diff --git a/requirements.txt b/requirements.txt index b5c9702..60c81ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,3 @@ humanize parsedatetime aiohttp gidgethub - -# Only if you'll enable imagemanip, which is only used for meme commands -Pillow From 38d8a4ce5f959ce59d4b4ce8a91c56d7cdd32252 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Tue, 21 Apr 2020 00:56:34 +0300 Subject: [PATCH 38/43] BREAKING: Move verification lines to config FORK MAINTAINERS: Merging this without updating your config WILL lead to your bot not starting properly. This is done to make future upstream merges easier. --- README.md | 98 --------------------------------- cogs/verification.py | 122 ++--------------------------------------- config_template.py | 125 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 215 deletions(-) diff --git a/README.md b/README.md index 0695857..be75256 100755 --- a/README.md +++ b/README.md @@ -31,104 +31,6 @@ If you're moving from Kurisu/Robocop, and want to preserve your data, you'll wan --- -## TODO - -All Robocop features are now supported. - -

-List of added Kurisu/Robocop features -

- -- [x] .py configs -- [x] membercount command -- [x] Meme commands and pegaswitch (honestly the easiest part) -- [x] source command -- [x] robocop command -- [x] Verification: Actual verification system -- [x] Verification: Reset command -- [x] Logging: joins -- [x] Logging: leaves -- [x] Logging: role changes -- [x] Logging: bans -- [x] Logging: kicks -- [x] Moderation: speak -- [x] Moderation: ban -- [x] Moderation: silentban -- [x] Moderation: kick -- [x] Moderation: userinfo -- [x] Moderation: approve-revoke (community) -- [x] Moderation: addhacker-removehacker (hacker) -- [x] Moderation: probate-unprobate (participant) -- [x] Moderation: lock-softlock-unlock (channel lockdown) -- [x] Moderation: mute-unmute -- [x] Moderation: playing -- [x] Moderation: botnickname -- [x] Moderation: nickname -- [x] Moderation: clear/purge -- [x] Moderation: restrictions (people who leave with muted role will get muted role on join) -- [x] Warns: warn -- [x] Warns: listwarns-listwarnsid -- [x] Warns: clearwarns-clearwarnsid -- [x] Warns: delwarnid-delwarn -- [x] .serr and .err (thanks tomger!) - -

-
- ---- - -The main goal of this project, to get Robocop functionality done, is complete. - -Secondary goal is adding new features: - -- [ ] Purge: On purge, send logs in form of txt file to server logs -- [ ] New feature: Modmail -- [ ] New feature: Submiterr (relies on modmail) -- [ ] Feature creep: Shortlink completion (gl/ao/etc) -- [ ] New moderation feature: timelock (channel lockdown with time, relies on robocronp) - -
-Completed features -

- -- [x] Better security, better checks and better guild whitelisting -- [x] Feature creep: Reminds -- [x] A system for running jobs in background with an interval (will be called robocronp) -- [x] Commands to list said jobs and remove them -- [x] New moderation feature: timemute (mute with time, relies on robocronp) -- [x] New moderation feature: timeban (ban with expiry, relies on robocronp) -- [x] Improvements to lockdown to ensure that staff can talk -- [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 moderation feature: watch-unwatch -- [x] New moderation feature: tracking suspicious keywords -- [x] New moderation feature: tracking invites posted -- [x] New self-moderation feature: .mywarns -- [x] New feature: Highlights (problematic words automatically get posted to modmail channel, relies on modmail) - -

-
- -
-TODO for robocronp -

- -- [ ] Reduce code repetition on mod_timed.py -- [x] Allow non-hour values on timed bans - -the following require me to rethink some of the lockdown code, which I don't feel like - -- [ ] lockdown in helper -- [ ] timelock command -- [ ] working cronjob for unlock - -

-
- ---- - ## Credits Robocop-NG was initially developed by @aveao and @tumGER. It is currently maintained by @aveao. Similarly, the official robocop-ng on reswitched discord guild is hosted by @aveao too. diff --git a/cogs/verification.py b/cogs/verification.py index b29da3d..8850078 100644 --- a/cogs/verification.py +++ b/cogs/verification.py @@ -10,122 +10,10 @@ import itertools from helpers.checks import check_if_staff -welcome_header = """ -<:ReSwitched:326421448543567872> __**Welcome to ReSwitched!**__ - -__**Be sure you read the following rules and information before participating. If you came here to ask about "backups", this is NOT the place.**__ - -__**Got questions about Nintendo Switch hacking? Before asking in the server, please see our FAQ at to see if your question has already been answered.**__ - -__**This is a server for technical discussion and development support. If you are looking for end-user support, the Nintendo Homebrew discord server may be a better fit: .**__ - -​:bookmark_tabs:__Rules:__ -""" - -welcome_rules = ( - # 1 - """ - Read all the rules before participating in chat. Not reading the rules is *not* an excuse for breaking them. - • It's suggested that you read channel topics and pins before asking questions as well, as some questions may have already been answered in those. - """, - - # 2 - """ - Be nice to each other. It's fine to disagree, it's not fine to insult or attack other people. - • You may disagree with anyone or anything you like, but you should try to keep it to opinions, and not people. Avoid vitriol. - • Constant antagonistic behavior is considered uncivil and appropriate action will be taken. - • The use of derogatory slurs -- sexist, racist, homophobic, transphobic, or otherwise -- is unacceptable and may be grounds for an immediate ban. - """, - - # 3 - 'If you have concerns about another user, please take up your concerns with a staff member (someone with the "mod" role in the sidebar) in private. Don\'t publicly call other users out.', - - # 4 - """ - From time to time, we may mention everyone in the server. We do this when we feel something important is going on that requires attention. Complaining about these pings may result in a ban. - • To disable notifications for these pings, suppress them in "ReSwitched → Notification Settings". - """, - - # 5 - """ - Don't spam. - • For excessively long text, use a service like . - """, - - # 6 - "Don't brigade, raid, or otherwise attack other people or communities. Don't discuss participation in these attacks. This may warrant an immediate permanent ban.", - - # 7 - 'Off-topic content goes to #off-topic. Keep low-quality content like memes out.', - - # 8 - 'Trying to evade, look for loopholes, or stay borderline within the rules will be treated as breaking them.', - - # 9 - """ - Absolutely no piracy or related discussion. This includes: - • "Backups", even if you legally own a copy of the game. - • "Installable" NSPs, XCIs, and NCAs; this **includes** installable homebrew (i.e. on the Home Menu instead of within nx-hbmenu). - • Signature and ES patches, also known as "sigpatches" - • Usage of piracy-focused groups' (Team Xecuter, etc.) hardware and software, such as SX OS. - This is a zero-tolerance, non-negotiable policy that is enforced strictly and swiftly, up to and including instant bans without warning. - """, - - # 10 - 'The first character of your server nickname should be alphanumeric if you wish to talk in chat.', - - # 11 - """ - Do not boost the server. - • ReSwitched neither wants nor needs your server boosts, and your money is better off elsewhere. Consider the EFF (or a charity of your choice). - • Boosting the server is liable to get you kicked (to remove the nitro boost role), and/or warned. Roles you possessed prior to the kick may not be restored in a timely fashion. - """ -) - -welcome_footer = ( - """ - :hash: __Channel Breakdown:__ - #news - Used exclusively for updates on ReSwitched progress and community information. Most major announcements are passed through this channel and whenever something is posted there it's usually something you'll want to look at. - - #switch-hacking-meta - For "meta-discussion" related to hacking the switch. This is where we talk *about* the switch hacking that's going on, and where you can get clarification about the hacks that exist and the work that's being done. - - #user-support - End-user focused support, mainly between users. Ask your questions about using switch homebrew here. - - #tool-support - Developer focused support. Ask your questions about using PegaSwitch, libtransistor, Mephisto, and other tools here. - - #hack-n-all - General hacking, hardware and software development channel for hacking on things *other* than the switch. This is a great place to ask about hacking other systems-- and for the community to have technical discussions. - """, - - """ - #switch-hacking-general - Channel for everyone working on hacking the switch-- both in an exploit and a low-level hardware sense. This is where a lot of our in-the-open development goes on. Note that this isn't the place for developing homebrew-- we have #homebrew-development for that! - - #homebrew-development - Discussion about the development of homebrew goes there. Feel free to show off your latest creation here. - - #off-topic - Channel for discussion of anything that doesn't belong in #general. Anything goes, so long as you make sure to follow the rules and be on your best behavior. - - #toolchain-development - Discussion about the development of libtransistor itself goes there. - - #cfw-development - Development discussion regarding custom firmware (CFW) projects, such as Atmosphère. This channel is meant for the discussion accompanying active development. - - #bot-cmds - Channel for excessive/random use of Robocop's various commands. - - **If you are still not sure how to get access to the other channels, please read the rules again.** - **If you have questions about the rules, feel free to ask here!** - - **Note: This channel is completely automated (aside from responding to questions about the rules). If your message didn't give you access to the other channels, you failed the test. Feel free to try again.** - """, -) - -hidden_term_line = ' • When you have finished reading all of the rules, send a message in this channel that includes the {0} hex digest of your discord "name#discriminator", and bot will automatically grant you access to the other channels. You can find your "name#discriminator" (your username followed by a ‘#’ and four numbers) under the discord channel list.' - - class Verification(Cog): def __init__(self, bot): self.bot = bot - # https://docs.python.org/3.7/library/hashlib.html#shake-variable-length-digests - self.blacklisted_hashes = {"shake_128", "shake_256"} - self.hash_choice = random.choice(tuple(hashlib.algorithms_guaranteed - - self.blacklisted_hashes)) + self.hash_choice = random.choice(config.welcome_hashes) # Export reset channel functions self.bot.do_reset = self.do_reset @@ -134,9 +22,9 @@ class Verification(Cog): async def do_reset(self, channel, author, limit: int = 100): await channel.purge(limit=limit) - await channel.send(welcome_header) + await channel.send(config.welcome_header) rules = ['**{}**. {}'.format(i, cleandoc(r)) for i, r in - enumerate(welcome_rules, 1)] + enumerate(config.welcome_rules, 1)] rule_choice = random.randint(2, len(rules)) hash_choice_str = self.hash_choice.upper() if hash_choice_str == "BLAKE2B": @@ -144,7 +32,7 @@ class Verification(Cog): elif hash_choice_str == "BLAKE2S": hash_choice_str += "-256" rules[rule_choice - 1] += \ - '\n' + hidden_term_line.format(hash_choice_str) + '\n' + config.hidden_term_line.format(hash_choice_str) msg = f"🗑 **Reset**: {author} cleared {limit} messages "\ f" in {channel.mention}" msg += f"\n💬 __Current challenge location__: under rule {rule_choice}" @@ -170,7 +58,7 @@ class Verification(Cog): await channel.send(item) await asyncio.sleep(1) - for x in welcome_footer: + for x in config.welcome_footer: await channel.send(cleandoc(x)) await asyncio.sleep(1) diff --git a/config_template.py b/config_template.py index b2be450..1669051 100644 --- a/config_template.py +++ b/config_template.py @@ -1,3 +1,4 @@ +import hashlib import datetime # Basic bot config, insert your token here, update description if you want @@ -156,3 +157,127 @@ suspect_ignored_words = ["excit", # == Only if you want to use cogs.pin == # Used for the pinboard. Leave empty if you don't wish for a gist pinboard. github_oauth_token = "" + +# == For cogs.verification == +# ReSwitched verification system is rather unique. +# You might want to reimplement it. +# If you do, use a different name for easier upstream merge. + +# https://docs.python.org/3.7/library/hashlib.html#shake-variable-length-digests +_welcome_blacklisted_hashes = {"shake_128", "shake_256"} + +# List of hashes that are to be used during verification +welcome_hashes = tuple(hashlib.algorithms_guaranteed - _welcome_blacklisted_hashes) + +# Header before rules in #newcomers - https://elixi.re/i/opviq90y.png +welcome_header = """ +<:ReSwitched:326421448543567872> __**Welcome to ReSwitched!**__ + +__**Be sure you read the following rules and information before participating. If you came here to ask about "backups", this is NOT the place.**__ + +__**Got questions about Nintendo Switch hacking? Before asking in the server, please see our FAQ at to see if your question has already been answered.**__ + +__**This is a server for technical discussion and development support. If you are looking for end-user support, the Nintendo Homebrew discord server may be a better fit: .**__ + +​:bookmark_tabs:__Rules:__ +""" + +# Rules in #newcomers - https://elixi.re/i/dp3enq5i.png +welcome_rules = ( + # 1 + """ + Read all the rules before participating in chat. Not reading the rules is *not* an excuse for breaking them. + • It's suggested that you read channel topics and pins before asking questions as well, as some questions may have already been answered in those. + """, + + # 2 + """ + Be nice to each other. It's fine to disagree, it's not fine to insult or attack other people. + • You may disagree with anyone or anything you like, but you should try to keep it to opinions, and not people. Avoid vitriol. + • Constant antagonistic behavior is considered uncivil and appropriate action will be taken. + • The use of derogatory slurs -- sexist, racist, homophobic, transphobic, or otherwise -- is unacceptable and may be grounds for an immediate ban. + """, + + # 3 + 'If you have concerns about another user, please take up your concerns with a staff member (someone with the "mod" role in the sidebar) in private. Don\'t publicly call other users out.', + + # 4 + """ + From time to time, we may mention everyone in the server. We do this when we feel something important is going on that requires attention. Complaining about these pings may result in a ban. + • To disable notifications for these pings, suppress them in "ReSwitched → Notification Settings". + """, + + # 5 + """ + Don't spam. + • For excessively long text, use a service like . + """, + + # 6 + "Don't brigade, raid, or otherwise attack other people or communities. Don't discuss participation in these attacks. This may warrant an immediate permanent ban.", + + # 7 + 'Off-topic content goes to #off-topic. Keep low-quality content like memes out.', + + # 8 + 'Trying to evade, look for loopholes, or stay borderline within the rules will be treated as breaking them.', + + # 9 + """ + Absolutely no piracy or related discussion. This includes: + • "Backups", even if you legally own a copy of the game. + • "Installable" NSPs, XCIs, and NCAs; this **includes** installable homebrew (i.e. on the Home Menu instead of within nx-hbmenu). + • Signature and ES patches, also known as "sigpatches" + • Usage of piracy-focused groups' (Team Xecuter, etc.) hardware and software, such as SX OS. + This is a zero-tolerance, non-negotiable policy that is enforced strictly and swiftly, up to and including instant bans without warning. + """, + + # 10 + 'The first character of your server nickname should be alphanumeric if you wish to talk in chat.', + + # 11 + """ + Do not boost the server. + • ReSwitched neither wants nor needs your server boosts, and your money is better off elsewhere. Consider the EFF (or a charity of your choice). + • Boosting the server is liable to get you kicked (to remove the nitro boost role), and/or warned. Roles you possessed prior to the kick may not be restored in a timely fashion. + """ +) + + +# Footer after rules in #newcomers - https://elixi.re/i/uhfiecib.png +welcome_footer = ( + """ + :hash: __Channel Breakdown:__ + #news - Used exclusively for updates on ReSwitched progress and community information. Most major announcements are passed through this channel and whenever something is posted there it's usually something you'll want to look at. + + #switch-hacking-meta - For "meta-discussion" related to hacking the switch. This is where we talk *about* the switch hacking that's going on, and where you can get clarification about the hacks that exist and the work that's being done. + + #user-support - End-user focused support, mainly between users. Ask your questions about using switch homebrew here. + + #tool-support - Developer focused support. Ask your questions about using PegaSwitch, libtransistor, Mephisto, and other tools here. + + #hack-n-all - General hacking, hardware and software development channel for hacking on things *other* than the switch. This is a great place to ask about hacking other systems-- and for the community to have technical discussions. + """, + + """ + #switch-hacking-general - Channel for everyone working on hacking the switch-- both in an exploit and a low-level hardware sense. This is where a lot of our in-the-open development goes on. Note that this isn't the place for developing homebrew-- we have #homebrew-development for that! + + #homebrew-development - Discussion about the development of homebrew goes there. Feel free to show off your latest creation here. + + #off-topic - Channel for discussion of anything that doesn't belong in #general. Anything goes, so long as you make sure to follow the rules and be on your best behavior. + + #toolchain-development - Discussion about the development of libtransistor itself goes there. + + #cfw-development - Development discussion regarding custom firmware (CFW) projects, such as Atmosphère. This channel is meant for the discussion accompanying active development. + + #bot-cmds - Channel for excessive/random use of Robocop's various commands. + + **If you are still not sure how to get access to the other channels, please read the rules again.** + **If you have questions about the rules, feel free to ask here!** + + **Note: This channel is completely automated (aside from responding to questions about the rules). If your message didn't give you access to the other channels, you failed the test. Feel free to try again.** + """, +) + +# Line to be hidden in rules +hidden_term_line = ' • When you have finished reading all of the rules, send a message in this channel that includes the {0} hex digest of your discord "name#discriminator", and bot will automatically grant you access to the other channels. You can find your "name#discriminator" (your username followed by a ‘#’ and four numbers) under the discord channel list.' From fd845ecb408dcdc52e4e1c23023588a1169cb9da Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Tue, 21 Apr 2020 01:05:32 +0300 Subject: [PATCH 39/43] Do a black pass and add black to contributing guidelines FORK MAINTAINERS: I'm so, so sorry, but this was planned since forever. If you need help integrating this, feel free to contact me. --- README.md | 8 + Robocop.py | 142 ++++++---- cogs/admin.py | 95 ++++--- cogs/basic.py | 29 +- cogs/common.py | 94 ++++--- cogs/err.py | 73 ++--- cogs/imagemanip.py | 14 +- cogs/invites.py | 14 +- cogs/legacy.py | 23 +- cogs/links.py | 86 +++--- cogs/lists.py | 153 ++++++---- cogs/lockdown.py | 35 ++- cogs/logs.py | 172 +++++++----- cogs/meme.py | 78 ++++-- cogs/mod.py | 286 +++++++++++-------- cogs/mod_note.py | 6 +- cogs/mod_reacts.py | 52 ++-- cogs/mod_timed.py | 105 ++++--- cogs/mod_userlog.py | 124 +++++---- cogs/pin.py | 54 ++-- cogs/remind.py | 42 +-- cogs/robocronp.py | 60 ++-- cogs/verification.py | 118 +++++--- config_template.py | 159 +++++------ helpers/checks.py | 12 +- helpers/errcodes.py | 648 +++++++++++++++++++++---------------------- helpers/userlogs.py | 42 +-- 27 files changed, 1521 insertions(+), 1203 deletions(-) diff --git a/README.md b/README.md index be75256..3d28419 100755 --- a/README.md +++ b/README.md @@ -31,6 +31,14 @@ If you're moving from Kurisu/Robocop, and want to preserve your data, you'll wan --- +## Contributing + +Contributions are welcome. If you're unsure if your PR would be merged or not, either open an issue, ask on ReSwitched off-topic pinging ave or DM ave. + +You're expected to use [black](https://github.com/psf/black) for code formatting before sending a PR. Simply install it with pip (`pip3 install black`), and run it with `black .`. + +--- + ## Credits Robocop-NG was initially developed by @aveao and @tumGER. It is currently maintained by @aveao. Similarly, the official robocop-ng on reswitched discord guild is hosted by @aveao too. diff --git a/Robocop.py b/Robocop.py index 15face3..39afd7d 100755 --- a/Robocop.py +++ b/Robocop.py @@ -9,7 +9,7 @@ import config import discord from discord.ext import commands -script_name = os.path.basename(__file__).split('.')[0] +script_name = os.path.basename(__file__).split(".")[0] log_file_name = f"{script_name}.log" @@ -17,15 +17,17 @@ log_file_name = f"{script_name}.log" max_file_size = 1000 * 1000 * 8 backup_count = 3 file_handler = logging.handlers.RotatingFileHandler( - filename=log_file_name, maxBytes=max_file_size, backupCount=backup_count) + filename=log_file_name, maxBytes=max_file_size, backupCount=backup_count +) stdout_handler = logging.StreamHandler(sys.stdout) log_format = logging.Formatter( - '[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s') + "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" +) file_handler.setFormatter(log_format) stdout_handler.setFormatter(log_format) -log = logging.getLogger('discord') +log = logging.getLogger("discord") log.setLevel(logging.INFO) log.addHandler(file_handler) log.addHandler(stdout_handler) @@ -37,13 +39,14 @@ def get_prefix(bot, message): return commands.when_mentioned_or(*prefixes)(bot, message) -wanted_jsons = ["data/restrictions.json", - "data/robocronptab.json", - "data/userlog.json", - "data/invites.json"] +wanted_jsons = [ + "data/restrictions.json", + "data/robocronptab.json", + "data/userlog.json", + "data/invites.json", +] -bot = commands.Bot(command_prefix=get_prefix, - description=config.bot_description) +bot = commands.Bot(command_prefix=get_prefix, description=config.bot_description) bot.help_command = commands.DefaultHelpCommand(dm_help=True) bot.log = log @@ -51,12 +54,12 @@ bot.config = config bot.script_name = script_name bot.wanted_jsons = wanted_jsons -if __name__ == '__main__': +if __name__ == "__main__": for cog in config.initial_cogs: try: bot.load_extension(cog) except: - log.error(f'Failed to load cog {cog}.') + log.error(f"Failed to load cog {cog}.") log.error(traceback.print_exc()) @@ -67,31 +70,37 @@ async def on_ready(): bot.app_info = await bot.application_info() bot.botlog_channel = bot.get_channel(config.botlog_channel) - log.info(f'\nLogged in as: {bot.user.name} - ' - f'{bot.user.id}\ndpy version: {discord.__version__}\n') + log.info( + f"\nLogged in as: {bot.user.name} - " + f"{bot.user.id}\ndpy version: {discord.__version__}\n" + ) game_name = f"{config.prefixes[0]}help" # Send "Robocop has started! x has y members!" guild = bot.botlog_channel.guild - msg = f"{bot.user.name} has started! "\ - f"{guild.name} has {guild.member_count} members!" + msg = ( + f"{bot.user.name} has started! " + f"{guild.name} has {guild.member_count} members!" + ) data_files = [discord.File(fpath) for fpath in wanted_jsons] await bot.botlog_channel.send(msg, files=data_files) - activity = discord.Activity(name=game_name, - type=discord.ActivityType.listening) + activity = discord.Activity(name=game_name, type=discord.ActivityType.listening) await bot.change_presence(activity=activity) @bot.event async def on_command(ctx): - log_text = f"{ctx.message.author} ({ctx.message.author.id}): "\ - f"\"{ctx.message.content}\" " + log_text = ( + f"{ctx.message.author} ({ctx.message.author.id}): " f'"{ctx.message.content}" ' + ) if ctx.guild: # was too long for tertiary if - log_text += f"on \"{ctx.channel.name}\" ({ctx.channel.id}) "\ - f"at \"{ctx.guild.name}\" ({ctx.guild.id})" + log_text += ( + f'on "{ctx.channel.name}" ({ctx.channel.id}) ' + f'at "{ctx.guild.name}" ({ctx.guild.id})' + ) else: log_text += f"on DMs ({ctx.channel.id})" log.info(log_text) @@ -106,9 +115,11 @@ async def on_error(event_method, *args, **kwargs): async def on_command_error(ctx, error): error_text = str(error) - err_msg = f"Error with \"{ctx.message.content}\" from "\ - f"\"{ctx.message.author} ({ctx.message.author.id}) "\ - f"of type {type(error)}: {error_text}" + err_msg = ( + f'Error with "{ctx.message.content}" from ' + f'"{ctx.message.author} ({ctx.message.author.id}) ' + f"of type {type(error)}: {error_text}" + ) log.error(err_msg) @@ -119,45 +130,60 @@ async def on_command_error(ctx, error): if isinstance(error, commands.NoPrivateMessage): return await ctx.send("This command doesn't work on DMs.") elif isinstance(error, commands.MissingPermissions): - roles_needed = '\n- '.join(error.missing_perms) - return await ctx.send(f"{ctx.author.mention}: You don't have the right" - " permissions to run this command. You need: " - f"```- {roles_needed}```") + roles_needed = "\n- ".join(error.missing_perms) + return await ctx.send( + f"{ctx.author.mention}: You don't have the right" + " permissions to run this command. You need: " + f"```- {roles_needed}```" + ) elif isinstance(error, commands.BotMissingPermissions): - roles_needed = '\n-'.join(error.missing_perms) - return await ctx.send(f"{ctx.author.mention}: Bot doesn't have " - "the right permissions to run this command. " - "Please add the following roles: " - f"```- {roles_needed}```") + roles_needed = "\n-".join(error.missing_perms) + return await ctx.send( + f"{ctx.author.mention}: Bot doesn't have " + "the right permissions to run this command. " + "Please add the following roles: " + f"```- {roles_needed}```" + ) elif isinstance(error, commands.CommandOnCooldown): - return await ctx.send(f"{ctx.author.mention}: You're being " - "ratelimited. Try in " - f"{error.retry_after:.1f} seconds.") + return await ctx.send( + f"{ctx.author.mention}: You're being " + "ratelimited. Try in " + f"{error.retry_after:.1f} seconds." + ) elif isinstance(error, commands.CheckFailure): - return await ctx.send(f"{ctx.author.mention}: Check failed. " - "You might not have the right permissions " - "to run this command, or you may not be able " - "to run this command in the current channel.") - elif isinstance(error, commands.CommandInvokeError) and\ - ("Cannot send messages to this user" in error_text): - return await ctx.send(f"{ctx.author.mention}: I can't DM you.\n" - "You might have me blocked or have DMs " - f"blocked globally or for {ctx.guild.name}.\n" - "Please resolve that, then " - "run the command again.") + return await ctx.send( + f"{ctx.author.mention}: Check failed. " + "You might not have the right permissions " + "to run this command, or you may not be able " + "to run this command in the current channel." + ) + elif isinstance(error, commands.CommandInvokeError) and ( + "Cannot send messages to this user" in error_text + ): + return await ctx.send( + f"{ctx.author.mention}: I can't DM you.\n" + "You might have me blocked or have DMs " + f"blocked globally or for {ctx.guild.name}.\n" + "Please resolve that, then " + "run the command again." + ) elif isinstance(error, commands.CommandNotFound): # Nothing to do when command is not found. return - help_text = f"Usage of this command is: ```{ctx.prefix}{ctx.command.name} "\ - f"{ctx.command.signature}```\nPlease see `{ctx.prefix}help "\ - f"{ctx.command.name}` for more info about this command." + help_text = ( + f"Usage of this command is: ```{ctx.prefix}{ctx.command.name} " + f"{ctx.command.signature}```\nPlease see `{ctx.prefix}help " + f"{ctx.command.name}` for more info about this command." + ) if isinstance(error, commands.BadArgument): - return await ctx.send(f"{ctx.author.mention}: You gave incorrect " - f"arguments. {help_text}") + return await ctx.send( + f"{ctx.author.mention}: You gave incorrect " f"arguments. {help_text}" + ) elif isinstance(error, commands.MissingRequiredArgument): - return await ctx.send(f"{ctx.author.mention}: You gave incomplete " - f"arguments. {help_text}") + return await ctx.send( + f"{ctx.author.mention}: You gave incomplete " f"arguments. {help_text}" + ) @bot.event @@ -171,13 +197,15 @@ async def on_message(message): # Ignore messages in newcomers channel, unless it's potentially # an allowed command welcome_allowed = ["reset", "kick", "ban", "warn"] - if message.channel.id == config.welcome_channel and\ - not any(cmd in message.content for cmd in welcome_allowed): + if message.channel.id == config.welcome_channel and not any( + cmd in message.content for cmd in welcome_allowed + ): return ctx = await bot.get_context(message) await bot.invoke(ctx) + if not os.path.exists("data"): os.makedirs("data") diff --git a/cogs/admin.py b/cogs/admin.py index 928f02d..cd7603d 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -16,7 +16,7 @@ class Admin(Cog): @commands.guild_only() @commands.check(check_if_bot_manager) - @commands.command(name='exit', aliases=["quit", "bye"]) + @commands.command(name="exit", aliases=["quit", "bye"]) async def _exit(self, ctx): """Shuts down the bot, bot manager only.""" await ctx.send(":wave: Goodbye!") @@ -27,8 +27,10 @@ class Admin(Cog): @commands.command() async def fetchlog(self, ctx): """Returns log""" - await ctx.send("Here's the current log file:", - file=discord.File(f"{self.bot.script_name}.log")) + await ctx.send( + "Here's the current log file:", + file=discord.File(f"{self.bot.script_name}.log"), + ) @commands.guild_only() @commands.check(check_if_bot_manager) @@ -40,32 +42,29 @@ class Admin(Cog): @commands.guild_only() @commands.check(check_if_bot_manager) - @commands.command(name='eval') + @commands.command(name="eval") async def _eval(self, ctx, *, code: str): """Evaluates some code, bot manager only.""" try: - code = code.strip('` ') + code = code.strip("` ") env = { - 'bot': self.bot, - 'ctx': ctx, - 'message': ctx.message, - 'server': ctx.guild, - 'guild': ctx.guild, - 'channel': ctx.message.channel, - 'author': ctx.message.author, - + "bot": self.bot, + "ctx": ctx, + "message": ctx.message, + "server": ctx.guild, + "guild": ctx.guild, + "channel": ctx.message.channel, + "author": ctx.message.author, # modules - 'discord': discord, - 'commands': commands, - + "discord": discord, + "commands": commands, # utilities - '_get': discord.utils.get, - '_find': discord.utils.find, - + "_get": discord.utils.get, + "_find": discord.utils.find, # last result - '_': self.last_eval_result, - '_p': self.previous_eval_code, + "_": self.last_eval_result, + "_p": self.previous_eval_code, } env.update(globals()) @@ -79,16 +78,15 @@ class Admin(Cog): self.previous_eval_code = code - sliced_message = await self.bot.slice_message(repr(result), - prefix="```", - suffix="```") + sliced_message = await self.bot.slice_message( + repr(result), prefix="```", suffix="```" + ) for msg in sliced_message: await ctx.send(msg) except: - sliced_message = \ - await self.bot.slice_message(traceback.format_exc(), - prefix="```", - suffix="```") + sliced_message = await self.bot.slice_message( + traceback.format_exc(), prefix="```", suffix="```" + ) for msg in sliced_message: await ctx.send(msg) @@ -102,22 +100,25 @@ class Admin(Cog): @commands.command() async def pull(self, ctx, auto=False): """Does a git pull, bot manager only.""" - tmp = await ctx.send('Pulling...') + tmp = await ctx.send("Pulling...") git_output = await self.bot.async_call_shell("git pull") await tmp.edit(content=f"Pull complete. Output: ```{git_output}```") if auto: - cogs_to_reload = re.findall(r'cogs/([a-z_]*).py[ ]*\|', git_output) + cogs_to_reload = re.findall(r"cogs/([a-z_]*).py[ ]*\|", git_output) for cog in cogs_to_reload: try: self.bot.unload_extension("cogs." + cog) self.bot.load_extension("cogs." + cog) - self.bot.log.info(f'Reloaded ext {cog}') - await ctx.send(f':white_check_mark: `{cog}` ' - 'successfully reloaded.') + self.bot.log.info(f"Reloaded ext {cog}") + await ctx.send( + f":white_check_mark: `{cog}` " "successfully reloaded." + ) await self.cog_load_actions(cog) except: - await ctx.send(f':x: Cog reloading failed, traceback: ' - f'```\n{traceback.format_exc()}\n```') + await ctx.send( + f":x: Cog reloading failed, traceback: " + f"```\n{traceback.format_exc()}\n```" + ) return @commands.guild_only() @@ -129,11 +130,13 @@ class Admin(Cog): self.bot.load_extension("cogs." + ext) await self.cog_load_actions(ext) except: - await ctx.send(f':x: Cog loading failed, traceback: ' - f'```\n{traceback.format_exc()}\n```') + await ctx.send( + f":x: Cog loading failed, traceback: " + f"```\n{traceback.format_exc()}\n```" + ) return - self.bot.log.info(f'Loaded ext {ext}') - await ctx.send(f':white_check_mark: `{ext}` successfully loaded.') + self.bot.log.info(f"Loaded ext {ext}") + await ctx.send(f":white_check_mark: `{ext}` successfully loaded.") @commands.guild_only() @commands.check(check_if_bot_manager) @@ -141,8 +144,8 @@ class Admin(Cog): async def unload(self, ctx, ext: str): """Unloads a cog, bot manager only.""" self.bot.unload_extension("cogs." + ext) - self.bot.log.info(f'Unloaded ext {ext}') - await ctx.send(f':white_check_mark: `{ext}` successfully unloaded.') + self.bot.log.info(f"Unloaded ext {ext}") + await ctx.send(f":white_check_mark: `{ext}` successfully unloaded.") @commands.check(check_if_bot_manager) @commands.command() @@ -158,11 +161,13 @@ class Admin(Cog): self.bot.load_extension("cogs." + ext) await self.cog_load_actions(ext) except: - await ctx.send(f':x: Cog reloading failed, traceback: ' - f'```\n{traceback.format_exc()}\n```') + await ctx.send( + f":x: Cog reloading failed, traceback: " + f"```\n{traceback.format_exc()}\n```" + ) return - self.bot.log.info(f'Reloaded ext {ext}') - await ctx.send(f':white_check_mark: `{ext}` successfully reloaded.') + self.bot.log.info(f"Reloaded ext {ext}") + await ctx.send(f":white_check_mark: `{ext}` successfully reloaded.") def setup(bot): diff --git a/cogs/basic.py b/cogs/basic.py index e00cae6..3ece545 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -32,50 +32,51 @@ class Basic(Cog): async def communitycount(self, ctx): """Prints the community member count of the server.""" community = ctx.guild.get_role(config.named_roles["community"]) - await ctx.send(f"{ctx.guild.name} has " - f"{len(community.members)} community members!") + await ctx.send( + f"{ctx.guild.name} has " f"{len(community.members)} community members!" + ) @commands.guild_only() @commands.command() async def hackercount(self, ctx): """Prints the hacker member count of the server.""" h4x0r = ctx.guild.get_role(config.named_roles["hacker"]) - await ctx.send(f"{ctx.guild.name} has " - f"{len(h4x0r.members)} people with hacker role!") + await ctx.send( + f"{ctx.guild.name} has " f"{len(h4x0r.members)} people with hacker role!" + ) @commands.guild_only() @commands.command() async def membercount(self, ctx): """Prints the member count of the server.""" - await ctx.send(f"{ctx.guild.name} has " - f"{ctx.guild.member_count} members!") + await ctx.send(f"{ctx.guild.name} has " f"{ctx.guild.member_count} members!") @commands.command(aliases=["robocopng", "robocop-ng"]) async def robocop(self, ctx): """Shows a quick embed with bot info.""" - embed = discord.Embed(title="Robocop-NG", - url=config.source_url, - description=config.embed_desc) + embed = discord.Embed( + title="Robocop-NG", url=config.source_url, description=config.embed_desc + ) embed.set_thumbnail(url=self.bot.user.avatar_url) await ctx.send(embed=embed) - @commands.command(aliases=['p']) + @commands.command(aliases=["p"]) async def ping(self, ctx): """Shows ping values to discord. RTT = Round-trip time, time taken to send a message to discord GW = Gateway Ping""" before = time.monotonic() - tmp = await ctx.send('Calculating ping...') + tmp = await ctx.send("Calculating ping...") after = time.monotonic() rtt_ms = (after - before) * 1000 gw_ms = self.bot.latency * 1000 - message_text = f":ping_pong:\n"\ - f"rtt: `{rtt_ms:.1f}ms`\n"\ - f"gw: `{gw_ms:.1f}ms`" + message_text = ( + f":ping_pong:\n" f"rtt: `{rtt_ms:.1f}ms`\n" f"gw: `{gw_ms:.1f}ms`" + ) self.bot.log.info(message_text) await tmp.edit(content=message_text) diff --git a/cogs/common.py b/cogs/common.py index 8af1a0c..364c3c4 100644 --- a/cogs/common.py +++ b/cogs/common.py @@ -31,9 +31,14 @@ class Common(Cog): res_timestamp = math.floor(time.mktime(time_struct)) return res_timestamp - def get_relative_timestamp(self, time_from=None, time_to=None, - humanized=False, include_from=False, - include_to=False): + def get_relative_timestamp( + self, + time_from=None, + time_to=None, + humanized=False, + include_from=False, + include_to=False, + ): # Setting default value to utcnow() makes it show time from cog load # which is not what we want if not time_from: @@ -43,17 +48,19 @@ class Common(Cog): if humanized: humanized_string = humanize.naturaltime(time_from - time_to) if include_from and include_to: - str_with_from_and_to = f"{humanized_string} "\ - f"({str(time_from).split('.')[0]} "\ - f"- {str(time_to).split('.')[0]})" + str_with_from_and_to = ( + f"{humanized_string} " + f"({str(time_from).split('.')[0]} " + f"- {str(time_to).split('.')[0]})" + ) return str_with_from_and_to elif include_from: - str_with_from = f"{humanized_string} "\ - f"({str(time_from).split('.')[0]})" + str_with_from = ( + f"{humanized_string} " f"({str(time_from).split('.')[0]})" + ) return str_with_from elif include_to: - str_with_to = f"{humanized_string} "\ - f"({str(time_to).split('.')[0]})" + str_with_to = f"{humanized_string} " f"({str(time_to).split('.')[0]})" return str_with_to return humanized_string else: @@ -61,8 +68,7 @@ class Common(Cog): epoch_from = (time_from - epoch).total_seconds() epoch_to = (time_to - epoch).total_seconds() second_diff = epoch_to - epoch_from - result_string = str(datetime.timedelta( - seconds=second_diff)).split('.')[0] + result_string = str(datetime.timedelta(seconds=second_diff)).split(".")[0] return result_string async def aioget(self, url): @@ -73,11 +79,12 @@ class Common(Cog): self.bot.log.info(f"Data from {url}: {text_data}") return text_data else: - self.bot.log.error(f"HTTP Error {data.status} " - "while getting {url}") + self.bot.log.error(f"HTTP Error {data.status} " "while getting {url}") except: - self.bot.log.error(f"Error while getting {url} " - f"on aiogetbytes: {traceback.format_exc()}") + self.bot.log.error( + f"Error while getting {url} " + f"on aiogetbytes: {traceback.format_exc()}" + ) async def aiogetbytes(self, url): try: @@ -87,11 +94,12 @@ class Common(Cog): self.bot.log.debug(f"Data from {url}: {byte_data}") return byte_data else: - self.bot.log.error(f"HTTP Error {data.status} " - "while getting {url}") + self.bot.log.error(f"HTTP Error {data.status} " "while getting {url}") except: - self.bot.log.error(f"Error while getting {url} " - f"on aiogetbytes: {traceback.format_exc()}") + self.bot.log.error( + f"Error while getting {url} " + f"on aiogetbytes: {traceback.format_exc()}" + ) async def aiojson(self, url): try: @@ -99,18 +107,19 @@ class Common(Cog): if data.status == 200: text_data = await data.text() self.bot.log.info(f"Data from {url}: {text_data}") - content_type = data.headers['Content-Type'] + content_type = data.headers["Content-Type"] return await data.json(content_type=content_type) else: - self.bot.log.error(f"HTTP Error {data.status} " - "while getting {url}") + self.bot.log.error(f"HTTP Error {data.status} " "while getting {url}") except: - self.bot.log.error(f"Error while getting {url} " - f"on aiogetbytes: {traceback.format_exc()}") + self.bot.log.error( + f"Error while getting {url} " + f"on aiogetbytes: {traceback.format_exc()}" + ) def hex_to_int(self, color_hex: str): """Turns a given hex color into an integer""" - return int("0x" + color_hex.strip('#'), 16) + return int("0x" + color_hex.strip("#"), 16) def escape_message(self, text: str): """Escapes unfun stuff from messages""" @@ -130,10 +139,12 @@ class Common(Cog): """Slices a message into multiple messages""" if len(text) > size * self.max_split_length: haste_url = await self.haste(text) - return [f"Message is too long ({len(text)} > " - f"{size * self.max_split_length} " - f"({size} * {self.max_split_length}))" - f", go to haste: <{haste_url}>"] + return [ + f"Message is too long ({len(text)} > " + f"{size * self.max_split_length} " + f"({size} * {self.max_split_length}))" + f", go to haste: <{haste_url}>" + ] reply_list = [] size_wo_fix = size - len(prefix) - len(suffix) while len(text) > size_wo_fix: @@ -142,28 +153,28 @@ class Common(Cog): reply_list.append(f"{prefix}{text}{suffix}") return reply_list - async def haste(self, text, instance='https://mystb.in/'): - response = await self.bot.aiosession.post(f"{instance}documents", - data=text) + async def haste(self, text, instance="https://mystb.in/"): + response = await self.bot.aiosession.post(f"{instance}documents", data=text) if response.status == 200: result_json = await response.json() return f"{instance}{result_json['key']}" else: return f"Error {response.status}: {response.text}" - async def async_call_shell(self, shell_command: str, - inc_stdout=True, inc_stderr=True): + async def async_call_shell( + self, shell_command: str, inc_stdout=True, inc_stderr=True + ): pipe = asyncio.subprocess.PIPE - proc = await asyncio.create_subprocess_shell(str(shell_command), - stdout=pipe, - stderr=pipe) + proc = await asyncio.create_subprocess_shell( + str(shell_command), stdout=pipe, stderr=pipe + ) if not (inc_stdout or inc_stderr): return "??? you set both stdout and stderr to False????" proc_result = await proc.communicate() - stdout_str = proc_result[0].decode('utf-8').strip() - stderr_str = proc_result[1].decode('utf-8').strip() + stdout_str = proc_result[0].decode("utf-8").strip() + stderr_str = proc_result[1].decode("utf-8").strip() if inc_stdout and not inc_stderr: return stdout_str @@ -171,8 +182,7 @@ class Common(Cog): return stderr_str if stdout_str and stderr_str: - return f"stdout:\n\n{stdout_str}\n\n"\ - f"======\n\nstderr:\n\n{stderr_str}" + return f"stdout:\n\n{stdout_str}\n\n" f"======\n\nstderr:\n\n{stderr_str}" elif stdout_str: return f"stdout:\n\n{stdout_str}" elif stderr_str: diff --git a/cogs/err.py b/cogs/err.py index 37e0e82..44c7f98 100644 --- a/cogs/err.py +++ b/cogs/err.py @@ -5,17 +5,20 @@ from discord.ext import commands from discord.ext.commands import Cog from helpers.errcodes import * + class Err(Cog): """Everything related to Nintendo 3DS, Wii U and Switch error codes""" def __init__(self, bot): self.bot = bot - self.dds_re = re.compile(r'0\d{2}\-\d{4}') - self.wiiu_re = re.compile(r'1\d{2}\-\d{4}') - self.switch_re = re.compile(r'2\d{3}\-\d{4}') - self.no_err_desc = "It seems like your error code is unknown. "\ - "You can check on Switchbrew for your error code at "\ - "" + self.dds_re = re.compile(r"0\d{2}\-\d{4}") + self.wiiu_re = re.compile(r"1\d{2}\-\d{4}") + self.switch_re = re.compile(r"2\d{3}\-\d{4}") + self.no_err_desc = ( + "It seems like your error code is unknown. " + "You can check on Switchbrew for your error code at " + "" + ) self.rickroll = "https://www.youtube.com/watch?v=z3ZiVn5L9vM" @commands.command(aliases=["3dserr", "3err", "dserr"]) @@ -28,9 +31,9 @@ class Err(Cog): else: err_description = self.no_err_desc # Make a nice Embed out of it - embed = discord.Embed(title=err, - url=self.rickroll, - description=err_description) + embed = discord.Embed( + title=err, url=self.rickroll, description=err_description + ) embed.set_footer(text="Console: 3DS") # Send message, crazy @@ -47,8 +50,7 @@ class Err(Cog): level = (rc >> 27) & 0x1F embed = discord.Embed(title=f"0x{rc:X}") embed.add_field(name="Module", value=dds_modules.get(mod, mod)) - embed.add_field(name="Description", - value=dds_descriptions.get(desc, desc)) + embed.add_field(name="Description", value=dds_descriptions.get(desc, desc)) embed.add_field(name="Summary", value=dds_summaries.get(summ, summ)) embed.add_field(name="Level", value=dds_levels.get(level, level)) embed.set_footer(text="Console: 3DS") @@ -56,8 +58,10 @@ class Err(Cog): await ctx.send(embed=embed) return else: - await ctx.send("Unknown Format - This is either " - "no error code or you made some mistake!") + await ctx.send( + "Unknown Format - This is either " + "no error code or you made some mistake!" + ) @commands.command(aliases=["wiiuserr", "uerr", "wuerr", "mochaerr"]) async def wiiuerr(self, ctx, err: str): @@ -72,9 +76,9 @@ class Err(Cog): err_description = self.no_err_desc # Make a nice Embed out of it - embed = discord.Embed(title=err, - url=self.rickroll, - description=err_description) + embed = discord.Embed( + title=err, url=self.rickroll, description=err_description + ) embed.set_footer(text="Console: Wii U") embed.add_field(name="Module", value=module, inline=True) embed.add_field(name="Description", value=desc, inline=True) @@ -82,8 +86,10 @@ class Err(Cog): # Send message, crazy await ctx.send(embed=embed) else: - await ctx.send("Unknown Format - This is either " - "no error code or you made some mistake!") + await ctx.send( + "Unknown Format - This is either " + "no error code or you made some mistake!" + ) @commands.command(aliases=["nxerr", "serr"]) async def err(self, ctx, err: str): @@ -125,19 +131,21 @@ class Err(Cog): err_description = errcode_range[2] # Make a nice Embed out of it - embed = discord.Embed(title=f"{str_errcode} / {hex(errcode)}", - url=self.rickroll, - description=err_description) - embed.add_field(name="Module", - value=f"{err_module} ({module})", - inline=True) + embed = discord.Embed( + title=f"{str_errcode} / {hex(errcode)}", + url=self.rickroll, + description=err_description, + ) + embed.add_field( + name="Module", value=f"{err_module} ({module})", inline=True + ) embed.add_field(name="Description", value=desc, inline=True) if "ban" in err_description: embed.set_footer("F to you | Console: Switch") else: embed.set_footer(text="Console: Switch") - + await ctx.send(embed=embed) # Special case handling because Nintendo feels like @@ -145,17 +153,17 @@ class Err(Cog): elif err in switch_game_err: game, desc = switch_game_err[err].split(":") - embed = discord.Embed(title=err, - url=self.rickroll, - description=desc) + embed = discord.Embed(title=err, url=self.rickroll, description=desc) embed.set_footer(text="Console: Switch") embed.add_field(name="Game", value=game, inline=True) await ctx.send(embed=embed) else: - await ctx.send("Unknown Format - This is either " - "no error code or you made some mistake!") + await ctx.send( + "Unknown Format - This is either " + "no error code or you made some mistake!" + ) @commands.command(aliases=["e2h"]) async def err2hex(self, ctx, err: str): @@ -167,8 +175,9 @@ class Err(Cog): errcode = (desc << 9) + module await ctx.send(hex(errcode)) else: - await ctx.send("This doesn't follow the typical" - " Nintendo Switch 2XXX-XXXX format!") + await ctx.send( + "This doesn't follow the typical" " Nintendo Switch 2XXX-XXXX format!" + ) @commands.command(aliases=["h2e"]) async def hex2err(self, ctx, err: str): diff --git a/cogs/imagemanip.py b/cogs/imagemanip.py index 4fe59b1..f63545f 100644 --- a/cogs/imagemanip.py +++ b/cogs/imagemanip.py @@ -21,7 +21,9 @@ class ImageManip(Cog): """Gives a cox headline""" mention = ctx.author.mention - headline = await commands.clean_content(fix_channel_mentions=True).convert(ctx, headline) + headline = await commands.clean_content(fix_channel_mentions=True).convert( + ctx, headline + ) in_vice = "assets/motherboardlogo.png" in_byjcox = "assets/byjcox.png" @@ -53,7 +55,7 @@ class ImageManip(Cog): for line in lines: # Get size of the text by font, create a new image of that size size = f.getsize(line) - txt = PIL.Image.new('L', size) + txt = PIL.Image.new("L", size) # Draw the text d = PIL.ImageDraw.Draw(txt) @@ -61,8 +63,9 @@ class ImageManip(Cog): # Paste the text into the base image w = txt.rotate(0, expand=1) - im.paste(PIL.ImageOps.colorize(w, (0, 0, 0), - (0, 0, 0)), (horipos, vertpos), w) + im.paste( + PIL.ImageOps.colorize(w, (0, 0, 0), (0, 0, 0)), (horipos, vertpos), w + ) # Calculate position on next line vertpos += size[1] + line_spacing @@ -77,8 +80,7 @@ class ImageManip(Cog): # Save image out_filename = f"/tmp/{ctx.message.id}-out.png" im.save(out_filename, quality=100, optimize=True) - await ctx.send(content=f"{mention}: Enjoy.", - file=discord.File(out_filename)) + await ctx.send(content=f"{mention}: Enjoy.", file=discord.File(out_filename)) def setup(bot): diff --git a/cogs/invites.py b/cogs/invites.py index 5a11404..8c60a31 100644 --- a/cogs/invites.py +++ b/cogs/invites.py @@ -4,6 +4,7 @@ from helpers.checks import check_if_collaborator import config import json + class Invites(Cog): def __init__(self, bot): self.bot = bot @@ -15,8 +16,9 @@ class Invites(Cog): welcome_channel = self.bot.get_channel(config.welcome_channel) author = ctx.message.author reason = f"Created by {str(author)} ({author.id})" - invite = await welcome_channel.create_invite(max_age = 0, - max_uses = 1, temporary = True, unique = True, reason = reason) + invite = await welcome_channel.create_invite( + max_age=0, max_uses=1, temporary=True, unique=True, reason=reason + ) with open("data/invites.json", "r") as f: invites = json.load(f) @@ -25,7 +27,7 @@ class Invites(Cog): "uses": 0, "url": invite.url, "max_uses": 1, - "code": invite.code + "code": invite.code, } with open("data/invites.json", "w") as f: @@ -35,8 +37,10 @@ class Invites(Cog): try: await ctx.author.send(f"Created single-use invite {invite.url}") except discord.errors.Forbidden: - await ctx.send(f"{ctx.author.mention} I could not send you the \ - invite. Send me a DM so I can reply to you.") + await ctx.send( + f"{ctx.author.mention} I could not send you the \ + invite. Send me a DM so I can reply to you." + ) def setup(bot): diff --git a/cogs/legacy.py b/cogs/legacy.py index d33a2c9..7a0b566 100644 --- a/cogs/legacy.py +++ b/cogs/legacy.py @@ -1,6 +1,7 @@ from discord.ext import commands from discord.ext.commands import Cog + class Legacy(Cog): def __init__(self, bot): self.bot = bot @@ -8,22 +9,28 @@ class Legacy(Cog): @commands.command(hidden=True, aliases=["removehacker"]) async def probate(self, ctx): """Use .revoke """ - await ctx.send("This command was replaced with `.revoke `" - " on Robocop-NG, please use that instead.") + await ctx.send( + "This command was replaced with `.revoke `" + " on Robocop-NG, please use that instead." + ) @commands.command(hidden=True) async def softlock(self, ctx): """Use .lock True""" - await ctx.send("This command was replaced with `.lock True`" - " on Robocop-NG, please use that instead.\n" - "Also... good luck, and sorry for taking your time. " - "Lockdown rarely means anything good.") + await ctx.send( + "This command was replaced with `.lock True`" + " on Robocop-NG, please use that instead.\n" + "Also... good luck, and sorry for taking your time. " + "Lockdown rarely means anything good." + ) @commands.command(hidden=True, aliases=["addhacker"]) async def unprobate(self, ctx): """Use .approve """ - await ctx.send("This command was replaced with `.approve `" - " on Robocop-NG, please use that instead.") + await ctx.send( + "This command was replaced with `.approve `" + " on Robocop-NG, please use that instead." + ) def setup(bot): diff --git a/cogs/links.py b/cogs/links.py index d3ebe25..5c25284 100644 --- a/cogs/links.py +++ b/cogs/links.py @@ -25,64 +25,74 @@ class Links(Cog): @commands.command(hidden=True, aliases=["xyproblem"]) async def xy(self, ctx): """Link to the "What is the XY problem?" post from SE""" - await ctx.send("\n\n" - "TL;DR: It's asking about your attempted solution " - "rather than your actual problem.\n" - "It's perfectly okay to want to learn about a " - "solution, but please be clear about your intentions " - "if you're not actually trying to solve a problem.") + await ctx.send( + "\n\n" + "TL;DR: It's asking about your attempted solution " + "rather than your actual problem.\n" + "It's perfectly okay to want to learn about a " + "solution, but please be clear about your intentions " + "if you're not actually trying to solve a problem." + ) @commands.command(hidden=True, aliases=["guides", "link"]) async def guide(self, ctx): """Link to the guide(s)""" - await ctx.send("**Generic starter guides:**\n" - "Nintendo Homebrew's Guide: " - "\n" - # "AtlasNX's Guide: " - # "\n" - # "Pegaswitch Guide: " - # "(outdated for anything but Pegaswitch/3.0.0)\n" - "\n**Specific guides:**\n" - "Manually Updating/Downgrading (with HOS): " - "\n" - "Manually Repairing/Downgrading (without HOS): " - "\n" - "How to set up a Homebrew development environment: " - "\n" - "Getting full RAM in homebrew without NSPs: " - "as of Atmosphere 0.8.6, hold R while opening any game.\n" - "Check if a switch is vulnerable to RCM through serial: " - "") + await ctx.send( + "**Generic starter guides:**\n" + "Nintendo Homebrew's Guide: " + "\n" + # "AtlasNX's Guide: " + # "\n" + # "Pegaswitch Guide: " + # "(outdated for anything but Pegaswitch/3.0.0)\n" + "\n**Specific guides:**\n" + "Manually Updating/Downgrading (with HOS): " + "\n" + "Manually Repairing/Downgrading (without HOS): " + "\n" + "How to set up a Homebrew development environment: " + "\n" + "Getting full RAM in homebrew without NSPs: " + "as of Atmosphere 0.8.6, hold R while opening any game.\n" + "Check if a switch is vulnerable to RCM through serial: " + "" + ) @commands.command() async def source(self, ctx): """Gives link to source code.""" - await ctx.send(f"You can find my source at {config.source_url}. " - "Serious PRs and issues welcome!") + await ctx.send( + f"You can find my source at {config.source_url}. " + "Serious PRs and issues welcome!" + ) @commands.command() async def rules(self, ctx, *, targetuser: discord.Member = None): """Post a link to the Rules""" if not targetuser: targetuser = ctx.author - await ctx.send(f"{targetuser.mention}: A link to the rules " - f"can be found here: {config.rules_url}") + await ctx.send( + f"{targetuser.mention}: A link to the rules " + f"can be found here: {config.rules_url}" + ) @commands.command() async def community(self, ctx, *, targetuser: discord.Member = None): """Post a link to the community section of the rules""" if not targetuser: targetuser = ctx.author - await ctx.send(f"{targetuser.mention}: " - "https://reswitched.team/discord/#member-roles-breakdown" - "\n\n" - "Community role allows access to the set of channels " - "on the community category (#off-topic, " - "#homebrew-development, #switch-hacking-general etc)." - "\n\n" - "What you need to get the role is to be around, " - "be helpful and nice to people and " - "show an understanding of rules.") + await ctx.send( + f"{targetuser.mention}: " + "https://reswitched.team/discord/#member-roles-breakdown" + "\n\n" + "Community role allows access to the set of channels " + "on the community category (#off-topic, " + "#homebrew-development, #switch-hacking-general etc)." + "\n\n" + "What you need to get the role is to be around, " + "be helpful and nice to people and " + "show an understanding of rules." + ) def setup(bot): diff --git a/cogs/lists.py b/cogs/lists.py index 4305f54..ec6ab97 100644 --- a/cogs/lists.py +++ b/cogs/lists.py @@ -20,38 +20,38 @@ class Lists(Cog): return any(r.id in config.staff_role_ids for r in target.roles) def is_edit(self, emoji): - return str(emoji)[0] == u"✏" or str(emoji)[0] == u"📝" + return str(emoji)[0] == "✏" or str(emoji)[0] == "📝" def is_delete(self, emoji): - return str(emoji)[0] == u"❌" or str(emoji)[0] == u"❎" + return str(emoji)[0] == "❌" or str(emoji)[0] == "❎" def is_recycle(self, emoji): - return str(emoji)[0] == u"♻" + return str(emoji)[0] == "♻" def is_insert_above(self, emoji): - return str(emoji)[0] == u"⤴️" or str(emoji)[0] == u"⬆" + return str(emoji)[0] == "⤴️" or str(emoji)[0] == "⬆" def is_insert_below(self, emoji): - return str(emoji)[0] == u"⤵️" or str(emoji)[0] == u"⬇" + return str(emoji)[0] == "⤵️" or str(emoji)[0] == "⬇" def is_reaction_valid(self, reaction): allowed_reactions = [ - u"✏", - u"📝", - u"❌", - u"❎", - u"♻", - u"⤴️", - u"⬆", - u"⬇", - u"⤵️", + "✏", + "📝", + "❌", + "❎", + "♻", + "⤴️", + "⬆", + "⬇", + "⤵️", ] return str(reaction.emoji)[0] in allowed_reactions - async def find_reactions(self, user_id, channel_id, limit = None): + async def find_reactions(self, user_id, channel_id, limit=None): reactions = [] channel = self.bot.get_channel(channel_id) - async for message in channel.history(limit = limit): + async for message in channel.history(limit=limit): if len(message.reactions) == 0: continue @@ -63,9 +63,11 @@ class Lists(Cog): return reactions - def create_log_message(self, emoji, action, user, channel, reason = ""): - msg = f"{emoji} **{action}** \n"\ + def create_log_message(self, emoji, action, user, channel, reason=""): + msg = ( + f"{emoji} **{action}** \n" f"from {self.bot.escape_message(user.name)} ({user.id}), in {channel.mention}" + ) if reason != "": msg += f":\n`{reason}`" @@ -84,11 +86,11 @@ class Lists(Cog): file_message = await files_channel.fetch_message(int(field.value)) await file_message.delete() - await message.edit(embed = None) + await message.edit(embed=None) # Commands - @commands.command(aliases = ["list"]) + @commands.command(aliases=["list"]) async def listitem(self, ctx, channel: discord.TextChannel, number: int): """Link to a specific list item.""" if number <= 0: @@ -100,20 +102,17 @@ class Lists(Cog): return counter = 0 - async for message in channel.history(limit = None, oldest_first = True): + async for message in channel.history(limit=None, oldest_first=True): if message.content.strip(): counter += 1 if counter == number: embed = discord.Embed( - title = f"Item #{number} in #{channel.name}", - description = message.content, - url = message.jump_url - ) - await ctx.send( - content = "", - embed = embed + title=f"Item #{number} in #{channel.name}", + description=message.content, + url=message.jump_url, ) + await ctx.send(content="", embed=embed) return await ctx.send(f"Unable to find item #{number} in {channel.mention}.") @@ -133,8 +132,13 @@ class Lists(Cog): member = channel.guild.get_member(payload.user_id) user = self.bot.get_user(payload.user_id) reaction = next( - (reaction for reaction in message.reactions - if str(reaction.emoji) == str(payload.emoji)), None) + ( + reaction + for reaction in message.reactions + if str(reaction.emoji) == str(payload.emoji) + ), + None, + ) if reaction is None: return @@ -155,8 +159,9 @@ class Lists(Cog): # Remove all other reactions from user in this channel. for r in await self.find_reactions(payload.user_id, payload.channel_id): - if r.message.id != message.id or (r.message.id == message.id and - str(r.emoji) != str(reaction.emoji)): + if r.message.id != message.id or ( + r.message.id == message.id and str(r.emoji) != str(reaction.emoji) + ): await r.remove(user) # When editing we want to provide the user a copy of the raw text. @@ -164,17 +169,16 @@ class Lists(Cog): files_channel = self.bot.get_channel(config.list_files_channel) file = discord.File( io.BytesIO(message.content.encode("utf-8")), - filename = f"{message.id}.txt") - file_message = await files_channel.send(file = file) + filename=f"{message.id}.txt", + ) + file_message = await files_channel.send(file=file) embed = discord.Embed( - title = "Click here to get the raw text to modify.", - url = f"{file_message.attachments[0].url}?") - embed.add_field( - name = "Message ID", - value = file_message.id, - inline = False) - await message.edit(embed = embed) + title="Click here to get the raw text to modify.", + url=f"{file_message.attachments[0].url}?", + ) + embed.add_field(name="Message ID", value=file_message.id, inline=False) + await message.edit(embed=embed) @Cog.listener() async def on_raw_reaction_remove(self, payload): @@ -221,9 +225,16 @@ class Lists(Cog): attachment_data = None if len(message.attachments) != 0: # Lists will only reupload the first image. - attachment = next((a for a in message.attachments if - a.filename.endswith(".png") or a.filename.endswith(".jpg") or - a.filename.endswith(".jpeg")), None) + attachment = next( + ( + a + for a in message.attachments + if a.filename.endswith(".png") + or a.filename.endswith(".jpg") + or a.filename.endswith(".jpeg") + ), + None, + ) if attachment is not None: attachment_filename = attachment.filename attachment_data = await attachment.read() @@ -237,16 +248,18 @@ class Lists(Cog): if len(reactions) != 1: if attachment_filename is not None and attachment_data is not None: file = discord.File( - io.BytesIO(attachment_data), - filename = attachment_filename) - await channel.send(content = content, file = file) + io.BytesIO(attachment_data), filename=attachment_filename + ) + await channel.send(content=content, file=file) else: await channel.send(content) for reaction in reactions: await reaction.remove(user) - await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + await log_channel.send( + self.create_log_message("💬", "List item added:", user, channel) + ) return targeted_reaction = reactions[0] @@ -255,47 +268,67 @@ class Lists(Cog): if self.is_edit(targeted_reaction): if config.list_files_channel != 0: await self.clean_up_raw_text_file_message(targeted_message) - await targeted_message.edit(content = content) + await targeted_message.edit(content=content) await targeted_reaction.remove(user) - await log_channel.send(self.create_log_message("📝", "List item edited:", user, channel)) + await log_channel.send( + self.create_log_message("📝", "List item edited:", user, channel) + ) elif self.is_delete(targeted_reaction): await targeted_message.delete() - await log_channel.send(self.create_log_message("❌", "List item deleted:", user, channel, content)) + await log_channel.send( + self.create_log_message( + "❌", "List item deleted:", user, channel, content + ) + ) elif self.is_recycle(targeted_reaction): - messages = await channel.history(limit = None, after = targeted_message, oldest_first = True).flatten() - await channel.purge(limit = len(messages) + 1, bulk = True) + messages = await channel.history( + limit=None, after=targeted_message, oldest_first=True + ).flatten() + await channel.purge(limit=len(messages) + 1, bulk=True) await channel.send(targeted_message.content) for message in messages: await channel.send(message.content) - await log_channel.send(self.create_log_message("♻", "List item recycled:", user, channel, content)) + await log_channel.send( + self.create_log_message( + "♻", "List item recycled:", user, channel, content + ) + ) elif self.is_insert_above(targeted_reaction): - messages = await channel.history(limit = None, after = targeted_message, oldest_first = True).flatten() - await channel.purge(limit = len(messages) + 1, bulk = True) + messages = await channel.history( + limit=None, after=targeted_message, oldest_first=True + ).flatten() + await channel.purge(limit=len(messages) + 1, bulk=True) await channel.send(content) await channel.send(targeted_message.content) for message in messages: await channel.send(message.content) - await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + await log_channel.send( + self.create_log_message("💬", "List item added:", user, channel) + ) elif self.is_insert_below(targeted_reaction): - messages = await channel.history(limit = None, after = targeted_message, oldest_first = True).flatten() - await channel.purge(limit = len(messages) + 1, bulk = True) + messages = await channel.history( + limit=None, after=targeted_message, oldest_first=True + ).flatten() + await channel.purge(limit=len(messages) + 1, bulk=True) await channel.send(targeted_message.content) await channel.send(content) for message in messages: await channel.send(message.content) - await log_channel.send(self.create_log_message("💬", "List item added:", user, channel)) + await log_channel.send( + self.create_log_message("💬", "List item added:", user, channel) + ) def setup(bot): diff --git a/cogs/lockdown.py b/cogs/lockdown.py index b00c20c..98a054d 100644 --- a/cogs/lockdown.py +++ b/cogs/lockdown.py @@ -4,19 +4,21 @@ import config import discord from helpers.checks import check_if_staff + class Lockdown(Cog): def __init__(self, bot): self.bot = bot - async def set_sendmessage(self, channel: discord.TextChannel, - role, allow_send, issuer): + async def set_sendmessage( + self, channel: discord.TextChannel, role, allow_send, issuer + ): try: roleobj = channel.guild.get_role(role) overrides = channel.overwrites_for(roleobj) overrides.send_messages = allow_send - await channel.set_permissions(roleobj, - overwrite=overrides, - reason=str(issuer)) + await channel.set_permissions( + roleobj, overwrite=overrides, reason=str(issuer) + ) except: pass @@ -27,8 +29,7 @@ class Lockdown(Cog): @commands.guild_only() @commands.check(check_if_staff) @commands.command() - async def lock(self, ctx, channel: discord.TextChannel = None, - soft: bool = False): + async def lock(self, ctx, channel: discord.TextChannel = None, soft: bool = False): """Prevents people from speaking in a channel, staff only. Defaults to current channel.""" @@ -50,14 +51,18 @@ class Lockdown(Cog): public_msg = "🔒 Channel locked down. " if not soft: - public_msg += "Only staff members may speak. "\ - "Do not bring the topic to other channels or risk "\ - "disciplinary actions." + public_msg += ( + "Only staff members may speak. " + "Do not bring the topic to other channels or risk " + "disciplinary actions." + ) await ctx.send(public_msg) safe_name = await commands.clean_content().convert(ctx, str(ctx.author)) - msg = f"🔒 **Lockdown**: {ctx.channel.mention} by {ctx.author.mention} "\ - f"| {safe_name}" + msg = ( + f"🔒 **Lockdown**: {ctx.channel.mention} by {ctx.author.mention} " + f"| {safe_name}" + ) await log_channel.send(msg) @commands.guild_only() @@ -83,8 +88,10 @@ class Lockdown(Cog): safe_name = await commands.clean_content().convert(ctx, str(ctx.author)) await ctx.send("🔓 Channel unlocked.") - msg = f"🔓 **Unlock**: {ctx.channel.mention} by {ctx.author.mention} "\ - f"| {safe_name}" + msg = ( + f"🔓 **Unlock**: {ctx.channel.mention} by {ctx.author.mention} " + f"| {safe_name}" + ) await log_channel.send(msg) diff --git a/cogs/logs.py b/cogs/logs.py index af07c23..012d358 100644 --- a/cogs/logs.py +++ b/cogs/logs.py @@ -14,21 +14,22 @@ class Logs(Cog): def __init__(self, bot): self.bot = bot - self.invite_re = re.compile(r"((discord\.gg|discordapp\.com/" - r"+invite)/+[a-zA-Z0-9-]+)", - re.IGNORECASE) + self.invite_re = re.compile( + r"((discord\.gg|discordapp\.com/" r"+invite)/+[a-zA-Z0-9-]+)", re.IGNORECASE + ) self.name_re = re.compile(r"[a-zA-Z0-9].*") - self.clean_re = re.compile(r'[^a-zA-Z0-9_ ]+', re.UNICODE) + self.clean_re = re.compile(r"[^a-zA-Z0-9_ ]+", re.UNICODE) # All lower case, no spaces, nothing non-alphanumeric - susp_hellgex = "|".join([r"\W*".join(list(word)) for - word in config.suspect_words]) + susp_hellgex = "|".join( + [r"\W*".join(list(word)) for word in config.suspect_words] + ) self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE) @Cog.listener() async def on_member_join(self, member): await self.bot.wait_until_ready() - if (member.guild.id not in config.guild_whitelist): + if member.guild.id not in config.guild_whitelist: return log_channel = self.bot.get_channel(config.log_channel) @@ -48,7 +49,7 @@ class Logs(Cog): "uses": 0, "url": invite.url, "max_uses": invite.max_uses, - "code": invite.code + "code": invite.code, } probable_invites_used = [] @@ -87,31 +88,39 @@ class Logs(Cog): age = member.joined_at - member.created_at if age < config.min_age: try: - await member.send(f"Your account is too new to " - f"join {member.guild.name}." - " Please try again later.") + await member.send( + f"Your account is too new to " + f"join {member.guild.name}." + " Please try again later." + ) sent = True except discord.errors.Forbidden: sent = False await member.kick(reason="Too new") - msg = f"🚨 **Account too new**: {member.mention} | "\ - f"{escaped_name}\n"\ - f"🗓 __Creation__: {member.created_at}\n"\ - f"🕓 Account age: {age}\n"\ - f"✉ Joined with: {invite_used}\n"\ - f"🏷 __User ID__: {member.id}" + msg = ( + f"🚨 **Account too new**: {member.mention} | " + f"{escaped_name}\n" + f"🗓 __Creation__: {member.created_at}\n" + f"🕓 Account age: {age}\n" + f"✉ Joined with: {invite_used}\n" + f"🏷 __User ID__: {member.id}" + ) if not sent: - msg += "\nThe user has disabled direct messages, "\ - "so the reason was not sent." + msg += ( + "\nThe user has disabled direct messages, " + "so the reason was not sent." + ) await log_channel.send(msg) return - msg = f"✅ **Join**: {member.mention} | "\ - f"{escaped_name}\n"\ - f"🗓 __Creation__: {member.created_at}\n"\ - f"🕓 Account age: {age}\n"\ - f"✉ Joined with: {invite_used}\n"\ - f"🏷 __User ID__: {member.id}" + msg = ( + f"✅ **Join**: {member.mention} | " + f"{escaped_name}\n" + f"🗓 __Creation__: {member.created_at}\n" + f"🕓 Account age: {age}\n" + f"✉ Joined with: {invite_used}\n" + f"🏷 __User ID__: {member.id}" + ) # Handles user restrictions # Basically, gives back muted role to users that leave with it. @@ -126,13 +135,16 @@ class Logs(Cog): if len(warns[str(member.id)]["warns"]) == 0: await log_channel.send(msg) else: - embed = discord.Embed(color=discord.Color.dark_red(), - title=f"Warns for {escaped_name}") + embed = discord.Embed( + color=discord.Color.dark_red(), title=f"Warns for {escaped_name}" + ) embed.set_thumbnail(url=member.avatar_url) for idx, warn in enumerate(warns[str(member.id)]["warns"]): - embed.add_field(name=f"{idx + 1}: {warn['timestamp']}", - value=f"Issuer: {warn['issuer_name']}" - f"\nReason: {warn['reason']}") + embed.add_field( + name=f"{idx + 1}: {warn['timestamp']}", + value=f"Issuer: {warn['issuer_name']}" + f"\nReason: {warn['reason']}", + ) await log_channel.send(msg, embed=embed) except KeyError: # if the user is not in the file await log_channel.send(msg) @@ -145,9 +157,11 @@ class Logs(Cog): return alert = False - cleancont = self.clean_re.sub('', message.content).lower() - msg = f"🚨 Suspicious message by {message.author.mention} "\ - f"({message.author.id}):" + cleancont = self.clean_re.sub("", message.content).lower() + msg = ( + f"🚨 Suspicious message by {message.author.mention} " + f"({message.author.id}):" + ) invites = self.invite_re.findall(message.content) for invite in invites: @@ -155,8 +169,9 @@ class Logs(Cog): alert = True for susp_word in config.suspect_words: - if susp_word in cleancont and\ - not any(ok_word in cleancont for ok_word in config.suspect_ignored_words): + if susp_word in cleancont and not any( + ok_word in cleancont for ok_word in config.suspect_ignored_words + ): msg += f"\n- Contains suspicious word: `{susp_word}`" alert = True @@ -166,13 +181,15 @@ class Logs(Cog): # Bad Code :tm:, blame retr0id message_clean = message.content.replace("*", "").replace("_", "") - regd = self.susp_hellgex.sub(lambda w: "**{}**".format(w.group(0)), - message_clean) + regd = self.susp_hellgex.sub( + lambda w: "**{}**".format(w.group(0)), message_clean + ) # Show a message embed embed = discord.Embed(description=regd) - embed.set_author(name=message.author.display_name, - icon_url=message.author.avatar_url) + embed.set_author( + name=message.author.display_name, icon_url=message.author.avatar_url + ) await spy_channel.send(msg, embed=embed) @@ -181,8 +198,9 @@ class Logs(Cog): if compliant: return - msg = f"R11 violating name by {message.author.mention} "\ - f"({message.author.id})." + msg = ( + f"R11 violating name by {message.author.mention} " f"({message.author.id})." + ) spy_channel = self.bot.get_channel(config.spylog_channel) await spy_channel.send(msg) @@ -209,10 +227,12 @@ class Logs(Cog): await self.do_spy(after) log_channel = self.bot.get_channel(config.log_channel) - msg = "📝 **Message edit**: \n"\ - f"from {self.bot.escape_message(after.author.name)} "\ - f"({after.author.id}), in {after.channel.mention}:\n"\ - f"`{before.clean_content}` → `{after.clean_content}`" + msg = ( + "📝 **Message edit**: \n" + f"from {self.bot.escape_message(after.author.name)} " + f"({after.author.id}), in {after.channel.mention}:\n" + f"`{before.clean_content}` → `{after.clean_content}`" + ) # If resulting message is too long, upload to hastebin if len(msg) > 2000: @@ -228,10 +248,12 @@ class Logs(Cog): return log_channel = self.bot.get_channel(config.log_channel) - msg = "🗑️ **Message delete**: \n"\ - f"from {self.bot.escape_message(message.author.name)} "\ - f"({message.author.id}), in {message.channel.mention}:\n"\ - f"`{message.clean_content}`" + msg = ( + "🗑️ **Message delete**: \n" + f"from {self.bot.escape_message(message.author.name)} " + f"({message.author.id}), in {message.channel.mention}:\n" + f"`{message.clean_content}`" + ) # If resulting message is too long, upload to hastebin if len(msg) > 2000: @@ -244,39 +266,45 @@ class Logs(Cog): async def on_member_remove(self, member): await self.bot.wait_until_ready() - if (member.guild.id not in config.guild_whitelist): + if member.guild.id not in config.guild_whitelist: return log_channel = self.bot.get_channel(config.log_channel) - msg = f"⬅️ **Leave**: {member.mention} | "\ - f"{self.bot.escape_message(member)}\n"\ - f"🏷 __User ID__: {member.id}" + msg = ( + f"⬅️ **Leave**: {member.mention} | " + f"{self.bot.escape_message(member)}\n" + f"🏷 __User ID__: {member.id}" + ) await log_channel.send(msg) @Cog.listener() async def on_member_ban(self, guild, member): await self.bot.wait_until_ready() - if (guild.id not in config.guild_whitelist): + if guild.id not in config.guild_whitelist: return log_channel = self.bot.get_channel(config.modlog_channel) - msg = f"⛔ **Ban**: {member.mention} | "\ - f"{self.bot.escape_message(member)}\n"\ - f"🏷 __User ID__: {member.id}" + msg = ( + f"⛔ **Ban**: {member.mention} | " + f"{self.bot.escape_message(member)}\n" + f"🏷 __User ID__: {member.id}" + ) await log_channel.send(msg) @Cog.listener() async def on_member_unban(self, guild, user): await self.bot.wait_until_ready() - if (guild.id not in config.guild_whitelist): + if guild.id not in config.guild_whitelist: return log_channel = self.bot.get_channel(config.modlog_channel) - msg = f"⚠️ **Unban**: {user.mention} | "\ - f"{self.bot.escape_message(user)}\n"\ - f"🏷 __User ID__: {user.id}" + msg = ( + f"⚠️ **Unban**: {user.mention} | " + f"{self.bot.escape_message(user)}\n" + f"🏷 __User ID__: {user.id}" + ) # if user.id in self.bot.timebans: # msg += "\nTimeban removed." # self.bot.timebans.pop(user.id) @@ -292,7 +320,7 @@ class Logs(Cog): async def on_member_update(self, member_before, member_after): await self.bot.wait_until_ready() - if (member_after.guild.id not in config.guild_whitelist): + if member_after.guild.id not in config.guild_whitelist: return msg = "" @@ -324,9 +352,11 @@ class Logs(Cog): msg += ", ".join(roles) if member_before.name != member_after.name: - msg += "\n📝 __Username change__: "\ - f"{self.bot.escape_message(member_before)} → "\ - f"{self.bot.escape_message(member_after)}" + msg += ( + "\n📝 __Username change__: " + f"{self.bot.escape_message(member_before)} → " + f"{self.bot.escape_message(member_after)}" + ) if member_before.nick != member_after.nick: if not member_before.nick: msg += "\n🏷 __Nickname addition__" @@ -334,11 +364,15 @@ class Logs(Cog): msg += "\n🏷 __Nickname removal__" else: msg += "\n🏷 __Nickname change__" - msg += f": {self.bot.escape_message(member_before.nick)} → "\ - f"{self.bot.escape_message(member_after.nick)}" + msg += ( + f": {self.bot.escape_message(member_before.nick)} → " + f"{self.bot.escape_message(member_after.nick)}" + ) if msg: - msg = f"ℹ️ **Member update**: {member_after.mention} | "\ - f"{self.bot.escape_message(member_after)}{msg}" + msg = ( + f"ℹ️ **Member update**: {member_after.mention} | " + f"{self.bot.escape_message(member_after)}{msg}" + ) await log_channel.send(msg) diff --git a/cogs/meme.py b/cogs/meme.py index 958c15e..9037f19 100644 --- a/cogs/meme.py +++ b/cogs/meme.py @@ -31,9 +31,11 @@ class Meme(Cog): 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).") + 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"]) @@ -42,9 +44,11 @@ class Meme(Cog): 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).") + 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"]) @@ -53,15 +57,18 @@ class Meme(Cog): 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"]) + @commands.command( + hidden=True, aliases=["reswitchedsilver", "silv3r", "reswitchedsilv3r"] + ) async def silver(self, ctx, user: discord.Member): """Gives a user ReSwitched Silver™""" - embed = discord.Embed(title="ReSwitched Silver™!", - description=f"Here's your ReSwitched Silver™," - f"{user.mention}!") - embed.set_image(url="https://cdn.discordapp.com/emojis/" - "548623626916724747.png?v=1") + embed = discord.Embed( + title="ReSwitched Silver™!", + description=f"Here's your ReSwitched Silver™," f"{user.mention}!", + ) + embed.set_image( + url="https://cdn.discordapp.com/emojis/" "548623626916724747.png?v=1" + ) await ctx.send(embed=embed) @commands.check(check_if_staff_or_ot) @@ -69,9 +76,11 @@ class Meme(Cog): async def btwiuse(self, ctx): """btw i use arch""" uname = platform.uname() - await ctx.send(f"BTW I use {platform.python_implementation()} " - f"{platform.python_version()} on {uname.system} " - f"{uname.release}") + await ctx.send( + f"BTW I use {platform.python_implementation()} " + f"{platform.python_version()} on {uname.system} " + f"{uname.release}" + ) @commands.check(check_if_staff_or_ot) @commands.command(hidden=True) @@ -95,25 +104,30 @@ class Meme(Cog): @commands.command(hidden=True, aliases=["outstanding"]) async def outstandingmove(self, ctx): """Posts the outstanding move meme""" - await ctx.send("https://cdn.discordapp.com/attachments" - "/371047036348268545/528413677007929344" - "/image0-5.jpg") + await ctx.send( + "https://cdn.discordapp.com/attachments" + "/371047036348268545/528413677007929344" + "/image0-5.jpg" + ) @commands.check(check_if_staff_or_ot) @commands.command(hidden=True) async def bones(self, ctx): - await ctx.send("https://cdn.discordapp.com/emojis/" - "443501365843591169.png?v=1") + await ctx.send( + "https://cdn.discordapp.com/emojis/" "443501365843591169.png?v=1" + ) @commands.check(check_if_staff_or_ot) @commands.command(hidden=True) async def headpat(self, ctx): - await ctx.send("https://cdn.discordapp.com/emojis/" - "465650811909701642.png?v=1") + await ctx.send( + "https://cdn.discordapp.com/emojis/" "465650811909701642.png?v=1" + ) @commands.check(check_if_staff_or_ot) - @commands.command(hidden=True, aliases=["when", "etawhen", - "emunand", "emummc", "thermosphere"]) + @commands.command( + hidden=True, aliases=["when", "etawhen", "emunand", "emummc", "thermosphere"] + ) async def eta(self, ctx): await ctx.send("June 15.") @@ -123,11 +137,14 @@ class Meme(Cog): """Bams a user owo""" 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( + "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(f"I'm sorry {ctx.author.mention}, " - "I'm afraid I can't do that.") + 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().convert(ctx, str(target)) await ctx.send(f"{safe_name} is ̶n͢ow b̕&̡.̷ 👍̡") @@ -145,8 +162,9 @@ class Meme(Cog): @commands.command(hidden=True, aliases=["yotld"]) async def yearoflinux(self, ctx): """Shows the year of Linux on the desktop""" - await ctx.send(f"{datetime.datetime.now().year} is the year of " - "Linux on the Desktop") + await ctx.send( + f"{datetime.datetime.now().year} is the year of " "Linux on the Desktop" + ) def setup(bot): diff --git a/cogs/mod.py b/cogs/mod.py index db42259..0301c73 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -25,11 +25,9 @@ class Mod(Cog): await ctx.send(f"Done!") log_channel = self.bot.get_channel(config.modlog_channel) - log_msg = f"✏️ **Guild Icon Update**: {ctx.author} "\ - "changed the guild icon." + log_msg = f"✏️ **Guild Icon Update**: {ctx.author} " "changed the guild icon." img_filename = url.split("/")[-1].split("#")[0] # hacky - img_file = discord.File(io.BytesIO(img_bytes), - filename=img_filename) + img_file = discord.File(io.BytesIO(img_bytes), filename=img_filename) await log_channel.send(log_msg, file=img_file) @commands.guild_only() @@ -41,11 +39,13 @@ class Mod(Cog): if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") elif target == self.bot.user: - return await ctx.send(f"I'm sorry {ctx.author.mention}, " - "I'm afraid I can't do that.") + return await ctx.send( + f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that." + ) elif self.check_if_target_is_staff(target): - return await ctx.send("I can't mute this user as " - "they're a member of staff.") + 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) @@ -53,7 +53,7 @@ class Mod(Cog): dm_message = f"You were muted!" if reason: - dm_message += f" The given reason is: \"{reason}\"." + dm_message += f' The given reason is: "{reason}".' try: await target.send(dm_message) @@ -66,15 +66,19 @@ class Mod(Cog): await target.add_roles(mute_role, reason=str(ctx.author)) - chan_message = f"🔇 **Muted**: {ctx.author.mention} muted "\ - f"{target.mention} | {safe_name}\n"\ - f"🏷 __User ID__: {target.id}\n" + chan_message = ( + f"🔇 **Muted**: {ctx.author.mention} muted " + f"{target.mention} | {safe_name}\n" + f"🏷 __User ID__: {target.id}\n" + ) if reason: - chan_message += f"✏️ __Reason__: \"{reason}\"" + chan_message += f'✏️ __Reason__: "{reason}"' else: - chan_message += "Please add an explanation below. In the future, "\ - "it is recommended to use `.mute [reason]`"\ - " as the reason is automatically sent to the user." + chan_message += ( + "Please add an explanation below. In the future, " + "it is recommended to use `.mute [reason]`" + " as the reason is automatically sent to the user." + ) log_channel = self.bot.get_channel(config.modlog_channel) await log_channel.send(chan_message) @@ -91,9 +95,11 @@ class Mod(Cog): mute_role = ctx.guild.get_role(config.mute_role) await target.remove_roles(mute_role, reason=str(ctx.author)) - chan_message = f"🔈 **Unmuted**: {ctx.author.mention} unmuted "\ - f"{target.mention} | {safe_name}\n"\ - f"🏷 __User ID__: {target.id}\n" + chan_message = ( + f"🔈 **Unmuted**: {ctx.author.mention} unmuted " + f"{target.mention} | {safe_name}\n" + f"🏷 __User ID__: {target.id}\n" + ) log_channel = self.bot.get_channel(config.modlog_channel) await log_channel.send(chan_message) @@ -110,11 +116,13 @@ class Mod(Cog): if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") elif target == self.bot.user: - return await ctx.send(f"I'm sorry {ctx.author.mention}, " - "I'm afraid I can't do that.") + return await ctx.send( + f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that." + ) elif self.check_if_target_is_staff(target): - return await ctx.send("I can't kick this user as " - "they're a member of staff.") + 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) @@ -122,9 +130,11 @@ class Mod(Cog): dm_message = f"You were kicked from {ctx.guild.name}." if reason: - dm_message += f" The given reason is: \"{reason}\"." - dm_message += "\n\nYou are able to rejoin the server,"\ - " but please be sure to behave when participating again." + dm_message += f' The given reason is: "{reason}".' + dm_message += ( + "\n\nYou are able to rejoin the server," + " but please be sure to behave when participating again." + ) try: await target.send(dm_message) @@ -134,16 +144,20 @@ class Mod(Cog): pass await target.kick(reason=f"{ctx.author}, reason: {reason}") - chan_message = f"👢 **Kick**: {ctx.author.mention} kicked "\ - f"{target.mention} | {safe_name}\n"\ - f"🏷 __User ID__: {target.id}\n" + chan_message = ( + f"👢 **Kick**: {ctx.author.mention} kicked " + f"{target.mention} | {safe_name}\n" + f"🏷 __User ID__: {target.id}\n" + ) if reason: - chan_message += f"✏️ __Reason__: \"{reason}\"" + chan_message += f'✏️ __Reason__: "{reason}"' else: - chan_message += "Please add an explanation below. In the future"\ - ", it is recommended to use "\ - "`.kick [reason]`"\ - " as the reason is automatically sent to the user." + chan_message += ( + "Please add an explanation below. In the future" + ", it is recommended to use " + "`.kick [reason]`" + " as the reason is automatically sent to the user." + ) log_channel = self.bot.get_channel(config.modlog_channel) await log_channel.send(chan_message) @@ -158,14 +172,18 @@ class Mod(Cog): # Hedge-proofing the code 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( + "https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG" + ) return await ctx.send("hedgeberg#7337 is now 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.") + return await ctx.send( + f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that." + ) elif self.check_if_target_is_staff(target): - return await ctx.send("I can't ban this user as " - "they're a member of staff.") + 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) @@ -173,7 +191,7 @@ class Mod(Cog): dm_message = f"You were banned from {ctx.guild.name}." if reason: - dm_message += f" The given reason is: \"{reason}\"." + dm_message += f' The given reason is: "{reason}".' dm_message += "\n\nThis ban does not expire." try: @@ -183,17 +201,22 @@ class Mod(Cog): # or has DMs disabled pass - await target.ban(reason=f"{ctx.author}, reason: {reason}", - delete_message_days=0) - chan_message = f"⛔ **Ban**: {ctx.author.mention} banned "\ - f"{target.mention} | {safe_name}\n"\ - f"🏷 __User ID__: {target.id}\n" + await target.ban( + reason=f"{ctx.author}, reason: {reason}", delete_message_days=0 + ) + chan_message = ( + f"⛔ **Ban**: {ctx.author.mention} banned " + f"{target.mention} | {safe_name}\n" + f"🏷 __User ID__: {target.id}\n" + ) if reason: - chan_message += f"✏️ __Reason__: \"{reason}\"" + chan_message += f'✏️ __Reason__: "{reason}"' else: - chan_message += "Please add an explanation below. In the future"\ - ", it is recommended to use `.ban [reason]`"\ - " as the reason is automatically sent to the user." + chan_message += ( + "Please add an explanation below. In the future" + ", it is recommended to use `.ban [reason]`" + " as the reason is automatically sent to the user." + ) log_channel = self.bot.get_channel(config.modlog_channel) await log_channel.send(chan_message) @@ -211,28 +234,34 @@ class Mod(Cog): if target == ctx.author.id: return await ctx.send("You can't do mod actions on yourself.") elif target == self.bot.user: - return await ctx.send(f"I'm sorry {ctx.author.mention}, " - "I'm afraid I can't do that.") + return await ctx.send( + f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that." + ) elif target_member and self.check_if_target_is_staff(target_member): - return await ctx.send("I can't ban this user as " - "they're a member of staff.") + 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 = await commands.clean_content().convert(ctx, str(target)) - await ctx.guild.ban(target_user, - reason=f"{ctx.author}, reason: {reason}", - delete_message_days=0) - chan_message = f"⛔ **Hackban**: {ctx.author.mention} banned "\ - f"{target_user.mention} | {safe_name}\n"\ - f"🏷 __User ID__: {target}\n" + await ctx.guild.ban( + target_user, reason=f"{ctx.author}, reason: {reason}", delete_message_days=0 + ) + chan_message = ( + f"⛔ **Hackban**: {ctx.author.mention} banned " + f"{target_user.mention} | {safe_name}\n" + f"🏷 __User ID__: {target}\n" + ) if reason: - chan_message += f"✏️ __Reason__: \"{reason}\"" + chan_message += f'✏️ __Reason__: "{reason}"' else: - chan_message += "Please add an explanation below. In the future"\ - ", it is recommended to use "\ - "`.hackban [reason]`." + chan_message += ( + "Please add an explanation below. In the future" + ", it is recommended to use " + "`.hackban [reason]`." + ) log_channel = self.bot.get_channel(config.modlog_channel) await log_channel.send(chan_message) @@ -248,27 +277,34 @@ class Mod(Cog): if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") elif target == self.bot.user: - return await ctx.send(f"I'm sorry {ctx.author.mention}, " - "I'm afraid I can't do that.") + return await ctx.send( + f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that." + ) elif self.check_if_target_is_staff(target): - return await ctx.send("I can't ban this user as " - "they're a member of staff.") + 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 = await commands.clean_content().convert(ctx, str(target)) - await target.ban(reason=f"{ctx.author}, reason: {reason}", - delete_message_days=0) - chan_message = f"⛔ **Silent ban**: {ctx.author.mention} banned "\ - f"{target.mention} | {safe_name}\n"\ - f"🏷 __User ID__: {target.id}\n" + await target.ban( + reason=f"{ctx.author}, reason: {reason}", delete_message_days=0 + ) + chan_message = ( + f"⛔ **Silent ban**: {ctx.author.mention} banned " + f"{target.mention} | {safe_name}\n" + f"🏷 __User ID__: {target.id}\n" + ) if reason: - chan_message += f"✏️ __Reason__: \"{reason}\"" + chan_message += f'✏️ __Reason__: "{reason}"' else: - chan_message += "Please add an explanation below. In the future"\ - ", it is recommended to use `.ban [reason]`"\ - " as the reason is automatically sent to the user." + chan_message += ( + "Please add an explanation below. In the future" + ", it is recommended to use `.ban [reason]`" + " as the reason is automatically sent to the user." + ) log_channel = self.bot.get_channel(config.modlog_channel) await log_channel.send(chan_message) @@ -276,12 +312,12 @@ 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: 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)) + return await ctx.send( + "No such role! Available roles: " + ",".join(config.named_roles) + ) log_channel = self.bot.get_channel(config.modlog_channel) target_role = ctx.guild.get_role(config.named_roles[role]) @@ -293,18 +329,19 @@ class Mod(Cog): await ctx.send(f"Approved {target.mention} to `{role}` role.") - await log_channel.send(f"✅ Approved: {ctx.author.mention} added" - f" {role} to {target.mention}") + await log_channel.send( + f"✅ Approved: {ctx.author.mention} added" f" {role} to {target.mention}" + ) @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: 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)) + return await ctx.send( + "No such role! Available roles: " + ",".join(config.named_roles) + ) log_channel = self.bot.get_channel(config.modlog_channel) target_role = ctx.guild.get_role(config.named_roles[role]) @@ -316,8 +353,10 @@ class Mod(Cog): await ctx.send(f"Un-approved {target.mention} from `{role}` role.") - await log_channel.send(f"❌ Un-approved: {ctx.author.mention} removed" - f" {role} from {target.mention}") + await log_channel.send( + f"❌ Un-approved: {ctx.author.mention} removed" + f" {role} from {target.mention}" + ) @commands.guild_only() @commands.check(check_if_staff) @@ -328,8 +367,10 @@ class Mod(Cog): if not channel: channel = ctx.channel await channel.purge(limit=limit) - msg = f"🗑 **Purged**: {ctx.author.mention} purged {limit} "\ - f"messages in {channel.mention}." + msg = ( + f"🗑 **Purged**: {ctx.author.mention} purged {limit} " + f"messages in {channel.mention}." + ) await log_channel.send(msg) @commands.guild_only() @@ -341,37 +382,46 @@ class Mod(Cog): if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") elif target == self.bot.user: - return await ctx.send(f"I'm sorry {ctx.author.mention}, " - "I'm afraid I can't do that.") + return await ctx.send( + f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that." + ) elif self.check_if_target_is_staff(target): - return await ctx.send("I can't warn this user as " - "they're a member of staff.") + return await ctx.send( + "I can't warn this user as " "they're a member of staff." + ) log_channel = self.bot.get_channel(config.modlog_channel) - warn_count = userlog(target.id, ctx.author, reason, - "warns", target.name) + warn_count = userlog(target.id, ctx.author, reason, "warns", target.name) safe_name = await commands.clean_content().convert(ctx, str(target)) - chan_msg = f"⚠️ **Warned**: {ctx.author.mention} warned "\ - f"{target.mention} (warn #{warn_count}) "\ - f"| {safe_name}\n" + chan_msg = ( + f"⚠️ **Warned**: {ctx.author.mention} warned " + f"{target.mention} (warn #{warn_count}) " + f"| {safe_name}\n" + ) msg = f"You were warned on {ctx.guild.name}." if reason: msg += " The given reason is: " + reason - msg += f"\n\nPlease read the rules in {config.rules_url}. "\ - f"This is warn #{warn_count}." + msg += ( + f"\n\nPlease read the rules in {config.rules_url}. " + f"This is warn #{warn_count}." + ) if warn_count == 2: msg += " __The next warn will automatically kick.__" if warn_count == 3: - msg += "\n\nYou were kicked because of this warning. "\ - "You can join again right away. "\ - "Two more warnings will result in an automatic ban." + msg += ( + "\n\nYou were kicked because of this warning. " + "You can join again right away. " + "Two more warnings will result in an automatic ban." + ) if warn_count == 4: - msg += "\n\nYou were kicked because of this warning. "\ - "This is your final warning. "\ - "You can join again, but "\ - "**one more warn will result in a ban**." + msg += ( + "\n\nYou were kicked because of this warning. " + "This is your final warning. " + "You can join again, but " + "**one more warn will result in a ban**." + ) chan_msg += "**This resulted in an auto-kick.**\n" if warn_count == 5: msg += "\n\nYou were automatically banned due to five warnings." @@ -385,17 +435,19 @@ class Mod(Cog): if warn_count == 3 or warn_count == 4: await target.kick() if warn_count >= 5: # just in case - await target.ban(reason="exceeded warn limit", - delete_message_days=0) - await ctx.send(f"{target.mention} warned. " - f"User has {warn_count} warning(s).") + await target.ban(reason="exceeded warn limit", delete_message_days=0) + await ctx.send( + f"{target.mention} warned. " f"User has {warn_count} warning(s)." + ) if reason: - chan_msg += f"✏️ __Reason__: \"{reason}\"" + chan_msg += f'✏️ __Reason__: "{reason}"' else: - chan_msg += "Please add an explanation below. In the future"\ - ", it is recommended to use `.warn [reason]`"\ - " as the reason is automatically sent to the user." + chan_msg += ( + "Please add an explanation below. In the future" + ", it is recommended to use `.warn [reason]`" + " as the reason is automatically sent to the user." + ) await log_channel.send(chan_msg) @commands.guild_only() @@ -415,7 +467,7 @@ class Mod(Cog): @commands.guild_only() @commands.check(check_if_staff) - @commands.command(aliases=['echo']) + @commands.command(aliases=["echo"]) async def say(self, ctx, *, the_text: str): """Repeats a given text, staff only.""" await ctx.send(the_text) diff --git a/cogs/mod_note.py b/cogs/mod_note.py index 9f407f1..a7e4499 100644 --- a/cogs/mod_note.py +++ b/cogs/mod_note.py @@ -14,8 +14,7 @@ class ModNote(Cog): @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) + userlog(target.id, ctx.author, note, "notes", target.name) await ctx.send(f"{ctx.author.mention}: noted!") @commands.guild_only() @@ -23,8 +22,7 @@ class ModNote(Cog): @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") + userlog(target, ctx.author, note, "notes") await ctx.send(f"{ctx.author.mention}: noted!") diff --git a/cogs/mod_reacts.py b/cogs/mod_reacts.py index 1e00ef7..36e692d 100644 --- a/cogs/mod_reacts.py +++ b/cogs/mod_reacts.py @@ -13,9 +13,14 @@ class ModReact(Cog): @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): + 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.modlog_channel) if not channel: @@ -27,18 +32,20 @@ class ModReact(Cog): 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}." + 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): + 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.modlog_channel) if not channel: @@ -48,8 +55,10 @@ class ModReact(Cog): 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}." + 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) @@ -58,8 +67,10 @@ class ModReact(Cog): @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_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 = [] @@ -74,10 +85,11 @@ class ModReact(Cog): 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) + 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: @@ -88,17 +100,17 @@ class ModReact(Cog): 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) + 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: diff --git a/cogs/mod_timed.py b/cogs/mod_timed.py index 5048985..bdfac32 100644 --- a/cogs/mod_timed.py +++ b/cogs/mod_timed.py @@ -20,31 +20,37 @@ class ModTimed(Cog): @commands.bot_has_permissions(ban_members=True) @commands.check(check_if_staff) @commands.command() - async def timeban(self, ctx, target: discord.Member, - duration: str, *, reason: str = ""): + async def timeban( + self, ctx, target: discord.Member, duration: str, *, reason: str = "" + ): """Bans a user for a specified amount of time, staff only.""" # Hedge-proofing the code if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") elif self.check_if_target_is_staff(target): - return await ctx.send("I can't ban this user as " - "they're a member of staff.") + return await ctx.send( + "I can't ban this user as " "they're a member of staff." + ) expiry_timestamp = self.bot.parse_time(duration) expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp) - duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime, - include_to=True, - humanized=True) + duration_text = self.bot.get_relative_timestamp( + time_to=expiry_datetime, include_to=True, humanized=True + ) - userlog(target.id, ctx.author, f"{reason} (Timed, until " - f"{duration_text})", - "bans", target.name) + userlog( + target.id, + ctx.author, + f"{reason} (Timed, until " f"{duration_text})", + "bans", + target.name, + ) safe_name = await commands.clean_content().convert(ctx, str(target)) dm_message = f"You were banned from {ctx.guild.name}." if reason: - dm_message += f" The given reason is: \"{reason}\"." + dm_message += f' The given reason is: "{reason}".' dm_message += f"\n\nThis ban will expire {duration_text}." try: @@ -54,53 +60,63 @@ class ModTimed(Cog): # or has DMs disabled pass - await target.ban(reason=f"{ctx.author}, reason: {reason}", - delete_message_days=0) - chan_message = f"⛔ **Timed Ban**: {ctx.author.mention} banned "\ - f"{target.mention} for {duration_text} | {safe_name}\n"\ - f"🏷 __User ID__: {target.id}\n" + await target.ban( + reason=f"{ctx.author}, reason: {reason}", delete_message_days=0 + ) + chan_message = ( + f"⛔ **Timed Ban**: {ctx.author.mention} banned " + f"{target.mention} for {duration_text} | {safe_name}\n" + f"🏷 __User ID__: {target.id}\n" + ) if reason: - chan_message += f"✏️ __Reason__: \"{reason}\"" + chan_message += f'✏️ __Reason__: "{reason}"' else: - chan_message += "Please add an explanation below. In the future"\ - ", it is recommended to use `.ban [reason]`"\ - " as the reason is automatically sent to the user." + chan_message += ( + "Please add an explanation below. In the future" + ", it is recommended to use `.ban [reason]`" + " as the reason is automatically sent to the user." + ) add_job("unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp) log_channel = self.bot.get_channel(config.log_channel) await log_channel.send(chan_message) - await ctx.send(f"{safe_name} is now b&. " - f"It will expire {duration_text}. 👍") + await ctx.send(f"{safe_name} is now b&. " f"It will expire {duration_text}. 👍") @commands.guild_only() @commands.check(check_if_staff) @commands.command() - async def timemute(self, ctx, target: discord.Member, - duration: str, *, reason: str = ""): + async def timemute( + self, ctx, target: discord.Member, duration: str, *, reason: str = "" + ): """Mutes a user for a specified amount of time, staff only.""" # Hedge-proofing the code if target == ctx.author: return await ctx.send("You can't do mod actions on yourself.") elif self.check_if_target_is_staff(target): - return await ctx.send("I can't mute this user as " - "they're a member of staff.") + return await ctx.send( + "I can't mute this user as " "they're a member of staff." + ) expiry_timestamp = self.bot.parse_time(duration) expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp) - duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime, - include_to=True, - humanized=True) + duration_text = self.bot.get_relative_timestamp( + time_to=expiry_datetime, include_to=True, humanized=True + ) - userlog(target.id, ctx.author, f"{reason} (Timed, until " - f"{duration_text})", - "mutes", target.name) + userlog( + target.id, + ctx.author, + f"{reason} (Timed, until " f"{duration_text})", + "mutes", + target.name, + ) safe_name = await commands.clean_content().convert(ctx, str(target)) dm_message = f"You were muted!" if reason: - dm_message += f" The given reason is: \"{reason}\"." + dm_message += f' The given reason is: "{reason}".' dm_message += f"\n\nThis mute will expire {duration_text}." try: @@ -114,22 +130,27 @@ class ModTimed(Cog): await target.add_roles(mute_role, reason=str(ctx.author)) - chan_message = f"🔇 **Timed Mute**: {ctx.author.mention} muted "\ - f"{target.mention} for {duration_text} | {safe_name}\n"\ - f"🏷 __User ID__: {target.id}\n" + chan_message = ( + f"🔇 **Timed Mute**: {ctx.author.mention} muted " + f"{target.mention} for {duration_text} | {safe_name}\n" + f"🏷 __User ID__: {target.id}\n" + ) if reason: - chan_message += f"✏️ __Reason__: \"{reason}\"" + chan_message += f'✏️ __Reason__: "{reason}"' else: - chan_message += "Please add an explanation below. In the future, "\ - "it is recommended to use `.mute [reason]`"\ - " as the reason is automatically sent to the user." + chan_message += ( + "Please add an explanation below. In the future, " + "it is recommended to use `.mute [reason]`" + " as the reason is automatically sent to the user." + ) add_job("unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp) 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. " - f"It will expire {duration_text}.") + await ctx.send( + f"{target.mention} can no longer speak. " f"It will expire {duration_text}." + ) add_restriction(target.id, config.mute_role) diff --git a/cogs/mod_userlog.py b/cogs/mod_userlog.py index 0ebf583..afb1720 100644 --- a/cogs/mod_userlog.py +++ b/cogs/mod_userlog.py @@ -11,8 +11,9 @@ class ModUserlog(Cog): def __init__(self, bot): self.bot = bot - def get_userlog_embed_for_id(self, uid: str, name: str, own: bool = False, - event=""): + 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 and not isinstance(event, list): @@ -30,12 +31,17 @@ class ModUserlog(Cog): 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) + 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 " @@ -65,17 +71,17 @@ class ModUserlog(Cog): if not event_count: return f"<@{uid}> has no {event_type}!" if idx > event_count: - return "Index is higher than "\ - f"count ({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] event_name = userlog_event_types[event_type] - embed = discord.Embed(color=discord.Color.dark_red(), - title=f"{event_name} {idx} on " - f"{event['timestamp']}", - description=f"Issuer: {event['issuer_name']}\n" - f"Reason: {event['reason']}") + embed = discord.Embed( + color=discord.Color.dark_red(), + title=f"{event_name} {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 @@ -85,21 +91,18 @@ class ModUserlog(Cog): @commands.command(aliases=["events"]) async def eventtypes(self, ctx): """Lists the available event types, staff only.""" - event_list = [f"{et} ({userlog_event_types[et]})" for et in - userlog_event_types] - event_text = ("Available events:\n``` - " + - "\n - ".join(event_list) + - "```") + event_list = [f"{et} ({userlog_event_types[et]})" for et in userlog_event_types] + event_text = "Available events:\n``` - " + "\n - ".join(event_list) + "```" await ctx.send(event_text) @commands.guild_only() @commands.check(check_if_staff) - @commands.command(name="userlog", - aliases=["listwarns", "getuserlog", "listuserlog"]) + @commands.command( + name="userlog", aliases=["listwarns", "getuserlog", "listuserlog"] + ) async def userlog_cmd(self, ctx, target: discord.Member, event=""): """Lists the userlog events for a user, staff only.""" - embed = self.get_userlog_embed_for_id(str(target.id), str(target), - event=event) + embed = self.get_userlog_embed_for_id(str(target.id), str(target), event=event) await ctx.send(embed=embed) @commands.guild_only() @@ -107,16 +110,16 @@ class ModUserlog(Cog): @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") + 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) + embed = self.get_userlog_embed_for_id(str(ctx.author.id), str(ctx.author), True) await ctx.send(embed=embed) @commands.guild_only() @@ -130,16 +133,17 @@ class ModUserlog(Cog): @commands.guild_only() @commands.check(check_if_staff) @commands.command(aliases=["clearwarns"]) - async def clearevent(self, ctx, target: discord.Member, - event="warns"): + 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.modlog_channel) msg = self.clear_event_from_id(str(target.id), event) safe_name = await commands.clean_content().convert(ctx, str(target)) await ctx.send(msg) - msg = f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"\ - f" all {event} events of {target.mention} | "\ - f"{safe_name}" + msg = ( + f"🗑 **Cleared {event}**: {ctx.author.mention} cleared" + f" all {event} events of {target.mention} | " + f"{safe_name}" + ) await log_channel.send(msg) @commands.guild_only() @@ -150,15 +154,16 @@ class ModUserlog(Cog): log_channel = self.bot.get_channel(config.modlog_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}> " + 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"): + 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.modlog_channel) del_event = self.delete_event_from_id(str(target.id), idx, event) @@ -167,9 +172,11 @@ class ModUserlog(Cog): if isinstance(del_event, discord.Embed): await ctx.send(f"{target.mention} has a {event_name} removed!") safe_name = await commands.clean_content().convert(ctx, str(target)) - msg = f"🗑 **Deleted {event_name}**: "\ - f"{ctx.author.mention} removed "\ - f"{event_name} {idx} from {target.mention} | {safe_name}" + msg = ( + f"🗑 **Deleted {event_name}**: " + f"{ctx.author.mention} removed " + f"{event_name} {idx} from {target.mention} | {safe_name}" + ) await log_channel.send(msg, embed=del_event) else: await ctx.send(del_event) @@ -185,9 +192,11 @@ class ModUserlog(Cog): # 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}> " + 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) @@ -202,23 +211,26 @@ class ModUserlog(Cog): role = "@ everyone" event_types = ["warns", "bans", "kicks", "mutes", "notes"] - embed = self.get_userlog_embed_for_id(str(user.id), str(user), - event=event_types) + embed = self.get_userlog_embed_for_id( + str(user.id), str(user), event=event_types + ) user_name = await commands.clean_content().convert(ctx, user.name) display_name = await commands.clean_content().convert(ctx, user.display_name) - await ctx.send(f"user = {user_name}\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 = {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", - embed=embed) + await ctx.send( + f"user = {user_name}\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 = {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", + embed=embed, + ) def setup(bot): diff --git a/cogs/pin.py b/cogs/pin.py index a91e509..9d8ac98 100644 --- a/cogs/pin.py +++ b/cogs/pin.py @@ -8,6 +8,7 @@ import gidgethub.aiohttp from helpers.checks import check_if_collaborator from helpers.checks import check_if_pin_channel + class Pin(Cog): """ Allow users to pin things @@ -17,9 +18,11 @@ class Pin(Cog): self.bot = bot def is_pinboard(self, msg): - return msg.author == self.bot.user and \ - len(msg.embeds) > 0 and \ - msg.embeds[0].title == "Pinboard" + return ( + msg.author == self.bot.user + and len(msg.embeds) > 0 + and msg.embeds[0].title == "Pinboard" + ) async def get_pinboard(self, gh, channel): # Find pinboard pin @@ -32,21 +35,25 @@ class Pin(Cog): return (id, data["files"]["pinboard.md"]["content"]) # Create pinboard pin if it does not exist - data = await gh.post("/gists", data={ - "files": { - "pinboard.md": { - "content": "Old pins are available here:\n\n" - } + data = await gh.post( + "/gists", + data={ + "files": { + "pinboard.md": {"content": "Old pins are available here:\n\n"} + }, + "description": f"Pinboard for SwitchRoot #{channel.name}", + "public": True, }, - "description": f"Pinboard for SwitchRoot #{channel.name}", - "public": True - }) + ) - msg = await channel.send(embed=Embed( - title="Pinboard", - description="Old pins are moved to the pinboard to make space for \ + msg = await channel.send( + embed=Embed( + title="Pinboard", + description="Old pins are moved to the pinboard to make space for \ new ones. Check it out!", - url=data["html_url"])) + url=data["html_url"], + ) + ) await msg.pin() return (data["id"], data["files"]["pinboard.md"]["content"]) @@ -57,18 +64,15 @@ class Pin(Cog): return async with aiohttp.ClientSession() as session: - gh = gidgethub.aiohttp.GitHubAPI(session, "RoboCop-NG", - oauth_token=config.github_oauth_token) + gh = gidgethub.aiohttp.GitHubAPI( + session, "RoboCop-NG", oauth_token=config.github_oauth_token + ) (id, content) = await self.get_pinboard(gh, channel) content += "- " + data + "\n" - await gh.patch(f"/gists/{id}", data={ - "files": { - "pinboard.md": { - "content": content - } - } - }) + await gh.patch( + f"/gists/{id}", data={"files": {"pinboard.md": {"content": content}}} + ) @commands.command() @commands.guild_only() @@ -136,7 +140,7 @@ class Pin(Cog): break # Wait for the automated "Pinned" message so we can delete it - waitable = self.bot.wait_for('message', check=check) + waitable = self.bot.wait_for("message", check=check) # Pin the message await target_msg.pin() diff --git a/cogs/remind.py b/cogs/remind.py index b91093d..64abf9e 100644 --- a/cogs/remind.py +++ b/cogs/remind.py @@ -22,12 +22,15 @@ class Remind(Cog): if uid not in ctab["remind"][jobtimestamp]: continue job_details = ctab["remind"][jobtimestamp][uid] - expiry_timestr = datetime.utcfromtimestamp(int(jobtimestamp))\ - .strftime('%Y-%m-%d %H:%M:%S (UTC)') - embed.add_field(name=f"Reminder for {expiry_timestr}", - value=f"Added on: {job_details['added']}, " - f"Text: {job_details['text']}", - inline=False) + expiry_timestr = datetime.utcfromtimestamp(int(jobtimestamp)).strftime( + "%Y-%m-%d %H:%M:%S (UTC)" + ) + embed.add_field( + name=f"Reminder for {expiry_timestr}", + value=f"Added on: {job_details['added']}, " + f"Text: {job_details['text']}", + inline=False, + ) await ctx.send(embed=embed) @commands.cooldown(1, 60, type=commands.BucketType.user) @@ -40,27 +43,32 @@ class Remind(Cog): expiry_timestamp = self.bot.parse_time(when) if current_timestamp + 5 > expiry_timestamp: - msg = await ctx.send(f"{ctx.author.mention}: Minimum " - "remind interval is 5 seconds.") + msg = await ctx.send( + f"{ctx.author.mention}: Minimum " "remind interval is 5 seconds." + ) await asyncio.sleep(5) await msg.delete() return expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp) - duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime, - include_to=True, - humanized=True) + duration_text = self.bot.get_relative_timestamp( + time_to=expiry_datetime, include_to=True, humanized=True + ) safe_text = await commands.clean_content().convert(ctx, str(text)) added_on = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S (UTC)") - add_job("remind", - ctx.author.id, - {"text": safe_text, "added": added_on}, - expiry_timestamp) + add_job( + "remind", + ctx.author.id, + {"text": safe_text, "added": added_on}, + expiry_timestamp, + ) - msg = await ctx.send(f"{ctx.author.mention}: I'll remind you in " - f"DMs about `{safe_text}` in {duration_text}.") + msg = await ctx.send( + f"{ctx.author.mention}: I'll remind you in " + f"DMs about `{safe_text}` in {duration_text}." + ) await asyncio.sleep(5) await msg.delete() diff --git a/cogs/robocronp.py b/cogs/robocronp.py index 47dee7f..d676464 100644 --- a/cogs/robocronp.py +++ b/cogs/robocronp.py @@ -33,17 +33,17 @@ class Robocronp(Cog): for jobtimestamp in ctab[jobtype]: for job_name in ctab[jobtype][jobtimestamp]: job_details = repr(ctab[jobtype][jobtimestamp][job_name]) - embed.add_field(name=f"{jobtype} for {job_name}", - value=f"Timestamp: {jobtimestamp}, " - f"Details: {job_details}", - inline=False) + embed.add_field( + name=f"{jobtype} for {job_name}", + value=f"Timestamp: {jobtimestamp}, " f"Details: {job_details}", + inline=False, + ) await ctx.send(embed=embed) @commands.guild_only() @commands.check(check_if_staff) @commands.command(aliases=["removejob"]) - async def deletejob(self, ctx, timestamp: str, - job_type: str, job_name: str): + async def deletejob(self, ctx, timestamp: str, job_type: str, job_name: str): """Removes a timed robocronp job, staff only. You'll need to supply: @@ -64,31 +64,34 @@ class Robocronp(Cog): target_user = await self.bot.fetch_user(job_name) target_guild = self.bot.get_guild(job_details["guild"]) delete_job(timestamp, jobtype, job_name) - await target_guild.unban(target_user, - reason="Robocronp: Timed " - "ban expired.") + await target_guild.unban( + target_user, reason="Robocronp: Timed " "ban expired." + ) elif jobtype == "unmute": remove_restriction(job_name, config.mute_role) target_guild = self.bot.get_guild(job_details["guild"]) target_member = target_guild.get_member(int(job_name)) target_role = target_guild.get_role(config.mute_role) - await target_member.remove_roles(target_role, - reason="Robocronp: Timed " - "mute expired.") + await target_member.remove_roles( + target_role, reason="Robocronp: Timed " "mute expired." + ) delete_job(timestamp, jobtype, job_name) elif jobtype == "remind": text = job_details["text"] added_on = job_details["added"] target = await self.bot.fetch_user(int(job_name)) if target: - await target.send("You asked to be reminded about" - f" `{text}` on {added_on}.") + await target.send( + "You asked to be reminded about" f" `{text}` on {added_on}." + ) delete_job(timestamp, jobtype, job_name) except: # Don't kill cronjobs if something goes wrong. delete_job(timestamp, jobtype, job_name) - await log_channel.send("Crondo has errored, job deleted: ```" - f"{traceback.format_exc()}```") + await log_channel.send( + "Crondo has errored, job deleted: ```" + f"{traceback.format_exc()}```" + ) async def clean_channel(self, channel_id): log_channel = self.bot.get_channel(config.botlog_channel) @@ -101,12 +104,14 @@ class Robocronp(Cog): count += len(purge_res) if len(purge_res) != 100: done_cleaning = True - await log_channel.send(f"Wiped {count} messages from " - f"<#{channel.id}> automatically.") + await log_channel.send( + f"Wiped {count} messages from " f"<#{channel.id}> automatically." + ) except: # Don't kill cronjobs if something goes wrong. - await log_channel.send("Cronclean has errored: ```" - f"{traceback.format_exc()}```") + await log_channel.send( + "Cronclean has errored: ```" f"{traceback.format_exc()}```" + ) async def minutely(self): await self.bot.wait_until_ready() @@ -125,8 +130,9 @@ class Robocronp(Cog): await self.clean_channel(clean_channel) except: # Don't kill cronjobs if something goes wrong. - await log_channel.send("Cron-minutely has errored: ```" - f"{traceback.format_exc()}```") + await log_channel.send( + "Cron-minutely has errored: ```" f"{traceback.format_exc()}```" + ) await asyncio.sleep(60) async def hourly(self): @@ -144,8 +150,9 @@ class Robocronp(Cog): await self.clean_channel(clean_channel) except: # Don't kill cronjobs if something goes wrong. - await log_channel.send("Cron-hourly has errored: ```" - f"{traceback.format_exc()}```") + await log_channel.send( + "Cron-hourly has errored: ```" f"{traceback.format_exc()}```" + ) # Your stuff that should run an hour after boot # and after that every hour goes here @@ -163,8 +170,9 @@ class Robocronp(Cog): await self.bot.do_resetalgo(verif_channel, "daily robocronp") except: # Don't kill cronjobs if something goes wrong. - await log_channel.send("Cron-daily has errored: ```" - f"{traceback.format_exc()}```") + await log_channel.send( + "Cron-daily has errored: ```" f"{traceback.format_exc()}```" + ) await asyncio.sleep(86400) # Your stuff that should run a day after boot # and after that every day goes here diff --git a/cogs/verification.py b/cogs/verification.py index 8850078..1fbb80c 100644 --- a/cogs/verification.py +++ b/cogs/verification.py @@ -23,18 +23,20 @@ class Verification(Cog): await channel.purge(limit=limit) await channel.send(config.welcome_header) - rules = ['**{}**. {}'.format(i, cleandoc(r)) for i, r in - enumerate(config.welcome_rules, 1)] + rules = [ + "**{}**. {}".format(i, cleandoc(r)) + for i, r in enumerate(config.welcome_rules, 1) + ] rule_choice = random.randint(2, len(rules)) hash_choice_str = self.hash_choice.upper() if hash_choice_str == "BLAKE2B": hash_choice_str += "-512" elif hash_choice_str == "BLAKE2S": hash_choice_str += "-256" - rules[rule_choice - 1] += \ - '\n' + config.hidden_term_line.format(hash_choice_str) - msg = f"🗑 **Reset**: {author} cleared {limit} messages "\ - f" in {channel.mention}" + rules[rule_choice - 1] += "\n" + config.hidden_term_line.format(hash_choice_str) + msg = ( + f"🗑 **Reset**: {author} cleared {limit} messages " f" in {channel.mention}" + ) msg += f"\n💬 __Current challenge location__: under rule {rule_choice}" log_channel = self.bot.get_channel(config.log_channel) await log_channel.send(msg) @@ -64,13 +66,17 @@ class Verification(Cog): async def do_resetalgo(self, channel, author, limit: int = 100): # randomize hash_choice on reset - self.hash_choice = \ - random.choice(tuple(hashlib.algorithms_guaranteed - - self.blacklisted_hashes - - {self.hash_choice})) + self.hash_choice = random.choice( + tuple( + hashlib.algorithms_guaranteed + - self.blacklisted_hashes + - {self.hash_choice} + ) + ) - msg = f"📘 **Reset Algorithm**: {author} reset "\ - f"algorithm in {channel.mention}" + msg = ( + f"📘 **Reset Algorithm**: {author} reset " f"algorithm in {channel.mention}" + ) msg += f"\n💬 __Current algorithm__: {self.hash_choice.upper()}" log_channel = self.bot.get_channel(config.log_channel) await log_channel.send(msg) @@ -82,8 +88,10 @@ class Verification(Cog): async def reset(self, ctx, limit: int = 100, force: bool = False): """Wipes messages and pastes the welcome message again. Staff only.""" if ctx.message.channel.id != config.welcome_channel and not force: - await ctx.send(f"This command is limited to" - f" <#{config.welcome_channel}>, unless forced.") + await ctx.send( + f"This command is limited to" + f" <#{config.welcome_channel}>, unless forced." + ) return await self.do_reset(ctx.channel, ctx.author.mention, limit) @@ -92,8 +100,10 @@ class Verification(Cog): async def resetalgo(self, ctx, limit: int = 100, force: bool = False): """Resets the verification algorithm and does what reset does. Staff only.""" if ctx.message.channel.id != config.welcome_channel and not force: - await ctx.send(f"This command is limited to" - f" <#{config.welcome_channel}>, unless forced.") + await ctx.send( + f"This command is limited to" + f" <#{config.welcome_channel}>, unless forced." + ) return await self.do_resetalgo(ctx.channel, ctx.author.mention, limit) @@ -113,13 +123,20 @@ class Verification(Cog): mcl = message.content.lower() # Reply to users that insult the bot - oof = ["bad", "broken", "buggy", "bugged", - "stupid", "dumb", "silly", "fuck", "heck", "h*ck"] + oof = [ + "bad", + "broken", + "buggy", + "bugged", + "stupid", + "dumb", + "silly", + "fuck", + "heck", + "h*ck", + ] if "bot" in mcl and any(insult in mcl for insult in oof): - snark = random.choice(["bad human", - "no u", - "no u, rtfm", - "pebkac"]) + snark = random.choice(["bad human", "no u", "no u, rtfm", "pebkac"]) return await chan.send(snark) # Get the role we will give in case of success @@ -127,38 +144,57 @@ class Verification(Cog): # Get a list of stuff we'll allow and will consider close allowed_names = [f"@{full_name}", full_name, str(member.id)] - close_names = [f"@{member.name}", member.name, discrim, - f"#{discrim}"] + close_names = [f"@{member.name}", member.name, discrim, f"#{discrim}"] # Now add the same things but with newlines at the end of them - allowed_names += [(an + '\n') for an in allowed_names] - close_names += [(cn + '\n') for cn in close_names] - allowed_names += [(an + '\r\n') for an in allowed_names] - close_names += [(cn + '\r\n') for cn in close_names] + allowed_names += [(an + "\n") for an in allowed_names] + close_names += [(cn + "\n") for cn in close_names] + allowed_names += [(an + "\r\n") for an in allowed_names] + close_names += [(cn + "\r\n") for cn in close_names] # [ ͡° ͜ᔦ ͡°] 𝐖𝐞𝐥𝐜𝐨𝐦𝐞 𝐭𝐨 𝐌𝐚𝐜 𝐎𝐒 𝟗. - allowed_names += [(an + '\r') for an in allowed_names] - close_names += [(cn + '\r') for cn in close_names] + allowed_names += [(an + "\r") for an in allowed_names] + close_names += [(cn + "\r") for cn in close_names] # Finally, hash the stuff so that we can access them later :) - hash_allow = [hashlib.new(self.hash_choice, - name.encode('utf-8')).hexdigest() - for name in allowed_names] + hash_allow = [ + hashlib.new(self.hash_choice, name.encode("utf-8")).hexdigest() + for name in allowed_names + ] # I'm not even going to attempt to break those into lines jfc if any(allow in mcl for allow in hash_allow): await member.add_roles(success_role) - return await chan.purge(limit=100, check=lambda m: m.author == message.author or (m.author == self.bot.user and message.author.mention in m.content)) + return await chan.purge( + limit=100, + check=lambda m: m.author == message.author + or ( + m.author == self.bot.user + and message.author.mention in m.content + ), + ) # Detect if the user uses the wrong hash algorithm - wrong_hash_algos = hashlib.algorithms_guaranteed - \ - {self.hash_choice} - self.blacklisted_hashes + wrong_hash_algos = ( + hashlib.algorithms_guaranteed + - {self.hash_choice} + - self.blacklisted_hashes + ) for algo in wrong_hash_algos: for name in itertools.chain(allowed_names, close_names): - if hashlib.new(algo, name.encode('utf-8')).hexdigest() in mcl: + if hashlib.new(algo, name.encode("utf-8")).hexdigest() in mcl: log_channel = self.bot.get_channel(config.log_channel) - await log_channel.send(f"User {message.author.mention} tried verification with algo {algo} instead of {self.hash_choice}.") - return await chan.send(f"{message.author.mention} :no_entry: Close, but not quite. Go back and re-read!") + await log_channel.send( + f"User {message.author.mention} tried verification with algo {algo} instead of {self.hash_choice}." + ) + return await chan.send( + f"{message.author.mention} :no_entry: Close, but not quite. Go back and re-read!" + ) - if full_name in message.content or str(member.id) in message.content or member.name in message.content or discrim in message.content: + if ( + full_name in message.content + or str(member.id) in message.content + or member.name in message.content + or discrim in message.content + ): no_text = ":no_entry: Incorrect. You need to do something *specific* with your name and discriminator instead of just posting it. Please re-read the rules carefully and look up any terms you are not familiar with." rand_num = random.randint(1, 100) if rand_num == 42: @@ -166,7 +202,7 @@ class Verification(Cog): elif rand_num == 43: no_text = "ugh, wrong, read the rules." elif rand_num == 44: - no_text = "\"The definition of insanity is doing the same thing over and over again, but expecting different results.\"\n-Albert Einstein" + no_text = '"The definition of insanity is doing the same thing over and over again, but expecting different results."\n-Albert Einstein' await chan.send(f"{message.author.mention} {no_text}") @Cog.listener() diff --git a/config_template.py b/config_template.py index 1669051..55bee67 100644 --- a/config_template.py +++ b/config_template.py @@ -11,31 +11,35 @@ source_url = "https://github.com/reswitched/robocop-ng" rules_url = "https://reswitched.team/discord/#rules" # The bot description to be used in .robocop embed -embed_desc = "Robocop-NG is developed by [Ave](https://github.com/aveao)"\ - " and [tomGER](https://github.com/tumGER), and is a rewrite "\ - "of Robocop.\nRobocop is based on Kurisu by 916253 and ihaveamac." +embed_desc = ( + "Robocop-NG is developed by [Ave](https://github.com/aveao)" + " and [tomGER](https://github.com/tumGER), and is a rewrite " + "of Robocop.\nRobocop is based on Kurisu by 916253 and ihaveamac." +) # The cogs the bot will load on startup. -initial_cogs = ['cogs.common', - 'cogs.admin', - 'cogs.verification', - 'cogs.mod', - 'cogs.mod_note', - 'cogs.mod_reacts', - 'cogs.mod_userlog', - 'cogs.mod_timed', - 'cogs.mod_watch', - 'cogs.basic', - 'cogs.logs', - 'cogs.err', - 'cogs.lockdown', - 'cogs.legacy', - 'cogs.links', - 'cogs.remind', - 'cogs.robocronp', - 'cogs.meme', - 'cogs.invites'] +initial_cogs = [ + "cogs.common", + "cogs.admin", + "cogs.verification", + "cogs.mod", + "cogs.mod_note", + "cogs.mod_reacts", + "cogs.mod_userlog", + "cogs.mod_timed", + "cogs.mod_watch", + "cogs.basic", + "cogs.logs", + "cogs.err", + "cogs.lockdown", + "cogs.legacy", + "cogs.links", + "cogs.remind", + "cogs.robocronp", + "cogs.meme", + "cogs.invites", +] # The following cogs are also available but aren't loaded by default: # cogs.imagemanip - Adds a meme command called .cox. @@ -52,27 +56,27 @@ initial_cogs = ['cogs.common', min_age = datetime.timedelta(minutes=15) # The bot will only work in these guilds -guild_whitelist = [ - 269333940928512010 # ReSwitched discord -] +guild_whitelist = [269333940928512010] # ReSwitched discord # Named roles to be used with .approve and .revoke # Example: .approve User hacker named_roles = { "community": 420010997877833731, "hacker": 364508795038072833, - "participant": 434353085926866946 + "participant": 434353085926866946, } # The bot manager and staff roles # Bot manager can run eval, exit and other destructive commands # Staff can run administrative commands bot_manager_role_id = 466447265863696394 # Bot management role in ReSwitched -staff_role_ids = [364647829248933888, # Team role in ReSwitched - 360138431524765707, # Mod role in ReSwitched - 466447265863696394, # Bot management role in ReSwitched - 360138163156549632, # Admin role in ReSwitched - 287289529986187266] # Wizard role in ReSwitched +staff_role_ids = [ + 364647829248933888, # Team role in ReSwitched + 360138431524765707, # Mod role in ReSwitched + 466447265863696394, # Bot management role in ReSwitched + 360138163156549632, # Admin role in ReSwitched + 287289529986187266, +] # Wizard role in ReSwitched # Various log channels used to log bot and guild's activity # You can use same channel for multiple log types @@ -86,29 +90,30 @@ welcome_channel = 326416669058662401 # newcomers channel in ReSwitched # These channel entries are used to determine which roles will be given # access when we unmute on them -general_channels = [420029476634886144, - 414949821003202562, - 383368936466546698, - 343244421044633602, - 491316901692178432, - 539212260350885908] # Channels everyone can access -community_channels = [269333940928512010, - 438839875970662400, - 404722395845361668, - 435687501068501002, - 286612533757083648] # Channels requiring community role +general_channels = [ + 420029476634886144, + 414949821003202562, + 383368936466546698, + 343244421044633602, + 491316901692178432, + 539212260350885908, +] # Channels everyone can access +community_channels = [ + 269333940928512010, + 438839875970662400, + 404722395845361668, + 435687501068501002, + 286612533757083648, +] # Channels requiring community role # Controls which roles are blocked during lockdown lockdown_configs = { # Used as a default value for channels without a config - "default": { - "channels": general_channels, - "roles": [named_roles["participant"]] - }, + "default": {"channels": general_channels, "roles": [named_roles["participant"]]}, "community": { "channels": community_channels, - "roles": [named_roles["community"], named_roles["hacker"]] - } + "roles": [named_roles["community"], named_roles["hacker"]], + }, } # Mute role is applied to users when they're muted @@ -135,24 +140,33 @@ list_files_channel = 0 list_channels = [] # All lower case, no spaces, nothing non-alphanumeric -suspect_words = ["sx", "tx", "reinx", # piracy-enabling cfws - "gomanx", # piracy-enabling cfws - "tinfoil", "dz", # title managers - "goldleaf", "lithium", # title managers - "cracked", # older term for pirated games - "xci", "nsz"] # "backup" format +suspect_words = [ + "sx", + "tx", + "reinx", # piracy-enabling cfws + "gomanx", # piracy-enabling cfws + "tinfoil", + "dz", # title managers + "goldleaf", + "lithium", # title managers + "cracked", # older term for pirated games + "xci", + "nsz", +] # "backup" format # List of words that will be ignored if they match one of the # suspect_words (This is used to remove false positives) -suspect_ignored_words = ["excit", - "s/x", - "3dsx", - "psx", - "txt", - "s(x", - "txd", - "t=x", - "osx"] +suspect_ignored_words = [ + "excit", + "s/x", + "3dsx", + "psx", + "txt", + "s(x", + "txd", + "t=x", + "osx", +] # == Only if you want to use cogs.pin == # Used for the pinboard. Leave empty if you don't wish for a gist pinboard. @@ -189,7 +203,6 @@ welcome_rules = ( Read all the rules before participating in chat. Not reading the rules is *not* an excuse for breaking them. • It's suggested that you read channel topics and pins before asking questions as well, as some questions may have already been answered in those. """, - # 2 """ Be nice to each other. It's fine to disagree, it's not fine to insult or attack other people. @@ -197,31 +210,24 @@ welcome_rules = ( • Constant antagonistic behavior is considered uncivil and appropriate action will be taken. • The use of derogatory slurs -- sexist, racist, homophobic, transphobic, or otherwise -- is unacceptable and may be grounds for an immediate ban. """, - # 3 'If you have concerns about another user, please take up your concerns with a staff member (someone with the "mod" role in the sidebar) in private. Don\'t publicly call other users out.', - # 4 """ From time to time, we may mention everyone in the server. We do this when we feel something important is going on that requires attention. Complaining about these pings may result in a ban. • To disable notifications for these pings, suppress them in "ReSwitched → Notification Settings". """, - # 5 """ Don't spam. • For excessively long text, use a service like . """, - # 6 "Don't brigade, raid, or otherwise attack other people or communities. Don't discuss participation in these attacks. This may warrant an immediate permanent ban.", - # 7 - 'Off-topic content goes to #off-topic. Keep low-quality content like memes out.', - + "Off-topic content goes to #off-topic. Keep low-quality content like memes out.", # 8 - 'Trying to evade, look for loopholes, or stay borderline within the rules will be treated as breaking them.', - + "Trying to evade, look for loopholes, or stay borderline within the rules will be treated as breaking them.", # 9 """ Absolutely no piracy or related discussion. This includes: @@ -231,16 +237,14 @@ welcome_rules = ( • Usage of piracy-focused groups' (Team Xecuter, etc.) hardware and software, such as SX OS. This is a zero-tolerance, non-negotiable policy that is enforced strictly and swiftly, up to and including instant bans without warning. """, - # 10 - 'The first character of your server nickname should be alphanumeric if you wish to talk in chat.', - + "The first character of your server nickname should be alphanumeric if you wish to talk in chat.", # 11 """ Do not boost the server. • ReSwitched neither wants nor needs your server boosts, and your money is better off elsewhere. Consider the EFF (or a charity of your choice). • Boosting the server is liable to get you kicked (to remove the nitro boost role), and/or warned. Roles you possessed prior to the kick may not be restored in a timely fashion. - """ + """, ) @@ -258,7 +262,6 @@ welcome_footer = ( #hack-n-all - General hacking, hardware and software development channel for hacking on things *other* than the switch. This is a great place to ask about hacking other systems-- and for the community to have technical discussions. """, - """ #switch-hacking-general - Channel for everyone working on hacking the switch-- both in an exploit and a low-level hardware sense. This is where a lot of our in-the-open development goes on. Note that this isn't the place for developing homebrew-- we have #homebrew-development for that! diff --git a/helpers/checks.py b/helpers/checks.py index aad2817..70a5c70 100644 --- a/helpers/checks.py +++ b/helpers/checks.py @@ -16,17 +16,19 @@ def check_if_bot_manager(ctx): def check_if_staff_or_ot(ctx): if not ctx.guild: return True - is_ot = (ctx.channel.name == "off-topic") - is_bot_cmds = (ctx.channel.name == "bot-cmds") + is_ot = ctx.channel.name == "off-topic" + is_bot_cmds = ctx.channel.name == "bot-cmds" is_staff = any(r.id in config.staff_role_ids for r in ctx.author.roles) - return (is_ot or is_staff or is_bot_cmds) + return is_ot or is_staff or is_bot_cmds def check_if_collaborator(ctx): if not ctx.guild: return False - return any(r.id in config.staff_role_ids + config.allowed_pin_roles - for r in ctx.author.roles) + return any( + r.id in config.staff_role_ids + config.allowed_pin_roles + for r in ctx.author.roles + ) def check_if_pin_channel(ctx): diff --git a/helpers/errcodes.py b/helpers/errcodes.py index 09f8a6f..91796fd 100644 --- a/helpers/errcodes.py +++ b/helpers/errcodes.py @@ -99,24 +99,18 @@ switch_modules = { 212: "GRC ", 216: "Migration ", 217: "Migration Idc Server ", - # Libnx - 345: "libnx ", 346: "Homebrew ABI ", 347: "Homebrew Loader ", 348: "libnx Nvidia", 349: "libnx Binder", - # Support Errors - 800: "General web-applet", 809: "WifiWebAuthApplet", 810: "Whitelisted-applet", 811: "ShopN", - # Custom Sysmodules - 311: "SwitchPresence", } @@ -150,11 +144,11 @@ switch_known_errcodes = { 0xFC01: "Reserved value ", 0xFE01: "Invalid hardware breakpoint ", 0x10001: "[Usermode] Fatal exception ", - 0x10201: "Last thread didn\'t belong to your process ", + 0x10201: "Last thread didn't belong to your process ", 0x10601: "Port closed ", 0x10801: "Resource limit exceeded ", 0x20801: "Command buffer too small ", - 0x40a01: "Invalid process ID.", + 0x40A01: "Invalid process ID.", 0x40C01: "Invalid thread ID.", 0x40E01: "Invalid thread ID (used in svcGetDebugThreadParam).", 0x6402: "NCA is older than version 3, or NCA SDK version is older than 0.11.0.0", @@ -248,7 +242,7 @@ switch_known_errcodes = { 0x3EA03: "Invalid handle ", 0x3EE03: "Invalid memory mirror ", 0x7FE03: "TLS slot is not allocated ", - 0xA05: "NcaID not found. Returned when attempting to mount titles which exist that aren\'t *8XX titles, the same way *8XX titles are mounted. ", + 0xA05: "NcaID not found. Returned when attempting to mount titles which exist that aren't *8XX titles, the same way *8XX titles are mounted. ", 0xE05: "TitleId not found ", 0x1805: "Invalid StorageId ", 0xDC05: "Gamecard not inserted ", @@ -273,7 +267,7 @@ switch_known_errcodes = { 0xA09: "Invalid files. ", 0xE09: "Already registered. ", 0x1009: "Title not found. ", - 0x1209: "Title-id in ACI0 doesn\'t match range in ACID. ", + 0x1209: "Title-id in ACI0 doesn't match range in ACID. ", 0x6609: "Invalid memory state/permission ", 0x6A09: "Invalid NRR ", 0xA209: "Unaligned NRR address ", @@ -281,15 +275,15 @@ switch_known_errcodes = { 0xAA09: "Bad NRR address ", 0xAE09: "Bad initialization ", 0xC809: "Unknown ACI0 descriptor ", - 0xCE09: "ACID/ACI0 don\'t match for descriptor KernelFlags ", - 0xD009: "ACID/ACI0 don\'t match for descriptor SyscallMask ", - 0xD409: "ACID/ACI0 don\'t match for descriptor MapIoOrNormalRange ", - 0xD609: "ACID/ACI0 don\'t match for descriptor MapNormalPage ", - 0xDE09: "ACID/ACI0 don\'t match for descriptor InterruptPair ", - 0xE209: "ACID/ACI0 don\'t match for descriptor ApplicationType ", - 0xE409: "ACID/ACI0 don\'t match for descriptor KernelReleaseVersion ", - 0xE609: "ACID/ACI0 don\'t match for descriptor HandleTableSize ", - 0xE809: "ACID/ACI0 don\'t match for descriptor DebugFlags ", + 0xCE09: "ACID/ACI0 don't match for descriptor KernelFlags ", + 0xD009: "ACID/ACI0 don't match for descriptor SyscallMask ", + 0xD409: "ACID/ACI0 don't match for descriptor MapIoOrNormalRange ", + 0xD609: "ACID/ACI0 don't match for descriptor MapNormalPage ", + 0xDE09: "ACID/ACI0 don't match for descriptor InterruptPair ", + 0xE209: "ACID/ACI0 don't match for descriptor ApplicationType ", + 0xE409: "ACID/ACI0 don't match for descriptor KernelReleaseVersion ", + 0xE609: "ACID/ACI0 don't match for descriptor HandleTableSize ", + 0xE809: "ACID/ACI0 don't match for descriptor DebugFlags ", 0x1940A: "Invalid CMIF header size. ", 0x1A60A: "Invalid CMIF input header. ", 0x1A80A: "Invalid CMIF output header. ", @@ -300,7 +294,7 @@ switch_known_errcodes = { 0x20B: "Unsupported operation ", 0xCC0B: "Out of server session memory ", 0x11A0B: "Went past maximum during marshalling. ", - 0x1900B: "Session doesn\'t support domains. ", + 0x1900B: "Session doesn't support domains. ", 0x25A0B: "Remote process is dead. ", 0x3260B: "Unknown request type ", 0x3D60B: "IPC Query 1 failed. ", @@ -343,8 +337,8 @@ switch_known_errcodes = { 0x1BC69: "Empty settings item key ", 0x1E269: "Setting group name is too long (64 character limit?) ", 0x1E469: "Setting name is too long (64 character limit?) ", - 0x20A69: "Setting group name ends with \'.\' or contains invalid characters (allowed: [a-z0-9_\-.]) ", - 0x20C69: "Setting name ends with \'.\' or contains invalid characters (allowed: [a-z0-9_\-.]) ", + 0x20A69: "Setting group name ends with '.' or contains invalid characters (allowed: [a-z0-9_\-.]) ", + 0x20C69: "Setting name ends with '.' or contains invalid characters (allowed: [a-z0-9_\-.]) ", 0x4DA69: "Null language code buffer ", 0x4EE69: "Null network settings buffer ", 0x4F069: "Null network settings output count buffer ", @@ -469,7 +463,7 @@ switch_known_errcodes = { 0xD48C: "Invalid descriptor ", 0x1928C: "USB device not bound / interface already enabled ", 0x299: "Invalid audio device ", - 0x499: "Operation couldn\'t complete successfully ", + 0x499: "Operation couldn't complete successfully ", 0x699: "Invalid sample rate ", 0x899: "Buffer size too small ", 0x1099: "Too many buffers are still unreleased ", @@ -486,7 +480,7 @@ switch_known_errcodes = { 0xF0CD: "IR image data not available/ready. ", 0x35B: "Failed to init SM. ", 0x55B: "Failed to init FS. ", - 0x75B: "Failed to to open NRO file. May also happen when SD card isn\'t inserted / SD mounting failed earlier. ", + 0x75B: "Failed to to open NRO file. May also happen when SD card isn't inserted / SD mounting failed earlier. ", 0x95B: "Failed to read NRO header. ", 0xB5B: "Invalid NRO magic. ", 0xD5B: "Invalid NRO segments. ", @@ -508,7 +502,7 @@ switch_known_errcodes = { 0x480: "Storage not available.", 0x1987E: "Development/debug-only behavior", 0xD27E: "Invalid database entry count", - 0xCE7E: "Invalid database signature value (should be \"NFDB\")", + 0xCE7E: 'Invalid database signature value (should be "NFDB")', 0x87E: "Entry not found", 0x27E: "Invalid argument", 0x7BC74: "Unimplemented functionality", @@ -565,15 +559,12 @@ switch_known_errcodes = { 0xC47A: "Invalid operation", 0x290: "Exited Abnormally ([[Applet_Manager_services#LibraryAppletExitReason|ExitReason]] == Abormal)", 0x690: "Canceled ([[Applet_Manager_services#LibraryAppletExitReason|ExitReason]] == Canceled)", - 0x890: "Rejected", #me_irl + 0x890: "Rejected", # me_irl 0xA90: "Exited Unexpectedly ([[Applet_Manager_services#LibraryAppletExitReason|ExitReason]] == Unexpected)", 0x58ACA: "Npad ID is out of range.", 0x1A8CD: "IR camera handle pointer is null.", 0x198CD: "IR camera invalid handle value.", - - # FS Codes - 0xD401: "Error: Passed buffer is not usable for fs library. ", 0x177A02: "Error: Specified value is out of range. ", 0x2F5C02: "Error: Invalid size was specified.", @@ -582,18 +573,14 @@ switch_known_errcodes = { 0x307202: "Error: OpenMode_AllowAppend is required for implicit extension of file size by WriteFile(). ", 0x346402: "Error: Enough journal space is not left. ", 0x346A02: "Error: The open count of files and directories reached the limitation. ", - # Fatal - 0x4A2: "Can be triggered by running svcBreak. The svcBreak params have no affect on the value of the thrown error-code.", 0xA8: "Userland ARM undefined instruction exception", 0x2A8: "Userland ARM prefetch-abort due to PC set to non-executable region", 0x4A8: "Userland ARM data abort. Also caused by abnormal process termination via svcExitProcess. Note: directly jumping to nnMain()-retaddr from non-main-thread has the same result.", 0x6A8: "Userland PC address not aligned to 4 bytes ", 0x10A8: "Can occur when attempting to call an svc outside the whitelist ", - # Libnx Errors from libnx/result.h - # - Normal - 0x359: "LibnxError_BadReloc", 0x559: "LibnxError_OutOfMemory", @@ -641,7 +628,6 @@ switch_known_errcodes = { 0x5959: "LibnxError_NvinfoFailedToInitialize", 0x5B59: "LibnxError_NvbufFailedToInitialize", 0x5D59: "LibnxError_LibAppletBadExit", - # - Libnx Binder - 0x35D: "LibnxBinderError_Unknown", 0x55D: "LibnxBinderError_NoMemory", @@ -660,7 +646,6 @@ switch_known_errcodes = { 0x1F5D: "LibnxBinderError_TimedOut", 0x215D: "LibnxBinderError_UnknownTransaction", 0x235D: "LibnxBinderError_FdsNotAllowed", - # - LibNX Nvidia - 0x35C: "LibnxNvidiaError_Unknown", 0x55C: "LibnxNvidiaError_NotImplemented", @@ -681,91 +666,81 @@ switch_known_errcodes = { 0x235C: "LibnxNvidiaError_SharedMemoryTooSmall", 0x255C: "LibnxNvidiaError_FileOperationFailed", 0x275C: "LibnxNvidiaError_IoctlFailed", - # Non-SwitchBrew Error Codes - Should probably add them to SwitchBrew if you read this - - 0x7E12B: 'Eshop connection failed', - 0x39D689: 'CDN Ban', - 0x3E8E7C: 'Error in account login/creation', - 0x3E8EA0: 'Failed connection test', - 0x1F4E7C: '(normal) console ban', - 0x27EE7C: '(potential) complete account ban', # This error is still super new, needs more informations + 0x7E12B: "Eshop connection failed", + 0x39D689: "CDN Ban", + 0x3E8E7C: "Error in account login/creation", + 0x3E8EA0: "Failed connection test", + 0x1F4E7C: "(normal) console ban", + 0x27EE7C: "(potential) complete account ban", # This error is still super new, needs more informations 0x36B72B: "Access token expired", 0x1F486E: "Internet connection lost because the console entered sleep mode.", 0x21C89: "Failed to base64-encode the EticketDeviceCertificate during an attempted AccountGetDynamicEtickets (personalized ticket) request to ecs.", 0x5089: "Failed to snprintf the AccountGetDynamicEtickets (personalized ticket) request JSON data.", 0x6410: "GetApplicationControlData: unable to find control for the input title ID", - 0xa073: "NFC is disabled", + 0xA073: "NFC is disabled", 0x16473: "Could not mount tag (invalid tag type?)", 0x8073: "Device unavailable", 0x10073: "App area not found", 0x11073: "Tag corrupted?", - 0xc880: "thrown by AM when qlaunch is terminated", - 0xc87c: "invalid user", - 0xc7e: "mii already exists", - 0xa7e: "full database", - 0x87e: "mii not found", - 0x115b: "[HBL] Stopped loading NROs", - 0x48c69: "device_cert_ecc_b223 failed to load", + 0xC880: "thrown by AM when qlaunch is terminated", + 0xC87C: "invalid user", + 0xC7E: "mii already exists", + 0xA7E: "full database", + 0x87E: "mii not found", + 0x115B: "[HBL] Stopped loading NROs", + 0x48C69: "device_cert_ecc_b223 failed to load", 0x138E02: "gamecard cmd buffer too small - must be 0x40 (or bigger)", 0x138E02: "gc out of bounds sector access", 0x13DC02: "gc sector start is out of range for partition 1", 0x13D802: "gc sector end out of range for partition 1", 0x13DA02: "gc sector wrong partition access", - - # 0x3E8E89: 'Failed to access Firmware Updates - Often because of DNS!', # ^ Also used by libcurl - # Atmosphere - 0xCAFEF: "Atmosphere: Version Mismatch", - # Pegaswitch - - 0xa7200: "Fake-Error by Pegaswitch", - + 0xA7200: "Fake-Error by Pegaswitch", # SwitchPresence - #Archived because the plugin got discontinued - #0x337: "Error_InitSocket", - #0x537: "Error_Listen", - #0x737: "Error_Accepting", - #0x937: "Error_ListAppFailed", + # Archived because the plugin got discontinued + # 0x337: "Error_InitSocket", + # 0x537: "Error_Listen", + # 0x737: "Error_Accepting", + # 0x937: "Error_ListAppFailed", # 0xb37: "Error_InvalidMagic", - #0xd37: "Error_CmdIdNotConfirm", - #0xf37: "Error_CmdIdNotSendBuff", - #0x1137: "Error_RecData", - #0x1337: "Error_SendData", - #0x1537: "Error_InitNS", - #0x1737: "Error_InitACC", - #0x1937: "Error_GetControlData", - #0x1b37: "Error_InvalidControlSize", - #0x1d37: "Error_GetAciveUser", - #0x1f37: "Error_GetProfile", - #0x2137: "Error_ProfileGet", - #0x2337: "Error_InitPMDMNT", - #0x2537: "Error_GetAppPid", - #0x2737: "Error_GetProcessTid", - #0x2937: "Error_InitPMINFO", - #0x2b37: "Error_GetPidList", - #0x2d37: "Error_GetDebugProc", - #0x2f37: "Error_CloseHandle", - + # 0xd37: "Error_CmdIdNotConfirm", + # 0xf37: "Error_CmdIdNotSendBuff", + # 0x1137: "Error_RecData", + # 0x1337: "Error_SendData", + # 0x1537: "Error_InitNS", + # 0x1737: "Error_InitACC", + # 0x1937: "Error_GetControlData", + # 0x1b37: "Error_InvalidControlSize", + # 0x1d37: "Error_GetAciveUser", + # 0x1f37: "Error_GetProfile", + # 0x2137: "Error_ProfileGet", + # 0x2337: "Error_InitPMDMNT", + # 0x2537: "Error_GetAppPid", + # 0x2737: "Error_GetProcessTid", + # 0x2937: "Error_InitPMINFO", + # 0x2b37: "Error_GetPidList", + # 0x2d37: "Error_GetDebugProc", + # 0x2f37: "Error_CloseHandle", # Joke - 0xDEADBEEF: "Congrats, you found some hexspeak \n \n https://www.youtube.com/watch?v=DLzxrzFCyOs", - # By Ave - 0x0: "Happens in various situations, not necessarily an error, but still prevents booting.\n\nIf you got this because you downgraded, it's because you downgraded between major/key versions (7.0.x -> 6.2.0, 6.2.0 -> 6.1.0 etc) without console initialization (deleting system save files).\n\nTo recover from that: Delete system all save files except 80...120. Keep in mind that this will effectively be a factory reset.", } switch_known_errcode_ranges = { # NIM 137: [ - [8001, 8096, 'libcurl error 1-96. Some of the libcurl errors in the error-table map to the above unknown-libcurl-error however.'], + [ + 8001, + 8096, + "libcurl error 1-96. Some of the libcurl errors in the error-table map to the above unknown-libcurl-error however.", + ], ], - # FS 2: [ [2000, 2499, "Error: Failed to access SD card."], @@ -786,12 +761,19 @@ switch_known_errcode_ranges = { [6300, 6399, "Error: Unsupported operation."], [6400, 6499, "Error: Permission denied."], ], - # NIFM Support Page Links 110: [ - [2900, 2999, "https://en-americas-support.nintendo.com/app/answers/detail/a_id/22277/p/897"], - [2000, 2899, "https://en-americas-support.nintendo.com/app/answers/detail/a_id/22263/p/897"], - ] + [ + 2900, + 2999, + "https://en-americas-support.nintendo.com/app/answers/detail/a_id/22277/p/897", + ], + [ + 2000, + 2899, + "https://en-americas-support.nintendo.com/app/answers/detail/a_id/22263/p/897", + ], + ], } # Game Erros - Strings because Nintendo decided that it would be useless to put them into normal ints ;^) @@ -799,7 +781,6 @@ switch_known_errcode_ranges = { switch_game_err = { # Splatoon 2 "2-AAB6A-3400": "Splatoon 2: A kick from online due to exefs edits.", - # Youtube "2-ARVHA-0000": "Youtube: Unknown Error", } @@ -847,272 +828,271 @@ switch_support_page = { } dds_summaries = { - 0: 'Success', - 1: 'Nothing happened', - 2: 'Would block', - 3: 'Out of resource', - 4: 'Not found', - 5: 'Invalid state', - 6: 'Not supported', - 7: 'Invalid argument', - 8: 'Wrong argument', - 9: 'Canceled', - 10: 'Status changed', - 11: 'Internal', - 63: 'Invalid result value' + 0: "Success", + 1: "Nothing happened", + 2: "Would block", + 3: "Out of resource", + 4: "Not found", + 5: "Invalid state", + 6: "Not supported", + 7: "Invalid argument", + 8: "Wrong argument", + 9: "Canceled", + 10: "Status changed", + 11: "Internal", + 63: "Invalid result value", } dds_levels = { 0: "Success", 1: "Info", - 25: "Status", 26: "Temporary", 27: "Permanent", 28: "Usage", 29: "Reinitialize", 30: "Reset", - 31: "Fatal" + 31: "Fatal", } dds_modules = { - 0: 'Common', - 1: 'Kernel', - 2: 'Util', - 3: 'File server', - 4: 'Loader server', - 5: 'TCB', - 6: 'OS', - 7: 'DBG', - 8: 'DMNT', - 9: 'PDN', - 10: 'GSP', - 11: 'I2C', - 12: 'GPIO', - 13: 'DD', - 14: 'CODEC', - 15: 'SPI', - 16: 'PXI', - 17: 'FS', - 18: 'DI', - 19: 'HID', - 20: 'CAM', - 21: 'PI', - 22: 'PM', - 23: 'PM_LOW', - 24: 'FSI', - 25: 'SRV', - 26: 'NDM', - 27: 'NWM', - 28: 'SOC', - 29: 'LDR', - 30: 'ACC', - 31: 'RomFS', - 32: 'AM', - 33: 'HIO', - 34: 'Updater', - 35: 'MIC', - 36: 'FND', - 37: 'MP', - 38: 'MPWL', - 39: 'AC', - 40: 'HTTP', - 41: 'DSP', - 42: 'SND', - 43: 'DLP', - 44: 'HIO_LOW', - 45: 'CSND', - 46: 'SSL', - 47: 'AM_LOW', - 48: 'NEX', - 49: 'Friends', - 50: 'RDT', - 51: 'Applet', - 52: 'NIM', - 53: 'PTM', - 54: 'MIDI', - 55: 'MC', - 56: 'SWC', - 57: 'FatFS', - 58: 'NGC', - 59: 'CARD', - 60: 'CARDNOR', - 61: 'SDMC', - 62: 'BOSS', - 63: 'DBM', - 64: 'Config', - 65: 'PS', - 66: 'CEC', - 67: 'IR', - 68: 'UDS', - 69: 'PL', - 70: 'CUP', - 71: 'Gyroscope', - 72: 'MCU', - 73: 'NS', - 74: 'News', - 75: 'RO', - 76: 'GD', - 77: 'Card SPI', - 78: 'EC', - 79: 'Web Browser', - 80: 'Test', - 81: 'ENC', - 82: 'PIA', - 83: 'ACT', - 84: 'VCTL', - 85: 'OLV', - 86: 'NEIA', - 87: 'NPNS', - 90: 'AVD', - 91: 'L2B', - 92: 'MVD', - 93: 'NFC', - 94: 'UART', - 95: 'SPM', - 96: 'QTM', - 97: 'NFP (amiibo)', - 254: 'Application', - 255: 'Invalid result value' + 0: "Common", + 1: "Kernel", + 2: "Util", + 3: "File server", + 4: "Loader server", + 5: "TCB", + 6: "OS", + 7: "DBG", + 8: "DMNT", + 9: "PDN", + 10: "GSP", + 11: "I2C", + 12: "GPIO", + 13: "DD", + 14: "CODEC", + 15: "SPI", + 16: "PXI", + 17: "FS", + 18: "DI", + 19: "HID", + 20: "CAM", + 21: "PI", + 22: "PM", + 23: "PM_LOW", + 24: "FSI", + 25: "SRV", + 26: "NDM", + 27: "NWM", + 28: "SOC", + 29: "LDR", + 30: "ACC", + 31: "RomFS", + 32: "AM", + 33: "HIO", + 34: "Updater", + 35: "MIC", + 36: "FND", + 37: "MP", + 38: "MPWL", + 39: "AC", + 40: "HTTP", + 41: "DSP", + 42: "SND", + 43: "DLP", + 44: "HIO_LOW", + 45: "CSND", + 46: "SSL", + 47: "AM_LOW", + 48: "NEX", + 49: "Friends", + 50: "RDT", + 51: "Applet", + 52: "NIM", + 53: "PTM", + 54: "MIDI", + 55: "MC", + 56: "SWC", + 57: "FatFS", + 58: "NGC", + 59: "CARD", + 60: "CARDNOR", + 61: "SDMC", + 62: "BOSS", + 63: "DBM", + 64: "Config", + 65: "PS", + 66: "CEC", + 67: "IR", + 68: "UDS", + 69: "PL", + 70: "CUP", + 71: "Gyroscope", + 72: "MCU", + 73: "NS", + 74: "News", + 75: "RO", + 76: "GD", + 77: "Card SPI", + 78: "EC", + 79: "Web Browser", + 80: "Test", + 81: "ENC", + 82: "PIA", + 83: "ACT", + 84: "VCTL", + 85: "OLV", + 86: "NEIA", + 87: "NPNS", + 90: "AVD", + 91: "L2B", + 92: "MVD", + 93: "NFC", + 94: "UART", + 95: "SPM", + 96: "QTM", + 97: "NFP (amiibo)", + 254: "Application", + 255: "Invalid result value", } dds_descriptions = { - 0: 'Success', - 2: 'Invalid memory permissions (kernel)', - 4: 'Invalid ticket version (AM)', - 5: 'Invalid string length. This error is returned when service name length is greater than 8 or zero. (srv)', - 6: 'Access denied. This error is returned when you request a service that you don\'t have access to. (srv)', - 7: 'String size does not match string contents. This error is returned when service name contains an unexpected null byte. (srv)', - 8: 'Camera already in use/busy (qtm).', - 10: 'Not enough memory (os)', - 26: 'Session closed by remote (os)', - 32: 'Empty CIA? (AM)', - 37: 'Invalid NCCH? (AM)', - 39: 'Invalid title version (AM)', - 43: 'Database doesn\'t exist/failed to open (AM)', - 44: 'Trying to uninstall system-app (AM)', - 47: 'Invalid command header (OS)', - 101: 'Archive not mounted/mount-point not found (fs)', - 105: 'Request timed out (http)', - 106: 'Invalid signature/CIA? (AM)', - 120: 'Title/object not found? (fs)', - 141: 'Gamecard not inserted? (fs)', - 190: 'Object does already exist/failed to create object.', - 230: 'Invalid open-flags / permissions? (fs)', - 250: 'FAT operation denied (fs?)', - 271: 'Invalid configuration (mvd).', - 335: '(No permission? Seemed to appear when JKSM was being used without its XML.)', - 391: 'NCCH hash-check failed? (fs)', - 392: 'RSA/AES-MAC verification failed? (fs)', - 393: 'Invalid database? (AM)', - 395: 'RomFS/Savedata hash-check failed? (fs)', - 630: 'Command not allowed / missing permissions? (fs)', - 702: 'Invalid path? (fs)', - 740: '(Occurred when NDS card was inserted and attempting to use AM_GetTitleCount on MEDIATYPE_GAME_CARD.) (fs)', - 761: 'Incorrect read-size for ExeFS? (fs)', - 1000: 'Invalid selection', - 1001: 'Too large', - 1002: 'Not authorized', - 1003: 'Already done', - 1004: 'Invalid size', - 1005: 'Invalid enum value', - 1006: 'Invalid combination', - 1007: 'No data', - 1008: 'Busy', - 1009: 'Misaligned address', - 1010: 'Misaligned size', - 1011: 'Out of memory', - 1012: 'Not implemented', - 1013: 'Invalid address', - 1014: 'Invalid pointer', - 1015: 'Invalid handle', - 1016: 'Not initialized', - 1017: 'Already initialized', - 1018: 'Not found', - 1019: 'Cancel requested', - 1020: 'Already exists', - 1021: 'Out of range', - 1022: 'Timeout', - 1023: 'Invalid result value' + 0: "Success", + 2: "Invalid memory permissions (kernel)", + 4: "Invalid ticket version (AM)", + 5: "Invalid string length. This error is returned when service name length is greater than 8 or zero. (srv)", + 6: "Access denied. This error is returned when you request a service that you don't have access to. (srv)", + 7: "String size does not match string contents. This error is returned when service name contains an unexpected null byte. (srv)", + 8: "Camera already in use/busy (qtm).", + 10: "Not enough memory (os)", + 26: "Session closed by remote (os)", + 32: "Empty CIA? (AM)", + 37: "Invalid NCCH? (AM)", + 39: "Invalid title version (AM)", + 43: "Database doesn't exist/failed to open (AM)", + 44: "Trying to uninstall system-app (AM)", + 47: "Invalid command header (OS)", + 101: "Archive not mounted/mount-point not found (fs)", + 105: "Request timed out (http)", + 106: "Invalid signature/CIA? (AM)", + 120: "Title/object not found? (fs)", + 141: "Gamecard not inserted? (fs)", + 190: "Object does already exist/failed to create object.", + 230: "Invalid open-flags / permissions? (fs)", + 250: "FAT operation denied (fs?)", + 271: "Invalid configuration (mvd).", + 335: "(No permission? Seemed to appear when JKSM was being used without its XML.)", + 391: "NCCH hash-check failed? (fs)", + 392: "RSA/AES-MAC verification failed? (fs)", + 393: "Invalid database? (AM)", + 395: "RomFS/Savedata hash-check failed? (fs)", + 630: "Command not allowed / missing permissions? (fs)", + 702: "Invalid path? (fs)", + 740: "(Occurred when NDS card was inserted and attempting to use AM_GetTitleCount on MEDIATYPE_GAME_CARD.) (fs)", + 761: "Incorrect read-size for ExeFS? (fs)", + 1000: "Invalid selection", + 1001: "Too large", + 1002: "Not authorized", + 1003: "Already done", + 1004: "Invalid size", + 1005: "Invalid enum value", + 1006: "Invalid combination", + 1007: "No data", + 1008: "Busy", + 1009: "Misaligned address", + 1010: "Misaligned size", + 1011: "Out of memory", + 1012: "Not implemented", + 1013: "Invalid address", + 1014: "Invalid pointer", + 1015: "Invalid handle", + 1016: "Not initialized", + 1017: "Already initialized", + 1018: "Not found", + 1019: "Cancel requested", + 1020: "Already exists", + 1021: "Out of range", + 1022: "Timeout", + 1023: "Invalid result value", } # Nintendo Error Codes dds_errcodes = { # Nintendo 3DS - '001-0502': 'Some sort of network error related to friend presence. "Allow Friends to see your online status" might fix this.', - '001-0803': 'Could not communicate with authentication server.', - '002-0102': 'System is permanently banned by Nintendo. ', - '002-0107': 'System is temporarily(?) banned by Nintendo. ', - '002-0119': 'System update required (outdated friends-module)', - '002-0120': 'Title update required (outdated title version)', - '002-0121': 'Local friend code SEED has invalid signature.\n\nThis should not happen unless it is modified. The only use case for modifying this file is for system unbanning, so ', - '002-0123': 'System is generally banned by Nintendo. ', - '003-1099': 'Access point could not be found with the given SSID.', - '003-2001': 'DNS error. If using a custom DNS server, make sure the settings are correct.', - '005-4800': 'HTTP Status 500 (Internal Error), unknown cause(?). eShop servers might have issues.', - '005-5602': 'Unable to connect to the eShop. This error is most likely the result of an incorrect region setting.\nMake sure your region is correctly set in System Settings. If you encounter this error after region-changing your system, make sure you followed all the steps properly.', - '005-5964': 'Your Nintendo Network ID has been banned from accessing the eShop.\nIf you think this was unwarranted, you will have to contact Nintendo Support to have it reversed.', - '005-7550': 'Replace SD card(?). Occurs on Nintendo eShop.', - '006-0102': 'Unexpected error. Could probably happen trying to play an out-of-region title online?', - '006-0332': 'Disconnected from the game server.', - '006-0502': 'Could not connect to the server.\n\n• Check the [network status page](http://support.nintendo.com/networkstatus)\n• Move closer to your wireless router\n• Verify DNS settings. If "Auto-Obtain" doesn\'t work, try Google\'s Public DNS (8.8.8.8, 8.8.4.4) and try again.', - '006-0612': 'Failed to join the session.', - '007-0200': 'Could not access SD card.', - '007-2001': 'Usually the result after region-changing the system. New 3DS cannot fix this issue right now.', - '007-2100': 'The connection to the Nintendo eShop timed out.\nThis may be due to an ongoing server maintenance, check to make sure the servers are operating normally. You may also encounter this error if you have a weak internet connection.', - '007-2404': 'An error occurred while attempting to connect to the Nintendo eShop.\nMake sure you are running the latest firmware, since this error will appear if you are trying to access the eShop on older versions.', - '007-2720': 'SSL error?', - '007-2916': 'HTTP error, server is probably down. Try again later?', - '007-2920': 'This error is caused by installing a game or game update from an unofficial source, as it contains a bad ticket.\nThe only solution is to delete the unofficial game or update as well as its ticket\nin FBI, and install the game or update legitimately.', - '007-2913': 'HTTP error, server is probably down. Try again later?', - '007-2923': 'The Nintendo Servers are currently down for maintenance. Please try again later.', - '007-3102': 'Cannot find title on Nintendo eShop. Probably pulled.', - '007-6054': 'Occurs when ticket database is full (8192 tickets).', - '009-1000': 'System update required. (friends module?)', - '009-2916': 'NIM HTTP error, server is probably down. Try again later?', - '009-2913': 'NIM HTTP error, server is probably down. Try again later?', - '009-4079': 'Could not access SD card. General purpose error.', - '009-4998': '"Local content is newer."\nThe actual cause of this error is unknown.', - '009-6106': '"AM error in NIM."\nProbably a bad ticket.', - '009-8401': 'Update data corrupted. Delete and re-install.', - '011-3021': 'Cannot find title on Nintendo eShop. Probably incorrect region, or never existed.', - '011-3136': 'Nintendo eShop is currently unavailable. Try again later.', - '011-6901': 'System is banned by Nintendo, this error code description is oddly Japanese, generic error code. ', - '012-1511': 'Certificate warning.', - '014-0016': 'Both systems have the same movable.sed key. Format the target and try system transfer again.', - '014-0062': 'Error during System Transfer. Move closer to the wireless router and keep trying.', - '022-2452': 'Occurs when trying to use Nintendo eShop with UNITINFO patches enabled.', - '022-2501': 'Attempting to use a Nintendo Network ID on one system when it is linked on another. This can be the result of using System Transfer, then restoring the source system\'s NAND and attempting to use services that require a Nintendo Network ID.\n\nIn a System Transfer, all Nintendo Network ID accounts associated with the system are transferred over, whether they are currently linked or not.', - '022-2511': 'System update required (what causes this? noticed while opening Miiverse, probably not friends module)', - '022-2613': 'Incorrect e-mail or password when trying to link an existing Nintendo Network ID. Make sure there are no typos, and the given e-mail is the correct one for the given ID.\nIf you forgot the password, reset it at ', - '022-2631': 'Nintendo Network ID deleted, or not usable on the current system. If you used System Transfer, the Nintendo Network ID will only work on the target system.', - '022-2633': 'Nintendo Network ID temporarily locked due to too many incorrect password attempts. Try again later.', - '022-2634': 'Nintendo Network ID is not correctly linked on the system. This can be a result of formatting the SysNAND using System Settings to unlink it from the EmuNAND.\n\n\n\nTinyFormat is recommended for unlinking in the future.', - '022-2812': 'System is permanently banned by Nintendo for illegally playing the Pokemon Sun & Moon ROM leak online before release. ', - '022-2815': 'System is banned by Nintendo from Miiverse access.', - '032-1820': 'Browser error that asks whether you want to go on to a potentially dangerous website. Can be bypassed by touching "yes".', - '090-0212': 'Game is permanently banned from Pokémon Global Link. This is most likely as a result of using altered or illegal save data.', + "001-0502": 'Some sort of network error related to friend presence. "Allow Friends to see your online status" might fix this.', + "001-0803": "Could not communicate with authentication server.", + "002-0102": "System is permanently banned by Nintendo. ", + "002-0107": "System is temporarily(?) banned by Nintendo. ", + "002-0119": "System update required (outdated friends-module)", + "002-0120": "Title update required (outdated title version)", + "002-0121": "Local friend code SEED has invalid signature.\n\nThis should not happen unless it is modified. The only use case for modifying this file is for system unbanning, so ", + "002-0123": "System is generally banned by Nintendo. ", + "003-1099": "Access point could not be found with the given SSID.", + "003-2001": "DNS error. If using a custom DNS server, make sure the settings are correct.", + "005-4800": "HTTP Status 500 (Internal Error), unknown cause(?). eShop servers might have issues.", + "005-5602": "Unable to connect to the eShop. This error is most likely the result of an incorrect region setting.\nMake sure your region is correctly set in System Settings. If you encounter this error after region-changing your system, make sure you followed all the steps properly.", + "005-5964": "Your Nintendo Network ID has been banned from accessing the eShop.\nIf you think this was unwarranted, you will have to contact Nintendo Support to have it reversed.", + "005-7550": "Replace SD card(?). Occurs on Nintendo eShop.", + "006-0102": "Unexpected error. Could probably happen trying to play an out-of-region title online?", + "006-0332": "Disconnected from the game server.", + "006-0502": "Could not connect to the server.\n\n• Check the [network status page](http://support.nintendo.com/networkstatus)\n• Move closer to your wireless router\n• Verify DNS settings. If \"Auto-Obtain\" doesn't work, try Google's Public DNS (8.8.8.8, 8.8.4.4) and try again.", + "006-0612": "Failed to join the session.", + "007-0200": "Could not access SD card.", + "007-2001": "Usually the result after region-changing the system. New 3DS cannot fix this issue right now.", + "007-2100": "The connection to the Nintendo eShop timed out.\nThis may be due to an ongoing server maintenance, check to make sure the servers are operating normally. You may also encounter this error if you have a weak internet connection.", + "007-2404": "An error occurred while attempting to connect to the Nintendo eShop.\nMake sure you are running the latest firmware, since this error will appear if you are trying to access the eShop on older versions.", + "007-2720": "SSL error?", + "007-2916": "HTTP error, server is probably down. Try again later?", + "007-2920": "This error is caused by installing a game or game update from an unofficial source, as it contains a bad ticket.\nThe only solution is to delete the unofficial game or update as well as its ticket\nin FBI, and install the game or update legitimately.", + "007-2913": "HTTP error, server is probably down. Try again later?", + "007-2923": "The Nintendo Servers are currently down for maintenance. Please try again later.", + "007-3102": "Cannot find title on Nintendo eShop. Probably pulled.", + "007-6054": "Occurs when ticket database is full (8192 tickets).", + "009-1000": "System update required. (friends module?)", + "009-2916": "NIM HTTP error, server is probably down. Try again later?", + "009-2913": "NIM HTTP error, server is probably down. Try again later?", + "009-4079": "Could not access SD card. General purpose error.", + "009-4998": '"Local content is newer."\nThe actual cause of this error is unknown.', + "009-6106": '"AM error in NIM."\nProbably a bad ticket.', + "009-8401": "Update data corrupted. Delete and re-install.", + "011-3021": "Cannot find title on Nintendo eShop. Probably incorrect region, or never existed.", + "011-3136": "Nintendo eShop is currently unavailable. Try again later.", + "011-6901": "System is banned by Nintendo, this error code description is oddly Japanese, generic error code. ", + "012-1511": "Certificate warning.", + "014-0016": "Both systems have the same movable.sed key. Format the target and try system transfer again.", + "014-0062": "Error during System Transfer. Move closer to the wireless router and keep trying.", + "022-2452": "Occurs when trying to use Nintendo eShop with UNITINFO patches enabled.", + "022-2501": "Attempting to use a Nintendo Network ID on one system when it is linked on another. This can be the result of using System Transfer, then restoring the source system's NAND and attempting to use services that require a Nintendo Network ID.\n\nIn a System Transfer, all Nintendo Network ID accounts associated with the system are transferred over, whether they are currently linked or not.", + "022-2511": "System update required (what causes this? noticed while opening Miiverse, probably not friends module)", + "022-2613": "Incorrect e-mail or password when trying to link an existing Nintendo Network ID. Make sure there are no typos, and the given e-mail is the correct one for the given ID.\nIf you forgot the password, reset it at ", + "022-2631": "Nintendo Network ID deleted, or not usable on the current system. If you used System Transfer, the Nintendo Network ID will only work on the target system.", + "022-2633": "Nintendo Network ID temporarily locked due to too many incorrect password attempts. Try again later.", + "022-2634": "Nintendo Network ID is not correctly linked on the system. This can be a result of formatting the SysNAND using System Settings to unlink it from the EmuNAND.\n\n\n\nTinyFormat is recommended for unlinking in the future.", + "022-2812": "System is permanently banned by Nintendo for illegally playing the Pokemon Sun & Moon ROM leak online before release. ", + "022-2815": "System is banned by Nintendo from Miiverse access.", + "032-1820": 'Browser error that asks whether you want to go on to a potentially dangerous website. Can be bypassed by touching "yes".', + "090-0212": "Game is permanently banned from Pokémon Global Link. This is most likely as a result of using altered or illegal save data.", } wii_u_errors = { - '102-2802': 'NNID is permanently banned by Nintendo. ', - '102-2805': 'System is banned from accessing Nintendo eShop. ', - '102-2812': 'System + linked NNID and access to online services are permanently banned by Nintendo. ', - '102-2813': 'System is banned by Nintendo. ', - '102-2814': 'System is permanently banned from online multiplayer in a/multiple game(s) (preferably Splatoon). ', - '102-2815': 'System is banned from accessing the Nintendo eShop. ', - '102-2816': 'System is banned for a/multiple game(s) (preferably Splatoon) for an unknown duration, by attempting to use modified static.pack/+ game files online. ', - '106-0306': 'NNID is temporarily banned from a/multiple games (preferably Splatoon) online multiplayer. ', - '106-0346': 'NNID is permanently banned from a/multiple games (preferably Splatoon) online multiplayer. ', - '115-1009': 'System is permanently banned from Miiverse.', - '121-0902': 'Permissions missing for the action you are trying to perfrom (Miiverse error).', - '150-1031': 'Disc could not be read. Either the disc is dirty, the lens is dirty, or the disc is unsupported (i.e. not a Wii or Wii U game).', - '160-0101': '"Generic error". Can happen when formatting a system with CBHC.', - '160-0102': 'Error in SLC/MLC or USB.', - '160-0103': '"The system memory is corrupted (MLC)."', - '160-0104': '"The system memory is corrupted (SLC)."', - '160-0105': 'USB storage corrupted?', - '199-9999': 'Usually occurs when trying to run an unsigned title without signature patches, or something unknown(?) is corrupted.', + "102-2802": "NNID is permanently banned by Nintendo. ", + "102-2805": "System is banned from accessing Nintendo eShop. ", + "102-2812": "System + linked NNID and access to online services are permanently banned by Nintendo. ", + "102-2813": "System is banned by Nintendo. ", + "102-2814": "System is permanently banned from online multiplayer in a/multiple game(s) (preferably Splatoon). ", + "102-2815": "System is banned from accessing the Nintendo eShop. ", + "102-2816": "System is banned for a/multiple game(s) (preferably Splatoon) for an unknown duration, by attempting to use modified static.pack/+ game files online. ", + "106-0306": "NNID is temporarily banned from a/multiple games (preferably Splatoon) online multiplayer. ", + "106-0346": "NNID is permanently banned from a/multiple games (preferably Splatoon) online multiplayer. ", + "115-1009": "System is permanently banned from Miiverse.", + "121-0902": "Permissions missing for the action you are trying to perfrom (Miiverse error).", + "150-1031": "Disc could not be read. Either the disc is dirty, the lens is dirty, or the disc is unsupported (i.e. not a Wii or Wii U game).", + "160-0101": '"Generic error". Can happen when formatting a system with CBHC.', + "160-0102": "Error in SLC/MLC or USB.", + "160-0103": '"The system memory is corrupted (MLC)."', + "160-0104": '"The system memory is corrupted (SLC)."', + "160-0105": "USB storage corrupted?", + "199-9999": "Usually occurs when trying to run an unsigned title without signature patches, or something unknown(?) is corrupted.", } # 1K (+120) Lines PogChamp diff --git a/helpers/userlogs.py b/helpers/userlogs.py index 275d6a4..bedcdd8 100644 --- a/helpers/userlogs.py +++ b/helpers/userlogs.py @@ -1,11 +1,13 @@ import json import time -userlog_event_types = {"warns": "Warn", - "bans": "Ban", - "kicks": "Kick", - "mutes": "Mute", - "notes": "Note"} +userlog_event_types = { + "warns": "Warn", + "bans": "Ban", + "kicks": "Kick", + "mutes": "Mute", + "notes": "Note", +} def get_userlog(): @@ -22,27 +24,31 @@ def fill_userlog(userid, uname): userlogs = get_userlog() uid = str(userid) if uid not in userlogs: - userlogs[uid] = {"warns": [], - "mutes": [], - "kicks": [], - "bans": [], - "notes": [], - "watch": False, - "name": "n/a"} + userlogs[uid] = { + "warns": [], + "mutes": [], + "kicks": [], + "bans": [], + "notes": [], + "watch": False, + "name": "n/a", + } if uname: userlogs[uid]["name"] = uname return userlogs, uid - - + + def userlog(uid, issuer, reason, event_type, uname: str = ""): userlogs, uid = fill_userlog(uid, 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} + 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) From 287cdd0cc35c3eb052ddf5374f9cffead25abe55 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Tue, 21 Apr 2020 01:17:20 +0300 Subject: [PATCH 40/43] Structure config_template.py further --- config_template.py | 47 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/config_template.py b/config_template.py index 55bee67..fced247 100644 --- a/config_template.py +++ b/config_template.py @@ -75,8 +75,8 @@ staff_role_ids = [ 360138431524765707, # Mod role in ReSwitched 466447265863696394, # Bot management role in ReSwitched 360138163156549632, # Admin role in ReSwitched - 287289529986187266, -] # Wizard role in ReSwitched + 287289529986187266, # Wizard role in ReSwitched +] # Various log channels used to log bot and guild's activity # You can use same channel for multiple log types @@ -129,30 +129,20 @@ hourly_clean_channels = [] # Edited and deletes messages in these channels will be logged spy_channels = general_channels -# Channels and roles where users can pin messages -allowed_pin_channels = [] -allowed_pin_roles = [] - -# Channel to upload text files while editing list items. (They are cleaned up.) -list_files_channel = 0 - -# Channels that are lists that are controlled by the lists cog. -list_channels = [] - # All lower case, no spaces, nothing non-alphanumeric suspect_words = [ - "sx", - "tx", - "reinx", # piracy-enabling cfws - "gomanx", # piracy-enabling cfws - "tinfoil", - "dz", # title managers - "goldleaf", - "lithium", # title managers + "sx", # piracy-enabling cfw + "tx", # piracy-enabling cfw + "reinx", # piracy-enabling cfw + "gomanx", # piracy-enabling cfw + "tinfoil", # title manager + "dz", # title manager + "goldleaf", # potential title manager + "lithium", # title manager "cracked", # older term for pirated games - "xci", - "nsz", -] # "backup" format + "xci", # "backup" format + "nsz", # "backup" format +] # List of words that will be ignored if they match one of the # suspect_words (This is used to remove false positives) @@ -172,6 +162,17 @@ suspect_ignored_words = [ # Used for the pinboard. Leave empty if you don't wish for a gist pinboard. github_oauth_token = "" +# Channels and roles where users can pin messages +allowed_pin_channels = [] +allowed_pin_roles = [] + +# Channel to upload text files while editing list items. (They are cleaned up.) +list_files_channel = 0 + +# == Only if you want to use cogs.lists == +# Channels that are lists that are controlled by the lists cog. +list_channels = [] + # == For cogs.verification == # ReSwitched verification system is rather unique. # You might want to reimplement it. From 8027a5a169efe3c46b88d32115378d3ef7c8c872 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Tue, 21 Apr 2020 01:31:04 +0300 Subject: [PATCH 41/43] links: Move guide text to config too Final change of today, I promise. --- cogs/links.py | 23 ++--------------------- config_template.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/cogs/links.py b/cogs/links.py index 5c25284..226d4ad 100644 --- a/cogs/links.py +++ b/cogs/links.py @@ -36,27 +36,8 @@ class Links(Cog): @commands.command(hidden=True, aliases=["guides", "link"]) async def guide(self, ctx): - """Link to the guide(s)""" - await ctx.send( - "**Generic starter guides:**\n" - "Nintendo Homebrew's Guide: " - "\n" - # "AtlasNX's Guide: " - # "\n" - # "Pegaswitch Guide: " - # "(outdated for anything but Pegaswitch/3.0.0)\n" - "\n**Specific guides:**\n" - "Manually Updating/Downgrading (with HOS): " - "\n" - "Manually Repairing/Downgrading (without HOS): " - "\n" - "How to set up a Homebrew development environment: " - "\n" - "Getting full RAM in homebrew without NSPs: " - "as of Atmosphere 0.8.6, hold R while opening any game.\n" - "Check if a switch is vulnerable to RCM through serial: " - "" - ) + """Link to the guides""" + await ctx.send(config.links_guide_text) @commands.command() async def source(self, ctx): diff --git a/config_template.py b/config_template.py index fced247..219f099 100644 --- a/config_template.py +++ b/config_template.py @@ -173,6 +173,18 @@ list_files_channel = 0 # Channels that are lists that are controlled by the lists cog. list_channels = [] +# == For cogs.links == +links_guide_text = """**Generic starter guides:** +Nintendo Homebrew's Guide: + +**Specific guides:** +Manually Updating/Downgrading (with HOS): +Manually Repairing/Downgrading (without HOS): +How to set up a Homebrew development environment: +Getting full RAM in homebrew without NSPs: As of Atmosphere 0.8.6, hold R while opening any game. +Check if a switch is vulnerable to RCM through serial: +""" + # == For cogs.verification == # ReSwitched verification system is rather unique. # You might want to reimplement it. From f219d4411d203a0c8370585dac87a304183b2684 Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Tue, 21 Apr 2020 01:31:59 +0300 Subject: [PATCH 42/43] config_template: Move optional cog configs to the bottom --- config_template.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/config_template.py b/config_template.py index 219f099..cf8c45d 100644 --- a/config_template.py +++ b/config_template.py @@ -158,21 +158,6 @@ suspect_ignored_words = [ "osx", ] -# == Only if you want to use cogs.pin == -# Used for the pinboard. Leave empty if you don't wish for a gist pinboard. -github_oauth_token = "" - -# Channels and roles where users can pin messages -allowed_pin_channels = [] -allowed_pin_roles = [] - -# Channel to upload text files while editing list items. (They are cleaned up.) -list_files_channel = 0 - -# == Only if you want to use cogs.lists == -# Channels that are lists that are controlled by the lists cog. -list_channels = [] - # == For cogs.links == links_guide_text = """**Generic starter guides:** Nintendo Homebrew's Guide: @@ -297,3 +282,18 @@ welcome_footer = ( # Line to be hidden in rules hidden_term_line = ' • When you have finished reading all of the rules, send a message in this channel that includes the {0} hex digest of your discord "name#discriminator", and bot will automatically grant you access to the other channels. You can find your "name#discriminator" (your username followed by a ‘#’ and four numbers) under the discord channel list.' + +# == Only if you want to use cogs.pin == +# Used for the pinboard. Leave empty if you don't wish for a gist pinboard. +github_oauth_token = "" + +# Channels and roles where users can pin messages +allowed_pin_channels = [] +allowed_pin_roles = [] + +# Channel to upload text files while editing list items. (They are cleaned up.) +list_files_channel = 0 + +# == Only if you want to use cogs.lists == +# Channels that are lists that are controlled by the lists cog. +list_channels = [] From 1282a40986a00e2c50e473c20b69896ea7576bfc Mon Sep 17 00:00:00 2001 From: Ave Ozkal Date: Tue, 21 Apr 2020 09:08:56 +0300 Subject: [PATCH 43/43] verification: fix some bugs --- cogs/verification.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cogs/verification.py b/cogs/verification.py index 1fbb80c..3618d9d 100644 --- a/cogs/verification.py +++ b/cogs/verification.py @@ -68,9 +68,7 @@ class Verification(Cog): # randomize hash_choice on reset self.hash_choice = random.choice( tuple( - hashlib.algorithms_guaranteed - - self.blacklisted_hashes - - {self.hash_choice} + config.welcome_hashes ) ) @@ -174,9 +172,8 @@ class Verification(Cog): # Detect if the user uses the wrong hash algorithm wrong_hash_algos = ( - hashlib.algorithms_guaranteed + config.welcome_hashes - {self.hash_choice} - - self.blacklisted_hashes ) for algo in wrong_hash_algos: for name in itertools.chain(allowed_names, close_names):