<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"><img alt="License" src="https://img.shields.io/badge/license-MIT-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 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
- [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)
- [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.
- **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
```toml
# Cargo.toml
[dependencies]
foukoapi = "0.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. |
| `full` | no | Turns on everything above. |
```toml
foukoapi = { version = "0.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 binary. |
| `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). 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:
```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.
### 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.
## 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 |
| Built-in `/help`, `/lang`, `/link` | ✅ 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
```
<a id="msrv"></a>
## 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](LICENSE).
---
<sub>Part of the <a href="https://fouko.xyz">Fouko</a> family.</sub>