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
<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

| 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.                             |

```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:

| Variable         | What it is                                                     |
|------------------|----------------------------------------------------------------|
| `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

| 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/`](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>