Exemple #1
0
    async def cmd_stream_del(self,
                             *,
                             _members: Optional[tuple[discord.Member,
                                                      ...]] = None):
        """
        ->type Reminders
        ->signature pg!stream del
        ->description Remove yourself from the stream-ping-list
        ->extended description
        Remove yourself from the stream-ping-list. You can always add you later
        with `pg!stream add`
        """
        async with db.DiscordDB("stream") as ping_db:
            data: list = ping_db.get([])

            try:
                if _members:
                    for mem in _members:
                        data.remove(mem.id)
                else:
                    data.remove(self.author.id)
            except ValueError:
                raise BotException(
                    "Could not remove member",
                    "Member was not previously added to the ping list",
                )

            ping_db.write(data)

        await self.cmd_stream()
Exemple #2
0
    async def cmd_fontify(self, msg: String):
        """
        ->type Play With Me :snake:
        ->signature pg!fontify <msg>
        ->description Display message in pygame font
        """
        fontified = ""

        emojis = ()
        if common.guild is not None:
            emojis = tuple(sorted(common.guild.emojis, key=lambda x: x.name))

        for char in unidecode.unidecode(msg.string):
            if char.isalnum():
                for emoji in emojis:
                    if (emoji.name == f"pg_char_{char}"
                            or emoji.name == f"pg_char_{char}".lower()):
                        fontified += str(emoji)
                        break
                else:
                    fontified += ":heavy_multiplication_x:"

            elif char.isspace():
                fontified += " " * 5

            else:
                fontified += ":heavy_multiplication_x:"

        if len(fontified) > 2000:
            raise BotException(
                "Could not execute comamnd",
                "Input text width exceeded maximum limit (2KB)",
            )

        if not fontified:
            raise BotException(
                "Could not execute comamnd",
                "Text cannot be empty",
            )

        await embed_utils.replace(
            self.response_msg,
            description=self.author.mention,
            color=0x40E32D,
        )

        await self.response_msg.edit(content=fontified)
Exemple #3
0
    async def cmd_refresh(self, msg: discord.Message):
        """
        ->type Other commands
        ->signature pg!refresh <message>
        ->description Refresh a message which support pages.
        -----
        Implement pg!refresh, to refresh a message which supports pages
        """

        if (not msg.embeds or not msg.embeds[0].footer
                or not isinstance(msg.embeds[0].footer.text, str)):
            raise BotException(
                "Message does not support pages",
                "The message specified does not support pages. Make sure you "
                "have replied to the correct message",
            )

        data = msg.embeds[0].footer.text.splitlines()

        if len(data) != 3 and not data[2].startswith("Command: "):
            raise BotException(
                "Message does not support pages",
                "The message specified does not support pages. Make sure "
                "the id of the message is correct.",
            )

        page = re.search(r"\d+", data[0]).group()
        cmd_str = data[2].replace("Command: ", "")

        if not page.isdigit() or not cmd_str:
            raise BotException(
                "Message does not support pages",
                "The message specified does not support pages. Make sure "
                "the id of the message is correct.",
            )

        try:
            await self.response_msg.delete()
        except discord.errors.NotFound:
            pass

        # Handle the new command, the one that pg!refresh is trying to refresh
        self.response_msg = msg
        self.cmd_str = cmd_str
        self.page = int(page) - 1
        await self.handle_cmd()
Exemple #4
0
    async def cmd_reminders_remove(self, *reminder_ids: int):
        """
        ->type Reminders
        ->signature pg!reminders remove [*ids]
        ->description Remove reminders
        ->extended description
        Remove variable number of reminders, corresponding to each datetime argument
        The reminder id argument must be an integer
        If no arguments are passed, the command clears all reminders
        ->example command pg!reminders remove 1
        -----
        Implement pg!reminders_remove, for users to remove their reminders
        """
        async with db.DiscordDB("reminders") as db_obj:
            db_data = db_obj.get({})
            db_data_copy = copy.deepcopy(db_data)
            cnt = 0
            if reminder_ids:
                for reminder_id in sorted(set(reminder_ids), reverse=True):
                    if self.author.id in db_data:
                        for i, dt in enumerate(db_data_copy[self.author.id]):
                            if i == reminder_id:
                                db_data[self.author.id].pop(dt)
                                cnt += 1
                                break
                    if (reminder_id >= len(db_data_copy[self.author.id])
                            or reminder_id < 0):
                        raise BotException(
                            "Invalid Reminder ID!",
                            "Reminder ID was not an existing reminder ID",
                        )

                if self.author.id in db_data and not db_data[self.author.id]:
                    db_data.pop(self.author.id)

            elif self.author.id in db_data:
                cnt = len(db_data.pop(self.author.id))

            db_obj.write(db_data)

        await embed_utils.replace(
            self.response_msg,
            title="Reminders removed!",
            description=f"Successfully removed {cnt} reminder(s)",
        )
