Hikari + Lightbulb Get Started Guide

By Neon

Warning

This guide is not complete, and will not be complete. The GitHub repository has been archived, and my GitHub account is inactive.

Intro

This tutorial assumes that you have some previous knowledge of making Discord bots, though most things complete beginners should be able to understand fairly well

Throughout this tutorial you will see links like Read the docs which go to either the Hikari docs or Lightbulb docs. I try to put them in useful places where people might need them to modify their code to suit a different purpose

The GitHub Repository for this guide is located here
This should really only be used as an assist to the guide, and not to just copy and paste :<

If you notice any issues with code and/or grammar please let me know on Discord @NeonJonn#1650

This tutorial was last updated on 19 December 2021

A Special Thanks

A special thanks goes to thommo and Dav, for answering my endless stream of questions, and to Jonxslays for being cool and helping me work with dicts in the animal command.

And lastly thanks to thommo again, for supporting this little guide, and for making lightbulb such an amazing library, it really wouldn’t be the same without such a cool owner ❤️

Making your Discord Bot Application

Carberra Tutorials made a video on Creating a bot on the Developer Portal which you can follow to do this

Setting up the files

Make a folder for your bot:

mkdir my_bot
cd my_bot

Then, make 3 files: - bot.py - requirements.txt - .env

After all that, your file structure should look like this:

my_bot
│ bot.py
│ requirements.txt
│ .env

Installing requirements

In requirements.txt paste the following

hikari>=2.0.0.dev104
hikari-lightbulb>=2.1.1
python-dotenv>=0.19.2
uvloop>=0.16; sys_platform != "win32"

And run

python -m pip install -r requirements.txt

Note

  • This guide will not work with hikari-lightbulb 2.0.2 due to a bug with BotApp.load_extensions_from in that version.

Please ensure you’re using the latest version - Uvloop is not supported on Windows, but is optional so you can still do this tutorial on a Windows machine

Lightbulb - a “simple and easy to use command framework for Hikari”

uvloop - optional dependency for additional performance benefits on UNIX-like systems

So now, let’s begin!

Hikari Bot

First, grab your bot’s token from the Discord Developer Portal (refer to Part 1 - Making your Discord Bot Application) and put it in the .env file, like so:

BOT_TOKEN=your_bot_token

Next, in bot.py paste the following

 1import os
 2
 3import dotenv
 4import hikari
 5
 6dotenv.load_dotenv()
 7
 8bot = hikari.GatewayBot(
 9    os.environ["BOT_TOKEN"],
10    intents=hikari.Intents.ALL,
11)
12
13
14@bot.listen()
15async def on_message_create(event: hikari.GuildMessageCreateEvent) -> None:
16    if event.is_bot or not event.content:
17        return
18
19    if event.content.strip() == "+ping":
20        await event.message.respond(
21            f"Pong! Latency: {bot.heartbeat_latency*1000:.2f}ms"
22        )
23
24if __name__ == "__main__":
25    if os.name != "nt":
26        import uvloop
27
28        uvloop.install()
29
30    bot.run()

Now save bot.py and run it

python bot.py

You should see an output similar to the following

oooo         o8o  oooo                            o8o       光 2.0.0.dev104 [79548984]
`888         `"'  `888                            `"'       © 2021 davfsa - MIT license
 888 .oo.   oooo   888  oooo   .oooo.   oooo d8b oooo       interpreter:   CPython 3.10.1
 888P"Y88b  `888   888 .8P'   `P  )88b  `888""8P `888       running on:    AMD64 Windows 10
 888   888   888   888888.     .oP"888   888      888       installed at:  C:\Users\Neon\Documents\my_bot\.venv\lib\site-packages\hikari
 888   888   888   888 `88b.  d8(  888   888      888       documentation: https://hikari-py.dev/hikari
o888o o888o o888o o888o o888o `Y888""8o d888b    o888o      support:       https://discord.gg/Jx4cNGG

