Beispiel #1
0
def url(url_):
    url_ = url_.lower()
    if not url_.startswith(("http://", "https://")) or "." not in url_:
        raise commands.BadArgument(f"URL `{url_}` is invalid.")

    return url_
Beispiel #2
0
    async def _pvp(self, ctx):
        """Handles pvp related commands"""

        if ctx.invoked_subcommand == None:
            raise commands.BadArgument()
Beispiel #3
0
 async def convert(self, ctx, arg):
     if arg.lower() == "none": return None
     if not organizationExists(arg):
         raise commands.BadArgument("Unexisting organization provided")
     return arg
Beispiel #4
0
    def __init__(self, argument, *, now=None):
        super().__init__(argument, now=now)

        if self._past:
            raise commands.BadArgument('this time is in the past')
    async def random(self, ctx, *arg):
        """Get a random number or make the bot choose between something

            **Example:**
              `-random` will choose a random number between 1 and 100
              `-random 6` will choose a random number between 1 and 6
              `-random 50 200` will choose a random number between 50 and 200
              `-random coin` will choose Heads or Tails
              `-random choice "England" "Rome"` will choose between England and Rome
            """
        """
        MIT License

        Copyright (c) 2016 - 2018 Eduard Nikoleisen
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        """

        if not arg:
            start = 1
            end = 100

        elif arg[0].lower() in ('flip', 'coin', 'coinflip'):
            coin = ['Heads', 'Tails']
            msg = discord.utils.escape_mentions(
                f':arrows_counterclockwise: **{random.choice(coin)}**')
            return await ctx.send(msg)

        elif arg[0].lower() == 'choice':
            choices = list(arg)
            choices.pop(0)
            msg = discord.utils.escape_mentions(
                f':tada: The winner is: **{random.choice(choices)}**')
            return await ctx.send(msg)

        elif len(arg) == 1:
            start = 1
            try:
                end = int(arg[0])
            except ValueError:
                raise commands.BadArgument()

        elif len(arg) == 2:
            try:
                start = int(arg[0])
                end = int(arg[1])
            except ValueError:
                raise commands.BadArgument()

        else:
            start = 1
            end = 100

        try:
            result = random.randint(start, end)
        except Exception:
            raise commands.BadArgument()

        msg = discord.utils.escape_mentions(
            f':arrows_counterclockwise: Random number ({start} - {end}): **{result}**'
        )
        await ctx.send(msg)
Beispiel #6
0
    async def warn(self, ctx: commands.Context, user: typing.Union[discord.Member, int], points: int, *, reason: str = "No reason.") -> None:
        """Warn a user (mod only)

        Example usage:
        --------------
        `!warn <@user/ID> <points> <reason (optional)>
`
        Parameters
        ----------
        user : discord.Member
            The member to warn
        points : int
            Number of points to warn far
        reason : str, optional
            Reason for warning, by default "No reason."

        """

        await self.check_permissions(ctx, user)

        if points < 1:  # can't warn for negative/0 points
            raise commands.BadArgument(message="Points can't be lower than 1.")

        # if the ID given is of a user who isn't in the guild, try to fetch the profile
        if isinstance(user, int):
            try:
                user = await self.bot.fetch_user(user)
            except discord.NotFound:
                raise commands.BadArgument(
                    f"Couldn't find user with ID {user}")

        guild = self.bot.settings.guild()

        reason = discord.utils.escape_markdown(reason)
        reason = discord.utils.escape_mentions(reason)

        # prepare the case object for database
        case = Case(
            _id=guild.case_id,
            _type="WARN",
            mod_id=ctx.author.id,
            mod_tag=str(ctx.author),
            reason=reason,
            punishment=str(points)
        )

        # increment case ID in database for next available case ID
        await self.bot.settings.inc_caseid()
        # add new case to DB
        await self.bot.settings.add_case(user.id, case)
        # add warnpoints to the user in DB
        await self.bot.settings.inc_points(user.id, points)

        # fetch latest document about user from DB
        results = await self.bot.settings.user(user.id)
        cur_points = results.warn_points

        # prepare log embed, send to #public-mod-logs, user, channel where invoked
        log = await logging.prepare_warn_log(ctx.author, user, case)
        log.add_field(name="Current points", value=cur_points, inline=True)

        log_kickban = None

        if cur_points >= 600:
            # automatically ban user if more than 600 points
            try:
                await user.send("You were banned from r/Jailbreak for reaching 600 or more points.", embed=log)
            except Exception:
                pass

            log_kickban = await self.add_ban_case(ctx, user, "600 or more warn points reached.")
            await user.ban(reason="600 or more warn points reached.")

        elif cur_points >= 400 and not results.was_warn_kicked and isinstance(user, discord.Member):
            # kick user if >= 400 points and wasn't previously kicked
            await self.bot.settings.set_warn_kicked(user.id)

            try:
                await user.send("You were kicked from r/Jailbreak for reaching 400 or more points. Please note that you will be banned at 600 points.", embed=log)
            except Exception:
                pass

            log_kickban = await self.add_kick_case(ctx, user, "400 or more warn points reached.")
            await user.kick(reason="400 or more warn points reached.")

        else:
            if isinstance(user, discord.Member):
                try:
                    await user.send("You were warned in r/Jailbreak. Please note that you will be kicked at 400 points and banned at 600 points.", embed=log)
                except Exception:
                    pass

        # also send response in channel where command was called
        await ctx.message.reply(embed=log, delete_after=10)
        await ctx.message.delete(delay=10)

        public_chan = ctx.guild.get_channel(
            self.bot.settings.guild().channel_public)
        if public_chan:
            log.remove_author()
            log.set_thumbnail(url=user.avatar_url)
            await public_chan.send(embed=log)

            if log_kickban:
                log_kickban.remove_author()
                log_kickban.set_thumbnail(url=user.avatar_url)
                await public_chan.send(embed=log_kickban)
Beispiel #7
0
 async def check_if_queue_exists_or_break(self, guild_id, queue):
     queues = await self.queue_dao.get_all_queues(guild_id)
     if not queue in queues:
         raise commands.BadArgument(
             f"Queue **{queue}** does not exist. Available queues: {', '.join(queues)}"
         )