Exemple #5
0
    async def cmd_fontify_remove(self, reply: discord.Message):
        """
        ->type Play With Me :snake:
        ->signature pg!fontify remove
        ->description Delete your fontified message by replying to it
        """

        # make typecheckers happy
        if common.bot.user is None:
            return

        if (reply.author.id != common.bot.user.id or not reply.embeds
                or reply.embeds[0].description != self.author.mention):
            raise BotException("Could not execute comamnd",
                               "Please reply to a fontified message")

        await reply.delete()
        try:
            await self.invoke_msg.delete()
            await self.response_msg.delete()
        except discord.NotFound:
            pass
Exemple #6
0
    async def cmd_rules(self, *rules: int):
        """
        ->type Get help
        ->signature pg!rules [*rule_numbers]
        ->description Get rules of the server
        -----
        Implement pg!rules, to get rules of the server
        """
        if not rules:
            raise BotException("Please enter rule number(s)", "")

        if common.GENERIC:
            raise BotException(
                "Cannot execute command!",
                "This command cannot be exected when the bot is on generic mode",
            )

        fields = []
        for rule in sorted(set(rules)):
            if 0 < rule <= len(common.ServerConstants.RULES):
                msg = await common.rules_channel.fetch_message(
                    common.ServerConstants.RULES[rule - 1])
                value = msg.content

            elif rule == 42:
                value = (
                    "*Shhhhh*, you have found an unwritten rule!\n"
                    "Click [here](https://bitly.com/98K8eH) to gain the most "
                    "secret and ultimate info!")

            else:
                value = "Does not exist lol"

            if len(str(rule)) > 200:
                raise BotException(
                    "Overflow in command",
                    "Why would you want to check such a large rule number?",
                )

            fields.append({
                "name": f"__Rule number {rule}:__",
                "value": value,
                "inline": False,
            })

        if len(rules) > 25:
            raise BotException(
                "Overflow in command",
                "Too many rules were requested",
            )

        if len(rules) == 1:
            await embed_utils.replace(
                self.response_msg,
                author_name="Pygame Community",
                author_icon_url=common.GUILD_ICON,
                title=fields[0]["name"],
                description=fields[0]["value"][:2048],
                color=0x228B22,
            )
        else:
            for field in fields:
                field["value"] = field["value"][:1024]

            await embed_utils.replace(
                self.response_msg,
                author_name="Pygame Community",
                author_icon_url=common.GUILD_ICON,
                title="Rules",
                fields=fields,
                color=0x228B22,
            )
Exemple #7
0
    async def cmd_clock(
        self,
        action: str = "",
        timezone: Optional[float] = None,
        color: Optional[pygame.Color] = None,
        *,
        _member: Optional[discord.Member] = None,
    ):
        """
        ->type Get help
        ->signature pg!clock
        ->description 24 Hour Clock showing <@&778205389942030377> s who are available to help
        -> Extended description
        People on the clock can run the clock with more arguments, to update their data.
        `pg!clock update [timezone in hours] [color as hex string]`
        `timezone` is float offset from GMT in hours.
        `color` optional color argument, that shows up on the clock.
        Note that you might not always display with that colour.
        This happens if more than one person are on the same timezone
        Use `pg!clock remove` to remove yourself from the clock
        -----
        Implement pg!clock, to display a clock of helpfulies/mods/wizards
        """
        async with db.DiscordDB("clock") as db_obj:
            timezones = db_obj.get({})
            if action:
                if _member is None:
                    member = self.author
                    if member.id not in timezones:
                        raise BotException(
                            "Cannot update clock!",
                            "You cannot run clock update commands because you are "
                            + "not on the clock",
                        )
                else:
                    member = _member

                if action == "update":
                    if timezone is not None and abs(timezone) > 12:
                        raise BotException("Failed to update clock!",
                                           "Timezone offset out of range")

                    if member.id in timezones:
                        if timezone is not None:
                            timezones[member.id][0] = timezone
                        if color is not None:
                            timezones[member.id][1] = utils.color_to_rgb_int(
                                color)
                    else:
                        if timezone is None:
                            raise BotException(
                                "Failed to update clock!",
                                "Timezone is required when adding new people",
                            )

                        if color is None:
                            color = pygame.Color((
                                random.randint(0, 0xFFFFFF) << 8) | 0xFF)

                        timezones[member.id] = [
                            timezone, utils.color_to_rgb_int(color)
                        ]

                    # sort timezones dict after an update operation
                    timezones = dict(
                        sorted(timezones.items(), key=lambda x: x[1][0]))

                elif action == "remove":
                    try:
                        timezones.pop(member.id)
                    except KeyError:
                        raise BotException(
                            "Failed to update clock!",
                            "Cannot remove non-existing person from clock",
                        )

                else:
                    raise BotException("Failed to update clock!",
                                       f"Invalid action specifier {action}")

                db_obj.write(timezones)

        t = time.time()

        pygame.image.save(
            await clock.user_clock(t, timezones, self.get_guild()),
            f"temp{t}.png")
        common.cmd_logs[self.invoke_msg.id] = await self.channel.send(
            file=discord.File(f"temp{t}.png"))
        os.remove(f"temp{t}.png")

        try:
            await self.response_msg.delete()
        except discord.NotFound:
            pass
