add a lot of examples

This commit is contained in:
AlexanderHOtt
2022-12-28 21:24:53 -08:00
parent 3ce6ab80d6
commit c8834aa9e3
8 changed files with 504 additions and 7 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
TOKEN=<discord bot token>
DECLARE_GLOBAL_COMMANDS=<testing guild id>
OWNER_IDS=<your user id>
OWNER_IDS=<your user id>
PREFIX="./"
+61
View File
@@ -28,6 +28,67 @@ To test the bot on your own discord server you need to register a discord applic
## Resources
### Structure
```graphql
.env # Environment variables
.env.example # Example environment variables
CONTRIBUTING.md # This file
dev-requirements.txt # Development requirements
flake8-requirements.txt # Flake8 extensions (for linting)
noxfile.py # Nox session definitions (for formatting, typechecking, linting)
pyproject.toml # Project configuration
README.md # Project readme
requirements.txt # Requirements
templates/ # Message templates
bot/
__init__.py
__main__.py # Entrypoint
bot.py # Main bot class
config.py # Configuration and secrets
utils.py # Utility Functions
db/ # Database related code
database.db # SQLite database
schema.sql # Database schema
extensions/ # Application logic, see https://hikari-lightbulb.readthedocs.io/en/latest/guides/extensions.html
hot_reload.py # Utility for hot reload extension
```
### Adding a new command/listener
1. Create a new file in the `extensions` folder
2. Copy the template below
```py
# -*- coding: utf-8 -*-
"""My plugin."""
import lightbulb
plugin = lightbulb.Plugin("MyPlugin")
# Add your commands here
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)
```
For example commands and listeners, see [here](/discord-bot/bot/extensions/_example.py)
### Docs
Discord
- [Discord API Reference](https://discord.com/developers/docs/intro)
Main framework
- [Hikari Repo](https://github.com/hikari-py/hikari)
+3 -4
View File
@@ -1,10 +1,10 @@
# -*- coding=utf-8 -*-
"""Bot logic."""
import hikari
import aiosqlite
import hikari
import lightbulb
import miru
from bot.config import Config
config = Config.from_env()
@@ -12,7 +12,7 @@ config = Config.from_env()
bot = lightbulb.BotApp(
token=config.token,
logs="DEBUG",
prefix="./",
prefix=config.prefix,
default_enabled_guilds=config.declare_global_commands,
owner_ids=config.owner_ids,
intents=hikari.Intents.ALL,
@@ -22,7 +22,6 @@ bot = lightbulb.BotApp(
@bot.listen()
async def on_starting(event: hikari.StartingEvent):
"""Setup."""
miru.install(bot) # component handler
bot.load_extensions_from("./bot/extensions") # load extensions
+2
View File
@@ -19,6 +19,7 @@ class Config:
token: str
declare_global_commands: int
owner_ids: list[int]
prefix: str
@classmethod
def from_env(cls):
@@ -32,4 +33,5 @@ class Config:
token=token,
declare_global_commands=int(getenv("DECLARE_GLOBAL_COMMANDS", 0)),
owner_ids=[int(x) for x in getenv("OWNER_IDS", "").split(",")],
prefix=getenv("PREFIX", "./"),
)
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""Extensions for the bot.
See: https://hikari-lightbulb.readthedocs.io/en/latest/guides/extensions.html
"""
+406
View File
@@ -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)
+2 -2
View File
@@ -14,8 +14,8 @@ EXTENSIONS_FOLDER = "bot/extensions"
def _get_extensions() -> list[str]:
# Recursively get all the .py files in the extensions directory.
exts = glob("bot/extensions/**/*.py", recursive=True)
# Recursively get all the .py files in the extensions directory not starting with an `_`.
exts = glob("bot/extensions/**/*[!_].py", recursive=True)
# Turn the path into a plugin path ("path/to/extension.py" -> "path.to.extension")
return [ext.replace("/", ".").replace("\\", ".").replace(".py", "") for ext in exts]
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
"""Utility functions."""
import typing as t
from datetime import datetime
def format_time(dt: datetime, fmt: t.Literal["t", "T", "D", "f", "F", "R"]) -> str:
"""Format a datetime object into the discord time format.
```
| t | HH:MM | 16:20
| T | HH:MM:SS | 16:20:11
| D | D Mo Yr | 20 April 2022
| f | D Mo Yr HH:MM | 20 April 2022 16:20
| F | W, D Mo Yr HH:MM | Wednesday, 20 April 2022 16:20
| R | relative | in an hour
```
"""
match fmt:
case "t" | "T" | "D" | "f" | "F" | "R":
return f"<t:{dt.timestamp():.0f}:{fmt}>"
case _:
raise ValueError(f"`fmt` must be 't', 'T', 'D', 'f', 'F' or 'R', not {fmt}")