Exemple #1
0
 async def cleanup_users(self, ctx, days: int = 1):
     """Cleanup inactive server members"""
     if days > 30:
         await ctx.send(
             chat.info(
                 _("Due to Discord Restrictions, you cannot use more than 30 days for that cmd."
                   )))
         days = 30
     elif days <= 0:
         await ctx.send(chat.info(_('"days" arg cannot be less than 1...')))
         days = 1
     to_kick = await ctx.guild.estimate_pruned_members(days=days)
     await ctx.send(
         chat.warning(
             _("You about to kick **{to_kick}** inactive for **{days}** days members from this server. "
               'Are you sure?\nTo agree, type "yes"').format(
                   to_kick=to_kick, days=days)))
     pred = MessagePredicate.yes_or_no(ctx)
     try:
         await self.bot.wait_for("message", check=pred, timeout=30)
     except AsyncTimeoutError:
         pass
     if pred.result:
         cleanup = await ctx.guild.prune_members(days=days,
                                                 reason=get_audit_reason(
                                                     ctx.author))
         await ctx.send(
             chat.info(
                 _("**{removed}**/**{all}** inactive members removed.\n"
                   "(They was inactive for **{days}** days)").format(
                       removed=cleanup, all=to_kick, days=days)))
     else:
         await ctx.send(chat.error(_("Inactive members cleanup canceled.")))
Exemple #2
0
 async def global_api(self,
                      ctx: commands.Context,
                      service: str,
                      api_key: str = None):
     """Get information on setting an API key for a global service."""
     if api_key:
         # Try deleting the command as fast as possible, so that others can't see the API key
         await delete(ctx.message)
     if service in self.supported_guild_services:
         await ctx.send(
             info(
                 "{} is not a global service, and should be set up per guild "
                 .format(self.get_nice_service_name(service)) +
                 "using the command:\n\n" +
                 "`[p]bancheckset service api {} <your_api_key_here>`".
                 format(service)))
         return
     if service not in self.supported_global_services:
         await ctx.send(
             error("{} is not a valid service name.".format(
                 self.get_nice_service_name(service))))
         return
     await ctx.send(
         info(
             "Global API keys are no longer set here. You should run this command instead:\n\n"
             +
             "`[p]set api {} api_key <your_api_key_here>`".format(service)))
Exemple #3
0
    async def request_channel(self, ctx, channel: discord.TextChannel = None):
        """Where `[p]request list` commands say to use the `[p]request` command. Use the command without a channel argument to set to no channel."""
        if channel is None:
            channel_id = 0
        elif channel.guild != ctx.guild:
            await self._raw_send_no_mention(
                ctx, error("Channel not found on this server."))
            return
        else:
            channel_id = channel.id

        await self.config.guild(ctx.guild).request_channel.set(channel_id)
        if await self.config.guild(ctx.guild).auto_post_list():
            await self._auto_post_list(ctx)
        if channel_id != 0:
            await self._raw_send_no_mention(
                ctx,
                info(
                    "The `{p}request list` response now suggests using the `{p}request` commands in {channel}."
                    .format(p=ctx.prefix, channel=channel.mention)))
        else:
            await self._raw_send_no_mention(
                ctx,
                info(
                    "The `{p}request list` response no longer suggests a channel to use `{p}request` commands in."
                    .format(p=ctx.prefix)))
Exemple #4
0
 async def list(self, ctx):
     """List terms from the auto-ban list."""
     async with self.config.guild(ctx.guild).terms() as terms:
         if len(terms) == 0:
             await ctx.send(info("No terms set on this server."))
         else:
             ls = humanize_list(["`{}`".format(term) for term in terms])
             await ctx.send(info("Terms on this server:\n{}".format(ls)))