Exemple #8
0
    async def cmd_sudo_edit(
        self,
        msg: discord.Message,
        data: Union[discord.Message, String],
        from_attachment: bool = True,
    ):
        """
        ->type More admin commands
        ->signature pg!sudo_edit <msg> <data> [from_attachment=True]
        ->description Replace a message that the bot sent
        ->extended description
        Replace the text content of a message using the given attributes.

        __Args__:
            `msg: (Message)`
            > A discord message whose text content
            > should be replaced.

            `data: (Message|String)`
            > The text data that should be used to replace
            > the input message text.

            `from_attachment (bool) = True`
            > Whether the attachment of the input message in `data`
            > should be used to edit the target message. If set to
            > `False`, the text content of the input message in
            > `data` will be used.

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

        ->example command
        pg!sudo edit 9876543211234676789 "bruh"
        pg!sudo edit 1234567890876543345/9876543211234676789 2345678427483744843
        -----
        """

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

        elif isinstance(data, discord.Message) and 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.",
            )

        attachment_msg: Optional[discord.Message] = None
        msg_text = ""

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

        elif isinstance(data, discord.Message):
            if from_attachment:
                attachment_msg = data
            else:
                src_msg_txt = data.content
                if src_msg_txt:
                    msg_text = src_msg_txt
                else:
                    raise BotException(
                        "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(
                    "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 not 0 < len(msg_text) <= 2000:
                raise BotException(
                    "Too little/many characters!",
                    "a Discord message must contain at least one character and cannot contain more than 2000.",
                )
        try:
            await msg.edit(content=msg_text)
        except discord.HTTPException as e:
            raise BotException(
                "An exception occured while handling the command!", e.args[0]
            )
        try:
            await self.invoke_msg.delete()
            await self.response_msg.delete()
        except discord.NotFound:
            pass
Exemple #9
0
    async def cmd_resources(
        self,
        limit: Optional[int] = None,
        filter_tag: Optional[String] = None,
        filter_members: Optional[tuple[discord.Object, ...]] = None,
        oldest_first: bool = False,
    ):
        """
        ->type Get help
        ->signature pg!resources [limit] [filter_tag] [filter_members] [oldest_first]
        ->description Browse through resources.
        ->extended description
        pg!resources takes in additional arguments, though they are optional.
        `oldest_first`: Set oldest_first to True to browse through the oldest resources
        `limit=[num]`: Limits the number of resources to the number
        `filter_tag=[tag]`: Includes only the resources with that tag(s)
        `filter_members=[members]`: Includes only the resources posted by those user(s). Can be a tuple of users
        ->example command pg!resources limit=5 oldest_first=True filter_tag="python, gamedev" filter_member=444116866944991236
        """

        # needed for typecheckers to know that self.author is a member
        if isinstance(self.author, discord.User):
            return

        if common.GENERIC:
            raise BotException(
                "Cannot execute command!",
                "This command cannot be exected when the bot is on generic mode",
            )

        def process_tag(tag: str):
            for to_replace in ("tag_", "tag-", "<", ">", "`"):
                tag = tag.replace(to_replace, "")
            return tag.title()

        resource_entries_channel = common.entry_channels["resource"]

        # Retrieves all messages inside resource entries channel
        msgs: list[discord.Message] = []
        async for msg in resource_entries_channel.history(
                oldest_first=oldest_first):
            if msg.id not in common.ServerConstants.MSGS_TO_FILTER:
                msgs.append(msg)

        if filter_tag:
            # Filter messages based on tag
            for tag in map(str.strip, filter_tag.string.split(",")):
                tag = tag.lower()
                msgs = list(
                    filter(
                        lambda x: f"tag_{tag}" in x.content.lower() or
                        f"tag-<{tag}>" in x.content.lower(),
                        msgs,
                    ))

        if filter_members:
            filter_member_ids = [obj.id for obj in filter_members]
            msgs = list(
                filter(lambda x: x.author.id in filter_member_ids, msgs))

        if limit is not None:
            # Uses list slicing instead of TextChannel.history's limit param
            # to include all param specified messages
            msgs = msgs[:limit]

        tags = {}
        old_tags = {}
        links = {}
        for msg in msgs:
            # Stores the tags (tag_{Your tag here}), old tags (tag-<{your tag here}>),
            # And links inside separate dicts with regex
            links[msg.id] = [
                match.group() for match in re.finditer(
                    "http[s]?://(www.)?[^ \n]+", msg.content)
            ]
            tags[msg.id] = [
                f"`{process_tag(match.group())}` "
                for match in re.finditer("tag_.+", msg.content.lower())
            ]
            old_tags[msg.id] = [
                f"`{process_tag(match.group())}` "
                for match in re.finditer("tag-<.+>", msg.content.lower())
            ]

        pages = []
        copy_msgs = msgs[:]
        i = 1
        while msgs:
            # Constructs embeds based on messages, and store them in pages to
            # be used in the paginator
            top_msg = msgs[:6]
            if len(copy_msgs) > 1:
                title = (f"Retrieved {len(copy_msgs)} entries in "
                         f"#{resource_entries_channel.name}")
            else:
                title = (f"Retrieved {len(copy_msgs)} entry in "
                         f"#{resource_entries_channel.name}")
            current_embed = discord.Embed(title=title)

            # Cycles through the top 6 messages
            for msg in top_msg:
                try:
                    name = msg.content.split("\n")[1].strip().replace("**", "")
                    if not name:
                        continue

                    field_name = f"{i}. {name}, posted by {msg.author.display_name}"
                    # If the field name is > 256 (discord limit), shorten it
                    # with list slicing
                    field_name = f"{field_name[:253]}..."

                    value = msg.content.split(name)[1].removeprefix(
                        "**").strip()
                    # If the preview of the resources > 80, shorten it with list slicing
                    value = f"{value[:80]}..."
                    value += f"\n\nLinks: **[Message]({msg.jump_url})**"

                    for j, link in enumerate(links[msg.id], 1):
                        value += f", [Link {j}]({link})"

                    value += "\nTags: "
                    if tags[msg.id]:
                        value += "".join(tags[msg.id]).removesuffix(",")
                    elif old_tags[msg.id]:
                        value += "".join(old_tags[msg.id]).removesuffix(",")
                    else:
                        value += "None"

                    current_embed.add_field(
                        name=field_name,
                        value=f"{value}\n{common.ZERO_SPACE}",
                        inline=True,
                    )
                    i += 1
                except IndexError:
                    # Suppresses IndexError because of rare bug
                    pass

            pages.append(current_embed)
            msgs = msgs[6:]

        if not pages:
            raise BotException(
                f"Retrieved 0 entries in #{resource_entries_channel.name}",
                "There are no results of resources with those parameters. "
                "Please try again.",
            )

        # Creates a paginator for the caller to use
        page_embed = embed_utils.PagedEmbed(
            self.response_msg,
            pages,
            self.author,
            self.cmd_str,
            self.page,
        )
        await page_embed.mainloop()
Exemple #10
0
    async def cmd_poll(
        self,
        desc: String,
        *emojis: tuple[str, String],
        multi_votes: bool = False,
        _destination: Optional[common.Channel] = None,
        _admin_embed_dict: dict = {},
    ):
        """
        ->type Other commands
        ->signature pg!poll <description> [*emojis] [multi_votes=True]
        ->description Start a poll.
        ->extended description
        `pg!poll description *args`
        The args must series of two element tuples, first element being emoji,
        and second being the description (see example command).
        The emoji must be a default emoji or one from this server. To close the poll see 'pg!poll close'.
        A `multi_votes` arg can also be passed indicating if the user can cast multiple votes in a poll or not
        ->example command pg!poll "Which apple is better?" ( 🍎 "Red apple") ( 🍏 "Green apple")
        """

        destination = self.channel if _destination is None else _destination

        base_embed_dict = {
            "title":
            "Voting in progress",
            "fields": [
                {
                    "name": "🔺",
                    "value": "Agree",
                    "inline": True,
                },
                {
                    "name": "🔻",
                    "value": "Disagree",
                    "inline": True,
                },
            ],
            "author": {
                "name": self.author.name,
            },
            "color":
            0x34A832,
            "footer": {
                "text":
                f"By {self.author.display_name}\n({self.author.id})\n"
                f"{'' if multi_votes else common.UNIQUE_POLL_MSG}Started"
            },
            "timestamp":
            self.response_msg.created_at.isoformat(),
            "description":
            desc.string,
        }
        base_embed_dict.update(_admin_embed_dict)

        # Make into dict because we want to get rid of emoji repetitions
        emojis_dict = {k.strip(): v.string.strip() for k, v in emojis}
        if emojis_dict:
            if len(emojis_dict) == 1:
                raise BotException(
                    "Invalid arguments for emojis",
                    "Please add at least 2 options in the poll\n"
                    "For more information, see `pg!help poll`",
                )

            base_embed_dict["fields"] = [{
                "name": k,
                "value": v,
                "inline": True
            } for k, v in emojis_dict.items()]

        final_embed = discord.Embed.from_dict(base_embed_dict)
        poll_msg = await destination.send(embed=final_embed)
        try:
            await self.response_msg.delete()
        except discord.errors.NotFound:
            pass

        for field in base_embed_dict["fields"]:
            try:
                emoji_id = utils.filter_emoji_id(field["name"].strip())
                emoji = common.bot.get_emoji(emoji_id)
                if emoji is None:
                    raise ValueError()
            except ValueError:
                emoji = field["name"]

            try:
                await poll_msg.add_reaction(emoji)
            except (discord.errors.HTTPException, discord.errors.NotFound):
                # Either a custom emoji was used (which could not be added by
                # our beloved snek) or some other error happened. Clear the
                # reactions and prompt the user to make sure it is the currect
                # emoji.
                await poll_msg.clear_reactions()
                raise BotException(
                    "Invalid emoji",
                    "The emoji could not be added as a reaction. Make sure it is"
                    " the correct emoji and that it is not from another server",
                )
Exemple #11
0
    async def cmd_poll_close(
        self,
        msg: discord.Message,
        *,
        _color: Optional[pygame.Color] = None,
    ):
        """
        ->type Other commands
        ->signature pg!poll close <message>
        ->description Close an ongoing poll.
        ->extended description
        The poll can only be closed by the person who started it or by mods.
        """
        # needed for typecheckers to know that self.author is a member
        if isinstance(self.author, discord.User):
            return

        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.",
            )

        if not msg.embeds:
            raise BotException(
                "Invalid message",
                "The message specified is not an ongoing vote."
                " Please double-check the id.",
            )

        embed = msg.embeds[0]
        if not isinstance(embed.footer.text, str):
            raise BotException(
                "Invalid message",
                "The message specified is not an ongoing vote."
                " Please double-check the id.",
            )

        # Take the second line remove the parenthesies
        if embed.footer.text and embed.footer.text.count("\n"):
            poll_owner = int(
                embed.footer.text.split("\n")[1].replace("(",
                                                         "").replace(")", ""))
        else:
            raise BotException(
                "Invalid message",
                "The message specified is not an ongiong vote."
                " Please double-check the id.",
            )

        if _color is None and self.author.id != poll_owner:
            raise BotException(
                "You can't stop this vote",
                "The vote was not started by you."
                " Ask the person who started it to close it.",
            )

        title = "Voting has ended"
        reactions = {}
        for reaction in msg.reactions:
            if isinstance(reaction.emoji, str):
                reactions[reaction.emoji] = reaction.count
            else:
                reactions[reaction.emoji.id] = reaction.count

        top: list[tuple[int, Any]] = [(0, None)]
        for reaction in msg.reactions:
            if getattr(reaction.emoji, "id", reaction.emoji) not in reactions:
                continue

            if reaction.count - 1 > top[0][0]:
                top = [(reaction.count - 1,
                        getattr(reaction.emoji, "id", reaction.emoji))]
                continue

            if reaction.count - 1 == top[0][0]:
                top.append((reaction.count - 1, reaction.emoji))

        fields = []
        for field in embed.fields:
            if not isinstance(field.name, str):
                continue

            try:
                r_count = reactions[utils.filter_emoji_id(field.name)] - 1
            except KeyError:
                # The reactions and the embed fields dont match up.
                # Someone is abusing their mod powers if this happens probably.
                continue

            fields.append(
                [field.name, f"{field.value} ({r_count} votes)", True])

            if utils.filter_emoji_id(field.name) == top[0][1]:
                title += (
                    f"\n{field.value}({field.name}) has won with {top[0][0]} votes!"
                )

        if len(top) >= 2:
            title = title.split("\n")[0]
            title += "\nIt's a draw!"

        await embed_utils.edit(
            msg,
            embed,
            color=0xA83232 if not _color else utils.color_to_rgb_int(_color),
            title=title,
            fields=fields,
            footer_text="Ended",
            timestamp=self.response_msg.created_at,
        )
        try:
            await self.response_msg.delete()
        except discord.errors.NotFound:
            pass