I 2021-12-19 18:58:07,535 hikari.bot: you can start 999 sessions before the next window which starts at 2021-12-20 12:06:04.514319+00:00; planning to start 1 session...
I 2021-12-19 18:58:07,995 hikari.gateway.0: shard is ready: 2 guilds, Hikari Guides#9057 (873205092923355136), session '14007c7e88f1d714b1612798165d35d4' on v8 gateway
I 2021-12-19 18:58:08,824 hikari.bot: started successfully in approx 0.81 seconds

Now go into the server you invited your bot to, and send +ping

The bot should respond with Pong! and it’s heartbeat latency, like so

../_images/ping.png

Congratulations, you’ve just run your first Hikari bot!

Now let’s go through what everything does

  • Line 1-4 - Import the os, dotenv and hikari modules

  • Line 6 - Load the .env file

  • Line 8-11 - Create a bot using that token, and all Discord intents

  • Line 14-22 - The bot listens for messages sent in guilds (servers)
    • If the message author is a bot or the message has no content (though it may have attachments), it ignores it

    • Otherwise, it checks if the message content is +ping and if it is, the bot responds with Pong! and it’s heartbeat latency

  • Line 24-30
    • If we’re on a non-Windows machine, import uvloop and install it

    • And finally, run the bot!

This bot works, but to add more commands other than +ping would be a huge hassle, so this is where lightbulb comes in

Lightbulb Bot

Lightbulb is a command handler for Hikari, making it easy to create commands and slash commands, handle interactions and more

So to start, let’s change our bot.py a little

 1import os
 2
 3import dotenv
 4import hikari
 5import lightbulb
 6
 7dotenv.load_dotenv()
 8
 9bot = lightbulb.BotApp(
10    os.environ["BOT_TOKEN"],
11    prefix="+",
12    banner=None,
13    intents=hikari.Intents.ALL,
14)
15
16
17@bot.command
18@lightbulb.command("ping", description="The bot's ping")
19@lightbulb.implements(lightbulb.PrefixCommand)
20async def ping(ctx: lightbulb.Context) -> None:
21    await ctx.respond(f"Pong! Latency: {bot.heartbeat_latency*1000:.2f}ms")
22
23
24
25if __name__ == "__main__":
26    if os.name != "nt":
27        import uvloop
28
29        uvloop.install()
30
31    bot.run()
  • Line 5 - We’ve imported lightbulb now too

  • Line 9-14 - We’ve used lightbulb to create the bot now adding
    • a prefix kwarg set to "+"

    • a banner kwarg set to None, disabling the hikari banner that appears when the bot starts
      This isn’t necessary, but the banner can get a little annoying after a while
  • Line 17-21 - Creates a command with the lightbulb bot named ping which works the same as the old ping command,

responding with Pong! and the bot’s heartbeat latency

Now let’s run the bot again!

You should see a slightly different output this time, like so

I 2021-12-19 19:15:37,068 hikari.bot: you can start 998 sessions before the next window which starts at 2021-12-20 12:06:04.517957+00:00; planning to start 1 session...
I 2021-12-19 19:15:37,513 hikari.gateway.0: shard is ready: 2 guilds, Hikari Guides#9057 (873205092923355136), session '69407e13f93111d66206b909af1c1567' on v8 gateway
I 2021-12-19 19:15:38,026 lightbulb.internal: Processing global commands
I 2021-12-19 19:15:38,287 lightbulb.internal: Command processing completed
I 2021-12-19 19:15:38,290 hikari.bot: started successfully in approx 1.52 seconds

Again, if you run the command +ping in your server, the bot should respond with it’s heartbeat latency

Making a lightbulb extension

Extensions are a useful way of separating parts of your bot into different files, making it easier to manage

So, let’s create an extension!

In your my_bot folder make a new folder named extensions

Then in that folder create a file named info.py

Your file structure should look like this now

my_bot
│ bot.py
│ requirements.txt
│ .env
│
└── extensions
│ │ info.py

In info.py paste the following

 1from datetime import datetime
 2
 3import hikari
 4import lightbulb
 5
 6info_plugin = lightbulb.Plugin("Info")
 7
 8
 9@info_plugin.command
10@lightbulb.option(
11    "target", "The member to get information about.", hikari.User, required=False
12)
13@lightbulb.command(
14    "userinfo", "Get info on a server member."
15)
16@lightbulb.implements(lightbulb.PrefixCommand, lightbulb.SlashCommand)
17async def userinfo(ctx: lightbulb.Context) -> None:
18    target = ctx.get_guild().get_member(ctx.options.target or ctx.user)
19
20    if not target:
21        await ctx.respond("That user is not in the server.")
22        return
23
24    created_at = int(target.created_at.timestamp())
25    joined_at = int(target.joined_at.timestamp())
26
27    roles = (await target.fetch_roles())[1:]  # All but @everyone
28
29    embed = (
30        hikari.Embed(
31            title=f"User Info - {target.display_name}",
32            description=f"ID: `{target.id}`",
33            colour=0x3B9DFF,
34            timestamp=datetime.now().astimezone(),
35        )
36        .set_footer(
37            text=f"Requested by {ctx.member.display_name}",
38            icon=ctx.member.avatar_url or ctx.member.default_avatar_url,
39        )
40        .set_thumbnail(target.avatar_url or target.default_avatar_url)
41        .add_field(
42            "Bot?",
43            str(target.is_bot),
44            inline=True,
45        )
46        .add_field(
47            "Created account on",
48            f"<t:{created_at}:d>\n(<t:{created_at}:R>)",
49            inline=True,
50        )
51        .add_field(
52            "Joined server on",
53            f"<t:{joined_at}:d>\n(<t:{joined_at}:R>)",
54            inline=True,
55        )
56        .add_field(
57            "Roles",
58            ", ".join(r.mention for r in roles),
59            inline=False,
60        )
61    )
62
63    await ctx.respond(embed)
64
65def load(bot: lightbulb.BotApp) -> None:
66    bot.add_plugin(info_plugin)

Next, in bot.py we’ll need to make two little changes:

After intents=hikari.Intents.ALL, add

default_enabled_guilds=(123456,)

replacing 123456 with the ID of your guild.

Note

Explanation:

By default, slash commands are global but can take up to an hour to appear after registering with Discord.

Setting default guild(s) means that slash commands will only appear in those guild(s), but will appear and update instantly when running the bot

And on line 23, add:

bot.load_extensions_from("./extensions/", must_exist=True)

So, now let’s run the bot with our new userinfo slash command!

You should see a new line in your output similar to this:

I 2021-12-19 19:32:11,853 lightbulb.app: Extension loaded 'extensions.info'

If all went okay, our slash command userinfo was successfully created!

Now let’s go and try it out:

../_images/userinfo1.png ../_images/userinfo2.png

And there we go, our first slash command!

Now to go through what everything does…

  • Line 6 - Create a plugin named Info, which will be used to add our new slash command
  • Line 19 - Decorator to attach the following command to the plugin

  • Line 10-12 - Add a command option named target with a type of hikari.User that is not required and a description of The member to get information about
  • Line 13-15 - Decorator to create the command, setting the name to userinfo and the description to Get info on a server member.

  • Line 16 - Converts the decorated function to a prefix command and slash command

  • Line 17 - The command’s function, which takes the parameter ctx (Read the docs - Context)

  • Line 18 - Get the guild (ctx.get_guild()) and then the member of that guild using ctx.options.target or, if target wasn’t passed and is None, ctx.user (the user who ran the command)
    Note: This will return None if the target is not found in the guild
  • Line 20-22 - Check if target is None, and then let the user know if it is. The return statement stops any code after it running, but this will only happen if target is None

  • Line 24-25 - Get the UNIX Timestamps for when the member created their account and joined the guild, and round them to the nearest integer
    The rounding is necessary, as Discord timestamps only work with integers, not floats
  • Line 27 - Get the member’s list of roles excluding @everyone

  • Line 30-35 - Make a Discord embed setting the title, description, colour and timestamp

  • Line 36-40 - Set the embed’s footer and thumbnail

  • Line 41-60 - Add fields to the embed, stating
    • whether the user is a bot or not

    • when their account was created & when they joined the server, using Discord Timestamps and

    • a list of roles the member has

  • Line 63 - respond to the interaction with the embed (Read the docs - Context.respond)

  • Line 65-66 - the load function to load the extension when the bot starts. This is required in each extension.

BotApp.d - a built in DataStore

This is preparation for the next section (Command Groups & Subcommands), but also just to show off a new feature of BotApp in lightbulb v2, the built in DataStore.

In our bot.py file, we’ll need to add some “listeners

Add

import aiohttp

just above import dotenv

Then, put the following code at the end of the file, just above bot.load_extensions_from("./extensions/")

1@bot.listen()
2async def on_starting(event: hikari.StartingEvent) -> None:
3    bot.d.aio_session = aiohttp.ClientSession()
4
5@bot.listen()
6async def on_stopping(event: hikari.StoppingEvent) -> None:
7    await bot.d.aio_session.close()

This creates 2 listeners, one for when the bot is starting, and one for when the bot is stopping.

  • When the bot is starting, it creates a new aiohttp.ClientSession named aio_session and stores it in the bot.d data store

  • When the bot is stopping, it closes the aio_session

Read the docs - aiohttp

Command Groups & Subcommands

Create a new file named fun.py in the extensions folder - this will contain our new lightbulb extension

In fun.py paste the following

 1import hikari
 2import lightbulb
 3
 4fun_plugin = lightbulb.Plugin("Fun")
 5
 6
 7@fun_plugin.command
 8@lightbulb.command("fun", "All the entertainment commands you'll ever need")
 9@lightbulb.implements(lightbulb.SlashCommandGroup, lightbulb.PrefixCommandGroup)
10async def fun_group(ctx: lightbulb.Context) -> None:
11    pass  # as slash commands cannot have their top-level command ran, we simply pass here
12
13
14@fun_group.child
15@lightbulb.command("meme", "Get a meme")
16@lightbulb.implements(lightbulb.SlashSubCommand, lightbulb.PrefixSubCommand)
17async def meme_subcommand(ctx: lightbulb.Context) -> None:
18    async with ctx.bot.d.aio_session.get(
19        "https://meme-api.herokuapp.com/gimme"
20    ) as response:
21        res = await response.json()
22
23        if response.ok and res["nsfw"] != True:
24            link = res["postLink"]
25            title = res["title"]
26            img_url = res["url"]
27
28            embed = hikari.Embed(colour=0x3B9DFF)
29            embed.set_author(name=title, url=link)
30            embed.set_image(img_url)
31
32            await ctx.respond(embed)
33
34        else:
35            await ctx.respond(
36                "Could not fetch a meme :c", flags=hikari.MessageFlag.EPHEMERAL
37            )
38
39
40def load(bot: lightbulb.BotApp) -> None:
41    bot.add_plugin(fun_plugin)
  • Line 4 - Create a new plugin named Fun

  • Line 7 - Decorator to attach the following command to the plugin

  • Line 8 - Decorator to create the command, setting the name to fun and adding a description

  • Line 9 - Converts the decorated function to a PrefixCommandGroup and SlashCommandGroup

  • Line 10 - The command’s function

  • Line 11 - pass the function, as slash commands cannot have their top-level command ran

  • Line 14 - attach the decorated function to the fun_group command

  • Line 15 - Decorator to create the subcommand, setting the name to meme and adding a description

  • Line 16 - Converts the decorated function to a PrefixSubCommand and SlashSubCommand

  • Line 17 - The subcommand’s function

  • Line 18-21 - Using the aio_session from the bot.d data store that we created in the previous section, get a meme from the API
  • Line 23 - If the response is successful and the meme is not NSFW (Not Safe For Work), then
    • Line 24-26 - Get the meme’s link, title and image url

    • Line 28 - Create an embed

    • Line 29 - Set the embed’s author to the meme’s title and link

    • Line 30 - Set the embed’s image to the meme’s image url

    • Line 32 - Respond to the interaction with the embed

  • Line 34 - Otherwise, if the response was not successful or the meme was NSFW, then
    • Line 35-37 - Respond to the interaction with an ephemeral message, stating that we could not fetch a meme

Now, let’s test it!

../_images/meme1.png ../_images/meme2.png

and if we can’t fetch a meme:

../_images/meme3.png

Note

Ephemeral response only work with slash commands, not prefix commands

Components - Dropdown Menus

Message components are relatively new features on Discord, allowing you to attach buttons and select menus to messages!

Let’s add some new code to fun.py

This should be inserted after the meme command, but above the load function

 1ANIMALS = {
 2    "Dog": "🐶",
 3    "Cat": "🐱",
 4    "Panda": "🐼",
 5    "Fox": "🦊",
 6    "Red Panda": "🐼",
 7    "Koala": "🐨",
 8    "Bird": "🐦",
 9    "Racoon": "🦝",
10    "Kangaroo": "🦘",
11}
12
13
14@fun_group.child
15@lightbulb.command("animal", "Get a fact + picture of a cute animal :3")
16@lightbulb.implements(lightbulb.SlashSubCommand, lightbulb.PrefixSubCommand)
17async def animal_subcommand(ctx: lightbulb.Context) -> None:
18    select_menu = (
19        ctx.bot.rest.build_action_row()
20        .add_select_menu("animal_select")
21        .set_placeholder("Pick an animal")
22    )
23
24    for name, emoji in ANIMALS.items():
25        select_menu.add_option(
26            name,  # the label, which users see
27            name.lower().replace(" ", "_"),  # the value, which is used by us later
28        ).set_emoji(emoji).add_to_menu()
29
30    resp = await ctx.respond(
31        "Pick an animal from the dropdown :3",
32        component=select_menu.add_to_container(),
33    )
34    msg = await resp.message()
35
36    try:
37        event = await ctx.bot.wait_for(
38            hikari.InteractionCreateEvent,
39            timeout=60,
40            predicate=lambda e:
41                isinstance(e.interaction, hikari.ComponentInteraction)
42                and e.interaction.user.id == ctx.author.id
43                and e.interaction.message.id == msg.id
44                and e.interaction.component_type == hikari.ComponentType.SELECT_MENU
45            )
46    except asyncio.TimeoutError:
47        await msg.edit("The menu timed out :c", components=[])
48    else:
49        animal = event.interaction.values[0]
50        async with ctx.bot.d.aio_session.get(
51            f"https://some-random-api.ml/animal/{animal}"
52        ) as res:
53            if res.ok:
54                res = await res.json()
55                embed = hikari.Embed(description=res["fact"], colour=0x3B9DFF)
56                embed.set_image(res["image"])
57
58                animal = animal.replace("_", " ")
59
60                await msg.edit(
61                    f"Here's a {animal} for you! :3", embed=embed, components=[]
62                )
63            else:
64                await msg.edit(
65                    f"API returned a {res.status} status :c", components=[]
66                )
  • Line 1-11 - Create a dict containing all the possible endpoints of some-random-api.ml/animal/

  • Line 14-16 - Set up prefix and slash sub commands

  • Line 18-22
    • Create an action row, which returns an ActionRowBuilder

    • Add a select menu to the action row, with animal_select as the custom ID

    • Set the placeholder (the text that is seen when no option is picked) to Pick an animal

  • Line 24-28 - For all the items in the ANIMALS dict, add an option to the select menu Read the docs - SelectMenuBuilder.add_option with
    • The name and

    • The value, which is the name of the animal, but lowercased and with spaces replaced with underscores

    • Setting the emoji to the value of the animal in the ANIMALS dict

  • Line 30-34
    • Respond to the context with the select menu

    • Fetch the message from the response (in lightbulb V2, Context.respond returns a ResponseProxy, not a Message)

  • Line 37-45 - Wait for an interaction to be created and
    • Check if the interaction is a component interaction

    • Check that the interaction user is the same who ran the command

    • Check that the interaction message is the same as the message we sent

    • Check that the interaction component type is a select menu

  • Line 46-47 - If the interaction times out, an asyncio.TimeoutError will be raised, and so we can use that to handle the timeout by editing the message and removing the components

  • Line 49 - Get the value of the interaction (the selected option) - Read the docs - ComponentInteraction.values

  • Line 50-52 - Make a GET request to some-random-api.ml with the selected animal as the option

  • Line 53 - If the response has an ok status, then
    • Line 54 - Get the response’s json

    • Line 55 - Create an embed, setting its title to the animal fact

    • Line 56 - Set the embed’s image to the animal image

    • Line 58 - Replace the underscore in animal with a space

    • Line 60-62 - Edit the message to contain the embed, and remove the select menu component

  • Line 63 - Otherwise, if the response was not successful, then
    • Line 64-66 - Edit the message to say what status code the API responded with, and remove the select menu component