Beispiel #8
0
    async def help(self, ctx, *command):
        "Displays help about stuff."
        icon = self.bot.get_cog("TapTitansModule").emoji("orange_question")
        fields = {}
        if not command:
            title = f"Available modules for {self.bot.user}"
            description = ", ".join([
                f"{c.replace('Module', '')}" for c in self.bot.cogs
                if str(c) != "PostgreModule"
            ])
        else:
            command = list(command)
            cog = [
                c for c, v in self.bot.cogs.items()
                if str(c).lower().startswith(command[0].lower())
                or command[0].lower() in getattr(v, "aliases", [])
            ]
            if not cog and not self.bot.get_command(command[1:]):
                raise cmd.BadArgument(
                    f"No module with name: **{command[0].lower()}**")
            if len(cog) > 1:
                raise cmd.BadArgument(
                    "I found multiple modules. Please try to narrow your search: {}"
                    .format(", ".join([
                        f"**{c.replace('Module', '').lower()}**" for c in cog
                    ])))
            command = [cog[0].replace("Module", "").lower()] + command[1:]
            cmx = self.bot.get_command(" ".join(command))
            cmg = self.bot.get_cog(" ".join(command).title() + "Module")
            if cmx is None and cmg is None and len(command) > 1:
                cmx = self.bot.get_command(" ".join(command[1:]))
                # cmg = self.bot.get_cog(command[0].title() + "Module")

            if cmx is not None and cmg is None and len(command) > 1:
                title = f"Help for command **{ctx.prefix}{cmx}**"
                obj = HELP.get(str(cmx), {})
                description = obj.get("desc", "Not found.").format(ctx=ctx)
                fields["Usage"] = ("\n".join(
                    [f"{ctx.prefix}{u}" for u in obj.get("usage", [])])
                                   or "Not found.")
                aliases = ", ".join(getattr(cmx, "aliases", []))
                if aliases:
                    fields["Aliases"] = aliases
            else:
                cg = self.bot.get_cog(cog[0])
                subcommands = list(cg.walk_commands())
                module = cg.qualified_name.replace("Module", "").lower()
                groups, commands = [], []
                for sub in subcommands:
                    strsub = str(sub)
                    if hasattr(sub, "walk_commands") and not strsub == module:
                        groups.append(strsub.replace(module, "").strip())
                    elif len(strsub.split()) < 3:
                        if not module in strsub and not sub.hidden:
                            commands.append(strsub)
                        elif strsub.replace(module, "") and not sub.hidden:
                            commands.append(strsub.replace(module, ""))
                aliases = ", ".join(getattr(cg, "aliases", []))
                title = f"Help for **{module}** module"
                obj = HELP.get(
                    str(cg.qualified_name).replace("Module", "").lower(), {})
                description = (obj.get("desc", None) or cg.__doc__
                               or "No helpfile found.")
                description = description.format(ctx=ctx)
                if aliases:
                    fields["Aliases"] = aliases
                if groups:
                    fields["Command Groups"] = ", ".join(set(groups))
                if commands:
                    fields["Commands"] = ", ".join(set(commands))

        embed = discord.Embed(title=title,
                              description=description,
                              color=0xF89D2A)
        if fields:
            for field in fields:
                embed.add_field(name=field, value=fields[field])
        if not command:
            cstm = await self.bot.db.hget(f"{ctx.guild.id}:set", "pfx")
            if not cstm or cstm is None:
                cstm = ""
            pfx = str(cstm) if cstm else self.bot.config["DEFAULT"]["prefix"]
            embed.add_field(name="Current Prefix", value=pfx)
        embed.set_thumbnail(url=icon.url)
        await ctx.send("", embed=embed)
Beispiel #9
0
 async def convert(self, ctx, argument):
     if not argument in ctx.bot.extensions:
         raise commands.BadArgument(f'Extension "{argument}" not found')
     return argument
Beispiel #10
0
 async def convert(cls, ctx: commands.Context, argument: str) -> Any:
     result = await cls._type(argument)
     if result is None:
         raise commands.BadArgument(cls._error_message.format(argument))
     return result
Beispiel #11
0
    async def convert(self, ctx: commands.Context, argument: str):
        result = ctx.bot.get_command(argument)

        if result is None:
            raise commands.BadArgument(f'Command \'{argument}\' not found.')
        return result
Beispiel #12
0
    async def batchraid(self, ctx: context.Context, *, phrases: str) -> None:
        """Add a list of (newline-separated) phrases to the raid filter.

        Example usage
        --------------
        !raid <phrase>

        Parameters
        ----------
        phrases : str
            "Phrases to add, separated with enter"
        """

        async with ctx.typing():
            phrases = list(set(phrases.split("\n")))
            phrases = [phrase.strip() for phrase in phrases]

            phrases_contenders = set(phrases)
            phrases_already_in_db = set(
                [phrase.word for phrase in ctx.settings.guild().raid_phrases])

            duplicate_count = len(
                phrases_already_in_db
                & phrases_contenders)  # count how many duplicates we have
            new_phrases = list(phrases_contenders - phrases_already_in_db)

        if not new_phrases:
            raise commands.BadArgument(
                "All the phrases you supplied are already in the database.")

        phrases_prompt_string = "\n".join(
            [f"**{i+1}**. {phrase}" for i, phrase in enumerate(new_phrases)])
        if len(phrases_prompt_string) > 3900:
            phrases_prompt_string = phrases_prompt_string[:3500] + "\n... (and some more)"

        embed = Embed(
            title="Confirm raidphrase batch",
            color=discord.Color.dark_orange(),
            description=
            f"{phrases_prompt_string}\n\nShould we add these {len(new_phrases)} phrases?"
        )

        if duplicate_count > 0:
            embed.set_footer(
                text=
                f"Note: we found {duplicate_count} duplicates in your list.")

        message = await ctx.send(embed=embed)

        prompt_data = context.PromptDataReaction(message=message,
                                                 reactions=['✅', '❌'],
                                                 timeout=120,
                                                 delete_after=True)
        response, _ = await ctx.prompt_reaction(info=prompt_data)

        if response == '✅':
            async with ctx.typing():
                for phrase in new_phrases:
                    await ctx.settings.add_raid_phrase(phrase)

            await ctx.send_success(
                f"Added {len(new_phrases)} phrases to the raid filter.",
                delete_after=5)
        else:
            await ctx.send_warning("Cancelled.", delete_after=5)

        await ctx.message.delete(delay=5)