Exemple #12
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
Exemple #13
0
    async def cmd_reminders_add(
        self,
        msg: String,
        on: datetime.datetime,
        *,
        _delta: Optional[datetime.timedelta] = None,
    ):
        """
        ->type Reminders
        ->signature pg!reminders add <message> <datetime in iso format>
        ->description Set a reminder to yourself
        ->extended description
        Allows you to set a reminder to yourself
        The date-time must be an ISO time formatted string, in UTC time
        string
        ->example command pg!reminders add "do the thing" "2034-10-26 11:19:36"
        -----
        Implement pg!reminders_add, for users to set reminders for themselves
        """

        if _delta is None:
            now = datetime.datetime.utcnow()
            _delta = on - now
        else:
            now = on
            on = now + _delta

        if on < now:
            raise BotException(
                "Failed to set reminder!",
                "Time cannot go backwards, negative time does not make sense..."
                "\n Or does it? \\*vsauce music plays in the background\\*",
            )

        elif _delta <= datetime.timedelta(seconds=10):
            raise BotException(
                "Failed to set reminder!",
                "Why do you want me to set a reminder for such a small duration?\n"
                "Pretty sure you can remember that one yourself :wink:",
            )

        # remove microsecond precision of the 'on' variable
        on -= datetime.timedelta(microseconds=on.microsecond)

        async with db.DiscordDB("reminders") as db_obj:
            db_data = db_obj.get({})
            if self.author.id not in db_data:
                db_data[self.author.id] = {}

            # user is editing old reminder message, discard the old reminder
            for key, (_, chan_id,
                      msg_id) in tuple(db_data[self.author.id].items()):
                if chan_id == self.channel.id and msg_id == self.invoke_msg.id:
                    db_data[self.author.id].pop(key)

            limit = 25 if self.is_priv else 10
            if len(db_data[self.author.id]) >= limit:
                raise BotException(
                    "Failed to set reminder!",
                    f"I cannot set more than {limit} reminders for you",
                )

            db_data[self.author.id][on] = (
                msg.string.strip(),
                self.channel.id,
                self.invoke_msg.id,
            )
            db_obj.write(db_data)

        await embed_utils.replace(
            self.response_msg,
            title="Reminder set!",
            description=
            (f"Gonna remind {self.author.name} in {utils.format_timedelta(_delta)}\n"
             f"And that is on {utils.format_datetime(on)}"),
        )
