FoukoApi
A Rust framework for building cross-platform bots. Write a command once, run it on Telegram, Discord and anything you plug in next.
api.fouko.xyz · fouko.xyz · Discord · Telegram
[!NOTE] FoukoApi shipped its first public release (
0.1.0-alpha.1) and is now at0.1.1-alpha.1. The public surface below is stable in spirit; feature-gated internals may still shift.
What is it
FoukoApi is a small, opinionated framework that lets you describe a bot once - its commands, handlers, state, linked-account UX - and run the same bot on multiple chat platforms without forking logic for each one.
No custom DSL, no macros you have to learn. Just Rust functions and a Bot builder.
Contents
Features
- One codebase, many platforms. Telegram and Discord shipped, more adapters possible behind the same
Platformtrait. Your handler doesn't care which one it's running on. - Unified buttons and keyboards. Build a
Keyboardonce, each adapter turns it into Telegram inline keyboards or Discord message components. - Unified embeds.
Embedwith title / description / fields / footer / color renders as a real Discord embed and as HTML on Telegram, from the same code. - Pluggable
Storagetrait. Any async key-value store works. Bundled: in-memory (great for tests) and SQLite (great for a self-hosted bot). - Built-in account linking.
Accounts::start_link/redeem_linklets users tie their Telegram and Discord identities with a 6-char code. Ships with a ready-made/linkcommand. - Built-in economy.
Economygives you XP, a level curve, coins, atomic transfers, cooldowns, achievements and a leaderboard out of the box. Balances follow the account link, so a user's XP is the same on Telegram and Discord. - Translations without the ceremony.
I18nis a plain key/language catalogue with English fallback and{}placeholders. No macros, no build step, no.pofiles. - Push messages with
Notifier. Send into any chat without an incoming event - reminders, scheduled jobs.send_dmreaches a user's DMs by id,user_nameresolves a display name outside any update. - Rate limiting and flood watch. A per-user
RateLimiteris on by default (tune withBot::rate_limit), andBot::on_floodfires a handler when the total update flow spikes - handy against spam waves. - Colorful startup banner. The
bannermodule prints gradient ASCII art and an aligned status table; honorsNO_COLORand non-TTY output. - Secrets at rest.
Secret(featurecrypto) encrypts strings with AES-256-GCM, keyed off an operator passphrase. - Handler quality-of-life.
Ctx::typing()shows a typing indicator,reply_temporaryauto-deletes after N seconds,avatar_url/banner_url/chat_infolook up user and chat details cross-platform. - Built-in
/helpand/lang. Turn on with one builder call, localised per user. Group commands into sections withBot::category. - Native command menus. Your registered commands are published as Discord slash commands and Telegram's command menu automatically, so they show up in each client's autocomplete.
- Type-safe, async from the ground up. Built on
tokio. No unsafe, no build-time code generation, no custom DSL. - Pluggable adapters. Every platform is a trait impl. Rolling your own for a niche chat service is a weekend project.
Quick start
# Cargo.toml
[]
= "0.1"
= { = "1", = ["full"] }
use ;
async
That single /help command responds on every platform you added, and the Keyboard is rendered natively on each one (inline keyboard in Telegram, message components in Discord).
Mini-wiki
Cargo features
| Feature | Default | What it does |
|---|---|---|
telegram |
yes | Adds the Telegram adapter (teloxide). |
discord |
yes | Adds the Discord adapter (serenity). |
sqlite |
yes | Enables the bundled SQLite storage backend. |
crypto |
no | Secret - AES-256-GCM at-rest encryption. |
full |
no | Turns on everything above. |
= { = "0.1", = false, = ["telegram", "sqlite"] }
Environment variables
bootstrap_env() creates a commented .env template on the first run. Defaults:
| Variable | What it is |
|---|---|
TG_TOKEN |
Telegram bot token from @BotFather. |
DISCORD_TOKEN |
Discord bot token (Developer Portal → Bot → Reset Token). |
FOUKO_DB |
Storage URL. sqlite:./foukobot.sqlite, memory:, or a custom scheme your own Storage understands. If unset, a SQLite file is placed next to the binary. |
RUST_LOG |
Standard tracing filter, e.g. info,foukoapi=debug. |
Core types
Bot - builder. Add platforms, commands, on_message hooks, defaults.
Platform - "here's a Telegram/Discord token". Plugged into Bot::add_platform.
Ctx - everything a handler needs: platform, user_id, text, args,
reply / reply_with / edit_reply, callback_data, is_dm, ...
Reply - what you send back. Reply::text(..), Reply::embed(..),
with optional .keyboard(kb).
Embed - rich reply card. .title / .description / .field(_inline) /
.footer / .image / .thumbnail / .url / .color.
Keyboard - rows of Buttons. Button::callback("label", "cb_data") or
Button::url("label", "https://...").
Storage - async KV store trait (get/set/del, set_nx write-if-absent,
list_prefix). AnyStorage is a boxed version.
Accounts - cross-platform user linking. Sits on top of Storage.
Economy - XP, levels, coins, transfers, cooldowns, achievements,
leaderboard. Sits on top of Accounts so balances follow
the account link.
I18n - key/language string catalogue with English fallback and tf(..)
placeholder filling.
Notifier - outbound handle: push into a chat or a user's DMs without an
incoming event, resolve display names by id.
RateLimiter - per-user rate limiting; on by default with a relaxed policy.
Built-in commands
One builder call each, all localised and DM-aware where it makes sense:
new
.with_accounts // needed by /help (i18n), /lang, /link
.with_default_help // lists every described command
.with_default_lang_command // picker with inline buttons
.with_default_link_command // 6-char code flow + unlink button
Minimal bot with state
use ;
async
Account linking flow
┌─ Telegram ─┐ ┌─ Discord ─┐
│ /link │ ── 6-char code ─▶ │
│ │ │ /link CODE │
│ │ ◀── linked ─────│ │
└────────────┘ └────────────┘
│ │
└──── pick primary (once) ───┘
│
primary keeps XP/coins/settings
the other side stays clean
┌─ later, anywhere ─┐
│ /link → Unlink │
│ │
│ primary keeps its │
│ profile, other │
│ side → fresh │
└───────────────────┘
Accounts::start_linkissues a 6-char code valid for 5 minutes.Accounts::redeem_linkon the other side ties them together.- The built-in
/linkcommand (Bot::with_default_link_command) adds an inline-button primary picker that locks in the choice exactly once. After that, the only way to pick a different primary is to/link→ Unlink and start over. - After
Accounts::unlink, the primary-ident keeps its data (XP/coins/settings/lang); the ex-partner platform falls back to its own ident and therefore starts from a clean slate.
Economy
Economy wraps the same Accounts you already have, so a user who linked two platforms shares one wallet:
use ;
# async
Coins are minted at one per XP_PER_COIN (10) XP; add_xp returns an XpGain so you can announce freshly minted coins. Balance updates are serialised, so parallel grants don't lose XP and concurrent transfers can't overdraw a wallet. Cooldowns (cooldown_remaining / touch_cooldown), achievements (grant_achievement / achievements) and cosmetic slots (title, color) round it out for /daily, /gamble and a profile card.
Translations (I18n)
use I18n;
let i18n = new
.add
.add;
assert_eq!;
assert_eq!; // English fallback
assert_eq!;
Missing translations fall back to English; a missing key falls back to the key itself, so nothing ever renders blank.
Utility helpers (foukoapi::util)
Small, platform-agnostic helpers that you'll reach for in pretty much every bot:
use ;
assert_eq!;
assert_eq!;
assert_eq!;
capitalize(&str) -> String- uppercases the first char, UTF-8 safe.progress_bar(done, total, width) -> String-▰/▱bar of the given width.urlencode(&str) -> String- minimal percent-encoder for query-string args.split_chunks(&str, limit) -> Vec<String>- split long text on line/word boundaries; the adapters use it to dodge platform length limits.
Status
| Platform | Status |
|---|---|
| Telegram | ✅ Working |
| Discord | ✅ Working |
| Feature | Status |
|---|---|
| Command router | ✅ Working |
| Keyboards / inline buttons | ✅ Working |
| Embeds (Discord + HTML TG) | ✅ Working |
| Storage (memory + sqlite) | ✅ Working |
| Account linking + primary | ✅ Working |
| Economy (XP, coins, achievements, leaderboard) | ✅ Working |
Translations (I18n) |
✅ Working |
Built-in /help, /lang, /link |
✅ Working |
Push messages (Notifier) |
✅ Working |
| Rate limiting + flood watch | ✅ Working |
| Startup banner | ✅ Working |
Secrets at rest (crypto) |
✅ Working |
| Util helpers | ✅ Working |
| Argument parsing helpers | ⏳ Planned |
| Middleware chain | ⏳ Planned |
Examples
The examples/ directory is where ready-to-run sample bots live. A reference bot that uses every feature is also published as FoukoBot - its source is a good place to watch the API take shape.
# run the quickstart example (needs TG_TOKEN / DISCORD_TOKEN)
MSRV
Rust 1.75 or newer (pinned via rust-version in Cargo.toml).
Contributing
Issues and PRs are welcome. The repo follows the usual "fork, branch, PR" flow. Be kind in the tracker, keep diffs small, and run cargo fmt + cargo clippy --all-targets before opening a PR.
License
MIT - see LICENSE.
Part of the Fouko family.