Files
Open-Assistant/discord-bot/EXAMPLES.md
T
2022-12-30 05:14:16 -08:00

15 KiB

hikari, lightbulb, and muri examples

Example plugin for reference.

import asyncio

import hikari
import lightbulb
import lightbulb.decorators
import miru
from miru.ext import nav

plugin = lightbulb.Plugin("ExamplePlugin")

# To add checks to a plugin, you can use the `@plugin.check` decorator
# or the `plugin.add_check` method. Lightbulb has some built-in checks.
# The check will be called before any command in the plugin is called.
plugin.add_checks(lightbulb.guild_only)


# To create a slash command, use the template below
@plugin.command
@lightbulb.command("example", "Example command.")
@lightbulb.implements(lightbulb.SlashCommand)
async def example(ctx: lightbulb.SlashContext):
    """Example command."""
    # To send a message, use the `respond` method on `ctx`.
    # !!! Be sure to use `await` when calling `respond` !!!
    await ctx.respond("Hello, world!")


# To add arguments, use the `@lightbulb.option` decorator.
@plugin.command
@lightbulb.option(
    "name",  # The name of the option. This is what you will use to access the value in `ctx.options.name`
    "Your name.",  # The description of the option. This will be shown in the slash command menu.
    # Whether or not the option is required.
    # If `required` is `True`, the user will not be able to use the command without providing a value for this option.
    required=False,
    default=None,  # The default value for the option. If `required` is `True`, this will be ignored.
    type=str | None,  # The type of the option. This is used to convert the value to the correct type.
    # https://hikari-lightbulb.readthedocs.io/en/latest/guides/commands.html#converters-and-slash-command-option-types
)
@lightbulb.option(
    "age",
    "Your age.",
    type=int,
    # These are enforced on the client side, so the user won't be able to enter a value outside of the range.
    min_value=0,
    max_value=100,
)
@lightbulb.option(
    "gender",
    "Your gender.",
    # You can also use `choices` to limit the user to a specific set of values.
    # This can be a list of `str`, `int, or `float`
    # choices=["Male", "Female", "Other"],
    # or a list of `hikari.CommandChoice` objects to have separate option names and values
    choices=[
        hikari.CommandChoice(name="male", value="M"),
        hikari.CommandChoice(name="female", value="F"),
        hikari.CommandChoice(name="other", value="Other"),
    ],
    type=str,
)
@lightbulb.command("args_example", "Example command with arguments.")
@lightbulb.implements(lightbulb.SlashCommand)
async def args_example(ctx: lightbulb.SlashContext):
    """Example command with arguments."""
    name: str | None = ctx.options.name
    if name is None:
        name = ctx.author.username
    age: int = ctx.options.age
    gender: str = ctx.options.gender

    await ctx.respond(
        f"Hello {ctx.author.mention}! Your name is {name}, you are {age} years old, and your gender is {gender}.",
        # in order to actually mention the user, you must pass `user_mentions=True`
        # otherwise, the user won't get a notification
        user_mentions=True,
    )


# To have autocomplete options, add the
# pass `autocomplete=function` to `@lightbulb.option`
# or `autocomplete=True` and mark the function with `@command.autocomplete("option_name")`.
# @autocomplete_example.autocomplete("language")
async def _programming_language_autocomplete(
    option: hikari.CommandInteractionOption, interaction: hikari.AutocompleteInteraction
) -> list[str]:
    # The `option` argument is the current text that the user typed in.
    if not isinstance(option.value, str):
        # This will raise a TypeError if `option.value` cannot be converted
        option.value = str(option.value)

    # You can query a database, fetch an api, or return any list of strings
    # !!! You can return a max of 25 options !!!
    langs = [
        "C",
        "C++",
        "C#",
        "CSS",
        "Go",
        "HTML",
        "Java",
        "Javascript",
        "Kotlin",
        "Matlab",
        "NoSQL",
        "PHP",
        "Perl",
        "Python",
        "R",
        "Ruby",
        "Rust",
        "SQL",
        "Scala",
        "Swift",
        "TypeScript",
        "Zig",
    ]
    return [lang for lang in langs if option.value.lower() in lang.lower()]


@plugin.command
@lightbulb.option(
    "language",
    "Your favorite programming language.",
    autocomplete=_programming_language_autocomplete,
)
@lightbulb.command("autocomplete_example", "Autocomplete example.")
@lightbulb.implements(lightbulb.SlashCommand)
async def autocomplete_example(ctx: lightbulb.SlashContext):
    """Autocomplete example."""
    await ctx.respond("Your favorite programming language is " + ctx.options.language)


# Command groups are like trees
# You can have subcommands, subcommand groups, and subcommand groups with subcommands
# Here is an example diagram:
# /group_example        (group)
#   subcommand          (executable)
#   subcommand_group    (group)
#     subsubcommand     (executable)

# Because those are slash commands, only the leaves (/subcommand and /subsubcommand) are callable.

# To create a group, use the template below
# 1. Create the command group
@plugin.command
@lightbulb.command("group_example", "Example command group.")
@lightbulb.implements(lightbulb.SlashCommandGroup)
async def group_example(_: lightbulb.SlashContext) -> None:
    """Group example."""
    # This will never execute because it is a group
    pass