Exemple #14
0
    async def cmd_sudo_swap(
        self,
        msg_a: discord.Message,
        msg_b: discord.Message,
        embeds: bool = True,
    ):
        """
        ->type More admin commands
        ->signature pg!sudo swap <message> <message>
        ->description Swap message contents and embeds between messages through the bot
        ->extended description
        Swap message contents and embeds between the two given messages.

        __Args__:
            `message_a: (Message)`
            > A discord message whose embed
            > should be swapped with that of `message_b`.

            `message_b: (Message)`
            > Another discord message whose embed
            > should be swapped with that of `message_a`.

            `embeds: (bool) = True`
            > If set to `True`, the first embeds will also
            > (when present) be swapped between the given messages.

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

        ->example command pg!sudo swap 123456789123456789 69696969969669420
        -----
        """

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

        if (
            not msg_a.content
            and not msg_a.embeds
            or not msg_b.content
            and not msg_b.embeds
        ):
            raise BotException(
                "Cannot execute command:",
                "Not enough data found in one or more of the given messages.",
            )

        elif common.bot.user.id not in (msg_a.author.id, msg_b.author.id):
            raise BotException(
                "Cannot execute command:",
                f"Both messages must have been authored by me, {common.bot.user.mention}.",
            )

        msg_embed_a = msg_a.embeds[0] if msg_a.embeds else None
        msg_embed_b = msg_b.embeds[0] if msg_b.embeds else None

        msg_content_a = msg_a.content
        msg_content_b = msg_b.content

        if embeds:
            await msg_a.edit(content=msg_content_b, embed=msg_embed_b)
            await msg_b.edit(content=msg_content_a, embed=msg_embed_a)
        else:
            await msg_a.edit(content=msg_content_b)
            await msg_b.edit(content=msg_content_a)

        try:
            await self.response_msg.delete()
        except discord.NotFound:
            pass
