Skip to content

Commit

Permalink
Update README & refactor examples (#44)
Browse files Browse the repository at this point in the history
Co-authored-by: Ayden <jaholaayden@gmail.com>
  • Loading branch information
novanai and aydenjahola authored Nov 12, 2024
1 parent c3b04f8 commit 65d1d95
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 143 deletions.
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TOKEN=
51 changes: 3 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,8 @@
# Blockbot

Blockbot is a Discord bot, written in Python, that is maintained by the Redbrick Webgroup. This project uses [`hikari`](https://github.com/hikari-py/hikari/), an opinionated microframework, to interface with the Discord API. [`hikari-arc`](https://github.com/hypergonial/hikari-arc) is the command handler of choice.
Blockbot is a Discord bot, written in Python, that is maintained by the [Redbrick Webgroup](https://docs.redbrick.dcu.ie/webgroup/).

## Resources
## Documentation

- [`hikari` Documentation](https://docs.hikari-py.dev/en/latest/)
- [`hikari-arc` Documentation](https://arc.hypergonial.com/)
- [Examples](https://github.com/hypergonial/hikari-arc/tree/main/examples/gateway)
Development documentation is hosted on the [Redbrick docs](https://docs.redbrick.dcu.ie/webgroup/blockbot/).

## File Structure

- `bot.py`
- This is the file that contains the bot configuration and instantiation, while also being responsible for loading the bot extensions.
- `extensions/`
- This directory is home to the custom extensions (known as cogs in `discord.py`, or command groups) that are loaded when the bot is started. Extensions are classes that encapsulate command logic and can be dynamically loaded/unloaded. In `hikari-arc`, an intuitive [plugin system](https://arc.hypergonial.com/guides/plugins_extensions/) is used.
- `config.py`
- Configuration secrets and important constants (such as identifiers) are stored here. The secrets are loaded from environment variables, so you can set them in your shell or in a `.env` file.
- `utils.py`
- Simple utility functions are stored here, that can be reused across the codebase.

## Installation

### Discord Developer Portal

As a prerequisite, you need to have a bot application registered on the Discord developer portal.

1. Create a Discord bot application [here](https://discord.com/developers/applications/).
2. When you have a bot application, register it for slash commands:
3. Go to *"OAuth2 > URL Generator"* on the left sidebar, select the `bot` and `applications.commands` scopes, scroll down & select the bot permissions you need (for development, you can select `Administator`).
4. Copy and visit the generated URL at the bottom of the page to invite it to the desired server.

#### Bot Token

- Go to the Discord developer portal and under *"Bot"* on the left sidebar, click `Reset Token`. Copy the generated token.

### Source Code
1. `git clone` and `cd` into this repository.
2. It's generally advised to work in a Python [virtual environment](https://docs.python.org/3/library/venv.html):
```sh
python3 -m venv .venv
source .venv/bin/activate
```
3. Create a new file called `.env` inside the repo folder and paste your bot token into the file as such:
```
TOKEN=<Discord bot token here>
```
4. Run `pip install -r requirements.txt` to install the required packages.
5. Once that's done, start the bot by running `python3 -m src`.

## FAQ

- If you get errors related to missing token environment variables, run `source .env`.
7 changes: 3 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
hikari==2.0.0.dev122
hikari-arc==1.1.0
ruff==0.2.0
pre-commit==3.6.0
hikari==2.1.0
hikari-arc==1.3.4
hikari-miru==4.1.1
python-dotenv==1.0.1
pyfiglet==1.0.2
fortune-python==1.1.1
2 changes: 2 additions & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ruff==0.6.9
pre-commit==4.0.0
8 changes: 8 additions & 0 deletions src/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import arc
import hikari
import miru

from src.config import DEBUG, TOKEN

Expand All @@ -20,8 +21,15 @@
logging.info(f"Debug mode is {DEBUG}; You can safely ignore this.")

client = arc.GatewayClient(bot, is_dm_enabled=False)
miru_client = miru.Client.from_arc(client)

client.set_type_dependency(miru.Client, miru_client)

client.load_extensions_from("./src/extensions/")

if DEBUG:
client.load_extensions_from("./src/examples/")


@client.set_error_handler
async def error_handler(ctx: arc.GatewayContext, exc: Exception) -> None:
Expand Down
35 changes: 35 additions & 0 deletions src/examples/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import arc

plugin = arc.GatewayPlugin(name="Example Commands")


@plugin.include
@arc.slash_command("hello", "Say hello!")
async def hello(ctx: arc.GatewayContext) -> None:
"""An individual command, invoked by `/hello`."""
await ctx.respond("Hello from hikari and hikari-arc!")


group = plugin.include_slash_group("base_group", "A base command group, with sub groups and sub commands.")


@group.include
@arc.slash_subcommand("sub_command", "A sub command")
async def sub_command(ctx: arc.GatewayContext) -> None:
"""A subcommand, invoked by `/base_command sub_command`."""
await ctx.respond("Hello, world! This is a sub command")


sub_group = group.include_subgroup("sub_group", "A subgroup to add commands to.")


@sub_group.include
@arc.slash_subcommand("sub_command", "A subgroup subcommand.")
async def sub_group_sub_command(ctx: arc.GatewayContext) -> None:
"""A subcommand belonging to a subgroup, invoked by `/base_group sub_group sub_command`."""
await ctx.respond("This is a subgroup subcommand.")


@arc.loader
def loader(client: arc.GatewayClient) -> None:
client.add_plugin(plugin)
67 changes: 67 additions & 0 deletions src/examples/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import arc
import hikari
import miru

plugin = arc.GatewayPlugin("Example Components")


class View(miru.View):
def __init__(self, user_id: int) -> None:
self.user_id = user_id

super().__init__(timeout=60)

@miru.button("Click me!", custom_id="click_me")
async def click_button(self, ctx: miru.ViewContext, button: miru.Button) -> None:
await ctx.respond(f"{ctx.user.mention}, you clicked me!")

# Defining select menus: https://miru.hypergonial.com/guides/selects/
@miru.text_select(
custom_id="select_me",
placeholder="Choose your favourite colours...",
min_values=1,
max_values=3,
options=[
miru.SelectOption(label=colour)
for colour in ["Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet"]
],
)
async def colour_select(self, ctx: miru.ViewContext, select: miru.TextSelect) -> None:
await ctx.respond(f"Your favourite colours are: {', '.join(select.values)}!")

# Defining a custom view check: https://miru.hypergonial.com/guides/checks_timeout/#checks
async def view_check(self, ctx: miru.ViewContext) -> bool:
# This view will only handle interactions that belong to the
# user who originally ran the command.
# For every other user they will receive an error message.
if ctx.user.id != self.user_id:
await ctx.respond("You can't press this!", flags=hikari.MessageFlag.EPHEMERAL)
return False

return True

# Handling view timeouts: https://miru.hypergonial.com/guides/checks_timeout/#timeout
# Editing view items: https://miru.hypergonial.com/guides/editing_items/
async def on_timeout(self) -> None:
message = self.message
assert message # Since the view is bound to a message, we can assert it's not None

for item in self.children:
item.disabled = True

await message.edit(components=self)
self.stop()


@plugin.include
@arc.slash_command("components", "A command with components.")
async def components_cmd(ctx: arc.GatewayContext, miru_client: miru.Client = arc.inject()) -> None:
view = View(ctx.user.id)
response = await ctx.respond("Here are some components...", components=view)

miru_client.start_view(view, bind_to=await response.retrieve_message())


@arc.loader
def loader(client: arc.GatewayClient) -> None:
client.add_plugin(plugin)
45 changes: 45 additions & 0 deletions src/examples/modals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import arc
import hikari
import miru

plugin = arc.GatewayPlugin("Example Modal")


# Modals Guide: https://miru.hypergonial.com/guides/modals/
class MyModal(miru.Modal, title="Tell us about yourself!"):
name = miru.TextInput(
label="Name",
placeholder="Enter your name!",
required=True,
)

bio = miru.TextInput(
label="Biography",
value="Age: \nHobbies:", # pre-filled content
style=hikari.TextInputStyle.PARAGRAPH,
required=False,
)

# The callback function is called after the user hits 'Submit'
async def callback(self, ctx: miru.ModalContext) -> None:
# values can also be accessed using ctx.values,
# Modal.values, or with ctx.get_value_by_id()
embed = hikari.Embed(title=self.name.value, description=self.bio.value)
await ctx.respond(embed=embed)


@plugin.include
@arc.slash_command("modal", "A command with a modal response.")
async def modal_command(ctx: arc.GatewayContext, miru_client: miru.Client = arc.inject()) -> None:
modal = MyModal()
builder = modal.build_response(miru_client)

# arc has a built-in way to respond with a builder
await ctx.respond_with_builder(builder)

miru_client.start_modal(modal)


@arc.loader
def loader(client: arc.GatewayClient) -> None:
client.add_plugin(plugin)
32 changes: 32 additions & 0 deletions src/examples/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import arc
import hikari

plugin = arc.GatewayPlugin("Example Options")


# Options Guide: https://arc.hypergonial.com/guides/options/
@plugin.include
@arc.slash_command("options", "A command with options")
async def options(
ctx: arc.GatewayContext,
str_option: arc.Option[str, arc.StrParams("A string option.", name="string")],
int_option: arc.Option[int, arc.IntParams("An integer option.", name="integer", min=5, max=150)],
attachment_option: arc.Option[hikari.Attachment, arc.AttachmentParams("An attachment option.", name="attachment")],
channel_option: arc.Option[
hikari.TextableChannel | None, arc.ChannelParams("A textable channel option.", name="channel")
] = None,
) -> None:
"""A command with lots of options."""
embed = hikari.Embed(title="There are a lot of options here", description="Maybe too many...", colour=0x5865F2)
embed.set_image(attachment_option)

embed.add_field("String option", str_option, inline=True)
embed.add_field("Integer option", str(int_option), inline=True)
embed.add_field("Channel option", f"<#{channel_option.id}>" if channel_option else "Not supplied", inline=True)

await ctx.respond(embed=embed)


@arc.loader
def loader(client: arc.GatewayClient) -> None:
client.add_plugin(plugin)
91 changes: 0 additions & 91 deletions src/extensions/hello_world.py

This file was deleted.

0 comments on commit 65d1d95

Please sign in to comment.