# 2. Add a child command
@group_example.child
@lightbulb.command("subcommand", "Example subcommand.")
@lightbulb.implements(lightbulb.SlashSubCommand)
async def subcommand(ctx: lightbulb.SlashContext) -> None:
    """An example subcommand."""
    await ctx.respond("invoked `/group_example subcommand`")


# 3. Add a sub-group
@group_example.child
@lightbulb.command("subcommand_group", "Example subcommand group.")
@lightbulb.implements(lightbulb.SlashSubGroup)
async def subcommand_group(_: lightbulb.SlashContext) -> None:
    """Subcommand group example."""
    # This will never execute because it is a sub-group
    pass


# 4. Add a child to the sub-group
@subcommand_group.child
@lightbulb.command("subsubcommand", "Example subsubcommand.")
@lightbulb.implements(lightbulb.SlashSubCommand)
async def subsubcommand(ctx: lightbulb.SlashContext) -> None:
    """An example subsubcommand."""
    await ctx.respond("invoked `/group_example subcommand_group subsubcommand`")


# Event listeners are a way to listen to events from the gateway.
# You can have stand alone event listeners or use `wait_for` to wait for a specific event inside a command / listener.
@plugin.listener(hikari.MemberCreateEvent)
async def on_member_join(event: hikari.MemberCreateEvent) -> None:
    """Event listener to welcome new members."""
    guild = event.get_guild()
    await event.member.send(f"Welcome to {guild.name if guild else 'the server'}!")


# You can also use `wait_for` to wait for a specific event
@plugin.command
@lightbulb.command("wait_for_example", "Example command with `wait_for` and `stream`.")
@lightbulb.implements(lightbulb.SlashCommand)
async def wait_for_example(ctx: lightbulb.SlashContext) -> None:
    """Wait for example."""
    await ctx.respond("Send a message!")

    # We can add a predicate to `wait_for` to filter out events
    def author_check(e: hikari.MessageCreateEvent) -> bool:
        return e.author_id == ctx.author.id

    # You need to wrap wait_for in a try/catch block because it can raise `asyncio.TimeoutError`
    try:
        event = await ctx.bot.wait_for(hikari.MessageCreateEvent, timeout=10, predicate=author_check)
        await ctx.respond(f"You sent: {event.message.content}")
    except asyncio.TimeoutError:
        await ctx.respond("Too slow!")
    # remember to use try/except/finally if you need to clean up any resources

    # You can also use `stream` to listen for events
    await ctx.respond("Waiting for guild events...")
    with ctx.bot.stream(hikari.Event, timeout=5).filter(
        # Only listen for events that have a guild_id and are not bots
        lambda e: getattr(e, "guild_id", None) == ctx.guild_id
        and getattr(e, "is_human", False)
    ) as stream:
        async for event in stream:
            await ctx.respond(f"New `{event.__class__.__name__}`")

    await ctx.respond("Done!")


# You can interact with discord's API using the `rest` attribute on the bot
# This allows you to
# - fetch information about users, channels, guilds, etc.
# - create, edit, and delete messages, channels, threads, roles, categories, etc.
# - add, remove, and edit reactions
@plugin.command
@lightbulb.command("rest_example", "Example command using the `rest` attribute.")
@lightbulb.implements(lightbulb.SlashCommand)
async def rest_example(ctx: lightbulb.SlashContext) -> None:
    """Example command using the `rest` attribute."""
    rest = ctx.bot.rest
    your_messages = await rest.fetch_messages(ctx.channel_id).filter(lambda m: m.author.id == ctx.author.id).count()
    await ctx.respond(f"{your_messages} out of the last 10 messages in this channel were sent by you.")


# Context Menus are a way to attach a command to a user or a message.
# By right clicking a user or a User, you can select to execute a command under the "Apps" menu item.
@plugin.command
@lightbulb.command("user_context_menu_example", "Example context menu on a user.")
@lightbulb.implements(lightbulb.UserCommand)
async def user_context_menu_example(ctx: lightbulb.UserContext) -> None:
    """User context menu example."""
    user: hikari.Member = ctx.options.target
    await ctx.respond(f"Hello {user.mention}!", user_mentions=True)


# Same with messages
@plugin.command
@lightbulb.command("message_context_menu_example", "Example context menu on a message.")
@lightbulb.implements(lightbulb.MessageCommand)
async def message_context_menu_example(ctx: lightbulb.MessageContext) -> None:
    """Message context menu example."""
    message: hikari.Message = ctx.options.target
    await ctx.respond(f"The message length is: {len(message.content or '')}", flags=hikari.MessageFlag.EPHEMERAL)


# Components are a way to add interactive buttons to your slash commands.
# We use `miru` to manage components and their callbacks.

