foukoapi 0.1.0-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 is approaching its first public release (0.1.0-alpha.*). 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 /help and /lang. Turn on with one builder call, localised per user.
  • 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.
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). AnyStorage is a boxed version.
Accounts     — cross-platform user linking. Sits on top of Storage.

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.

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.

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
Built-in /help, /lang, /link ✅ 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

Latest stable Rust. The crate pins an explicit MSRV once 0.1 ships.

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.