Merge branch 'master' into formatting-edit

This commit is contained in:
Jan 2020-04-21 12:59:33 +02:00 committed by GitHub
commit 9f53bcca4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 2021 additions and 1337 deletions

100
README.md
View file

@ -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. 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.
<details> 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 .`.
<summary>List of added Kurisu/Robocop features</summary>
<p>
- [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!)
</p>
</details>
---
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)
<details>
<summary>Completed features</summary>
<p>
- [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)
</p>
</details>
<details>
<summary>TODO for robocronp</summary>
<p>
- [ ] 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
</p>
</details>
--- ---
## Credits ## 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: I (ave) would like to thank the following, in no particular order:

View file

@ -1,5 +1,4 @@
import os import os
import asyncio
import sys import sys
import logging import logging
import logging.handlers import logging.handlers
@ -10,7 +9,7 @@ import config
import discord import discord
from discord.ext import commands 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" log_file_name = f"{script_name}.log"
@ -18,15 +17,17 @@ log_file_name = f"{script_name}.log"
max_file_size = 1000 * 1000 * 8 max_file_size = 1000 * 1000 * 8
backup_count = 3 backup_count = 3
file_handler = logging.handlers.RotatingFileHandler( 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) stdout_handler = logging.StreamHandler(sys.stdout)
log_format = logging.Formatter( 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) file_handler.setFormatter(log_format)
stdout_handler.setFormatter(log_format) stdout_handler.setFormatter(log_format)
log = logging.getLogger('discord') log = logging.getLogger("discord")
log.setLevel(logging.INFO) log.setLevel(logging.INFO)
log.addHandler(file_handler) log.addHandler(file_handler)
log.addHandler(stdout_handler) log.addHandler(stdout_handler)
@ -38,46 +39,27 @@ def get_prefix(bot, message):
return commands.when_mentioned_or(*prefixes)(bot, message) return commands.when_mentioned_or(*prefixes)(bot, message)
wanted_jsons = ["data/restrictions.json", wanted_jsons = [
"data/restrictions.json",
"data/robocronptab.json", "data/robocronptab.json",
"data/userlog.json", "data/userlog.json",
"data/invites.json"] "data/invites.json",
]
initial_extensions = ['cogs.common', bot = commands.Bot(command_prefix=get_prefix, description=config.bot_description)
'cogs.admin', bot.help_command = commands.DefaultHelpCommand(dm_help=True)
'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.log = log bot.log = log
bot.config = config bot.config = config
bot.script_name = script_name bot.script_name = script_name
bot.wanted_jsons = wanted_jsons bot.wanted_jsons = wanted_jsons
if __name__ == '__main__': if __name__ == "__main__":
for extension in initial_extensions: for cog in config.initial_cogs:
try: try:
bot.load_extension(extension) bot.load_extension(cog)
except Exception as e: except:
log.error(f'Failed to load extension {extension}.') log.error(f"Failed to load cog {cog}.")
log.error(traceback.print_exc()) log.error(traceback.print_exc())
@ -88,31 +70,37 @@ async def on_ready():
bot.app_info = await bot.application_info() bot.app_info = await bot.application_info()
bot.botlog_channel = bot.get_channel(config.botlog_channel) bot.botlog_channel = bot.get_channel(config.botlog_channel)
log.info(f'\nLogged in as: {bot.user.name} - ' log.info(
f'{bot.user.id}\ndpy version: {discord.__version__}\n') f"\nLogged in as: {bot.user.name} - "
f"{bot.user.id}\ndpy version: {discord.__version__}\n"
)
game_name = f"{config.prefixes[0]}help" game_name = f"{config.prefixes[0]}help"
# Send "Robocop has started! x has y members!" # Send "Robocop has started! x has y members!"
guild = bot.botlog_channel.guild guild = bot.botlog_channel.guild
msg = f"{bot.user.name} has started! "\ msg = (
f"{bot.user.name} has started! "
f"{guild.name} has {guild.member_count} members!" f"{guild.name} has {guild.member_count} members!"
)
data_files = [discord.File(fpath) for fpath in wanted_jsons] data_files = [discord.File(fpath) for fpath in wanted_jsons]
await bot.botlog_channel.send(msg, files=data_files) await bot.botlog_channel.send(msg, files=data_files)
activity = discord.Activity(name=game_name, activity = discord.Activity(name=game_name, type=discord.ActivityType.listening)
type=discord.ActivityType.listening)
await bot.change_presence(activity=activity) await bot.change_presence(activity=activity)
@bot.event @bot.event
async def on_command(ctx): async def on_command(ctx):
log_text = f"{ctx.message.author} ({ctx.message.author.id}): "\ log_text = (
f"\"{ctx.message.content}\" " f"{ctx.message.author} ({ctx.message.author.id}): " f'"{ctx.message.content}" '
)
if ctx.guild: # was too long for tertiary if if ctx.guild: # was too long for tertiary if
log_text += f"on \"{ctx.channel.name}\" ({ctx.channel.id}) "\ log_text += (
f"at \"{ctx.guild.name}\" ({ctx.guild.id})" f'on "{ctx.channel.name}" ({ctx.channel.id}) '
f'at "{ctx.guild.name}" ({ctx.guild.id})'
)
else: else:
log_text += f"on DMs ({ctx.channel.id})" log_text += f"on DMs ({ctx.channel.id})"
log.info(log_text) log.info(log_text)
@ -127,9 +115,11 @@ async def on_error(event_method, *args, **kwargs):
async def on_command_error(ctx, error): async def on_command_error(ctx, error):
error_text = str(error) error_text = str(error)
err_msg = f"Error with \"{ctx.message.content}\" from "\ err_msg = (
f"\"{ctx.message.author} ({ctx.message.author.id}) "\ f'Error with "{ctx.message.content}" from '
f'"{ctx.message.author} ({ctx.message.author.id}) '
f"of type {type(error)}: {error_text}" f"of type {type(error)}: {error_text}"
)
log.error(err_msg) log.error(err_msg)
@ -140,45 +130,60 @@ async def on_command_error(ctx, error):
if isinstance(error, commands.NoPrivateMessage): if isinstance(error, commands.NoPrivateMessage):
return await ctx.send("This command doesn't work on DMs.") return await ctx.send("This command doesn't work on DMs.")
elif isinstance(error, commands.MissingPermissions): elif isinstance(error, commands.MissingPermissions):
roles_needed = '\n- '.join(error.missing_perms) roles_needed = "\n- ".join(error.missing_perms)
return await ctx.send(f"{ctx.author.mention}: You don't have the right" return await ctx.send(
f"{ctx.author.mention}: You don't have the right"
" permissions to run this command. You need: " " permissions to run this command. You need: "
f"```- {roles_needed}```") f"```- {roles_needed}```"
)
elif isinstance(error, commands.BotMissingPermissions): elif isinstance(error, commands.BotMissingPermissions):
roles_needed = '\n-'.join(error.missing_perms) roles_needed = "\n-".join(error.missing_perms)
return await ctx.send(f"{ctx.author.mention}: Bot doesn't have " return await ctx.send(
f"{ctx.author.mention}: Bot doesn't have "
"the right permissions to run this command. " "the right permissions to run this command. "
"Please add the following roles: " "Please add the following roles: "
f"```- {roles_needed}```") f"```- {roles_needed}```"
)
elif isinstance(error, commands.CommandOnCooldown): elif isinstance(error, commands.CommandOnCooldown):
return await ctx.send(f"{ctx.author.mention}: You're being " return await ctx.send(
f"{ctx.author.mention}: You're being "
"ratelimited. Try in " "ratelimited. Try in "
f"{error.retry_after:.1f} seconds.") f"{error.retry_after:.1f} seconds."
)
elif isinstance(error, commands.CheckFailure): elif isinstance(error, commands.CheckFailure):
return await ctx.send(f"{ctx.author.mention}: Check failed. " return await ctx.send(
f"{ctx.author.mention}: Check failed. "
"You might not have the right permissions " "You might not have the right permissions "
"to run this command, or you may not be able " "to run this command, or you may not be able "
"to run this command in the current channel.") "to run this command in the current channel."
elif isinstance(error, commands.CommandInvokeError) and\ )
("Cannot send messages to this user" in error_text): elif isinstance(error, commands.CommandInvokeError) and (
return await ctx.send(f"{ctx.author.mention}: I can't DM you.\n" "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 " "You might have me blocked or have DMs "
f"blocked globally or for {ctx.guild.name}.\n" f"blocked globally or for {ctx.guild.name}.\n"
"Please resolve that, then " "Please resolve that, then "
"run the command again.") "run the command again."
)
elif isinstance(error, commands.CommandNotFound): elif isinstance(error, commands.CommandNotFound):
# Nothing to do when command is not found. # Nothing to do when command is not found.
return return
help_text = f"Usage of this command is: ```{ctx.prefix}"\ help_text = (
f"{ctx.command.signature}```\nPlease see `{ctx.prefix}help "\ 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." f"{ctx.command.name}` for more info about this command."
)
if isinstance(error, commands.BadArgument): if isinstance(error, commands.BadArgument):
return await ctx.send(f"{ctx.author.mention}: You gave incorrect " return await ctx.send(
f"arguments. {help_text}") f"{ctx.author.mention}: You gave incorrect " f"arguments. {help_text}"
)
elif isinstance(error, commands.MissingRequiredArgument): elif isinstance(error, commands.MissingRequiredArgument):
return await ctx.send(f"{ctx.author.mention}: You gave incomplete " return await ctx.send(
f"arguments. {help_text}") f"{ctx.author.mention}: You gave incomplete " f"arguments. {help_text}"
)
@bot.event @bot.event
@ -192,13 +197,15 @@ async def on_message(message):
# Ignore messages in newcomers channel, unless it's potentially # Ignore messages in newcomers channel, unless it's potentially
# an allowed command # an allowed command
welcome_allowed = ["reset", "kick", "ban", "warn"] welcome_allowed = ["reset", "kick", "ban", "warn"]
if message.channel.id == config.welcome_channel and\ if message.channel.id == config.welcome_channel and not any(
not any(cmd in message.content for cmd in welcome_allowed): cmd in message.content for cmd in welcome_allowed
):
return return
ctx = await bot.get_context(message) ctx = await bot.get_context(message)
await bot.invoke(ctx) await bot.invoke(ctx)
if not os.path.exists("data"): if not os.path.exists("data"):
os.makedirs("data") os.makedirs("data")

View file

@ -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. 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 ## Supported Versions

2
assets/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.otf
*.ttf

BIN
assets/byjcox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
assets/motherboardlogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -16,7 +16,7 @@ class Admin(Cog):
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_bot_manager) @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): async def _exit(self, ctx):
"""Shuts down the bot, bot manager only.""" """Shuts down the bot, bot manager only."""
await ctx.send(":wave: Goodbye!") await ctx.send(":wave: Goodbye!")
@ -27,8 +27,10 @@ class Admin(Cog):
@commands.command() @commands.command()
async def fetchlog(self, ctx): async def fetchlog(self, ctx):
"""Returns log""" """Returns log"""
await ctx.send("Here's the current log file:", await ctx.send(
file=discord.File(f"{self.bot.script_name}.log")) "Here's the current log file:",
file=discord.File(f"{self.bot.script_name}.log"),
)
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_bot_manager) @commands.check(check_if_bot_manager)
@ -40,32 +42,29 @@ class Admin(Cog):
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_bot_manager) @commands.check(check_if_bot_manager)
@commands.command(name='eval') @commands.command(name="eval")
async def _eval(self, ctx, *, code: str): async def _eval(self, ctx, *, code: str):
"""Evaluates some code, bot manager only.""" """Evaluates some code, bot manager only."""
try: try:
code = code.strip('` ') code = code.strip("` ")
env = { env = {
'bot': self.bot, "bot": self.bot,
'ctx': ctx, "ctx": ctx,
'message': ctx.message, "message": ctx.message,
'server': ctx.guild, "server": ctx.guild,
'guild': ctx.guild, "guild": ctx.guild,
'channel': ctx.message.channel, "channel": ctx.message.channel,
'author': ctx.message.author, "author": ctx.message.author,
# modules # modules
'discord': discord, "discord": discord,
'commands': commands, "commands": commands,
# utilities # utilities
'_get': discord.utils.get, "_get": discord.utils.get,
'_find': discord.utils.find, "_find": discord.utils.find,
# last result # last result
'_': self.last_eval_result, "_": self.last_eval_result,
'_p': self.previous_eval_code, "_p": self.previous_eval_code,
} }
env.update(globals()) env.update(globals())
@ -79,16 +78,15 @@ class Admin(Cog):
self.previous_eval_code = code self.previous_eval_code = code
sliced_message = await self.bot.slice_message(repr(result), sliced_message = await self.bot.slice_message(
prefix="```", repr(result), prefix="```", suffix="```"
suffix="```") )
for msg in sliced_message: for msg in sliced_message:
await ctx.send(msg) await ctx.send(msg)
except: except:
sliced_message = \ sliced_message = await self.bot.slice_message(
await self.bot.slice_message(traceback.format_exc(), traceback.format_exc(), prefix="```", suffix="```"
prefix="```", )
suffix="```")
for msg in sliced_message: for msg in sliced_message:
await ctx.send(msg) await ctx.send(msg)
@ -102,22 +100,25 @@ class Admin(Cog):
@commands.command() @commands.command()
async def pull(self, ctx, auto=False): async def pull(self, ctx, auto=False):
"""Does a git pull, bot manager only.""" """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") git_output = await self.bot.async_call_shell("git pull")
await tmp.edit(content=f"Pull complete. Output: ```{git_output}```") await tmp.edit(content=f"Pull complete. Output: ```{git_output}```")
if auto: 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: for cog in cogs_to_reload:
try: try:
self.bot.unload_extension("cogs." + cog) self.bot.unload_extension("cogs." + cog)
self.bot.load_extension("cogs." + cog) self.bot.load_extension("cogs." + cog)
self.bot.log.info(f'Reloaded ext {cog}') self.bot.log.info(f"Reloaded ext {cog}")
await ctx.send(f':white_check_mark: `{cog}` ' await ctx.send(
'successfully reloaded.') f":white_check_mark: `{cog}` " "successfully reloaded."
)
await self.cog_load_actions(cog) await self.cog_load_actions(cog)
except: except:
await ctx.send(f':x: Cog reloading failed, traceback: ' await ctx.send(
f'```\n{traceback.format_exc()}\n```') f":x: Cog reloading failed, traceback: "
f"```\n{traceback.format_exc()}\n```"
)
return return
@commands.guild_only() @commands.guild_only()
@ -129,11 +130,13 @@ class Admin(Cog):
self.bot.load_extension("cogs." + ext) self.bot.load_extension("cogs." + ext)
await self.cog_load_actions(ext) await self.cog_load_actions(ext)
except: except:
await ctx.send(f':x: Cog loading failed, traceback: ' await ctx.send(
f'```\n{traceback.format_exc()}\n```') f":x: Cog loading failed, traceback: "
f"```\n{traceback.format_exc()}\n```"
)
return return
self.bot.log.info(f'Loaded ext {ext}') self.bot.log.info(f"Loaded ext {ext}")
await ctx.send(f':white_check_mark: `{ext}` successfully loaded.') await ctx.send(f":white_check_mark: `{ext}` successfully loaded.")
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_bot_manager) @commands.check(check_if_bot_manager)
@ -141,8 +144,8 @@ class Admin(Cog):
async def unload(self, ctx, ext: str): async def unload(self, ctx, ext: str):
"""Unloads a cog, bot manager only.""" """Unloads a cog, bot manager only."""
self.bot.unload_extension("cogs." + ext) self.bot.unload_extension("cogs." + ext)
self.bot.log.info(f'Unloaded ext {ext}') self.bot.log.info(f"Unloaded ext {ext}")
await ctx.send(f':white_check_mark: `{ext}` successfully unloaded.') await ctx.send(f":white_check_mark: `{ext}` successfully unloaded.")
@commands.check(check_if_bot_manager) @commands.check(check_if_bot_manager)
@commands.command() @commands.command()
@ -158,11 +161,13 @@ class Admin(Cog):
self.bot.load_extension("cogs." + ext) self.bot.load_extension("cogs." + ext)
await self.cog_load_actions(ext) await self.cog_load_actions(ext)
except: except:
await ctx.send(f':x: Cog reloading failed, traceback: ' await ctx.send(
f'```\n{traceback.format_exc()}\n```') f":x: Cog reloading failed, traceback: "
f"```\n{traceback.format_exc()}\n```"
)
return return
self.bot.log.info(f'Reloaded ext {ext}') self.bot.log.info(f"Reloaded ext {ext}")
await ctx.send(f':white_check_mark: `{ext}` successfully reloaded.') await ctx.send(f":white_check_mark: `{ext}` successfully reloaded.")
def setup(bot): def setup(bot):

View file

@ -32,42 +32,51 @@ class Basic(Cog):
async def communitycount(self, ctx): async def communitycount(self, ctx):
"""Prints the community member count of the server.""" """Prints the community member count of the server."""
community = ctx.guild.get_role(config.named_roles["community"]) community = ctx.guild.get_role(config.named_roles["community"])
await ctx.send(f"{ctx.guild.name} has " await ctx.send(
f"{len(community.members)} community members!") 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.guild_only()
@commands.command() @commands.command()
async def membercount(self, ctx): async def membercount(self, ctx):
"""Prints the member count of the server.""" """Prints the member count of the server."""
await ctx.send(f"{ctx.guild.name} has " await ctx.send(f"{ctx.guild.name} has " f"{ctx.guild.member_count} members!")
f"{ctx.guild.member_count} members!")
@commands.command(aliases=["robocopng", "robocop-ng"]) @commands.command(aliases=["robocopng", "robocop-ng"])
async def robocop(self, ctx): async def robocop(self, ctx):
"""Shows a quick embed with bot info.""" """Shows a quick embed with bot info."""
embed = discord.Embed(title="Robocop-NG", embed = discord.Embed(
url=config.source_url, title="Robocop-NG", url=config.source_url, description=config.embed_desc
description=config.embed_desc) )
embed.set_thumbnail(url=self.bot.user.avatar_url) embed.set_thumbnail(url=self.bot.user.avatar_url)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(aliases=['p']) @commands.command(aliases=["p"])
async def ping(self, ctx): async def ping(self, ctx):
"""Shows ping values to discord. """Shows ping values to discord.
RTT = Round-trip time, time taken to send a message to discord RTT = Round-trip time, time taken to send a message to discord
GW = Gateway Ping""" GW = Gateway Ping"""
before = time.monotonic() before = time.monotonic()
tmp = await ctx.send('Calculating ping...') tmp = await ctx.send("Calculating ping...")
after = time.monotonic() after = time.monotonic()
rtt_ms = (after - before) * 1000 rtt_ms = (after - before) * 1000
gw_ms = self.bot.latency * 1000 gw_ms = self.bot.latency * 1000
message_text = f":ping_pong:\n"\ message_text = (
f"rtt: `{rtt_ms:.1f}ms`\n"\ f":ping_pong:\n" f"rtt: `{rtt_ms:.1f}ms`\n" f"gw: `{gw_ms:.1f}ms`"
f"gw: `{gw_ms:.1f}ms`" )
self.bot.log.info(message_text) self.bot.log.info(message_text)
await tmp.edit(content=message_text) await tmp.edit(content=message_text)

View file

@ -31,9 +31,14 @@ class Common(Cog):
res_timestamp = math.floor(time.mktime(time_struct)) res_timestamp = math.floor(time.mktime(time_struct))
return res_timestamp return res_timestamp
def get_relative_timestamp(self, time_from=None, time_to=None, def get_relative_timestamp(
humanized=False, include_from=False, self,
include_to=False): 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 # Setting default value to utcnow() makes it show time from cog load
# which is not what we want # which is not what we want
if not time_from: if not time_from:
@ -43,17 +48,19 @@ class Common(Cog):
if humanized: if humanized:
humanized_string = humanize.naturaltime(time_from - time_to) humanized_string = humanize.naturaltime(time_from - time_to)
if include_from and include_to: if include_from and include_to:
str_with_from_and_to = f"{humanized_string} "\ str_with_from_and_to = (
f"({str(time_from).split('.')[0]} "\ f"{humanized_string} "
f"({str(time_from).split('.')[0]} "
f"- {str(time_to).split('.')[0]})" f"- {str(time_to).split('.')[0]})"
)
return str_with_from_and_to return str_with_from_and_to
elif include_from: elif include_from:
str_with_from = f"{humanized_string} "\ str_with_from = (
f"({str(time_from).split('.')[0]})" f"{humanized_string} " f"({str(time_from).split('.')[0]})"
)
return str_with_from return str_with_from
elif include_to: elif include_to:
str_with_to = f"{humanized_string} "\ str_with_to = f"{humanized_string} " f"({str(time_to).split('.')[0]})"
f"({str(time_to).split('.')[0]})"
return str_with_to return str_with_to
return humanized_string return humanized_string
else: else:
@ -61,8 +68,7 @@ class Common(Cog):
epoch_from = (time_from - epoch).total_seconds() epoch_from = (time_from - epoch).total_seconds()
epoch_to = (time_to - epoch).total_seconds() epoch_to = (time_to - epoch).total_seconds()
second_diff = epoch_to - epoch_from second_diff = epoch_to - epoch_from
result_string = str(datetime.timedelta( result_string = str(datetime.timedelta(seconds=second_diff)).split(".")[0]
seconds=second_diff)).split('.')[0]
return result_string return result_string
async def aioget(self, url): async def aioget(self, url):
@ -73,11 +79,12 @@ class Common(Cog):
self.bot.log.info(f"Data from {url}: {text_data}") self.bot.log.info(f"Data from {url}: {text_data}")
return text_data return text_data
else: else:
self.bot.log.error(f"HTTP Error {data.status} " self.bot.log.error(f"HTTP Error {data.status} " "while getting {url}")
"while getting {url}")
except: except:
self.bot.log.error(f"Error while getting {url} " self.bot.log.error(
f"on aiogetbytes: {traceback.format_exc()}") f"Error while getting {url} "
f"on aiogetbytes: {traceback.format_exc()}"
)
async def aiogetbytes(self, url): async def aiogetbytes(self, url):
try: try:
@ -87,11 +94,12 @@ class Common(Cog):
self.bot.log.debug(f"Data from {url}: {byte_data}") self.bot.log.debug(f"Data from {url}: {byte_data}")
return byte_data return byte_data
else: else:
self.bot.log.error(f"HTTP Error {data.status} " self.bot.log.error(f"HTTP Error {data.status} " "while getting {url}")
"while getting {url}")
except: except:
self.bot.log.error(f"Error while getting {url} " self.bot.log.error(
f"on aiogetbytes: {traceback.format_exc()}") f"Error while getting {url} "
f"on aiogetbytes: {traceback.format_exc()}"
)
async def aiojson(self, url): async def aiojson(self, url):
try: try:
@ -99,18 +107,19 @@ class Common(Cog):
if data.status == 200: if data.status == 200:
text_data = await data.text() text_data = await data.text()
self.bot.log.info(f"Data from {url}: {text_data}") 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) return await data.json(content_type=content_type)
else: else:
self.bot.log.error(f"HTTP Error {data.status} " self.bot.log.error(f"HTTP Error {data.status} " "while getting {url}")
"while getting {url}")
except: except:
self.bot.log.error(f"Error while getting {url} " self.bot.log.error(
f"on aiogetbytes: {traceback.format_exc()}") f"Error while getting {url} "
f"on aiogetbytes: {traceback.format_exc()}"
)
def hex_to_int(self, color_hex: str): def hex_to_int(self, color_hex: str):
"""Turns a given hex color into an integer""" """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): def escape_message(self, text: str):
"""Escapes unfun stuff from messages""" """Escapes unfun stuff from messages"""
@ -130,10 +139,12 @@ class Common(Cog):
"""Slices a message into multiple messages""" """Slices a message into multiple messages"""
if len(text) > size * self.max_split_length: if len(text) > size * self.max_split_length:
haste_url = await self.haste(text) haste_url = await self.haste(text)
return [f"Message is too long ({len(text)} > " return [
f"Message is too long ({len(text)} > "
f"{size * self.max_split_length} " f"{size * self.max_split_length} "
f"({size} * {self.max_split_length}))" f"({size} * {self.max_split_length}))"
f", go to haste: <{haste_url}>"] f", go to haste: <{haste_url}>"
]
reply_list = [] reply_list = []
size_wo_fix = size - len(prefix) - len(suffix) size_wo_fix = size - len(prefix) - len(suffix)
while len(text) > size_wo_fix: while len(text) > size_wo_fix:
@ -142,28 +153,28 @@ class Common(Cog):
reply_list.append(f"{prefix}{text}{suffix}") reply_list.append(f"{prefix}{text}{suffix}")
return reply_list return reply_list
async def haste(self, text, instance='https://mystb.in/'): async def haste(self, text, instance="https://mystb.in/"):
response = await self.bot.aiosession.post(f"{instance}documents", response = await self.bot.aiosession.post(f"{instance}documents", data=text)
data=text)
if response.status == 200: if response.status == 200:
result_json = await response.json() result_json = await response.json()
return f"{instance}{result_json['key']}" return f"{instance}{result_json['key']}"
else: else:
return f"Error {response.status}: {response.text}" return f"Error {response.status}: {response.text}"
async def async_call_shell(self, shell_command: str, async def async_call_shell(
inc_stdout=True, inc_stderr=True): self, shell_command: str, inc_stdout=True, inc_stderr=True
):
pipe = asyncio.subprocess.PIPE pipe = asyncio.subprocess.PIPE
proc = await asyncio.create_subprocess_shell(str(shell_command), proc = await asyncio.create_subprocess_shell(
stdout=pipe, str(shell_command), stdout=pipe, stderr=pipe
stderr=pipe) )
if not (inc_stdout or inc_stderr): if not (inc_stdout or inc_stderr):
return "??? you set both stdout and stderr to False????" return "??? you set both stdout and stderr to False????"
proc_result = await proc.communicate() proc_result = await proc.communicate()
stdout_str = proc_result[0].decode('utf-8').strip() stdout_str = proc_result[0].decode("utf-8").strip()
stderr_str = proc_result[1].decode('utf-8').strip() stderr_str = proc_result[1].decode("utf-8").strip()
if inc_stdout and not inc_stderr: if inc_stdout and not inc_stderr:
return stdout_str return stdout_str
@ -171,8 +182,7 @@ class Common(Cog):
return stderr_str return stderr_str
if stdout_str and stderr_str: if stdout_str and stderr_str:
return f"stdout:\n\n{stdout_str}\n\n"\ return f"stdout:\n\n{stdout_str}\n\n" f"======\n\nstderr:\n\n{stderr_str}"
f"======\n\nstderr:\n\n{stderr_str}"
elif stdout_str: elif stdout_str:
return f"stdout:\n\n{stdout_str}" return f"stdout:\n\n{stdout_str}"
elif stderr_str: elif stderr_str:

