async def put_doc(
    name: str, original_msg: discord.Message, msg_invoker: discord.Member, page: int = 0
):
    """
    Helper function to get docs
    """
    module_objs, main_embeds = await put_main_doc(name, original_msg)
    if module_objs is None or main_embeds is None:
        return

    allowed_obj_names = {
        "Modules": [],
        "Types": [],
        "Functions": [],
        "Methods": [],
    }

    formatted_obj_names = {
        "module": "Modules",
        "type": "Types",
        "function": "Functions",
        "method_descriptor": "Methods",
    }

    for oname, modmember in module_objs.items():
        if type(modmember).__name__ == "builtin_function_or_method":
            # Disambiguate into funtion or method
            obj_type_name = None
            if isinstance(modmember, types.BuiltinFunctionType):
                obj_type_name = "Functions"
            elif isinstance(modmember, types.BuiltinMethodType):
                obj_type_name = "Methods"
        else:
            obj_type_name = formatted_obj_names.get(type(modmember).__name__)

        if oname.startswith("__") or obj_type_name is None:
            continue

        allowed_obj_names[obj_type_name].append(oname)

    embeds = []

    for otype, olist in allowed_obj_names.items():
        if not olist:
            continue

        embeds.append(
            embed_utils.create(
                title=f"{otype} in `{name}`",
                description=utils.code_block("\n".join(olist)),
            )
        )

    main_embeds.extend(embeds)

    page_embed = embed_utils.PagedEmbed(
        original_msg, main_embeds, msg_invoker, f"doc {name}", page
    )
    await page_embed.mainloop()
Exemple #2
0
    async def cmd_vibecheck(self):
        """
        ->type Play With Me :snake:
        ->signature pg!vibecheck
        ->description Check my mood.
        -----
        Implement pg!vibecheck, to check the snek's emotion
        """
        async with db.DiscordDB("emotions") as db_obj:
            all_emotions = db_obj.get({})

        emotion_percentage = vibecheck.get_emotion_percentage(all_emotions,
                                                              round_by=-1)
        all_emotion_response = vibecheck.get_emotion_desc_dict(all_emotions)

        bot_emotion = max(emotion_percentage.keys(),
                          key=lambda key: emotion_percentage[key])
        msg = all_emotion_response[bot_emotion]["msg"]
        emoji_link = all_emotion_response[bot_emotion]["emoji_link"]

        if all_emotion_response[bot_emotion].get("override_emotion", None):
            bot_emotion = all_emotion_response[bot_emotion]["override_emotion"]

        color = pygame.Color(vibecheck.EMOTION_COLORS[bot_emotion])

        t = time.time()
        pygame.image.save(vibecheck.emotion_pie_chart(all_emotions, 400),
                          f"temp{t}.png")
        file = discord.File(f"temp{t}.png")

        try:
            await self.response_msg.delete()
        except discord.errors.NotFound:
            # Message already deleted
            pass

        embed_dict = {
            "title": f"The snek is {bot_emotion} right now!",
            "description": msg,
            "thumbnail_url": emoji_link,
            "footer_text":
            "This is currently in beta version, so the end product may look different",
            "footer_icon_url":
            "https://cdn.discordapp.com/emojis/844513909158969374.png?v=1",
            "image_url": f"attachment://temp{t}.png",
            "color": utils.color_to_rgb_int(color),
        }
        embed = embed_utils.create(**embed_dict)
        await self.invoke_msg.reply(file=file,
                                    embed=embed,
                                    mention_author=False)

        os.remove(f"temp{t}.png")