Exemple #5
0
    async def auto_post_list(self, ctx, value: bool = None):
        """Whether to automatically update existing post_list posts when roles or counts change.

        For value, pass in "true" or "false".
        Omit the value to toggle."""
        if value is None:
            value = not await self.config.guild(ctx.guild).auto_post_list()

        await self.config.guild(ctx.guild).auto_post_list.set(value)
        if value:
            await ctx.send(info("Will automatically update post_list posts."))
        else:
            await ctx.send(
                info("Will not automatically update post_list posts."))
    async def request_channel(self, ctx, channel : discord.TextChannel = None):
        """Where `[p]request list` commands say to use the `[p]request` command."""
        if channel is None:
            channel_id = 0
        elif channel.guild != ctx.guild:
            await ctx.send(error("Channel not found on this server."))
            return
        else:
            channel_id = channel.id

        await self.config.guild(ctx.guild).request_channel.set(channel_id)
        if channel_id != 0:
            await ctx.send(info("Set where to suggest using the `[p]request` commands in `[p]request list` to {}.".format(channel.mention)))
        else:
            await ctx.send(info("Set the `[p]request list` to not suggest a channel."))
Exemple #7
0
    async def auto_react_match_text(self, ctx, match_text: str = ""):
        """Text to check for to automatically react, to allow users to one-click decode."""
        if "@" in match_text:
            await ctx.send(error("You cannot use that match text."))
            return

        await self.config.guild(ctx.guild).auto_react_to.set(match_text)
        if match_text:
            await ctx.send(
                info(
                    "If new user messages on this server match `{}`, I will add a reaction automatically."
                    .format(match_text)))
        else:
            await ctx.send(
                info("Disabled automatically reacting on this erver."))
Exemple #8
0
    async def captcha_type_setter(self, ctx: commands.Context,
                                  captcha_type: str):
        """
        Change the type of Captcha challenge.

        You choose the following type of Captcha:
        - image: A normal captcha image, hardest to understand.
        - wheezy: A less complex captcha image, easier than image method.
        - plain: Plain text captcha. Easiest to read, cannot be copy/pasted.
        """

        available = ("wheezy", "image", "plain")
        captcha_type = captcha_type.lower()
        if captcha_type not in available:
            await ctx.send_help()
            await ctx.send(
                form.error(
                    form.bold(
                        "{type} is not a valid Captcha type option.".format(
                            type=form.bordered(captcha_type)))))
            return

        await self.data.guild(ctx.guild).type.set(captcha_type)
        await ctx.send(
            form.info(
                "Captcha type registered: {type}".format(type=captcha_type)))
Exemple #9
0
 async def other(self, ctx: commands.Context):
     """Learn how to modify default bitrate and user limits."""
     await ctx.send(
         info(
             "Default bitrate and user limit settings are now copied from the AutoRoom Source."
         )
     )
 async def mr_list(self, ctx):
     """Assigned roles list"""
     members_data = await self.config.all_members(ctx.guild)
     if not members_data:
         await ctx.send(
             chat.info(
                 _("There is no assigned personal roles on this server")))
         return
     assigned_roles = []
     for member, data in members_data.items():
         if not data["role"]:
             continue
         dic = {
             _("User"):
             ctx.guild.get_member(member)
             or f"[X] {await self.bot.fetch_user(member)}",
             _("Role"):
             await self.smart_truncate(
                 ctx.guild.get_role(data["role"])
                 or "[X] {}".format(data["role"])),
         }
         assigned_roles.append(dic)
     pages = list(
         chat.pagify(
             tabulate(assigned_roles, headers="keys", tablefmt="orgtbl")))
     pages = [chat.box(page) for page in pages]
     await menu(ctx, pages, DEFAULT_CONTROLS)