View file

@ -5,17 +5,20 @@ from discord.ext import commands
from discord.ext.commands import Cog from discord.ext.commands import Cog
from helpers.errcodes import * from helpers.errcodes import *
class Err(Cog): class Err(Cog):
"""Everything related to Nintendo 3DS, Wii U and Switch error codes""" """Everything related to Nintendo 3DS, Wii U and Switch error codes"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.dds_re = re.compile(r'0\d{2}\-\d{4}') self.dds_re = re.compile(r"0\d{2}\-\d{4}")
self.wiiu_re = re.compile(r'1\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.switch_re = re.compile(r"2\d{3}\-\d{4}")
self.no_err_desc = "It seems like your error code is unknown. "\ self.no_err_desc = (
"You can check on Switchbrew for your error code at "\ "It seems like your error code is unknown. "
"You can check on Switchbrew for your error code at "
"<https://switchbrew.org/wiki/Error_codes>" "<https://switchbrew.org/wiki/Error_codes>"
)
self.rickroll = "https://www.youtube.com/watch?v=z3ZiVn5L9vM" self.rickroll = "https://www.youtube.com/watch?v=z3ZiVn5L9vM"
@commands.command(aliases=["3dserr", "3err", "dserr"]) @commands.command(aliases=["3dserr", "3err", "dserr"])
@ -28,9 +31,9 @@ class Err(Cog):
else: else:
err_description = self.no_err_desc err_description = self.no_err_desc
# Make a nice Embed out of it # Make a nice Embed out of it
embed = discord.Embed(title=err, embed = discord.Embed(
url=self.rickroll, title=err, url=self.rickroll, description=err_description
description=err_description) )
embed.set_footer(text="Console: 3DS") embed.set_footer(text="Console: 3DS")
# Send message, crazy # Send message, crazy
@ -47,8 +50,7 @@ class Err(Cog):
level = (rc >> 27) & 0x1F level = (rc >> 27) & 0x1F
embed = discord.Embed(title=f"0x{rc:X}") embed = discord.Embed(title=f"0x{rc:X}")
embed.add_field(name="Module", value=dds_modules.get(mod, mod)) embed.add_field(name="Module", value=dds_modules.get(mod, mod))
embed.add_field(name="Description", embed.add_field(name="Description", value=dds_descriptions.get(desc, desc))
value=dds_descriptions.get(desc, desc))
embed.add_field(name="Summary", value=dds_summaries.get(summ, summ)) embed.add_field(name="Summary", value=dds_summaries.get(summ, summ))
embed.add_field(name="Level", value=dds_levels.get(level, level)) embed.add_field(name="Level", value=dds_levels.get(level, level))
embed.set_footer(text="Console: 3DS") embed.set_footer(text="Console: 3DS")
@ -56,8 +58,10 @@ class Err(Cog):
await ctx.send(embed=embed) await ctx.send(embed=embed)
return return
else: else:
await ctx.send("Unknown Format - This is either " await ctx.send(
"no error code or you made some mistake!") "Unknown Format - This is either "
"no error code or you made some mistake!"
)
@commands.command(aliases=["wiiuserr", "uerr", "wuerr", "mochaerr"]) @commands.command(aliases=["wiiuserr", "uerr", "wuerr", "mochaerr"])
async def wiiuerr(self, ctx, err: str): async def wiiuerr(self, ctx, err: str):
@ -72,9 +76,9 @@ class Err(Cog):
err_description = self.no_err_desc err_description = self.no_err_desc
# Make a nice Embed out of it # Make a nice Embed out of it
embed = discord.Embed(title=err, embed = discord.Embed(
url=self.rickroll, title=err, url=self.rickroll, description=err_description
description=err_description) )
embed.set_footer(text="Console: Wii U") embed.set_footer(text="Console: Wii U")
embed.add_field(name="Module", value=module, inline=True) embed.add_field(name="Module", value=module, inline=True)
embed.add_field(name="Description", value=desc, inline=True) embed.add_field(name="Description", value=desc, inline=True)
@ -82,8 +86,10 @@ class Err(Cog):
# Send message, crazy # Send message, crazy
await ctx.send(embed=embed) await ctx.send(embed=embed)
else: else:
await ctx.send("Unknown Format - This is either " await ctx.send(
"no error code or you made some mistake!") "Unknown Format - This is either "
"no error code or you made some mistake!"
)
@commands.command(aliases=["nxerr", "serr"]) @commands.command(aliases=["nxerr", "serr"])
async def err(self, ctx, err: str): async def err(self, ctx, err: str):
@ -125,12 +131,14 @@ class Err(Cog):
err_description = errcode_range[2] err_description = errcode_range[2]
# Make a nice Embed out of it # Make a nice Embed out of it
embed = discord.Embed(title=f"{str_errcode} / {hex(errcode)}", embed = discord.Embed(
title=f"{str_errcode} / {hex(errcode)}",
url=self.rickroll, url=self.rickroll,
description=err_description) description=err_description,
embed.add_field(name="Module", )
value=f"{err_module} ({module})", embed.add_field(
inline=True) name="Module", value=f"{err_module} ({module})", inline=True
)
embed.add_field(name="Description", value=desc, inline=True) embed.add_field(name="Description", value=desc, inline=True)
if "ban" in err_description: if "ban" in err_description:
@ -145,17 +153,17 @@ class Err(Cog):
elif err in switch_game_err: elif err in switch_game_err:
game, desc = switch_game_err[err].split(":") game, desc = switch_game_err[err].split(":")
embed = discord.Embed(title=err, embed = discord.Embed(title=err, url=self.rickroll, description=desc)
url=self.rickroll,
description=desc)
embed.set_footer(text="Console: Switch") embed.set_footer(text="Console: Switch")
embed.add_field(name="Game", value=game, inline=True) embed.add_field(name="Game", value=game, inline=True)
await ctx.send(embed=embed) await ctx.send(embed=embed)
else: else:
await ctx.send("Unknown Format - This is either " await ctx.send(
"no error code or you made some mistake!") "Unknown Format - This is either "
"no error code or you made some mistake!"
)
@commands.command(aliases=["e2h"]) @commands.command(aliases=["e2h"])
async def err2hex(self, ctx, err: str): async def err2hex(self, ctx, err: str):
@ -167,8 +175,9 @@ class Err(Cog):
errcode = (desc << 9) + module errcode = (desc << 9) + module
await ctx.send(hex(errcode)) await ctx.send(hex(errcode))
else: else:
await ctx.send("This doesn't follow the typical" await ctx.send(
" Nintendo Switch 2XXX-XXXX format!") "This doesn't follow the typical" " Nintendo Switch 2XXX-XXXX format!"
)
@commands.command(aliases=["h2e"]) @commands.command(aliases=["h2e"])
async def hex2err(self, ctx, err: str): async def hex2err(self, ctx, err: str):

87
cogs/imagemanip.py Normal file
View file

@ -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))

View file

@ -4,6 +4,7 @@ from helpers.checks import check_if_collaborator
import config import config
import json import json
class Invites(Cog): class Invites(Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@ -15,8 +16,9 @@ class Invites(Cog):
welcome_channel = self.bot.get_channel(config.welcome_channel) welcome_channel = self.bot.get_channel(config.welcome_channel)
author = ctx.message.author author = ctx.message.author
reason = f"Created by {str(author)} ({author.id})" reason = f"Created by {str(author)} ({author.id})"
invite = await welcome_channel.create_invite(max_age = 0, invite = await welcome_channel.create_invite(
max_uses = 1, temporary = True, unique = True, reason = reason) max_age=0, max_uses=1, temporary=True, unique=True, reason=reason
)
with open("data/invites.json", "r") as f: with open("data/invites.json", "r") as f:
invites = json.load(f) invites = json.load(f)
@ -25,7 +27,7 @@ class Invites(Cog):
"uses": 0, "uses": 0,
"url": invite.url, "url": invite.url,
"max_uses": 1, "max_uses": 1,
"code": invite.code "code": invite.code,
} }
with open("data/invites.json", "w") as f: with open("data/invites.json", "w") as f:
@ -35,8 +37,10 @@ class Invites(Cog):
try: try:
await ctx.author.send(f"Created single-use invite {invite.url}") await ctx.author.send(f"Created single-use invite {invite.url}")
except discord.errors.Forbidden: except discord.errors.Forbidden:
await ctx.send(f"{ctx.author.mention} I could not send you the \ await ctx.send(
invite. Send me a DM so I can reply to you.") f"{ctx.author.mention} I could not send you the \
invite. Send me a DM so I can reply to you."
)
def setup(bot): def setup(bot):

View file

@ -1,6 +1,7 @@
from discord.ext import commands from discord.ext import commands
from discord.ext.commands import Cog from discord.ext.commands import Cog
class Legacy(Cog): class Legacy(Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@ -8,22 +9,28 @@ class Legacy(Cog):
@commands.command(hidden=True, aliases=["removehacker"]) @commands.command(hidden=True, aliases=["removehacker"])
async def probate(self, ctx): async def probate(self, ctx):
"""Use .revoke <user> <role>""" """Use .revoke <user> <role>"""
await ctx.send("This command was replaced with `.revoke <user> <role>`" await ctx.send(
" on Robocop-NG, please use that instead.") "This command was replaced with `.revoke <user> <role>`"
" on Robocop-NG, please use that instead."
)
@commands.command(hidden=True) @commands.command(hidden=True)
async def softlock(self, ctx): async def softlock(self, ctx):
"""Use .lock True""" """Use .lock True"""
await ctx.send("This command was replaced with `.lock True`" await ctx.send(
"This command was replaced with `.lock True`"
" on Robocop-NG, please use that instead.\n" " on Robocop-NG, please use that instead.\n"
"Also... good luck, and sorry for taking your time. " "Also... good luck, and sorry for taking your time. "
"Lockdown rarely means anything good.") "Lockdown rarely means anything good."
)
@commands.command(hidden=True, aliases=["addhacker"]) @commands.command(hidden=True, aliases=["addhacker"])
async def unprobate(self, ctx): async def unprobate(self, ctx):
"""Use .approve <user> <role>""" """Use .approve <user> <role>"""
await ctx.send("This command was replaced with `.approve <user> <role>`" await ctx.send(
" on Robocop-NG, please use that instead.") "This command was replaced with `.approve <user> <role>`"
" on Robocop-NG, please use that instead."
)
def setup(bot): def setup(bot):

View file

@ -25,56 +25,45 @@ class Links(Cog):
@commands.command(hidden=True, aliases=["xyproblem"]) @commands.command(hidden=True, aliases=["xyproblem"])
async def xy(self, ctx): async def xy(self, ctx):
"""Link to the "What is the XY problem?" post from SE""" """Link to the "What is the XY problem?" post from SE"""
await ctx.send("<https://meta.stackexchange.com/q/66377/285481>\n\n" await ctx.send(
"<https://meta.stackexchange.com/q/66377/285481>\n\n"
"TL;DR: It's asking about your attempted solution " "TL;DR: It's asking about your attempted solution "
"rather than your actual problem.\n" "rather than your actual problem.\n"
"It's perfectly okay to want to learn about a " "It's perfectly okay to want to learn about a "
"solution, but please be clear about your intentions " "solution, but please be clear about your intentions "
"if you're not actually trying to solve a problem.") "if you're not actually trying to solve a problem."
)
@commands.command(hidden=True, aliases=["guides", "link"]) @commands.command(hidden=True, aliases=["guides", "link"])
async def guide(self, ctx): async def guide(self, ctx):
"""Link to the guide(s)""" """Link to the guides"""
await ctx.send("**Generic starter guides:**\n" await ctx.send(config.links_guide_text)
"Nintendo Homebrew's Guide: "
"<https://nh-server.github.io/switch-guide/>\n"
"AtlasNX's Guide: "
"<https://switch.homebrew.guide>\n"
# "Pegaswitch Guide: <https://switch.hacks.guide/> "
# "(outdated for anything but Pegaswitch/3.0.0)\n"
"\n**Specific guides:**\n"
"Manually Updating/Downgrading (with HOS): "
"<https://switch.homebrew.guide/usingcfw/manualupgrade>\n"
"Manually Repairing/Downgrading (without HOS): "
"<https://switch.homebrew.guide/usingcfw/manualchoiupgrade>\n"
"How to get started developing Homebrew: "
"<https://gbatemp.net/threads/"
"tutorial-switch-homebrew-development.507284/>\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: "
"<https://akdm.github.io/ssnc/checker/>")
@commands.command() @commands.command()
async def source(self, ctx): async def source(self, ctx):
"""Gives link to source code.""" """Gives link to source code."""
await ctx.send(f"You can find my source at {config.source_url}. " await ctx.send(
"Serious PRs and issues welcome!") f"You can find my source at {config.source_url}. "
"Serious PRs and issues welcome!"
)
@commands.command() @commands.command()
async def rules(self, ctx, *, targetuser: discord.Member = None): async def rules(self, ctx, *, targetuser: discord.Member = None):
"""Post a link to the Rules""" """Post a link to the Rules"""
if not targetuser: if not targetuser:
targetuser = ctx.author targetuser = ctx.author
await ctx.send(f"{targetuser.mention}: A link to the rules " await ctx.send(
f"can be found here: {config.rules_url}") f"{targetuser.mention}: A link to the rules "
f"can be found here: {config.rules_url}"
)
@commands.command() @commands.command()
async def community(self, ctx, *, targetuser: discord.Member = None): async def community(self, ctx, *, targetuser: discord.Member = None):
"""Post a link to the community section of the rules""" """Post a link to the community section of the rules"""
if not targetuser: if not targetuser:
targetuser = ctx.author targetuser = ctx.author
await ctx.send(f"{targetuser.mention}: " await ctx.send(
f"{targetuser.mention}: "
"https://reswitched.team/discord/#member-roles-breakdown" "https://reswitched.team/discord/#member-roles-breakdown"
"\n\n" "\n\n"
"Community role allows access to the set of channels " "Community role allows access to the set of channels "
@ -83,7 +72,8 @@ class Links(Cog):
"\n\n" "\n\n"
"What you need to get the role is to be around, " "What you need to get the role is to be around, "
"be helpful and nice to people and " "be helpful and nice to people and "
"show an understanding of rules.") "show an understanding of rules."
)
def setup(bot): def setup(bot):

335
cogs/lists.py Normal file
View file

@ -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))

View file

@ -4,19 +4,21 @@ import config
import discord import discord
from helpers.checks import check_if_staff from helpers.checks import check_if_staff
class Lockdown(Cog): class Lockdown(Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
async def set_sendmessage(self, channel: discord.TextChannel, async def set_sendmessage(
role, allow_send, issuer): self, channel: discord.TextChannel, role, allow_send, issuer
):
try: try:
roleobj = channel.guild.get_role(role) roleobj = channel.guild.get_role(role)
overrides = channel.overwrites_for(roleobj) overrides = channel.overwrites_for(roleobj)
overrides.send_messages = allow_send overrides.send_messages = allow_send
await channel.set_permissions(roleobj, await channel.set_permissions(
overwrite=overrides, roleobj, overwrite=overrides, reason=str(issuer)
reason=str(issuer)) )
except: except:
pass pass
@ -27,8 +29,7 @@ class Lockdown(Cog):
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command() @commands.command()
async def lock(self, ctx, channel: discord.TextChannel = None, async def lock(self, ctx, channel: discord.TextChannel = None, soft: bool = False):
soft: bool = False):
"""Prevents people from speaking in a channel, staff only. """Prevents people from speaking in a channel, staff only.
Defaults to current channel.""" Defaults to current channel."""
@ -50,14 +51,18 @@ class Lockdown(Cog):
public_msg = "🔒 Channel locked down. " public_msg = "🔒 Channel locked down. "
if not soft: if not soft:
public_msg += "Only staff members may speak. "\ public_msg += (
"Do not bring the topic to other channels or risk "\ "Only staff members may speak. "
"Do not bring the topic to other channels or risk "
"disciplinary actions." "disciplinary actions."
)
await ctx.send(public_msg) await ctx.send(public_msg)
safe_name = await commands.clean_content().convert(ctx, str(ctx.author)) safe_name = await commands.clean_content().convert(ctx, str(ctx.author))
msg = f"🔒 **Lockdown**: {ctx.channel.mention} by {ctx.author.mention} "\ msg = (
f"🔒 **Lockdown**: {ctx.channel.mention} by {ctx.author.mention} "
f"| {safe_name}" f"| {safe_name}"
)
await log_channel.send(msg) await log_channel.send(msg)
@commands.guild_only() @commands.guild_only()
@ -83,8 +88,10 @@ class Lockdown(Cog):
safe_name = await commands.clean_content().convert(ctx, str(ctx.author)) safe_name = await commands.clean_content().convert(ctx, str(ctx.author))
await ctx.send("🔓 Channel unlocked.") await ctx.send("🔓 Channel unlocked.")
msg = f"🔓 **Unlock**: {ctx.channel.mention} by {ctx.author.mention} "\ msg = (
f"🔓 **Unlock**: {ctx.channel.mention} by {ctx.author.mention} "
f"| {safe_name}" f"| {safe_name}"
)
await log_channel.send(msg) await log_channel.send(msg)

View file

@ -14,26 +14,24 @@ class Logs(Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.invite_re = re.compile(r"((discord\.gg|discordapp\.com/" self.invite_re = re.compile(
r"+invite)/+[a-zA-Z0-9-]+)", r"((discord\.gg|discordapp\.com/" r"+invite)/+[a-zA-Z0-9-]+)", re.IGNORECASE
re.IGNORECASE) )
self.name_re = re.compile(r"[a-zA-Z0-9].*") 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 # All lower case, no spaces, nothing non-alphanumeric
self.susp_words = ["sx", "tx", "reinx", # piracy-enabling cfws susp_hellgex = "|".join(
"tinfoil", "dz", # title managers [r"\W*".join(list(word)) for word in config.suspect_words]
"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])
self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE) self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE)
self.ok_words = []
@Cog.listener() @Cog.listener()
async def on_member_join(self, member): async def on_member_join(self, member):
await self.bot.wait_until_ready() 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) log_channel = self.bot.get_channel(config.log_channel)
# We use this a lot, might as well get it once # We use this a lot, might as well get it once
escaped_name = self.bot.escape_message(member) escaped_name = self.bot.escape_message(member)
@ -51,7 +49,7 @@ class Logs(Cog):
"uses": 0, "uses": 0,
"url": invite.url, "url": invite.url,
"max_uses": invite.max_uses, "max_uses": invite.max_uses,
"code": invite.code "code": invite.code,
} }
probable_invites_used = [] probable_invites_used = []
@ -90,31 +88,39 @@ class Logs(Cog):
age = member.joined_at - member.created_at age = member.joined_at - member.created_at
if age < config.min_age: if age < config.min_age:
try: try:
await member.send(f"Your account is too new to " await member.send(
f"Your account is too new to "
f"join {member.guild.name}." f"join {member.guild.name}."
" Please try again later.") " Please try again later."
)
sent = True sent = True
except discord.errors.Forbidden: except discord.errors.Forbidden:
sent = False sent = False
await member.kick(reason="Too new") await member.kick(reason="Too new")
msg = f"🚨 **Account too new**: {member.mention} | "\ msg = (
f"{escaped_name}\n"\ f"🚨 **Account too new**: {member.mention} | "
f"🗓 __Creation__: {member.created_at}\n"\ f"{escaped_name}\n"
f"🕓 Account age: {age}\n"\ f"🗓 __Creation__: {member.created_at}\n"
f"✉ Joined with: {invite_used}\n"\ f"🕓 Account age: {age}\n"
f"✉ Joined with: {invite_used}\n"
f"🏷 __User ID__: {member.id}" f"🏷 __User ID__: {member.id}"
)
if not sent: if not sent:
msg += "\nThe user has disabled direct messages, "\ msg += (
"\nThe user has disabled direct messages, "
"so the reason was not sent." "so the reason was not sent."
)
await log_channel.send(msg) await log_channel.send(msg)
return return
msg = f"✅ **Join**: {member.mention} | "\ msg = (
f"{escaped_name}\n"\ f"✅ **Join**: {member.mention} | "
f"🗓 __Creation__: {member.created_at}\n"\ f"{escaped_name}\n"
f"🕓 Account age: {age}\n"\ f"🗓 __Creation__: {member.created_at}\n"
f"✉ Joined with: {invite_used}\n"\ f"🕓 Account age: {age}\n"
f"✉ Joined with: {invite_used}\n"
f"🏷 __User ID__: {member.id}" f"🏷 __User ID__: {member.id}"
)
# Handles user restrictions # Handles user restrictions
# Basically, gives back muted role to users that leave with it. # 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: if len(warns[str(member.id)]["warns"]) == 0:
await log_channel.send(msg) await log_channel.send(msg)
else: else:
embed = discord.Embed(color=discord.Color.dark_red(), embed = discord.Embed(
title=f"Warns for {escaped_name}") color=discord.Color.dark_red(), title=f"Warns for {escaped_name}"
)
embed.set_thumbnail(url=member.avatar_url) embed.set_thumbnail(url=member.avatar_url)
for idx, warn in enumerate(warns[str(member.id)]["warns"]): for idx, warn in enumerate(warns[str(member.id)]["warns"]):
embed.add_field(name=f"{idx + 1}: {warn['timestamp']}", embed.add_field(
name=f"{idx + 1}: {warn['timestamp']}",
value=f"Issuer: {warn['issuer_name']}" value=f"Issuer: {warn['issuer_name']}"
f"\nReason: {warn['reason']}") f"\nReason: {warn['reason']}",
)
await log_channel.send(msg, embed=embed) await log_channel.send(msg, embed=embed)
except KeyError: # if the user is not in the file except KeyError: # if the user is not in the file
await log_channel.send(msg) await log_channel.send(msg)
@ -148,18 +157,21 @@ class Logs(Cog):
return return
alert = False alert = False
cleancont = self.clean_re.sub('', message.content).lower() cleancont = self.clean_re.sub("", message.content).lower()
msg = f"🚨 Suspicious message by {message.author.mention} "\ msg = (
f"🚨 Suspicious message by {message.author.mention} "
f"({message.author.id}):" f"({message.author.id}):"
)
invites = self.invite_re.findall(message.content) invites = self.invite_re.findall(message.content)
for invite in invites: for invite in invites:
msg += f"\n- Has invite: https://{invite[0]}" msg += f"\n- Has invite: https://{invite[0]}"
alert = True alert = True
for susp_word in self.susp_words: for susp_word in config.suspect_words:
if susp_word in cleancont and\ if susp_word in cleancont and not any(
not any(ok_word in cleancont for ok_word in self.ok_words): ok_word in cleancont for ok_word in config.suspect_ignored_words
):
msg += f"\n- Contains suspicious word: `{susp_word}`" msg += f"\n- Contains suspicious word: `{susp_word}`"
alert = True alert = True
@ -169,13 +181,15 @@ class Logs(Cog):
# Bad Code :tm:, blame retr0id # Bad Code :tm:, blame retr0id
message_clean = message.content.replace("*", "").replace("_", "") message_clean = message.content.replace("*", "").replace("_", "")
regd = self.susp_hellgex.sub(lambda w: "**{}**".format(w.group(0)), regd = self.susp_hellgex.sub(
message_clean) lambda w: "**{}**".format(w.group(0)), message_clean
)
# Show a message embed # Show a message embed
embed = discord.Embed(description=regd) embed = discord.Embed(description=regd)
embed.set_author(name=message.author.display_name, embed.set_author(
icon_url=message.author.avatar_url) name=message.author.display_name, icon_url=message.author.avatar_url
)
await spy_channel.send(msg, embed=embed) await spy_channel.send(msg, embed=embed)
@ -184,8 +198,9 @@ class Logs(Cog):
if compliant: if compliant:
return return
msg = f"R11 violating name by {message.author.mention} "\ msg = (
f"({message.author.id})." f"R11 violating name by {message.author.mention} " f"({message.author.id})."
)
spy_channel = self.bot.get_channel(config.spylog_channel) spy_channel = self.bot.get_channel(config.spylog_channel)
await spy_channel.send(msg) await spy_channel.send(msg)
@ -216,10 +231,13 @@ class Logs(Cog):
after_content = after.clean_content.replace("`", "`\u200d") after_content = after.clean_content.replace("`", "`\u200d")
log_channel = self.bot.get_channel(config.log_channel) log_channel = self.bot.get_channel(config.log_channel)
msg = "📝 **Message edit**: \n"\
msg = (
"📝 **Message edit**: \n"\
f"from {self.bot.escape_message(after.author.name)} "\ f"from {self.bot.escape_message(after.author.name)} "\
f"({after.author.id}), in {after.channel.mention}:\n"\ f"({after.author.id}), in {after.channel.mention}:\n"\
f"```{before_content}``` → ```{after_content}```" f"```{before_content}``` → ```{after_content}```"
)
# If resulting message is too long, upload to hastebin # If resulting message is too long, upload to hastebin
if len(msg) > 2000: if len(msg) > 2000:
@ -235,10 +253,12 @@ class Logs(Cog):
return return
log_channel = self.bot.get_channel(config.log_channel) log_channel = self.bot.get_channel(config.log_channel)
msg = "🗑️ **Message delete**: \n"\ msg = (
f"from {self.bot.escape_message(message.author.name)} "\ "🗑️ **Message delete**: \n"
f"({message.author.id}), in {message.channel.mention}:\n"\ f"from {self.bot.escape_message(message.author.name)} "
f"({message.author.id}), in {message.channel.mention}:\n"
f"`{message.clean_content}`" f"`{message.clean_content}`"
)
# If resulting message is too long, upload to hastebin # If resulting message is too long, upload to hastebin
if len(msg) > 2000: if len(msg) > 2000:
@ -250,28 +270,46 @@ class Logs(Cog):
@Cog.listener() @Cog.listener()
async def on_member_remove(self, member): async def on_member_remove(self, member):
await self.bot.wait_until_ready() 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) log_channel = self.bot.get_channel(config.log_channel)
msg = f"⬅️ **Leave**: {member.mention} | "\ msg = (
f"{self.bot.escape_message(member)}\n"\ f"⬅️ **Leave**: {member.mention} | "
f"{self.bot.escape_message(member)}\n"
f"🏷 __User ID__: {member.id}" f"🏷 __User ID__: {member.id}"
)
await log_channel.send(msg) await log_channel.send(msg)
@Cog.listener() @Cog.listener()
async def on_member_ban(self, guild, member): async def on_member_ban(self, guild, member):
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
if guild.id not in config.guild_whitelist:
return
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
msg = f"⛔ **Ban**: {member.mention} | "\ msg = (
f"{self.bot.escape_message(member)}\n"\ f"⛔ **Ban**: {member.mention} | "
f"{self.bot.escape_message(member)}\n"
f"🏷 __User ID__: {member.id}" f"🏷 __User ID__: {member.id}"
)
await log_channel.send(msg) await log_channel.send(msg)
@Cog.listener() @Cog.listener()
async def on_member_unban(self, guild, user): async def on_member_unban(self, guild, user):
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
if guild.id not in config.guild_whitelist:
return
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
msg = f"⚠️ **Unban**: {user.mention} | "\ msg = (
f"{self.bot.escape_message(user)}\n"\ f"⚠️ **Unban**: {user.mention} | "
f"{self.bot.escape_message(user)}\n"
f"🏷 __User ID__: {user.id}" f"🏷 __User ID__: {user.id}"
)
# if user.id in self.bot.timebans: # if user.id in self.bot.timebans:
# msg += "\nTimeban removed." # msg += "\nTimeban removed."
# self.bot.timebans.pop(user.id) # self.bot.timebans.pop(user.id)
@ -286,6 +324,10 @@ class Logs(Cog):
@Cog.listener() @Cog.listener()
async def on_member_update(self, member_before, member_after): async def on_member_update(self, member_before, member_after):
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
if member_after.guild.id not in config.guild_whitelist:
return
msg = "" msg = ""
log_channel = self.bot.get_channel(config.log_channel) log_channel = self.bot.get_channel(config.log_channel)
if member_before.roles != member_after.roles: if member_before.roles != member_after.roles:
@ -315,9 +357,11 @@ class Logs(Cog):
msg += ", ".join(roles) msg += ", ".join(roles)
if member_before.name != member_after.name: if member_before.name != member_after.name:
msg += "\n📝 __Username change__: "\ msg += (
f"{self.bot.escape_message(member_before)}"\ "\n📝 __Username change__: "
f"{self.bot.escape_message(member_before)}"
f"{self.bot.escape_message(member_after)}" f"{self.bot.escape_message(member_after)}"
)
if member_before.nick != member_after.nick: if member_before.nick != member_after.nick:
if not member_before.nick: if not member_before.nick:
msg += "\n🏷 __Nickname addition__" msg += "\n🏷 __Nickname addition__"
@ -325,11 +369,15 @@ class Logs(Cog):
msg += "\n🏷 __Nickname removal__" msg += "\n🏷 __Nickname removal__"
else: else:
msg += "\n🏷 __Nickname change__" msg += "\n🏷 __Nickname change__"
msg += f": {self.bot.escape_message(member_before.nick)}"\ msg += (
f": {self.bot.escape_message(member_before.nick)}"
f"{self.bot.escape_message(member_after.nick)}" f"{self.bot.escape_message(member_after.nick)}"
)
if msg: if msg:
msg = f" **Member update**: {member_after.mention} | "\ msg = (
f" **Member update**: {member_after.mention} | "
f"{self.bot.escape_message(member_after)}{msg}" f"{self.bot.escape_message(member_after)}{msg}"
)
await log_channel.send(msg) await log_channel.send(msg)

View file

@ -31,9 +31,11 @@ class Meme(Cog):
celsius = random.randint(15, 100) celsius = random.randint(15, 100)
fahrenheit = self.c_to_f(celsius) fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius) kelvin = self.c_to_k(celsius)
await ctx.send(f"{user.mention} warmed." await ctx.send(
f"{user.mention} warmed."
f" User is now {celsius}°C " f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K).") f"({fahrenheit}°F, {kelvin}K)."
)
@commands.check(check_if_staff_or_ot) @commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="chill", aliases=["cold"]) @commands.command(hidden=True, name="chill", aliases=["cold"])
@ -42,9 +44,11 @@ class Meme(Cog):
celsius = random.randint(-50, 15) celsius = random.randint(-50, 15)
fahrenheit = self.c_to_f(celsius) fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius) kelvin = self.c_to_k(celsius)
await ctx.send(f"{user.mention} chilled." await ctx.send(
f"{user.mention} chilled."
f" User is now {celsius}°C " f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K).") f"({fahrenheit}°F, {kelvin}K)."
)
@commands.check(check_if_staff_or_ot) @commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["thank", "reswitchedgold"]) @commands.command(hidden=True, aliases=["thank", "reswitchedgold"])
@ -53,15 +57,18 @@ class Meme(Cog):
await ctx.send(f"{user.mention} gets a :star:, yay!") await ctx.send(f"{user.mention} gets a :star:, yay!")
@commands.check(check_if_staff_or_ot) @commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["reswitchedsilver", "silv3r", @commands.command(
"reswitchedsilv3r"]) hidden=True, aliases=["reswitchedsilver", "silv3r", "reswitchedsilv3r"]
)
async def silver(self, ctx, user: discord.Member): async def silver(self, ctx, user: discord.Member):
"""Gives a user ReSwitched Silver™""" """Gives a user ReSwitched Silver™"""
embed = discord.Embed(title="ReSwitched Silver™!", embed = discord.Embed(
description=f"Here's your ReSwitched Silver™," title="ReSwitched Silver™!",
f"{user.mention}!") description=f"Here's your ReSwitched Silver™," f"{user.mention}!",
embed.set_image(url="https://cdn.discordapp.com/emojis/" )
"548623626916724747.png?v=1") embed.set_image(
url="https://cdn.discordapp.com/emojis/" "548623626916724747.png?v=1"
)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.check(check_if_staff_or_ot) @commands.check(check_if_staff_or_ot)
@ -69,9 +76,11 @@ class Meme(Cog):
async def btwiuse(self, ctx): async def btwiuse(self, ctx):
"""btw i use arch""" """btw i use arch"""
uname = platform.uname() uname = platform.uname()
await ctx.send(f"BTW I use {platform.python_implementation()} " await ctx.send(
f"BTW I use {platform.python_implementation()} "
f"{platform.python_version()} on {uname.system} " f"{platform.python_version()} on {uname.system} "
f"{uname.release}") f"{uname.release}"
)
@commands.check(check_if_staff_or_ot) @commands.check(check_if_staff_or_ot)
@commands.command(hidden=True) @commands.command(hidden=True)
@ -79,6 +88,12 @@ class Meme(Cog):
"""secret command""" """secret command"""
await ctx.send(f"🍂 you found me 🍂") 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.check(check_if_staff_or_ot)
@commands.command(hidden=True) @commands.command(hidden=True)
async def peng(self, ctx): async def peng(self, ctx):
@ -89,25 +104,30 @@ class Meme(Cog):
@commands.command(hidden=True, aliases=["outstanding"]) @commands.command(hidden=True, aliases=["outstanding"])
async def outstandingmove(self, ctx): async def outstandingmove(self, ctx):
"""Posts the outstanding move meme""" """Posts the outstanding move meme"""
await ctx.send("https://cdn.discordapp.com/attachments" await ctx.send(
"https://cdn.discordapp.com/attachments"
"/371047036348268545/528413677007929344" "/371047036348268545/528413677007929344"
"/image0-5.jpg") "/image0-5.jpg"
)
@commands.check(check_if_staff_or_ot) @commands.check(check_if_staff_or_ot)
@commands.command(hidden=True) @commands.command(hidden=True)
async def bones(self, ctx): async def bones(self, ctx):
await ctx.send("https://cdn.discordapp.com/emojis/" await ctx.send(
"443501365843591169.png?v=1") "https://cdn.discordapp.com/emojis/" "443501365843591169.png?v=1"
)
@commands.check(check_if_staff_or_ot) @commands.check(check_if_staff_or_ot)
@commands.command(hidden=True) @commands.command(hidden=True)
async def headpat(self, ctx): async def headpat(self, ctx):
await ctx.send("https://cdn.discordapp.com/emojis/" await ctx.send(
"465650811909701642.png?v=1") "https://cdn.discordapp.com/emojis/" "465650811909701642.png?v=1"
)
@commands.check(check_if_staff_or_ot) @commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["when", "etawhen", @commands.command(
"emunand", "emummc", "thermosphere"]) hidden=True, aliases=["when", "etawhen", "emunand", "emummc", "thermosphere"]
)
async def eta(self, ctx): async def eta(self, ctx):
await ctx.send("June 15.") await ctx.send("June 15.")
@ -117,11 +137,14 @@ class Meme(Cog):
"""Bams a user owo""" """Bams a user owo"""
if target == ctx.author: if target == ctx.author:
if target.id == 181627658520625152: 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̕&̡.̷ 👍̡") return await ctx.send("hedgeberg#7337 is ̶n͢ow b̕&̡.̷ 👍̡")
elif target == self.bot.user: elif target == self.bot.user:
return await ctx.send(f"I'm sorry {ctx.author.mention}, " return await ctx.send(
"I'm afraid I can't do that.") 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)) safe_name = await commands.clean_content().convert(ctx, str(target))
await ctx.send(f"{safe_name} is ̶n͢ow b̕&̡.̷ 👍̡") await ctx.send(f"{safe_name} is ̶n͢ow b̕&̡.̷ 👍̡")
@ -139,8 +162,9 @@ class Meme(Cog):
@commands.command(hidden=True, aliases=["yotld"]) @commands.command(hidden=True, aliases=["yotld"])
async def yearoflinux(self, ctx): async def yearoflinux(self, ctx):
"""Shows the year of Linux on the desktop""" """Shows the year of Linux on the desktop"""
await ctx.send(f"{datetime.datetime.now().year} is the year of " await ctx.send(
"Linux on the Desktop") f"{datetime.datetime.now().year} is the year of " "Linux on the Desktop"
)
def setup(bot): def setup(bot):

View file

@ -25,11 +25,9 @@ class Mod(Cog):
await ctx.send(f"Done!") await ctx.send(f"Done!")
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
log_msg = f"✏️ **Guild Icon Update**: {ctx.author} "\ log_msg = f"✏️ **Guild Icon Update**: {ctx.author} " "changed the guild icon."
"changed the guild icon."
img_filename = url.split("/")[-1].split("#")[0] # hacky img_filename = url.split("/")[-1].split("#")[0] # hacky
img_file = discord.File(io.BytesIO(img_bytes), img_file = discord.File(io.BytesIO(img_bytes), filename=img_filename)
filename=img_filename)
await log_channel.send(log_msg, file=img_file) await log_channel.send(log_msg, file=img_file)
@commands.guild_only() @commands.guild_only()
@ -41,11 +39,13 @@ class Mod(Cog):
if target == ctx.author: if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.") return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user: elif target == self.bot.user:
return await ctx.send(f"I'm sorry {ctx.author.mention}, " return await ctx.send(
"I'm afraid I can't do that.") f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target): elif self.check_if_target_is_staff(target):
return await ctx.send("I can't mute this user as " return await ctx.send(
"they're a member of staff.") "I can't mute this user as " "they're a member of staff."
)
userlog(target.id, ctx.author, reason, "mutes", target.name) userlog(target.id, ctx.author, reason, "mutes", target.name)
@ -53,7 +53,7 @@ class Mod(Cog):
dm_message = f"You were muted!" dm_message = f"You were muted!"
if reason: if reason:
dm_message += f" The given reason is: \"{reason}\"." dm_message += f' The given reason is: "{reason}".'
try: try:
await target.send(dm_message) await target.send(dm_message)
@ -66,15 +66,19 @@ class Mod(Cog):
await target.add_roles(mute_role, reason=str(ctx.author)) await target.add_roles(mute_role, reason=str(ctx.author))
chan_message = f"🔇 **Muted**: {ctx.author.mention} muted "\ chan_message = (
f"{target.mention} | {safe_name}\n"\ f"🔇 **Muted**: {ctx.author.mention} muted "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n" f"🏷 __User ID__: {target.id}\n"
)
if reason: if reason:
chan_message += f"✏️ __Reason__: \"{reason}\"" chan_message += f'✏️ __Reason__: "{reason}"'
else: else:
chan_message += "Please add an explanation below. In the future, "\ chan_message += (
"it is recommended to use `.mute <user> [reason]`"\ "Please add an explanation below. In the future, "
"it is recommended to use `.mute <user> [reason]`"
" as the reason is automatically sent to the user." " as the reason is automatically sent to the user."
)
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message) await log_channel.send(chan_message)
@ -91,9 +95,11 @@ class Mod(Cog):
mute_role = ctx.guild.get_role(config.mute_role) mute_role = ctx.guild.get_role(config.mute_role)
await target.remove_roles(mute_role, reason=str(ctx.author)) await target.remove_roles(mute_role, reason=str(ctx.author))
chan_message = f"🔈 **Unmuted**: {ctx.author.mention} unmuted "\ chan_message = (
f"{target.mention} | {safe_name}\n"\ f"🔈 **Unmuted**: {ctx.author.mention} unmuted "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n" f"🏷 __User ID__: {target.id}\n"
)
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message) await log_channel.send(chan_message)
@ -110,11 +116,13 @@ class Mod(Cog):
if target == ctx.author: if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.") return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user: elif target == self.bot.user:
return await ctx.send(f"I'm sorry {ctx.author.mention}, " return await ctx.send(
"I'm afraid I can't do that.") f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target): elif self.check_if_target_is_staff(target):
return await ctx.send("I can't kick this user as " return await ctx.send(
"they're a member of staff.") "I can't kick this user as " "they're a member of staff."
)
userlog(target.id, ctx.author, reason, "kicks", target.name) 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}." dm_message = f"You were kicked from {ctx.guild.name}."
if reason: if reason:
dm_message += f" The given reason is: \"{reason}\"." dm_message += f' The given reason is: "{reason}".'
dm_message += "\n\nYou are able to rejoin the server,"\ dm_message += (
"\n\nYou are able to rejoin the server,"
" but please be sure to behave when participating again." " but please be sure to behave when participating again."
)
try: try:
await target.send(dm_message) await target.send(dm_message)
@ -134,19 +144,24 @@ class Mod(Cog):
pass pass
await target.kick(reason=f"{ctx.author}, reason: {reason}") await target.kick(reason=f"{ctx.author}, reason: {reason}")
chan_message = f"👢 **Kick**: {ctx.author.mention} kicked "\ chan_message = (
f"{target.mention} | {safe_name}\n"\ f"👢 **Kick**: {ctx.author.mention} kicked "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n" f"🏷 __User ID__: {target.id}\n"
)
if reason: if reason:
chan_message += f"✏️ __Reason__: \"{reason}\"" chan_message += f'✏️ __Reason__: "{reason}"'
else: else:
chan_message += "Please add an explanation below. In the future"\ chan_message += (
", it is recommended to use "\ "Please add an explanation below. In the future"
"`.kick <user> [reason]`"\ ", it is recommended to use "
"`.kick <user> [reason]`"
" as the reason is automatically sent to the user." " as the reason is automatically sent to the user."
)
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message) await log_channel.send(chan_message)
await ctx.send(f"👢 {safe_name}, 👍.")
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(ban_members=True) @commands.bot_has_permissions(ban_members=True)
@ -157,14 +172,18 @@ class Mod(Cog):
# Hedge-proofing the code # Hedge-proofing the code
if target == ctx.author: if target == ctx.author:
if target.id == 181627658520625152: 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&. 👍") return await ctx.send("hedgeberg#7337 is now b&. 👍")
elif target == self.bot.user: elif target == self.bot.user:
return await ctx.send(f"I'm sorry {ctx.author.mention}, " return await ctx.send(
"I'm afraid I can't do that.") f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target): elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as " return await ctx.send(
"they're a member of staff.") "I can't ban this user as " "they're a member of staff."
)
userlog(target.id, ctx.author, reason, "bans", target.name) 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}." dm_message = f"You were banned from {ctx.guild.name}."
if reason: 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." dm_message += "\n\nThis ban does not expire."
try: try:
@ -182,17 +201,22 @@ class Mod(Cog):
# or has DMs disabled # or has DMs disabled
pass pass
await target.ban(reason=f"{ctx.author}, reason: {reason}", await target.ban(
delete_message_days=0) reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
chan_message = f"⛔ **Ban**: {ctx.author.mention} banned "\ )
f"{target.mention} | {safe_name}\n"\ chan_message = (
f"⛔ **Ban**: {ctx.author.mention} banned "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n" f"🏷 __User ID__: {target.id}\n"
)
if reason: if reason:
chan_message += f"✏️ __Reason__: \"{reason}\"" chan_message += f'✏️ __Reason__: "{reason}"'
else: else:
chan_message += "Please add an explanation below. In the future"\ chan_message += (
", it is recommended to use `.ban <user> [reason]`"\ "Please add an explanation below. In the future"
", it is recommended to use `.ban <user> [reason]`"
" as the reason is automatically sent to the user." " as the reason is automatically sent to the user."
)
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message) await log_channel.send(chan_message)
@ -210,28 +234,34 @@ class Mod(Cog):
if target == ctx.author.id: if target == ctx.author.id:
return await ctx.send("You can't do mod actions on yourself.") return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user: elif target == self.bot.user:
return await ctx.send(f"I'm sorry {ctx.author.mention}, " return await ctx.send(
"I'm afraid I can't do that.") 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): elif target_member and self.check_if_target_is_staff(target_member):
return await ctx.send("I can't ban this user as " return await ctx.send(
"they're a member of staff.") "I can't ban this user as " "they're a member of staff."
)
userlog(target, ctx.author, reason, "bans", target_user.name) userlog(target, ctx.author, reason, "bans", target_user.name)
safe_name = await commands.clean_content().convert(ctx, str(target)) safe_name = await commands.clean_content().convert(ctx, str(target))
await ctx.guild.ban(target_user, await ctx.guild.ban(
reason=f"{ctx.author}, reason: {reason}", target_user, reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
delete_message_days=0) )
chan_message = f"⛔ **Hackban**: {ctx.author.mention} banned "\ chan_message = (
f"{target_user.mention} | {safe_name}\n"\ f"⛔ **Hackban**: {ctx.author.mention} banned "
f"{target_user.mention} | {safe_name}\n"
f"🏷 __User ID__: {target}\n" f"🏷 __User ID__: {target}\n"
)
if reason: if reason:
chan_message += f"✏️ __Reason__: \"{reason}\"" chan_message += f'✏️ __Reason__: "{reason}"'
else: else:
chan_message += "Please add an explanation below. In the future"\ chan_message += (
", it is recommended to use "\ "Please add an explanation below. In the future"
", it is recommended to use "
"`.hackban <user> [reason]`." "`.hackban <user> [reason]`."
)
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message) await log_channel.send(chan_message)
@ -247,27 +277,34 @@ class Mod(Cog):
if target == ctx.author: if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.") return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user: elif target == self.bot.user:
return await ctx.send(f"I'm sorry {ctx.author.mention}, " return await ctx.send(
"I'm afraid I can't do that.") f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target): elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as " return await ctx.send(
"they're a member of staff.") "I can't ban this user as " "they're a member of staff."
)
userlog(target.id, ctx.author, reason, "bans", target.name) userlog(target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content().convert(ctx, str(target)) safe_name = await commands.clean_content().convert(ctx, str(target))
await target.ban(reason=f"{ctx.author}, reason: {reason}", await target.ban(
delete_message_days=0) 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"\ chan_message = (
f"⛔ **Silent ban**: {ctx.author.mention} banned "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n" f"🏷 __User ID__: {target.id}\n"
)
if reason: if reason:
chan_message += f"✏️ __Reason__: \"{reason}\"" chan_message += f'✏️ __Reason__: "{reason}"'
else: else:
chan_message += "Please add an explanation below. In the future"\ chan_message += (
", it is recommended to use `.ban <user> [reason]`"\ "Please add an explanation below. In the future"
", it is recommended to use `.ban <user> [reason]`"
" as the reason is automatically sent to the user." " as the reason is automatically sent to the user."
)
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message) await log_channel.send(chan_message)
@ -275,12 +312,12 @@ class Mod(Cog):
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command() @commands.command()
async def approve(self, ctx, target: discord.Member, async def approve(self, ctx, target: discord.Member, role: str = "community"):
role: str = "community"):
"""Add a role to a user (default: community), staff only.""" """Add a role to a user (default: community), staff only."""
if role not in config.named_roles: if role not in config.named_roles:
return await ctx.send("No such role! Available roles: " + return await ctx.send(
','.join(config.named_roles)) "No such role! Available roles: " + ",".join(config.named_roles)
)
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
target_role = ctx.guild.get_role(config.named_roles[role]) 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 ctx.send(f"Approved {target.mention} to `{role}` role.")
await log_channel.send(f"✅ Approved: {ctx.author.mention} added" await log_channel.send(
f" {role} to {target.mention}") f"✅ Approved: {ctx.author.mention} added" f" {role} to {target.mention}"
)
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command(aliases=["unapprove"]) @commands.command(aliases=["unapprove"])
async def revoke(self, ctx, target: discord.Member, async def revoke(self, ctx, target: discord.Member, role: str = "community"):
role: str = "community"):
"""Remove a role from a user (default: community), staff only.""" """Remove a role from a user (default: community), staff only."""
if role not in config.named_roles: if role not in config.named_roles:
return await ctx.send("No such role! Available roles: " + return await ctx.send(
','.join(config.named_roles)) "No such role! Available roles: " + ",".join(config.named_roles)
)
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
target_role = ctx.guild.get_role(config.named_roles[role]) 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 ctx.send(f"Un-approved {target.mention} from `{role}` role.")
await log_channel.send(f"❌ Un-approved: {ctx.author.mention} removed" await log_channel.send(
f" {role} from {target.mention}") f"❌ Un-approved: {ctx.author.mention} removed"
f" {role} from {target.mention}"
)
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@ -327,8 +367,10 @@ class Mod(Cog):
if not channel: if not channel:
channel = ctx.channel channel = ctx.channel
await channel.purge(limit=limit) await channel.purge(limit=limit)
msg = f"🗑 **Purged**: {ctx.author.mention} purged {limit} "\ msg = (
f"🗑 **Purged**: {ctx.author.mention} purged {limit} "
f"messages in {channel.mention}." f"messages in {channel.mention}."
)
await log_channel.send(msg) await log_channel.send(msg)
@commands.guild_only() @commands.guild_only()
@ -340,37 +382,46 @@ class Mod(Cog):
if target == ctx.author: if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.") return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user: elif target == self.bot.user:
return await ctx.send(f"I'm sorry {ctx.author.mention}, " return await ctx.send(
"I'm afraid I can't do that.") f"I'm sorry {ctx.author.mention}, " "I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target): elif self.check_if_target_is_staff(target):
return await ctx.send("I can't warn this user as " return await ctx.send(
"they're a member of staff.") "I can't warn this user as " "they're a member of staff."
)
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
warn_count = userlog(target.id, ctx.author, reason, warn_count = userlog(target.id, ctx.author, reason, "warns", target.name)
"warns", target.name)
safe_name = await commands.clean_content().convert(ctx, str(target)) safe_name = await commands.clean_content().convert(ctx, str(target))
chan_msg = f"⚠️ **Warned**: {ctx.author.mention} warned "\ chan_msg = (
f"{target.mention} (warn #{warn_count}) "\ f"⚠️ **Warned**: {ctx.author.mention} warned "
f"{target.mention} (warn #{warn_count}) "
f"| {safe_name}\n" f"| {safe_name}\n"
)
msg = f"You were warned on {ctx.guild.name}." msg = f"You were warned on {ctx.guild.name}."
if reason: if reason:
msg += " The given reason is: " + reason msg += " The given reason is: " + reason
msg += f"\n\nPlease read the rules in {config.rules_url}. "\ msg += (
f"\n\nPlease read the rules in {config.rules_url}. "
f"This is warn #{warn_count}." f"This is warn #{warn_count}."
)
if warn_count == 2: if warn_count == 2:
msg += " __The next warn will automatically kick.__" msg += " __The next warn will automatically kick.__"
if warn_count == 3: if warn_count == 3:
msg += "\n\nYou were kicked because of this warning. "\ msg += (
"You can join again right away. "\ "\n\nYou were kicked because of this warning. "
"You can join again right away. "
"Two more warnings will result in an automatic ban." "Two more warnings will result in an automatic ban."
)
if warn_count == 4: if warn_count == 4:
msg += "\n\nYou were kicked because of this warning. "\ msg += (
"This is your final warning. "\ "\n\nYou were kicked because of this warning. "
"You can join again, but "\ "This is your final warning. "
"You can join again, but "
"**one more warn will result in a ban**." "**one more warn will result in a ban**."
)
chan_msg += "**This resulted in an auto-kick.**\n" chan_msg += "**This resulted in an auto-kick.**\n"
if warn_count == 5: if warn_count == 5:
msg += "\n\nYou were automatically banned due to five warnings." 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: if warn_count == 3 or warn_count == 4:
await target.kick() await target.kick()
if warn_count >= 5: # just in case if warn_count >= 5: # just in case
await target.ban(reason="exceeded warn limit", await target.ban(reason="exceeded warn limit", delete_message_days=0)
delete_message_days=0) await ctx.send(
await ctx.send(f"{target.mention} warned. " f"{target.mention} warned. " f"User has {warn_count} warning(s)."
f"User has {warn_count} warning(s).") )
if reason: if reason:
chan_msg += f"✏️ __Reason__: \"{reason}\"" chan_msg += f'✏️ __Reason__: "{reason}"'
else: else:
chan_msg += "Please add an explanation below. In the future"\ chan_msg += (
", it is recommended to use `.warn <user> [reason]`"\ "Please add an explanation below. In the future"
", it is recommended to use `.warn <user> [reason]`"
" as the reason is automatically sent to the user." " as the reason is automatically sent to the user."
)
await log_channel.send(chan_msg) await log_channel.send(chan_msg)
@commands.guild_only() @commands.guild_only()
@ -414,7 +467,7 @@ class Mod(Cog):
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command(aliases=['echo']) @commands.command(aliases=["echo"])
async def say(self, ctx, *, the_text: str): async def say(self, ctx, *, the_text: str):
"""Repeats a given text, staff only.""" """Repeats a given text, staff only."""
await ctx.send(the_text) await ctx.send(the_text)

View file

@ -14,8 +14,7 @@ class ModNote(Cog):
@commands.command(aliases=["addnote"]) @commands.command(aliases=["addnote"])
async def note(self, ctx, target: discord.Member, *, note: str = ""): async def note(self, ctx, target: discord.Member, *, note: str = ""):
"""Adds a note to a user, staff only.""" """Adds a note to a user, staff only."""
userlog(target.id, ctx.author, note, userlog(target.id, ctx.author, note, "notes", target.name)
"notes", target.name)
await ctx.send(f"{ctx.author.mention}: noted!") await ctx.send(f"{ctx.author.mention}: noted!")
@commands.guild_only() @commands.guild_only()
@ -23,9 +22,8 @@ class ModNote(Cog):
@commands.command(aliases=["addnoteid"]) @commands.command(aliases=["addnoteid"])
async def noteid(self, ctx, target: int, *, note: str = ""): async def noteid(self, ctx, target: int, *, note: str = ""):
"""Adds a note to a user by userid, staff only.""" """Adds a note to a user by userid, staff only."""
userlog(target, ctx.author, note, userlog(target, ctx.author, note, "notes")
"notes") await ctx.send(f"{ctx.author.mention}: noted!")
await ctx.send(f"{target.mention}: noted!")
def setup(bot): def setup(bot):

View file

@ -13,9 +13,14 @@ class ModReact(Cog):
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command() @commands.command()
async def clearreactsbyuser(self, ctx, user: discord.Member, *, async def clearreactsbyuser(
self,
ctx,
user: discord.Member,
*,
channel: discord.TextChannel = None, channel: discord.TextChannel = None,
limit: int = 50): limit: int = 50,
):
"""Clears reacts from a given user in the given channel, staff only.""" """Clears reacts from a given user in the given channel, staff only."""
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
if not channel: if not channel:
@ -27,18 +32,20 @@ class ModReact(Cog):
count += 1 count += 1
async for u in react.users(): async for u in react.users():
await msg.remove_reaction(react, u) await msg.remove_reaction(react, u)
msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared "\ msg = (
f"{user.mention}'s reacts from the last {limit} messages "\ f"✏️ **Cleared reacts**: {ctx.author.mention} cleared "
f"{user.mention}'s reacts from the last {limit} messages "
f"in {channel.mention}." f"in {channel.mention}."
)
await ctx.channel.send(f"Cleared {count} unique reactions") await ctx.channel.send(f"Cleared {count} unique reactions")
await log_channel.send(msg) await log_channel.send(msg)
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command() @commands.command()
async def clearallreacts(self, ctx, *, async def clearallreacts(
limit: int = 50, self, ctx, *, limit: int = 50, channel: discord.TextChannel = None
channel: discord.TextChannel = None): ):
"""Clears all reacts in a given channel, staff only. Use with care.""" """Clears all reacts in a given channel, staff only. Use with care."""
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
if not channel: if not channel:
@ -48,8 +55,10 @@ class ModReact(Cog):
if msg.reactions: if msg.reactions:
count += 1 count += 1
await msg.clear_reactions() await msg.clear_reactions()
msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared all "\ msg = (
f"✏️ **Cleared reacts**: {ctx.author.mention} cleared all "
f"reacts from the last {limit} messages in {channel.mention}." f"reacts from the last {limit} messages in {channel.mention}."
)
await ctx.channel.send(f"Cleared reacts from {count} messages!") await ctx.channel.send(f"Cleared reacts from {count} messages!")
await log_channel.send(msg) await log_channel.send(msg)
@ -58,8 +67,10 @@ class ModReact(Cog):
@commands.command() @commands.command()
async def clearreactsinteractive(self, ctx): async def clearreactsinteractive(self, ctx):
"""Clears reacts interactively, staff only. Use with care.""" """Clears reacts interactively, staff only. Use with care."""
msg_text = f"{ctx.author.mention}, react to the reactions you want "\ msg_text = (
f"{ctx.author.mention}, react to the reactions you want "
f"to remove. React to this message when you're done." f"to remove. React to this message when you're done."
)
msg = await ctx.channel.send(msg_text) msg = await ctx.channel.send(msg_text)
tasks = [] tasks = []
@ -74,10 +85,11 @@ class ModReact(Cog):
else: else:
# remove a reaction # remove a reaction
async def impl(): async def impl():
msg = await self.bot \ msg = (
.get_guild(event.guild_id) \ await self.bot.get_guild(event.guild_id)
.get_channel(event.channel_id) \ .get_channel(event.channel_id)
.get_message(event.message_id) .get_message(event.message_id)
)
def check_emoji(r): def check_emoji(r):
if event.emoji.is_custom_emoji() == r.custom_emoji: if event.emoji.is_custom_emoji() == r.custom_emoji:
@ -88,17 +100,17 @@ class ModReact(Cog):
return event.emoji.name == r.emoji return event.emoji.name == r.emoji
else: else:
return False return False
for reaction in filter(check_emoji, msg.reactions): for reaction in filter(check_emoji, msg.reactions):
async for u in reaction.users(): async for u in reaction.users():
await reaction.message.remove_reaction(reaction, u) await reaction.message.remove_reaction(reaction, u)
# schedule immediately # schedule immediately
tasks.append(asyncio.create_task(impl())) tasks.append(asyncio.create_task(impl()))
return False return False
try: try:
await self.bot.wait_for("raw_reaction_add", await self.bot.wait_for("raw_reaction_add", timeout=120.0, check=check)
timeout=120.0,
check=check)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await msg.edit(content=f"{msg_text} Timed out.") await msg.edit(content=f"{msg_text} Timed out.")
else: else:

View file

@ -20,31 +20,37 @@ class ModTimed(Cog):
@commands.bot_has_permissions(ban_members=True) @commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command() @commands.command()
async def timeban(self, ctx, target: discord.Member, async def timeban(
duration: str, *, reason: str = ""): self, ctx, target: discord.Member, duration: str, *, reason: str = ""
):
"""Bans a user for a specified amount of time, staff only.""" """Bans a user for a specified amount of time, staff only."""
# Hedge-proofing the code # Hedge-proofing the code
if target == ctx.author: if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.") return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target): elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as " return await ctx.send(
"they're a member of staff.") "I can't ban this user as " "they're a member of staff."
)
expiry_timestamp = self.bot.parse_time(duration) expiry_timestamp = self.bot.parse_time(duration)
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp) expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime, duration_text = self.bot.get_relative_timestamp(
include_to=True, time_to=expiry_datetime, include_to=True, humanized=True
humanized=True) )
userlog(target.id, ctx.author, f"{reason} (Timed, until " userlog(
f"{duration_text})", target.id,
"bans", target.name) ctx.author,
f"{reason} (Timed, until " f"{duration_text})",
"bans",
target.name,
)
safe_name = await commands.clean_content().convert(ctx, str(target)) safe_name = await commands.clean_content().convert(ctx, str(target))
dm_message = f"You were banned from {ctx.guild.name}." dm_message = f"You were banned from {ctx.guild.name}."
if reason: 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}." dm_message += f"\n\nThis ban will expire {duration_text}."
try: try:
@ -54,53 +60,63 @@ class ModTimed(Cog):
# or has DMs disabled # or has DMs disabled
pass pass
await target.ban(reason=f"{ctx.author}, reason: {reason}", await target.ban(
delete_message_days=0) 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"\ chan_message = (
f"⛔ **Timed Ban**: {ctx.author.mention} banned "
f"{target.mention} for {duration_text} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n" f"🏷 __User ID__: {target.id}\n"
)
if reason: if reason:
chan_message += f"✏️ __Reason__: \"{reason}\"" chan_message += f'✏️ __Reason__: "{reason}"'
else: else:
chan_message += "Please add an explanation below. In the future"\ chan_message += (
", it is recommended to use `.ban <user> [reason]`"\ "Please add an explanation below. In the future"
", it is recommended to use `.ban <user> [reason]`"
" as the reason is automatically sent to the user." " as the reason is automatically sent to the user."
)
add_job("unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp) add_job("unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
log_channel = self.bot.get_channel(config.log_channel) log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(chan_message) await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. " await ctx.send(f"{safe_name} is now b&. " f"It will expire {duration_text}. 👍")
f"It will expire {duration_text}. 👍")
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command() @commands.command()
async def timemute(self, ctx, target: discord.Member, async def timemute(
duration: str, *, reason: str = ""): self, ctx, target: discord.Member, duration: str, *, reason: str = ""
):
"""Mutes a user for a specified amount of time, staff only.""" """Mutes a user for a specified amount of time, staff only."""
# Hedge-proofing the code # Hedge-proofing the code
if target == ctx.author: if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.") return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target): elif self.check_if_target_is_staff(target):
return await ctx.send("I can't mute this user as " return await ctx.send(
"they're a member of staff.") "I can't mute this user as " "they're a member of staff."
)
expiry_timestamp = self.bot.parse_time(duration) expiry_timestamp = self.bot.parse_time(duration)
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp) expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime, duration_text = self.bot.get_relative_timestamp(
include_to=True, time_to=expiry_datetime, include_to=True, humanized=True
humanized=True) )
userlog(target.id, ctx.author, f"{reason} (Timed, until " userlog(
f"{duration_text})", target.id,
"mutes", target.name) ctx.author,
f"{reason} (Timed, until " f"{duration_text})",
"mutes",
target.name,
)
safe_name = await commands.clean_content().convert(ctx, str(target)) safe_name = await commands.clean_content().convert(ctx, str(target))
dm_message = f"You were muted!" dm_message = f"You were muted!"
if reason: 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}." dm_message += f"\n\nThis mute will expire {duration_text}."
try: try:
@ -114,22 +130,27 @@ class ModTimed(Cog):
await target.add_roles(mute_role, reason=str(ctx.author)) await target.add_roles(mute_role, reason=str(ctx.author))
chan_message = f"🔇 **Timed Mute**: {ctx.author.mention} muted "\ chan_message = (
f"{target.mention} for {duration_text} | {safe_name}\n"\ f"🔇 **Timed Mute**: {ctx.author.mention} muted "
f"{target.mention} for {duration_text} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n" f"🏷 __User ID__: {target.id}\n"
)
if reason: if reason:
chan_message += f"✏️ __Reason__: \"{reason}\"" chan_message += f'✏️ __Reason__: "{reason}"'
else: else:
chan_message += "Please add an explanation below. In the future, "\ chan_message += (
"it is recommended to use `.mute <user> [reason]`"\ "Please add an explanation below. In the future, "
"it is recommended to use `.mute <user> [reason]`"
" as the reason is automatically sent to the user." " as the reason is automatically sent to the user."
)
add_job("unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp) add_job("unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
log_channel = self.bot.get_channel(config.log_channel) log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(chan_message) await log_channel.send(chan_message)
await ctx.send(f"{target.mention} can no longer speak. " await ctx.send(
f"It will expire {duration_text}.") f"{target.mention} can no longer speak. " f"It will expire {duration_text}."
)
add_restriction(target.id, config.mute_role) add_restriction(target.id, config.mute_role)

View file

@ -11,8 +11,9 @@ class ModUserlog(Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
def get_userlog_embed_for_id(self, uid: str, name: str, own: bool = False, def get_userlog_embed_for_id(
event=""): self, uid: str, name: str, own: bool = False, event=""
):
own_note = " Good for you!" if own else "" own_note = " Good for you!" if own else ""
wanted_events = ["warns", "bans", "kicks", "mutes"] wanted_events = ["warns", "bans", "kicks", "mutes"]
if event and not isinstance(event, list): 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]: if event_type in userlog[uid] and userlog[uid][event_type]:
event_name = userlog_event_types[event_type] event_name = userlog_event_types[event_type]
for idx, event in enumerate(userlog[uid][event_type]): for idx, event in enumerate(userlog[uid][event_type]):
issuer = "" if own else f"Issuer: {event['issuer_name']} "\ issuer = (
""
if own
else f"Issuer: {event['issuer_name']} "
f"({event['issuer_id']})\n" f"({event['issuer_id']})\n"
embed.add_field(name=f"{event_name} {idx + 1}: " )
f"{event['timestamp']}", embed.add_field(
name=f"{event_name} {idx + 1}: " f"{event['timestamp']}",
value=issuer + f"Reason: {event['reason']}", value=issuer + f"Reason: {event['reason']}",
inline=False) inline=False,
)
if not own and "watch" in userlog[uid]: if not own and "watch" in userlog[uid]:
watch_state = "" if userlog[uid]["watch"] else "NOT " watch_state = "" if userlog[uid]["watch"] else "NOT "
@ -65,17 +71,17 @@ class ModUserlog(Cog):
if not event_count: if not event_count:
return f"<@{uid}> has no {event_type}!" return f"<@{uid}> has no {event_type}!"
if idx > event_count: if idx > event_count:
return "Index is higher than "\ return "Index is higher than " f"count ({event_count})!"
f"count ({event_count})!"
if idx < 1: if idx < 1:
return "Index is below 1!" return "Index is below 1!"
event = userlog[uid][event_type][idx - 1] event = userlog[uid][event_type][idx - 1]
event_name = userlog_event_types[event_type] event_name = userlog_event_types[event_type]
embed = discord.Embed(color=discord.Color.dark_red(), embed = discord.Embed(
title=f"{event_name} {idx} on " color=discord.Color.dark_red(),
f"{event['timestamp']}", title=f"{event_name} {idx} on " f"{event['timestamp']}",
description=f"Issuer: {event['issuer_name']}\n" description=f"Issuer: {event['issuer_name']}\n"
f"Reason: {event['reason']}") f"Reason: {event['reason']}",
)
del userlog[uid][event_type][idx - 1] del userlog[uid][event_type][idx - 1]
set_userlog(json.dumps(userlog)) set_userlog(json.dumps(userlog))
return embed return embed
@ -85,21 +91,18 @@ class ModUserlog(Cog):
@commands.command(aliases=["events"]) @commands.command(aliases=["events"])
async def eventtypes(self, ctx): async def eventtypes(self, ctx):
"""Lists the available event types, staff only.""" """Lists the available event types, staff only."""
event_list = [f"{et} ({userlog_event_types[et]})" for et in event_list = [f"{et} ({userlog_event_types[et]})" for et in userlog_event_types]
userlog_event_types] event_text = "Available events:\n``` - " + "\n - ".join(event_list) + "```"
event_text = ("Available events:\n``` - " +
"\n - ".join(event_list) +
"```")
await ctx.send(event_text) await ctx.send(event_text)
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command(name="userlog", @commands.command(
aliases=["listwarns", "getuserlog", "listuserlog"]) name="userlog", aliases=["listwarns", "getuserlog", "listuserlog"]
)
async def userlog_cmd(self, ctx, target: discord.Member, event=""): async def userlog_cmd(self, ctx, target: discord.Member, event=""):
"""Lists the userlog events for a user, staff only.""" """Lists the userlog events for a user, staff only."""
embed = self.get_userlog_embed_for_id(str(target.id), str(target), embed = self.get_userlog_embed_for_id(str(target.id), str(target), event=event)
event=event)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.guild_only() @commands.guild_only()
@ -107,16 +110,16 @@ class ModUserlog(Cog):
@commands.command(aliases=["listnotes", "usernotes"]) @commands.command(aliases=["listnotes", "usernotes"])
async def notes(self, ctx, target: discord.Member): async def notes(self, ctx, target: discord.Member):
"""Lists the notes for a user, staff only.""" """Lists the notes for a user, staff only."""
embed = self.get_userlog_embed_for_id(str(target.id), str(target), embed = self.get_userlog_embed_for_id(
event="notes") str(target.id), str(target), event="notes"
)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.guild_only() @commands.guild_only()
@commands.command(aliases=["mywarns"]) @commands.command(aliases=["mywarns"])
async def myuserlog(self, ctx): async def myuserlog(self, ctx):
"""Lists your userlog events (warns etc).""" """Lists your userlog events (warns etc)."""
embed = self.get_userlog_embed_for_id(str(ctx.author.id), embed = self.get_userlog_embed_for_id(str(ctx.author.id), str(ctx.author), True)
str(ctx.author), True)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.guild_only() @commands.guild_only()
@ -130,16 +133,17 @@ class ModUserlog(Cog):
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command(aliases=["clearwarns"]) @commands.command(aliases=["clearwarns"])
async def clearevent(self, ctx, target: discord.Member, async def clearevent(self, ctx, target: discord.Member, event="warns"):
event="warns"):
"""Clears all events of given type for a user, staff only.""" """Clears all events of given type for a user, staff only."""
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
msg = self.clear_event_from_id(str(target.id), event) msg = self.clear_event_from_id(str(target.id), event)
safe_name = await commands.clean_content().convert(ctx, str(target)) safe_name = await commands.clean_content().convert(ctx, str(target))
await ctx.send(msg) await ctx.send(msg)
msg = f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"\ msg = (
f" all {event} events of {target.mention} | "\ f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"
f" all {event} events of {target.mention} | "
f"{safe_name}" f"{safe_name}"
)
await log_channel.send(msg) await log_channel.send(msg)
@commands.guild_only() @commands.guild_only()
@ -150,15 +154,16 @@ class ModUserlog(Cog):
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
msg = self.clear_event_from_id(str(target), event) msg = self.clear_event_from_id(str(target), event)
await ctx.send(msg) await ctx.send(msg)
msg = f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"\ msg = (
f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"
f" all {event} events of <@{target}> " f" all {event} events of <@{target}> "
)
await log_channel.send(msg) await log_channel.send(msg)
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command(aliases=["delwarn"]) @commands.command(aliases=["delwarn"])
async def delevent(self, ctx, target: discord.Member, idx: int, async def delevent(self, ctx, target: discord.Member, idx: int, event="warns"):
event="warns"):
"""Removes a specific event from a user, staff only.""" """Removes a specific event from a user, staff only."""
log_channel = self.bot.get_channel(config.modlog_channel) log_channel = self.bot.get_channel(config.modlog_channel)
del_event = self.delete_event_from_id(str(target.id), idx, event) 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): if isinstance(del_event, discord.Embed):
await ctx.send(f"{target.mention} has a {event_name} removed!") await ctx.send(f"{target.mention} has a {event_name} removed!")
safe_name = await commands.clean_content().convert(ctx, str(target)) safe_name = await commands.clean_content().convert(ctx, str(target))
msg = f"🗑 **Deleted {event_name}**: "\ msg = (
f"{ctx.author.mention} removed "\ f"🗑 **Deleted {event_name}**: "
f"{ctx.author.mention} removed "
f"{event_name} {idx} from {target.mention} | {safe_name}" f"{event_name} {idx} from {target.mention} | {safe_name}"
)
await log_channel.send(msg, embed=del_event) await log_channel.send(msg, embed=del_event)
else: else:
await ctx.send(del_event) await ctx.send(del_event)
@ -185,9 +192,11 @@ class ModUserlog(Cog):
# This is hell. # This is hell.
if isinstance(del_event, discord.Embed): if isinstance(del_event, discord.Embed):
await ctx.send(f"<@{target}> has a {event_name} removed!") await ctx.send(f"<@{target}> has a {event_name} removed!")
msg = f"🗑 **Deleted {event_name}**: "\ msg = (
f"{ctx.author.mention} removed "\ f"🗑 **Deleted {event_name}**: "
f"{ctx.author.mention} removed "
f"{event_name} {idx} from <@{target}> " f"{event_name} {idx} from <@{target}> "
)
await log_channel.send(msg, embed=del_event) await log_channel.send(msg, embed=del_event)
else: else:
await ctx.send(del_event) await ctx.send(del_event)
@ -202,20 +211,26 @@ class ModUserlog(Cog):
role = "@ everyone" role = "@ everyone"
event_types = ["warns", "bans", "kicks", "mutes", "notes"] event_types = ["warns", "bans", "kicks", "mutes", "notes"]
embed = self.get_userlog_embed_for_id(str(user.id), str(user), embed = self.get_userlog_embed_for_id(
event=event_types) str(user.id), str(user), event=event_types
)
await ctx.send(f"user = {user}\n" user_name = await commands.clean_content().convert(ctx, user.name)
display_name = await commands.clean_content().convert(ctx, user.display_name)
await ctx.send(
f"user = {user_name}\n"
f"id = {user.id}\n" f"id = {user.id}\n"
f"avatar = {user.avatar_url}\n" f"avatar = {user.avatar_url}\n"
f"bot = {user.bot}\n" f"bot = {user.bot}\n"
f"created_at = {user.created_at}\n" f"created_at = {user.created_at}\n"
f"display_name = {user.display_name}\n" f"display_name = {display_name}\n"
f"joined_at = {user.joined_at}\n" f"joined_at = {user.joined_at}\n"
f"activities = `{user.activities}`\n" f"activities = `{user.activities}`\n"
f"color = {user.colour}\n" f"color = {user.colour}\n"
f"top_role = {role}\n", f"top_role = {role}\n",
embed=embed) embed=embed,
)
def setup(bot): def setup(bot):

View file

@ -8,6 +8,7 @@ import gidgethub.aiohttp
from helpers.checks import check_if_collaborator from helpers.checks import check_if_collaborator
from helpers.checks import check_if_pin_channel from helpers.checks import check_if_pin_channel
class Pin(Cog): class Pin(Cog):
""" """
Allow users to pin things Allow users to pin things
@ -17,9 +18,11 @@ class Pin(Cog):
self.bot = bot self.bot = bot
def is_pinboard(self, msg): def is_pinboard(self, msg):
return msg.author == self.bot.user and \ return (
len(msg.embeds) > 0 and \ msg.author == self.bot.user
msg.embeds[0].title == "Pinboard" and len(msg.embeds) > 0
and msg.embeds[0].title == "Pinboard"
)
async def get_pinboard(self, gh, channel): async def get_pinboard(self, gh, channel):
# Find pinboard pin # Find pinboard pin
@ -32,21 +35,25 @@ class Pin(Cog):
return (id, data["files"]["pinboard.md"]["content"]) return (id, data["files"]["pinboard.md"]["content"])
# Create pinboard pin if it does not exist # Create pinboard pin if it does not exist
data = await gh.post("/gists", data={ data = await gh.post(
"/gists",
data={
"files": { "files": {
"pinboard.md": { "pinboard.md": {"content": "Old pins are available here:\n\n"}
"content": "Old pins are available here:\n\n"
}
}, },
"description": f"Pinboard for SwitchRoot #{channel.name}", "description": f"Pinboard for SwitchRoot #{channel.name}",
"public": True "public": True,
}) },
)
msg = await channel.send(embed=Embed( msg = await channel.send(
embed=Embed(
title="Pinboard", title="Pinboard",
description="Old pins are moved to the pinboard to make space for \ description="Old pins are moved to the pinboard to make space for \
new ones. Check it out!", new ones. Check it out!",
url=data["html_url"])) url=data["html_url"],
)
)
await msg.pin() await msg.pin()
return (data["id"], data["files"]["pinboard.md"]["content"]) return (data["id"], data["files"]["pinboard.md"]["content"])
@ -57,18 +64,15 @@ class Pin(Cog):
return return
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
gh = gidgethub.aiohttp.GitHubAPI(session, "RoboCop-NG", gh = gidgethub.aiohttp.GitHubAPI(
oauth_token=config.github_oauth_token) session, "RoboCop-NG", oauth_token=config.github_oauth_token
)
(id, content) = await self.get_pinboard(gh, channel) (id, content) = await self.get_pinboard(gh, channel)
content += "- " + data + "\n" content += "- " + data + "\n"
await gh.patch(f"/gists/{id}", data={ await gh.patch(
"files": { f"/gists/{id}", data={"files": {"pinboard.md": {"content": content}}}
"pinboard.md": { )
"content": content
}
}
})
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -136,7 +140,7 @@ class Pin(Cog):
break break
# Wait for the automated "Pinned" message so we can delete it # 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 # Pin the message
await target_msg.pin() await target_msg.pin()

View file

@ -22,12 +22,15 @@ class Remind(Cog):
if uid not in ctab["remind"][jobtimestamp]: if uid not in ctab["remind"][jobtimestamp]:
continue continue
job_details = ctab["remind"][jobtimestamp][uid] job_details = ctab["remind"][jobtimestamp][uid]
expiry_timestr = datetime.utcfromtimestamp(int(jobtimestamp))\ expiry_timestr = datetime.utcfromtimestamp(int(jobtimestamp)).strftime(
.strftime('%Y-%m-%d %H:%M:%S (UTC)') "%Y-%m-%d %H:%M:%S (UTC)"
embed.add_field(name=f"Reminder for {expiry_timestr}", )
embed.add_field(
name=f"Reminder for {expiry_timestr}",
value=f"Added on: {job_details['added']}, " value=f"Added on: {job_details['added']}, "
f"Text: {job_details['text']}", f"Text: {job_details['text']}",
inline=False) inline=False,
)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.cooldown(1, 60, type=commands.BucketType.user) @commands.cooldown(1, 60, type=commands.BucketType.user)
@ -40,27 +43,32 @@ class Remind(Cog):
expiry_timestamp = self.bot.parse_time(when) expiry_timestamp = self.bot.parse_time(when)
if current_timestamp + 5 > expiry_timestamp: if current_timestamp + 5 > expiry_timestamp:
msg = await ctx.send(f"{ctx.author.mention}: Minimum " msg = await ctx.send(
"remind interval is 5 seconds.") f"{ctx.author.mention}: Minimum " "remind interval is 5 seconds."
)
await asyncio.sleep(5) await asyncio.sleep(5)
await msg.delete() await msg.delete()
return return
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp) expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime, duration_text = self.bot.get_relative_timestamp(
include_to=True, time_to=expiry_datetime, include_to=True, humanized=True
humanized=True) )
safe_text = await commands.clean_content().convert(ctx, str(text)) safe_text = await commands.clean_content().convert(ctx, str(text))
added_on = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S (UTC)") added_on = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S (UTC)")
add_job("remind", add_job(
"remind",
ctx.author.id, ctx.author.id,
{"text": safe_text, "added": added_on}, {"text": safe_text, "added": added_on},
expiry_timestamp) expiry_timestamp,
)
msg = await ctx.send(f"{ctx.author.mention}: I'll remind you in " msg = await ctx.send(
f"DMs about `{safe_text}` in {duration_text}.") f"{ctx.author.mention}: I'll remind you in "
f"DMs about `{safe_text}` in {duration_text}."
)
await asyncio.sleep(5) await asyncio.sleep(5)
await msg.delete() await msg.delete()

View file

@ -33,17 +33,17 @@ class Robocronp(Cog):
for jobtimestamp in ctab[jobtype]: for jobtimestamp in ctab[jobtype]:
for job_name in ctab[jobtype][jobtimestamp]: for job_name in ctab[jobtype][jobtimestamp]:
job_details = repr(ctab[jobtype][jobtimestamp][job_name]) job_details = repr(ctab[jobtype][jobtimestamp][job_name])
embed.add_field(name=f"{jobtype} for {job_name}", embed.add_field(
value=f"Timestamp: {jobtimestamp}, " name=f"{jobtype} for {job_name}",
f"Details: {job_details}", value=f"Timestamp: {jobtimestamp}, " f"Details: {job_details}",
inline=False) inline=False,
)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.guild_only() @commands.guild_only()
@commands.check(check_if_staff) @commands.check(check_if_staff)
@commands.command(aliases=["removejob"]) @commands.command(aliases=["removejob"])
async def deletejob(self, ctx, timestamp: str, async def deletejob(self, ctx, timestamp: str, job_type: str, job_name: str):
job_type: str, job_name: str):
"""Removes a timed robocronp job, staff only. """Removes a timed robocronp job, staff only.
You'll need to supply: You'll need to supply:
@ -64,31 +64,34 @@ class Robocronp(Cog):
target_user = await self.bot.fetch_user(job_name) target_user = await self.bot.fetch_user(job_name)
target_guild = self.bot.get_guild(job_details["guild"]) target_guild = self.bot.get_guild(job_details["guild"])
delete_job(timestamp, jobtype, job_name) delete_job(timestamp, jobtype, job_name)
await target_guild.unban(target_user, await target_guild.unban(
reason="Robocronp: Timed " target_user, reason="Robocronp: Timed " "ban expired."
"ban expired.") )
elif jobtype == "unmute": elif jobtype == "unmute":
remove_restriction(job_name, config.mute_role) remove_restriction(job_name, config.mute_role)
target_guild = self.bot.get_guild(job_details["guild"]) target_guild = self.bot.get_guild(job_details["guild"])
target_member = target_guild.get_member(int(job_name)) target_member = target_guild.get_member(int(job_name))
target_role = target_guild.get_role(config.mute_role) target_role = target_guild.get_role(config.mute_role)
await target_member.remove_roles(target_role, await target_member.remove_roles(
reason="Robocronp: Timed " target_role, reason="Robocronp: Timed " "mute expired."
"mute expired.") )
delete_job(timestamp, jobtype, job_name) delete_job(timestamp, jobtype, job_name)
elif jobtype == "remind": elif jobtype == "remind":
text = job_details["text"] text = job_details["text"]
added_on = job_details["added"] added_on = job_details["added"]
target = await self.bot.fetch_user(int(job_name)) target = await self.bot.fetch_user(int(job_name))
if target: if target:
await target.send("You asked to be reminded about" await target.send(
f" `{text}` on {added_on}.") "You asked to be reminded about" f" `{text}` on {added_on}."
)
delete_job(timestamp, jobtype, job_name) delete_job(timestamp, jobtype, job_name)
except: except:
# Don't kill cronjobs if something goes wrong. # Don't kill cronjobs if something goes wrong.
delete_job(timestamp, jobtype, job_name) delete_job(timestamp, jobtype, job_name)
await log_channel.send("Crondo has errored, job deleted: ```" await log_channel.send(
f"{traceback.format_exc()}```") "Crondo has errored, job deleted: ```"
f"{traceback.format_exc()}```"
)
async def clean_channel(self, channel_id): async def clean_channel(self, channel_id):
log_channel = self.bot.get_channel(config.botlog_channel) log_channel = self.bot.get_channel(config.botlog_channel)
@ -101,12 +104,14 @@ class Robocronp(Cog):
count += len(purge_res) count += len(purge_res)
if len(purge_res) != 100: if len(purge_res) != 100:
done_cleaning = True done_cleaning = True
await log_channel.send(f"Wiped {count} messages from " await log_channel.send(
f"<#{channel.id}> automatically.") f"Wiped {count} messages from " f"<#{channel.id}> automatically."
)
except: except:
# Don't kill cronjobs if something goes wrong. # Don't kill cronjobs if something goes wrong.
await log_channel.send("Cronclean has errored: ```" await log_channel.send(
f"{traceback.format_exc()}```") "Cronclean has errored: ```" f"{traceback.format_exc()}```"
)
async def minutely(self): async def minutely(self):
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
@ -125,8 +130,9 @@ class Robocronp(Cog):
await self.clean_channel(clean_channel) await self.clean_channel(clean_channel)
except: except:
# Don't kill cronjobs if something goes wrong. # Don't kill cronjobs if something goes wrong.
await log_channel.send("Cron-minutely has errored: ```" await log_channel.send(
f"{traceback.format_exc()}```") "Cron-minutely has errored: ```" f"{traceback.format_exc()}```"
)
await asyncio.sleep(60) await asyncio.sleep(60)
async def hourly(self): async def hourly(self):
@ -144,8 +150,9 @@ class Robocronp(Cog):
await self.clean_channel(clean_channel) await self.clean_channel(clean_channel)
except: except:
# Don't kill cronjobs if something goes wrong. # Don't kill cronjobs if something goes wrong.
await log_channel.send("Cron-hourly has errored: ```" await log_channel.send(
f"{traceback.format_exc()}```") "Cron-hourly has errored: ```" f"{traceback.format_exc()}```"
)
# Your stuff that should run an hour after boot # Your stuff that should run an hour after boot
# and after that every hour goes here # and after that every hour goes here
@ -163,8 +170,9 @@ class Robocronp(Cog):
await self.bot.do_resetalgo(verif_channel, "daily robocronp") await self.bot.do_resetalgo(verif_channel, "daily robocronp")
except: except:
# Don't kill cronjobs if something goes wrong. # Don't kill cronjobs if something goes wrong.
await log_channel.send("Cron-daily has errored: ```" await log_channel.send(
f"{traceback.format_exc()}```") "Cron-daily has errored: ```" f"{traceback.format_exc()}```"
)
await asyncio.sleep(86400) await asyncio.sleep(86400)
# Your stuff that should run a day after boot # Your stuff that should run a day after boot
# and after that every day goes here # and after that every day goes here

View file

@ -10,115 +10,10 @@ import itertools
from helpers.checks import check_if_staff 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 <https://reswitched.team/faq/> 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: <https://discord.gg/C29hYvh>.**__
: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 <https://0bin.net/>.
""",
# 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): class Verification(Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
# https://docs.python.org/3.7/library/hashlib.html#shake-variable-length-digests self.hash_choice = random.choice(config.welcome_hashes)
self.blacklisted_hashes = {"shake_128", "shake_256"}
self.hash_choice = random.choice(tuple(hashlib.algorithms_guaranteed -
self.blacklisted_hashes))
# Export reset channel functions # Export reset channel functions
self.bot.do_reset = self.do_reset self.bot.do_reset = self.do_reset
@ -127,19 +22,21 @@ class Verification(Cog):
async def do_reset(self, channel, author, limit: int = 100): async def do_reset(self, channel, author, limit: int = 100):
await channel.purge(limit=limit) await channel.purge(limit=limit)
await channel.send(welcome_header) await channel.send(config.welcome_header)
rules = ['**{}**. {}'.format(i, cleandoc(r)) for i, r in rules = [
enumerate(welcome_rules, 1)] "**{}**. {}".format(i, cleandoc(r))
for i, r in enumerate(config.welcome_rules, 1)
]
rule_choice = random.randint(2, len(rules)) rule_choice = random.randint(2, len(rules))
hash_choice_str = self.hash_choice.upper() hash_choice_str = self.hash_choice.upper()
if hash_choice_str == "BLAKE2B": if hash_choice_str == "BLAKE2B":
hash_choice_str += "-512" hash_choice_str += "-512"
elif hash_choice_str == "BLAKE2S": elif hash_choice_str == "BLAKE2S":
hash_choice_str += "-256" hash_choice_str += "-256"
rules[rule_choice - 1] += \ rules[rule_choice - 1] += "\n" + config.hidden_term_line.format(hash_choice_str)
'\n' + hidden_term_line.format(hash_choice_str) msg = (
msg = f"🗑 **Reset**: {author} cleared {limit} messages "\ f"🗑 **Reset**: {author} cleared {limit} messages " f" in {channel.mention}"
f" in {channel.mention}" )
msg += f"\n💬 __Current challenge location__: under rule {rule_choice}" msg += f"\n💬 __Current challenge location__: under rule {rule_choice}"
log_channel = self.bot.get_channel(config.log_channel) log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(msg) await log_channel.send(msg)
@ -163,19 +60,21 @@ class Verification(Cog):
await channel.send(item) await channel.send(item)
await asyncio.sleep(1) await asyncio.sleep(1)
for x in welcome_footer: for x in config.welcome_footer:
await channel.send(cleandoc(x)) await channel.send(cleandoc(x))
await asyncio.sleep(1) await asyncio.sleep(1)
async def do_resetalgo(self, channel, author, limit: int = 100): async def do_resetalgo(self, channel, author, limit: int = 100):
# randomize hash_choice on reset # randomize hash_choice on reset
self.hash_choice = \ self.hash_choice = random.choice(
random.choice(tuple(hashlib.algorithms_guaranteed - tuple(
self.blacklisted_hashes - config.welcome_hashes
{self.hash_choice})) )
)
msg = f"📘 **Reset Algorithm**: {author} reset "\ msg = (
f"algorithm in {channel.mention}" f"📘 **Reset Algorithm**: {author} reset " f"algorithm in {channel.mention}"
)
msg += f"\n💬 __Current algorithm__: {self.hash_choice.upper()}" msg += f"\n💬 __Current algorithm__: {self.hash_choice.upper()}"
log_channel = self.bot.get_channel(config.log_channel) log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(msg) await log_channel.send(msg)
@ -187,8 +86,10 @@ class Verification(Cog):
async def reset(self, ctx, limit: int = 100, force: bool = False): async def reset(self, ctx, limit: int = 100, force: bool = False):
"""Wipes messages and pastes the welcome message again. Staff only.""" """Wipes messages and pastes the welcome message again. Staff only."""
if ctx.message.channel.id != config.welcome_channel and not force: if ctx.message.channel.id != config.welcome_channel and not force:
await ctx.send(f"This command is limited to" await ctx.send(
f" <#{config.welcome_channel}>, unless forced.") f"This command is limited to"
f" <#{config.welcome_channel}>, unless forced."
)
return return
await self.do_reset(ctx.channel, ctx.author.mention, limit) 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): async def resetalgo(self, ctx, limit: int = 100, force: bool = False):
"""Resets the verification algorithm and does what reset does. Staff only.""" """Resets the verification algorithm and does what reset does. Staff only."""
if ctx.message.channel.id != config.welcome_channel and not force: if ctx.message.channel.id != config.welcome_channel and not force:
await ctx.send(f"This command is limited to" await ctx.send(
f" <#{config.welcome_channel}>, unless forced.") f"This command is limited to"
f" <#{config.welcome_channel}>, unless forced."
)
return return
await self.do_resetalgo(ctx.channel, ctx.author.mention, limit) await self.do_resetalgo(ctx.channel, ctx.author.mention, limit)
@ -218,13 +121,20 @@ class Verification(Cog):
mcl = message.content.lower() mcl = message.content.lower()
# Reply to users that insult the bot # Reply to users that insult the bot
oof = ["bad", "broken", "buggy", "bugged", oof = [
"stupid", "dumb", "silly", "fuck", "heck", "h*ck"] "bad",
"broken",
"buggy",
"bugged",
"stupid",
"dumb",
"silly",
"fuck",
"heck",
"h*ck",
]
if "bot" in mcl and any(insult in mcl for insult in oof): if "bot" in mcl and any(insult in mcl for insult in oof):
snark = random.choice(["bad human", snark = random.choice(["bad human", "no u", "no u, rtfm", "pebkac"])
"no u",
"no u, rtfm",
"pebkac"])
return await chan.send(snark) return await chan.send(snark)
# Get the role we will give in case of success # 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 # Get a list of stuff we'll allow and will consider close
allowed_names = [f"@{full_name}", full_name, str(member.id)] allowed_names = [f"@{full_name}", full_name, str(member.id)]
close_names = [f"@{member.name}", member.name, discrim, close_names = [f"@{member.name}", member.name, discrim, f"#{discrim}"]
f"#{discrim}"]
# Now add the same things but with newlines at the end of them # Now add the same things but with newlines at the end of them
allowed_names += [(an + '\n') for an in allowed_names] allowed_names += [(an + "\n") for an in allowed_names]
close_names += [(cn + '\n') for cn in close_names] close_names += [(cn + "\n") for cn in close_names]
allowed_names += [(an + '\r\n') for an in allowed_names] allowed_names += [(an + "\r\n") for an in allowed_names]
close_names += [(cn + '\r\n') for cn in close_names] close_names += [(cn + "\r\n") for cn in close_names]
# [ ͡° ͜ᔦ ͡°] 𝐖𝐞𝐥𝐜𝐨𝐦𝐞 𝐭𝐨 𝐌𝐚𝐜 𝐎𝐒 𝟗. # [ ͡° ͜ᔦ ͡°] 𝐖𝐞𝐥𝐜𝐨𝐦𝐞 𝐭𝐨 𝐌𝐚𝐜 𝐎𝐒 𝟗.
allowed_names += [(an + '\r') for an in allowed_names] allowed_names += [(an + "\r") for an in allowed_names]
close_names += [(cn + '\r') for cn in close_names] close_names += [(cn + "\r") for cn in close_names]
# Finally, hash the stuff so that we can access them later :) # Finally, hash the stuff so that we can access them later :)
hash_allow = [hashlib.new(self.hash_choice, hash_allow = [
name.encode('utf-8')).hexdigest() hashlib.new(self.hash_choice, name.encode("utf-8")).hexdigest()
for name in allowed_names] for name in allowed_names
]
# I'm not even going to attempt to break those into lines jfc # I'm not even going to attempt to break those into lines jfc
if any(allow in mcl for allow in hash_allow): if any(allow in mcl for allow in hash_allow):
await member.add_roles(success_role) 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 # Detect if the user uses the wrong hash algorithm
wrong_hash_algos = hashlib.algorithms_guaranteed - \ wrong_hash_algos = (
{self.hash_choice} - self.blacklisted_hashes config.welcome_hashes
- {self.hash_choice}
)
for algo in wrong_hash_algos: for algo in wrong_hash_algos:
for name in itertools.chain(allowed_names, close_names): 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) 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}.") await log_channel.send(
return await chan.send(f"{message.author.mention} :no_entry: Close, but not quite. Go back and re-read!") 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." 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) rand_num = random.randint(1, 100)
if rand_num == 42: if rand_num == 42:
@ -271,7 +199,7 @@ class Verification(Cog):
elif rand_num == 43: elif rand_num == 43:
no_text = "ugh, wrong, read the rules." no_text = "ugh, wrong, read the rules."
elif rand_num == 44: 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}") await chan.send(f"{message.author.mention} {no_text}")
@Cog.listener() @Cog.listener()

