コード例 #1
0
class BotStats(vbu.Cog):
    @vbu.command(
        application_command_meta=commands.ApplicationCommandMeta(), )
    @commands.defer()
    @vbu.checks.is_config_set('bot_info', 'enabled')
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def info(self, ctx: vbu.Context):
        """
        Gives you information about the bot, including some important links, such as its invite.
        """

        # Get the info embed
        bot_info = self.bot.config.get("bot_info", {})
        info_embed = vbu.Embed(description=bot_info.get(
            "content",
            "").format(bot=self.bot), ).set_author_to_user(self.bot.user, )

        # See if we have images
        if bot_info.get("thumbnail"):
            info_embed.set_thumbnail(bot_info["thumbnail"])
        if bot_info.get("image"):
            info_embed.set_image(bot_info["image"])

        # Make up our buttons
        links = bot_info.get("links", dict())
        buttons = []
        if (invite_link := self.get_invite_link()):
            buttons.append(
                discord.ui.Button(
                    label="Invite",
                    url=invite_link,
                    style=discord.ui.ButtonStyle.link,
                ))
        for label, info in links.items():
            buttons.append(
                discord.ui.Button(emoji=info.get("emoji") or None,
                                  label=label,
                                  url=info['url'],
                                  style=discord.ui.ButtonStyle.link))
        components = discord.ui.MessageComponents.add_buttons_with_rows(
            *buttons)

        # See if we want to include stats
        embeds = [info_embed]
        if bot_info.get("include_stats"):
            embeds.append(await self.get_stats_embed())

        # And send
        return await ctx.send(embeds=embeds, components=components)
コード例 #2
0
class WolframAlpha(vbu.Cog):
    @commands.command(
        aliases=["wf", "wolframalpha", "wfa"],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="search",
                description="Your query for WolframAlpha.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.defer()
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    @vbu.checks.is_config_set("api_keys", "wolfram")
    async def wolfram(self, ctx: vbu.Context, *, search: str):
        """
        Send a query to WolframAlpha.
        """

        # Build our request
        params = {
            "input": search,
            "appid": self.bot.config['api_keys']['wolfram'],
            "format": "image",
            "output": "json",
        }
        headers = {
            "User-Agent": self.bot.user_agent,
        }

        # Send our request
        async with self.bot.session.get(
            "https://api.wolframalpha.com/v2/query",
            params=params,
            headers=headers) as r:
            data = json.loads(await r.text())

        # Send output
        try:
            pod = data['queryresult']['pods'][1]
            embed = vbu.Embed(
                title=pod['title'],
                use_random_colour=True,
            ).set_image(url=pod['subpods'][0]['img']['src'], )
            return await ctx.send(embed=embed)
        except (KeyError, IndexError):
            return await ctx.send("No results for that query!")
コード例 #3
0
        responses = [
            discord.ApplicationCommandOptionChoice(name=(repo := str(GitRepo(r['host'], r['owner'], r['repo']))), value=repo)
            for r in rows
        ]
        return await interaction.response.send_autocomplete(responses)

    @issue.command(
        name="list",
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="repo",
                    description="The repo that you want to list the issues on.",
                    type=discord.ApplicationCommandOptionType.string,
                ),
                discord.ApplicationCommandOption(
                    name="list_closed",
                    description="Whether or not you want to list closed issues.",
                    type=discord.ApplicationCommandOptionType.boolean,
                    required=False,
                ),
            ],
        ),
    )
    async def issue_list(self, ctx: vbu.Context, repo: GitRepo, list_closed: bool = False):
        """
        List all of the issues on a git repo.
        """

        # Get the database because whatever why not
        async with vbu.Database() as db:
コード例 #4
0
            magic_constant * width * height
        )  # The size modifier to reach the intended size

        return (int(width * size_mod), int(height * size_mod))

    @commands.command(
        aliases=['stealemoji'],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="emoji",
                description="The emoji that you want to add.",
                type=discord.ApplicationCommandOptionType.string,
            ),
            discord.ApplicationCommandOption(
                name="name",
                description="The name of the emoji.",
                type=discord.ApplicationCommandOptionType.string,
                required=False,
            ),
            discord.ApplicationCommandOption(
                name="animated",
                description="Whether or not the emoji is animated.",
                type=discord.ApplicationCommandOptionType.boolean,
                required=False),
        ], ),
    )
    @commands.defer()
    @commands.bot_has_permissions(manage_emojis=True)
    @commands.has_guild_permissions(manage_emojis=True)
    @commands.guild_only()
    async def addemoji(
        self,
コード例 #5
0
ファイル: menu.py プロジェクト: Voxel-Fox-Ltd/VoxelBotUtils
    def create_cog(
            self,
            bot: Bot = None,
            *,
            cog_name: str = "Bot Settings",
            name: str = "settings",
            aliases: typing.List[str] = ["setup"],
            permissions: typing.List[str] = None,
            post_invoke: MaybeCoroContextCallable = None,
            guild_only: bool = True,
            **command_kwargs
            ) -> typing.Union[commands.Cog, typing.Type[commands.Cog]]:
        """
        Creates a cog that can be loaded into the bot in a setup method.

        Parameters
        ----------
        bot : Optional[Bot]
            The bot object. If given, the cog will be instantiated with that object.
        cog_name : Optional[str]
            The name of the cog to be added.
        name : Optional[str]
            The name of the command to be added.
        aliases : Optional[List[str]]
            A list of aliases to be added to the settings command.
        permissions : Optional[List[str]]
            A list of permission names should be required for the command run.
        post_invoke : Optional[MaybeCoroContextCallable]
            A post-invoke method that can be called.
        guild_only : Optional[bool]
            If the command should be guild-only.
        **command_kwargs
            Arguments to be passed down to the command decorator.

        Returns
        -------
        Union[commands.Cog, Type[commands.Cog]]
            Either a cog type to add to your bot, or if a bot instance was passed
            as a parameter, the added cog instance.
        """

        permissions = permissions if permissions is not None else ["manage_guild"]
        meta = commands.ApplicationCommandMeta(guild_only=guild_only)

        class NestedCog(Cog, name=cog_name):

            def __init__(nested_self, bot):
                super().__init__(bot)
                if guild_only:
                    nested_self.settings.add_check(commands.guild_only().predicate)

            def cog_unload(nested_self):
                nested_self.bot.remove_command(nested_self.settings.name)
                super().cog_unload()

            @commands.command(
                cls=Command,
                name=name,
                aliases=aliases,
                application_command_meta=command_kwargs.pop("application_command_meta", meta),
                **command_kwargs,
            )
            @commands.has_permissions(**{i: True for i in permissions})
            @commands.bot_has_permissions(send_messages=True, embed_links=True)
            async def settings(nested_self, ctx):
                """
                Modify some of the bot's settings.
                """

                await self.start(ctx)
                if post_invoke is None:
                    return
                if inspect.iscoroutine(post_invoke):
                    await post_invoke(ctx)
                else:
                    post_invoke(ctx)

        if bot:
            return NestedCog(bot)
        return NestedCog
コード例 #6
0
        codeblock_backtick_count = message.content.count('```')
        if codeblock_backtick_count == 0:
            return await ctx.send("There aren't any codeblocks in that message.")
        elif codeblock_backtick_count % 2 == 1:
            return await ctx.send("The codeblocks in that message are a bit messed up.")
        elif codeblock_backtick_count == 2:
            pass
        else:
            return await ctx.send("I can only unindent messages with one codeblock.")
        block_match = re.search(r"```(.+)\n([\s\S]+)\n```", message.content)
        if not block_match:
            return await ctx.send("I couldn't regex that message for codeblocks.")
        return await ctx.send(f"```{block_match.group(1)}\n{textwrap.dedent(block_match.group(2))}\n```")

    @commands.group(application_command_meta=commands.ApplicationCommandMeta())
    async def rtfm(self, ctx):
        """
        Get some data from the docs.
        """

    @rtfm.command(
        name="djs",
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="obj",
                    description="The object that you want to look up.",
                    type=discord.ApplicationCommandOptionType.string,
                ),
            ],
コード例 #7
0
class MovieCommand(vbu.Cog):
    async def send_omdb_query(self,
                              name: str,
                              media_type: str,
                              search: bool,
                              year=None) -> dict:
        """
        Send a query to the OMDB API, returning the results dict.
        """

        # Build up the params
        key = 's' if search else 't'
        params = {
            'apikey': self.bot.config['api_keys']['omdb'],
            'type': media_type,
            key: name,
        }
        if year:
            params.update({'year': year})
        headers = {"User-Agent": self.bot.user_agent}

        # Send the request
        async with self.bot.session.get("http://www.omdbapi.com/",
                                        params=params,
                                        headers=headers) as r:
            data = await r.json()
        return data

    def generate_embed(self, data) -> typing.Optional[vbu.Embed]:
        """
        Make an embed based on some OMDB data.
        """

        search = data.get('Title') is None
        if search and data.get('Search') is None:
            return None

        embed = vbu.Embed(use_random_colour=True)
        if not search:
            embed.title = f"{data['Title']} ({data['Year']})"

        valid_info = lambda v: v not in [None, 'N/A', 'n/a']

        # List short details of up to 10 results
        if search:
            description_list = []
            for index, row in enumerate(data['Search'][:10], start=1):
                if valid_info(row.get('Poster')):
                    description_list.append(
                        f"{index}. **{row['Title']}** ({row['Year']}) - [Poster]({row['Poster']})"
                    )
                else:
                    description_list.append(
                        f"{index}. **{row['Title']}** ({row['Year']})")
            embed.description = '\n'.join(description_list)
            return embed

        # List full details
        if data.get('Plot'):
            embed.description = data['Plot']
        if data.get('Released'):
            embed.add_field("Release Date", data['Released'])
        if data.get('Rated'):
            embed.add_field("Age Rating", data['Rated'])
        if data.get('Runtime'):
            embed.add_field("Runtime", data['Runtime'])
        if data.get('Genre'):
            embed.add_field(f"Genre{'s' if ',' in data['Genre'] else ''}",
                            data['Genre'])
        if data.get('imdbRating'):
            embed.add_field("IMDB Rating", data['imdbRating'])
        if data.get('Production'):
            embed.add_field(
                f"Production Compan{'ies' if ',' in data['Production'] else 'y'}",
                data['Production'])
        if data.get('Director'):
            embed.add_field(
                f"Director{'s' if ',' in data['Director'] else ''}",
                data['Director'])
        if data.get('Writer'):
            embed.add_field(f"Writer{'s' if ',' in data['Writer'] else ''}",
                            data['Writer'],
                            inline=False)
        if data.get('imdbID'):
            embed.add_field(
                "IMDB Page",
                f"[Direct Link](https://www.imdb.com/title/{data['imdbID']}/) - IMDB ID `{data['imdbID']}`",
                inline=False)
        if valid_info(data.get('Poster')):
            embed.set_thumbnail(data['Poster'])
        return embed

    @commands.group(
        invoke_without_command=True,
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    @vbu.checks.is_config_set('api_keys', 'omdb')
    async def movie(self, ctx: vbu.Context):
        """
        The parent command for movie commands.
        """

        if ctx.invoked_subcommand is None:
            return await ctx.send_help(ctx.command)

    @movie.group(
        name="get",
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="name",
                description="The name of the movie that you want to get.",
                type=discord.ApplicationCommandOptionType.string,
            ),
            discord.ApplicationCommandOption(
                name="year",
                description="The year that the movie was released.",
                type=discord.ApplicationCommandOptionType.integer,
                min_value=1800,
                max_value=2100,
            ),
        ], ),
    )
    @commands.defer()
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    @vbu.checks.is_config_set('api_keys', 'omdb')
    async def movie_get(self,
                        ctx: vbu.Context,
                        *,
                        name: str,
                        year: int = None):
        """
        Gets a movie from the OMDB API.
        """

        # Try and return the found data
        data = await self.send_omdb_query(name, 'movie', False, year)
        embed = self.generate_embed(data)
        if not embed:
            return await ctx.send(
                f"Couldn't find any results for **{name}**",
                allowed_mentions=discord.AllowedMentions.none(),
            )
        return await ctx.send(embed=embed)

    # @movie.group(name="search")
    # @commands.bot_has_permissions(send_messages=True, embed_links=True)
    # @vbu.checks.is_config_set('api_keys', 'omdb')
    # async def movie_search(self, ctx: vbu.Context, *, name: str):
    #     """
    #     Searches for a movie on the OMDB API.
    #     """

    #     # See if we gave a year
    #     original_name = name
    #     if name.split(' ')[-1].isdigit() and int(name.split(' ')[-1]) > 1900:
    #         *name, year = name.split(' ')
    #         name = ' '.join(name)
    #     else:
    #         year = None

    #     # Try and return the found data
    #     await ctx.defer()
    #     data = await self.send_omdb_query(name,'movie',True,year)
    #     embed = self.generate_embed(data)
    #     if not embed:
    #         return await ctx.send(f"No movie results for `{original_name}` could be found.", allowed_mentions=discord.AllowedMentions.none())
    #     return await ctx.send(embed=embed)

    @commands.group(
        invoke_without_command=True,
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    @vbu.checks.is_config_set('api_keys', 'omdb')
    async def tv(self, ctx: vbu.Context):
        """
        The parent group for the TV commands.
        """

        if ctx.invoked_subcommand is None:
            return await ctx.send_help(ctx.command)

    @tv.command(
        name="get",
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="name",
                description="The name of the TV show that you want to get.",
                type=discord.ApplicationCommandOptionType.string,
            ),
            discord.ApplicationCommandOption(
                name="year",
                description="The year that the TV show was released.",
                type=discord.ApplicationCommandOptionType.integer,
                min_value=1800,
                max_value=2100,
            ),
        ], ),
    )
    @commands.defer()
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    @vbu.checks.is_config_set('api_keys', 'omdb')
    async def tv_get(self, ctx: vbu.Context, *, name: str, year: int = None):
        """
        Gets a TV show from the OMDB API.
        """

        # Try and return the found data
        data = await self.send_omdb_query(name, 'series', False, year)
        embed = self.generate_embed(data)
        if not embed:
            return await ctx.send(
                f"Couldn't find any results for **{name}**",
                allowed_mentions=discord.AllowedMentions.none(),
            )
        return await ctx.send(embed=embed)
