|
|
|
@@ -0,0 +1,406 @@
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
"""Example plugins for reference.
|
|
|
|
|
|
|
|
|
|
Because this file starts with an `_`, it cannot be loaded by the bot. To see the example plugin in action, rename this file to `example.py`.
|
|
|
|
|
"""
|
|
|
|
|
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)
|