Hubot chatbot connected to Discord, containerised with Docker and published to GitHub Container Registry (GHCR) via GitHub Actions.
- Prerequisites
- Create a Discord bot token
- Environment variables
- Run locally with Docker Compose
- Run locally with Docker
- Build locally
- GitHub Actions publish flow
- GHCR image naming convention
- Extending Hubot with additional scripts
| Tool | Version |
|---|---|
| Docker | 24+ |
| Docker Compose | v2+ |
| Node.js (local dev only) | 22 LTS |
| npm | 9+ |
| A Discord application & bot | — |
- Go to https://discord.com/developers/applications and click New Application.
- Give it a name, then open the Bot tab.
- Click Add Bot → Yes, do it!.
- Under Privileged Gateway Intents enable Message Content Intent (required for reading message content).
- Click Reset Token and copy the token – this becomes
HUBOT_DISCORD_TOKEN. - Use OAuth2 → URL Generator (scopes:
bot; permissions:Send Messages,Read Message History) to generate an invite link, then invite the bot to your server.
Copy .env.example to .env and fill in the values:
cp .env.example .env| Variable | Required | Default | Description |
|---|---|---|---|
HUBOT_DISCORD_TOKEN |
yes | — | Discord bot token |
HUBOT_NAME |
no | hubot |
The name the bot responds to |
HUBOT_OWNER |
no | — | Owner name shown in help |
HUBOT_DESCRIPTION |
no | — | Bot description shown in help |
HUBOT_LOG_LEVEL |
no | info |
Log verbosity: debug | info | warning | error |
REDIS_URL |
no | redis://localhost:6379 |
Redis connection URL used by hubot-redis-brain |
LOKI_URL |
no | http://host.docker.internal:3100 |
Loki base URL used by hubot node logs |
LOKI_USERNAME |
no | — | Optional Loki basic auth username |
LOKI_PASSWORD |
no | — | Optional Loki basic auth password |
HUBOT_AUTHZ_REPORT_ENABLED |
no | false |
Enable scheduled AUTHZ denial summary embeds |
HUBOT_AUTHZ_REPORT_CHANNEL_ID |
no | — | Discord channel ID used for scheduled AUTHZ summary embeds |
HUBOT_AUTHZ_REPORT_RUN_MINUTE |
no | 0 |
Wall-clock minute each hour to run (top, bottom, or 0-59) |
HUBOT_AUTHZ_REPORT_LOOKBACK_MINUTES |
no | 60 |
Loki lookback window in minutes for each scheduled summary |
HUBOT_AUTHZ_REPORT_TOP_LIMIT |
no | 10 |
Maximum topic+username pairs listed in each embed |
HUBOT_AUTHZ_REPORT_SEND_EMPTY |
no | false |
If true, post an embed even when no denials are found |
HUBOT_DISCORD_PERMISSION_GUILD_ID |
no | MQTT_ADMIN_GUILD_ID |
Discord guild ID used by command-bus role checks for DM commands |
NODE_LOGS_CACHE_TTL_SECONDS |
no | 30 |
Redis cache TTL for node.logs replies; set 0 to disable |
MONGO_HOST |
no | host.docker.internal |
MongoDB host used when MONGO_URL is not set |
MONGO_PORT |
no | 27017 |
MongoDB port used when MONGO_URL is not set |
MONGO_USERNAME |
no | — | MongoDB username used when MONGO_URL is not set |
MONGO_PASSWORD |
no | — | MongoDB password used when MONGO_URL is not set |
MONGO_AUTH_SOURCE |
no | mqtt |
MongoDB auth source used when MONGO_URL is not set |
MONGO_URL |
no | — | Optional full MongoDB connection string override for the mqtt account commands |
MONGO_DB_NAME |
no | mqtt |
MongoDB database name used by the mqtt account commands |
MQTT_ADMIN_GUILD_ID |
no | — | Discord guild ID used to verify admin role membership for DM commands |
MQTT_ADMIN_ROLE_IDS |
no | — | Comma-separated Discord role IDs allowed to run MQTT admin commands |
EMQX_API_URL |
no | — | EMQX HTTP API base URL used for MQTT ban commands and active client lookup |
EMQX_API_KEY |
no | — | EMQX API key used for HTTP Basic Auth |
EMQX_API_SECRET |
no | — | EMQX API secret used for HTTP Basic Auth |
EMQX_BAN_DEFAULT_DAYS |
no | 7 |
Default number of days used by mqtt.ban |
Security note: Never commit your
.envfile. It is listed in.gitignore.
A docker-compose.yml is provided for local testing. It builds the image and
passes your .env file into the container automatically:
# Copy the example env file and fill in your token
cp .env.example .env
# Build and start (foreground – useful for watching logs)
docker compose up --build
# Build and start detached
docker compose up --build -d
docker compose logs -f
# Tear down
docker compose down# Build the image
docker build -t hubot-discord .
# Run with environment variables from your .env file
docker run --rm --env-file .env hubot-discord# Install dependencies
npm install
# Start the bot (reads variables from the shell environment)
export HUBOT_DISCORD_TOKEN=your-token-here
npm start
# Start with verbose logging
npm run dev
# Run tests
npm testThe workflow at .github/workflows/docker-publish.yml runs on:
| Trigger | Build | Push to GHCR |
|---|---|---|
Pull request targeting main |
✅ | ❌ |
Push to main |
✅ | ✅ |
Push of a v* tag |
✅ | ✅ |
Authentication uses the automatic GITHUB_TOKEN — no extra secrets are required.
If the first push to GHCR fails with a 403, go to:
Settings → Actions → General → Workflow permissions
and set it to Read and write permissions, then re-run the workflow.
ghcr.io/flmesh/hubot:latest # tracks main
ghcr.io/flmesh/hubot:sha-<shortsha> # every pushed commit
ghcr.io/flmesh/hubot:1.2.3 # semver release (v1.2.3 tag)
ghcr.io/flmesh/hubot:1.2
ghcr.io/flmesh/hubot:1
- Add a
.js(ES module) file to thescripts/directory. - Export a default function that receives the
robotinstance:
export default (robot) => {
robot.commands.register({
id: "hello",
description: "Say hello",
confirm: "never",
handler: async () => "Hello there!",
});
};Command specs can require Discord roles through the command bus:
robot.commands.register({
id: "admin.example",
description: "Admin-only example",
permissions: {
roles: ["env:MQTT_ADMIN_ROLE_IDS"],
},
handler: async () => "Only admins can run this.",
});Role entries can be raw Discord role IDs or env:VARIABLE references that
expand to comma-separated role IDs. For DM commands, role membership is checked
against HUBOT_DISCORD_PERMISSION_GUILD_ID, falling back to
MQTT_ADMIN_GUILD_ID.
- Rebuild the Docker image (or restart
npm startlocally) - Hubot loads all files inscripts/automatically.
For reusable community scripts, install them via npm and list them in external-scripts.json.
This repository includes a built-in command that queries Loki for EMQX log entries for a single client ID.
Supported command form:
hubot node.logs clientid:<clientid> [minutes:<n>] [limit:<n>]
Behavior:
- Input is an 8-character lowercase hexadecimal node ID. It can be passed as
a1b2c3d4or!a1b2c3d4. - The command searches EMQX entries that match approved client ID patterns, and then refines results to entries that resolve to the provided node ID.
- If
minutesis omitted, it defaults to15. limitis optional and capped server-side to avoid oversized chat messages.- Returned fields are fixed to: timestamp, level, clientid, action, topic, and msg.
- Replies are cached in Redis for
30seconds by default to reduce repeated Loki queries. SetNODE_LOGS_CACHE_TTL_SECONDS=0to disable caching. - Command help is available with
@hubot node.logs --help.
Adapter note: The Discord adapter is loaded by its full npm package name
@hubot-friends/hubot-discord. Do not use the short aliasdiscord– Hubot will fail to resolve the module.
This repository includes a scheduled AUTHZ denial summary that queries Loki and posts a Discord embed to a configured channel.
Schedule behavior:
- Runs once per hour at the wall-clock minute defined by
HUBOT_AUTHZ_REPORT_RUN_MINUTE. - Supports top-of-hour (
topor0) and bottom-of-hour (bottomor30) scheduling.
Query behavior:
- Aggregates
authorization_permission_deniedevents from EMQX logs by(topic, username)overHUBOT_AUTHZ_REPORT_LOOKBACK_MINUTES. - Uses Loki API directly, with optional basic auth via
LOKI_USERNAMEandLOKI_PASSWORD.
Manual trigger:
hubot authz.report.now
This repository includes an initial MongoDB-backed MQTT account workflow for EMQX.
Supported command forms:
hubot mqtt.request username:<username>
hubot mqtt.my-account
hubot mqtt.rotate
hubot mqtt.reset username:<username>
hubot mqtt.whois username:<username>
hubot mqtt.disable username:<username>
hubot mqtt.enable username:<username>
hubot mqtt.profile.set username:<username> profile:<profile>
hubot mqtt.profile.list
hubot mqtt.profile.show profile:<profile>
hubot mqtt.profile.apply profile:<profile>
Behavior:
mqtt.requestvalidates the requested username againstusername_policy, loads the current default profile, provisions the account in MongoDB, and sends the generated password by DM.- Username policy currently honors
min_length,max_length,reserved_usernames, andbanned_substrings, while format validation is intentionally fixed to lowercase letters, digits, underscores, and hyphens with a leading lowercase letter. mqtt.my-accountshows the caller's current MQTT username, status, profile, active EMQX client connections when the EMQX API is configured, and creation time.mqtt.rotaterotates the caller's password and sends the new password by DM.mqtt.resetresets another user's password and attempts to DM the new password to the account owner.mqtt.whoisshows admin account details, including active EMQX client connections when the EMQX API is configured.mqtt.reset,mqtt.whois,mqtt.disable,mqtt.enable, andmqtt.profile.setare admin-only and require the caller to hold a configured Discord role fromMQTT_ADMIN_ROLE_IDS.mqtt.profile.listlists stored profiles, their status, default flag, and rule counts.mqtt.profile.showdisplays a profile's metadata and ACL template rules.mqtt.profile.applyis admin-only and reapplies the selected active profile template to all accounts currently assigned to that profile.- Profile templates in MongoDB now use a richer internal rule schema with
who,action, and topic match objects. Hubot compiles those templates into the flattermqtt_aclrows EMQX expects. - The current materializer supports
username,username_re,clientid,clientid_re, andipaddrselectors plus optionalqosandretainaction qualifiers. Topic entries are currently compiled only for filter-style matches. mqtt.my-account,mqtt.whois,mqtt.profile.list, andmqtt.profile.showreturn Discord embeds when running under the Discord adapter for better readability.help,help <prefix>, and<command> --helpare provided by Hubot's command bus and currently list registered commands without filtering by the caller's Discord role. Access control still applies when a protected command is executed.- The commands can either use
MONGO_URLdirectly or construct a connection string fromMONGO_HOST,MONGO_PORT,MONGO_USERNAME,MONGO_PASSWORD,MONGO_AUTH_SOURCE, andMONGO_DB_NAME. - The discrete settings are the recommended path because Hubot safely encodes the MongoDB username and password when building the URI.
- Command usage is also written to the
mqtt_auditcollection with phases such asattempted,confirm_requested,confirmed,cancelled,succeeded,denied, andfailed.