コード例 #8
0
class ReminderCommands(vbu.Cog):
    def __init__(self, bot):
        super().__init__(bot)
        if bot.database.enabled:
            self.reminder_finish_handler.start()

    def cog_unload(self):
        self.reminder_finish_handler.stop()

    @commands.group(
        aliases=["reminders"],
        invoke_without_command=True,
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def reminder(self, ctx: vbu.Context):
        """
        The parent group for the reminder commands.
        """

        if ctx.invoked_subcommand is not None:
            return
        return await ctx.send_help(ctx.command)

    @reminder.command(
        name="list",
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def reminder_list(self, ctx: vbu.Context):
        """
        Shows you your reminders.
        """

        # Get the guild ID
        try:
            guild_id = ctx.guild.id
        except AttributeError:
            guild_id = 0

        # Grab their remidners
        async with vbu.Database() as db:
            rows = await db(
                "SELECT * FROM reminders WHERE user_id=$1 and guild_id=$2",
                ctx.author.id, guild_id)

        # Format an output string
        reminders = ""
        for reminder in rows:
            expiry = discord.utils.format_dt(reminder['timestamp'])
            reminders += f"\n`{reminder['reminder_id']}` - {reminder['message'][:70]} ({expiry})"
        message = f"Your reminders: {reminders}"

        # Send to the user
        await ctx.send(message or "You have no reminders.",
                       allowed_mentions=discord.AllowedMentions.none())

    @reminder.command(
        name="set",
        aliases=['create'],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="time",
                description=
                "How far into the future you want to set the reminder.",
                type=discord.ApplicationCommandOptionType.string,
            ),
            discord.ApplicationCommandOption(
                name="message",
                description="The message that you want to set.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def reminder_set(self, ctx: vbu.Context, time: vbu.TimeValue, *,
                           message: str):
        """
        Adds a reminder to your account.
        """

        # Grab the guild ID
        try:
            guild_id = ctx.guild.id
        except AttributeError:
            guild_id = 0

        # Get untaken id
        db = await vbu.Database.get_connection()
        while True:
            reminder_id = create_id()
            data = await db("SELECT * FROM reminders WHERE reminder_id=$1",
                            reminder_id)
            if not data:
                break

        # Let them know its been set
        m = await ctx.send(
            f"Reminder set for {discord.utils.format_dt(discord.utils.utcnow() + time.delta)}."
        )

        # Chuck the info in the database
        await db(
            """INSERT INTO reminders (reminder_id, guild_id, channel_id, message_id,
            timestamp, user_id, message)
            VALUES ($1, $2, $3, $4, $5, $6, $7)""",
            reminder_id,
            guild_id,
            ctx.channel.id,
            m.id,
            (discord.utils.utcnow() + time.delta).replace(tzinfo=None),
            ctx.author.id,
            message,
        )
        await db.disconnect()

    @reminder.command(
        name="delete",
        aliases=['remove'],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="reminder_id",
                description="The ID of the reminder that you want to delete.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def reminder_delete(self, ctx: vbu.Context, reminder_id: str):
        """
        Deletes a reminder from your account.
        """

        # Grab the guild ID
        try:
            guild_id = ctx.guild.id
        except AttributeError:
            guild_id = 0

        # Grab the reminder
        async with self.bot.database() as db:
            data = await db(
                "SELECT * FROM reminders WHERE reminder_id=$1 and guild_id=$2",
                reminder_id, guild_id)

            # Check if it exists
            if not data:
                return await ctx.send("That reminder doesn't exist.")

            # Delete it
            await db(
                "DELETE FROM reminders WHERE reminder_id=$1 and user_id=$2",
                reminder_id, ctx.author.id)

        # Send feedback saying it was deleted
        await ctx.send("Reminder deleted.")

    @tasks.loop(seconds=30)
    async def reminder_finish_handler(self):
        """
        Handles reminders expiring.
        """

        # Grab finished stuff from the database
        db = await vbu.Database.get_connection()
        rows = await db(
            "SELECT * FROM reminders WHERE timestamp < TIMEZONE('UTC', NOW())")
        if not rows:
            await db.disconnect()
            return

        # Go through all finished reminders
        expired_reminders = []
        for reminder in rows:
            channel_id = reminder["channel_id"]
            user_id = reminder["user_id"]
            message_id = reminder["message_id"]
            message = reminder["message"]
            reminder_id = reminder["reminder_id"]

            try:
                channel = self.bot.get_channel(
                    channel_id) or await self.bot.fetch_channel(channel_id)
            except discord.HTTPException:
                channel = None
            sendable = {
                "content":
                f"<@{user_id}> reminder `{reminder_id}` triggered - {message}",
                "allowed_mentions":
                discord.AllowedMentions(users=[discord.Object(user_id)]),
            }
            if message_id:
                sendable.update({
                    "reference":
                    discord.MessageReference(message_id=message_id,
                                             channel_id=channel_id),
                    "mention_author":
                    True,
                })
            try:
                assert channel is not None
                try:
                    await channel.send(**sendable)
                except Exception:
                    sendable.pop("reference")
                    await channel.send(**sendable)
            except (AssertionError, discord.Forbidden):
                try:
                    user = self.bot.get_user(
                        user_id) or await self.bot.fetch_user(user_id)
                    await user.send(**sendable)
                except discord.HTTPException:
                    pass
            except AttributeError:
                pass
            expired_reminders.append(reminder_id)

        # Delete expired reminders
        await db("DELETE FROM reminders WHERE reminder_id=ANY($1::TEXT[])",
                 expired_reminders)
        await db.disconnect()
コード例 #9
0
class TimezoneInfo(vbu.Cog):

    @commands.group(
        aliases=['tz'],
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    async def timezone(self, ctx: vbu.Context):
        """
        The parent group for timezone commands.
        """

        if ctx.invoked_subcommand is None:
            return await ctx.send_help(ctx.command)

    @staticmethod
    def get_common_timezone(name) -> str:
        if len(name) <= 4:
            name = name.upper()
        else:
            name = name.title()
        common_timezones = {
            "PST": "US/Pacific",
            "MST": "US/Mountain",
            "CST": "US/Central",
            "EST": "US/Eastern",
        }
        if name in common_timezones:
            name = common_timezones[name]
        return name

    @timezone.command(
        name="set",
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="offset",
                    description="The timezone that you live in.",
                    type=discord.ApplicationCommandOptionType.string,
                    autocomplete=True,
                ),
            ],
        ),
    )
    async def timezone_set(self, ctx: vbu.Context, *, offset: str = None):
        """
        Sets and stores your UTC offset into the bot.
        """

        # Ask them the question
        if offset is None:
            ask_message = await ctx.send((
                f"Hey, {ctx.author.mention}, what timezone are you currently in? You can give its name (`EST`, `GMT`, etc) "
                "or you can give your continent and nearest large city (`Europe/Amsterdam`, `Australia/Sydney`, etc) - this is "
                "case sensitive."
            ))
            try:
                check = lambda m: m.author.id == ctx.author.id and m.channel.id == ctx.channel.id
                response_message = await self.bot.wait_for("message", check=check, timeout=30)
                offset = response_message.content
            except asyncio.TimeoutError:
                return await ask_message.delete()

        # See if it's one of the more common ones that I know don't actually exist
        offset = self.get_common_timezone(offset)

        # Try and parse the timezone name
        try:
            zone = pytz.timezone(offset)
        except pytz.UnknownTimeZoneError:
            return await ctx.send(f"I can't work out what timezone you're referring to - please run this command again to try later, or go to the website (`{ctx.clean_prefix}info`) and I can work it out automatically.")

        # Store it in the database
        async with vbu.Database() as db:
            await db(
                """INSERT INTO user_settings (user_id, timezone_name) VALUES ($1, $2) ON CONFLICT (user_id)
                DO UPDATE SET timezone_name=excluded.timezone_name""",
                ctx.author.id, zone.zone,
            )
        await ctx.send(f"I think your current time is **{discord.utils.utcnow().astimezone(zone).strftime('%-I:%M %p')}** - I've stored this in the database.")

    @commands.context_command(name="Get user's timezone")
    async def _context_command_timezone_get(self, ctx: vbu.SlashContext, user: discord.Member):
        command = self.timezone_get
        await command.can_run(ctx)
        await ctx.invoke(command, user)

    @timezone.command(
        name="get",
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="target",
                    description="The user whose timezone you want to get.",
                    type=discord.ApplicationCommandOptionType.user,
                    required=False,
                ),
            ],
        ),
    )
    @commands.defer()
    async def timezone_get(self, ctx: vbu.Context, target: typing.Union[discord.Member, str] = None):
        """
        Get the current time for a given user.
        """

        # Check if they are a bot
        target = target or ctx.author
        target_is_timezone = False
        if isinstance(target, str):
            target_is_timezone = True
            target = self.get_common_timezone(target)
        if isinstance(target, discord.Member) and target.bot:
            return await ctx.send("I don't think bots have timezones...")

        # See if they've set a timezone
        if not target_is_timezone:
            async with vbu.Database() as db:
                rows = await db("SELECT timezone_name, timezone_offset FROM user_settings WHERE user_id=$1", target.id)
            if not rows or (rows[0]['timezone_name'] is None and rows[0]['timezone_offset'] is None):
                return await ctx.send(f"{target.mention} hasn't set up their timezone information! They can set it by running `{ctx.clean_prefix}timezone set`.")

        # Grab their current time and output
        if target_is_timezone:
            try:
                formatted_time = (discord.utils.utcnow().astimezone(pytz.timezone(target))).strftime('%-I:%M %p')
            except pytz.UnknownTimeZoneError:
                return await ctx.send("That isn't a valid timezone.")
            return await ctx.send(f"The current time in **{target}** is estimated to be **{formatted_time}**.")
        elif rows:
            if rows[0]['timezone_name']:
                formatted_time = (discord.utils.utcnow().astimezone(pytz.timezone(rows[0]['timezone_name']))).strftime('%-I:%M %p')
            else:
                formatted_time = (discord.utils.utcnow() + timedelta(minutes=rows[0]['timezone_offset'])).strftime('%-I:%M %p')
            await ctx.send(f"The current time for {target.mention} is estimated to be **{formatted_time}**.", allowed_mentions=discord.AllowedMentions.none())
