diff --git a/README.md b/README.md index 4aa0b4b..3d28419 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. --- @@ -31,107 +31,17 @@ If you're moving from Kurisu/Robocop, and want to preserve your data, you'll wan --- -## TODO +## Contributing -All Robocop features are now supported. +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. -
-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 - -

-
+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 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: diff --git a/Robocop.py b/Robocop.py index c924d4d..39afd7d 100755 --- a/Robocop.py +++ b/Robocop.py @@ -1,5 +1,4 @@ import os -import asyncio import sys import logging import logging.handlers @@ -10,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" @@ -18,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) @@ -38,46 +39,27 @@ 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", +] -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.pin', - 'cogs.invites'] - -bot = commands.Bot(command_prefix=get_prefix, - description=config.bot_description, pm_help=True) +bot = commands.Bot(command_prefix=get_prefix, description=config.bot_description) +bot.help_command = commands.DefaultHelpCommand(dm_help=True) bot.log = log bot.config = config bot.script_name = script_name bot.wanted_jsons = wanted_jsons -if __name__ == '__main__': - for extension in initial_extensions: +if __name__ == "__main__": + 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()) @@ -88,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) @@ -127,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) @@ -140,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}"\ - 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 @@ -192,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/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 diff --git a/assets/.gitignore b/assets/.gitignore new file mode 100644 index 0000000..70994af --- /dev/null +++ b/assets/.gitignore @@ -0,0 +1,2 @@ +*.otf +*.ttf diff --git a/assets/byjcox.png b/assets/byjcox.png new file mode 100644 index 0000000..e6fcd51 Binary files /dev/null and b/assets/byjcox.png differ diff --git a/assets/motherboardlogo.png b/assets/motherboardlogo.png new file mode 100644 index 0000000..d1a07db Binary files /dev/null and b/assets/motherboardlogo.png differ 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 f902302..3ece545 100644 --- a/cogs/basic.py +++ b/cogs/basic.py @@ -32,42 +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!" + ) @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 new file mode 100644 index 0000000..f63545f --- /dev/null +++ b/cogs/imagemanip.py @@ -0,0 +1,87 @@ +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 +import PIL.ImageOps +import PIL.ImageFont +import PIL.ImageDraw + + +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): + """Gives a cox headline""" + mention = ctx.author.mention + + headline = await commands.clean_content(fix_channel_mentions=True).convert( + ctx, headline + ) + + 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 = 800 + 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/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 e86a19e..226d4ad 100644 --- a/cogs/links.py +++ b/cogs/links.py @@ -25,65 +25,55 @@ 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 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: " - "") + """Link to the guides""" + await ctx.send(config.links_guide_text) @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 new file mode 100644 index 0000000..ec6ab97 --- /dev/null +++ b/cogs/lists.py @@ -0,0 +1,335 @@ +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] == "✏" or str(emoji)[0] == "πŸ“" + + def is_delete(self, emoji): + return str(emoji)[0] == "❌" or str(emoji)[0] == "❎" + + def is_recycle(self, emoji): + return str(emoji)[0] == "β™»" + + def is_insert_above(self, emoji): + return str(emoji)[0] == "‴️" or str(emoji)[0] == "⬆" + + def is_insert_below(self, emoji): + return str(emoji)[0] == "‡️" or str(emoji)[0] == "⬇" + + def is_reaction_valid(self, reaction): + allowed_reactions = [ + "✏", + "πŸ“", + "❌", + "❎", + "β™»", + "‴️", + "⬆", + "⬇", + "‡️", + ] + 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 is not 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 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) + + 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: + 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: + 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/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 e5ad47a..8b80752 100644 --- a/cogs/logs.py +++ b/cogs/logs.py @@ -14,26 +14,24 @@ 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 - self.susp_words = ["sx", "tx", "reinx", # piracy-enabling cfws - "tinfoil", "dz", # title managers - "goldleaf", "lithium", # title managers - "cracked", # older term for pirated games - "xci"] # "backup" format - susp_hellgex = "|".join([r"\W*".join(list(word)) for - word in self.susp_words]) + susp_hellgex = "|".join( + [r"\W*".join(list(word)) for 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() + + if member.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) @@ -51,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 = [] @@ -90,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. @@ -129,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) @@ -148,18 +157,21 @@ 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: msg += f"\n- Has invite: https://{invite[0]}" alert = True - for susp_word in self.susp_words: - if susp_word in cleancont and\ - not any(ok_word in cleancont for ok_word in self.ok_words): + 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 + ): msg += f"\n- Contains suspicious word: `{susp_word}`" alert = True @@ -169,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) @@ -184,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) @@ -216,10 +231,13 @@ class Logs(Cog): after_content = after.clean_content.replace("`", "`\u200d") 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_content}``` β†’ ```{after_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_content}``` β†’ ```{after_content}```" + ) # If resulting message is too long, upload to hastebin if len(msg) > 2000: @@ -235,10 +253,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: @@ -250,28 +270,46 @@ 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"\ - 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: + 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: + 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) @@ -286,6 +324,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: @@ -315,9 +357,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__" @@ -325,11 +369,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 02c7f79..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) @@ -79,6 +88,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): @@ -89,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.") @@ -117,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Μ•&Μ‘.Μ· πŸ‘Μ‘") @@ -139,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 e3b6ee4..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,19 +144,24 @@ 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) + await ctx.send(f"πŸ‘’ {safe_name}, πŸ‘.") @commands.guild_only() @commands.bot_has_permissions(ban_members=True) @@ -157,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) @@ -172,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: @@ -182,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) @@ -210,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) @@ -247,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) @@ -275,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]) @@ -292,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]) @@ -315,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) @@ -327,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() @@ -340,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." @@ -384,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() @@ -414,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 df943e4..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,9 +22,8 @@ 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") - await ctx.send(f"{target.mention}: noted!") + userlog(target, ctx.author, note, "notes") + await ctx.send(f"{ctx.author.mention}: noted!") def setup(bot): 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 1b55ec6..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,20 +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 + ) - await ctx.send(f"user = {user}\n" - f"id = {user.id}\n" - f"avatar = {user.avatar_url}\n" - f"bot = {user.bot}\n" - f"created_at = {user.created_at}\n" - f"display_name = {user.display_name}\n" - f"joined_at = {user.joined_at}\n" - f"activities = `{user.activities}`\n" - f"color = {user.colour}\n" - f"top_role = {role}\n", - embed=embed) + 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, + ) 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 fafea82..3618d9d 100644 --- a/cogs/verification.py +++ b/cogs/verification.py @@ -10,115 +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.' -) - -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 @@ -127,19 +22,21 @@ class Verification(Cog): async def do_reset(self, channel, author, limit: int = 100): await channel.purge(limit=limit) - await channel.send(welcome_header) - rules = ['**{}**. {}'.format(i, cleandoc(r)) for i, r in - enumerate(welcome_rules, 1)] + await channel.send(config.welcome_header) + 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' + 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) @@ -163,19 +60,21 @@ 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) 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( + config.welcome_hashes + ) + ) - 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) @@ -187,8 +86,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) @@ -197,8 +98,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) @@ -218,13 +121,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 @@ -232,38 +142,56 @@ 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 = ( + config.welcome_hashes + - {self.hash_choice} + ) 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: @@ -271,7 +199,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 70f4e29..cf8c45d 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 @@ -10,9 +11,43 @@ 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", +] + +# 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 @@ -21,27 +56,27 @@ embed_desc = "Robocop-NG is developed by [Ave](https://github.com/aveao)"\ 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 @@ -55,45 +90,210 @@ 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 # 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 = [] # Edited and deletes messages in these channels will be logged spy_channels = general_channels +# All lower case, no spaces, nothing non-alphanumeric +suspect_words = [ + "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", # "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) +suspect_ignored_words = [ + "excit", + "s/x", + "3dsx", + "psx", + "txt", + "s(x", + "txd", + "t=x", + "osx", +] + +# == 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. +# 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.' + +# == 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 = [] -# 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 + +# == Only if you want to use cogs.lists == +# Channels that are lists that are controlled by the lists cog. +list_channels = [] 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 679a4de..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(): @@ -18,24 +20,35 @@ 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": [], - "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) @@ -44,19 +57,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))