foukoapi 0.1.2-alpha.2

Cross-platform bot framework in Rust: one codebase, many platforms. Shared accounts, embeds, keyboards, economy, i18n and pluggable storage; Telegram and Discord adapters included.
Documentation
# Changelog

All notable changes to FoukoApi are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.2-alpha.1] - 2026-08-08

### Added
- `genai` - an OpenAI-compatible generation client behind the `genai`
  feature. `GenClient` speaks to LiteLLM, Ollama, LM Studio, vLLM,
  OpenRouter and anything else on the same dialect: `chat` completions,
  `list_models` discovery with capability tags (host metadata like
  `output_modalities`/`mode` wins over a model-name heuristic), `image`,
  `video` (job + polling) and `speech` generation, all with hard size
  caps on downloads.
- `genai`: `GenClient::chat_stream` - streaming chat completion over
  SSE. The callback receives the accumulated text so far as tokens
  arrive; hosts and proxies that ignore streaming still work, the whole
  answer just lands in one callback. A stream may run up to 600 s total
  and is dropped if the host stalls for 180 s mid-answer - generous
  enough for thinking models that pause before answering.
- `genai`: `ChatMessage::with_image` - attach an image to a chat message
  for vision models, sent as an OpenAI `image_url` content part with a
  base64 data URL. Images over 10 MiB are dropped rather than sent, and
  the image is never serialized into stored history - only role and text
  persist.
- `genai`: `KNOWN_VOICES` - the OpenAI speech voice names, as a hint for
  bot UIs. Nothing is validated against it, since proxies may serve
  their own voices.
- `Ctx::incoming_image` and `Ctx::has_incoming_image` - access the photo
  attached to an incoming message (a Telegram photo or image document, a
  Discord image attachment). `has_incoming_image` is known up front
  without a download; `incoming_image` downloads the bytes lazily on
  first call, capped at 10 MiB.
- `examples/genai.rs` - a showcase bot for the `genai` feature: `/ask`
  streams a model's answer into the chat, editing the reply in place as
  tokens arrive.
- `Reply::image_bytes` - attach a picture by raw bytes (e.g. an
  AI-generated image decoded from base64) and send it as a native photo
  on Telegram and a file attachment on Discord. Text or embed content
  becomes the caption, keyboards stick to the photo message, and images
  over 8 MB are rejected with an error at send time.
- `Reply::video_bytes` and `Reply::audio_bytes` - attach a video or an
  audio track by raw bytes. Telegram sends them natively (`sendVideo` /
  `sendAudio`, body as caption); Discord attaches the file next to the
  message. One raw attachment per reply, the last call wins. Cap is
  25 MiB, except Discord which rejects video/audio over 8 MiB at send
  time (regular-server upload limit).
- `genai`: tool calling. `ToolSpec` describes a function the model may
  call, `GenClient::chat_tools` returns a `ChatOutcome` - either the
  final text or the model's `ToolCall`s; feed results back with
  `ChatMessage::tool_result` and `ChatMessage::assistant_tool_calls`.
  `GenClient::chat_stream_tools` does the same over streaming, with
  tool-call deltas accumulated across SSE chunks. Hosts that reject
  requests with tools yield `GenError::NotSupported`, so a caller can
  retry a plain chat.
- `genai`: `GenClient::insecure(bool)` - opt in to hosts with
  self-signed TLS certificates. Off by default; certificate validation
  stays on unless you ask. Calling it pins the mode: automatic TLS
  adaptation (below) never overrides an explicit choice.
- `genai`: automatic TLS adaptation, on by default. When a request fails
  with a certificate error, the client retries it once in insecure mode
  and remembers the answer per host; when the host later comes back with
  a valid certificate, the client switches back to strict validation on
  its own. `GenClient::auto_tls(false)` turns the behaviour off.
- `genai`: model probes - `GenClient::probe_chat`, `probe_speech` and
  `probe_image_cheap` detect "canned" models: a dead upstream that
  returns the same captured response to every request. Each probe sends
  tiny inputs a live backend must react to and returns a `ProbeReport`
  with a `ProbeVerdict` (live, canned, unstable or not supported) plus
  the evidence behind it.
- `Bot::menu_web_app` - publish a Telegram Mini App button next to the
  chat input field (via `setChatMenuButton` at startup). The URL must be
  `https`; anything else is rejected with a warning instead of being
  sent to Telegram.