コード例 #10
0
class RunescapeCommands(vbu.Cog):
    def __init__(self, bot):
        super().__init__(bot)
        self.item_ids_path = Path().parent.joinpath('config').joinpath(
            'osrs-item-ids.json')
        with open(self.item_ids_path) as item_ids_file:
            self.item_ids = json.load(item_ids_file)

    @staticmethod
    def rs_notation_to_int(value_str: str) -> int:
        """
        Change a value string ("1.2m") into an int (1200000)
        https://github.com/JMcB17/osrs-blast-furnace-calc
        """

        multipliers = {
            'k': 10**3,
            'm': 10**6,
            'b': 10**9,
        }
        value_str = value_str.replace(',', '').strip()

        for multi, value in multipliers.items():
            if value_str.endswith(multi):
                value_str = value_str.rstrip(multi)
                value = int(float(value_str) * value)
                break
        else:
            value = int(value_str)
        return value

    async def get_item_details_by_id(self, item_id: int) -> dict:
        """
        Return the JSON response from the rs API given an item's Runescape ID.
        """

        # Send our web request
        url = API_BASE_URL + 'api/catalogue/detail.json'
        params = {
            'item': item_id,
        }
        headers = {
            "User-Agent": self.bot.user_agent,
        }
        async with self.bot.session.get(url, params=params,
                                        headers=headers) as response:
            # The Runescape API doesn't set a json header, so aiohttp complains about it.
            # We can just say to not check the content type.
            # https://github.com/aio-libs/aiohttp/blob/8c82ba11b9e38851d75476d261a1442402cc7592/aiohttp/web_request.py#L664-L681
            item = await response.json(content_type=None)

        # revolver ocelot (revolver ocelot)
        item = item['item']
        return item

    async def parse_item_value(
            self,
            item: dict,
            return_int: bool = True) -> typing.Union[int, str]:
        """
        Parse the value of an item from the JSON response from the rs API.
        """

        value = item['current']['price']
        if isinstance(value, str):
            value = value.strip()
            if return_int:
                value = self.rs_notation_to_int(value)
        else:
            value = str(value)

        return value

    @commands.command(
        aliases=['ge'],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="item",
                description="The item that you want to search.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    async def grandexchange(self, ctx: vbu.Context, *, item: str):
        """
        Get the value of an item on the grand exchange (OSRS).
        """

        async with ctx.typing():
            if item.lower() in ['random']:
                item_id = random.choice(list(self.item_ids.values()))
            else:
                item = item.capitalize()
                item_id = self.item_ids.get(item)

            if item_id:
                item_dict = await self.get_item_details_by_id(item_id)
                item_value = await self.parse_item_value(item_dict,
                                                         return_int=False)

                name = item_dict['name']
                item_page_url = API_BASE_URL + f"a=373/{name.replace(' ', '+')}/viewitem?obj={item_id}"

                with vbu.Embed() as embed:
                    embed.set_author(name=name,
                                     url=item_page_url,
                                     icon_url=item_dict['icon'])
                    embed.set_thumbnail(url=item_dict['icon_large'])
                    embed.add_field('Value',
                                    f'{item_value} coins',
                                    inline=False)
                    embed.add_field(f'Examine {name}',
                                    item_dict['description'],
                                    inline=False)
                    embed.add_field('Members',
                                    MEMBERS_MAPPING[item_dict['members']],
                                    inline=False)

                return await ctx.send(embed=embed)
            else:
                return await ctx.send('Item not found')
コード例 #11
0
class DNDCommands(vbu.Cog[Bot]):

    DICE_REGEX = re.compile(
        r"^(?P<count>\d+)?[dD](?P<type>\d+) *(?P<modifier>(?P<modifier_parity>[+-]) *(?P<modifier_amount>\d+))?$"
    )
    ATTRIBUTES = {
        "strength": "STR",
        "dexterity": "DEX",
        "constitution": "CON",
        "intelligence": "INT",
        "wisdom": "WIS",
        "charisma": "CHR",
    }

    @commands.command(
        aliases=['roll'],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="dice",
                type=discord.ApplicationCommandOptionType.string,
                description=
                "The die (in form 'XdY+Z', eg '5d6+2') that you want to roll.",
            ),
        ], ),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def dice(self, ctx: commands.Context, *, dice: str):
        """
        Rolls a dice for you.
        """

        # Validate the dice
        if not dice:
            raise vbu.errors.MissingRequiredArgumentString(dice)
        match = self.DICE_REGEX.search(dice)
        if not match:
            raise commands.BadArgument(
                "Your dice was not in the format `AdB+C`.")

        # Roll em
        dice_count = int(match.group("count") or 1)
        dice_type = int(match.group("type"))
        modifier = int((match.group("modifier") or "+0").replace(" ", ""))
        rolls = [random.randint(1, dice_type) for _ in range(dice_count)]
        total = sum(rolls) + modifier
        dice_string = f"{dice_count}d{dice_type}{modifier:+}"
        if not modifier:
            dice_string = dice_string[:-2]

        # Get formatted output
        if dice_count > 1 or modifier:
            equals_string = f"{sum(rolls)} {'+' if modifier > 0 else '-'} {abs(modifier)}"
            if modifier:
                text = f"Total **{total:,}** ({dice_string})\n({', '.join([str(i) for i in rolls])}) = {equals_string}"
            else:
                text = f"Total **{total:,}** ({dice_string})\n({', '.join([str(i) for i in rolls])}) = {equals_string}"
        else:
            text = f"Total **{total}** ({dice_string})"

        # And output
        if isinstance(ctx, commands.SlashContext):
            await ctx.interaction.response.send_message(text)
        else:
            await ctx.send(text)

    async def send_web_request(self, resource: str,
                               item: str) -> typing.Optional[dict]:
        """
        Send a web request to the dnd5eapi website.
        """

        url = f"https://www.dnd5eapi.co/api/{resource}/{quote(item.lower().replace(' ', '-'))}/"
        headers = {"User-Agent": self.bot.user_agent}
        async with self.bot.session.get(url, headers=headers) as r:
            v = await r.json()
        if v.get("error"):
            return None
        return v

    @staticmethod
    def group_field_descriptions(
        embed: discord.Embed,
        field_name: str,
        input_list: typing.List[_DNDMonsterAction],
    ) -> None:
        """
        Add fields grouped to the embed character limit.
        """

        original_field_name = field_name
        joiner = "\n"
        action_text = [
            f"**{i['name']}**{joiner}{i['desc']}" for i in input_list
        ]
        add_text = ""
        for text in action_text:
            if len(add_text) + len(text) + 1 > 1024:
                embed.add_field(
                    name=field_name,
                    value=add_text,
                    inline=False,
                )
                field_name = f"{original_field_name} Continued"
                add_text = ""
            add_text += joiner + text
        if add_text:
            embed.add_field(
                name=field_name,
                value=add_text,
                inline=False,
            )

    @commands.group(
        aliases=["d&d"],
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def dnd(self, ctx: commands.Context):
        """
        The parent group for the D&D commands.
        """

        if ctx.invoked_subcommand is None:
            return await ctx.send_help(ctx.command)

    @dnd.command(
        name="spell",
        aliases=["spells"],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="spell_name",
                description=
                "The name of the spell that you want to get the information of.",
                type=discord.ApplicationCommandOptionType.string,
                autocomplete=True,
            ),
        ], ),
    )
    @commands.defer()
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def dnd_spell(self, ctx: commands.Context, *, spell_name: str):
        """
        Gives you information on a D&D spell.
        """

        # Get our data
        data = await self.send_web_request("spells", spell_name)
        if not data:
            return await ctx.send(
                "I couldn't find any information for that spell.")

        # Make an embed
        embed = vbu.Embed(
            use_random_colour=True,
            title=data['name'],
            description=data['desc'][0],
        ).add_field(
            name="Casting Time",
            value=data['casting_time'],
        ).add_field(
            name="Range",
            value=data['range'],
        ).add_field(
            name="Components",
            value=', '.join(data['components']),
        ).add_field(
            name="Material",
            value=data.get('material', 'N/A'),
        ).add_field(
            name="Duration",
            value=data['duration'],
        ).add_field(
            name="Classes",
            value=', '.join([i['name'] for i in data['classes']]),
        ).add_field(
            name="Ritual",
            value=data['ritual'],
        ).add_field(
            name="Concentration",
            value=data['concentration'],
        )
        if data.get('higher_level'):
            embed.add_field(
                name="Higher Level",
                value="\n".join(data['higher_level']),
                inline=False,
            )
        elif data.get('damage'):
            text = ""
            if data['damage'].get('damage_at_character_level'):
                text += "\nCharacter level " + ", ".join([
                    f"{i}: {o}" for i, o in data['damage']
                    ['damage_at_character_level'].items()
                ])
            if data['damage'].get('damage_at_slot_level'):
                text += "\nSlot level " + ", ".join([
                    f"{i}: {o}"
                    for i, o in data['damage']['damage_at_slot_level'].items()
                ])
            embed.add_field(
                name="Damage",
                value=text.strip(),
                inline=False,
            )

        # And send
        return await ctx.send(embed=embed)

    @functools.cached_property
    def all_spells(self) -> typing.List[str]:
        with open("cogs/data/dnd_spells.txt") as a:
            return a.read().strip().split("\n")

    @dnd_spell.autocomplete
    async def dnd_spell_autocomplete(self, ctx: commands.SlashContext,
                                     interaction: discord.Interaction[None]):
        """
        Fuzzy match what the user input and the list of all spells.
        """

        # Get what the user gave us
        assert interaction.options is not None
        user_input = interaction.options[0].options[0].value
        assert user_input

        # Determine what's closest to what they said
        fuzzed = [(
            i,
            fuzz.ratio(i, user_input),
        ) for i in self.all_spells if user_input.casefold() in i.casefold()]
        fuzzed.sort(key=operator.itemgetter(1), reverse=True)

        # And give them the top results
        await interaction.response.send_autocomplete([
            discord.ApplicationCommandOptionChoice(name=i, value=i)
            for i, _ in fuzzed[:25]
        ])

    @dnd.command(
        name="monster",
        aliases=["monsters"],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="monster_name",
                description=
                "The monster that you want to get the information of.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.defer()
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def dnd_monster(self, ctx: commands.Context, *, monster_name: str):
        """
        Gives you information on a D&D monster.
        """

        # Get our data
        data = await self.send_web_request("monsters", monster_name)
        if not data:
            return await ctx.send(
                "I couldn't find any information for that monster.")

        # Make an embed
        embed = vbu.Embed(
            use_random_colour=True,
            title=data['name'],
            description="\n".join([
                f"{data['size'].capitalize()} | {data['type']} | {data['hit_points']:,} ({data['hit_dice']}) HP | {data['xp']:,} XP",
                ", ".join(
                    [f"{o} {data[i]}" for i, o in self.ATTRIBUTES.items()]),
            ])).add_field(
                name="Proficiencies",
                value=", ".join([
                    f"{i['proficiency']['name']} {i['value']}"
                    for i in data['proficiencies']
                ]) or "None",
            ).add_field(
                name="Damage Vulnerabilities",
                value="\n".join(data['damage_vulnerabilities']).capitalize()
                or "None",
            ).add_field(
                name="Damage Resistances",
                value="\n".join(data['damage_resistances']).capitalize()
                or "None",
            ).add_field(
                name="Damage Immunities",
                value="\n".join(data['damage_immunities']).capitalize()
                or "None",
            ).add_field(
                name="Condition Immunities",
                value="\n".join(
                    [i['name']
                     for i in data['condition_immunities']]).capitalize()
                or "None",
            ).add_field(
                name="Senses",
                value="\n".join([
                    f"{i.replace('_', ' ').capitalize()} {o}"
                    for i, o in data['senses'].items()
                ]) or "None",
            )
        self.group_field_descriptions(embed, "Actions", data['actions'])
        self.group_field_descriptions(embed, "Legendary Actions",
                                      data.get('legendary_actions', list()))
        if data.get('special_abilities'):
            embed.add_field(
                name="Special Abilities",
                value="\n".join([
                    f"**{i['name']}**\n{i['desc']}"
                    for i in data['special_abilities']
                    if i['name'] != 'Spellcasting'
                ]) or "None",
                inline=False,
            )
        spellcasting = [
            i for i in data.get('special_abilities', list())
            if i['name'] == 'Spellcasting'
        ]
        if spellcasting:
            spellcasting = spellcasting[0]
            embed.add_field(
                name="Spellcasting",
                value=spellcasting['desc'].replace('\n\n', '\n'),
                inline=False,
            )

        # And send
        return await ctx.send(embed=embed)

    @dnd.command(
        name="condition",
        aliases=["conditions"],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="condition_name",
                description=
                "The condition that you want to get the information of.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.defer()
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def dnd_condition(self, ctx: commands.Context, *,
                            condition_name: str):
        """
        Gives you information on a D&D condition.
        """

        # Get our data
        async with ctx.typing():
            data = await self.send_web_request("conditions", condition_name)
        if not data:
            return await ctx.send(
                "I couldn't find any information for that condition.")

        # Make an embed
        embed = vbu.Embed(
            use_random_colour=True,
            title=data['name'],
            description="\n".join(data['desc']),
        )

        # And send
        return await ctx.send(embed=embed)

    @dnd.command(
        name="class",
        aliases=["classes"],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="class_name",
                description="The class that you want to get the informaton of.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.defer()
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def dnd_class(self, ctx: commands.Context, *, class_name: str):
        """
        Gives you information on a D&D class.
        """

        # Get our data
        async with ctx.typing():
            data = await self.send_web_request("classes", class_name)
        if not data:
            return await ctx.send(
                "I couldn't find any information for that class.")

        # Make embed
        embed = vbu.Embed(
            use_random_colour=True,
            title=data['name'],
        ).add_field(
            "Proficiencies",
            ", ".join([i['name'] for i in data['proficiencies']]),
        ).add_field(
            "Saving Throws",
            ", ".join([i['name'] for i in data['saving_throws']]),
        ).add_field(
            "Starting Equipment",
            "\n".join([
                f"{i['quantity']}x {i['equipment']['name']}"
                for i in data['starting_equipment']
            ]),
        )

        # And send
        return await ctx.send(embed=embed)
コード例 #12
0
class GoogleCommands(vbu.Cog):
    def get_search_page(self, query: str, num: int, image: bool = False):
        """
        Get a number of results from Google
        """
        async def wrapper(page_number: int):
            params = {
                'key': self.bot.config['api_keys']['google']['api_key'],
                'cx':
                self.bot.config['api_keys']['google']['search_engine_id'],
                'num': num,
                'q': query,
                'safe': 'active',
                'start': num * page_number,
            }
            if image:
                params.update({'searchType': 'image'})
                formatter = lambda d: (d['title'][:256], d['link'])
            else:
                formatter = lambda d: (
                    d['title'][:256],
                    f"[{d['displayLink']}]({d['link']}) - {d['snippet'].replace(ENDL, ' ')}"
                )
            async with self.bot.session.get(
                    "https://customsearch.googleapis.com/customsearch/v1",
                    params=params) as r:
                data = await r.json()
            ENDL = '\n'
            output_data = []
            for d in data.get('items', list()):
                output_data.append(formatter(d))
            if not output_data:
                raise StopAsyncIteration()
            return output_data

        return wrapper

    @commands.group(
        invoke_without_command=True,
        aliases=['search'],
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    @vbu.checks.is_config_set('api_keys', 'google', 'search_engine_id')
    @vbu.checks.is_config_set('api_keys', 'google', 'api_key')
    async def google(self, ctx: vbu.Context):
        """
        The parent group for the google commands.
        """

        if ctx.invoked_subcommand is None:
            return await ctx.send_help(ctx.command)

    @google.command(
        name="search",
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="query",
                description="The text that you want to search.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    @vbu.checks.is_config_set('api_keys', 'google', 'search_engine_id')
    @vbu.checks.is_config_set('api_keys', 'google', 'api_key')
    async def google_search(self, ctx: vbu.Context, *, query: str):
        """
        Search a query on Google.
        """

        if query.startswith("-"):
            raise vbu.errors.MissingRequiredArgumentString("query")

        def formatter(menu, data):
            embed = vbu.Embed(use_random_colour=True)
            for d in data:
                embed.add_field(*d, inline=False)
            embed.set_footer(f"Page {menu.current_page + 1}/{menu.max_pages}")
            return embed

        await vbu.Paginator(self.get_search_page(query, 3),
                            formatter=formatter).start(ctx)

    @google.command(
        name='images',
        aliases=['image', 'i'],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="query",
                description="The text that you want to search.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.has_permissions(embed_links=True)
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    @vbu.checks.is_config_set('api_keys', 'google', 'search_engine_id')
    @vbu.checks.is_config_set('api_keys', 'google', 'api_key')
    async def google_image(self, ctx: vbu.Context, *, query: str):
        """
        Search a query on Google Images.
        """

        if query.startswith("-"):
            raise vbu.errors.MissingRequiredArgumentString("query")

        def formatter(menu, data):
            return vbu.Embed(
                use_random_colour=True,
                title=data[0][0]).set_image(data[0][1]).set_footer(
                    f"Page {menu.current_page + 1}/{menu.max_pages}")

        await vbu.Paginator(self.get_search_page(query, 1, True),
                            formatter=formatter).start(ctx)
コード例 #13
0
class QuoteCommands(vbu.Cog):

    IMAGE_URL_REGEX = re.compile(
        r"(http(?:s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png|jpeg|webp)")
    QUOTE_SEARCH_CHARACTER_CUTOFF = 100

    async def get_quote_messages(self,
                                 ctx: vbu.Context,
                                 messages: typing.List[discord.Message],
                                 *,
                                 allow_self_quote: bool = False) -> dict:
        """
        Gets the messages that the user has quoted, returning a dict with keys `success` (bool) and `message` (str or voxelbotutils.Embed).
        If `success` is `False`, then the resulting `message` can be directly output to the user, and if it's `True` then we can go ahead
        with the message save flowthrough.
        """

        # Make sure they have a quote channel
        assert ctx.guild
        if self.bot.guild_settings[ctx.guild.id].get(
                'quote_channel_id') is None:
            func = "You don't have a quote channel set!"
            return {'success': False, 'message': func}

        # Make sure a message was passed
        if not messages:
            if ctx.message.reference is not None:
                message_from_reply = await ctx.fetch_message(
                    ctx.message.reference.message_id)
                messages = [message_from_reply]
            else:
                return {
                    'success':
                    False,
                    'message':
                    "I couldn't find any references to messages in your command call."
                }

        # Recreate the message list without duplicates
        unique_messages = []
        unique_message_ids = set()
        for i in messages:
            if i.id not in unique_message_ids:
                unique_messages.append(i)
            unique_message_ids.add(i.id)
        messages = unique_messages

        # Make sure they're all sent as a reasonable time apart
        quote_is_url = False
        messages = sorted(messages, key=lambda m: m.created_at)
        for i, o in zip(messages, messages[1:]):
            if o is None:
                break
            if (o.created_at - i.created_at).total_seconds() > 3 * 60:
                return {
                    'success':
                    False,
                    'message':
                    "Those messages are too far apart to quote together."
                }
            if not i.content or i.attachments:
                if len(i.attachments) == 0:
                    return {
                        'success': False,
                        'message': "Embeds can't be quoted."
                    }
                if i.attachments:
                    return {
                        'success':
                        False,
                        'message':
                        "You can't quote multiple messages when quoting images."
                    }

        # Validate the message content
        for message in messages:
            if (quote_is_url and message.content) or (
                    message.content and message.attachments
                    and message.content != message.attachments[0].url):
                return {
                    'success': False,
                    'message': "You can't quote both messages and images."
                }
            elif message.embeds and getattr(message.embeds[0].thumbnail, "url",
                                            None) != message.content:
                return {'success': False, 'message': "You can't quote embeds."}
            elif len(message.attachments) > 1:
                return {
                    'success': False,
                    'message': "Multiple images can't be quoted."
                }
            elif message.attachments:
                if self.IMAGE_URL_REGEX.search(
                        message.attachments[0].url) is None:
                    return {
                        'success':
                        False,
                        'message':
                        "The attachment in that image isn't a valid image URL."
                    }
                message.content = message.attachments[0].url
                quote_is_url = True

        # Validate input
        timestamp = discord.utils.naive_dt(messages[0].created_at)
        user = messages[0].author
        text = '\n'.join([m.content for m in messages])
        if len(set([i.author.id for i in messages])) != 1:
            return {
                'success': False,
                'message': "You can only quote one person at a time."
            }

        # Make sure they're not quoting themself if there are no reactions needed
        message_author = messages[0].author
        reactions_needed = self.bot.guild_settings[
            ctx.guild.id]['quote_reactions_needed']
        if ctx.author.id in self.bot.owner_ids:
            pass
        elif ctx.author.id == message_author.id and (
                reactions_needed or allow_self_quote is False):
            return {
                'success': False,
                'message': "You can't quote yourself when there's no vote :/"
            }

        # Return an embed
        with vbu.Embed(use_random_colour=True) as embed:
            embed.set_author_to_user(user)
            if quote_is_url:
                embed.set_image(text)
            else:
                embed.description = text
            embed.timestamp = timestamp
        return {
            'success': True,
            'message': embed,
            'user': user,
            'timestamp': timestamp
        }

    @commands.group(
        invoke_without_command=True,
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True,
                                  embed_links=True,
                                  add_reactions=True)
    @commands.guild_only()
    async def quote(self, ctx: vbu.Context,
                    messages: commands.Greedy[discord.Message]):
        """
        Quotes a user's message to the guild's quote channel.
        """

        # Make sure no subcommand is passed
        if ctx.invoked_subcommand is not None:
            return
        response = await self.get_quote_messages(ctx, messages)

        # Make embed
        if response['success'] is False:
            return await ctx.send(response['message'])
        embed = response['message']
        user = response['user']
        timestamp = response['timestamp']

        # See if we should bother saving it
        reactions_needed = self.bot.guild_settings[
            ctx.guild.id]['quote_reactions_needed']
        ask_to_save_message = await ctx.send(
            f"Should I save this quote? If I receive {reactions_needed} positive reactions in the next 60 seconds, the quote will be saved.",
            embed=embed,
        )
        self.bot.loop.create_task(
            ask_to_save_message.add_reaction("\N{THUMBS UP SIGN}"))
        self.bot.loop.create_task(
            ask_to_save_message.add_reaction("\N{THUMBS DOWN SIGN}"))
        await asyncio.sleep(60)

        # Get the message again so we can refresh the reactions
        try:
            ask_to_save_message_again = await ask_to_save_message.channel.fetch_message(
                ask_to_save_message.id)
            reaction_count = sum([
                i.count if str(i.emoji) == "\N{THUMBS UP SIGN}" else
                -i.count if str(i.emoji) == "\N{THUMBS DOWN SIGN}" else 0
                for i in ask_to_save_message_again.reactions
            ])
        except discord.HTTPException:
            return
        try:
            await ask_to_save_message.delete()
        except discord.HTTPException:
            pass
        if reaction_count < reactions_needed:
            return await ctx.send(
                f"_Not_ saving the quote asked by {ctx.author.mention} - not enough reactions received."
            )

        # If we get here, we can save to db
        quote_id = create_id()

        # See if they have a quotes channel
        quote_channel_id = self.bot.guild_settings[ctx.guild.id].get(
            'quote_channel_id')
        embed.set_footer(text=f"Quote ID {quote_id.upper()}")
        posted_message = None
        if quote_channel_id:
            channel = self.bot.get_channel(quote_channel_id)
            try:
                posted_message = await channel.send(embed=embed)
            except (discord.Forbidden, AttributeError):
                pass
        if quote_channel_id is None or posted_message is None:
            return await ctx.send(
                "I couldn't send your quote into the quote channel.")

        # And save it to the database
        async with vbu.Database() as db:
            await db(
                """INSERT INTO user_quotes (quote_id, guild_id, channel_id, message_id, user_id, timestamp, quoter_id)
                VALUES ($1, $2, $3, $4, $5, $6, $7)""",
                quote_id,
                ctx.guild.id,
                posted_message.channel.id,
                posted_message.id,
                user.id,
                timestamp,
                ctx.author.id,
            )

        # Output to user
        await ctx.send(
            f"{ctx.author.mention}'s quote request saved with ID `{quote_id.upper()}`",
            embed=embed)

    @commands.context_command(name="Quote message")
    async def _context_command_quote_create(self, ctx: vbu.Context,
                                            message: discord.Message):
        command = self.quote_create
        await command.can_run(ctx)
        await ctx.invoke(command, message)

    @quote.command(name="create")
    @commands.bot_has_permissions(send_messages=True,
                                  embed_links=True,
                                  add_reactions=True)
    @commands.guild_only()
    async def quote_create(self, ctx: vbu.Context, message: discord.Message):
        """
        Quotes a user's message to the guild's quote channel.
        """

        await self.quote(ctx, [message])

    @quote.command(name="force")
    @commands.has_guild_permissions(manage_guild=True)
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def quote_force(self, ctx: vbu.Context,
                          messages: commands.Greedy[discord.Message]):
        """
        Quotes a user's message to the guild's quote channel.
        """

        # Make sure no subcommand is passed
        if ctx.invoked_subcommand is not None:
            return
        response = await self.get_quote_messages(ctx,
                                                 messages,
                                                 allow_self_quote=True)

        # Make embed
        if response['success'] is False:
            return await ctx.send(response['message'])
        embed = response['message']
        user = response['user']
        timestamp = response['timestamp']

        # See if they have a quotes channel
        quote_channel_id = self.bot.guild_settings[ctx.guild.id].get(
            'quote_channel_id')
        quote_id = create_id()
        embed.set_footer(text=f"Quote ID {quote_id.upper()}")
        posted_message = None
        if quote_channel_id:
            channel = self.bot.get_channel(quote_channel_id)
            try:
                posted_message = await channel.send(embed=embed)
            except (discord.Forbidden, AttributeError):
                pass
        if quote_channel_id is None or posted_message is None:
            return await ctx.send(
                "I couldn't send your quote into the quote channel.")

        # And save it to the database
        async with vbu.Database() as db:
            await db(
                """INSERT INTO user_quotes (quote_id, guild_id, channel_id,
                message_id, user_id, timestamp, quoter_id)
                VALUES ($1, $2, $3, $4, $5, $6, $7)""",
                quote_id,
                ctx.guild.id,
                posted_message.channel.id,
                posted_message.id,
                user.id,
                timestamp.replace(tzinfo=None),
                ctx.author.id,
            )

        # Output to user
        await ctx.send(
            f"{ctx.author.mention}'s quote saved with ID `{quote_id.upper()}`",
            embed=embed)

    @quote.command(
        name="get",
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="identifier",
                description="The ID of the quote that you want to get.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def quote_get(self, ctx: vbu.Context, identifier: str):
        """
        Gets a quote from the guild's quote channel.
        """

        # Get quote from database
        async with vbu.Database() as db:
            quote_rows = await db(
                """SELECT user_quotes.quote_id as quote_id, user_id, channel_id, message_id FROM user_quotes LEFT JOIN
                quote_aliases ON user_quotes.quote_id=quote_aliases.quote_id
                WHERE user_quotes.quote_id=$1 OR quote_aliases.alias=$1""",
                identifier.lower(),
            )
        if not quote_rows:
            return await ctx.send(
                f"There's no quote with the identifier `{identifier.upper()}`.",
                allowed_mentions=discord.AllowedMentions.none(),
            )

        # Get the message
        data = quote_rows[0]
        if data['channel_id'] is None:
            return await ctx.send(
                "There's no quote channel set for that quote.")
        channel = self.bot.get_channel(data['channel_id'])
        if channel is None:
            return await ctx.send("I wasn't able to get your quote channel.")
        try:
            message = await channel.fetch_message(data['message_id'])
            assert message is not None
        except (AssertionError, discord.HTTPException):
            return await ctx.send("I wasn't able to get your quote message.")

        # try to refresh the user name and icon of the embed by getting the user from the user ID in the DB
        quote_embed = message.embeds[0]
        quote_author = self.bot.get_user(data['user_id'])
        if quote_author:
            quote_embed.set_author(name=quote_author.display_name,
                                   icon_url=quote_author.display_avatar.url)

        # Output to user
        return await ctx.send(embed=quote_embed)

    @quote.command(
        name="random",
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="user",
                description="The user whose quotes you want to search.",
                type=discord.ApplicationCommandOptionType.user,
                required=False,
            ),
        ], ),
    )
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def quote_random(self,
                           ctx: vbu.Context,
                           user: discord.Member = None):
        """
        Gets a random quote for a given user.
        """

        # Get quote from database
        user = user or ctx.author
        async with vbu.Database() as db:
            quote_rows = await db(
                """SELECT quote_id as quote_id, user_id, channel_id, message_id
                FROM user_quotes WHERE user_id=$1 AND guild_id=$2 ORDER BY RANDOM() LIMIT 1""",
                user.id,
                ctx.guild.id,
            )
        if not quote_rows:
            return await ctx.send(
                f"{user.mention} has no available quotes.",
                allowed_mentions=discord.AllowedMentions.none())

        # Get the message
        data = quote_rows[0]
        if data['channel_id'] is None:
            self.logger.info(f"Deleting legacy quote - {data['quote_id']}")
            async with vbu.Database() as db:
                await db("DELETE FROM user_quotes WHERE quote_id=$1",
                         data['quote_id'])
            return await ctx.reinvoke()
        channel = self.bot.get_channel(data['channel_id'])
        if channel is None:
            self.logger.info(
                f"Deleting quote from deleted channel - {data['quote_id']}")
            async with vbu.Database() as db:
                await db("DELETE FROM user_quotes WHERE quote_id=$1",
                         data['quote_id'])
            return await ctx.reinvoke()
        try:
            message = await channel.fetch_message(data['message_id'])
            assert message is not None
        except (AssertionError, discord.HTTPException):
            self.logger.info(
                f"Deleting quote from deleted message - {data['quote_id']}")
            async with vbu.Database() as db:
                await db("DELETE FROM user_quotes WHERE quote_id=$1",
                         data['quote_id'])
            return await ctx.reinvoke()

        # Output to user
        quote_embed = message.embeds[0]
        quote_author = self.bot.get_user(data['user_id'])
        if quote_author:
            quote_embed.set_author(name=quote_author.display_name,
                                   icon_url=quote_author.display_avatar.url)
        return await ctx.send(embed=quote_embed)

    # @quote.group(name="alias", invoke_without_command=True)
    # @commands.guild_only()
    # @commands.has_guild_permissions(manage_guild=True)
    # @commands.bot_has_permissions(send_messages=True)
    # async def quote_alias(self, ctx: vbu.Context, quote_id: commands.clean_content, alias: commands.clean_content):
    #     """
    #     Adds an alias to a quote.
    #     """

    #     # Grab data from db
    #     async with vbu.Database() as db:
    #         rows = await db("SELECT * FROM user_quotes WHERE quote_id=$1 AND guild_id=$2", quote_id.lower(), ctx.guild.id)
    #     if not rows:
    #         return await ctx.send(f"There's no quote with the ID `{quote_id.upper()}`.")

    #     # Insert alias into db
    #     async with vbu.Database() as db:
    #         rows = await db("SELECT * FROM quote_aliases WHERE alias=$1", alias)
    #         if rows:
    #             return await ctx.send(f"The alias `{alias}` is already being used.")
    #         await db("INSERT INTO quote_aliases (quote_id, alias) VALUES ($1, $2)", quote_id.lower(), alias.lower())
    #     await ctx.send(f"Added the alias `{alias.upper()}` to quote ID `{quote_id.upper()}`.")

    # @quote.command(name="list")
    # @commands.guild_only()
    # @commands.has_guild_permissions(manage_guild=True)
    # @commands.bot_has_permissions(send_messages=True)
    # async def quote_list(self, ctx: vbu.Context, user: discord.Member=None):
    #     """
    #     List the IDs of quotes for a user.
    #     """

    #     # Grab data from db
    #     user = user or ctx.author
    #     async with vbu.Database() as db:
    #         rows = await db("SELECT quote_id FROM user_quotes WHERE user_id=$1 AND guild_id=$2", user, ctx.guild.id)
    #     if not rows:
    #         embed = vbu.Embed(
    #             use_random_colour=True, description="This user has no quotes.",
    #         ).set_author_to_user(user)
    #         return await ctx.send(embed=embed)
    #     embed = vbu.Embed(
    #         use_random_colour=True, description="\n".join([i['quote_id'] for i in rows[:50]]),
    #     ).set_author_to_user(user)
    #     return await ctx.send(embed=embed)

    # @quote_alias.command(name="remove", aliases=["delete"])
    # @commands.guild_only()
    # @commands.has_guild_permissions(manage_guild=True)
    # @commands.bot_has_permissions(send_messages=True)
    # async def quote_alias_remove(self, ctx: vbu.Context, alias: commands.clean_content):
    #     """
    #     Deletes an alias from a quote.
    #     """

    #     # Grab data from db
    #     async with vbu.Database() as db:
    #         quote_rows = await db(
    #             """SELECT user_quotes.quote_id as quote_id, user_id, channel_id, message_id FROM user_quotes LEFT JOIN
    #             quote_aliases ON user_quotes.quote_id=quote_aliases.quote_id
    #             WHERE quote_aliases.alias=$1 AND guild_id=$2""",
    #             alias.lower(), ctx.guild.id
    #         )
    #         if not quote_rows:
    #             return await ctx.send(f"There's no quote with the alias `{alias.upper()}`.")
    #         await db("DELETE FROM quote_aliases WHERE alias=$1", alias.lower())
    #     return await ctx.send(f"Deleted alias `{alias.upper()}`.")

    @quote.command(
        name="delete",
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="quote_id",
                description="The ID of the quote that you want to delete.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.guild_only()
    @commands.has_permissions(manage_messages=True)
    @commands.bot_has_permissions(send_messages=True)
    async def quote_delete(self, ctx: vbu.Context, quote_id: str):
        """
        Deletes a quote from your server.
        """

        # quote_ids = [i.lower() for i in quote_ids]
        quote_ids = [quote_id.lower()]
        quote_channel_id = self.bot.guild_settings[ctx.guild.id].get(
            'quote_channel_id')
        if quote_channel_id:
            quote_channel = self.bot.get_channel(quote_channel_id)
            try:
                async for message in quote_channel.history(limit=150):
                    if not message.author.id == ctx.guild.me.id:
                        continue
                    if not message.embeds:
                        continue
                    embed = message.embeds[0]
                    if not embed.footer:
                        continue
                    footer_text = embed.footer.text
                    if not footer_text:
                        continue
                    if not footer_text.startswith("Quote ID"):
                        continue
                    message_quote_id = footer_text.split(' ')[2].lower()
                    if message_quote_id in quote_ids:
                        try:
                            await message.delete()
                        except discord.HTTPException:
                            pass
            except (discord.HTTPException, AttributeError) as e:
                await ctx.send(e)

        async with vbu.Database() as db:
            await db(
                "DELETE FROM user_quotes WHERE quote_id=ANY($1) AND guild_id=$2",
                quote_ids, ctx.guild.id)
        return await ctx.send("Deleted quote(s).")
コード例 #14
0
class UserInfo(vbu.Cog):

    @commands.command(
        aliases=["avatar", "av"],
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="target",
                    description="The item that you want to enlarge.",
                    type=discord.ApplicationCommandOptionType.string,
                ),
            ],
        ),
    )
    async def enlarge(
            self,
            ctx: vbu.Context,
            target: typing.Union[discord.Member, discord.User, discord.Emoji, discord.PartialEmoji] = None,
            ):
        """
        Enlarges the avatar or given emoji.
        """

        target = target or ctx.author
        if isinstance(target, (discord.User, discord.Member, discord.ClientUser)):
            url = target.display_avatar.url
        elif isinstance(target, (discord.Emoji, discord.PartialEmoji)):
            url = target.url
        with vbu.Embed(color=0x1) as embed:
            embed.set_image(url=str(url))
        await ctx.send(embed=embed)

    @commands.context_command(name="Get user info")
    async def _get_user_info(self, ctx: vbu.SlashContext, user: discord.Member):
        command = self.whois
        await command.can_run(ctx)
        await ctx.invoke(command, user)

    @commands.command(
        aliases=["whoami"],
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="user",
                    description="The user you want to get the information of.",
                    type=discord.ApplicationCommandOptionType.user,
                    required=False,
                ),
            ],
        ),
    )
    async def whois(self, ctx: vbu.Context, user: discord.Member = None):
        """
        Give you some information about a user.
        """

        # Set up our intial vars
        user = user or ctx.author
        embed = vbu.Embed(use_random_colour=True)
        embed.set_author_to_user(user)

        # Get the user account creation time
        create_value = f"{discord.utils.format_dt(user.created_at)}\n{discord.utils.format_dt(user.created_at, 'R')}"
        embed.add_field("Account Creation Time", create_value, inline=False)

        # Get the user guild join time
        if ctx.guild:
            join_value = f"{discord.utils.format_dt(user.joined_at)}\n{discord.utils.format_dt(user.joined_at, 'R')}"
            embed.add_field("Guild Join Time", join_value, inline=False)

        # Set the embed thumbnail
        embed.set_thumbnail(user.display_avatar.with_size(1024).url)

        # Sick
        if isinstance(ctx, commands.SlashContext):
            return await ctx.interaction.response.send_message(embed=embed)
        else:
            return await ctx.send(embed=embed)

    @commands.command(
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="amount",
                    description="The number of messages that you want to log.",
                    type=discord.ApplicationCommandOptionType.integer,
                    required=False,
                    max_value=500,
                ),
            ],
        ),
    )
    @commands.defer()
    @commands.guild_only()
    async def createlog(self, ctx: vbu.Context, amount: int = 100):
        """
        Create a log of chat.
        """

        # Make some assertions so we don't get errors elsewhere
        assert isinstance(ctx.channel, discord.TextChannel)
        assert ctx.guild

        # Create the data we're gonna send
        data = {
            "channel_name": ctx.channel.name,
            "category_name": ctx.channel.category.name if ctx.channel.category else "Uncategorized",
            "guild_name": ctx.guild.name,
            "guild_icon_url": str(ctx.guild.icon.with_format("png").with_size(512)) if ctx.guild.icon else None,
        }
        data_authors = {}
        data_messages = []

        # Get the data from the server
        async for message in ctx.channel.history(limit=min([max([1, amount]), 250])):
            for user in message.mentions + [message.author]:
                data_authors[user.id] = {
                    "username": user.name,
                    "discriminator": user.discriminator,
                    "avatar_url": str(user.display_avatar.with_size(512).with_format("png").url),
                    "bot": user.bot,
                    "display_name": user.display_name,
                    "color": user.colour.value,
                }
            message_data = {
                "id": message.id,
                "content": message.content,
                "author_id": message.author.id,
                "timestamp": int(message.created_at.timestamp()),
                "attachments": [str(i.url) for i in message.attachments],
            }
            embeds = []
            for i in message.embeds:
                embed_data: dict = i.to_dict()  # type: ignore
                if i.timestamp:
                    embed_data.update({'timestamp': i.timestamp.timestamp()})
                embeds.append(embed_data)
            message_data.update({'embeds': embeds})
            data_messages.append(message_data)

        # This takes a while
        async with ctx.typing():

            # Send data to the API
            data.update({"users": data_authors, "messages": data_messages[::-1]})
            async with self.bot.session.post("https://voxelfox.co.uk/discord/chatlog", json=data) as r:
                string = io.StringIO(await r.text())

        # Output it into the chat
        await ctx.send(file=discord.File(string, filename=f"Logs-{int(ctx.message.created_at.timestamp())}.html"))

    @commands.context_command(name="Screenshot message")
    @commands.guild_only()
    async def _context_command_screenshot_message(self, ctx: vbu.Context, message: discord.Message):
        command = self.screenshotmessage
        await command.can_run(ctx)
        await ctx.invoke(command, user=message.author, content=message)

    @commands.command(
        aliases=["screenshotmessage"],
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="user",
                    description="The user who you want to fake a message from.",
                    type=discord.ApplicationCommandOptionType.user,
                ),
                discord.ApplicationCommandOption(
                    name="content",
                    description="The content that you want in the message.",
                    type=discord.ApplicationCommandOptionType.string,
                ),
            ],
        ),
    )
    @commands.defer()
    @commands.guild_only()
    async def fakemessage(self, ctx: vbu.Context, user: discord.Member, *, content: typing.Union[str, discord.Message]):
        """
        Create a log of chat.
        """

        # See if we're getting a real message or a fake one
        content_message = ctx.message
        if isinstance(content, discord.Message):
            content_message = content
            user = content.author
            content = content.content
        assert isinstance(content_message.channel, discord.TextChannel)
        assert content_message.guild

        # Create the data we're gonna send
        data = {
            "channel_name": content_message.channel.name,
            "category_name": content_message.channel.category.name if content_message.channel.category else "Uncategorized",
            "guild_name": content_message.guild.name,
            "guild_icon_url": str(content_message.guild.icon.with_format("png").with_size(512)) if content_message.guild.icon else None,
        }
        data_authors = {}
        data_authors[user.id] = {
            "username": user.name,
            "discriminator": user.discriminator,
            "avatar_url": str(user.display_avatar.with_size(512).with_format("png").url),
            "bot": user.bot,
            "display_name": user.display_name,
            "color": user.colour.value,
        }
        for i in content_message.mentions:
            data_authors[i.id] = {
                "username": i.name,
                "discriminator": i.discriminator,
                "avatar_url": str(i.display_avatar.with_size(512).with_format("png").url),
                "bot": i.bot,
                "display_name": i.display_name,
                "color": i.colour.value,
            }
        message_data = {
            "id": 69,
            "content": content,
            "author_id": user.id,
            "timestamp": int(discord.utils.utcnow().timestamp()),
        }

        # This takes a while
        async with ctx.typing():

            # Send data to the API
            data.update({"users": data_authors, "messages": [message_data]})
            async with self.bot.session.post("https://voxelfox.co.uk/discord/chatlog", json=data) as r:
                string = await r.text()

            # Remove the preamble
            soup = BeautifulSoup(string, "html.parser")
            soup.find(class_="preamble").decompose()
            subset = str(soup)

            # Screenshot it
            options = {
                "quiet": "",
                "enable-local-file-access": "",
                "width": "600",
                "enable-javascript": "",
                "javascript-delay": "1000",
                # "window-status": "RenderingComplete",
                # "debug-javascript": "",
                # "no-stop-slow-scripts": "",
            }
            filename = f"FakedMessage-{ctx.author.id}.png"
            from_string = functools.partial(imgkit.from_string, subset, filename, options=options)
            await self.bot.loop.run_in_executor(None, from_string)

        # Output it into the chat
        await ctx.send(file=discord.File(filename))

        # And delete file
        await asyncio.sleep(1)
        await asyncio.create_subprocess_exec("rm", filename)