async def handle(invoke_msg: discord.Message,
                 response_msg: discord.Message = None):
    """
    Handle a pg! command posted by a user
    """
    is_admin, is_priv = get_perms(invoke_msg.author)

    if is_admin and invoke_msg.content.startswith(f"{common.PREFIX}stop"):
        splits = invoke_msg.content.strip().split(" ")
        splits.pop(0)
        try:
            if splits:
                for uid in map(utils.filter_id, splits):
                    if uid in common.TEST_USER_IDS:
                        break
                else:
                    return

        except ValueError:
            if response_msg is None:
                await embed_utils.send(
                    invoke_msg.channel,
                    title="Invalid arguments!",
                    description=
                    "All arguments must be integer IDs or member mentions",
                    color=0xFF0000,
                )
            else:
                await embed_utils.replace(
                    response_msg,
                    title="Invalid arguments!",
                    description=
                    "All arguments must be integer IDs or member mentions",
                    color=0xFF0000,
                )
            return

        if response_msg is None:
            await embed_utils.send(
                invoke_msg.channel,
                title="Stopping bot...",
                description="Change da world,\nMy final message,\nGoodbye.",
            )
        else:
            await embed_utils.replace(
                response_msg,
                title="Stopping bot...",
                description="Change da world,\nMy final message,\nGoodbye.",
            )
        sys.exit(0)

    if (common.TEST_MODE and common.TEST_USER_IDS
            and invoke_msg.author.id not in common.TEST_USER_IDS):
        return

    if response_msg is None:
        response_msg = await embed_utils.send(
            invoke_msg.channel,
            title="Your command is being processed:",
            fields=(("\u2800", "`Loading...`", False), ),
        )

    if not common.TEST_MODE and not common.GENERIC:
        log_txt_file = None
        escaped_cmd_text = discord.utils.escape_markdown(invoke_msg.content)
        if len(escaped_cmd_text) > 2047:
            with io.StringIO(invoke_msg.content) as log_buffer:
                log_txt_file = discord.File(log_buffer, filename="command.txt")

        await common.log_channel.send(
            embed=embed_utils.create(
                title=
                f"Command invoked by {invoke_msg.author} / {invoke_msg.author.id}",
                description=escaped_cmd_text if len(escaped_cmd_text) <= 2047
                else escaped_cmd_text[:2044] + "...",
                fields=((
                    "\u200b",
                    f"by {invoke_msg.author.mention}\n**[View Original]({invoke_msg.jump_url})**",
                    False,
                ), ),
            ),
            file=log_txt_file,
        )

    cmd = (admin.AdminCommand(invoke_msg, response_msg)
           if is_admin else user.UserCommand(invoke_msg, response_msg))
    cmd.is_priv = is_priv
    await cmd.handle_cmd()
    return response_msg
async def put_main_doc(name: str, original_msg: discord.Message):
    """
    Put main part of the doc into embed(s)
    """
    splits = name.split(".")

    try:
        is_builtin = bool(getattr(builtins, splits[0]))
    except AttributeError:
        is_builtin = False

    if splits[0] not in doc_module_dict and not is_builtin:
        await embed_utils.replace(
            original_msg,
            title="Unknown module!",
            description="No such module was found.",
        )
        return None, None

    module_objs = dict(doc_module_dict)
    obj = None

    for part in splits:
        try:
            try:
                obj = getattr(builtins, part)
            except AttributeError:
                obj = module_objs[part]

            module_objs = {}
            for i in dir(obj):
                if i != "__abstractmethods__":
                    module_objs[i] = getattr(obj, i)
        except KeyError:
            await embed_utils.replace(
                original_msg,
                title="Class/function/sub-module not found!",
                description=f"There's no such thing here named `{name}`",
            )
            return None, None

    if isinstance(obj, (int, float, str, dict, list, tuple, bool)):
        await embed_utils.replace(
            original_msg,
            title=f"Documentation for `{name}`",
            description=f"{name} is a constant with a type of "
            f"`{obj.__class__.__name__}` which does not have documentation.",
        )
        return None, None

    header = ""
    if splits[0] == "pygame":
        doclink = "https://www.pygame.org/docs"
        if len(splits) > 1:
            doclink += "/ref/" + splits[1].lower() + ".html"
            doclink += "#"
            doclink += "".join([s + "." for s in splits])[:-1]
        header = "Online documentation: " + doclink + "\n"

    docs = "" if obj.__doc__ is None else obj.__doc__

    embeds = []
    lastchar = 0
    cnt = 0
    while len(docs) >= lastchar:
        cnt += 1
        if cnt >= common.DOC_EMBED_LIMIT:
            text = docs[lastchar:]
        else:
            text = docs[lastchar : lastchar + 2040]

            # Try to split docs into paragraphs. If that does not work, split
            # based on sentences. If that too does not work, then just split
            # blindly
            ind = text.rfind("\n\n")
            if ind > 1500:
                lastchar += ind
                text = text[:lastchar]
            else:
                ind = text.rfind("\n")
                if ind > 1500:
                    lastchar += ind
                    text = text[:lastchar]
                else:
                    lastchar += 2040

        if text:
            embeds.append(
                embed_utils.create(
                    title=f"Documentation for `{name}`",
                    description=header + utils.code_block(text),
                )
            )

        header = ""
        if cnt >= common.DOC_EMBED_LIMIT:
            break

    if not embeds:
        await embed_utils.replace(
            original_msg,
            title="Class/function/sub-module not found!",
            description=f"There's no such thing here named `{name}`",
        )
        return None, None

    return module_objs, embeds