View file

@ -1,3 +1,4 @@
import hashlib
import datetime import datetime
# Basic bot config, insert your token here, update description if you want # 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" rules_url = "https://reswitched.team/discord/#rules"
# The bot description to be used in .robocop embed # The bot description to be used in .robocop embed
embed_desc = "Robocop-NG is developed by [Ave](https://github.com/aveao)"\ embed_desc = (
" and [tomGER](https://github.com/tumGER), and is a rewrite "\ "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." "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 # 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) min_age = datetime.timedelta(minutes=15)
# The bot will only work in these guilds # The bot will only work in these guilds
guild_whitelist = [ guild_whitelist = [269333940928512010] # ReSwitched discord
269333940928512010 # ReSwitched discord
]
# Named roles to be used with .approve and .revoke # Named roles to be used with .approve and .revoke
# Example: .approve User hacker # Example: .approve User hacker
named_roles = { named_roles = {
"community": 420010997877833731, "community": 420010997877833731,
"hacker": 364508795038072833, "hacker": 364508795038072833,
"participant": 434353085926866946 "participant": 434353085926866946,
} }
# The bot manager and staff roles # The bot manager and staff roles
# Bot manager can run eval, exit and other destructive commands # Bot manager can run eval, exit and other destructive commands
# Staff can run administrative commands # Staff can run administrative commands
bot_manager_role_id = 466447265863696394 # Bot management role in ReSwitched bot_manager_role_id = 466447265863696394 # Bot management role in ReSwitched
staff_role_ids = [364647829248933888, # Team role in ReSwitched staff_role_ids = [
364647829248933888, # Team role in ReSwitched
360138431524765707, # Mod role in ReSwitched 360138431524765707, # Mod role in ReSwitched
466447265863696394, # Bot management role in ReSwitched 466447265863696394, # Bot management role in ReSwitched
360138163156549632, # Admin role in ReSwitched 360138163156549632, # Admin role in ReSwitched
287289529986187266] # Wizard role in ReSwitched 287289529986187266, # Wizard role in ReSwitched
]
# Various log channels used to log bot and guild's activity # Various log channels used to log bot and guild's activity
# You can use same channel for multiple log types # 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 # These channel entries are used to determine which roles will be given
# access when we unmute on them # access when we unmute on them
general_channels = [420029476634886144, general_channels = [
420029476634886144,
414949821003202562, 414949821003202562,
383368936466546698, 383368936466546698,
343244421044633602, 343244421044633602,
491316901692178432, 491316901692178432,
539212260350885908] # Channels everyone can access 539212260350885908,
community_channels = [269333940928512010, ] # Channels everyone can access
community_channels = [
269333940928512010,
438839875970662400, 438839875970662400,
404722395845361668, 404722395845361668,
435687501068501002, 435687501068501002,
286612533757083648] # Channels requiring community role 286612533757083648,
] # Channels requiring community role
# Controls which roles are blocked during lockdown # Controls which roles are blocked during lockdown
lockdown_configs = { lockdown_configs = {
# Used as a default value for channels without a config # Used as a default value for channels without a config
"default": { "default": {"channels": general_channels, "roles": [named_roles["participant"]]},
"channels": general_channels,
"roles": [named_roles["participant"]]
},
"community": { "community": {
"channels": community_channels, "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 # 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 # As we no longer have mute role on ReSwitched, I set it to 0 here
mute_role = 0 # Mute role in ReSwitched 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 = [] minutely_clean_channels = []
hourly_clean_channels = [] hourly_clean_channels = []
# Edited and deletes messages in these channels will be logged # Edited and deletes messages in these channels will be logged
spy_channels = general_channels 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: <https://nh-server.github.io/switch-guide/>
**Specific guides:**
Manually Updating/Downgrading (with HOS): <https://switch.homebrew.guide/usingcfw/manualupgrade>
Manually Repairing/Downgrading (without HOS): <https://switch.homebrew.guide/usingcfw/manualchoiupgrade>
How to set up a Homebrew development environment: <https://devkitpro.org/wiki/Getting_Started>
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: <https://akdm.github.io/ssnc/checker/>
"""
# == 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 <https://reswitched.team/faq/> 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: <https://discord.gg/C29hYvh>.**__
: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 <https://0bin.net/>.
""",
# 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 # Channels and roles where users can pin messages
allowed_pin_channels = [] allowed_pin_channels = []
allowed_pin_roles = [] allowed_pin_roles = []
# Used for the pinboard. Leave empty if you don't wish for a gist pinboard. # Channel to upload text files while editing list items. (They are cleaned up.)
github_oauth_token = "" 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 = []

View file

@ -16,17 +16,19 @@ def check_if_bot_manager(ctx):
def check_if_staff_or_ot(ctx): def check_if_staff_or_ot(ctx):
if not ctx.guild: if not ctx.guild:
return True return True
is_ot = (ctx.channel.name == "off-topic") is_ot = ctx.channel.name == "off-topic"
is_bot_cmds = (ctx.channel.name == "bot-cmds") is_bot_cmds = ctx.channel.name == "bot-cmds"
is_staff = any(r.id in config.staff_role_ids for r in ctx.author.roles) 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): def check_if_collaborator(ctx):
if not ctx.guild: if not ctx.guild:
return False return False
return any(r.id in config.staff_role_ids + config.allowed_pin_roles return any(
for r in ctx.author.roles) r.id in config.staff_role_ids + config.allowed_pin_roles
for r in ctx.author.roles
)
def check_if_pin_channel(ctx): def check_if_pin_channel(ctx):

View file

@ -99,24 +99,18 @@ switch_modules = {
212: "GRC ", 212: "GRC ",
216: "Migration ", 216: "Migration ",
217: "Migration Idc Server ", 217: "Migration Idc Server ",
# Libnx # Libnx
345: "libnx ", 345: "libnx ",
346: "Homebrew ABI ", 346: "Homebrew ABI ",
347: "Homebrew Loader ", 347: "Homebrew Loader ",
348: "libnx Nvidia", 348: "libnx Nvidia",
349: "libnx Binder", 349: "libnx Binder",
# Support Errors # Support Errors
800: "General web-applet", 800: "General web-applet",
809: "WifiWebAuthApplet", 809: "WifiWebAuthApplet",
810: "Whitelisted-applet", 810: "Whitelisted-applet",
811: "ShopN", 811: "ShopN",
# Custom Sysmodules # Custom Sysmodules
311: "SwitchPresence", 311: "SwitchPresence",
} }
@ -150,11 +144,11 @@ switch_known_errcodes = {
0xFC01: "Reserved value ", 0xFC01: "Reserved value ",
0xFE01: "Invalid hardware breakpoint ", 0xFE01: "Invalid hardware breakpoint ",
0x10001: "[Usermode] Fatal exception ", 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 ", 0x10601: "Port closed ",
0x10801: "Resource limit exceeded ", 0x10801: "Resource limit exceeded ",
0x20801: "Command buffer too small ", 0x20801: "Command buffer too small ",
0x40a01: "Invalid process ID.", 0x40A01: "Invalid process ID.",
0x40C01: "Invalid thread ID.", 0x40C01: "Invalid thread ID.",
0x40E01: "Invalid thread ID (used in svcGetDebugThreadParam).", 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", 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 ", 0x3EA03: "Invalid handle ",
0x3EE03: "Invalid memory mirror ", 0x3EE03: "Invalid memory mirror ",
0x7FE03: "TLS slot is not allocated ", 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 ", 0xE05: "TitleId not found ",
0x1805: "Invalid StorageId ", 0x1805: "Invalid StorageId ",
0xDC05: "Gamecard not inserted ", 0xDC05: "Gamecard not inserted ",
@ -273,7 +267,7 @@ switch_known_errcodes = {
0xA09: "Invalid files. ", 0xA09: "Invalid files. ",
0xE09: "Already registered. ", 0xE09: "Already registered. ",
0x1009: "Title not found. ", 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 ", 0x6609: "Invalid memory state/permission ",
0x6A09: "Invalid NRR ", 0x6A09: "Invalid NRR ",
0xA209: "Unaligned NRR address ", 0xA209: "Unaligned NRR address ",
@ -281,15 +275,15 @@ switch_known_errcodes = {
0xAA09: "Bad NRR address ", 0xAA09: "Bad NRR address ",
0xAE09: "Bad initialization ", 0xAE09: "Bad initialization ",
0xC809: "Unknown ACI0 descriptor ", 0xC809: "Unknown ACI0 descriptor ",
0xCE09: "ACID/ACI0 don\'t match for descriptor KernelFlags ", 0xCE09: "ACID/ACI0 don't match for descriptor KernelFlags ",
0xD009: "ACID/ACI0 don\'t match for descriptor SyscallMask ", 0xD009: "ACID/ACI0 don't match for descriptor SyscallMask ",
0xD409: "ACID/ACI0 don\'t match for descriptor MapIoOrNormalRange ", 0xD409: "ACID/ACI0 don't match for descriptor MapIoOrNormalRange ",
0xD609: "ACID/ACI0 don\'t match for descriptor MapNormalPage ", 0xD609: "ACID/ACI0 don't match for descriptor MapNormalPage ",
0xDE09: "ACID/ACI0 don\'t match for descriptor InterruptPair ", 0xDE09: "ACID/ACI0 don't match for descriptor InterruptPair ",
0xE209: "ACID/ACI0 don\'t match for descriptor ApplicationType ", 0xE209: "ACID/ACI0 don't match for descriptor ApplicationType ",
0xE409: "ACID/ACI0 don\'t match for descriptor KernelReleaseVersion ", 0xE409: "ACID/ACI0 don't match for descriptor KernelReleaseVersion ",
0xE609: "ACID/ACI0 don\'t match for descriptor HandleTableSize ", 0xE609: "ACID/ACI0 don't match for descriptor HandleTableSize ",
0xE809: "ACID/ACI0 don\'t match for descriptor DebugFlags ", 0xE809: "ACID/ACI0 don't match for descriptor DebugFlags ",
0x1940A: "Invalid CMIF header size. ", 0x1940A: "Invalid CMIF header size. ",
0x1A60A: "Invalid CMIF input header. ", 0x1A60A: "Invalid CMIF input header. ",
0x1A80A: "Invalid CMIF output header. ", 0x1A80A: "Invalid CMIF output header. ",
@ -300,7 +294,7 @@ switch_known_errcodes = {
0x20B: "Unsupported operation ", 0x20B: "Unsupported operation ",
0xCC0B: "Out of server session memory ", 0xCC0B: "Out of server session memory ",
0x11A0B: "Went past maximum during marshalling. ", 0x11A0B: "Went past maximum during marshalling. ",
0x1900B: "Session doesn\'t support domains. ", 0x1900B: "Session doesn't support domains. ",
0x25A0B: "Remote process is dead. ", 0x25A0B: "Remote process is dead. ",
0x3260B: "Unknown request type ", 0x3260B: "Unknown request type ",
0x3D60B: "IPC Query 1 failed. ", 0x3D60B: "IPC Query 1 failed. ",
@ -343,8 +337,8 @@ switch_known_errcodes = {
0x1BC69: "Empty settings item key ", 0x1BC69: "Empty settings item key ",
0x1E269: "Setting group name is too long (64 character limit?) ", 0x1E269: "Setting group name is too long (64 character limit?) ",
0x1E469: "Setting 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_\-.]) ", 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_\-.]) ", 0x20C69: "Setting name ends with '.' or contains invalid characters (allowed: [a-z0-9_\-.]) ",
0x4DA69: "Null language code buffer ", 0x4DA69: "Null language code buffer ",
0x4EE69: "Null network settings buffer ", 0x4EE69: "Null network settings buffer ",
0x4F069: "Null network settings output count buffer ", 0x4F069: "Null network settings output count buffer ",
@ -469,7 +463,7 @@ switch_known_errcodes = {
0xD48C: "Invalid descriptor ", 0xD48C: "Invalid descriptor ",
0x1928C: "USB device not bound / interface already enabled ", 0x1928C: "USB device not bound / interface already enabled ",
0x299: "Invalid audio device ", 0x299: "Invalid audio device ",
0x499: "Operation couldn\'t complete successfully ", 0x499: "Operation couldn't complete successfully ",
0x699: "Invalid sample rate ", 0x699: "Invalid sample rate ",
0x899: "Buffer size too small ", 0x899: "Buffer size too small ",
0x1099: "Too many buffers are still unreleased ", 0x1099: "Too many buffers are still unreleased ",
@ -486,7 +480,7 @@ switch_known_errcodes = {
0xF0CD: "IR image data not available/ready. ", 0xF0CD: "IR image data not available/ready. ",
0x35B: "Failed to init SM. ", 0x35B: "Failed to init SM. ",
0x55B: "Failed to init FS. ", 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. ", 0x95B: "Failed to read NRO header. ",
0xB5B: "Invalid NRO magic. ", 0xB5B: "Invalid NRO magic. ",
0xD5B: "Invalid NRO segments. ", 0xD5B: "Invalid NRO segments. ",
@ -508,7 +502,7 @@ switch_known_errcodes = {
0x480: "Storage not available.", 0x480: "Storage not available.",
0x1987E: "Development/debug-only behavior", 0x1987E: "Development/debug-only behavior",
0xD27E: "Invalid database entry count", 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", 0x87E: "Entry not found",
0x27E: "Invalid argument", 0x27E: "Invalid argument",
0x7BC74: "Unimplemented functionality", 0x7BC74: "Unimplemented functionality",
@ -565,15 +559,12 @@ switch_known_errcodes = {
0xC47A: "Invalid operation", 0xC47A: "Invalid operation",
0x290: "Exited Abnormally ([[Applet_Manager_services#LibraryAppletExitReason|ExitReason]] == Abormal)", 0x290: "Exited Abnormally ([[Applet_Manager_services#LibraryAppletExitReason|ExitReason]] == Abormal)",
0x690: "Canceled ([[Applet_Manager_services#LibraryAppletExitReason|ExitReason]] == Canceled)", 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)", 0xA90: "Exited Unexpectedly ([[Applet_Manager_services#LibraryAppletExitReason|ExitReason]] == Unexpected)",
0x58ACA: "Npad ID is out of range.", 0x58ACA: "Npad ID is out of range.",
0x1A8CD: "IR camera handle pointer is null.", 0x1A8CD: "IR camera handle pointer is null.",
0x198CD: "IR camera invalid handle value.", 0x198CD: "IR camera invalid handle value.",
# FS Codes # FS Codes
0xD401: "Error: Passed buffer is not usable for fs library. ", 0xD401: "Error: Passed buffer is not usable for fs library. ",
0x177A02: "Error: Specified value is out of range. ", 0x177A02: "Error: Specified value is out of range. ",
0x2F5C02: "Error: Invalid size was specified.", 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(). ", 0x307202: "Error: OpenMode_AllowAppend is required for implicit extension of file size by WriteFile(). ",
0x346402: "Error: Enough journal space is not left. ", 0x346402: "Error: Enough journal space is not left. ",
0x346A02: "Error: The open count of files and directories reached the limitation. ", 0x346A02: "Error: The open count of files and directories reached the limitation. ",
# Fatal # Fatal
0x4A2: "Can be triggered by running svcBreak. The svcBreak params have no affect on the value of the thrown error-code.", 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", 0xA8: "Userland ARM undefined instruction exception",
0x2A8: "Userland ARM prefetch-abort due to PC set to non-executable region", 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.", 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 ", 0x6A8: "Userland PC address not aligned to 4 bytes ",
0x10A8: "Can occur when attempting to call an svc outside the whitelist ", 0x10A8: "Can occur when attempting to call an svc outside the whitelist ",
# Libnx Errors from libnx/result.h # Libnx Errors from libnx/result.h
# - Normal - # - Normal -
0x359: "LibnxError_BadReloc", 0x359: "LibnxError_BadReloc",
0x559: "LibnxError_OutOfMemory", 0x559: "LibnxError_OutOfMemory",
@ -641,7 +628,6 @@ switch_known_errcodes = {
0x5959: "LibnxError_NvinfoFailedToInitialize", 0x5959: "LibnxError_NvinfoFailedToInitialize",
0x5B59: "LibnxError_NvbufFailedToInitialize", 0x5B59: "LibnxError_NvbufFailedToInitialize",
0x5D59: "LibnxError_LibAppletBadExit", 0x5D59: "LibnxError_LibAppletBadExit",
# - Libnx Binder - # - Libnx Binder -
0x35D: "LibnxBinderError_Unknown", 0x35D: "LibnxBinderError_Unknown",
0x55D: "LibnxBinderError_NoMemory", 0x55D: "LibnxBinderError_NoMemory",
@ -660,7 +646,6 @@ switch_known_errcodes = {
0x1F5D: "LibnxBinderError_TimedOut", 0x1F5D: "LibnxBinderError_TimedOut",
0x215D: "LibnxBinderError_UnknownTransaction", 0x215D: "LibnxBinderError_UnknownTransaction",
0x235D: "LibnxBinderError_FdsNotAllowed", 0x235D: "LibnxBinderError_FdsNotAllowed",
# - LibNX Nvidia - # - LibNX Nvidia -
0x35C: "LibnxNvidiaError_Unknown", 0x35C: "LibnxNvidiaError_Unknown",
0x55C: "LibnxNvidiaError_NotImplemented", 0x55C: "LibnxNvidiaError_NotImplemented",
@ -681,91 +666,81 @@ switch_known_errcodes = {
0x235C: "LibnxNvidiaError_SharedMemoryTooSmall", 0x235C: "LibnxNvidiaError_SharedMemoryTooSmall",
0x255C: "LibnxNvidiaError_FileOperationFailed", 0x255C: "LibnxNvidiaError_FileOperationFailed",
0x275C: "LibnxNvidiaError_IoctlFailed", 0x275C: "LibnxNvidiaError_IoctlFailed",
# Non-SwitchBrew Error Codes - Should probably add them to SwitchBrew if you read this # Non-SwitchBrew Error Codes - Should probably add them to SwitchBrew if you read this
0x7E12B: "Eshop connection failed",
0x7E12B: 'Eshop connection failed', 0x39D689: "CDN Ban",
0x39D689: 'CDN Ban', 0x3E8E7C: "Error in account login/creation",
0x3E8E7C: 'Error in account login/creation', 0x3E8EA0: "Failed connection test",
0x3E8EA0: 'Failed connection test', 0x1F4E7C: "(normal) console ban",
0x1F4E7C: '(normal) console ban', 0x27EE7C: "(potential) complete account ban", # This error is still super new, needs more informations
0x27EE7C: '(potential) complete account ban', # This error is still super new, needs more informations
0x36B72B: "Access token expired", 0x36B72B: "Access token expired",
0x1F486E: "Internet connection lost because the console entered sleep mode.", 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.", 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.", 0x5089: "Failed to snprintf the AccountGetDynamicEtickets (personalized ticket) request JSON data.",
0x6410: "GetApplicationControlData: unable to find control for the input title ID", 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?)", 0x16473: "Could not mount tag (invalid tag type?)",
0x8073: "Device unavailable", 0x8073: "Device unavailable",
0x10073: "App area not found", 0x10073: "App area not found",
0x11073: "Tag corrupted?", 0x11073: "Tag corrupted?",
0xc880: "thrown by AM when qlaunch is terminated", 0xC880: "thrown by AM when qlaunch is terminated",
0xc87c: "invalid user", 0xC87C: "invalid user",
0xc7e: "mii already exists", 0xC7E: "mii already exists",
0xa7e: "full database", 0xA7E: "full database",
0x87e: "mii not found", 0x87E: "mii not found",
0x115b: "[HBL] Stopped loading NROs", 0x115B: "[HBL] Stopped loading NROs",
0x48c69: "device_cert_ecc_b223 failed to load", 0x48C69: "device_cert_ecc_b223 failed to load",
0x138E02: "gamecard cmd buffer too small - must be 0x40 (or bigger)", 0x138E02: "gamecard cmd buffer too small - must be 0x40 (or bigger)",
0x138E02: "gc out of bounds sector access", 0x138E02: "gc out of bounds sector access",
0x13DC02: "gc sector start is out of range for partition 1", 0x13DC02: "gc sector start is out of range for partition 1",
0x13D802: "gc sector end out of range for partition 1", 0x13D802: "gc sector end out of range for partition 1",
0x13DA02: "gc sector wrong partition access", 0x13DA02: "gc sector wrong partition access",
# 0x3E8E89: 'Failed to access Firmware Updates - Often because of DNS!', # 0x3E8E89: 'Failed to access Firmware Updates - Often because of DNS!',
# ^ Also used by libcurl # ^ Also used by libcurl
# Atmosphere # Atmosphere
0xCAFEF: "Atmosphere: Version Mismatch", 0xCAFEF: "Atmosphere: Version Mismatch",
# Pegaswitch # Pegaswitch
0xA7200: "Fake-Error by Pegaswitch",
0xa7200: "Fake-Error by Pegaswitch",
# SwitchPresence # SwitchPresence
#Archived because the plugin got discontinued # Archived because the plugin got discontinued
#0x337: "Error_InitSocket", # 0x337: "Error_InitSocket",
#0x537: "Error_Listen", # 0x537: "Error_Listen",
#0x737: "Error_Accepting", # 0x737: "Error_Accepting",
#0x937: "Error_ListAppFailed", # 0x937: "Error_ListAppFailed",
# 0xb37: "Error_InvalidMagic", # 0xb37: "Error_InvalidMagic",
#0xd37: "Error_CmdIdNotConfirm", # 0xd37: "Error_CmdIdNotConfirm",
#0xf37: "Error_CmdIdNotSendBuff", # 0xf37: "Error_CmdIdNotSendBuff",
#0x1137: "Error_RecData", # 0x1137: "Error_RecData",
#0x1337: "Error_SendData", # 0x1337: "Error_SendData",
#0x1537: "Error_InitNS", # 0x1537: "Error_InitNS",
#0x1737: "Error_InitACC", # 0x1737: "Error_InitACC",
#0x1937: "Error_GetControlData", # 0x1937: "Error_GetControlData",
#0x1b37: "Error_InvalidControlSize", # 0x1b37: "Error_InvalidControlSize",
#0x1d37: "Error_GetAciveUser", # 0x1d37: "Error_GetAciveUser",
#0x1f37: "Error_GetProfile", # 0x1f37: "Error_GetProfile",
#0x2137: "Error_ProfileGet", # 0x2137: "Error_ProfileGet",
#0x2337: "Error_InitPMDMNT", # 0x2337: "Error_InitPMDMNT",
#0x2537: "Error_GetAppPid", # 0x2537: "Error_GetAppPid",
#0x2737: "Error_GetProcessTid", # 0x2737: "Error_GetProcessTid",
#0x2937: "Error_InitPMINFO", # 0x2937: "Error_InitPMINFO",
#0x2b37: "Error_GetPidList", # 0x2b37: "Error_GetPidList",
#0x2d37: "Error_GetDebugProc", # 0x2d37: "Error_GetDebugProc",
#0x2f37: "Error_CloseHandle", # 0x2f37: "Error_CloseHandle",
# Joke # Joke
0xDEADBEEF: "Congrats, you found some hexspeak \n \n https://www.youtube.com/watch?v=DLzxrzFCyOs", 0xDEADBEEF: "Congrats, you found some hexspeak \n \n https://www.youtube.com/watch?v=DLzxrzFCyOs",
# By Ave # 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.", 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 = { switch_known_errcode_ranges = {
# NIM # NIM
137: [ 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 # FS
2: [ 2: [
[2000, 2499, "Error: Failed to access SD card."], [2000, 2499, "Error: Failed to access SD card."],
@ -786,12 +761,19 @@ switch_known_errcode_ranges = {
[6300, 6399, "Error: Unsupported operation."], [6300, 6399, "Error: Unsupported operation."],
[6400, 6499, "Error: Permission denied."], [6400, 6499, "Error: Permission denied."],
], ],
# NIFM Support Page Links # NIFM Support Page Links
110: [ 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 ;^) # 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 = { switch_game_err = {
# Splatoon 2 # Splatoon 2
"2-AAB6A-3400": "Splatoon 2: A kick from online due to exefs edits.", "2-AAB6A-3400": "Splatoon 2: A kick from online due to exefs edits.",
# Youtube # Youtube
"2-ARVHA-0000": "Youtube: Unknown Error", "2-ARVHA-0000": "Youtube: Unknown Error",
} }
@ -847,272 +828,271 @@ switch_support_page = {
} }
dds_summaries = { dds_summaries = {
0: 'Success', 0: "Success",
1: 'Nothing happened', 1: "Nothing happened",
2: 'Would block', 2: "Would block",
3: 'Out of resource', 3: "Out of resource",
4: 'Not found', 4: "Not found",
5: 'Invalid state', 5: "Invalid state",
6: 'Not supported', 6: "Not supported",
7: 'Invalid argument', 7: "Invalid argument",
8: 'Wrong argument', 8: "Wrong argument",
9: 'Canceled', 9: "Canceled",
10: 'Status changed', 10: "Status changed",
11: 'Internal', 11: "Internal",
63: 'Invalid result value' 63: "Invalid result value",
} }
dds_levels = { dds_levels = {
0: "Success", 0: "Success",
1: "Info", 1: "Info",
25: "Status", 25: "Status",
26: "Temporary", 26: "Temporary",
27: "Permanent", 27: "Permanent",
28: "Usage", 28: "Usage",
29: "Reinitialize", 29: "Reinitialize",
30: "Reset", 30: "Reset",
31: "Fatal" 31: "Fatal",
} }
dds_modules = { dds_modules = {
0: 'Common', 0: "Common",
1: 'Kernel', 1: "Kernel",
2: 'Util', 2: "Util",
3: 'File server', 3: "File server",
4: 'Loader server', 4: "Loader server",
5: 'TCB', 5: "TCB",
6: 'OS', 6: "OS",
7: 'DBG', 7: "DBG",
8: 'DMNT', 8: "DMNT",
9: 'PDN', 9: "PDN",
10: 'GSP', 10: "GSP",
11: 'I2C', 11: "I2C",
12: 'GPIO', 12: "GPIO",
13: 'DD', 13: "DD",
14: 'CODEC', 14: "CODEC",
15: 'SPI', 15: "SPI",
16: 'PXI', 16: "PXI",
17: 'FS', 17: "FS",
18: 'DI', 18: "DI",
19: 'HID', 19: "HID",
20: 'CAM', 20: "CAM",
21: 'PI', 21: "PI",
22: 'PM', 22: "PM",
23: 'PM_LOW', 23: "PM_LOW",
24: 'FSI', 24: "FSI",
25: 'SRV', 25: "SRV",
26: 'NDM', 26: "NDM",
27: 'NWM', 27: "NWM",
28: 'SOC', 28: "SOC",
29: 'LDR', 29: "LDR",
30: 'ACC', 30: "ACC",
31: 'RomFS', 31: "RomFS",
32: 'AM', 32: "AM",
33: 'HIO', 33: "HIO",
34: 'Updater', 34: "Updater",
35: 'MIC', 35: "MIC",
36: 'FND', 36: "FND",
37: 'MP', 37: "MP",
38: 'MPWL', 38: "MPWL",
39: 'AC', 39: "AC",
40: 'HTTP', 40: "HTTP",
41: 'DSP', 41: "DSP",
42: 'SND', 42: "SND",
43: 'DLP', 43: "DLP",
44: 'HIO_LOW', 44: "HIO_LOW",
45: 'CSND', 45: "CSND",
46: 'SSL', 46: "SSL",
47: 'AM_LOW', 47: "AM_LOW",
48: 'NEX', 48: "NEX",
49: 'Friends', 49: "Friends",
50: 'RDT', 50: "RDT",
51: 'Applet', 51: "Applet",
52: 'NIM', 52: "NIM",
53: 'PTM', 53: "PTM",
54: 'MIDI', 54: "MIDI",
55: 'MC', 55: "MC",
56: 'SWC', 56: "SWC",
57: 'FatFS', 57: "FatFS",
58: 'NGC', 58: "NGC",
59: 'CARD', 59: "CARD",
60: 'CARDNOR', 60: "CARDNOR",
61: 'SDMC', 61: "SDMC",
62: 'BOSS', 62: "BOSS",
63: 'DBM', 63: "DBM",
64: 'Config', 64: "Config",
65: 'PS', 65: "PS",
66: 'CEC', 66: "CEC",
67: 'IR', 67: "IR",
68: 'UDS', 68: "UDS",
69: 'PL', 69: "PL",
70: 'CUP', 70: "CUP",
71: 'Gyroscope', 71: "Gyroscope",
72: 'MCU', 72: "MCU",
73: 'NS', 73: "NS",
74: 'News', 74: "News",
75: 'RO', 75: "RO",
76: 'GD', 76: "GD",
77: 'Card SPI', 77: "Card SPI",
78: 'EC', 78: "EC",
79: 'Web Browser', 79: "Web Browser",
80: 'Test', 80: "Test",
81: 'ENC', 81: "ENC",
82: 'PIA', 82: "PIA",
83: 'ACT', 83: "ACT",
84: 'VCTL', 84: "VCTL",
85: 'OLV', 85: "OLV",
86: 'NEIA', 86: "NEIA",
87: 'NPNS', 87: "NPNS",
90: 'AVD', 90: "AVD",
91: 'L2B', 91: "L2B",
92: 'MVD', 92: "MVD",
93: 'NFC', 93: "NFC",
94: 'UART', 94: "UART",
95: 'SPM', 95: "SPM",
96: 'QTM', 96: "QTM",
97: 'NFP (amiibo)', 97: "NFP (amiibo)",
254: 'Application', 254: "Application",
255: 'Invalid result value' 255: "Invalid result value",
} }
dds_descriptions = { dds_descriptions = {
0: 'Success', 0: "Success",
2: 'Invalid memory permissions (kernel)', 2: "Invalid memory permissions (kernel)",
4: 'Invalid ticket version (AM)', 4: "Invalid ticket version (AM)",
5: 'Invalid string length. This error is returned when service name length is greater than 8 or zero. (srv)', 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)', 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)', 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).', 8: "Camera already in use/busy (qtm).",
10: 'Not enough memory (os)', 10: "Not enough memory (os)",
26: 'Session closed by remote (os)', 26: "Session closed by remote (os)",
32: 'Empty CIA? (AM)', 32: "Empty CIA? (AM)",
37: 'Invalid NCCH? (AM)', 37: "Invalid NCCH? (AM)",
39: 'Invalid title version (AM)', 39: "Invalid title version (AM)",
43: 'Database doesn\'t exist/failed to open (AM)', 43: "Database doesn't exist/failed to open (AM)",
44: 'Trying to uninstall system-app (AM)', 44: "Trying to uninstall system-app (AM)",
47: 'Invalid command header (OS)', 47: "Invalid command header (OS)",
101: 'Archive not mounted/mount-point not found (fs)', 101: "Archive not mounted/mount-point not found (fs)",
105: 'Request timed out (http)', 105: "Request timed out (http)",
106: 'Invalid signature/CIA? (AM)', 106: "Invalid signature/CIA? (AM)",
120: 'Title/object not found? (fs)', 120: "Title/object not found? (fs)",
141: 'Gamecard not inserted? (fs)', 141: "Gamecard not inserted? (fs)",
190: 'Object does already exist/failed to create object.', 190: "Object does already exist/failed to create object.",
230: 'Invalid open-flags / permissions? (fs)', 230: "Invalid open-flags / permissions? (fs)",
250: 'FAT operation denied (fs?)', 250: "FAT operation denied (fs?)",
271: 'Invalid configuration (mvd).', 271: "Invalid configuration (mvd).",
335: '(No permission? Seemed to appear when JKSM was being used without its XML.)', 335: "(No permission? Seemed to appear when JKSM was being used without its XML.)",
391: 'NCCH hash-check failed? (fs)', 391: "NCCH hash-check failed? (fs)",
392: 'RSA/AES-MAC verification failed? (fs)', 392: "RSA/AES-MAC verification failed? (fs)",
393: 'Invalid database? (AM)', 393: "Invalid database? (AM)",
395: 'RomFS/Savedata hash-check failed? (fs)', 395: "RomFS/Savedata hash-check failed? (fs)",
630: 'Command not allowed / missing permissions? (fs)', 630: "Command not allowed / missing permissions? (fs)",
702: 'Invalid path? (fs)', 702: "Invalid path? (fs)",
740: '(Occurred when NDS card was inserted and attempting to use AM_GetTitleCount on MEDIATYPE_GAME_CARD.) (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)', 761: "Incorrect read-size for ExeFS? (fs)",
1000: 'Invalid selection', 1000: "Invalid selection",
1001: 'Too large', 1001: "Too large",
1002: 'Not authorized', 1002: "Not authorized",
1003: 'Already done', 1003: "Already done",
1004: 'Invalid size', 1004: "Invalid size",
1005: 'Invalid enum value', 1005: "Invalid enum value",
1006: 'Invalid combination', 1006: "Invalid combination",
1007: 'No data', 1007: "No data",
1008: 'Busy', 1008: "Busy",
1009: 'Misaligned address', 1009: "Misaligned address",
1010: 'Misaligned size', 1010: "Misaligned size",
1011: 'Out of memory', 1011: "Out of memory",
1012: 'Not implemented', 1012: "Not implemented",
1013: 'Invalid address', 1013: "Invalid address",
1014: 'Invalid pointer', 1014: "Invalid pointer",
1015: 'Invalid handle', 1015: "Invalid handle",
1016: 'Not initialized', 1016: "Not initialized",
1017: 'Already initialized', 1017: "Already initialized",
1018: 'Not found', 1018: "Not found",
1019: 'Cancel requested', 1019: "Cancel requested",
1020: 'Already exists', 1020: "Already exists",
1021: 'Out of range', 1021: "Out of range",
1022: 'Timeout', 1022: "Timeout",
1023: 'Invalid result value' 1023: "Invalid result value",
} }
# Nintendo Error Codes # Nintendo Error Codes
dds_errcodes = { dds_errcodes = {
# Nintendo 3DS # Nintendo 3DS
'001-0502': 'Some sort of network error related to friend presence. "Allow Friends to see your online status" might fix this.', "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.', "001-0803": "Could not communicate with authentication server.",
'002-0102': 'System is permanently banned by Nintendo. ', "002-0102": "System is permanently banned by Nintendo. ",
'002-0107': 'System is temporarily(?) banned by Nintendo. ', "002-0107": "System is temporarily(?) banned by Nintendo. ",
'002-0119': 'System update required (outdated friends-module)', "002-0119": "System update required (outdated friends-module)",
'002-0120': 'Title update required (outdated title version)', "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-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. ', "002-0123": "System is generally banned by Nintendo. ",
'003-1099': 'Access point could not be found with the given SSID.', "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.', "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-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-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-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.', "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-0102": "Unexpected error. Could probably happen trying to play an out-of-region title online?",
'006-0332': 'Disconnected from the game server.', "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-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.', "006-0612": "Failed to join the session.",
'007-0200': 'Could not access SD card.', "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-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 <https://support.nintendo.com/networkstatus> to make sure the servers are operating normally. You may also encounter this error if you have a weak internet connection.', "007-2100": "The connection to the Nintendo eShop timed out.\nThis may be due to an ongoing server maintenance, check <https://support.nintendo.com/networkstatus> 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-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-2720": "SSL error?",
'007-2916': 'HTTP error, server is probably down. Try again later?', "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-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-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-2923": "The Nintendo Servers are currently down for maintenance. Please try again later.",
'007-3102': 'Cannot find title on Nintendo eShop. Probably pulled.', "007-3102": "Cannot find title on Nintendo eShop. Probably pulled.",
'007-6054': 'Occurs when ticket database is full (8192 tickets).', "007-6054": "Occurs when ticket database is full (8192 tickets).",
'009-1000': 'System update required. (friends module?)', "009-1000": "System update required. (friends module?)",
'009-2916': 'NIM HTTP error, server is probably down. Try again later?', "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-2913": "NIM HTTP error, server is probably down. Try again later?",
'009-4079': 'Could not access SD card. General purpose error.', "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-4998": '"Local content is newer."\nThe actual cause of this error is unknown.',
'009-6106': '"AM error in NIM."\nProbably a bad ticket.', "009-6106": '"AM error in NIM."\nProbably a bad ticket.',
'009-8401': 'Update data corrupted. Delete and re-install.', "009-8401": "Update data corrupted. Delete and re-install.",
'011-3021': 'Cannot find title on Nintendo eShop. Probably incorrect region, or never existed.', "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-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. ', "011-6901": "System is banned by Nintendo, this error code description is oddly Japanese, generic error code. ",
'012-1511': 'Certificate warning.', "012-1511": "Certificate warning.",
'014-0016': 'Both systems have the same movable.sed key. Format the target and try system transfer again.', "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.', "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-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-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-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 <https://id.nintendo.net/account/forgotten-password>', "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 <https://id.nintendo.net/account/forgotten-password>",
'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-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-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<steps on how to fix>\n\nTinyFormat is recommended for unlinking in the future.', "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<steps on how to fix>\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-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.', "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".', "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.', "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 = { wii_u_errors = {
'102-2802': 'NNID is permanently banned by Nintendo. ', "102-2802": "NNID is permanently banned by Nintendo. ",
'102-2805': 'System is banned from accessing Nintendo eShop. ', "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-2812": "System + linked NNID and access to online services are permanently banned by Nintendo. ",
'102-2813': 'System is 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-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-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. ', "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-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. ', "106-0346": "NNID is permanently banned from a/multiple games (preferably Splatoon) online multiplayer. ",
'115-1009': 'System is permanently banned from Miiverse.', "115-1009": "System is permanently banned from Miiverse.",
'121-0902': 'Permissions missing for the action you are trying to perfrom (Miiverse error).', "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).', "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-0101": '"Generic error". Can happen when formatting a system with CBHC.',
'160-0102': 'Error in SLC/MLC or USB.', "160-0102": "Error in SLC/MLC or USB.",
'160-0103': '"The system memory is corrupted (MLC)."', "160-0103": '"The system memory is corrupted (MLC)."',
'160-0104': '"The system memory is corrupted (SLC)."', "160-0104": '"The system memory is corrupted (SLC)."',
'160-0105': 'USB storage corrupted?', "160-0105": "USB storage corrupted?",
'199-9999': 'Usually occurs when trying to run an unsigned title without signature patches, or something unknown(?) is corrupted.', "199-9999": "Usually occurs when trying to run an unsigned title without signature patches, or something unknown(?) is corrupted.",
} }
# 1K (+120) Lines PogChamp # 1K (+120) Lines PogChamp

View file

@ -1,11 +1,13 @@
import json import json
import time import time
userlog_event_types = {"warns": "Warn", userlog_event_types = {
"warns": "Warn",
"bans": "Ban", "bans": "Ban",
"kicks": "Kick", "kicks": "Kick",
"mutes": "Mute", "mutes": "Mute",
"notes": "Note"} "notes": "Note",
}
def get_userlog(): def get_userlog():
@ -18,24 +20,35 @@ def set_userlog(contents):
f.write(contents) f.write(contents)
def userlog(uid, issuer, reason, event_type, uname: str = ""): def fill_userlog(userid, uname):
userlogs = get_userlog() userlogs = get_userlog()
uid = str(uid) uid = str(userid)
if uid not in userlogs: if uid not in userlogs:
userlogs[uid] = {"warns": [], userlogs[uid] = {
"warns": [],
"mutes": [], "mutes": [],
"kicks": [], "kicks": [],
"bans": [], "bans": [],
"notes": [], "notes": [],
"watch": False, "watch": False,
"name": "n/a"} "name": "n/a",
}
if uname: if uname:
userlogs[uid]["name"] = 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()) timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
log_data = {"issuer_id": issuer.id, log_data = {
"issuer_id": issuer.id,
"issuer_name": f"{issuer}", "issuer_name": f"{issuer}",
"reason": reason, "reason": reason,
"timestamp": timestamp} "timestamp": timestamp,
}
if event_type not in userlogs[uid]: if event_type not in userlogs[uid]:
userlogs[uid][event_type] = [] userlogs[uid][event_type] = []
userlogs[uid][event_type].append(log_data) 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 = ""): def setwatch(uid, issuer, watch_state, uname: str = ""):
userlogs = get_userlog() userlogs, uid = fill_userlog(uid, uname)
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]["watch"] = watch_state userlogs[uid]["watch"] = watch_state
set_userlog(json.dumps(userlogs)) set_userlog(json.dumps(userlogs))