コード例 #15
0
class GithubCommands(vbu.Cog[Bot]):

    GIT_ISSUE_OPEN_EMOJI = "<:github_issue_open:817984658456707092>"
    GIT_ISSUE_CLOSED_EMOJI = "<:github_issue_closed:817984658372689960>"
    GIT_PR_OPEN_EMOJI = "<:github_pr_open:817986200618139709>"
    GIT_PR_CLOSED_EMOJI = "<:github_pr_closed:817986200962072617>"
    GIT_PR_CHANGES_EMOJI = "<:github_changes_requested:819115452948938772>"

    def __init__(self, bot: Bot):
        super().__init__(bot)
        GitRepo.bot = bot

    async def increase_repo_usage_counter(self, user: typing.Union[discord.User, discord.Member], repo: GitRepo):
        async with vbu.Database() as db:
            await db(
                """INSERT INTO github_repo_uses (user_id, owner, repo, host, uses)
                VALUES ($1, $2, $3, $4, 1) ON CONFLICT (user_id, owner, repo, host)
                DO UPDATE SET uses=github_repo_uses.uses+excluded.uses""",
                user.id, repo.owner, repo.repo, repo.host,
            )

    @vbu.Cog.listener()
    async def on_message(self, message: discord.Message):
        """
        Sends GitHub/Lab links if a message sent in the server matches the format `gh/user/repo`.
        """

        if message.author.bot:
            return
        if (await self.bot.get_context(message)).command is not None:
            return

        # Find matches in the message
        m = re.finditer(
            (
                r'(?:\s|^)(?P<ident>g[hl])/(?P<url>(?P<user>[a-zA-Z0-9_-]{1,255})/(?P<repo>[a-zA-Z0-9_-]{1,255}))'
                r'(?:[#!]?(?P<issue>\d+?))?(?:\s|$)'
            ),
            message.content,
        )
        n = re.finditer(
            r'(?:\s|^)(?P<ident>g[hl]) (?P<alias>\S{1,255})(?: [#!]?(?P<issue>\d+?))?(?:\s|$)',
            message.content,
        )

        # Dictionary of possible Git() links
        git_dict = {
            "gh": "hub",
            "gl": "lab",
        }

        # Add the url of each matched link to the final output
        sendable = ""
        for i in m:
            url = i.group("url")
            ident = i.group("ident")
            issue = i.group("issue")
            url = f"https://git{git_dict[ident]}.com/{url}"
            if issue:
                if ident == "gh":
                    url = f"{url}/issues/{issue}"
                elif ident == "gl":
                    url = f"{url}/-/issues/{issue}"
            sendable += f"<{url}>\n"
        if n:
            async with vbu.Database() as db:
                for i in n:
                    issue = i.group("issue")
                    rows = await db("SELECT * FROM github_repo_aliases WHERE alias=$1", i.group("alias"))
                    if rows:
                        url = f"https://{rows[0]['host'].lower()}.com/{rows[0]['owner']}/{rows[0]['repo']}"
                        if issue:
                            if rows[0]['host'] == "Github":
                                url = f"{url}/issues/{issue}"
                            elif rows[0]['host'] == "Gitlab":
                                url = f"{url}/-/issues/{issue}"
                        sendable += f"<{url}>\n"

        # Send the GitHub links if there's any output
        if sendable:
            await message.channel.send(sendable, allowed_mentions=discord.AllowedMentions.none())

    @commands.group(application_command_meta=commands.ApplicationCommandMeta())
    async def repoalias(self, ctx: vbu.Context):
        """
        The parent command for handling git repo aliases.
        """

        if ctx.invoked_subcommand is None:
            return await ctx.send_help(ctx.command)

    @repoalias.command(
        name="add",
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="alias",
                    description="The user-readable name of the repo.",
                    type=discord.ApplicationCommandOptionType.string,
                ),
                discord.ApplicationCommandOption(
                    name="repo",
                    description="A link to the repo that you want to alias.",
                    type=discord.ApplicationCommandOptionType.string,
                ),
            ],
        ),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def repoalias_add(self, ctx: vbu.Context, alias: str, repo: GitRepo):
        """
        Add a Github repo alias to the database.
        """

        async with vbu.Database() as db:
            try:
                await db(
                    """INSERT INTO github_repo_aliases (alias, owner, repo, host, added_by)
                    VALUES (LOWER($1), $2, $3, $4, $5)""",
                    alias, repo.owner, repo.repo, repo.host, ctx.author.id,
                )
            except asyncpg.UniqueViolationError:
                data = await db("SELECT * FROM github_repo_aliases WHERE alias=LOWER($1) AND added_by=$2", alias, ctx.author.id)
                if not data:
                    return await ctx.send(
                        f"The alias `{alias.lower()}` is already in use.",
                        allowed_mentions=discord.AllowedMentions.none(),
                    )
                await db("DELETE FROM github_repo_aliases WHERE alias=$1", alias)
                return await self.repoalias_add(ctx, alias, repo)
        await ctx.send("Done.")

    @repoalias.command(
        name="remove",
        aliases=['delete', 'del', 'rem'],
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="alias",
                    description="The alias that you want to remove.",
                    type=discord.ApplicationCommandOptionType.string,
                ),
            ],
        ),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def repoalias_remove(self, ctx: vbu.Context, alias: str):
        """
        Removes a Github repo alias from the database.
        """

        async with vbu.Database() as db:
            data = await db(
                "SELECT * FROM github_repo_aliases WHERE alias=LOWER($1) AND added_by=$2",
                alias,
                ctx.author.id,
            )
            if not data:
                return await ctx.send(
                    "You don't own that repo alias.",
                    allowed_mentions=discord.AllowedMentions.none(),
                )
            await db("DELETE FROM github_repo_aliases WHERE alias=LOWER($1)", alias)
        await ctx.send("Done.")

    @commands.group(
        aliases=['issues'],
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def issue(self, ctx: vbu.Context):
        """
        The parent group for the git issue commands.
        """

        if ctx.invoked_subcommand is None:
            return await ctx.send_help(ctx.command)

    # @issue.command(name='frommessage', context_command_type=vbu.ApplicationCommandType.MESSAGE, context_command_name="Create issue from VBU webhook")
    # @commands.bot_has_permissions(send_messages=True, embed_links=True)
    # async def issue_frommessage(self, ctx: vbu.Context, message: discord.Message):
    #     """
    #     Create a Github issue from a VBU error webhook.
    #     """

    #     # Run some checks
    #     if message.author.discriminator != "0000":
    #         return await ctx.send("That message wasn't sent by a webhook.")
    #     author_split = message.author.name.split("-")
    #     if author_split[-1].strip() != "Error":
    #         return await ctx.send("That message wasn't sent by a VBU error webhook.")

    #     # Build up our content
    #     repo_str = "-".join(author_split[:-1]).strip()
    #     repo = await GitRepo.convert(ctx, repo_str)
    #     body_match = VBU_ERROR_WEBHOOK_PATTERN.search(message.content)
    #     title = f"Issue encountered running `{body_match.group('command_invoke').strip()}` command"
    #     async with self.bot.session.get(message.attachments[0].url) as r:
    #         error_file = await r.text()
    #     body = (
    #         f"The bot hit a `{body_match.group('error').strip()}` error while running the `{body_match.group('command_invoke')}` "
    #         f"command.\n```python\n{error_file.strip()}\n```"
    #     )
    #     return await ctx.invoke(self.issue_create, repo, title=title, body=body)

    @issue.command(
        name='create',
        aliases=['make'],
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="repo",
                    description="The repo that you want to create an issue on.",
                    type=discord.ApplicationCommandOptionType.string,
                    autocomplete=True,
                ),
                discord.ApplicationCommandOption(
                    name="title",
                    description="The title of the issue that you want to make.",
                    type=discord.ApplicationCommandOptionType.string,
                ),
                discord.ApplicationCommandOption(
                    name="body",
                    description="Any body text for the issue.",
                    type=discord.ApplicationCommandOptionType.string,
                    required=False,
                ),
            ],
        ),
    )
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def issue_create(self, ctx: vbu.Context, repo: GitRepo, *, title: str, body: str = ""):
        """
        Create a Github issue on a given repo.
        """

        # Get the database because whatever why not
        async with vbu.Database() as db:
            user_rows = await db("SELECT * FROM user_settings WHERE user_id=$1", ctx.author.id)
            if not user_rows or not user_rows[0][f'{repo.host.lower()}_username']:
                return await ctx.send(
                    (
                        f"You need to link your {repo.host} account to Discord to run this "
                        f"command - see the website at `{ctx.clean_prefix}info`."
                    ),
                )

        # Work out what components we want to use
        embed = vbu.Embed(title=title, description=body, use_random_colour=True).set_footer(text=str(repo))
        components = discord.ui.MessageComponents.boolean_buttons()
        components.components[0].components.append(discord.ui.Button(label="Set title", custom_id="TITLE"))
        components.components[0].components.append(discord.ui.Button(label="Set body", custom_id="BODY"))
        components.components[0].components.append(discord.ui.Button(label="Set repo", custom_id="REPO"))
        options = [
            discord.ui.SelectOption(label=i.name, value=i.name, description=i.description)
            for i in await repo.get_labels(user_rows[0])
        ]
        components.add_component(discord.ui.ActionRow(
            discord.ui.SelectMenu(
                custom_id="LABELS",
                min_values=0,
                max_values=len(options),
                options=options,
            )
        ))
        labels = []

        # Ask if we want to do this
        m = None
        while True:

            # See if we want to update the body
            embed = vbu.Embed(
                title=title,
                description=body or "...",
                use_random_colour=True,
            ).set_footer(
                text=str(repo),
            ).add_field(
                "Labels",
                ", ".join([f"`{i}`" for i in labels]) or "...",
            )
            if m is None:
                m = await ctx.send("Are you sure you want to create this issue?", embed=embed, components=components)
            else:
                await m.edit(embed=embed, components=components.enable_components())
            try:
                payload = await self.bot.wait_for("component_interaction", check=vbu.component_check(ctx.author, m), timeout=120)
            except asyncio.TimeoutError:
                return await ctx.send("Timed out asking for issue create confirmation.")

            # Disable components
            if payload.component.custom_id not in ["LABELS"]:
                await payload.response.edit_message(components=components.disable_components())

            # Get the body
            if payload.component.custom_id == "BODY":

                # Wait for their body message
                n = await payload.followup.send("What body content do you want to be added to your issue?")
                try:
                    check = lambda n: n.author.id == ctx.author.id and n.channel.id == ctx.channel.id
                    body_message = await self.bot.wait_for("message", check=check, timeout=60 * 5)
                except asyncio.TimeoutError:
                    return await payload.followup.send("Timed out asking for issue body text.")

                # Grab the attachments
                attachment_urls = []
                for i in body_message.attachments:
                    try:
                        async with self.bot.session.get(i.url) as r:
                            data = await r.read()
                        file = discord.File(io.BytesIO(data), filename=i.filename)
                        cache_message = await ctx.author.send(file=file)
                        attachment_urls.append((file.filename, cache_message.attachments[0].url))
                    except discord.HTTPException:
                        break

                # Delete their body message and our asking message
                try:
                    await n.delete()
                    await body_message.delete()
                except discord.HTTPException:
                    pass

                # Fix up the body
                body = body.strip() + "\n\n" + body_message.content + "\n\n"
                for name, url in attachment_urls:
                    body += f"![{name}]({url})\n"

            # Get the title
            elif payload.component.custom_id == "TITLE":

                # Wait for their body message
                n = await payload.followup.send("What do you want to set the issue title to?")
                try:
                    check = lambda n: n.author.id == ctx.author.id and n.channel.id == ctx.channel.id
                    title_message = await self.bot.wait_for("message", check=check, timeout=60 * 5)
                except asyncio.TimeoutError:
                    return await payload.followup.send("Timed out asking for issue title text.")

                # Delete their body message and our asking message
                try:
                    await n.delete()
                    await title_message.delete()
                except discord.HTTPException:
                    pass
                title = title_message.content

            # Get the repo
            elif payload.component.custom_id == "REPO":

                # Wait for their body message
                n = await payload.followup.send("What do you want to set the repo to?")
                try:
                    check = lambda n: n.author.id == ctx.author.id and n.channel.id == ctx.channel.id
                    repo_message = await self.bot.wait_for("message", check=check, timeout=60 * 5)
                except asyncio.TimeoutError:
                    return await payload.followup.send("Timed out asking for issue title text.")

                # Delete their body message and our asking message
                try:
                    await n.delete()
                    await repo_message.delete()
                except discord.HTTPException:
                    pass

                # Edit the message
                try:
                    repo = await GitRepo.convert(ctx, repo_message.content)
                except Exception:
                    await ctx.send(f"That repo isn't valid, {ctx.author.mention}", delete_after=3)

            # Get the labels
            elif payload.component.custom_id == "LABELS":
                await payload.response.defer_update()
                labels = payload.values

            # Check for exiting
            elif payload.component.custom_id == "NO":
                return await payload.followup.send("Alright, cancelling issue add.")
            elif payload.component.custom_id == "YES":
                break

        # Work out our args
        headers = {}
        if repo.host == "Github":
            json = {'title': title, 'body': body.strip(), 'labels': labels}
            headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f"token {user_rows[0]['github_access_token']}"}
        elif repo.host == "Gitlab":
            json = {'title': title, 'description': body.strip(), 'labels': ",".join(labels)}
            headers = {'Authorization': f"Bearer {user_rows[0]['gitlab_bearer_token']}"}
        else:
            raise Exception("Invalid host")
        headers.update({'User-Agent': self.bot.user_agent})

        # Make the post request
        async with self.bot.session.post(repo.issue_api_url, json=json, headers=headers) as r:
            data = await r.json()
            self.logger.info(f"Received data from git {r.url!s} - {data!s}")
            if not r.ok:
                return await ctx.send(f"I was unable to create an issue on that repository - `{data}`.",)
        await ctx.send(f"Your issue has been created - <{data.get('html_url') or data.get('web_url')}>.",)
        await self.increase_repo_usage_counter(ctx.author, repo)

    @issue_create.autocomplete
    async def issue_create_autocomplete(self, ctx: commands.SlashContext, interaction: discord.Interaction):
        """
        Send the user's most frequently used repos.
        """

        if not interaction.user:
            return await interaction.response.send_autocomplete(None)
        async with vbu.Database() as db:
            rows = await db(
                """SELECT * FROM github_repo_uses WHERE user_id=$1 ORDER BY uses DESC""",
                interaction.user.id,
            )
        responses = [
            discord.ApplicationCommandOptionChoice(name=(repo := str(GitRepo(r['host'], r['owner'], r['repo']))), value=repo)
            for r in rows
        ]