Exemple #15
0
    async def cmd_reminders_set(
        self,
        msg: String,
        timestr: Union[String, str] = "",
        weeks: int = 0,
        days: int = 0,
        hours: int = 0,
        minutes: int = 0,
        seconds: int = 0,
    ):
        """
        ->type Reminders
        ->signature pg!reminders set <message> [time string] [weeks] [days] [hours] [minutes] [seconds]
        ->description Set a reminder to yourself
        ->extended description
        There are two ways you can pass the time duration, one is via a "time string"
        and the other is via keyword arguments
        `weeks`, `days`, `hours`, `minutes` and `seconds` are optional arguments you can
        specify to describe the time duration you want to set the reminder for
        ->example command pg!reminders set "Become pygame expert" weeks=9 days=12 hours=23 minutes=16 seconds=35
        -----
        Implement pg!reminders_set, for users to set reminders for themselves
        """
        timestr = (timestr.string.strip()
                   if isinstance(timestr, String) else timestr.strip())

        if timestr:
            time_formats = {
                "w": 7 * 24 * 60 * 60,
                "d": 24 * 60 * 60,
                "h": 60 * 60,
                "m": 60,
                "s": 1,
            }
            sec = 0

            for time_format, dt in time_formats.items():
                try:
                    results = re.search(rf"\d+{time_format}", timestr).group()
                    parsed_time = int(results.replace(time_format, ""))
                    sec += parsed_time * dt
                except AttributeError:
                    pass

            if "mo" in timestr:
                month_results = re.search(r"\d+mo", timestr).group()
                parsed_month_time = int(month_results.replace("mo", ""))
                sec += (self.invoke_msg.created_at.replace(
                    month=self.invoke_msg.created_at.month + parsed_month_time)
                        - self.invoke_msg.created_at).total_seconds()

            if sec == 0:
                raise BotException(
                    "Failed to set reminder!",
                    "There is something wrong with your time parameter.\n"
                    "Please check that it is correct and try again",
                )

            delta = datetime.timedelta(seconds=sec)
        else:
            delta = datetime.timedelta(
                weeks=weeks,
                days=days,
                hours=hours,
                minutes=minutes,
                seconds=seconds,
            )

        await self.cmd_reminders_add(msg,
                                     datetime.datetime.utcnow(),
                                     _delta=delta)