- `Button::web_app` - a Telegram Mini App button that opens a web app
  inside the client. Discord has no equivalent, so it falls back to a
  plain link button there. `Button::web_app_url` reads the URL back.
- `webapp` module (feature `webapp`): `validate_init_data` checks a
  Telegram Mini App's `initData` signature server-side - HMAC per the
  official scheme, constant-time comparison, and a `max_age` guard
  against replayed payloads. Returns the parsed `InitData` with the
  `WebAppUser` inside.
- `tunnel` module (feature `tunnel`): `cloudflared_quick` starts a
  Cloudflare quick tunnel to a local port and hands back its public
  `https://<name>.trycloudflare.com` URL, so a Mini App can run from a
  laptop behind NAT with no domain of its own. All it needs is the
  `cloudflared` binary on PATH. The returned `QuickTunnel` owns the
  child process - drop it and the tunnel dies with it.
- `env_file_path` - the location of the `.env` file that
  `bootstrap_env` found or created, for anything else that wants to
  live next to it.

### Changed
- `Ctx::edit_reply` now also works on regular messages, not only on
  button callbacks: it rewrites the last reply the bot sent through the
  same context. That's what makes live streaming (send a placeholder,
  then keep editing it as text grows) possible in plain chats. The
  fallback to a fresh message when there's nothing to edit is unchanged.
- Messages that carry media are no longer dropped: a Telegram photo's
  caption now arrives as the message text, and a photo without a caption
  still dispatches with empty text so handlers can react to the image
  itself.
- `genai`: `GenClient::speech` takes a new `voice` parameter (breaking).
  Pass `None` for the previous behaviour (the `alloy` default); the
  value is forwarded unvalidated since proxies may serve their own
  voices.
- `genai`: network errors now say why. A failed request reports the
  underlying cause ("host unreachable: invalid peer certificate ...")
  instead of a bare "host unreachable", so a certificate problem is
  distinguishable from a host that's simply down.
- `.env` lookup: `bootstrap_env` checks the current directory first and
  then next to the binary, so a bot started from elsewhere still finds
  its config.
- Telegram updates are now handled concurrently: a long-running handler
  (say, a streaming AI answer) no longer blocks the other commands in
  the chat - they are dispatched while it works.
- `genai`: the video-model name heuristic knows more markers (`wan2`,
  `t2v`, `i2v`, `r2v`), so those models get the video tag without a
  manual override. Host metadata still wins when present.

### Fixed
- The default SQLite database is placed next to the discovered `.env`
  and never inside `target/`, where a `cargo clean` would erase it. An
  existing database at the old path keeps being picked up.
- `genai`: `GenClient::image` retries without `response_format` when a
  backend rejects that parameter, instead of failing the whole request.
- `genai`: big responses from real-world hosts parse now. A `content`
  field that arrives as an array of parts is joined into text, and an
  answer hidden in `reasoning_content` (thinking models) is used when
  the visible field is empty - in plain and streaming chat alike.
- `genai`: hosts that announce SSE but answer with a single JSON body no
  longer produce an empty stream - the body is parsed as a regular chat
  response instead.
- `genai`: an "empty response" error now includes what the host actually
  sent, so a misbehaving proxy can be diagnosed instead of guessed at.
- Discord: a keyboard that fits Telegram but blows Discord's caps (5
  rows of 5 buttons) used to make Discord reject the whole message.
  Now the buttons are repacked into full rows, in order; only what
  falls past the 25-button cap is dropped, with a warning in the log.

## [0.1.1-alpha.1] - 2026-08-02

### Added
- `Economy` - XP, levels, coins, transfers, cooldowns, achievements and a
  leaderboard, with balances shared across a user's linked accounts.
- `I18n` - a small translation catalogue with English fallback, `{}`
  placeholders and a `missing()` coverage check.
- `Notifier` - an outbound handle for pushing messages into a chat without
  an incoming event (reminders, scheduled jobs). Adapters register their
  sender on startup.
- `Notifier::send_dm` and `Notifier::is_dm_ready` - deliver a message into
  a user's direct messages by user id. On Telegram the user id is the DM
  chat id; on Discord the adapter opens the DM channel over REST and
  caches the mapping.
- `Notifier::user_name` - look up a user's display name by id outside any
  update, e.g. to greet the operator at startup. Returns `Ok(None)` when
  the platform doesn't know the user; on Telegram the lookup only works
  after the user has messaged the bot at least once.