コード例 #16
0
class MiscCommands(vbu.Cog):
    def __init__(self, bot: vbu.Bot):
        super().__init__(bot)
        self.button_message_locks = collections.defaultdict(asyncio.Lock)

    @commands.group(
        aliases=['topics'],
        invoke_without_command=False,
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def topic(self, ctx: vbu.Context):
        """
        The parent group for the topic commands.
        """

        async with vbu.Database() as db:
            rows = await db("SELECT * FROM topics ORDER BY RANDOM() LIMIT 1")
        if not rows:
            return await ctx.send(
                "There aren't any topics set up in the database for this bot :<"
            )
        return await ctx.send(rows[0]['topic'])

    @topic.command(
        name="get",
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.bot_has_permissions(send_messages=True)
    async def topic_get(self, ctx: vbu.Context):
        """
        Gives you a conversation topic.
        """

        await self.topic(ctx)

    # @topic.command(
    #     name="add",
    #     application_command_meta=commands.ApplicationCommandMeta(
    #         options=[
    #             discord.ApplicationCommandOption(
    #                 name="topic",
    #                 description="",
    #                 type=discord.ApplicationCommandOptionType.string,
    #             ),
    #         ],
    #     ),
    # )
    # @vbu.checks.is_bot_support()
    # @commands.bot_has_permissions(send_messages=True)
    # async def topic_add(self, ctx: vbu.Context, *, topic: str):
    #     """
    #     Add a new topic to the database.
    #     """

    #     async with vbu.Database() as db:
    #         await db("INSERT INTO topics VALUES ($1)", topic)
    #     return await ctx.send("Added to database.")

    @commands.command(
        application_command_meta=commands.ApplicationCommandMeta())
    @commands.bot_has_permissions(send_messages=True)
    async def coinflip(self, ctx: vbu.Context):
        """
        Flips a coin.
        """

        coin = ["Heads", "Tails"]
        return await ctx.send(random.choice(coin))

    # @commands.command(aliases=['http'])
    # @commands.cooldown(1, 5, commands.BucketType.channel)
    # async def httpcat(self, ctx: vbu.Context, errorcode: str):
    #     """
    #     Gives you a cat based on an HTTP error code.
    #     """

    #     standard_errorcodes = [error.value for error in http.HTTPStatus]

    #     if errorcode in ('random', 'rand', 'r'):
    #         errorcode = random.choice(standard_errorcodes)
    #     else:
    #         try:
    #             errorcode = int(errorcode)
    #         except ValueError:
    #             return ctx.channel.send('Converting to "int" failed for parameter "errorcode".')

    #     await ctx.trigger_typing()
    #     headers = {"User-Agent": self.bot.user_agent}
    #     async with self.bot.session.get(f"https://http.cat/{errorcode}", headers=headers) as r:
    #         if r.status == 404:
    #             if errorcode not in standard_errorcodes:
    #                 await ctx.send("That HTTP code doesn't exist.")
    #             else:
    #                 await ctx.send('Image for HTTP code not found on provider.')
    #             return
    #         if r.status != 200:
    #             await ctx.send(f'Something went wrong, try again later. ({r.status})')
    #             return
    #     with vbu.Embed(use_random_colour=True) as embed:
    #         embed.set_image(url=f'https://http.cat/{errorcode}')
    #     await ctx.send(embed=embed)

    # @commands.command()
    # @commands.cooldown(1, 5, commands.BucketType.channel)
    # async def httpdog(self, ctx: vbu.Context, errorcode: str):
    #     """
    #     Gives you a dog based on an HTTP error code.
    #     """

    #     standard_errorcodes = [error.value for error in http.HTTPStatus]

    #     if errorcode in ('random', 'rand', 'r'):
    #         errorcode = random.choice(standard_errorcodes)
    #     else:
    #         try:
    #             errorcode = int(errorcode)
    #         except ValueError:
    #             return ctx.channel.send('Converting to "int" failed for parameter "errorcode".')

    #     await ctx.trigger_typing()
    #     headers = {"User-Agent": self.bot.user_agent}
    #     async with self.bot.session.get(
    #             f"https://httpstatusdogs.com/img/{errorcode}.jpg", headers=headers, allow_redirects=False) as r:
    #         if str(r.status)[0] != "2":
    #             if errorcode not in standard_errorcodes:
    #                 await ctx.send("That HTTP code doesn't exist.")
    #             else:
    #                 await ctx.send('Image for HTTP code not found on provider.')
    #             return
    #     with vbu.Embed(use_random_colour=True) as embed:
    #         embed.set_image(url=f'https://httpstatusdogs.com/img/{errorcode}.jpg')
    #     await ctx.send(embed=embed, wait=False)

    @commands.command(
        aliases=['color'],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="colour",
                description="The colour that you want to post.",
                type=discord.ApplicationCommandOptionType.string,
            ),
        ], ),
    )
    @commands.bot_has_permissions(send_messages=True, embed_links=True)
    async def colour(
        self,
        ctx: vbu.Context,
        *,
        colour: typing.Union[vbu.converters.ColourConverter, discord.Colour,
                             discord.Role, discord.Member],
    ):
        """
        Get you a colour.
        """

        # https://www.htmlcsscolor.com/preview/gallery/5dadec.png
        if isinstance(colour, discord.Role):
            colour = colour.colour
        elif isinstance(colour, discord.Member):
            try:
                colour = [i for i in colour.roles
                          if i.colour.value > 0][-1].colour
            except IndexError:
                colour = discord.Colour(0)
        hex_colour = colour.value
        with vbu.Embed(colour=hex_colour,
                       title=f"#{hex_colour:0>6X}") as embed:
            embed.set_image(
                url=
                f"https://www.htmlcsscolor.com/preview/gallery/{hex_colour:0>6X}.png"
            )
        await ctx.send(embed=embed)

    @commands.command(
        aliases=['disconnectvc', 'emptyvc'],
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="channel",
                description="The VC that you want to clear.",
                type=discord.ApplicationCommandOptionType.channel,
                channel_types=[discord.ChannelType.voice],
            ),
        ], ),
    )
    @commands.has_guild_permissions(move_members=True)
    @commands.bot_has_guild_permissions(move_members=True)
    @commands.bot_has_permissions(send_messages=True)
    async def clearvc(self, ctx: vbu.Context, channel: discord.VoiceChannel):
        """
        Removes all the people from a given VC.
        """

        if not channel.members:
            return await ctx.send(
                "There are no people in that VC for me to remove.")
        member_count = len(channel.members)
        await ctx.defer()
        for member in channel.members:
            try:
                await member.edit(voice_channel=None)
            except discord.Forbidden:
                return await ctx.send(
                    "I don't have permission to remove members from that channel."
                )
        return await ctx.send(f"Dropped {member_count} members from the VC.")