And at the very top of the file, don’t forget to import asyncio!

import asyncio
../_images/animal1.png ../_images/animal2.png ../_images/animal3.png

And if the menu times out:

../_images/animal4.png

Command Checks

We’ll be making a purge command, which will delete messages in bulk to demonstrate how to use command checks.

So, create a new file named mod.py in the extensions folder

In it paste the following

 1import asyncio
 2
 3import hikari
 4import lightbulb
 5from lightbulb import errors
 6
 7mod_plugin = lightbulb.Plugin("Mod")
 8
 9
10@mod_plugin.command
11@lightbulb.option(
12    "messages", "The number of messages to purge.", type=int, required=True
13)
14@lightbulb.command("purge", "Purge messages.", aliases=["clear"])
15@lightbulb.implements(lightbulb.PrefixCommand, lightbulb.SlashCommand)
16async def purge_messages(ctx: lightbulb.Context) -> None:
17    num_msgs = ctx.options.messages
18    channel = ctx.channel_id
19
20    # If the command was invoked using the PrefixCommand, it will create a message
21    # before we purge the messages, so you want to delete this message first
22    if isinstance(ctx, lightbulb.PrefixContext):
23        await ctx.event.message.delete()
24
25    msgs = await ctx.bot.rest.fetch_messages(channel).limit(num_msgs)
26    await ctx.bot.rest.delete_messages(channel, msgs)
27
28    resp = await ctx.respond(f"{len(msgs)} messages deleted")
29
30    await asyncio.sleep(5)
31    await resp.delete()
32
33
34def load(bot: lightbulb.BotApp) -> None:
35    bot.add_plugin(mod_plugin)

limiting it to num_msgs (Read the docs - LazyIterator.limit()) - Line 26 - Delete the messages that we fetched

Note: ctx.respond() returns a ResponseProxy, not a Message. If you want to get the message, you can use ResponseProxy.message.

Now this command works fine, but now everyone can delete messages using the bot. We only want people with the manage messages permission to do this, so this is where checks come in.

Just above line 11 (@lightbulb.option), add the following

@lightbulb.add_checks(
    lightbulb.has_guild_permissions(hikari.Permissions.MANAGE_MESSAGES),
    lightbulb.bot_has_guild_permissions(hikari.Permissions.MANAGE_MESSAGES)
)

This checks if the both the user who ran the command and the bot has the manage messages permission in the guild

If the both the user and bot have permission to run the command, it will work. If they don’t, the command will raise CheckFailure.

But raising an error and the command failing isn’t that useful, we want to tell the user what happened

So, onto error handling!

Error Handling

raise NotImplementedError("This part hasn't been written yet!")

The End, for now…

Unfortunately this is where the guide ends for now, but fear not because I’ll be updating this regularly!

What I’ll be adding next:

  • Checks & Cooldowns for commands

  • Error handling

My new testing server, Gamma Rays, is open! Join it here!

If you need help or want to receive Hikari + Lightbulb updates, why not join the Hikari server