Beispiel #13
0
    async def stats(self, ctx, raid_id: Optional[int]):
        # TODO: check if raid is complete
        if not raid_id:
            last_cleared_raid = await self.raid_service.get_last_completed_raid(
                ctx.guild.id)
            if not last_cleared_raid:
                raise commands.BadArgument(
                    "Could not find any completed ( including uploaded attacks ) raid for your guild"
                )

            raid_id = last_cleared_raid.id

        has_permission_and_exists, attacks_exist = await asyncio.gather(
            self.raid_stat_service.has_raid_permission_and_raid_exists(
                ctx.guild.id, raid_id),
            self.raid_stat_service.check_if_attacks_exist(
                ctx.guild.id, raid_id),
        )

        if not has_permission_and_exists:
            raise commands.BadArgument("Raid does not exist for your guild")

        if not attacks_exist:
            raise commands.BadArgument("Please upload player attacks")

        logger.debug("Loading raid stats for raid %s", raid_id)

        awaitables = []  # list to collect all messages
        raid_stats = await self.raid_stat_service.calculate_raid_stats(raid_id)
        raid_data = await self.raid_stat_service.load_raid_data_for_stats(
            ctx.guild.id, raid_id)

        duration_hms = get_hms(raid_stats.cleared_at - raid_stats.started_at)
        d_h, d_m, d_s = duration_hms[0], duration_hms[1], duration_hms[2]

        embed = create_embed(self.bot)

        embed.title = f"Raid conclusion (raid_id: {raid_id})"

        embed.add_field(
            name="**General Info**",
            value="{}".format("\n".join([
                f"Attackers: {raid_stats.attackers}",
                f"Cycles: {math.ceil(raid_stats.max_hits / 4)}",
                f"Total Damage Dealt: {num_to_hum(raid_stats.total_dmg)}",
                f"Started at: {raid_stats.started_at.format(DATETIME_FORMAT)}",
                f"Cleared at: {raid_stats.cleared_at.format(DATETIME_FORMAT)}",
                f"Time needed: {d_h}h {d_m}m {d_s}s",
            ])),
            inline=False,
        )

        embed.add_field(
            name="**Attacks**",
            value="\n".join(
                self._create_min_max_avg_texts(raid_stats.min_hits,
                                               raid_stats.max_hits)),
        )

        embed.add_field(
            name="**Average Player Damage**",
            value="\n".join(
                self._create_min_max_avg_texts(raid_stats.min_avg,
                                               raid_stats.max_avg,
                                               raid_stats.total_avg)),
        )

        embed.add_field(
            name="**Player Damage**",
            value="\n".join(
                self._create_min_max_avg_texts(raid_stats.min_dmg,
                                               raid_stats.max_dmg,
                                               raid_stats.avg_dmg)),
        )

        awaitables.append(ctx.send(embed=embed))

        raid_player_hits = []
        string_length = 0
        DISCORD_MAX_CONTENT_LENGHT = 2000 - 50  # some buffer

        latest_raid_data = raid_data[0]
        if len(raid_data) > 1:
            reference_raid_data = raid_data[1]
        else:
            reference_raid_data = None

        for idx, player_attack in enumerate(
                latest_raid_data.raid_player_attacks):
            if reference_raid_data:
                reference_player_attack = next(
                    (raid_player_attack for raid_player_attack in
                     reference_raid_data.raid_player_attacks
                     if raid_player_attack.player_id == player_attack.player_id
                     ),
                    None,
                )
            else:
                reference_player_attack = None

            if not reference_player_attack:
                logger.debug(
                    "Did not found any previous attacks for player: %s (%s)",
                    player_attack.player_name,
                    player_attack.player_id,
                )

            player_average = (player_attack.total_dmg /
                              player_attack.total_hits
                              if player_attack.total_hits else 0)
            player_reference_average = 0

            if reference_player_attack and reference_player_attack.total_hits:
                player_reference_average = (reference_player_attack.total_dmg /
                                            reference_player_attack.total_hits)

            player_average_diff = player_average - player_reference_average
            player_average_diff_str = "({}{})".format(
                "+" if player_average_diff > 0 else "",
                num_to_hum(player_average_diff))

            stat_string = "{:2}. {:<20}: {:>8}, {:2}, {:>8} {}".format(
                idx + 1,
                player_attack.player_name,
                num_to_hum(player_attack.total_dmg),
                player_attack.total_hits,
                num_to_hum(player_average),
                player_average_diff_str if reference_player_attack else "",
            )

            if string_length + len(stat_string) >= DISCORD_MAX_CONTENT_LENGHT:
                awaitables.append(
                    ctx.send("```{}```".format("\n".join(raid_player_hits))))
                string_length = 0
                raid_player_hits.clear()

            string_length += len(stat_string)
            raid_player_hits.append(stat_string)

        awaitables.append(
            ctx.send("```{}```".format("\n".join(raid_player_hits))))
        await asyncio.gather(*(awaitables))
Beispiel #14
0
 async def convert(self, ctx, argument):
     if argument not in ['>', '>=', '<', '<=', '=']:
         raise commands.BadArgument('Not a valid selector')
     return argument
