# 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