Exemplo n.º 1
0
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"
Exemplo n.º 2
0
    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
Exemplo n.º 3
0
    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)]
Exemplo n.º 4
0
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
Exemplo n.º 5
0
    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 ""))
Exemplo n.º 6
0
    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)
Exemplo n.º 7
0
    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
Exemplo n.º 8
0
    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 ""))
Exemplo n.º 9
0
    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)
Exemplo n.º 10
0
 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
Exemplo n.º 11
0
    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))
Exemplo n.º 12
0
def test_format_dt(dt: datetime.datetime,
                   style: typing.Optional[utils.TimestampStyle],
                   formatted: str):
    assert utils.format_dt(dt, style=style) == formatted
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
def format_relative(dt):
    """Returns the discord markdown for a relative timestamp"""
    return format_dt(dt, style="R")
Exemplo n.º 16
0
    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"])
Exemplo n.º 17
0
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)