Exemple #11
0
    async def cogwhitelist_reset(self, ctx: commands.Context):
        """Reset whitelisted cog settings"""
        cogs = len(await self.config.cogs())
        if not cogs:
            return await fmt(
                ctx,
                info(
                    _(
                        "No cogs are currently setup to require a whitelist to use, and as such "
                        "you cannot reset any cog whitelist settings."
                    )
                ),
            )

        warn_str = warning(
            _(
                "Are you sure you want to reset your cog whitelist settings?\n"
                "This action will make {cogs} cog(s) usable by any server.\n\n"
                "Unless you have a time machine, **this action cannot be undone.**"
            )
        )

        if await ConfirmMenu(ctx=ctx, content=warn_str.format(cogs=cogs)).prompt():
            await self.config.cogs.set({})
            await ctx.tick()
        else:
            await ctx.send(_("Ok then."))
Exemple #12
0
    async def logging_channel(self, ctx: commands.Context, *,
                              destination: Union[discord.TextChannel, str]):
        """Set a channel where events are registered.

        The following actions will be registered:
        - User join and must answer the captcha.
        - User passed captcha/failed captcha.
        - User got kicked.

        ``destination``: Text channel.
        You can also use "None" if you wish to remove the logging channel.
        """

        if not isinstance(destination, discord.TextChannel):
            if destination.lower() == "none":
                await self.data.guild(ctx.guild).logschannel.clear()
                await ctx.send(form.info("Logging channel removed."))
            else:
                await ctx.send(form.error("Invalid destination."))
            return

        if needperm := await check_permissions_in_channel(
            [
                "read_messages",
                "read_message_history",
                "send_messages",
                "attach_files",
            ],
                destination,
        ):
            await ctx.send(
                embed=await build_embed_with_missing_permissions(needperm))
            return
Exemple #13
0
    async def delsfx(self, ctx, soundname: str):
        """Deletes an existing sound."""

        # await self.bot.type()

        server = ctx.message.guild
        serverid = str(server.id)

        if serverid not in os.listdir(self.sound_base):
            os.makedirs(os.path.join(self.sound_base, serverid))

        f = glob.glob(os.path.join(self.sound_base, serverid,
                                   soundname + ".*"))
        if len(f) < 1:
            await ctx.send(cf.error(
                "Sound file not found. Try `{}allsfx` for a list.".format(
                    ctx.prefix)))
            return
        elif len(f) > 1:
            await ctx.send(cf.error(
                "There are {} sound files with the same name, but different"
                " extensions, and I can't deal with it. Please make filenames"
                " (excluding extensions) unique.".format(len(f))))
            return

        os.remove(f[0])

        if soundname in self.settings[serverid]:
            del self.settings[serverid][soundname]
            await self.save_json(self.settings_path, self.settings)

        await ctx.send(cf.info("Sound {} deleted.".format(soundname)))
Exemple #14
0
    async def clear(self, ctx, channel: discord.TextChannel = None):
        """Clear the channel topic.

        If the channel is omitted, the current channel is assumed."""
        if channel is None:
            # Did not provide channel; get current channel
            channel = ctx.channel
        if channel.guild != ctx.guild:
            await ctx.send(content=error("I cannot do that."))
            return
        if isinstance(channel, discord.TextChannel):
            # This not is a DM or group DM
            # async with ctx.typing():
            reason = "Topic edit [clear] by request of {author} ({id})" \
                .format(author=ctx.author, id=ctx.author.id)
            try:
                await channel.edit(topic="", reason=reason)
            except (discord.Forbidden, discord.HTTPException):
                await ctx.send(content=error(
                    "I don't have permission to edit this topic!"))
                return

            await ctx.send(content=info(
                "Channel {channel}'s topic has been cleared.".format(
                    channel=channel.mention)))
        else:
            # Private location: only reply with text
            await ctx.send(
                content=error("This channel type cannot have a topic."))