Exemple #5
0
    async def cmd_sudo_clone(
        self,
        *msgs: discord.Message,
        destination: Optional[common.Channel] = None,
        embeds: bool = True,
        attachments: bool = True,
        as_spoiler: bool = False,
        info: bool = False,
        author_info: bool = True,
        skip_empty: bool = True,
    ):
        """
        ->type More admin commands
        ->signature pg!sudo clone <*messages> [destination=] [embeds=True] [attachments=True] [as_spoiler=False]
        [info=False] [author_info=True]
        ->description Clone a message through the bot
        ->extended description
        Clone the given messages and send them to the given destination channel.

        __Args__:
            `*messages: (Message)`
            > A sequence of discord messages whose text,
            contents, attachments or embeds should be cloned.

            `destination: (Channel) =`
            > A destination channel to send the cloned outputs to.
            > If omitted, the destination will be the channel where
            > this command was invoked.

            `as_attachment: (bool) = False`
            > Whether the text content (if present) of the given
            > messages should be sent as an attachment (`.txt`)
            > or as embed containing it inside a code block in its
            > description.

            `attachments: (bool) = True`
            > Whether the attachments of the given messages
            > should be cloned as well (if possible).

            `embeds: (bool) = True`
            > Whether the embeds of the given messages
            > should be cloned along with the outut messages.

        +===+

            `as_spoiler: (bool) = False`
            > If set to `True`, the attachments of the input messages
            > will be explicitly marked as spoilers when sent to the
            > destination channel.

            `info: (bool) = False`
            > If set to `True`, an embed containing info
            > about each message will be sent along with
            > the message data output.

            `author_info: (bool) = True`
            > If set to `True`, extra information about
            > the message authors will be added to the
            > info embed which is sent if `info` is set
            > to `True`.

            `skip_empty: (bool) = True`
            > Whether empty messages
            > should be skipped.

        __Returns__:
            > One or more cloned messages with attachents
            > or embeds based on the given input.

        __Raises__:
            > `BotException`: One or more given arguments are invalid.
            > `HTTPException`: An invalid operation was blocked by Discord.
        -----
        """

        if not isinstance(destination, discord.TextChannel):
            destination = self.channel

        if not utils.check_channel_permissions(
            self.author, destination, permissions=("view_channel", "send_messages")
        ):
            raise BotException(
                "Not enough permissions",
                "You do not have enough permissions to run this command with the specified arguments.",
            )

        checked_channels = set()
        for i, msg in enumerate(msgs):
            if msg.channel not in checked_channels:
                if not utils.check_channel_permissions(
                    self.author, msg.channel, permissions=("view_channel",)
                ):
                    raise BotException(
                        "Not enough permissions",
                        "You do not have enough permissions to run this command with the specified arguments.",
                    )
                else:
                    checked_channels.add(msg.channel)

            if not i % 50:
                await asyncio.sleep(0)

        if not msgs:
            raise BotException(
                "Invalid arguments!",
                "No messages given as input.",
            )

        load_embed = embed_utils.create(
            title="Your command is being processed:",
            fields=(("\u2800", "`...`", False),),
        )

        msg_count = len(msgs)
        no_mentions = discord.AllowedMentions.none()
        for i, msg in enumerate(msgs):
            if msg_count > 2 and not i % 3:
                await embed_utils.edit_field_from_dict(
                    self.response_msg,
                    load_embed,
                    dict(
                        name="Processing Messages",
                        value=f"`{i}/{msg_count}` messages processed\n"
                        f"{(i/msg_count)*100:.01f}% | "
                        + utils.progress_bar(i / msg_count, divisions=30),
                    ),
                    0,
                )

            await destination.trigger_typing()
            cloned_msg0 = None
            attached_files = []
            if msg.attachments and attachments:
                with io.StringIO("This file was too large to be cloned.") as fobj:
                    attached_files = [
                        (
                            await a.to_file(spoiler=a.is_spoiler() or as_spoiler)
                            if a.size <= self.filesize_limit
                            else discord.File(fobj, f"filetoolarge - {a.filename}.txt")
                        )
                        for a in msg.attachments
                    ]

            if msg.content or msg.embeds or attached_files:
                if len(msg.content) > 2000:
                    start_idx = 0
                    stop_idx = 0
                    for i in range(len(msg.content) // 2000):
                        start_idx = 2000 * i
                        stop_idx = 2000 + 2000 * i

                        if not i:
                            cloned_msg0 = await destination.send(
                                content=msg.content[start_idx:stop_idx],
                                allowed_mentions=no_mentions,
                            )
                        else:
                            await destination.send(
                                content=msg.content[start_idx:stop_idx],
                                allowed_mentions=no_mentions,
                            )

                    with io.StringIO(msg.content) as fobj:
                        await destination.send(
                            content=msg.content[stop_idx:],
                            embed=embed_utils.create(footer_text="Full message data"),
                            file=discord.File(fobj, filename="messagedata.txt"),
                            allowed_mentions=no_mentions,
                        )

                    await destination.send(
                        embed=msg.embeds[0] if msg.embeds and embeds else None,
                        file=attached_files[0] if attached_files else None,
                    )
                else:
                    cloned_msg0 = await destination.send(
                        content=msg.content,
                        embed=msg.embeds[0] if msg.embeds and embeds else None,
                        file=attached_files[0] if attached_files else None,
                        allowed_mentions=no_mentions,
                    )
            elif not skip_empty:
                raise BotException("Cannot clone an empty message!", "")

            for i in range(1, len(attached_files)):
                await self.channel.send(
                    file=attached_files[i],
                )

            for i in range(1, len(msg.embeds)):
                await self.channel.send(
                    embed=msg.embeds[i],
                )

            if info:
                await self.channel.send(
                    embed=embed_utils.get_msg_info_embed(msg, author=author_info),
                    reference=cloned_msg0,
                )

            await asyncio.sleep(0)

        if msg_count > 2:
            await embed_utils.edit_field_from_dict(
                self.response_msg,
                load_embed,
                dict(
                    name="Processing Completed",
                    value=f"`{msg_count}/{msg_count}` messages processed\n"
                    "100% | " + utils.progress_bar(1.0, divisions=30),
                ),
                0,
            )

        try:
            await self.response_msg.delete(delay=10 if msg_count > 2 else 0)
        except discord.NotFound:
            pass
Exemple #6
0
    async def cmd_sudo_get(
        self,
        *msgs: discord.Message,
        destination: Optional[common.Channel] = None,
        as_attachment: bool = False,
        attachments: bool = True,
        embeds: bool = True,
        info: bool = False,
        author_info: bool = True,
    ):
        """
        ->type More admin commands
        ->signature pg!sudo_get <*messages> [destination=] [as_attachment=False] [attachments=True]
        [embeds=True] [info=False] [author_info=False]
        ->description Get the text of messages through the bot
        ->extended description
        Get the contents, attachments and serialized embeds of the given messages and send them to the given destination channel.

        __Args__:
            `*messages: (Message)`
            > A sequence of discord messages whose text,
            contents, attachments or embeds should be retrieved.

            `destination: (Channel) =`
            > A destination channel to send the generated outputs to.
            > If omitted, the destination will be the channel where
            > this command was invoked.

            `as_attachment: (bool) = False`
            > Whether the text content (if present) of the given
            > messages should be sent as an attachment (`.txt`)
            > or as embed containing it inside a code block in its
            > description. This will always occur if the text
            > content is above 2000 characters.

            `attachments: (bool) = True`
            > Whether the attachments of the given messages
            > should be retrieved (when possible).

            `embeds: (bool) = True`
            > Whether the embeds of the given messages
            > should be retrieved (as serialized JSON data).

        +===+

            `info: (bool) = False`
            > If set to `True`, an embed containing info
            > about each message will be sent along with
            > the message data output.

            `author_info: (bool) = True`
            > If set to `True`, extra information about
            > the message authors will be added to the
            > info embed which is sent if `info` is set
            > to `True`.

        __Returns__:
            > One or more messages with attachents or embeds
            > based on the given input.

        __Raises__:
            > `BotException`: One or more given arguments are invalid.
            > `HTTPException`: An invalid operation was blocked by Discord.
        -----
        """
        if not isinstance(destination, discord.TextChannel):
            destination = self.channel

        if not utils.check_channel_permissions(
            self.author, destination, permissions=("view_channel", "send_messages")
        ):
            raise BotException(
                "Not enough permissions",
                "You do not have enough permissions to run this command with the specified arguments.",
            )

        checked_channels = set()
        for i, msg in enumerate(msgs):
            if msg.channel not in checked_channels:
                if not utils.check_channel_permissions(
                    self.author, msg.channel, permissions=("view_channel",)
                ):
                    raise BotException(
                        "Not enough permissions",
                        "You do not have enough permissions to run this command with the specified arguments.",
                    )
                else:
                    checked_channels.add(msg.channel)

            if not i % 50:
                await asyncio.sleep(0)

        if not msgs:
            raise BotException(
                "Invalid arguments!",
                "No messages given as input.",
            )

        load_embed = embed_utils.create(
            title="Your command is being processed:",
            fields=(("\u2800", "`...`", False),),
        )

        msg_count = len(msgs)
        for i, msg in enumerate(msgs):
            if msg_count > 2 and not i % 3:
                await embed_utils.edit_field_from_dict(
                    self.response_msg,
                    load_embed,
                    dict(
                        name="Processing Messages",
                        value=f"`{i}/{msg_count}` messages processed\n"
                        f"{(i/msg_count)*100:.01f}% | "
                        + utils.progress_bar(i / msg_count, divisions=30),
                    ),
                    0,
                )
            await destination.trigger_typing()

            escaped_msg_content = msg.content.replace("```", "\\`\\`\\`")
            attached_files = None
            if attachments:
                with io.StringIO("This file was too large to be duplicated.") as fobj:
                    attached_files = [
                        (
                            await a.to_file(spoiler=a.is_spoiler())
                            if a.size <= self.filesize_limit
                            else discord.File(fobj, f"filetoolarge - {a.filename}.txt")
                        )
                        for a in msg.attachments
                    ]

            if info:
                info_embed = embed_utils.get_msg_info_embed(msg, author_info)
                info_embed.set_author(name="Message data & info")
                info_embed.title = ""

                info_embed.description = "".join(
                    (
                        "__Text"
                        + (" (Shortened)" if len(escaped_msg_content) > 2000 else "")
                        + "__:",
                        f"\n\n ```\n{escaped_msg_content[:2001]}\n\n[...]\n```"
                        + "\n\u2800"
                        if len(escaped_msg_content) > 2000
                        else "\n\u2800",
                    )
                )

                content_file = None
                if as_attachment or len(msg.content) > 2000:
                    with io.StringIO(msg.content) as fobj:
                        content_file = discord.File(fobj, "messagedata.txt")

                await destination.send(embed=info_embed, file=content_file)

            elif as_attachment:
                with io.StringIO(msg.content) as fobj:
                    await destination.send(
                        file=discord.File(fobj, "messagedata.txt"),
                        embed=embed_utils.create(
                            author_name="Message data",
                            description=f"**[View Original Message]({msg.jump_url})**",
                        ),
                    )
            else:
                if len(msg.content) > 2000 or len(escaped_msg_content) > 2000:
                    with io.StringIO(msg.content) as fobj:
                        await destination.send(
                            file=discord.File(fobj, "messagedata.txt"),
                            embed=embed_utils.create(
                                author_name="Message data",
                                description=f"**[View Original Message]({msg.jump_url})**",
                            ),
                        )
                else:
                    await embed_utils.send(
                        self.channel,
                        author_name="Message data",
                        description="```\n{0}```".format(escaped_msg_content),
                        fields=(
                            (
                                "\u2800",
                                f"**[View Original Message]({msg.jump_url})**",
                                False,
                            ),
                        ),
                    )

            if attached_files:
                for i in range(len(attached_files)):
                    await self.channel.send(
                        content=f"**Message attachment** ({i+1}):",
                        file=attached_files[i],
                    )

            if embeds and msg.embeds:
                embed_data_fobjs = []
                for embed in msg.embeds:
                    embed_data_fobj = io.StringIO()
                    embed_utils.export_embed_data(
                        embed.to_dict(),
                        fp=embed_data_fobj,
                        indent=4,
                        as_json=True,
                    )
                    embed_data_fobj.seek(0)
                    embed_data_fobjs.append(embed_data_fobj)

                for i in range(len(embed_data_fobjs)):
                    await self.channel.send(
                        content=f"**Message embed** ({i+1}):",
                        file=discord.File(
                            embed_data_fobjs[i], filename="embeddata.json"
                        ),
                    )

                for embed_data_fobj in embed_data_fobjs:
                    embed_data_fobj.close()

            await asyncio.sleep(0)

        if msg_count > 2:
            await embed_utils.edit_field_from_dict(
                self.response_msg,
                load_embed,
                dict(
                    name="Processing Completed",
                    value=f"`{msg_count}/{msg_count}` messages processed\n"
                    "100% | " + utils.progress_bar(1.0, divisions=30),
                ),
                0,
            )

        try:
            await self.response_msg.delete(delay=10 if msg_count > 2 else 0)
        except discord.NotFound:
            pass
Exemple #7
0
    async def cmd_sudo(
        self,
        *datas: Union[discord.Message, String],
        destination: Optional[common.Channel] = None,
        from_attachment: bool = True,
        mention: bool = False,
    ):
        """
        ->type More admin commands
        ->signature pg!sudo <*datas> [destination=] [from_attachment=True]
        ->description Send a message through the bot
        ->extended description
        Send a sequence of messages contain text from the given
        data using the specified arguments.

        __Args__:
            `*datas: (Message|String)`
            > A sequence of discord messages whose text
            > or text attachment should be used as input,
            > or strings.

            `destination (TextChannel) = `
            > A destination channel to send the output to.

            `from_attachment (bool) = True`
            > Whether the attachment of an input message should be
            > used to create a message.

            `mention (bool) = False`
            > Whether any mentions in the given input text
            > should ping their target. If set to `True`,
            > any role/user/member that the bot is allowed to ping will
            > be pinged.

        __Returns__:
            > One or more generated messages based on the given input.

        __Raises__:
            > `BotException`: One or more given arguments are invalid.
            > `HTTPException`: An invalid operation was blocked by Discord.

        ->example command
        pg!sudo "lol" "that" "was" "funny /s" destination=#general
        pg!sudo 987654321987654321 "Additionally, ..." 123456739242423 from_attachment=True
        -----
        Implement pg!sudo, for admins to send messages via the bot
        """

        if destination is None:
            destination = self.channel

        if not utils.check_channel_permissions(
            self.author, destination, permissions=("view_channel", "send_messages")
        ):
            raise BotException(
                "Not enough permissions",
                "You do not have enough permissions to run this command with the specified arguments.",
            )

        for i, data in enumerate(datas):
            if isinstance(data, discord.Message):
                if not utils.check_channel_permissions(
                    self.author,
                    data.channel,
                    permissions=("view_channel",),
                ):
                    raise BotException(
                        "Not enough permissions",
                        "You do not have enough permissions to run this command with the specified arguments.",
                    )

            if not i % 50:
                await asyncio.sleep(0)

        output_strings = []
        load_embed = embed_utils.create(
            title="Your command is being processed:",
            fields=(
                ("\u2800", "`...`", False),
                ("\u2800", "`...`", False),
            ),
        )
        data_count = len(datas)
        for i, data in enumerate(datas):
            if data_count > 2 and not i % 3:
                await embed_utils.edit_field_from_dict(
                    self.response_msg,
                    load_embed,
                    dict(
                        name="Processing Inputs",
                        value=f"`{i}/{data_count}` inputs processed\n"
                        f"{(i/data_count)*100:.01f}% | "
                        + utils.progress_bar(i / data_count, divisions=30),
                    ),
                    0,
                )
            attachment_msg = None

            if isinstance(data, String):
                if not data.string:
                    attachment_msg = self.invoke_msg
                else:
                    msg_text = data.string
                    output_strings.append(msg_text)

            elif isinstance(data, discord.Message):
                if not utils.check_channel_permissions(
                    self.author,
                    data.channel,
                    permissions=("view_channel",),
                ):
                    raise BotException(
                        "Not enough permissions",
                        "You do not have enough permissions to run this command with the specified arguments.",
                    )
                if from_attachment:
                    attachment_msg = data
                else:
                    src_msg_txt = data.content
                    if src_msg_txt:
                        output_strings.append(src_msg_txt)
                    else:
                        raise BotException(
                            f"Input {i}: No message text found!",
                            "The message given as input does not have any text content.",
                        )

            if attachment_msg:
                if not attachment_msg.attachments:
                    raise BotException(
                        f"Input {i}: No valid attachment found in message.",
                        "It must be a `.txt` file containing text data."
                        " If you want to retrieve the content of the"
                        " given message(s) instead, set the"
                        "` from_attachment=` argument to `False`",
                    )

                for attachment in attachment_msg.attachments:
                    if (
                        attachment.content_type is not None
                        and attachment.content_type.startswith("text")
                    ):
                        attachment_obj = attachment
                        break
                else:
                    raise BotException(
                        f"Input {i}: No valid attachment found in message.",
                        "It must be a `.txt` file containing text data.",
                    )

                msg_text = await attachment_obj.read()
                msg_text = msg_text.decode()

                if 0 < len(msg_text) <= 2000:
                    output_strings.append(msg_text)
                else:
                    raise BotException(
                        f"Input {i}: Too little/many characters!",
                        "a Discord message must contain at least one character and cannot contain more than 2000.",
                    )

            await asyncio.sleep(0)

        if not datas:
            data_count = 1
            attachment_msg = self.invoke_msg
            if not attachment_msg.attachments:
                raise BotException(
                    "No valid attachment found in message.",
                    "It must be a `.txt` file containing text data.",
                )

            for attachment in attachment_msg.attachments:
                if (
                    attachment.content_type is not None
                    and attachment.content_type.startswith("text")
                ):
                    attachment_obj = attachment
                    break
            else:
                raise BotException(
                    "No valid attachment found in message.",
                    "It must be a `.txt` file containing text data.",
                )

            msg_text = await attachment_obj.read()
            msg_text = msg_text.decode()

            if 0 < len(msg_text) <= 2000:
                output_strings.append(msg_text)
            else:
                raise BotException(
                    "Too little/many characters!",
                    "a Discord message must contain at least one character and cannot contain more than 2000.",
                )

        if data_count > 2:
            await embed_utils.edit_field_from_dict(
                self.response_msg,
                load_embed,
                dict(
                    name="Processing Completed",
                    value=f"`{data_count}/{data_count}` inputs processed\n"
                    "100% | " + utils.progress_bar(1.0, divisions=30),
                ),
                0,
            )

        allowed_mentions = (
            discord.AllowedMentions.all() if mention else discord.AllowedMentions.none()
        )
        output_count = len(output_strings)
        for j, msg_txt in enumerate(output_strings):
            if output_count > 2 and not j % 3:
                await embed_utils.edit_field_from_dict(
                    self.response_msg,
                    load_embed,
                    dict(
                        name="Creating Messages",
                        value=f"`{j}/{output_count}` messages created\n"
                        f"{(j/output_count)*100:.01f}% | "
                        + utils.progress_bar(j / output_count, divisions=30),
                    ),
                    1,
                )
            await destination.send(content=msg_txt, allowed_mentions=allowed_mentions)

        if data_count > 2:
            await embed_utils.edit_field_from_dict(
                self.response_msg,
                load_embed,
                dict(
                    name="Creation Completed",
                    value=f"`{output_count}/{output_count}` messages created\n"
                    "100% | " + utils.progress_bar(1.0, divisions=30),
                ),
                1,
            )

        try:
            await self.invoke_msg.delete()
            await self.response_msg.delete(delay=10.0 if data_count > 2 else 0.0)
        except discord.NotFound:
            pass
async def send_help_message(
    original_msg: discord.Message,
    invoker: discord.Member,
    commands: tuple[str, ...],
    cmds_and_funcs: dict[str, typing.Callable],
    groups: dict[str, list],
    page: int = 0,
):
    """
    Edit original_msg to a help message. If command is supplied it will
    only show information about that specific command. Otherwise sends
    the general help embed.

    Args:
        original_msg: The message to edit
        invoker: The member who requested the help command
        commands: A tuple of command names passed by user for help.
        cmds_and_funcs: The name-function pairs to get the docstrings from
        groups: The name-list pairs of group commands
        page: The page of the embed, 0 by default
    """

    doc_fields = {}
    embeds = []

    if not commands:
        functions = {}
        for key, func in cmds_and_funcs.items():
            if hasattr(func, "groupname"):
                key = f"{func.groupname} {' '.join(func.subcmds)}"
            functions[key] = func

        for func in functions.values():
            data = get_doc_from_func(func)
            if not data:
                continue

            if not doc_fields.get(data["type"]):
                doc_fields[data["type"]] = ["", "", True]

            doc_fields[data["type"]][0] += f"{data['signature'][2:]}\n"
            doc_fields[data["type"]][1] += (f"`{data['signature']}`\n"
                                            f"{data['description']}\n\n")

        doc_fields_cpy = doc_fields.copy()

        for doc_field_name in doc_fields:
            doc_field_list = doc_fields[doc_field_name]
            doc_field_list[
                1] = f"```\n{doc_field_list[0]}\n```\n\n{doc_field_list[1]}"
            doc_field_list[0] = f"__**{doc_field_name}**__"

        doc_fields = doc_fields_cpy

        embeds.append(
            discord.Embed(
                title=common.BOT_HELP_PROMPT["title"],
                description=common.BOT_HELP_PROMPT["description"],
                color=common.BOT_HELP_PROMPT["color"],
            ))
        for doc_field in list(doc_fields.values()):
            body = f"{doc_field[0]}\n\n{doc_field[1]}"
            embeds.append(
                embed_utils.create(
                    title=common.BOT_HELP_PROMPT["title"],
                    description=body,
                    color=common.BOT_HELP_PROMPT["color"],
                ))

    elif commands[0] in cmds_and_funcs:
        func_name = commands[0]
        funcs = groups[func_name] if func_name in groups else []
        funcs.insert(0, cmds_and_funcs[func_name])

        for func in funcs:
            if (commands[1:]
                    and commands[1:] != getattr(func, "subcmds",
                                                ())[:len(commands[1:])]):
                continue

            doc = get_doc_from_func(func)
            if not doc:
                # function found, but does not have help.
                return await embed_utils.replace(
                    original_msg,
                    title="Could not get docs",
                    description="Command has no documentation",
                    color=0xFF0000,
                )

            body = f"`{doc['signature']}`\n`Category: {doc['type']}`\n\n"

            desc = doc["description"]

            ext_desc = doc.get("extended description")
            if ext_desc:
                desc = f"> *{desc}*\n\n{ext_desc}"

            desc_list = desc.split(sep="+===+")

            body += f"**Description:**\n{desc_list[0]}"

            embed_fields = []

            example_cmd = doc.get("example command")
            if example_cmd:
                embed_fields.append(["Example command(s):", example_cmd, True])

            if len(desc_list) == 1:
                embeds.append(
                    embed_utils.create(
                        title=f"Help for `{func_name}`",
                        description=body,
                        color=common.BOT_HELP_PROMPT["color"],
                        fields=embed_fields,
                    ))
            else:
                embeds.append(
                    embed_utils.create(
                        title=f"Help for `{func_name}`",
                        description=body,
                        color=common.BOT_HELP_PROMPT["color"],
                    ))
                desc_list_len = len(desc_list)
                for i in range(1, desc_list_len):
                    embeds.append(
                        embed_utils.create(
                            title=f"Help for `{func_name}`",
                            description=desc_list[i],
                            color=common.BOT_HELP_PROMPT["color"],
                            fields=embed_fields if i == desc_list_len - 1 else
                            (),
                        ))

    if not embeds:
        return await embed_utils.replace(
            original_msg,
            title="Command not found",
            description="No such command exists",
            color=0xFF0000,
        )

    await embed_utils.PagedEmbed(original_msg, embeds, invoker,
                                 f"help {' '.join(commands)}",
                                 page).mainloop()