def maybe_timestamp(when: datetime, since: datetime = None) -> str: since = since or datetime.now() secs_from_now = (when - since).total_seconds() if secs_from_now > 10800: # 3 hours return utils.format_dt(when, style="f") elif secs_from_now > 60: return utils.format_dt(when, style="R") else: return f"in {'%.0f' % secs_from_now} seconds"
async def update_netinfo(self): async with self.bot.session.get( 'https://www.nintendo.co.jp/netinfo/en_US/status.json?callback=getJSON', timeout=45) as r: if r.status == 200: j = await r.json() else: self.netinfo_embed.description = "Failure when checking the Network Maintenance Information page." logger.warning("Status %s while trying to update netinfo.", r.status) return now = datetime.now(self.tz) embed = discord.Embed( title="Network Maintenance Information / Online Status", url="https://www.nintendo.co.jp/netinfo/en_US/index.html", timestamp=now) embed.set_footer(text="This information was last updated:") for status_type in ("operational_statuses", "temporary_maintenances"): descriptor = "Maintenance" if status_type == "temporary_maintenances" else "Status" for entry in j[status_type]: if "platform" in entry: entry_desc = ', '.join(entry["platform"]).replace( "nintendo", "Nintendo").replace("web", "Web") else: entry_desc = 'No console specified.' begin = datetime(year=2000, month=1, day=1, tzinfo=self.tz) end = datetime(year=2099, month=1, day=1, tzinfo=self.tz) if "begin" in entry: begin = self.netinfo_parse_time(entry["begin"]) entry_desc += '\nBegins: ' + format_dt(begin) if "end" in entry: end = self.netinfo_parse_time(entry["end"]) entry_desc += '\nEnds: ' + format_dt(end) if now < end: entry_name = "{} {}: {}".format( "Current" if begin <= now else "Upcoming", descriptor, entry["software_title"].replace(' <br />\r\n', ', ')) if "services" in entry: entry_name += ", " + ', '.join(entry["services"]) embed.add_field(name=entry_name, value=entry_desc, inline=False) if len(embed.fields) == 0: embed.description = "No ongoing or upcoming maintenances." self.netinfo_embed = embed
async def handle_get_user_status_entries(self, user_id) -> list[tuple[str, str]]: inactive_days = await InactivitySettings.inactive_days.get() activity: Optional[Activity] = await db.get(Activity, id=user_id) if activity is None: status = t.status.inactive elif (utcnow() - activity.timestamp).days >= inactive_days: status = t.status.inactive_since( format_dt(activity.timestamp, style="R")) else: status = t.status.active(format_dt(activity.timestamp, style="R")) return [(t.activity, status)]
class HeartbeatCog(Cog, name="Heartbeat"): CONTRIBUTORS = [Contributor.Defelo, Contributor.wolflu] def __init__(self): super().__init__() self.initialized = False def get_owner(self) -> Optional[User]: return self.bot.get_user(OWNER_ID) @tasks.loop(seconds=20) async def status_loop(self): if (owner := self.get_owner()) is None: return try: await send_editable_log( owner, t.online_status, t.status_description(Config.NAME, Config.VERSION), t.heartbeat, format_dt(now := utcnow(), style="D") + " " + format_dt(now, style="T"), ) Path("health").write_text(str(int(datetime.now().timestamp()))) except Forbidden: pass
async def timeban_member(self, ctx: GuildContext, member: Union[discord.Member, discord.User], length: int = commands.parameter(converter=DateOrTimeToSecondsConverter), *, reason=""): """Bans a user for a limited period of time. OP+ only.\n\nLength format: #d#h#m#s""" if await check_bot_or_staff(ctx, member, "timeban"): return timestamp = datetime.datetime.now() delta = datetime.timedelta(seconds=length) unban_time = timestamp + delta unban_time_string = format_dt(unban_time) if isinstance(member, discord.Member): msg = f"You were banned from {ctx.guild.name}." if reason != "": msg += " The given reason is: " + reason msg += f"\n\nThis ban lasts until {unban_time_string}." await send_dm_message(member, msg, ctx) try: self.bot.actions.append("ub:" + str(member.id)) await ctx.guild.ban(member, reason=reason, delete_message_days=0) except discord.errors.Forbidden: await ctx.send("๐ข I don't have permission to do this.") return await crud.add_timed_restriction(member.id, unban_time, 'timeban') await ctx.send(f"{member} is now b& until {unban_time_string}. ๐") msg = f"โ **Time ban**: {ctx.author.mention} banned {member.mention} until {unban_time_string} | {member}\n๐ท __User ID__: {member.id}" if reason != "": msg += "\nโ๏ธ __Reason__: " + reason await self.bot.channels['server-logs'].send(msg) signature = command_signature(ctx.command) await self.bot.channels['mod-logs'].send(msg + (f"\nPlease add an explanation below. " f"In the future, it is recommended to use `{signature}`" f" as the reason is automatically sent to the user." if reason == "" else ""))
async def snowflake(self, ctx: Context, arg: int): if arg < 0: raise CommandError(t.invalid_snowflake) try: await reply(ctx, format_dt(snowflake_time(arg), style="F")) except (OverflowError, ValueError, OSError): raise CommandError(t.invalid_snowflake)
def create_embed(self, reminder: RemindMeEntry): embed = discord.Embed(color=self.colour) embed.title = f"Reminder {self.idx + 1}" embed.add_field(name='Content', value=reminder.reminder, inline=False) embed.add_field(name='Set to', value=format_dt(reminder.date), inline=False) if self.n_pages > 1: embed.title += f" [{self.idx + 1}/{self.n_pages}]" return embed
async def ban_member_slash(self, interaction: discord.Interaction, member: discord.Member, reason: str = "", delete_messages: app_commands.Range[int, 0, 7] = 0, duration: app_commands.Transform[int, TimeTransformer] = None): """Bans a user from the server. OP+ only.""" if await check_bot_or_staff(interaction, member, "ban"): return msg = f"You were banned from {interaction.guild.name}." if reason != "": msg += " The given reason is: " + reason if duration is not None: timestamp = datetime.datetime.now() delta = datetime.timedelta(seconds=duration) unban_time = timestamp + delta unban_time_string = format_dt(unban_time) try: await interaction.guild.ban(member, reason=reason, delete_message_days=delete_messages) except discord.errors.Forbidden: await interaction.response.send_message("๐ข I don't have permission to do this.") return self.bot.actions.append("ub:" + str(member.id)) await crud.add_timed_restriction(member.id, unban_time, 'timeban') msg += f"\n\nThis ban expires in {unban_time_string}." msg_send = await send_dm_message(member, msg) await interaction.response.send_message(f"{member} is now b& until {unban_time_string}. ๐" + ("\nFailed to send DM message" if not msg_send else "")) msg = f"โ **Time ban**: {interaction.user.mention} banned {member.mention} until {unban_time_string} | {member}\n๐ท __User ID__: {member.id}" else: try: await interaction.guild.ban(member, reason=reason, delete_message_days=delete_messages) except discord.errors.Forbidden: await interaction.response.send_message("๐ข I don't have permission to do this.") return self.bot.actions.append("ub:" + str(member.id)) await crud.remove_timed_restriction(member.id, 'timeban') msg += "\n\nThis ban does not expire." msg_send = await send_dm_message(member, msg) await interaction.response.send_message(f"{member} is now b&. ๐" + ("\nFailed to send DM message" if not msg_send else "")) msg = f"โ **Ban**: {interaction.user.mention} banned {member.mention} | {self.bot.escape_text(member)}\n๐ท __User ID__: {member.id}" if reason != "": msg += "\nโ๏ธ __Reason__: " + reason await self.bot.channels['server-logs'].send(msg) await self.bot.channels['mod-logs'].send(msg + ("\nPlease add an explanation below. In the future, it is recommended add a reason as it is automatically sent to the user." if reason == "" else ""))
async def user_notes_show(self, ctx: Context, *, user: UserMemberConverter): user: Union[User, Member] embed = Embed(title=t.user_notes, colour=Colors.user_notes) embed.set_author(name=f"{user} ({user.id})", icon_url=user.display_avatar.url) note: UserNote async for note in await db.stream( select(UserNote).filter_by(member_id=user.id)): embed.add_field( name=format_dt(note.timestamp, style="D") + " " + format_dt(note.timestamp, style="T"), value=t.user_note_entry(id=note.id, author=f"<@{note.author_id}>", content=note.content), inline=False, ) if not embed.fields: embed.colour = Colors.error embed.description = t.no_notes await send_long_embed(ctx, embed, paginate=True)
async def on_ready(self): if (owner := self.get_owner()) is not None: try: await send_editable_log( owner, t.online_status, t.status_description(Config.NAME, Config.VERSION), t.logged_in, format_dt(now := utcnow(), style="D") + " " + format_dt(now, style="T"), force_resend=True, force_new_embed=not self.initialized, ) except Forbidden: pass
async def userinfo(self, ctx: Context, user: Optional[Union[User, int]] = None): """ show information about a user """ user, user_id, arg_passed = await get_user(ctx, user, UserInfoPermission.view_userinfo) embed = Embed(title=t.userinfo, color=Colors.stats) if isinstance(user, int): embed.set_author(name=str(user)) else: embed.set_author(name=f"{user} ({user_id})", icon_url=user.display_avatar.url) for response in await get_user_info_entries(user_id): for name, value in response: embed.add_field(name=name, value=value, inline=True) if (member := self.bot.guilds[0].get_member(user_id)) is not None: status = t.member_since(format_dt(member.joined_at))
def test_format_dt(dt: datetime.datetime, style: typing.Optional[utils.TimestampStyle], formatted: str): assert utils.format_dt(dt, style=style) == formatted
class InactivityCog(Cog, name="Inactivity"): CONTRIBUTORS = [Contributor.Defelo] async def on_message(self, message: Message): if message.guild is None: return await Activity.update(message.author.id, message.created_at) role: Role for role in message.role_mentions: await Activity.update(role.id, message.created_at) @commands.command() @InactivityPermission.scan.check @max_concurrency(1) @guild_only() async def scan(self, ctx: Context, days: int): """ scan all channels for latest message of each user """ if days <= 0: raise CommandError(tg.invalid_duration) await scan(ctx, days) @get_user_status_entries.subscribe async def handle_get_user_status_entries(self, user_id) -> list[tuple[str, str]]: inactive_days = await InactivitySettings.inactive_days.get() activity: Optional[Activity] = await db.get(Activity, id=user_id) if activity is None: status = t.status.inactive elif (utcnow() - activity.timestamp).days >= inactive_days: status = t.status.inactive_since( format_dt(activity.timestamp, style="R")) else: status = t.status.active(format_dt(activity.timestamp, style="R")) return [(t.activity, status)] @commands.command(aliases=["in"]) @InactivityPermission.read.check @guild_only() async def inactive(self, ctx: Context, days: Optional[int], *roles: Optional[Role]): """ list inactive users """ if role := ctx.guild.get_role(days): roles += (role, ) days = None if days is None: days = await InactivitySettings.inactive_days.get() elif days not in range(1, 10001): raise CommandError(tg.invalid_duration) now = utcnow() @db_wrapper async def load_member(m: Member) -> tuple[Member, Optional[datetime]]: ts = await db.get(Activity, id=m.id) return m, ts.timestamp if ts else None if roles: members: set[Member] = { member for role in roles for member in role.members } else: members: set[Member] = set(ctx.guild.members) last_activity: list[tuple[ Member, Optional[datetime]]] = await semaphore_gather( 50, *map(load_member, members)) last_activity.sort( key=lambda a: (a[1].timestamp() if a[1] else -1, str(a[0]))) out = [] for member, timestamp in last_activity: if timestamp is None: out.append( t.user_inactive(status_icon(member.status), member.mention, f"@{member}")) elif timestamp >= now - timedelta(days=days): break else: out.append( t.user_inactive_since(status_icon(member.status), member.mention, f"@{member}", format_dt(timestamp, style="R"))) embed = Embed(title=t.inactive_users, colour=0x256BE6) if out: embed.title = t.inactive_users_cnt(len(out)) embed.description = "\n".join(out) else: embed.description = t.no_inactive_users embed.colour = 0x03AD28 await send_long_embed(ctx, embed, paginate=True)
class Logs(commands.Cog): """ Logs join and leave messages, bans and unbans, and member changes. """ def __init__(self, bot: Kurisu): self.bot: Kurisu = bot self.bot.loop.create_task(self.init_rules()) welcome_msg = """ Hello {0}, welcome to the {1} server on Discord! Please review all of the rules in {2} before asking for help or chatting. In particular, we do not allow assistance relating to piracy. You can find a list of staff and helpers in {2}. Do you simply need a place to start hacking your 3DS system? Check out **<https://3ds.hacks.guide>**! Do you simply need a place to start hacking your Wii U system? Check out **<https://wiiu.hacks.guide>**! Do you simply need a place to start hacking your Switch system? Check out **<https://nh-server.github.io/switch-guide/>**! By participating in this server, you acknowledge that user data (including messages, user IDs, user tags) will be collected and logged for moderation purposes. If you disagree with this collection, please leave the server immediately. Thanks for stopping by and have a good time! """ # ughhhhhhhh async def init_rules(self): await self.bot.wait_until_all_ready() self.logo_nitro = discord.utils.get( self.bot.guild.emojis, name="nitro") or discord.PartialEmoji.from_str("โ") self.logo_boost = discord.utils.get( self.bot.guild.emojis, name="boost") or discord.PartialEmoji.from_str("โ") self.nitro_msg = ( f"Thanks for boosting {self.logo_nitro} Nintendo Homebrew!\n" f"As a Nitro Booster you have the following bonuses:\n" f"- React permissions in {self.bot.channels['off-topic'].mention}, {self.bot.channels['elsewhere'].mention}," f" and {self.bot.channels['nintendo-discussion'].mention}.\n" f"- Able to use the `.nickme` command in DMs with Kurisu to change your nickname every 6 hours.\n" f"- Able to stream in the {self.bot.channels['streaming-gamer'].mention} voice channel.\n" f"Thanks for boosting and have a good time! {self.logo_boost}") @commands.Cog.listener() async def on_member_join(self, member): await self.bot.wait_until_all_ready() msg = f"โ **Join**: {member.mention} | {self.bot.escape_text(member)}\n๐ __Creation__: {member.created_at}\n๐ท __User ID__: {member.id}" softban = await crud.get_softban(member.id) if softban: message_sent = await send_dm_message( member, f"This account has not been permitted to participate in {self.bot.guild.name}." f" The reason is: {softban.reason}") self.bot.actions.append("sbk:" + str(member.id)) await member.kick() msg = f"๐จ **Attempted join**: {member.mention} is soft-banned by <@{softban.issuer}> | {self.bot.escape_text(member)}" if not message_sent: msg += "\nThis message did not send to the user." embed = discord.Embed(color=discord.Color.red()) embed.description = softban.reason await self.bot.channels['server-logs'].send(msg, embed=embed) return perm_roles = await crud.get_permanent_roles(member.id) if perm_roles: roles = [ member.guild.get_role(perm_role.id) for perm_role in perm_roles ] await member.add_roles(*roles) warns = await crud.get_warns(member.id) if len(warns) == 0: await self.bot.channels['server-logs'].send(msg) else: embed = discord.Embed(color=discord.Color.dark_red()) embed.set_author(name=f"Warns for {member}", icon_url=member.display_avatar.url) for idx, warn in enumerate(warns): when = discord.utils.snowflake_time( warn.id).strftime('%Y-%m-%d %H:%M:%S') name = self.bot.escape_text( (await self.bot.fetch_user(warn.issuer)).display_name) embed.add_field(name=f"{idx + 1}: {when}", value=f"Issuer: {name}\nReason: {warn.reason}") await self.bot.channels['server-logs'].send(msg, embed=embed) await send_dm_message( member, self.welcome_msg.format( member.name, member.guild.name, self.bot.channels['welcome-and-rules'].mention)) @commands.Cog.listener() async def on_member_remove(self, member): await self.bot.wait_until_all_ready() if self.bot.pruning is True: return if "uk:" + str(member.id) in self.bot.actions: self.bot.actions.remove("uk:" + str(member.id)) return if "sbk:" + str(member.id) in self.bot.actions: self.bot.actions.remove("sbk:" + str(member.id)) return msg = f"{'๐ข **Auto-kick**' if 'wk:' + str(member.id) in self.bot.actions else 'โฌ ๏ธ **Leave**'}: {member.mention} | {self.bot.escape_text(member)}\n๐ท __User ID__: {member.id}" await self.bot.channels['server-logs'].send(msg) if "wk:" + str(member.id) in self.bot.actions: self.bot.actions.remove("wk:" + str(member.id)) await self.bot.channels['mod-logs'].send(msg) @commands.Cog.listener() async def on_member_ban(self, guild, member): await self.bot.wait_until_all_ready() ban = await guild.fetch_ban(member) auto_ban = 'wb:' + str(member.id) in self.bot.actions if "ub:" + str(member.id) in self.bot.actions: self.bot.actions.remove("ub:" + str(member.id)) return msg = f"{'โ **Auto-ban**' if auto_ban else 'โ **Ban**'}: {member.mention} | {self.bot.escape_text(member)}\n๐ท __User ID__: {member.id}" if ban.reason: msg += "\nโ๏ธ __Reason__: " + ban.reason if auto_ban: self.bot.actions.remove("wb:" + str(member.id)) await self.bot.channels['mods'].send(msg) await self.bot.channels['server-logs'].send(msg) if not ban.reason: msg += "\nThe responsible staff member should add an explanation below." await self.bot.channels['mod-logs'].send(msg) @commands.Cog.listener() async def on_member_unban(self, guild, user): await self.bot.wait_until_all_ready() if "tbr:" + str(user.id) in self.bot.actions: self.bot.actions.remove("tbr:" + str(user.id)) return msg = f"โ ๏ธ **Unban**: {user.mention} | {self.bot.escape_text(user)}" if await crud.get_time_restriction_by_user_type(user.id, 'timeban'): msg += "\nTimeban removed." await crud.remove_timed_restriction(user.id, 'timeban') await self.bot.channels['mod-logs'].send(msg) @commands.Cog.listener() async def on_member_update(self, member_before, member_after): await self.bot.wait_until_all_ready() do_log = False # only nickname and roles should be logged dest = self.bot.channels['server-logs'] roles_before = set(member_before.roles) roles_after = set(member_after.roles) msg = "" if roles_before ^ roles_after: do_log = True # role removal if roles_before - roles_after: msg = "\n๐ __Role removal__: " roles = [] for role in roles_before: if role.name == "@everyone": continue role_name = self.bot.escape_text(role.name) if role not in roles_after: roles.append("_~~" + role_name + "~~_") else: roles.append(role_name) msg += ', '.join(roles) # role addition elif diff := roles_after - roles_before: msg = "\n๐ __Role addition__: " roles = [] if self.bot.roles["Nitro Booster"] in diff: try: await member_after.send(self.nitro_msg) except discord.Forbidden: pass for role in roles_after: if role.name == "@everyone": continue role_name = self.bot.escape_text(role.name) if role not in roles_before: roles.append("__**" + role_name + "**__") else: roles.append(role_name) msg += ', '.join(roles) if member_before.nick != member_after.nick: do_log = True if member_before.nick is None: msg = "\n๐ท __Nickname addition__" elif member_after.nick is None: msg = "\n๐ท __Nickname removal__" else: msg = "\n๐ท __Nickname change__" msg += f": {self.bot.escape_text(member_before.nick)} โ {self.bot.escape_text(member_after.nick)}" if member_before.timed_out_until != member_after.timed_out_until: do_log = True if member_before.timed_out_until is None: msg = "\n๐ท __Timeout addition__" elif member_after.timed_out_until is None: msg = "\n๐ท __Timeout removal__" else: msg = "\n๐ท __Timeout change__" timeout_before = format_dt( member_before.timed_out_until ) if member_before.timed_out_until else 'None' timeout_after = format_dt( member_after.timed_out_until ) if member_after.timed_out_until else 'None' msg += f": {timeout_before} โ {timeout_after}" if do_log: msg = f"โน๏ธ **Member update**: {member_after.mention} | {self.bot.escape_text(member_after)} {msg}" await dest.send(msg)
def format_relative(dt): """Returns the discord markdown for a relative timestamp""" return format_dt(dt, style="R")
async def userlogs(self, ctx: Context, user: Optional[Union[User, int]] = None): """ show moderation log of a user """ guild: Guild = self.bot.guilds[0] user, user_id, arg_passed = await get_user(ctx, user, UserInfoPermission.view_userlog) out: list[tuple[datetime, str]] = [(snowflake_time(user_id), t.ulog.created)] join: Join async for join in await db.stream(filter_by(Join, member=user_id)): out.append((join.timestamp, t.ulog.joined(join.member_name))) leave: Leave async for leave in await db.stream(filter_by(Leave, member=user_id)): out.append((leave.timestamp, t.ulog.left)) username_update: UsernameUpdate async for username_update in await db.stream(filter_by(UsernameUpdate, member=user_id)): if not username_update.nick: msg = t.ulog.username_updated(username_update.member_name, username_update.new_name) elif username_update.member_name is None: msg = t.ulog.nick.set(username_update.new_name) elif username_update.new_name is None: msg = t.ulog.nick.cleared(username_update.member_name) else: msg = t.ulog.nick.updated(username_update.member_name, username_update.new_name) out.append((username_update.timestamp, msg)) if await RoleSettings.get("verified") in {role.id for role in guild.roles}: verification: Verification async for verification in await db.stream(filter_by(Verification, member=user_id)): if verification.accepted: out.append((verification.timestamp, t.ulog.verification.accepted)) else: out.append((verification.timestamp, t.ulog.verification.revoked)) responses = await get_userlog_entries(user_id, ctx.author) for response in responses: out += response out.sort() embed = Embed(title=t.userlogs, color=Colors.userlog) if isinstance(user, int): embed.set_author(name=str(user)) else: embed.set_author(name=f"{user} ({user_id})", icon_url=user.display_avatar.url) for row in out: name = format_dt(row[0], style="D") + " " + format_dt(row[0], style="T") value = row[1] embed.add_field(name=name, value=value, inline=False) if arg_passed: await send_long_embed(ctx, embed, paginate=True) else: try: await send_long_embed(ctx.author, embed) except (Forbidden, HTTPException): raise CommandError(t.could_not_send_dm) await ctx.message.add_reaction(name_to_emoji["white_check_mark"])
class ErrorHandler(vbu.Cog): COMMAND_ERROR_RESPONSES = ( (vbu.errors.MissingRequiredArgumentString, lambda ctx, error: gt( "errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True ).gettext( "You're missing `{parameter_name}`, which is required for this command.", ).format(parameter_name=error.param)), (commands.MissingRequiredArgument, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext( "You're missing `{parameter_name}`, which is required for this command.", ).format(parameter_name=error.param.name)), ((commands.UnexpectedQuoteError, commands.InvalidEndOfQuotedStringError, commands.ExpectedClosingQuoteError), lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("The quotes in your message have been done incorrectly.", )), (commands.CommandOnCooldown, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("You can use this command again in {timestamp}.", ). format(timestamp=utils.format_dt( utils.utcnow() + timedelta(seconds=error.retry_after), style="R")) ), (vbu.errors.BotNotReady, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext( "The bot isn't ready to start processing that command yet - please wait.", )), (commands.NSFWChannelRequired, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("You can only run this command in channels set as NSFW.", )), (commands.IsNotSlashCommand, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("This command can only be run as a slash command.", )), (commands.DisabledCommand, lambda ctx, error: gt( "errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True).gettext("This command has been disabled.", )), (vbu.errors.NotBotSupport, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext( "You need to be part of the bot's support team to be able to run this command.", )), (commands.MissingAnyRole, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext( "You need to have at least one of {roles} to be able to run this command.", ).format(roles=', '.join(f"`{i.mention}`" for i in error.missing_roles))), (commands.BotMissingAnyRole, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext( "I need to have one of the {roles} roles for you to be able to run this command.", ).format(roles=', '.join(f"`{i.mention}`" for i in error.missing_roles))), (commands.MissingRole, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext( "You need to have the `{role}` role to be able to run this command.", ).format(role=error.missing_role)), (commands.BotMissingRole, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext( "I need to have the `{role}` role for you to be able to run this command.", ).format(role=error.missing_role)), (commands.MissingPermissions, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("You need the `{permission}` permission to run this command.", ).format(permission=error.missing_permissions[0].replace( "_", " "))), (commands.BotMissingPermissions, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext( "I need the `{permission}` permission for me to be able to run this command.", ).format(permission=error.missing_permissions[0].replace("_", " "))), (commands.NoPrivateMessage, lambda ctx, error: gt( "errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True).gettext("This command can't be run in DMs.", )), (commands.PrivateMessageOnly, lambda ctx, error: gt( "errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True).gettext("This command can only be run in DMs.", )), (commands.NotOwner, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("You need to be registered as an owner to run this command.", )), (commands.MessageNotFound, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert `{argument}` into a message.", ).format( argument=error.argument)), (commands.MemberNotFound, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert `{argument}` into a guild member.", ).format(argument=error.argument)), (commands.UserNotFound, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert `{argument}` into a user.", ).format( argument=error.argument)), (commands.ChannelNotFound, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert `{argument}` into a channel.", ).format( argument=error.argument)), (commands.ChannelNotReadable, lambda ctx, error: gt( "errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True).gettext("I can't read messages in <#{id}>.", ). format(id=error.argument.id)), (commands.BadColourArgument, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert `{argument}` into a colour.", ).format( argument=error.argument)), (commands.RoleNotFound, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert `{argument}` into a role.", ).format( argument=error.argument)), (commands.BadInviteArgument, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert `{argument}` into an invite.", ).format( argument=error.argument)), ((commands.EmojiNotFound, commands.PartialEmojiConversionFailure), lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert `{argument}` into an emoji.", ).format( argument=error.argument)), (commands.BadBoolArgument, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert `{argument}` into a boolean.", ).format( argument=error.argument)), (commands.BadUnionArgument, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("I couldn't convert your provided `{parameter_name}`.", ).format(parameter_name=error.param.name)), (commands.BadArgument, lambda ctx, error: str(error).format(ctx=ctx, error=error)), # ( # commands.CommandNotFound, # This is only handled in slash commands # lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True).gettext( # "I wasn't able to find that command to be able to run it.", # ) # ), (commands.MaxConcurrencyReached, lambda ctx, error: gt( "errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True).gettext("You can't run this command right now.", )), (commands.TooManyArguments, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("You gave too many arguments to this command.", )), (discord.NotFound, lambda ctx, error: str(error).format(ctx=ctx, error=error)), (commands.CheckFailure, lambda ctx, error: str(error).format(ctx=ctx, error=error)), (discord.Forbidden, lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext("Discord is saying I'm unable to perform that action.", )), ((discord.HTTPException, aiohttp.ClientOSError), lambda ctx, error: gt("errors", localedir=LOCALE_PATH, languages=[ctx.locale], fallback=True). gettext( "Either I or Discord messed up running this command. Please try again later.", )), # Disabled because they're base classes for the subclasses above # (commands.CommandError, lambda ctx, error: ""), # (commands.CheckFailure, lambda ctx, error: ""), # (commands.CheckAnyFailure, lambda ctx, error: ""), # (commands.CommandInvokeError, lambda ctx, error: ""), # (commands.UserInputError, lambda ctx, error: ""), # (commands.ConversionError, lambda ctx, error: ""), # (commands.ArgumentParsingError, lambda ctx, error: ""), # Disabled because they all refer to extension and command loading # (commands.ExtensionError, lambda ctx, error: ""), # (commands.ExtensionAlreadyLoaded, lambda ctx, error: ""), # (commands.ExtensionNotLoaded, lambda ctx, error: ""), # (commands.NoEntryPointError, lambda ctx, error: ""), # (commands.ExtensionFailed, lambda ctx, error: ""), # (commands.ExtensionNotFound, lambda ctx, error: ""), # (commands.CommandRegistrationError, lambda ctx, error: ""), ) async def send_to_ctx_or_author( self, ctx: vbu.Context, text: str, author_text: str = None) -> typing.Optional[discord.Message]: """ Tries to send the given text to ctx, but failing that, tries to send it to the author instead. If it fails that too, it just stays silent. """ kwargs = { "content": text, "allowed_mentions": discord.AllowedMentions.none() } if isinstance(ctx, commands.SlashContext) and self.bot.config.get( "ephemeral_error_messages", True): kwargs.update({"ephemeral": True}) try: return await ctx.send(**kwargs) except discord.Forbidden: kwargs["content"] = text or author_text try: return await ctx.author.send(**kwargs) except discord.Forbidden: pass except discord.NotFound: pass return None @vbu.Cog.listener() async def on_command_error(self, ctx: vbu.Context, error: commands.CommandError): """ Global error handler for all the commands around wew. """ # Set up some errors that are just straight up ignored ignored_errors = ( commands.CommandNotFound, vbu.errors.InvokedMetaCommand, ) if isinstance(error, ignored_errors): return # See what we've got to deal with setattr(ctx, "original_author_id", getattr(ctx, "original_author_id", ctx.author.id)) # Set up some errors that the owners are able to bypass owner_reinvoke_errors = ( commands.MissingRole, commands.MissingAnyRole, commands.MissingPermissions, commands.CommandOnCooldown, commands.DisabledCommand, commands.CheckFailure, vbu.errors.IsNotUpgradeChatSubscriber, vbu.errors.IsNotVoter, vbu.errors.NotBotSupport, ) if isinstance(error, owner_reinvoke_errors ) and ctx.original_author_id in self.bot.owner_ids: if not self.bot.config.get("owners_ignore_check_failures", True) and isinstance( error, commands.CheckFailure): pass else: return await ctx.reinvoke() # See if the command itself has an error handler AND it isn't a locally handlled arg # if hasattr(ctx.command, "on_error") and not isinstance(ctx.command, vbu.Command): if hasattr(ctx.command, "on_error"): return # See if it's in our list of common outputs output = None error_found = False for error_types, function in self.COMMAND_ERROR_RESPONSES: if isinstance(error, error_types): error_found = True output = function(ctx, error) break # See if they're tryina f**k me up if output is not None and ctx.message and output in ctx.message.content and isinstance( error, commands.NotOwner): output = "\N{UNAMUSED FACE}" # Send a message based on the output if output: try: _, _ = output except ValueError: output = (output, ) return await self.send_to_ctx_or_author(ctx, *output) # Make sure not to send an error if it's "handled" if error_found: return # The output isn't a common output -- send them a plain error response try: await ctx.send(f"`{str(error).strip()}`", allowed_mentions=discord.AllowedMentions.none()) except (discord.Forbidden, discord.NotFound): pass # Ping unhandled errors to the owners and to the event webhook error_string = "".join( traceback.format_exception(None, error, error.__traceback__)) file_handle = io.StringIO(error_string + "\n") guild_id = ctx.guild.id if ctx.guild else None error_text = ( f"Error `{error}` encountered.\nGuild `{guild_id}`, channel `{ctx.channel.id}`, " f"user `{ctx.author.id}`\n```\n{ctx.message.content if ctx.message else '[No message content]'}\n```" ) # DM to owners if self.bot.config.get('dm_uncaught_errors', False): for owner_id in self.bot.owner_ids: owner = self.bot.get_user( owner_id) or await self.bot.fetch_user(owner_id) file_handle.seek(0) await owner.send(error_text, file=discord.File(file_handle, filename="error_log.py")) # Ping to the webook event_webhook: typing.Optional[ discord.Webhook] = self.bot.get_event_webhook("unhandled_error") try: avatar_url = str(self.bot.user.display_avatar.url) except Exception: avatar_url = None if event_webhook: file_handle.seek(0) try: file = discord.File(file_handle, filename="error_log.py") await event_webhook.send( error_text, file=file, username=f"{self.bot.user.name} - Error", avatar_url=avatar_url, allowed_mentions=discord.AllowedMentions.none(), ) except discord.HTTPException as e: self.logger.error( f"Failed to send webhook for event unhandled_error - {e}") # And throw it into the console logger = getattr(getattr(ctx, 'cog', self), 'logger', self.logger) for line in error_string.strip().split("\n"): logger.error(line)