Exemple #15
0
    async def global_api(
        self, ctx: commands.Context, service: str, api_key: str = None
    ):
        """Set (or delete) an API key for a global service.

        Behind the scenes, this is the same as `[p]set api <service> api_key <your_api_key_here>`
        """
        if api_key:
            # Try deleting the command as fast as possible, so that others can't see the API key
            await delete(ctx.message)
        if service in self.supported_guild_services:
            await ctx.send(
                info(
                    f"{self.get_nice_service_name(service)} is not a global service, "
                    "and should be set up per server using the command:\n\n"
                    f"`[p]bancheckset service api {service} <your_api_key_here>`"
                )
            )
            return
        if service not in self.supported_global_services:
            await ctx.send(
                error(
                    f"{self.get_nice_service_name(service)} is not a valid service name."
                )
            )
            return
        action = "set"
        if api_key:
            await ctx.bot.set_shared_api_tokens(service, api_key=api_key)
        else:
            await ctx.bot.remove_shared_api_tokens(service, "api_key")
            action = "removed"
        response = f"API key for the {self.get_nice_service_name(service)} BanCheck service has been {action}."
        await ctx.send(checkmark(response))
Exemple #16
0
 async def disable_autocheck(self, ctx: commands.Context):
     """Disable automatically checking new users against ban lists."""
     if await self.config.guild(ctx.guild).notify_channel() is None:
         await ctx.send(info("AutoCheck is already disabled."))
     else:
         await self.config.guild(ctx.guild).notify_channel.set(None)
         await ctx.send(checkmark("AutoCheck is now disabled."))
Exemple #17
0
    async def postlist(self, ctx, channel: discord.TextChannel):
        """Lists the roles that can be requested and posts them permanently to the specified channel."""
        msg = await self._get_role_list_message(ctx)

        if channel is None:
            channel = ctx.channel

        if channel.guild.id != ctx.guild.id:
            await self._raw_send_no_mention(
                ctx, error("That channel is not on this server."))
            return

        post_id = await self.config.channel(channel).role_info_post()
        result = await self._post_list(channel, post_id, msg)

        upd_resp = "Manually update it later with `{p}request postlist #{channel}`."
        if result is None:
            resp = "No update needed."
        elif result:
            resp = "Message updated."
        else:
            resp = "Message posted."

        await self._raw_send_no_mention(
            ctx,
            info("{} {}".format(resp, upd_resp).format(p=ctx.prefix,
                                                       channel=channel)))
Exemple #18
0
 async def mr_list(self, ctx):
     """Assigned roles list"""
     members_data = await self.config.all_members(ctx.guild)
     assigned_roles = []
     for member, data in members_data.items():
         if not data["role"]:
             continue
         dic = {
             _("User"):
             ctx.guild.get_member(member) or f"[X] {member}",
             _("Role"):
             shorten(
                 str(
                     ctx.guild.get_role(data["role"])
                     or "[X] {}".format(data["role"])),
                 32,
                 placeholder="…",
             ),
         }
         assigned_roles.append(dic)
     pages = list(
         chat.pagify(
             tabulate(assigned_roles, headers="keys", tablefmt="orgtbl")))
     pages = [chat.box(page) for page in pages]
     if pages:
         await menu(ctx, pages, DEFAULT_CONTROLS)
     else:
         await ctx.send(
             chat.info(
                 _("There is no assigned personal roles on this server")))
Exemple #19
0
    async def addTag(self, ctx: Context, user: discord.User, *,
                     description: str):
        """Add a description to a user.

        Parameters:
        -----------
        user: discord.User
            The user to add a description to.
        description: str
            The description to add.
        """
        userId = str(user.id)
        if len(description) > MAX_DESCRIPTION_LENGTH:
            await ctx.send(
                "The description is too long! "
                f"Max length is {MAX_DESCRIPTION_LENGTH} characters.")
            return

        async with self.config.guild(
                ctx.guild).get_attr(KEY_DESCRIPTIONS)() as descDict:
            descDict[userId] = description

        await ctx.send(
            info(f"Description set for {user.mention}."),
            allowed_mentions=discord.AllowedMentions.none(),
        )
        LOGGER.info(
            "A welcome description has been added for %s#%s (%s)",
            user.name,
            user.discriminator,
            user.id,
        )
        LOGGER.debug(description)