コード例 #17
0
ファイル: role_picker.py プロジェクト: Voxel-Fox-Ltd/Apple.Py
class RolePicker(vbu.Cog[vbu.Bot]):

    @commands.group(
        application_command_meta=commands.ApplicationCommandMeta(
            guild_only=True,
            permissions=discord.Permissions(manage_roles=True, manage_guild=True),
        ),
    )
    @commands.is_slash_command()
    async def rolepicker(self, _: vbu.SlashContext):
        pass

    @rolepicker.command(
        name="create",
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="name",
                    type=discord.ApplicationCommandOptionType.string,
                    description="The name of your role picker. Only you can see this.",
                ),
                discord.ApplicationCommandOption(
                    name="text",
                    type=discord.ApplicationCommandOptionType.string,
                    description="The text you want to be displayed above the dropdown.",
                ),
                discord.ApplicationCommandOption(
                    name="channel",
                    type=discord.ApplicationCommandOptionType.channel,
                    description="The place you want to create the role picker.",
                    channel_types=[discord.ChannelType.text],
                    required=False,
                ),
                discord.ApplicationCommandOption(
                    name="min_roles",
                    type=discord.ApplicationCommandOptionType.integer,
                    description="The minimum number of roles that the user can have.",
                    min_value=0,
                    required=False,
                ),
                discord.ApplicationCommandOption(
                    name="max_roles",
                    type=discord.ApplicationCommandOptionType.integer,
                    description="The maximum number of roles that the user can have.",
                    max_value=25,
                    required=False,
                ),
            ],
            guild_only=True,
            permissions=discord.Permissions(manage_roles=True, manage_guild=True),
        ),
    )
    @commands.is_slash_command()
    async def rolepicker_create(
            self, 
            ctx: vbu.SlashContext,
            name: str,
            text: str,
            channel: Optional[discord.TextChannel] = None,
            min_roles: Optional[int] = None,
            max_roles: Optional[int] = None):
        """
        Create a new role picker message.
        """

        # Make sure the things they gave are valid
        if min_roles and max_roles and max_roles < min_roles:
            return await ctx.interaction.response.send_message(
                "The maximum cannot be a higher number than the minimum.",
                ephemeral=True,
            )

        # Only work if they can send messages in that channel
        channel = channel or ctx.channel  # type: ignore - will be a TextChannel, I guess
        assert channel
        author: discord.Member = ctx.author  # type: ignore - will be a member
        assert author
        if not channel.permissions_for(author).send_messages:
            return await ctx.interaction.response.send_message(
                "You cannot send messages into that channel.",
                ephemeral=True,
            )

        # Send message
        await ctx.interaction.response.defer(ephemeral=True)
        component_id = str(uuid4())
        message = await channel.send(
            text,
            components=discord.ui.MessageComponents(
                discord.ui.ActionRow(
                    discord.ui.SelectMenu(
                        custom_id=f"ROLEPICKER {component_id}",
                        options=[
                            discord.ui.SelectOption(
                                label="No roles have been added :(",
                                value="NULL",
                            ),
                        ],
                    ),
                ),
            ),
        )

        # Send and store
        async with vbu.Database() as db:
            await db.call(
                """
                INSERT INTO
                    role_pickers
                    (
                        guild_id,
                        name,
                        message_id,
                        channel_id,
                        component_id,
                        min_roles,
                        max_roles
                    )
                VALUES
                    (
                        $1,  -- guild_id
                        $2,  -- name
                        $3,  -- message_id
                        $4,  -- channel_id
                        $5,  -- component_id
                        $6,  -- min_roles
                        $7  -- max_role
                    )
                """,
                ctx.guild.id, name, message.id,
                message.channel.id, component_id,
                min_roles, max_roles,
            )

        # And tell them about it
        await ctx.interaction.followup.send(
            "Created role picker!~",
            ephemeral=True,
        )

    @rolepicker.command(
        name="delete",
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="name",
                    type=discord.ApplicationCommandOptionType.string,
                    description="The name of your role picker.",
                    autocomplete=True,
                ),
            ],
            guild_only=True,
            permissions=discord.Permissions(manage_roles=True, manage_guild=True),
        ),
    )
    @commands.is_slash_command()
    async def rolepicker_delete(
            self,
            ctx: vbu.SlashContext,
            name: str):
        """
        Remove a role picker message.
        """

        # Defer so we can database call
        await ctx.interaction.response.defer(ephemeral=True)

        # Delete stored info
        async with vbu.Database() as db:
            role_picker_rows = await db.call(
                """
                DELETE FROM
                    role_pickers
                WHERE
                    guild_id = $1
                AND
                    name = $2
                RETURNING *
                """,
                ctx.guild.id, name,
            )

        # See if we can delete the message as well
        row = role_picker_rows[0]
        messageable = self.bot.get_partial_messageable(
            row['channel_id'],
            type=discord.ChannelType.text,
        )
        message = messageable.get_partial_message(row['message_id'])
        try:
            await message.delete()
        except:
            pass

        # And tell them about it
        await ctx.interaction.followup.send(
            "Deleted role picker.",
            ephemeral=True,
        )

    @rolepicker.command(
        name="add",
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="name",
                    type=discord.ApplicationCommandOptionType.string,
                    description="The name of the role picker that you want to modify.",
                    autocomplete=True,
                ),
                discord.ApplicationCommandOption(
                    name="role",
                    type=discord.ApplicationCommandOptionType.role,
                    description="The role you want to add to the role picker.",
                ),
            ],
            guild_only=True,
            permissions=discord.Permissions(manage_roles=True, manage_guild=True),
        ),
    )
    @commands.is_slash_command()
    async def rolepicker_add(
            self,
            ctx: vbu.SlashContext,
            name: str,
            role: discord.Role):
        """
        Add a new role to a role picker.
        """

        # Defer so we can fetch
        await ctx.interaction.response.defer(ephemeral=True)

        # Fetch some values from the API
        author: discord.Member
        author = await ctx.guild.fetch_member(ctx.author.id)  # type: ignore - author will definitely exist
        guild: discord.Guild = ctx.guild
        guild_roles = guild.roles

        # Make sure the role they gave is lower than their top
        author_top_role = [i for i in guild_roles if i.id == author.roles[-1].id][0]
        role = [i for i in guild_roles if i.id == role.id][0]
        if author_top_role < role:
            return await ctx.interaction.followup.send(
                "Your top role is below the one you're trying to manage.",
                ephemeral=True,
            )

        # Add that role to the database
        async with vbu.Database() as db:
            await db.call(
                """
                INSERT INTO
                    role_picker_role
                    (
                        guild_id,
                        name,
                        role_id
                    )
                VALUES
                    (
                        $1,  -- guild_id
                        $2,  -- name
                        $3  -- role_id
                    )
                ON CONFLICT (guild_id, name, role_id)
                DO NOTHING
                """,
                guild.id, name, role.id,
            )

        # Tell the user it's done
        await ctx.interaction.followup.send(
            "Added role to role picker!~",
            ephemeral=True,
        )
        self.bot.dispatch("role_picker_update", guild, name)

    @rolepicker.command(
        name="remove",
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="name",
                    type=discord.ApplicationCommandOptionType.string,
                    description="The name of the role picker that you want to modify.",
                    autocomplete=True,
                ),
                discord.ApplicationCommandOption(
                    name="role",
                    type=discord.ApplicationCommandOptionType.string,
                    description="The role you want to remove from the role picker.",
                ),
            ],
            guild_only=True,
            permissions=discord.Permissions(manage_roles=True, manage_guild=True),
        ),
    )
    @commands.is_slash_command()
    async def rolepicker_remove(
            self,
            ctx: vbu.SlashContext,
            name: str,
            role: discord.Role):
        """
        Remove a role from one of your role pickers.
        """

        # Defer so we can fetch
        await ctx.interaction.response.defer(ephemeral=True)

        # Remove that role from the database
        async with vbu.Database() as db:
            removed_role = await db.call(
                """
                DELETE FROM
                    role_picker_role
                WHERE
                    guild_id = $1
                AND
                    name = $2
                AND
                    role_id = $3
                RETURNING *
                """,
                ctx.guild.id, name, role.id,
            )

        # See if anything was removed
        if removed_role:
            await ctx.interaction.followup.send(
                "Removed role from role picker.",
                ephemeral=True,
            )
        else:
            await ctx.interaction.followup.send(
                "That role wasn't in the picker.",
                ephemeral=True,
            )
            return
        self.bot.dispatch("role_picker_update", ctx.guild, name)

    @vbu.Cog.listener()
    async def on_role_picker_update(
            self,
            guild: Union[discord.abc.Snowflake, discord.Guild],
            role_picker_name_or_component_id: str):
        """
        Update one of the published role pickers.
        """

        # Get the current data
        async with vbu.Database() as db:
            role_rows = await db.call(
                """
                SELECT
                    role_pickers.message_id,
                    role_pickers.channel_id,
                    role_pickers.component_id,
                    role_pickers.min_roles,
                    role_pickers.max_roles,
                    role_picker_role.role_id
                FROM
                    role_picker_role
                LEFT JOIN
                    role_pickers
                ON
                    role_picker_role.guild_id = role_pickers.guild_id
                AND
                    role_picker_role.name = role_pickers.name
                WHERE
                    role_picker_role.guild_id = $1
                AND
                    (
                        role_pickers.name = $2
                        OR
                        role_pickers.component_id = $2
                    )
                """,
                guild.id, role_picker_name_or_component_id,
            )
            if not role_rows:
                role_rows = await db.call(
                    """
                    SELECT
                        message_id,
                        channel_id,
                        component_id,
                        min_roles,
                        max_roles
                    FROM
                        role_pickers
                    WHERE
                        guild_id = $1
                    AND
                        (
                            name = $2
                            OR
                            component_id = $2
                        )
                    """,
                    guild.id, role_picker_name_or_component_id,
                )

        # Get a partial message we can edit
        messageable = self.bot.get_partial_messageable(
            role_rows[0]['channel_id'],
            type=discord.ChannelType.text,
        )
        message = messageable.get_partial_message(role_rows[0]['message_id'])

        # Get the roles
        guild_roles: List[discord.Role]
        if isinstance(guild, discord.Guild):
            guild_roles = await guild.fetch_roles()
        else:
            guild = await self.bot.fetch_guild(guild.id)
            guild_roles = guild.roles

        # Create the menu options
        menu_options: List[discord.ui.SelectOption] = [
            discord.ui.SelectOption(
                label="No roles have been added :(",
                value="NULL",
            ),
        ]
        if "role_id" in role_rows[0]:
            menu_options = [
                discord.ui.SelectOption(
                    label=role.name,
                    value=i['role_id'],
                )
                for i in role_rows
                if (role := discord.utils.get(guild_roles, id=i['role_id']))
            ]

        # Edit the message
        row = role_rows[0]
        await message.edit(
            components=discord.ui.MessageComponents(
                discord.ui.ActionRow(
                    discord.ui.SelectMenu(
                        custom_id=f"ROLEPICKER {row['component_id']}",
                        options=menu_options[:25],
                        min_values=row['min_roles'],
                        max_values=row['max_roles'],
                    ),
                ),
            ),
        )

    @vbu.Cog.listener()
    async def on_component_interaction(
            self,
            interaction: discord.Interaction[str]):
        """
        Listen for a rolepicker component being clicked and manage
        that for the user.
        """

        # Make sure it's a rolepicker component
        if not interaction.custom_id.startswith("ROLEPICKER"):
            return
        component_id = interaction.custom_id.split(" ")[1]

        # See what they selected
        picked_role_ids = [int(i) for i in interaction.values if i != "NULL"]  # type: ignore - interaction values won't be none here
        if not picked_role_ids:
            return await interaction.response.defer_update()
        await interaction.response.defer(ephemeral=True)
        guild_id: int = interaction.guild_id  # type: ignore - this will be run in a guild
        guild: discord.Guild = interaction.guild or await self.bot.fetch_guild(guild_id)
        picked_roles = [i for i in guild.roles if i.id in picked_role_ids]

        # See if they have any of those roles currently
        user: discord.Member = interaction.user  # type: ignore - user is definitely a member object
        user_roles: List[discord.Role] = user.roles
        roles_they_have_currently = [i for i in user_roles if i in picked_roles]

        # See if they have ALL of the roles they picked
        if len(roles_they_have_currently) == len(picked_roles):
            try:
                await user.remove_roles(*picked_roles, reason="Role picker")
            # except discord.NotFound:
            #     await interaction.followup.send(
            #         "I can't find one of the roles you picked in the server any more.",
            #         ephemeral=True,
            #     )
            #     self.bot.dispatch("role_picker_update", guild, component_id)
            #     return
            except discord.Forbidden:
                await interaction.followup.send(
                    "I'm unable to remove that role from you.",
                    ephemeral=True,
                )
                return
            await interaction.followup.send(
                vbu.format("Removed {0} {0:plural,role,roles} from you.", len(picked_roles)),
                ephemeral=True,
            )
            return

        # If not then we'll just add them to the user
        try:
            await user.add_roles(*picked_roles, reason="Role picker")
        except discord.NotFound:
            await interaction.followup.send(
                "I can't find one of the roles you picked in the server any more.",
                ephemeral=True,
            )
            self.bot.dispatch("role_picker_update", guild, component_id)
            return
        except discord.Forbidden:
            await interaction.followup.send(
                "I'm unable to add that role to you.",
                ephemeral=True,
            )
            return
        await interaction.followup.send(
            vbu.format("Added {0} {0:plural,role,roles} to you.", len(picked_roles)),
            ephemeral=True,
        )
        return

    @rolepicker_add.autocomplete
    @rolepicker_remove.autocomplete
    @rolepicker_delete.autocomplete
    async def rolepicker_name_autocomplete(
            self,
            ctx: vbu.SlashContext,
            interaction: discord.Interaction):
        """
        Handle autocompletes for rolepicker names.
        """

        async with vbu.Database() as db:
            role_picker_rows = await db.call(
                """
                SELECT
                    name
                FROM
                    role_pickers
                WHERE
                    guild_id = $1
                AND
                    name LIKE '%' || $2 || '%'
                """,
                interaction.guild_id,
                interaction.options[0].options[0].value,
            )
        return await interaction.response.send_autocomplete([
            discord.ApplicationCommandOptionChoice(name=i['name'], value=i['name'])
            for i in role_picker_rows
        ])