Beispiel #15
0
class ModActions(commands.Cog):
    """This cog handles all the possible moderator actions.
    - Kick
    - Ban
    - Unban
    - Warn
    - Liftwarn
    - Mute
    - Unmute
    - Purge
    """

    def __init__(self, bot):
        self.bot = bot

    async def check_permissions(self, ctx, user: typing.Union[discord.Member, int] = None):
        if isinstance(user, discord.Member):
            if user.id == ctx.author.id:
                await ctx.message.add_reaction("🤔")
                raise commands.BadArgument("You can't call that on yourself.")
            if user.id == self.bot.user.id:
                await ctx.message.add_reaction("🤔")
                raise commands.BadArgument("You can't call that on me :(")

        # must be at least a mod
        if not self.bot.settings.permissions.hasAtLeast(ctx.guild, ctx.author, 5):
            raise commands.BadArgument(
                "You do not have permission to use this command.")
        if user:
            if isinstance(user, discord.Member):
                if user.top_role >= ctx.author.top_role:
                    raise commands.BadArgument(
                        message=f"{user.mention}'s top role is the same or higher than yours!")

    @commands.guild_only()
    @commands.bot_has_guild_permissions(kick_members=True, ban_members=True)
    @commands.command(name="warn")
    async def warn(self, ctx: commands.Context, user: typing.Union[discord.Member, int], points: int, *, reason: str = "No reason.") -> None:
        """Warn a user (mod only)

        Example usage:
        --------------
        `!warn <@user/ID> <points> <reason (optional)>
`
        Parameters
        ----------
        user : discord.Member
            The member to warn
        points : int
            Number of points to warn far
        reason : str, optional
            Reason for warning, by default "No reason."

        """

        await self.check_permissions(ctx, user)

        if points < 1:  # can't warn for negative/0 points
            raise commands.BadArgument(message="Points can't be lower than 1.")

        # if the ID given is of a user who isn't in the guild, try to fetch the profile
        if isinstance(user, int):
            try:
                user = await self.bot.fetch_user(user)
            except discord.NotFound:
                raise commands.BadArgument(
                    f"Couldn't find user with ID {user}")

        guild = self.bot.settings.guild()

        reason = discord.utils.escape_markdown(reason)
        reason = discord.utils.escape_mentions(reason)

        # prepare the case object for database
        case = Case(
            _id=guild.case_id,
            _type="WARN",
            mod_id=ctx.author.id,
            mod_tag=str(ctx.author),
            reason=reason,
            punishment=str(points)
        )

        # increment case ID in database for next available case ID
        await self.bot.settings.inc_caseid()
        # add new case to DB
        await self.bot.settings.add_case(user.id, case)
        # add warnpoints to the user in DB
        await self.bot.settings.inc_points(user.id, points)

        # fetch latest document about user from DB
        results = await self.bot.settings.user(user.id)
        cur_points = results.warn_points

        # prepare log embed, send to #public-mod-logs, user, channel where invoked
        log = await logging.prepare_warn_log(ctx.author, user, case)
        log.add_field(name="Current points", value=cur_points, inline=True)

        log_kickban = None

        if cur_points >= 600:
            # automatically ban user if more than 600 points
            try:
                await user.send("You were banned from r/Jailbreak for reaching 600 or more points.", embed=log)
            except Exception:
                pass

            log_kickban = await self.add_ban_case(ctx, user, "600 or more warn points reached.")
            await user.ban(reason="600 or more warn points reached.")

        elif cur_points >= 400 and not results.was_warn_kicked and isinstance(user, discord.Member):
            # kick user if >= 400 points and wasn't previously kicked
            await self.bot.settings.set_warn_kicked(user.id)

            try:
                await user.send("You were kicked from r/Jailbreak for reaching 400 or more points. Please note that you will be banned at 600 points.", embed=log)
            except Exception:
                pass

            log_kickban = await self.add_kick_case(ctx, user, "400 or more warn points reached.")
            await user.kick(reason="400 or more warn points reached.")

        else:
            if isinstance(user, discord.Member):
                try:
                    await user.send("You were warned in r/Jailbreak. Please note that you will be kicked at 400 points and banned at 600 points.", embed=log)
                except Exception:
                    pass

        # also send response in channel where command was called
        await ctx.message.reply(embed=log, delete_after=10)
        await ctx.message.delete(delay=10)

        public_chan = ctx.guild.get_channel(
            self.bot.settings.guild().channel_public)
        if public_chan:
            log.remove_author()
            log.set_thumbnail(url=user.avatar_url)
            await public_chan.send(embed=log)

            if log_kickban:
                log_kickban.remove_author()
                log_kickban.set_thumbnail(url=user.avatar_url)
                await public_chan.send(embed=log_kickban)

    @commands.guild_only()
    @commands.command(name="liftwarn")
    async def liftwarn(self, ctx: commands.Context, user: discord.Member, case_id: int, *, reason: str = "No reason.") -> None:
        """Mark a warn as lifted and remove points. (mod only)

        Example usage:
        --------------
        `!liftwarn <@user/ID> <case ID> <reason (optional)>`

        Parameters
        ----------
        user : discord.Member
            User to remove warn from
        case_id : int
            The ID of the case for which we want to remove points
        reason : str, optional
            Reason for lifting warn, by default "No reason."

        """

        await self.check_permissions(ctx, user)

        # retrieve user's case with given ID
        cases = await self.bot.settings.get_case(user.id, case_id)
        case = cases.cases.filter(_id=case_id).first()

        reason = discord.utils.escape_markdown(reason)
        reason = discord.utils.escape_mentions(reason)

        # sanity checks
        if case is None:
            raise commands.BadArgument(
                message=f"{user} has no case with ID {case_id}")
        elif case._type != "WARN":
            raise commands.BadArgument(
                message=f"{user}'s case with ID {case_id} is not a warn case.")
        elif case.lifted:
            raise commands.BadArgument(
                message=f"Case with ID {case_id} already lifted.")
Beispiel #16
0
def mention_converter(argument):
    try:
        return MentionMode[argument.lower()]
    except:
        raise commands.BadArgument('\U0001f52b Valid modes: ' +
                                   ', '.join(MentionMode.__members__))
Beispiel #17
0
    async def removepoints(self, ctx: commands.Context, user: discord.Member, points: int, *, reason: str = "No reason.") -> None:
        """Remove warnpoints from a user. (mod only)

        Example usage:
        --------------
        `!removepoints <@user/ID> <points> <reason (optional)>`

        Parameters
        ----------
        user : discord.Member
            User to remove warn from
        points : int
            Amount of points to remove
        reason : str, optional
            Reason for lifting warn, by default "No reason."

        """

        await self.check_permissions(ctx, user)

        reason = discord.utils.escape_markdown(reason)
        reason = discord.utils.escape_mentions(reason)

        if points < 1:
            raise commands.BadArgument("Points can't be lower than 1.")

        u = await self.bot.settings.user(id=user.id)
        if u.warn_points - points < 0:
            raise commands.BadArgument(
                message=f"Can't remove {points} points because it would make {user.mention}'s points negative.")

        # passed sanity checks, so update the case in DB
        # remove the warn points from the user in DB
        await self.bot.settings.inc_points(user.id, -1 * points)

        case = Case(
            _id=self.bot.settings.guild().case_id,
            _type="REMOVEPOINTS",
            mod_id=ctx.author.id,
            mod_tag=str(ctx.author),
            punishment=str(points),
            reason=reason,
        )

        # increment DB's max case ID for next case
        await self.bot.settings.inc_caseid()
        # add case to db
        await self.bot.settings.add_case(user.id, case)

        # prepare log embed, send to #public-mod-logs, user, channel where invoked
        log = await logging.prepare_removepoints_log(ctx.author, user, case)
        try:
            await user.send("Your points were removed in r/Jailbreak.", embed=log)
        except Exception:
            pass

        await ctx.message.reply(embed=log, delete_after=10)
        await ctx.message.delete(delay=10)

        public_chan = ctx.guild.get_channel(
            self.bot.settings.guild().channel_public)
        if public_chan:
            log.remove_author()
            log.set_thumbnail(url=user.avatar_url)
            await public_chan.send(embed=log)