Exemple #20
0
    async def getTag(self, ctx: Context, user: discord.User):
        """Get a description for a user.

        Parameters:
        -----------
        user: discord.User
            The user to get a description for.
        """
        userId = str(user.id)
        descDict: dict = await self.config.guild(ctx.guild
                                                 ).get_attr(KEY_DESCRIPTIONS)()
        if userId in descDict:
            description = descDict[userId]
            if description:
                descText = "\n".join(
                    [f"**{user.mention}:**",
                     box(description)])
                embed = discord.Embed(
                    title=f"Description for {user.name}#{user.discriminator}",
                    description=descText)
                await ctx.send(embed=embed,
                               allowed_mentions=discord.AllowedMentions.none())
                return

        await ctx.send(info("No description found for that user."))
Exemple #21
0
 async def server(self, ctx: commands.Context):
     """Ignore/Unignore the current server."""
     await ctx.send(
         info(
             "Use the `[p]command enablecog` and `[p]command disablecog` to enable or disable this cog."
         )
     )
Exemple #22
0
 async def globalstats(self, ctx, *, date : str = None):
     """
     Command to check global Coronavirus stats
     date : it can be almost any kind of date format for the required date
     Example : 
     [p]globalstats "3 March 2020" or [p]globalstats 3/3/2020 or [p]globalstats "2020 3 March"
     """
     global_data = await data_client.get_global_stats()
     response_embed = discord.Embed(title="Global Coronavirus Stats", color=discord.Color.red())
     current_day_data = {}
     if not date:
         current_day_data = global_data[0]
     else:
         try:
             required_date = date_parser.parse(date)
         except date_parser.ParserError:
             return await ctx.send(chat_formatter.error("The specified date is invalid."))
         current_day_data = [day_data for day_data in global_data if date_parser.parse(day_data['date']).date() == required_date.date()]
     if not current_day_data:
         response_embed.add_field(name="Overall stats", value=chat_formatter.info("No data found for the current day."), inline=True)
         return await ctx.send(embed=response_embed)
     response_embed.add_field(name="Overall stats", value="", inline=True)
     self.add_stats_field(response_embed, 0, current_day_data, "confirmed", "Total cases", "new_confirmed")
     self.add_stats_field(response_embed, 0, current_day_data, "deaths", "Total deaths", "new_deaths")
     self.add_stats_field(response_embed, 0, current_day_data, "recovered", "Total recovered", "new_recovered")
     graph = Graph(ctx.bot, "Global Coronavirus(Sars-Cov2) stats", "Date", "Cases", global_data[::-1], "seaborn")
     await graph.plot("date", "confirmed", "b", "Confirmed cases")
     await graph.plot("date", "deaths", "r", "Deaths")
     await graph.plot("date", "recovered", "g", "Recovered cases")
     await graph.save()
     img_file = discord.File(graph.file_obj, filename='globalstats.png')
     response_embed.set_image(url='attachment://globalstats.png')
     await ctx.send(embed=response_embed, file=img_file)
Exemple #23
0
    async def set(self, ctx, channel: discord.TextChannel, *, topic: str):
        """Set the channel topic to the given value.

        Any custom emojis, mentions, links, and other formatting are
        supported.
        Note that mentions (including @here and @everyone) in your own
        message will ping, so best to set the channel from another
        channel (or the server's settings) if those are needed."""
        # if isinstance(channel, str):
        #     # Did not provide channel; get current channel
        #     topic = "{} {}".format(channel, topic)
        #     channel = ctx.channel
        if channel.guild != ctx.guild:
            await ctx.send(content=error("I cannot do that."))
            return
        if isinstance(channel, discord.TextChannel):
            # This not is a DM or group DM
            # async with ctx.typing():
            reason = "Topic edit by request of {author} ({id})" \
                    .format(author=ctx.author, id=ctx.author.id)
            try:
                await channel.edit(topic=topic, reason=reason)
            except (discord.Forbidden, discord.HTTPException):
                await ctx.send(content=error(
                    "I don't have permission to edit this topic!"))
                return

            await ctx.send(content=info(
                "Channel {channel}'s topic has been updated.".format(
                    channel=channel.mention)))
        else:
            # Private location: only reply with text
            await ctx.send(
                content=error("This channel type cannot have a topic."))