コード例 #18
0
class SteamCommand(vbu.Cog):

    ALL_GAMES_URL = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
    GAME_DATA_URL = "https://store.steampowered.com/api/appdetails"
    GAME_URL_REGEX = re.compile(r"https:\/\/store\.steampowered\.com\/app\/(\d+)")

    def __init__(self, bot: vbu.Bot):
        super().__init__(bot)
        self.game_cache: dict = {}
        self.sent_message_cache = {}  # MessageID: {embed: Embed, index: ScreenshotIndex, screenshots: List[str]}

    async def load_game_cache(self):
        """
        Loads the games from Steam into cache.
        """

        params = {
            "key": self.bot.config['api_keys']['steam']
        }
        headers = {
            "User-Agent": self.bot.user_agent
        }
        async with self.bot.session.get(self.ALL_GAMES_URL, params=params, headers=headers) as r:
            data = await r.json()
        apps = data['applist']['apps']
        self.game_cache = apps

    def get_valid_name(self, name):
        return ''.join(i for i in name if i.isdigit() or i.isalpha() or i.isspace())

    @commands.command(
        aliases=['steamsearch'],
        application_command_meta=commands.ApplicationCommandMeta(
            options=[
                discord.ApplicationCommandOption(
                    name="app_name",
                    description="The name of the game that you want to search for.",
                    type=discord.ApplicationCommandOptionType.string,
                ),
            ],
        ),
    )
    @commands.defer()
    @vbu.checks.is_config_set('api_keys', 'steam')
    async def steam(self, ctx: vbu.Context, *, app_name: str):
        """
        Search Steam for an item.
        """

        # Load cache
        if not self.game_cache:
            await self.load_game_cache()

        # Get app
        app_object = None
        appid = None
        await ctx.trigger_typing()

        # By url
        match = self.GAME_URL_REGEX.search(app_name)
        if match is not None:
            app_name = match.group(1)

        # By app id
        if app_name.isdigit():
            appid = int(app_name)
            try:
                app_object = [i for i in self.game_cache if i['appid'] == int(app_name)][0]
            except IndexError:
                pass

        # By app name
        if app_object is None and appid is None:
            app_name = self.get_valid_name(app_name)
            valid_items = [i for i in self.game_cache if app_name.lower() in self.get_valid_name(i['name']).lower()]
            full_title_match = [i for i in valid_items if app_name.lower() == self.get_valid_name(i['name']).lower()]
            if full_title_match:
                valid_items = [full_title_match[0]]
            if len(valid_items) > 1:
                output_items = valid_items[:10]
                output_ids = [f"`{i['appid']}` - {i['name']}" for i in output_items]
                return await ctx.send("There are multiple results with that name:\n" + "\n".join(output_ids))  # TODO
            elif len(valid_items) == 0:
                return await ctx.send("There are no results with that name.")
            app_object = valid_items[0]
            appid = app_object['appid']

        # Get info
        params = {
            "appids": appid
        }
        headers = {
            "User-Agent": self.bot.user_agent
        }
        async with self.bot.session.get(self.GAME_DATA_URL, params=params, headers=headers) as r:
            game_data = await r.json()
        if game_data[str(appid)]['success'] is False:
            return await ctx.send(f"I couldn't find an application with ID `{appid}`.")
        game_object = game_data[str(appid)]['data']

        # See if it's NSFW
        if int(game_object.get('required_age', '0')) >= 18 and ctx.channel.nsfw is False:
            return await ctx.send("That game is marked as an 18+, so can't be sent in a non-NSFW channel.")

        # Embed it babey
        with vbu.Embed(use_random_colour=True) as embed:
            embed.title = game_object['name']
            embed.set_footer(text=f"AppID: {appid}")
            embed.description = game_object['short_description']
            embed.add_field("Developer", ', '.join(game_object.get('developers', list())) or 'None', inline=True)
            embed.add_field("Publisher", ', '.join(game_object.get('publishers', list())) or 'None', inline=True)
            embed.add_field("Genre", ', '.join(i['description'] for i in game_object['genres']) or 'None', inline=True)
            if game_object.get('price_overview') is not None:
                initial_price = game_object['price_overview']['initial_formatted']
                final_price = game_object['price_overview']['final_formatted']
                embed.add_field("Price", f"~~{initial_price}~~ {final_price}" if initial_price else final_price, inline=True)
            embed.add_field("Link", f"Open with Steam - steam://store/{appid}\nOpen in browser - [Link](https://store.steampowered.com/app/{appid}/)", inline=False)
            screenshots = [i['path_full'] for i in game_object['screenshots']]
            embed.set_image(url=screenshots[0])

        # Send
        m = await ctx.send(embed=embed)