# To create a component, use the template below
# 1. Create the view
class MyView(miru.View):
    """An example view with buttons."""

    @miru.button(label="Rock", emoji="\N{ROCK}", style=hikari.ButtonStyle.PRIMARY)
    async def rock_button(self, button: miru.Button, ctx: miru.ViewContext) -> None:
        await ctx.respond("Paper!")

    @miru.button(label="Paper", emoji="\N{SCROLL}", style=hikari.ButtonStyle.PRIMARY)
    async def paper_button(self, button: miru.Button, ctx: miru.ViewContext) -> None:
        await ctx.respond("Scissors!")

    @miru.button(label="Scissors", emoji="\N{BLACK SCISSORS}", style=hikari.ButtonStyle.PRIMARY)
    async def scissors_button(self, button: miru.Button, ctx: miru.ViewContext):
        await ctx.respond("Rock!")

    @miru.button(emoji="\N{BLACK SQUARE FOR STOP}", style=hikari.ButtonStyle.DANGER, row=2)
    async def stop_button(self, button: miru.Button, ctx: miru.ViewContext) -> None:
        self.stop()  # Stop listening for interactions

    @miru.select(
        options=[
            hikari.SelectMenuOption(
                label="Thing 1",
                value="1",
                description="This is a thing",
                emoji=hikari.UnicodeEmoji("🗿"),
                is_default=True,
            ),
            hikari.SelectMenuOption(
                label="Thing 2",
                value="2",
                description="This is another thing",
                emoji=hikari.UnicodeEmoji("🗿"),
                is_default=False,
            ),
            hikari.SelectMenuOption(
                label="Thing 3",
                value="3",
                description="This is a different thing",
                emoji=hikari.UnicodeEmoji("🗿"),
                is_default=False,
            ),
        ],
        placeholder="Select some stuff!",
        min_values=0,
        max_values=2,
        row=3,
    )
    async def select(self, select: miru.Select, ctx: miru.ViewContext) -> None:
        await ctx.respond(f"You selected {select.values}")


# 2. Create a command to use the view
@plugin.command
@lightbulb.command("button_example", "Example command with buttons.")
@lightbulb.implements(lightbulb.SlashCommand)
async def button_example(ctx: lightbulb.SlashContext) -> None:
    """Wait for example."""
    # 3. Create an instance of the view and start it
    view = MyView(timeout=60)
    resp = await ctx.respond("Rock Paper Scissors!", components=view)
    msg = await resp.message()
    await view.start(msg)
    await view.wait()

    await ctx.respond("Thank you for playing!")


# You can use buttons to create a navigation menu
@plugin.command
@lightbulb.command("nav_example", "Example command with button navigation.", auto_defer=True)
@lightbulb.implements(lightbulb.SlashCommand)
async def navigation_example(ctx: lightbulb.SlashContext) -> None:
    """Navigation example."""
    # await ctx.respond(response_type=hikari.ResponseType.DEFERRED_MESSAGE_UPDATE)
    embed = hikari.Embed(title="I'm the second page!", description="Also an embed!")
    pages = ["I'm the first page!", embed, "I'm the last page!"]

    navigator = nav.NavigatorView(pages=pages, timeout=10)
    # You may also pass an interaction object to this function
    await navigator.send(ctx.channel_id)

    await navigator.wait()  # This is not necessary, but we want to wait anyway
    await ctx.respond("Done!")


# Miru also has modal support
class MyModal(miru.Modal):
    """An example modal."""

    # Define our modal items
    # You can also use Modal.add_item() to add items to the modal after instantiation, just like with views.
    name = miru.TextInput(label="Name", placeholder="Enter your name!", required=True)
    bio = miru.TextInput(label="Biography", value="Pre-filled content!", style=hikari.TextInputStyle.PARAGRAPH)

    # You can currently only use TextInputs
    # https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-modal

    # The callback function is called after the user hits 'Submit'
    async def callback(self, context: miru.ModalContext) -> None:
        # You can also access the values using ctx.values, Modal.values, or use ctx.get_value_by_id()
        await context.respond(f"Your name: `{self.name.value}`\nYour bio: ```{self.bio.value}```")


class ModalView(miru.View):
    """An example view that opens a modal."""

    # Create a new button that will invoke our modal
    @miru.button(label="Click me!", style=hikari.ButtonStyle.PRIMARY)
    async def modal_button(self, button: miru.Button, ctx: miru.ViewContext) -> None:
        modal = MyModal(title="Example Title")
        # You may also use Modal.send(interaction) if not working with a miru context object. (e.g. slash commands)
        # Keep in mind that modals can only be sent in response to interactions.
        await ctx.respond_with_modal(modal)
        # OR
        # await modal.send(ctx.interaction)


@plugin.command
@lightbulb.command("modal_example", "Example command with a modal.")
@lightbulb.implements(lightbulb.SlashCommand)
async def modal_example(ctx: lightbulb.SlashContext) -> None:
    """Navigation example."""
    view = ModalView()
    resp = await ctx.respond("This button triggers a modal!", components=view)
    await view.start(await resp.message())



def load(bot: lightbulb.BotApp):
    """Add the plugin to the bot."""
    bot.add_plugin(plugin)


def unload(bot: lightbulb.BotApp):
    """Remove the plugin to the bot."""
    bot.remove_plugin(plugin)