A Facebook Messenger / Instagram DM chatbot that runs a small "alchemy" combination game: players send two symbols (emoji), and the bot tells them what those two ingredients make — or nudges them to try again if the combination is unknown.
It's a compact, fully-tested example of a webhook chatbot in async Rust (tokio + hyper), built with a clean ports-and-adapters layout so the game logic is completely decoupled from the Messenger platform.
Each player has a session that walks through a small state machine, driven entirely by the messages they send:
"stop" (any time) ──────────────┐
▼
New ──▶ DecidingToJoin ──"yes"──▶ ChoosingOption ... Quit
│ │ "no" │
greeting rules / bye combo ─┤─ known → result
├─ partial → "wrong combo"
└─ garbage → "invalid input"
Matching is emoji-aware: combinations are compared order-independently and the
Facebook variation selector (U+FE0F) is stripped, so ☕🍋 and 🍋 ☕️ resolve
to the same result.
Ports-and-adapters — the core domain knows nothing about Facebook or HTTP:
| Module | Role |
|---|---|
core/processing |
Per-message state machine that advances a session and queues responses |
core/game |
Game model: combination lookup, generic yes/no/stop matching, response templates |
core/types |
Domain types and the port traits (ResponseSender, DefinitionsRepository, SessionRepository) |
services/response |
Adapter: sends replies via the Facebook Graph API (/me/messages) |
services/definitions |
Adapter: loads channels and games from JSON files |
services/sessions |
Adapter: in-memory session store |
handler |
Wires an incoming platform message to the domain and back out to the sender |
fb_hook_srv |
The Messenger webhook server: verification handshake + event parsing |
Because the platform is behind traits, the whole game is exercised in tests with
mock senders and no network — see the tests modules (18 tests covering the
state machine, emoji matching, and webhook parsing).
The bot is data-driven. DATA_DIR holds:
channels.json— the pages/accounts the bot serves, each with its Facebook page access token used to send replies. Treat this as a secret; the copy intest_resourcescontains placeholders only.game-<n>.json— a game'sresults(each acombination→result), optional customresponses(greeting, rules, …), and optionalgeneric_answers(what counts as yes / no / stop).
All configuration is environment-driven:
| Variable | Default | Purpose |
|---|---|---|
PORT |
3021 |
Port the webhook server listens on |
TOKEN |
MY_TEST_TOKEN |
Webhook verify token for the subscription handshake |
DATA_DIR |
./src/test_resources/data |
Directory with channels.json + game-*.json |
RUST_LOG |
info |
Log level (env_logger) |
The Messenger webhook is served at GET/POST /api/webhook.
The toolchain is pinned with mise:
mise install # installs the pinned Rust toolchain
mise run test # run the test suite
mise run run # start the webhook server on $PORT
mise run lint # clippy (warnings denied)
mise run build # optimized release binaryOr with cargo directly from backend/:
cd backend
cargo test
cargo run --bin serverbackend/ Rust crate (the bot)
src/core/ domain: game rules + session state machine
src/services/ adapters: FB sender, file repo, session store
src/handler.rs platform ⇄ domain wiring
src/fb_hook_srv.rs Messenger webhook server
scripts/ Ansible deployment (static musl build, nginx + LetsEncrypt, systemd)
scripts/ builds a fully static Linux binary inside the rust-musl-builder
Docker image and ships it with Ansible: it provisions an nginx reverse proxy
with LetsEncrypt TLS, a dedicated service user, and a systemd unit. Per-host
inventory lives in scripts/hosts/ (gitignored). See scripts/deploy.sh.
A 2022 hobby project, kept as a small, self-contained example of an async-Rust chat bot with a clean, testable architecture.