A Discord bot written in Rust. Third rewrite of a passion project — this time with async, proper error handling, and a macro-driven database layer.
| Command group | What it does |
|---|---|
/mimic |
Create named personas (name + avatar). Talk as them via Discord webhooks. Enable auto-mode to have every message you send automatically re-posted as your active mimic. |
/schedule |
Add timezone-aware events with date and time. The bot DMs you a reminder when the event arrives. Reminders survive bot restarts. |
/profile |
View and customise a profile card with bio, banner, colorway, equipped title, and badges. Custom banner and custom hex colorway charge tabs every time you set them; equipping an owned named colorway is free. Custom title is a one-time unlock. |
/shop |
browse the catalog, view your inventory, buy titles / colorways / unlocks / lootboxes, change your custom-role colour or name (buy rolecolor, buy rolename), or gift cosmetics to other users. |
/color preview |
Preview a hex colour as a 256×256 PNG swatch (free). |
/daily |
Claim 10 tabs once every 24 hours. Consecutive days build a streak that adds up to +5 bonus tabs. |
/balance |
Check your tab balance. |
/leaderboard |
Top tab-holders in the guild. |
/achievements |
Show your unlocked and locked achievements. |
/pfp |
Show a user's avatar. |
/vox say |
Synthesise text as speech using the DECtalk TTS engine and post the WAV file. |
A passive tab-reaction faucet also runs in the background: a small chance per guild message spawns a tab-emoji reaction; the first user to click it receives 5 tabs.
- Rust (edition 2024, stable toolchain)
- A Discord bot token in
s.json(see Configuration) - The DECtalk shared libraries — pre-built copies live in
vendor/dectalk/dist/
On Linux the DECtalk .so files must be on LD_LIBRARY_PATH at runtime. The Docker setup handles this automatically.
Create s.json in the working directory (next to the binary or in the project root when running with cargo run):
{ "token": "your-discord-bot-token-here" }Do not commit this file. Add it to .gitignore.
The bot reads and writes three JSON files in the working directory:
| File | Contents |
|---|---|
user.json |
All per-user data (mimics, schedule events, wallet balances). Created automatically on first run. |
wallet_list.json |
Tracks which users have claimed their daily reward today. Resets at midnight. |
s.json |
Bot token (you provide this). |
cargo build --release
LD_LIBRARY_PATH=vendor/dectalk/dist ./target/release/logosV3Pass --log-level debug (or -l debug) for verbose output:
./target/release/logosV3 --log-level debug# Build the image
docker build -t logos-bot:latest .
# Run (mount s.json and data files from the host)
docker run -d --name logos --restart unless-stopped \
-v $(pwd)/s.json:/app/s.json \
-v $(pwd)/user.json:/app/user.json \
logos-bot:latestsrc/
├── main.rs # Entry point — CLI args, logging, client startup
├── framework.rs # Poise framework construction, persistence task, schedule task
├── handlers.rs # Discord event handler (mimic auto-mode, faucet) and error handler
├── logging.rs # SimpleLogger initialisation
├── setup.rs # Token loading, re-exports for main.rs
├── utils.rs # reply_ok/err/info helpers, embed builder, webhook helper
├── dectalk.rs # Safe Rust wrapper around the DECtalk C library
├── commands/
│ ├── mod.rs # Command registry + general commands (help, pfp, daily, balance,
│ │ # color, leaderboard, achievements) + admin prefix commands
│ ├── vox.rs # /vox say — DECtalk TTS
│ ├── mimic/
│ │ ├── mod.rs # /mimic add, list, say
│ │ ├── set.rs # /mimic set active_mimic, channel_override, auto
│ │ └── delete.rs # /mimic delete mimic, active_mimic, channel_override
│ ├── schedule/
│ │ └── mod.rs # /schedule add, list, delete, set_tz
│ ├── profile/
│ │ ├── mod.rs # /profile view (parent registers set + unset)
│ │ ├── set.rs # /profile set bio, banner, colorway, namedcolorway,
│ │ │ # title, customtitle, badges
│ │ └── unset.rs # /profile unset title, colorway, banner, badges
│ └── shop/
│ ├── mod.rs # /shop browse, /shop inventory (parent registers buy + gift)
│ ├── buy.rs # /shop buy title, colorway, unlock, lootbox,
│ │ # rolecolor, rolename
│ └── gift.rs # /shop gift title, colorway
└── pawthos/ # Core domain — all data structures and logic
├── mod.rs
├── consts/ # Magic numbers and strings (costs, colours, emoji, faucet
│ # tuning, lootbox tuning, …)
├── types/ # Type aliases (Error, Context, Reply, Result)
├── traits/ # UserDbSpec marker trait + impl_user_db_spec! macro
├── enums/ # Error types (one per feature), EmbedType, PersistentData
└── structs/ # Data, UserDB, User, the five sub-structs (MimicUser,
# ScheduleUser, WalletUser, ProfileUser, InventoryUser),
# plus shop_catalog (static catalog data) and badge
All per-user state lives in a single RwLock<UserDB> inside Data. Five marker types (MimicDbMarker, ScheduleDbMarker, WalletDbMarker, ProfileDbMarker, InventoryDbMarker) implement the UserDbSpec trait to route generic read/write helpers to the correct field on each User. The def_db_access! macro in data.rs generates the public async methods from one line each.
Every write automatically snapshots the database and sends it to the persistence task over an mpsc channel — no command ever touches the filesystem directly.
The shop catalogue (titles, named colorways, badges, achievements, the custom-title unlock) lives in pawthos/structs/shop_catalog.rs as const arrays. Each entry has a stable string ID; InventoryUser stores those IDs in Vec<String> collections, and ProfileUser stores the IDs of currently equipped items. Catalog IDs are persisted data — renaming one is a migration, not a refactor.
Three things charge tabs but never produce a catalog item:
/profile set banner <url|attachment>—BANNER_SET_COSTtabs per call. Banners are user-supplied; there is no banner catalog./profile set colorway <hex>—CUSTOM_COLORWAY_SET_COSTtabs per call. Equipping an owned named colorway via/profile set namedcolorwayis free./shop buy rolecolor <hex>//shop buy rolename <text>—ROLE_COLOR_COST/ROLE_NAME_COSTtabs per call. Manage the user's zero-width-space-prefixed colour role on the current guild.
Custom title text is the only remaining one-time unlock (unlocked_custom_title).
A single background tokio::spawn loop receives PersistentData messages and handles all file I/O sequentially. Writes are atomic: the bot writes to a .tmp file and renames it into place, so a crash mid-write never corrupts the database.
A second background loop receives (UserId, ScheduleEvent) pairs and spawns a tokio::time::sleep task for each one. All saved events are re-queued on startup so reminders survive restarts.
When auto-mode is enabled, the Discord Message event handler intercepts every message the user sends, re-posts it via a per-channel webhook as the active mimic persona, and deletes the original message. Channel overrides let the user use a different mimic in specific channels.
The same Message handler rolls a per-message chance (FAUCET_TRIGGER_CHANCE in consts/) to drop a tab-emoji reaction on the message, gated by a global cooldown. The first user to click the reaction receives FAUCET_REWARD tabs; the bot's reaction is removed after FAUCET_EXPIRY_SECS. This is why GUILD_MESSAGE_REACTIONS is in the gateway intents.
- Write the command function in
src/commands/myfeature.rs. - Add
mod myfeature;anduse crate::commands::myfeature::*;tocommands/mod.rs. - Add
myfeature()to thereturn_commands()vector. - Use
ctx.send(utils::reply_ok("Title", "body")).await?.
- Create
src/pawthos/structs/myfeature_user.rs— the per-user sub-struct. - Create
src/pawthos/enums/myfeature_errors.rs— athiserrorerror enum with aNoUserFoundvariant. - Add
pub myfeature: MyfeatureUsertosrc/pawthos/structs/user.rs. - Add a
Myfeature(#[from] MyfeatureError)variant topawthos_errors.rs. - Add
pub struct MyfeatureDbMarker;andimpl_user_db_spec!(MyfeatureDbMarker, MyfeatureUser, myfeature);totraits/mod.rs. - Add
def_db_access!(with_myfeature_user_read, with_myfeature_user_write, MyfeatureDbMarker, MyfeatureUser, MyfeatureError, MyfeatureError::NoUserFound);insideimpl Dataindata.rs. - Export the new modules from the relevant
mod.rsfiles. - Write the command file and register it (see above).