- `RateLimiter` - a per-user rate limiter. `Bot` enables a relaxed policy by
  default; tune it with `Bot::rate_limit` / `Bot::no_rate_limit` and reply to
  throttled users with `Bot::on_rate_limited`.
- `Bot::on_flood` - a watchdog over the total incoming update flow. Counts
  every update before the per-user rate limit and fires a handler (at most
  once per cooldown) when a minute's total crosses a threshold, for
  detecting spam waves and DDoS-style bursts.
- `Bot::category` - optional command groups; the built-in `/help` renders
  them as sections and falls back to a flat list when unused.
- `Bot::max_update_age` - drop stale updates so a bot doesn't replay a
  backlog after downtime.
- `Ctx::typing()` - show a typing indicator before slow work.
- `Ctx::avatar_url`, `Ctx::banner_url` and `Ctx::chat_info` - cross-platform
  lookups for user avatars/banners and server/chat details.
- `Storage::list_prefix` for scanning keys under a prefix.
- `Storage::set_nx` - write-if-absent that reports whether the write
  happened. Atomic in the bundled memory and SQLite backends; the default
  get+set fallback is documented as non-atomic.
- `Secret` - AES-256-GCM at-rest encryption (feature `crypto`), keyed off
  an operator passphrase, for storing secrets and private text encrypted.
- `banner` - colorful startup output for any bot: a `Banner` builder with
  optional gradient ASCII art and an aligned status table, plus
  `print_ready`, `print_check` and `print_warn` one-liners. Colors honor
  `NO_COLOR` and switch off when stdout is not a terminal.
- `util::split_chunks` - split long text on line/word boundaries; the
  Telegram adapter uses it to send overly long replies as several messages
  instead of hitting the length limit.
- Telegram now renders the bot's markdown (`` `code` ``, `**bold**`,
  `*italic*`, links) as native formatting instead of showing raw symbols.
  `Reply::raw(true)` opts out and sends text verbatim.
- Registered commands are now published to Telegram's command menu, matching
  the Discord slash-command registration.

### Changed
- `Economy::add_xp` now returns `XpGain { xp, coins_minted }` instead of a
  bare XP total, so callers can announce minted coins.
- `Decision::Deny` gained a `first` flag; `Bot::on_rate_limited` now fires
  only on the first denial per window instead of answering every flooded
  message.
- Command names are matched case-insensitively (`/Help` finds `/help`).

### Fixed
- Discord slash commands now work in servers, not only DMs.
- `Economy` serialises balance updates: parallel XP/coin grants no longer
  lose updates, concurrent transfers can no longer overdraw a wallet, and
  a failed transfer credit refunds the sender.
- `Economy::add_xp` mints coins before writing XP, so a mid-write failure
  can no longer swallow a crossed coin boundary.
- Account link records self-heal: a one-sided link left by a crash is
  repaired on the next `partner_for` read.
- `/link` codes are no longer predictable from the clock; the generator
  mixes nanosecond time, process id and a global counter.
- The `/link` primary pick is claimed atomically, so two racing button
  taps can no longer both lock in a primary.
- Telegram markdown: `***bold italic***` renders as valid nesting,
  URLs with `_`, `*` or `"` survive conversion, `snake_case_word` and
  `2 * 3 * 4` stay literal, and stray NUL bytes in input are stripped.
- Telegram: fetching the bot's own id at startup retries on transient
  network errors instead of silently disabling reply-to-bot detection.
- Discord messages longer than 2000 chars are now split across messages
  in every send path, matching the Telegram adapter.
- Discord text messages and button presses now get the same avatar,
  banner, chat-info, typing and user-name capabilities as slash commands.
- Discord: a failed interaction defer falls back to plain channel sends,
  and an unanswered slash command that errored shows a warning marker
  instead of a false success check.
- Rate limiter sweeps its key map immediately when it grows too large, so
  a flood of unique keys can't balloon memory.

## [0.1.0-alpha.1] - 2026-05-03

Initial public release.

[0.1.2-alpha.1]: https://github.com/FoukoDev/FoukoApi/releases/tag/v0.1.2-alpha.1
[0.1.1-alpha.1]: https://github.com/FoukoDev/FoukoApi/releases/tag/v0.1.1-alpha.1
[0.1.0-alpha.1]: https://github.com/FoukoDev/FoukoApi/releases/tag/v0.1.0-alpha.1