Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add development docs #44

Merged
merged 10 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Loading