Skip to content

Instantly share code, notes, and snippets.

@AlexanderHott
Last active May 11, 2023 12:11
Show Gist options
  • Save AlexanderHott/7805843a7120f755938a3b75d680d2e7 to your computer and use it in GitHub Desktop.
Save AlexanderHott/7805843a7120f755938a3b75d680d2e7 to your computer and use it in GitHub Desktop.
`hikari`, `lightbulb`, and `miru` examples
# -*- 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