Last active
May 11, 2023 12:11
-
-
Save AlexanderHott/7805843a7120f755938a3b75d680d2e7 to your computer and use it in GitHub Desktop.
`hikari`, `lightbulb`, and `miru` examples
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
"""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 | |
# like `guild_only` which make sure the command was called from a server. | |
# 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", True) | |
) 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment