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
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
Make a virtual environment (optional but recommended)¶
Windows:
python -m venv .venv
.\.venv\Scripts\activate
Linux:
python -m venv .venv
source .venv/bin/activate
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 withBotApp.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

Congratulations, you’ve just run your first Hikari bot!
Now let’s go through what everything does
Line 1-4 - Import the
os
,dotenv
andhikari
modulesLine 6 - Load the
.env
fileLine 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 withPong!
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 toNone
, disabling the hikari banner that appears when the bot startsThis 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 oldping
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:


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 ofhikari.User
that is not required and a description ofThe member to get information about
Line 13-15 - Decorator to create the command, setting the name to
userinfo
and the description toGet 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 usingctx.options.target
or, if target wasn’t passed and isNone
,ctx.user
(the user who ran the command)Note: This will returnNone
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. Thereturn
statement stops any code after it running, but this will only happen if target isNone
- Line 24-25 - Get the UNIX Timestamps for when the member created their account and joined the guild, and round them to the nearest integerThe 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 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
namedaio_session
and stores it in thebot.d
data storeWhen the bot is stopping, it closes the
aio_session
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 descriptionLine 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
commandLine 15 - Decorator to create the subcommand, setting the name to
meme
and adding a descriptionLine 16 - Converts the decorated function to a PrefixSubCommand and SlashSubCommand
Line 17 - The subcommand’s function
- Line 18-21 - Using the
aio_session
from thebot.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!


and if we can’t fetch a meme:

Note
Ephemeral response only work with slash commands, not prefix commands
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)
Line 25 - Fetch the most recent messages in the channel (Read the docs - fetch_messages),
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