foukoapi 0.1.1-alpha.1

Cross-platform bot framework in Rust. Write your handlers once, run the same bot on Telegram and Discord with shared accounts, embeds, keyboards and SQLite storage.
Documentation

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 at 0.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 Platform trait. Your handler doesn't care which one it's running on.
  • Unified buttons and keyboards. Build a Keyboard once, each adapter turns it into Telegram inline keyboards or Discord message components.
  • Unified embeds. Embed with title / description / fields / footer / color renders as a real Discord embed and as HTML on Telegram, from the same code.
  • Pluggable Storage trait. 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_link lets users tie their Telegram and Discord identities with a 6-char code. Ships with a ready-made /link command.
  • Built-in economy. Economy gives 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. I18n is a plain key/language catalogue with English fallback and {} placeholders. No macros, no build step, no .po files.
  • Push messages with Notifier. Send into any chat without an incoming event - reminders, scheduled jobs. send_dm reaches a user's DMs by id, user_name resolves a display name outside any update.
  • Rate limiting and flood watch. A per-user RateLimiter is on by default (tune with Bot::rate_limit), and Bot::on_flood fires a handler when the total update flow spikes - handy against spam waves.
  • Colorful startup banner. The banner module prints gradient ASCII art and an aligned status table; honors NO_COLOR and non-TTY output.
  • Secrets at rest. Secret (feature crypto) encrypts strings with AES-256-GCM, keyed off an operator passphrase.
  • Handler quality-of-life. Ctx::typing() shows a typing indicator, reply_temporary auto-deletes after N seconds, avatar_url / banner_url / chat_info look up user and chat details cross-platform.
  • Built-in /help and /lang. Turn on with one builder call, localised per user. Group commands into sections with Bot::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
[dependencies]
foukoapi = "0.1"
tokio = { version = "1", features = ["full"] }
use foukoapi::{Bot, Button, Keyboard, Platform, Reply};

#[tokio::main]
async fn main() -> foukoapi::Result<()> {
    Bot::new()
        .add_platform(Platform::telegram(std::env::var("TG_TOKEN")?))
        .add_platform(Platform::discord(std::env::var("DISCORD_TOKEN")?))
        .command("/help", |ctx| async move {
            ctx.reply("hi! i work everywhere").await
        })
        .command("/menu", |ctx| async move {
            let kb = Keyboard::new()
                .row([Button::callback("roll", "roll"), Button::callback("coin", "coin")])
                .row([Button::url("website", "https://bot.fouko.xyz")]);
            ctx.reply_with(Reply::text("pick one").keyboard(kb)).await
        })
        .run()
        .await
}

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.
foukoapi = { version = "0.1", default-features = false, features = ["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:

Bot::new()
    .with_accounts(accounts.clone())       // needed by /help (i18n), /lang, /link
    .with_default_help()                   // lists every described command
    .with_default_lang_command(["en", "ru"]) // picker with inline buttons
    .with_default_link_command()           // 6-char code flow + unlink button

Minimal bot with state

use foukoapi::{open_storage, Accounts, Bot, Embed, Platform, Reply};

#[tokio::main]
async fn main() -> foukoapi::Result<()> {
    foukoapi::bootstrap_env()?;
    let storage = open_storage()?;               // reads FOUKO_DB
    let accounts = Accounts::with_arc(storage.clone());

    Bot::new()
        .with_accounts(accounts.clone())
        .add_platform(Platform::telegram(std::env::var("TG_TOKEN")?))
        .add_platform(Platform::discord(std::env::var("DISCORD_TOKEN")?))
        .command_described("/ping", "health check", |ctx| async move {
            let em = Embed::new().title("\u{1F3D3} Pong").description("alive");
            ctx.reply_with(Reply::embed(em)).await
        })
        .with_default_help()
        .with_default_lang_command(["en", "ru"])
        .with_default_link_command()
        .run()
        .await
}

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_link issues a 6-char code valid for 5 minutes.
  • Accounts::redeem_link on the other side ties them together.
  • The built-in /link command (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 /linkUnlink 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 foukoapi::{Accounts, Economy, Metric, PlatformKind};

# async fn demo(accounts: Accounts) -> foukoapi::Result<()> {
let econ = Economy::new(accounts);
let d = PlatformKind::Discord;

econ.add_xp(d, "42", 30).await?;          // -> XpGain { xp, coins_minted }
let w = econ.wallet(d, "42").await;       // { xp, coins, level }
econ.add_coins(d, "42", 100).await?;
econ.transfer((d, "42"), (d, "7"), 25).await?;   // send coins, atomic

let top = econ.leaderboard(Metric::Xp, 10).await; // ranked players
let me  = econ.rank_of(d, "42", Metric::Xp).await; // your position
# Ok(())
# }

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 foukoapi::I18n;

let i18n = I18n::new()
    .add("hi", &[("en", "Hello"), ("ru", "Привет"), ("de", "Hallo")])
    .add("bye", &[("en", "Bye, {}!"), ("ru", "Пока, {}!")]);

assert_eq!(i18n.t("ru", "hi"), "Привет");
assert_eq!(i18n.t("fr", "hi"), "Hello");           // English fallback
assert_eq!(i18n.tf("en", "bye", &["Sam"]), "Bye, Sam!");

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 foukoapi::util::{capitalize, progress_bar, urlencode};

assert_eq!(capitalize("telegram"), "Telegram");
assert_eq!(progress_bar(3, 10, 10), "▰▰▰▱▱▱▱▱▱▱");
assert_eq!(urlencode("hello world"), "hello%20world");
  • 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)
cargo run --example quickstart

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.