Beispiel #18
0
    async def block(self, ctx, user: Optional[User] = None, *,
                    after: UserFriendlyTime = None):
        """
        Block a user from using Modmail.

        Note: reasons that start with "System Message: " are reserved for internal
        use only.
        """
        reason = ''

        if user is None:
            thread = ctx.thread
            if thread:
                user = thread.recipient
            elif after is None:
                raise commands.MissingRequiredArgument(param(name='user'))
            else:
                raise commands.BadArgument(f'User "{after.arg}" not found')

        if after is not None:
            reason = after.arg
            if reason.startswith('System Message: '):
                raise commands.BadArgument('The reason cannot start with `System Message:`.')
            if re.search(r'%(.+?)%$', reason) is not None:
                raise commands.MissingRequiredArgument(param(name='reason'))
            if after.dt > after.now:
                reason = f'{reason} %{after.dt.isoformat()}%'

        if not reason:
            reason = None

        mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`'

        extend = f' for `{reason}`' if reason is not None else ''
        msg = self.bot.blocked_users.get(str(user.id))
        if msg is None:
            msg = ''

        if str(user.id) not in self.bot.blocked_users or extend or msg.startswith('System Message: '):
            if str(user.id) in self.bot.blocked_users:

                old_reason = msg.strip().rstrip('.') or 'no reason'
                embed = discord.Embed(
                    title='Success',
                    description=f'{mention} was previously blocked for '
                    f'"{old_reason}". {mention} is now blocked{extend}.',
                    color=self.bot.main_color
                )
            else:
                embed = discord.Embed(
                    title='Success',
                    color=self.bot.main_color,
                    description=f'{mention} is now blocked{extend}.'
                )
            self.bot.config.blocked[str(user.id)] = reason
            await self.bot.config.update()
        else:
            embed = discord.Embed(
                title='Error',
                color=discord.Color.red(),
                description=f'{mention} is already blocked.'
            )

        return await ctx.send(embed=embed)
Beispiel #19
0
    async def convert(self, ctx, argument):
        if argument in RAID_CONFIG_KEYS:
            return argument

        raise commands.BadArgument(
            f"Config key must be one of {', '.join(RAID_CONFIG_KEYS)} ")
Beispiel #20
0
    async def mute(
        self,
        ctx,
        member: HierarchyMemberConverter,
        time: TimeConverter,
        *,
        reason="None Given",
    ):
        """
        Mutes a member for the specified time format is `6d`

        Valid time specifiers are `d`, `m`, `s`, `h`
        """
        muted_role = ctx.guild.get_role(ctx.cache.get("muteid"))

        if muted_role is None:
            raise commands.BadArgument(
                "I cannot find the muted role in the config, this is probably because the role was deleted."
            )

        if muted_role >= ctx.me.top_role:
            raise commands.BadArgument(
                "The muted role is greater than or equal to my top role in the hierarchy, please move my role above it."
            )

        if muted_role in member.roles:
            await ctx.reply(embed=CustomEmbed(
                description=
                "User was already muted, changing the mute to the new time."))
            await member.remove_roles(muted_role, reason="Unmuting")
            await ctx.db.execute(
                "DELETE FROM mutes WHERE guildid = $1 and userid = $2",
                ctx.guild.id,
                member.id,
            )
        sleep = (time - datetime.datetime.utcnow()).total_seconds()
        if sleep <= 1800:
            task = self.bot.loop.create_task(
                self.perform_unmute(member=member,
                                    role=muted_role,
                                    when=time,
                                    record=None))
            task.add_done_callback(self.unmute_error)

        await ctx.db.execute(
            "INSERT INTO mutes(userid, guildid, starttime, endtime, reason) VALUES($1, $2, $3, $4, $5)",
            member.id,
            ctx.guild.id,
            datetime.datetime.utcnow(),
            time,
            reason,
        )

        await member.add_roles(
            muted_role, reason=f"Mute done by {ctx.author} [{ctx.author.id}]")

        await ctx.reply(embed=CustomEmbed(
            description=(f"Muted {member}\n"
                         f"For Reason: `{reason}`\n"),
            timestamp=time,
        ).set_footer(text="Ends at"))
Beispiel #21
0
    async def convert(self, ctx, argument):
        try:
            calendar = HumanTime.calendar
            regex = ShortTime.compiled
            now = ctx.message.created_at

            match = regex.match(argument)
            if match is not None and match.group(0):
                data = {k: int(v) for k, v in match.groupdict(default=0).items()}
                remaining = argument[match.end():].strip()
                self.dt = now + relativedelta(**data)
                return await self.check_constraints(ctx, now, remaining)

            # apparently nlp does not like "from now"
            # it likes "from x" in other cases though so let me handle the 'now' case
            if argument.endswith('from now'):
                argument = argument[:-8].strip()

            if argument[0:2] == 'me':
                # starts with "me to", "me in", or "me at "
                if argument[0:6] in ('me to ', 'me in ', 'me at '):
                    argument = argument[6:]

            elements = calendar.nlp(argument, sourceTime=now)
            if elements is None or len(elements) == 0:
                raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".')

            # handle the following cases:
            # "date time" foo
            # date time foo
            # foo date time

            # first the first two cases:
            dt, status, begin, end, dt_string = elements[0]

            if not status.hasDateOrTime:
                raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".')

            if begin not in (0, 1) and end != len(argument):
                raise commands.BadArgument('Time is either in an inappropriate location, which '
                                           'must be either at the end or beginning of your input, '
                                           'or I just flat out did not understand what you meant. Sorry.')

            if not status.hasTime:
                # replace it with the current time
                dt = dt.replace(hour=now.hour, minute=now.minute, second=now.second, microsecond=now.microsecond)

            # if midnight is provided, just default to next day
            if status.accuracy == pdt.pdtContext.ACU_HALFDAY:
                dt = dt.replace(day=now.day + 1)

            self.dt = dt

            if begin in (0, 1):
                if begin == 1:
                    # check if it's quoted:
                    if argument[0] != '"':
                        raise commands.BadArgument('Expected quote before time input...')

                    if not (end < len(argument) and argument[end] == '"'):
                        raise commands.BadArgument('If the time is quoted, you must unquote it.')

                    remaining = argument[end + 1:].lstrip(' ,.!')
                else:
                    remaining = argument[end:].lstrip(' ,.!')
            elif len(argument) == end:
                remaining = argument[:begin].strip()

            return await self.check_constraints(ctx, now, remaining)
        except Exception:
            import traceback
            traceback.print_exc()
            raise
Beispiel #22
0
    async def get_run_output(self, ctx):
        # Get parameters to call api depending on how the command was called (file <> codeblock)
        if ctx.message.attachments:
            alias, source, args, stdin = await self.get_api_parameters_with_file(
                ctx)
        else:
            alias, source, args, stdin = await self.get_api_parameters_with_codeblock(
                ctx)

        # Resolve aliases for language
        language = self.languages[alias]

        # Add boilerplate code to supported languages
        source = add_boilerplate(language, source)

        # Split args at newlines
        if args:
            args = [arg for arg in args.strip().split('\n') if arg]

        if not source:
            raise commands.BadArgument(f'No source code found')

        # Call piston API
        data = {
            'language': alias,
            'version': '*',
            'files': [{
                'content': source
            }],
            'args': args,
            'stdin': stdin or "",
            'log': 0
        }
        headers = {'Authorization': self.client.config["emkc_key"]}
        async with self.client.session.post(
                'https://emkc.org/api/v2/piston/execute',
                headers=headers,
                json=data) as response:
            try:
                r = await response.json()
            except ContentTypeError:
                raise PistonInvalidContentType('invalid content type')
        if not response.status == 200:
            raise PistonInvalidStatus(
                f'status {response.status}: {r.get("message", "")}')

        r = r['run']
        if r['output'] is None:
            raise PistonNoOutput('no output')

        # Logging
        await self.send_to_log(ctx, language, source)

        # Return early if no output was received
        if len(r['output']) == 0:
            return f'Your code ran without output {ctx.author.mention}'

        # Limit output to 30 lines maximum
        output = '\n'.join(r['output'].split('\n')[:30])

        # Prevent mentions in the code output
        output = escape_mentions(output)

        # Prevent code block escaping by adding zero width spaces to backticks
        output = output.replace("`", "`\u200b")

        # Truncate output to be below 2000 char discord limit.
        if len(r['stdout']) == 0 and len(r['stderr']) > 0:
            introduction = f'{ctx.author.mention} I only received error output\n'
        else:
            introduction = f'Here is your output {ctx.author.mention}\n'
        truncate_indicator = '[...]'
        len_codeblock = 7  # 3 Backticks + newline + 3 Backticks
        available_chars = 2000 - len(introduction) - len_codeblock
        if len(output) > available_chars:
            output = output[:available_chars -
                            len(truncate_indicator)] + truncate_indicator

        return (introduction + '```\n' + output + '```')
Beispiel #23
0
 async def convert(self, _: Context, argument: str) -> IO:
     try:
         return JLPT_LOOKUP[argument.lower().strip()]
     except KeyError:
         raise commands.BadArgument("Invalid key for JLPT level.")
Beispiel #24
0
    async def base(self, ctx, b1: Base, b2: Base, *values):
        '''
        Converts values (separated by spaces*) from one base to another.

        __Supported bases__
            `2` - `36` (case-insensitive), 
            `37` - `62` (0-9, A-Z, a-z), 
            `63` - (list of values are returned), 
            `unicode`
        
        *A single value in base Unicode is interpreted as one char instead of a block separated by spaces.
        *A single value in base 63+ is interpreted as one list instead of a block separated by spaces.
        '''
        if b1 == b2:
            await ctx.send(' '.join(values))
            return
        [b1, b1m, b2, b2m] = [*b1, *b2]
        chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' if b1m == 1 or b2m == 1 else '0123456789abcdefghijklmnopqrstuvwxyz'

        if b1 == 'u':
            ustr = ' '.join(values)  # returns the unicode base string
            ord_lst = [ord(c) for c in ustr]

            if b2 == 10:
                await ctx.send(' '.join(str(l) for l in ord_lst))
                return

            newb_lst = [self.b10_to_base(b2, v)
                        for v in ord_lst]  # converts ords to list of new base

            # converts list into display format & return
            if b2m != 2:
                disp_lst = [''.join(chars[v] for v in l) for l in newb_lst]
            else:
                disp_lst = [str(l) for l in newb_lst]
            await ctx.send(' '.join(disp_lst))
            return

        try:  # returns a list of numeric values
            if b1m == 0:
                val_lst = [int(s, base=b1) for s in values]
            if b1m == 1:
                val_lst = [[chars[:b1].index(c) for c in s] for s in values]
                val_lst = [self.base_to_b10(b1, v) for v in val_lst]
            if b1m == 2:
                # separates values by array
                values = (' '.join(values)).split('] [')
                values = [s if s.startswith('[') else f'[{s}' for s in values]
                values = [s if s.endswith(']') else f'{s}]' for s in values]

                val_lst = [loads(lst) for lst in values]
                val_lst = [self.base_to_b10(b1, v) for v in val_lst]
        except ValueError:
            print('\n\n\n')
            print_exc()
            raise commands.BadArgument('The input values are invalid.')

        if b2 == 'u':  # convert vals to chrs and return
            chr_lst = [chr(v) for v in val_lst]
            await ctx.send(''.join(chr_lst))
            return

        newb_lst = [self.b10_to_base(b2, v) for v in val_lst]

        # converts list to display format & return
        if b2m != 2:
            disp_lst = [''.join(chars[v] for v in l) for l in newb_lst]
        else:
            disp_lst = [str(l) for l in newb_lst]
        await ctx.send(' '.join(disp_lst))
    async def roll(self, ctx, *, dices):
        """Roll some dice

            **Supported Notation**
            - Dice rolls take the form NdX, where N is the number of dice to roll, and X are the faces of the dice. For example, 1d6 is one six-sided die.
            - A dice roll can be followed by an Ln or Hn, where it will discard the lowest n rolls or highest n rolls, respectively. So 2d20L1 means to roll two d20s and discard the lower. I.E advantage.
            - A dice roll can be part of a mathematical expression, such as 1d4 +5.

            **Example:**
              `-roll 1d6` will roll a d6
              `-roll (2d20L1) + 1d4 + 5` will roll 2d20s, discard the lower one, and add 1d4 and 5 to the result

            *Full notation can be found here: https://xdice.readthedocs.io/en/latest/dice_notation.html*
            """

        #Todo:
        # []: Make the special message system more robust and logical

        try:
            # Attempt to parse the user command and roll the dice
            dice_pattern = xdice.Pattern(dices)
            dice_pattern.compile()
            roll = dice_pattern.roll()
        except (SyntaxError, TypeError, ValueError):
            raise commands.BadArgument()

        # Defining some helper variables
        special_message = ""
        roll_information = []

        # Loop over each dice roll and add it to the intermediate text
        for score in roll.scores():

            score_string = ""

            if len(score.detail) > 1:
                score_string = f"{score_string}{' + '.join(map(str,score.detail))}"
            else:
                score_string = f"{score_string}{score.detail[0]}"

            if score.dropped == []:
                pass
            elif len(score.dropped) > 1:
                score_string = f"{score_string} ~~+ {' + '.join(map(str,score.dropped))}~~"
            else:
                score_string = f"{score_string} ~~+ {score.dropped[0]}~~"

            # Add a special message if a user rolls a 20 or 1
            if "d20" in score.name:
                if 1 in score.detail:
                    special_message = "Aww, you rolled a natural 1"
                elif 20 in score.detail:
                    special_message = "Yay! You rolled a natural 20"

            score_string = f"[{score_string}]"
            roll_information.append(score_string)

        # Put spaces between the operators in the xdice template
        format_string = dice_pattern.format_string
        for i in ["+", "-", "/", "*"]:
            format_string = format_string.replace(i, f" {i} ")

        #Format the intermediate text using the previous template
        rolls = format_string.format(*roll_information)

        #Create the final message
        msg = f"{ctx.author.mention}:\n> `{dices}` = {rolls} = {roll}"
        if special_message:
            msg = f"{msg}\n{special_message}"

        await ctx.send(msg)
Beispiel #26
0
    async def infraction_edit(
            self,
            ctx: Context,
            infraction_id: t.Union[
                int, allowed_strings("l", "last", "recent")],  # noqa: F821
            duration: t.Union[Expiry,
                              allowed_strings("p", "permanent"),
                              None],  # noqa: F821
            *,
            reason: str = None) -> None:
        """
        Edit the duration and/or the reason of an infraction.

        Durations are relative to the time of updating and should be appended with a unit of time.
        Units (∗case-sensitive):
        \u2003`y` - years
        \u2003`m` - months∗
        \u2003`w` - weeks
        \u2003`d` - days
        \u2003`h` - hours
        \u2003`M` - minutes∗
        \u2003`s` - seconds

        Use "l", "last", or "recent" as the infraction ID to specify that the most recent infraction
        authored by the command invoker should be edited.

        Use "p" or "permanent" to mark the infraction as permanent. Alternatively, an ISO 8601
        timestamp can be provided for the duration.
        """
        if duration is None and reason is None:
            # Unlike UserInputError, the error handler will show a specified message for BadArgument
            raise commands.BadArgument(
                "Neither a new expiry nor a new reason was specified.")

        # Retrieve the previous infraction for its information.
        if isinstance(infraction_id, str):
            params = {"actor__id": ctx.author.id, "ordering": "-inserted_at"}
            infractions = await self.bot.api_client.get("bot/infractions",
                                                        params=params)

            if infractions:
                old_infraction = infractions[0]
                infraction_id = old_infraction["id"]
            else:
                await ctx.send(
                    ":x: Couldn't find most recent infraction; you have never given an infraction."
                )
                return
        else:
            old_infraction = await self.bot.api_client.get(
                f"bot/infractions/{infraction_id}")

        request_data = {}
        confirm_messages = []
        log_text = ""

        if duration is not None and not old_infraction['active']:
            if reason is None:
                await ctx.send(
                    ":x: Cannot edit the expiration of an expired infraction.")
                return
            confirm_messages.append(
                "expiry unchanged (infraction already expired)")
        elif isinstance(duration, str):
            request_data['expires_at'] = None
            confirm_messages.append("marked as permanent")
        elif duration is not None:
            request_data['expires_at'] = duration.isoformat()
            expiry = time.format_infraction_with_duration(
                request_data['expires_at'])
            confirm_messages.append(f"set to expire on {expiry}")
        else:
            confirm_messages.append("expiry unchanged")

        if reason:
            request_data['reason'] = reason
            confirm_messages.append("set a new reason")
            log_text += f"""
                Previous reason: {old_infraction['reason']}
                New reason: {reason}
            """.rstrip()
        else:
            confirm_messages.append("reason unchanged")

        # Update the infraction
        new_infraction = await self.bot.api_client.patch(
            f'bot/infractions/{infraction_id}',
            json=request_data,
        )

        # Re-schedule infraction if the expiration has been updated
        if 'expires_at' in request_data:
            # A scheduled task should only exist if the old infraction wasn't permanent
            if old_infraction['expires_at']:
                self.infractions_cog.scheduler.cancel(new_infraction['id'])

            # If the infraction was not marked as permanent, schedule a new expiration task
            if request_data['expires_at']:
                self.infractions_cog.schedule_expiration(new_infraction)

            log_text += f"""
                Previous expiry: {old_infraction['expires_at'] or "Permanent"}
                New expiry: {new_infraction['expires_at'] or "Permanent"}
            """.rstrip()

        changes = ' & '.join(confirm_messages)
        await ctx.send(
            f":ok_hand: Updated infraction #{infraction_id}: {changes}")

        # Get information about the infraction's user
        user_id = new_infraction['user']
        user = ctx.guild.get_member(user_id)

        if user:
            user_text = messages.format_user(user)
            thumbnail = user.avatar_url_as(static_format="png")
        else:
            user_text = f"<@{user_id}>"
            thumbnail = None

        await self.mod_log.send_log_message(icon_url=constants.Icons.pencil,
                                            colour=discord.Colour.blurple(),
                                            title="Infraction edited",
                                            thumbnail=thumbnail,
                                            text=textwrap.dedent(f"""
                Member: {user_text}
                Actor: <@{new_infraction['actor']}>
                Edited by: {ctx.message.author.mention}{log_text}
            """))
    async def updates(self, ctx: context.Context, *, board:str):
        """(alias !updates) Get ChromeOS version data for a specified Chromebook board name
        
        Example usage
        --------------
        !updates edgar


        Parameters
        ----------
        board : str
            "name of board to get updates for"
        """
        
        # ensure the board arg is only alphabetical chars
        if (not board.isalpha()):
            raise commands.BadArgument("The board should only be alphabetical characters!")

        # case insensitivity
        board = board.lower()

        # fetch data from skylar's API
        data = ""
        async with aiohttp.ClientSession() as session:
            data = await fetch(session, 'https://raw.githubusercontent.com/skylartaylor/cros-updates/master/src/data/cros-updates.json')
            if data is None:
                return
        
        #parse response to json
        data = json.loads(data)
        # loop through response to find board
        for data_board in data:
            # did we find a match
            if data_board['Codename'] == board:
                # yes, send the data
                embed = Embed(title=f"ChromeOS update status for {board}", color=Color(value=0x37b83b))
                version = data_board["Stable"].split("<br>")
                embed.add_field(name=f'Stable Channel', value=f'**Version**: {version[1]}\n**Platform**: {version[0]}')
                
                version = data_board["Beta"].split("<br>")
                if len(version) == 2:
                    embed.add_field(name=f'Beta Channel', value=f'**Version**: {version[1]}\n**Platform**: {version[0]}')
                else:
                    embed.add_field(name=f'Beta Channel', value=f'**Version**: {data_board["Beta"]}')
                
                version = data_board["Dev"].split("<br>")
                if len(version) == 2:
                    embed.add_field(name=f'Dev Channel', value=f'**Version**: {version[1]}\n**Platform**: {version[0]}')
                else:
                    embed.add_field(name=f'Dev Channel', value=f'**Version**: {data["Dev"]}')
                
                if (data_board["Canary"] is not None):
                    version = data_board["Canary"].split("<br>")
                    if len(version) == 2:
                        embed.add_field(name=f'Canary Channel', value=f'**Version**: {version[1]}\n**Platform**: {version[0]}')
                
                embed.set_footer(text=f"Powered by https://cros.tech/ (by Skylar), requested by {ctx.author.name}#{ctx.author.discriminator}", icon_url=ctx.author.avatar_url)
                await ctx.message.reply(embed=embed)
                return

        # board not found, error
        raise commands.BadArgument("Couldn't find a result with that boardname!")
Beispiel #28
0
        reason = discord.utils.escape_mentions(reason)

        # sanity checks
        if case is None:
            raise commands.BadArgument(
                message=f"{user} has no case with ID {case_id}")
        elif case._type != "WARN":
            raise commands.BadArgument(
                message=f"{user}'s case with ID {case_id} is not a warn case.")
        elif case.lifted:
            raise commands.BadArgument(
                message=f"Case with ID {case_id} already lifted.")

        u = await self.bot.settings.user(id=user.id)
        if u.warn_points - int(case.punishment) < 0:
            raise commands.BadArgument(
                message=f"Can't lift Case #{case_id} because it would make {user.mention}'s points negative.")

        # passed sanity checks, so update the case in DB
        case.lifted = True
        case.lifted_reason = reason
        case.lifted_by_tag = str(ctx.author)
        case.lifted_by_id = ctx.author.id
        case.lifted_date = datetime.datetime.now()
        cases.save()

        # remove the warn points from the user in DB
        await self.bot.settings.inc_points(user.id, -1 * int(case.punishment))

        # prepare log embed, send to #public-mod-logs, user, channel where invoked
        log = await logging.prepare_liftwarn_log(ctx.author, user, case)
        try:
Beispiel #29
0
 async def convert(self, ctx, arg):
     if re.match(r"\w+:\d\d?", arg):
         tag, value = arg.split(":")
         return tag, int(value)
     raise commands.BadArgument(
         "Invalid Battle Entity provided, cannot convert")
Beispiel #30
0
    async def charinfo(self, ctx, *, data: str):
        """Shows information about one or several characters.

        'data' can either be a character, a unicode escape sequence, a unicode character name or a string.
        If 'data' is a string only a summary of each character's info will be displayed.
        """
        data = data.lower()

        if data.startswith('\\u'):
            # Let's interpret the unicode escape sequence
            hex_values = data.split('\\u')[1:]
            try:
                code_points = [int(val, 16) for val in hex_values]
            except ValueError:
                raise commands.BadArgument('Invalid unicode escape sequence.')
            else:
                data = ''.join(chr(cp) for cp in code_points)
        elif len(data) > 1:
            # Maybe we've been given the character's name ?
            try:
                data = unicodedata.lookup(data)
            except KeyError:
                pass

        # Normalise the input
        data = unicodedata.normalize('NFC', data)
        url_fmt = '<http://unicode-table.com/en/{:X}>'

        if len(data) == 1:
            # Detailed info on the character
            entries = [('Character', data),
                       ('Name', unicodedata.name(data, 'None')),
                       ('Code point', f'{ord(data):04x}')]
            decomposition = unicodedata.decomposition(data)
            if decomposition != '':
                entries.append(('Decomposition', decomposition))

            combining = unicodedata.combining(data)
            if combining:
                entries.append(('Combining class', combining))

            entries.append(('Category', unicodedata.category(data)))
            bidirectional = unicodedata.bidirectional(data)
            entries.append(('Bidirectional',
                            bidirectional if bidirectional != '' else 'None'))
            entries.append(
                ('Mirrored',
                 'True' if unicodedata.mirrored(data) == 1 else 'False'))
            entries.append(
                ('East asian width', unicodedata.east_asian_width(data)))
            entries.append(('Url', url_fmt.format(ord(data))))

            # Create the message's content and send it
            content = utils.indented_entry_to_str(entries)
            await ctx.send(utils.format_block(content))
        else:
            # Minimal info for each character
            entries = [
                f'`\N{ZERO WIDTH SPACE}{c}\N{ZERO WIDTH SPACE}` | `\\u{ord(c):04x}` | `{unicodedata.name(c, "None")}` | {url_fmt.format(ord(c))}'
                for c in data
            ]
            content = '\n'.join(entries)
            await ctx.send(content)