Exemple #16
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 #17
0
    async def cmd_sudo_fetch(
        self,
        origin: discord.TextChannel,
        quantity: int,
        channel_ids: bool = False,
        urls: bool = False,
        pinned: bool = False,
        pin_range: Optional[range] = None,
        before: Optional[Union[discord.PartialMessage, datetime.datetime]] = None,
        after: Optional[Union[discord.PartialMessage, datetime.datetime]] = None,
        around: Optional[Union[discord.PartialMessage, datetime.datetime]] = None,
        oldest_first: bool = True,
        prefix: String = String(""),
        sep: String = String(" "),
        suffix: String = String(""),
    ):
        """
        ->type More admin commands
        ->signature pg!sudo fetch <origin channel> <quantity> [urls=False] [pinned=False] [pin_range=]
        [before=None] [after=None] [around=None] [oldest_first=True] [prefix=""] [sep=" "] [suffix=""]
        ->description Fetch message IDs or URLs
        -----
        """

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

        prefix, sep, suffix = prefix.string, sep.string, suffix.string
        output_str = prefix
        destination = self.channel

        if pinned:
            messages = await origin.pins()
            if not messages:
                raise BotException(
                    "No pinned messages found",
                    "No pinned messages were found in the specified channel.",
                )

            if oldest_first:
                messages.reverse()

            if quantity > 0:
                messages = messages[: quantity + 1]
                if oldest_first:
                    messages.reverse()

            elif quantity == 0:
                if pin_range:
                    messages = messages[
                        pin_range.start : pin_range.stop : pin_range.step
                    ]

                    if pin_range.step != -1 and oldest_first:
                        messages.reverse()

            elif quantity < 0:
                raise BotException(
                    "Invalid `quantity` argument",
                    "Quantity has to be a positive integer (`=> 0`).",
                )

        else:
            if (
                isinstance(before, discord.PartialMessage)
                and before.channel.id != origin.id
            ):
                raise BotException(
                    "Invalid `before` argument",
                    "`before` has to be an ID to a message from the origin channel",
                )

            if (
                isinstance(after, discord.PartialMessage)
                and after.channel.id != origin.id
            ):
                raise BotException(
                    "Invalid `after` argument",
                    "`after` has to be an ID to a message from the origin channel",
                )

            if (
                isinstance(around, discord.PartialMessage)
                and around.channel.id != origin.id
            ):
                raise BotException(
                    "Invalid `around` argument",
                    "`around` has to be an ID to a message from the origin channel",
                )

            if quantity <= 0:
                if quantity == 0 and not after:
                    raise BotException(
                        "Invalid `quantity` argument",
                        "`quantity` must be above 0 when `after=` is not specified.",
                    )
                elif quantity != 0:
                    raise BotException(
                        "Invalid `quantity` argument",
                        "Quantity has to be a positive integer (or `0` when `after=` is specified).",
                    )

            await destination.trigger_typing()
            messages = await origin.history(
                limit=quantity if quantity != 0 else None,
                before=before,
                after=after,
                around=around,
            ).flatten()

            if not messages:
                raise BotException(
                    "Invalid message/time range",
                    "No messages were found for the specified input values.",
                )

            if not after and oldest_first:
                messages.reverse()

        msg_count = len(messages)
        msgs_per_loop = 200

        if urls:
            output_filename = "message_urls.txt"
            start_idx = 0
            end_idx = 0
            for i in range(msg_count // msgs_per_loop):
                start_idx = msgs_per_loop * i
                end_idx = start_idx + msgs_per_loop - 1
                output_str += sep.join(
                    messages[j].jump_url
                    for j in range(start_idx, start_idx + msgs_per_loop)
                )
                await asyncio.sleep(0)

            output_str += (
                sep.join(messages[j].jump_url for j in range(end_idx + 1, msg_count))
                + suffix
            )

        elif channel_ids:
            output_filename = "message_and_channel_ids.txt"
            start_idx = 0
            end_idx = 0
            for i in range(msg_count // msgs_per_loop):
                start_idx = msgs_per_loop * i
                end_idx = start_idx + msgs_per_loop - 1
                output_str += sep.join(
                    f"{messages[j].channel.id}/{messages[j].id}"
                    for j in range(start_idx, start_idx + msgs_per_loop)
                )
                await asyncio.sleep(0)

            output_str += (
                sep.join(
                    f"{messages[j].channel.id}/{messages[j].id}"
                    for j in range(end_idx + 1, msg_count)
                )
                + suffix
            )

        else:
            output_filename = "message_and_channel_ids.txt"
            start_idx = 0
            end_idx = 0
            for i in range(msg_count // msgs_per_loop):
                start_idx = msgs_per_loop * i
                end_idx = start_idx + msgs_per_loop - 1
                output_str += sep.join(
                    f"{messages[j].id}"
                    for j in range(start_idx, start_idx + msgs_per_loop)
                )
                await asyncio.sleep(0)

            output_str += (
                sep.join(f"{messages[j].id}" for j in range(end_idx + 1, msg_count))
                + suffix
            )

        with io.StringIO(output_str) as fobj:
            await destination.send(file=discord.File(fobj, filename=output_filename))
        try:
            await self.response_msg.delete()
        except discord.NotFound:
            pass
Exemple #18
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