コード例 #19
0
class NicknameHandler(vbu.Cog):

    LETTER_REPLACEMENT_FILE_PATH = "config/letter_replacements.json"
    ASCII_CHARACTERS = string.ascii_letters + string.digits + string.punctuation

    def __init__(self, bot: vbu.Bot):
        super().__init__(bot)
        self.animal_names = None
        self.letter_replacements = None
        self.consecutive_character_regex = None

    async def get_animal_names(self):
        """
        Grabs all names from the Github page.
        """

        if self.animal_names:
            return self.animal_names
        headers = {"User-Agent": self.bot.user_agent}
        async with self.bot.session.get(ANIMAL_NAMES, headers=headers) as r:
            text = await r.text()
        self.animal_names = text.strip().split('\n')
        return await self.get_animal_names()

    def get_letter_replacements(self):
        """
        Grabs all names from the Github page.
        """

        if self.letter_replacements:
            return self.letter_replacements
        with open(self.LETTER_REPLACEMENT_FILE_PATH) as a:
            self.letter_replacements = json.load(a)
        for i in string.ascii_letters + string.punctuation + string.digits + string.whitespace:
            self.letter_replacements[i] = i
        return self.get_letter_replacements()

    @vbu.Cog.listener()
    async def on_member_join(self, member: discord.Member):
        """
        Pings a member nickname update on member join.
        """

        # Check if they are a bot
        if member.bot:
            return

        # See if they have a permanent nickname set
        async with vbu.Database() as db:
            data = await db(
                """SELECT nickname FROM permanent_nicknames WHERE guild_id=$1 AND user_id=$2""",
                member.guild.id, member.id)
        if data:
            try:
                await member.edit(nick=data[0]["nickname"],
                                  reason="Changed by Apple.Py automagically")
                self.logger.info(
                    f"Set permanent nickname of {member.id} in {member.guild.id} from member join"
                )
            except discord.Forbidden as e:
                self.logger.error(
                    f"Couldn't set permanent nickname of {member.id} in {member.guild.id} - {e}"
                )
            return

        # See if we want to fun their name
        if self.bot.guild_settings[
                member.guild.id]['automatic_nickname_update']:
            self.logger.info(
                f"Pinging nickname update for member join (G{member.guild.id}/U{member.id})"
            )
            await self.fix_user_nickname(member)

    @vbu.Cog.listener()
    async def on_member_update(self, before: discord.Member,
                               member: discord.Member):
        """
        Pings a member nickname update on nickname update.
        """

        # Only ping if they've change nicknames/usernames
        if before.display_name == member.display_name:
            return

        # Only ping for non-moderators
        if member.guild_permissions.manage_nicknames:
            return

        # Only ping for non-bots
        if member.bot:
            return

        # See if they have a permanent nickname
        async with vbu.Database() as db:
            data = await db(
                """SELECT nickname FROM permanent_nicknames WHERE guild_id=$1 AND user_id=$2""",
                member.guild.id, member.id)
        if data:
            new_nickname = data[0]["nickname"]
            if member.nick == new_nickname:
                return
            try:
                await member.edit(nick=new_nickname,
                                  reason="Changed by Apple.Py automagically")
                self.logger.info(
                    f"Set permanent nickname of {member.id} in {member.guild.id} to '{new_nickname}' from member update"
                )
            except discord.Forbidden as e:
                self.logger.error(
                    f"Couldn't set permanent nickname of {member.id} in {member.guild.id} - {e}"
                )
            return

        # See if they're nickname banned
        if self.bot.guild_settings[
                member.guild.id]['nickname_banned_role_id'] in member._roles:

            # See if their name was changed by an admin
            try:
                async for entry in member.guild.audit_logs(
                        limit=1, action=discord.AuditLogAction.member_update):
                    if entry.target.id == member.id:
                        if entry.user.id != member.id:
                            self.logger.info(
                                f"Not pinging nickname update for a name changed by moderator (G{member.guild.id}/U{member.id})"
                            )
                            return
                        break
            except discord.Forbidden:
                return

            # Change nickname back
            try:
                await member.edit(
                    nick=before.nick or before.name,
                    reason="Changed by Apple.Py due to nickname ban role")
                self.logger.info(
                    f"User {member.id} on guild {member.guild.id} changed nickname - changing back due to nickname ban role"
                )
            except discord.Forbidden as e:
                self.logger.error(
                    f"Can't change user {member.id}'s nickname on guild {member.guild.id} - {e}"
                )
            return

        # See if we want to update their nickname
        if self.bot.guild_settings[
                member.guild.id]['automatic_nickname_update']:

            # See if their name was changed by an admin
            try:
                async for entry in member.guild.audit_logs(
                        limit=1, action=discord.AuditLogAction.member_update):
                    if entry.target.id == member.id:
                        if entry.user.id != member.id:
                            self.logger.info(
                                f"Not pinging nickname update for a name changed by moderator (G{member.guild.id}/U{member.id})"
                            )
                            return
                        break
            except discord.Forbidden:
                pass

            # Fix their name
            self.logger.info(
                f"Pinging nickname update for member update (G{member.guild.id}/U{member.id})"
            )
            await self.fix_user_nickname(member)

    async def fix_user_nickname(self,
                                user: discord.Member,
                                *,
                                force_to_animal: bool = False) -> str:
        """
        Fix the nickname of a user.
        """

        current_name = user.nick or user.name
        new_name = current_name

        # See if we should even bother trying to translate it
        if force_to_animal is False:

            # See if every other character is a space
            if new_name[1::2].strip() == "":
                new_name = new_name[::2]

            # Read the letter replacements file
            replacements = self.get_letter_replacements()

            # Make a translator
            translator = str.maketrans(replacements)

            # Try and fix their name
            new_name_with_zalgo = new_name.translate(translator)
            new_name = ''.join(
                [i for i in new_name_with_zalgo if i not in ZALGO_CHARACTERS])

            # Remove obnoxious exclamation marks to boost to top of member list
            if current_name.startswith("! "):
                new_name = new_name.lstrip("! ")

            # See if they have enough valid characters
            if not self.consecutive_character_regex:
                chars = [
                    i if i not in string.punctuation else f"\\{i}"
                    for i in self.ASCII_CHARACTERS
                ]
                self.consecutive_character_regex = re.compile(
                    f"[{''.join(chars)}]{{3,}}")

        if force_to_animal or self.consecutive_character_regex.search(
                new_name) is None:
            new_name = random.choice(await self.get_animal_names())

        # See if it needs editing
        if current_name == new_name:
            self.logger.info(
                f"Not updating the nickname '{new_name}' (G{user.guild.id}/U{user.id})"
            )
            return new_name

        # Change their name
        self.logger.info(
            f"Updating nickname '{current_name}' to '{new_name}' (G{user.guild.id}/U{user.id})"
        )
        await user.edit(nick=new_name,
                        reason="Changed by Apple.Py automagically")
        return new_name

    @vbu.group(aliases=['fun'], invoke_without_command=True)
    @commands.has_permissions(manage_nicknames=True)
    @commands.bot_has_permissions(manage_nicknames=True)
    @commands.guild_only()
    async def fixunzalgoname(self,
                             ctx: vbu.Context,
                             user: discord.Member,
                             force_to_animal: bool = False):
        """
        Fixes a user's nickname to remove dumbass characters.
        """

        if ctx.invoked_subcommand is not None:
            return
        assert ctx.guild
        user = await ctx.guild.fetch_member(user.id)
        current_name = user.nick or user.name
        new_name = await self.fix_user_nickname(
            user, force_to_animal=force_to_animal)
        return await ctx.send(
            f"Changed their name from `{current_name}` to `{new_name}`.")

    @fixunzalgoname.command(
        name="user",
        application_command_meta=commands.ApplicationCommandMeta(options=[
            discord.ApplicationCommandOption(
                name="user",
                description="The user whose username you want to fix.",
                type=discord.ApplicationCommandOptionType.user,
            ),
        ], ),
    )
    @commands.has_permissions(manage_nicknames=True)
    @commands.bot_has_permissions(manage_nicknames=True)
    @commands.guild_only()
    async def fixunzalgoname_user(self,
                                  ctx: vbu.Context,
                                  user: discord.Member,
                                  force_to_animal: bool = False):
        """
        Fixes a user's nickname to remove dumbass characters.
        """

        await self.fixunzalgoname(ctx, user, force_to_animal)

    @fixunzalgoname.command(name='text')
    @commands.bot_has_permissions(send_messages=True)
    async def fixunzalgoname_text(self, ctx: vbu.Context, *, text: str):
        """
        Fixes a user's nickname to remove dumbass characters.
        """

        # Read the letter replacements file
        replacements = self.get_letter_replacements()

        # Make a translator
        translator = str.maketrans(replacements)

        # Try and fix their name
        new_name_with_zalgo = text.translate(translator)
        new_name = ''.join(
            [i for i in new_name_with_zalgo if i not in ZALGO_CHARACTERS])

        # See if they have enough valid characters
        if not self.consecutive_character_regex:
            chars = [
                i if i not in string.punctuation else f"\\{i}"
                for i in self.ASCII_CHARACTERS
            ]
            self.consecutive_character_regex = re.compile(
                f"[{''.join(chars)}]{{3,}}")

        if self.consecutive_character_regex.search(new_name) is None:
            new_name = random.choice(await self.get_animal_names())

        return await ctx.send(
            f"I would change that from `{text}` to `{new_name}`.")

    @commands.command()
    @vbu.checks.is_bot_support()
    @commands.bot_has_permissions(send_messages=True)
    async def addfixablename(self, ctx: vbu.Context, user: discord.Member, *,
                             fixed_name: str):
        """
        Adds a given user's name to the fixable letters.
        """

        await ctx.invoke(self.bot.get_command("addfixableletters"),
                         user.display_name, fixed_name)
        await ctx.invoke(self.bot.get_command("fixunzalgoname"), user)

    @commands.command(ignore_extra=False)
    @vbu.checks.is_bot_support()
    @commands.bot_has_permissions(send_messages=True)
    async def addfixableletters(self, ctx: vbu.Context, phrase1: str,
                                phrase2: str):
        """
        Adds fixable letters to the replacement list.
        """

        if len(phrase1) != len(phrase2):
            return await ctx.send(
                "The lengths of the two provided phrases don't match.")
        try:
            replacements = self.get_letter_replacements()
        except Exception as e:
            return await ctx.send(
                f"Could not open letter replacement file - `{e}`.")
        for i, o in zip(phrase1, phrase2):
            replacements[i] = o
        try:
            with open(self.LETTER_REPLACEMENT_FILE_PATH, "w") as a:
                json.dump(replacements, a)
        except Exception as e:
            return await ctx.send(
                f"Could not open letter replacement file to write to it - `{e}`."
            )
        self.letter_replacements = None
        return await ctx.send("Written to file successfully.")
コード例 #20
0
class AnimalCommands(vbu.Cog[Bot]):
    @commands.command(
        aliases=['kitty'],
        application_command_meta=commands.ApplicationCommandMeta(),
    )
    @commands.defer()
    @commands.cooldown(1, 5, commands.BucketType.channel)
    @vbu.checks.is_config_set('api_keys', 'cat_api')
    async def cat(self, ctx: commands.Context):
        """
        Give you a cat picture!
        """

        await ctx.trigger_typing()
        headers = {
            "User-Agent": self.bot.user_agent,
            "x-api-key": self.bot.config['api_keys']['cat_api']
        }
        params = {"limit": 1}
        async with self.bot.session.get(
                "https://api.thecatapi.com/v1/images/search",
                params=params,
                headers=headers) as r:
            data = await r.json()
        if not data:
            return await ctx.send("I couldn't find that breed of cat.")
        with vbu.Embed(use_random_colour=True) as embed:
            embed.set_image(url=data[0]['url'])
        await ctx.send(embed=embed)

    @commands.command(aliases=['doggo', 'puppy', 'pupper'])
    @commands.defer()
    @commands.cooldown(1, 2, commands.BucketType.channel)
    async def dog(self, ctx: commands.Context):
        """
        Give you a doggo picture!
        """

        await ctx.trigger_typing()
        headers = {"User-Agent": self.bot.user_agent}
        url = "https://dog.ceo/api/breeds/image/random"
        async with self.bot.session.get(url, headers=headers) as r:
            data = await r.json()
        if data['status'] == "error":
            return await ctx.send("No dogs were found :(")
        with vbu.Embed(use_random_colour=True) as embed:
            embed.set_image(url=data['message'])
        await ctx.send(embed=embed)

    @commands.command(aliases=['foxo', 'foxxo'])
    @commands.cooldown(1, 5, commands.BucketType.channel)
    async def fox(self, ctx: commands.Context):
        """
        Give you a foxxo picture!
        """

        await ctx.trigger_typing()
        headers = {"User-Agent": self.bot.user_agent}
        async with self.bot.session.get("https://randomfox.ca/floof/",
                                        headers=headers) as r:
            data = await r.json()
        with vbu.Embed(use_random_colour=True) as embed:
            embed.set_image(url=data['image'])
        await ctx.send(embed=embed)