Exemple #24
0
    async def addsfx(self, ctx, link: str=None):
        """Adds a new sound.

        Either upload the file as a Discord attachment and make your comment
        "[p]addsfx", or use "[p]addsfx direct-URL-to-file".
        """

        # await self.bot.type()

        server = ctx.message.guild
        serverid = str(server.id)

        if serverid not in os.listdir(self.sound_base):
            os.makedirs(os.path.join(self.sound_base, serverid))

        if server.id not in self.settings:
            self.settings[server.id] = {}
            await self.save_json(self.settings_path, self.settings)

        attach = ctx.message.attachments
        if len(attach) > 1 or (attach and link):
            await ctx.send(
                cf.error("Please only add one sound at a time."))
            return

        url = ""
        filename = ""
        if attach:
            a = attach[0]
            url = a.url
            filename = a.filename
        elif link:
            url = "".join(link)
            filename = os.path.basename(
                "_".join(url.split()).replace("%20", "_"))
        else:
            await ctx.send(
                cf.error("You must provide either a Discord attachment or a"
                         " direct link to a sound."))
            return

        filepath = os.path.join(self.sound_base, serverid, filename)

        if os.path.splitext(filename)[0] in self._list_sounds(serverid):
            await ctx.send(
                cf.error("A sound with that filename already exists."
                         " Please change the filename and try again."))
            return

        async with self.session.get(url) as new_sound:
            f = open(filepath, "wb")
            f.write(await new_sound.read())
            f.close()

        self.settings[server.id][
            os.path.splitext(filename)[0]] = {"volume": self.default_volume}
        await self.save_json(self.settings_path, self.settings)

        await ctx.send(
            cf.info("Sound {} added.".format(os.path.splitext(filename)[0])))
    async def max_requestable(self, ctx, count : int):
        """Maximum number of roles that users can request."""
        if count < 0:
            await ctx.send(error("Maximum must not be negative."))

        await self.config.guild(ctx.guild).max_requestable.set(count)
        await ctx.send(info("Set maximum number of requestable roles per user to {}.".format(count)))
 async def saucenao(self, ctx, image: ImageFinder = None):
     """Reverse search image via SauceNAO"""
     if image is None:
         try:
             image = await ImageFinder().search_for_images(ctx)
         except ValueError as e:
             await ctx.send(e)
             return
     image = image[0]
     try:
         search = await SauceNAO.from_image(ctx, image)
     except ValueError as e:
         await ctx.send(e)
         return
     if not search.results:
         await ctx.send(_("Nothing found"))
         return
     self.saucenao_limits["short"] = search.limits.short
     self.saucenao_limits["long"] = search.limits.long
     self.saucenao_limits["long_remaining"] = search.limits.remaining.long
     self.saucenao_limits["short_remaining"] = search.limits.remaining.short
     embeds = []
     page = 0
     for entry in search.results:
         page += 1
         try:
             url = entry.urls[0]
         except IndexError:
             url = None
         # design is shit, ideas?
         e = discord.Embed(
             title=entry.source or entry.title or entry.service,
             description="\n".join([
                 _("Similarity: {}%").format(entry.similarity),
                 "\n".join(
                     [n
                      for n in [entry.eng_name, entry.jp_name] if n]) or "",
                 entry.part and _("Part/Episode: {}").format(entry.part)
                 or "",
                 entry.year and _("Year: {}").format(entry.year) or "",
                 entry.est_time
                 and _("Est. Time: {}").format(entry.est_time) or "",
             ]),
             url=url,
             color=await ctx.embed_colour(),
             timestamp=entry.created_at or discord.Embed.Empty,
         )
         e.set_footer(
             text=_("Via SauceNAO • Page {}/{}").format(
                 page, search.results_returned),
             icon_url=
             "https://www.google.com/s2/favicons?domain=saucenao.com",
         )
         e.set_thumbnail(url=entry.thumbnail)
         embeds.append(e)
     if embeds:
         await menu(ctx, embeds, DEFAULT_CONTROLS)
     else:
         await ctx.send(chat.info(_("Nothing found")))
