<div align="center">
<img src="https://fouko.xyz/assets/brand/LogoGold.png" alt="FoukoApi" width="96" height="96">
# FoukoApi
**A Rust framework for building cross-platform bots.**
Write a command once, run it on Telegram, Discord and anything you plug in next.
<a href="https://crates.io/crates/foukoapi"><img alt="crates.io" src="https://img.shields.io/crates/v/foukoapi?style=for-the-badge&color=fbbf24&labelColor=0a0a0f"></a>
<a href="https://docs.rs/foukoapi"><img alt="docs.rs" src="https://img.shields.io/docsrs/foukoapi?style=for-the-badge&color=f97316&labelColor=0a0a0f"></a>
<a href="https://github.com/FoukoDev/FoukoApi/actions"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/FoukoDev/FoukoApi/ci.yml?style=for-the-badge&labelColor=0a0a0f"></a>
<a href="LICENSE-MIT"><img alt="License" src="https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-6366f1?style=for-the-badge&labelColor=0a0a0f"></a>
<a href="https://api.fouko.xyz">api.fouko.xyz</a> · <a href="https://fouko.xyz">fouko.xyz</a> · <a href="https://discord.gg/rx9nXt735R">Discord</a> · <a href="https://t.me/foukoo">Telegram</a>
</div>
---
> [!NOTE]
> FoukoApi is in its alpha series (currently `0.1.2-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
- [Quick start](#quick-start)
- [Mini-wiki](#mini-wiki)
- [Cargo features](#cargo-features)
- [Environment variables](#environment-variables)
- [Core types](#core-types)
- [Built-in commands](#built-in-commands)
- [Minimal bot with state](#minimal-bot-with-state)
- [Account linking flow](#account-linking-flow)
- [Economy](#economy)
- [Translations (`I18n`)](#translations-i18n)
- [Utility helpers (`foukoapi::util`)](#utility-helpers-foukoapiutil)
- [Status](#status)
- [Examples](#examples)
- [MSRV, contributing, license](#msrv)
## 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.
- Build a `Keyboard` once - Telegram gets inline keyboards, Discord gets message components. A keyboard that fits Telegram but overflows Discord's 5x5 cap is repacked there instead of rejected.
- `Embed` with title / description / fields / footer / color renders as a real Discord embed and as HTML on Telegram, from the same code.
- **Pluggable storage.** Any async key-value store works behind the `Storage` trait. Bundled: in-memory (good for tests) and SQLite (good for a self-hosted bot).
- **Account linking.** `Accounts::start_link` / `redeem_link` ties Telegram and Discord identities with a 6-char code. A ready-made `/link` command ships with it.
- **Economy.** XP, a level curve, coins, atomic transfers, cooldowns, achievements, a leaderboard. Balances follow the account link, so a user's XP is the same on both platforms.
- `I18n` - a plain key/language catalogue with English fallback and `{}` placeholders. No macros, no build step, no `.po` files.
- **Push messages.** `Notifier` sends 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.
- Per-user rate limiting 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: gradient ASCII art plus 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.
- **GenAI client.** `GenClient` (feature `genai`) speaks to any OpenAI-compatible host: chat with SSE streaming, tool calling, vision input, model discovery with capability tags, image/video/speech generation with selectable voices.
- **Telegram Mini Apps.** `Button::web_app` opens a Mini App from a button (Discord falls back to a link), and `validate_init_data` (feature `webapp`) verifies the app's `initData` signature server-side.
- **Quick https tunnels.** `tunnel::cloudflared_quick` (feature `tunnel`) gets a free public https URL for a local port via the `cloudflared` binary - a Mini App can run from a laptop behind NAT.
- 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`, one builder call each, localised per user. Group commands into sections with `Bot::category`.
- Registered commands are published as Discord slash commands *and* Telegram's command menu automatically, so they show up in each client's autocomplete.
- Async from the ground up, built on `tokio`. No unsafe, no build-time code generation, no custom DSL.
- Every platform is a trait impl. Rolling an adapter for a niche chat service is a weekend project.
## Quick start
```toml
# Cargo.toml
[dependencies]
foukoapi = "0.1.2-alpha.1"
tokio = { version = "1", features = ["full"] }
```
```rust
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
| `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. |
| `genai` | no | `GenClient` - OpenAI-compatible generation client. |
| `webapp` | no | Telegram Mini App `initData` validation. |
| `tunnel` | no | Cloudflare quick tunnels via the `cloudflared` binary. |
| `full` | no | Turns on everything above. |
```toml
foukoapi = { version = "0.1.2-alpha.1", default-features = false, features = ["telegram", "sqlite"] }
```
### Environment variables
`bootstrap_env()` creates a commented `.env` template on the first run. Defaults:
| `TG_TOKEN` | Telegram bot token from [@BotFather](https://t.me/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 discovered `.env` (never inside `target/`). |
| `RUST_LOG` | Standard `tracing` filter, e.g. `info,foukoapi=debug`. |
### Core types
```text
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:
```rust
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
```rust
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 `/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:
```rust
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`)
```rust
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:
```rust
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
| Telegram | ✅ Working |
| Discord | ✅ Working |
| 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 |
| GenAI client (`genai`) | ✅ Working |
| Mini App validation (`webapp`) | ✅ Working |
| Quick tunnels (`tunnel`) | ✅ Working |
| Util helpers | ✅ Working |
| Argument parsing helpers | ⏳ Planned |
| Middleware chain | ⏳ Planned |
## Examples
The [`examples/`](examples) directory is where ready-to-run sample bots live. A reference bot that uses every feature is also published as [FoukoBot](https://github.com/FoukoDev/Fouko) - its source is a good place to watch the API take shape.
```bash
# run the quickstart example (needs TG_TOKEN / DISCORD_TOKEN)
cargo run --example quickstart
# streaming GenAI showcase (needs TG_TOKEN and an OpenAI-compatible host)
cargo run --example genai --features "genai telegram"
```
<a id="msrv"></a>
## 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
Licensed under either of [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE),
at your option. Unless you explicitly state otherwise, any contribution you
submit for inclusion is dual-licensed the same way, without additional terms.
---
<sub>Part of the <a href="https://fouko.xyz">Fouko</a> family.</sub>