SmurfBot is a small Discord bot for a private Discord group.
Before the bot can listen to a Discord server, it needs to be added to that server.
The default command delimiter is $.
To run locally:
uv sync
SMURFBOT_DISCORD_TOKEN=your_discord_bot_token uv run python src/start.pyBy default, mutable bot data is stored under data/. Set SMURFBOT_DATA_DIR to use a different state directory.
The bot loads runtime settings from environment variables:
SMURFBOT_DELIM='$'
SMURFBOT_CONSUME_TIME=.1
SMURFBOT_REFRESH_GROUP_INTERVAL=600
SMURFBOT_DISCORD_TOKEN=your_discord_bot_tokenIf SMURFBOT_DISCORD_TOKEN is not set, the bot reads the Discord token from Vault. Set HASHICORP_VAULT_URL or SMURFBOT_VAULT_URL, plus one Vault auth token variable: HASHICORP_VAULT_TOKEN, SMURFBOT_VAULT_TOKEN, or VAULT_TOKEN.
The Compose service mounts a state directory from the repo:
./data -> /home/smurfbot/data
The container sets SMURFBOT_DATA_DIR=/home/smurfbot/data, and the data/tags and data/reminders directories keep bot data persistent across container restarts.
For Docker Compose, export the hosted Vault URL and token, or set SMURFBOT_DISCORD_TOKEN directly:
export HASHICORP_VAULT_URL=https://vault.example.com
export HASHICORP_VAULT_TOKEN=your_vault_tokenYou can also copy .env.example to .env; Docker Compose automatically reads .env for variable interpolation when it is in the same directory as docker-compose.yml.
You can use the Makefile wrappers:
make build
make up
make up-build
make down
make down-rmOr use Docker Compose directly:
docker compose build
docker compose up
docker compose up --build
docker compose down
docker compose down --rmi all --volumes --remove-orphansEvery command starts with the delimiter. The default delimiter is $.
| command | argument(s) | example |
|---|---|---|
| ping | none | $ping |
| git | none | $git |
| mock | [message] |
$mock this is a test |
| remind | See Reminder Command | $remind 1h check laundry |
| tag | See Tag System | $tag list |
| command | argument(s) | example |
|---|---|---|
| remind help | none | $remind help |
| remind list | none | $remind list |
| remind | [duration] [message] |
$remind 10m check the oven |
Reminder parameters:
duration: an integer followed by a unit with no space. Supported units aresfor seconds,mfor minutes,hfor hours, anddfor days.message: the reminder text. Everything after the duration is saved as the reminder message.
The bot rejects reminders longer than 31557600 seconds, which is about one year. Reminders are sent back to the channel where they were created.
Reminder files are stored as <data_dir>/reminders/<guild_id>.json. By default, local runs use data/reminders/<guild_id>.json. The bot creates one reminder file per Discord guild and removes expired reminders when it loads or lists them.
{
"id": 224644073795878913,
"name": "guild name",
"reminders": [
{
"user_id": 112784387862450176,
"name": "username",
"message": "check the oven",
"created_at": "2026-06-03T14:16:47",
"execution_time": "2026-06-03T14:26:47",
"timezone": "utc",
"guild_id": 224644073795878913,
"channel_id": 224644073795878913
}
]
}Field notes:
- top-level
id: Discord guild ID for this reminder file. - top-level
name: Discord guild name. reminders: active reminders for the guild.user_id: Discord user ID to mention when the reminder fires.- reminder item
name: Discord username captured when the reminder was created. message: reminder text.created_at: UTC creation time formatted asYYYY-MM-DDTHH:MM:SS.execution_time: UTC time when the reminder should fire, formatted asYYYY-MM-DDTHH:MM:SS.timezone: currently saved asutc.guild_id: Discord guild ID stored on the reminder record.channel_id: Discord channel ID where the reminder should be sent.
| command | argument(s) | example |
|---|---|---|
| create | [tag_name] [stuff] |
$tag create cooltag this is a cool tag |
| edit | [tag_name] [stuff] |
$tag edit cooltag this is a new edit |
| delete | [tag_name] |
$tag delete cooltag |
| none | [tag_name] |
$tag cooltag |
| list | none | $tag list |
| owner | [tag_name] |
$tag owner cooltag |
| gift | [tag_name] @mention |
$tag gift cooltag @new_owner |
| rename | [tag_name] [new_tag_name] |
$tag rename cooltag coolertag |
| help | none | $tag help |
[stuff] can be a string, a link, or an uploaded image. If [stuff] is an uploaded image, the message text should only be:
$tag create [tag_name]
The create, edit, delete, gift, and rename commands can only be used by the owner of the tag. The owner is whoever created the tag.
The owner command returns the owner user ID and attempts to match it to the owner's Discord username.
Tag files are stored as <data_dir>/tags/<guild_id>.json. By default, local runs use data/tags/<guild_id>.json.
{
"id": "guild id goes here",
"name": "guild name goes here",
"tags": {
"tag1": {
"content": "tag1 content",
"owner": "owner id"
},
"tag2": {
"content": "tag2 content",
"owner": "owner id"
}
}
}