Exemple #27
0
    async def tracemoe(self, ctx, image: ImageFinder = None):
        """Reverse search image via WAIT

        If search performed not in NSFW channel, NSFW results will be not shown"""
        if image is None:
            try:
                image = await ImageFinder().search_for_images(ctx)
            except ValueError as e:
                await ctx.send(e)
                return
        image = image[0]
        try:
            search = await TraceMoe.from_image(ctx, image)
        except ValueError as e:
            await ctx.send(e)
            return
        embeds = []
        page = 0
        for doc in search.docs:
            page += 1
            if getattr(ctx.channel, "nsfw", True) and doc.is_adult:
                continue
            # NOTE: Probably someone will come up with better embed design, but not me
            e = discord.Embed(
                title=doc.title,
                description="\n".join(
                    s
                    for s in [
                        _("Similarity: {:.2f}%").format(doc.similarity * 100),
                        doc.title_native
                        and "🇯🇵 " + _("Native title: {}").format(doc.title_native),
                        doc.title_romaji
                        and "🇯🇵 " + _("Romaji transcription: {}").format(doc.title_romaji),
                        doc.title_chinese
                        and "🇨🇳 " + _("Chinese title: {}").format(doc.title_chinese),
                        doc.title_english
                        and "🇺🇸 " + _("English title: {}").format(doc.title_english),
                        _("Est. Time: {}").format(doc.time_str),
                        _("Episode: {}").format(doc.episode),
                        doc.synonyms and _("Also known as: {}").format(", ".join(doc.synonyms)),
                    ]
                    if s
                ),
                url=doc.mal_id
                and f"https://myanimelist.net/anime/{doc.mal_id}"
                or f"https://anilist.co/anime/{doc.anilist_id}",
                color=await ctx.embed_color(),
            )
            e.set_thumbnail(url=doc.thumbnail)
            e.set_footer(
                text=_("Via WAIT (trace.moe) • Page {}/{}").format(page, len(search.docs)),
                icon_url="https://trace.moe/favicon128.png",
            )
            embeds.append(e)
        if embeds:
            ctx.search_docs = search.docs
            await menu(ctx, embeds, TRACEMOE_MENU_CONTROLS)
        else:
            await ctx.send(chat.info(_("Nothing found")))
 async def mess_bulk(self, ctx):
     """Toggle saving of bulk message deletion"""
     save_bulk = self.config.guild(ctx.guild).save_bulk
     await save_bulk.set(not await save_bulk())
     state = _("enabled") if await self.config.guild(
         ctx.guild).save_bulk() else _("disabled")
     await ctx.send(
         chat.info(_("Bulk message removal saving {}").format(state)))
 async def mess_edit(self, ctx):
     """Toggle logging of message editing"""
     editing = self.config.guild(ctx.guild).editing
     await editing.set(not await editing())
     state = _("enabled") if await self.config.guild(
         ctx.guild).editing() else _("disabled")
     await ctx.send(chat.info(
         _("Message editing logging {}").format(state)))
 async def mess_delete(self, ctx):
     """Toggle logging of message deletion"""
     deletion = self.config.guild(ctx.guild).deletion
     await deletion.set(not await deletion())
     state = _("enabled") if await self.config.guild(
         ctx.guild).deletion() else _("disabled")
     await ctx.send(
         chat.info(_("Message deletion logging {}").format(state)))