Skip to main content

CHANGELOG.md

# Changelog

## 0.10.0

Reliability release: 37 audited defects fixed. **Breaking changes** — see
[UPGRADING.md](UPGRADING.md) for the per-call migration guide.

### Breaking

- `Sexy.Bot.send/2` (and `Sender.deliver/2`) with a list returns per-item responses instead of `:ok`.
- `Sexy.Bot.start_link/1` raises `ArgumentError` on a nil/empty token or an unloadable session module.
- `Sexy.Bot.notify/3` returns `"ok" => false` (with `"result"` preserved) when buttons could not be attached.
- `Sexy.TDL.open/3` returns `{:error, {:already_started, pid}}` for duplicate session names and `{:error, reason}` when the port can't open (was `{:ok, pid}` + silence).
- `mix sexy.tdl.setup` no longer generates types (they ship with the package); `mix sexy.tdl.generate_types` refuses to run outside the sexy repo unless `--force`.

### Fixed

- **Poller:** a malformed update (e.g. `callback_query` without `data`) crash-looped the poller and took down the consumer's supervision tree via offset-0 replay. All update parsing now runs in supervised tasks; the offset survives restarts (atomics); the loop is driven by `send_after` and ignores stray messages instead of crashing (or stalling — the old GenServer-timeout loop was cancelled by any message).
- **Api:** all methods (not just `get_updates`) return the documented error map on non-JSON responses instead of raising `Jason.DecodeError`; transport-error descriptions no longer crash on tuple reasons.
- **Payments:** optional Session callbacks are resolved reliably (session module loaded at boot); `handle_poll` is guarded like the other optional callbacks; `poll_answer` updates are routed to `handle_poll` instead of being dropped; built-in `/_delete`//`_transit` no-op on forged/malformed callback data, and `/_transit` verifies the handler exists *before* deleting the message.
- **Rate limits:** sends retry once after Telegram's `retry_after` on 429.
- **Query format:** `-` inside values (negative chat ids, UUIDs, dates) no longer corrupts parsing — pairs split only on `-` followed by `key=`; the library's own `navigate:` flow round-trips.
- **TDL:** `close/1` resurrected the session (permanent child of a DynamicSupervisor) or crash-looped all sessions — now terminates for real; Backend/Handler init no longer `MatchError`-crash-loops when the registry entry is gone; tdlib binary death is detected in non-proxy mode (`:exit_status`) and restarts the pair via the Riser's `one_for_all` (was: silent zombie); system/proxy events go directly to `app_pid` (were undeliverable on cold start/restart); Registry monitors session supervisors and drops dead entries; `transmit/2` returns `{:error, :no_backend}` instead of `:noproc`-exiting the caller; nested `vector<vector<T>>` fields (inline keyboards) deserialize recursively.
- **Codegen:** output directory is created (`mkdir_p`); generation writes atomically (malformed `types.json` can't destroy previous files); doc text is escaped, closing a compile-time code-execution hole; setup prompts handle `:eof` with a clear error.
- **Utils:** `fiat_chunk(-123456, 0)` renders `"-123 456"` instead of `"- 123 456"`; `get_message_media/2` returns `nil` (not `:ok`) for unsupported types; `Object.build/1` is idempotent for already-built Objects.
- **Compatibility:** removed an Elixir 1.17+ guard (`is_non_struct_map/1`) — the declared `~> 1.14` support is real again.
- **Lifecycle:** persistent_term config is erased when the bot stops (a stopped bot no longer sends with a stale token); delayed deletions run under a supervised Task.Supervisor.

### Changed

- `Sexy.TDL` supervision uses `:rest_for_one`; per-session Risers are `restart: :temporary` (a persistently failing session dies alone instead of cascading into other sessions).
- Updates of the same chat are processed **in order** (partitioned dispatch by chat id via `Sexy.Bot.Dispatcher`); different chats run concurrently. Previously all updates were dispatched as unordered concurrent tasks.
- Documented delivery semantics: at-least-once — payment handlers should deduplicate by `update_id`.

## 0.9.15

### Fixed

- Poller no longer crashes (and crash-loops the whole supervision tree) when Telegram or a proxy returns a non-JSON body. `get_updates/1` now matches `status_code: 200` and decodes without `Jason.decode!`; a 502 HTML page returns `{:error, {:http_status, 502}}` instead of raising.
- Poller error diagnostics use `inspect/1` instead of `Atom.to_string/1`, which crashed on tuple reasons like `{:tls_alert, …}`.
- Empty and errored polls keep the current offset instead of resetting to `0`, so an unconfirmed update batch is no longer replayed after a transport failure (double-firing handlers, including `handle_successful_payment`). Errored polls also back off from 100ms to 5s instead of hot-looping.

### Changed

- `Sexy.Bot.get_updates/1` (and `Sexy.Bot.Api.get_updates/1`) error return widened: alongside `{:error, atom}` it can now return `{:error, {:http_status, integer}}` or `{:error, :invalid_json}`. Bare `:error` (no `result` key) was already possible.

## 0.9.14

Cleanup pass (ponytail audit). **Breaking changes** — see below.

### Breaking

- Removed `Sexy.Bot.Screen`. Use `Sexy.Bot.build/1` (now backed by `Sexy.Utils.Object.build/1`).
- Removed `wallet_init/5` (Wallet.tg integration + global `WALLET` env). Keep it in your app — it is not Telegram Bot API.
- `send_dice/2` now takes a Telegram dice emoji (`"🎲"`, `"🎯"`, `"🏀"`, `"⚽"`, `"🎳"`, `"🎰"`) instead of the `"dice"/"bowl"/"foot"/"bask"/"dart"/"777"` codes.
- `send_chat_action/2` now takes a real Telegram action string (`"typing"`, `"upload_photo"`, …) instead of the `"txt"/"pic"/"vid"` codes.
- `get_user_photo/1` returns `nil` when the user has no photo, instead of a hardcoded fallback `file_id`. It also handles any number of photo sizes.
- `Sexy.Utils.Bot.wrap_text` is now `wrap_text/2` (`wrapper, text`), replacing the previous `wrap_text/1` (decorative borders) and `wrap_text/3` (HTML tag).

### Fixed

- `Sexy.Utils` query parsing no longer calls `String.to_atom` on user-supplied callback keys (atom-table exhaustion / DoS). Unknown or malformed keys are dropped.
- `mix sexy.tdl.generate_types` now writes to `lib/tdl/{object,method}.ex` (the real paths), so regeneration overwrites the shipped files.

### Internal

- Deduplicated HTTP response decoding in `Sexy.Bot.Api` (`decode_response/1`).
- `get_photo_id/1` simplified to `List.last/1`; shared `titlecase_once/1` moved to `Sexy.Utils`.

## 0.9.13

- Multipart upload for photo, video and animation (parallel to `send_document`):
  - New: `Sexy.Bot.Api.send_photo/5`, `send_video/5`, `send_animation/5`
  - Top-level delegates: `Sexy.Bot.send_photo/5`, `send_video/5`, `send_animation/5`
- New `Sexy.Utils.Object` field `:upload_type` (`:photo | :video | :animation | :document | nil`)
  drives multipart uploads. Detection prefers `upload_type` over `media`.
- `detect_object_type/1` no longer crashes when `media` is not a binary
  (guards added; non-string media → `"unknown"`).
- Legacy `media: "file"` sentinel still works (equivalent to `upload_type: :document`).

## 0.9.12

- Fix `send_document` multipart format for hackney 1.25.0.
  Use standard 4-tuple `{"document", binary, disposition, []}` instead of `{binary, disposition, []}`.

## 0.9.11

- Add `after: seconds` option to `notify/3` for auto-deleting notifications.

## 0.9.10

- Add delayed deletion: `delete_message(chat_id, mid, after: seconds)`.
  Runs asynchronously via `Task` — accepts integers and floats.

## 0.9.9

- Fix `send_document` badarg when sending binary content as multipart.
  HTTPoison was interpreting `{:file, binary, ...}` as a file path instead of raw content.

## 0.9.8

- Replace individual `*_pid` fields in `Sexy.TDL.Registry` with Elixir `Registry`-backed
  worker discovery (`Sexy.TDL.Workers`). Workers auto-unregister on death — no stale PIDs.
  New API: `register_worker/2`, `get_worker/2`, `list_workers/1`
- Add `Sexy.TDL.Workers` (Elixir Registry, `:unique` keys) to supervision tree

## 0.9.7

- Add client process pid fields to `Sexy.TDL.Registry` defstruct
  (`sorter_pid`, `updater_pid`, `sender_pid`, `answerer_pid`, `reactor_pid`, `direct_pid`)

## 0.9.6

- Add Telegram Payments (Stars) support in Poller and Session:
  - Route `pre_checkout_query` updates to `handle_pre_checkout/1` callback
  - Route `successful_payment` messages to `handle_successful_payment/1` callback
  - Both callbacks are optional: pre_checkout auto-approves, successful_payment logs
  - `successful_payment` clause placed before general `message` to ensure correct matching

## 0.9.5

- Rewrite `bot-quickstart.md` for beginners: add intro (single-message UI concept),
  BotFather step, split Session into 3 blocks (Storage, Dispatch, Screens),
  explain update structure, callback_query, inline_keyboard layout, answer_callback,
  add "How it works" flow diagram
- Replace all Russian comments and log messages with English
- Rename `extra` → `update_data` in Session callback definition, docs, and examples
  for consistency with `Sexy.Utils.Object.update_data` field
- Fix Screen module examples to use `Sexy.Bot.build/send` (public API) instead of
  `Sexy.Bot.Screen.build` / `Sexy.Bot.Sender.deliver` (internal modules)

## 0.9.4

- Add comprehensive test suite: 113 tests, 0 failures (7 test files)
  - `mox` + `bypass` test dependencies for HTTP stubbing and behaviour mocking
  - Pure function tests: Utils, Utils.Bot, Utils.Object, Bot.Screen
  - Integration tests with Bypass: Bot.Api, Bot.Sender, Bot.Notification
- Add `credo` dependency, pass `mix credo --strict` (0 issues)
- Add 66 `@spec` annotations and `@type` definitions across 10 modules
- Refactor `get_message_type`: 11-branch cond → `Enum.find_value` over `@message_types`
- Refactor `fiat_chunk`: extract `format_integer_part/1` + `join_chunks/1`, remove 100 lines of duplication
- Refactor `Sender.deliver`: extract `parse_mode/1`, `send_by_type/4`, `update_screen/3` (complexity 10→3, nesting 3→1)
- Refactor `Handler.recursive_match`: extract `recurse_if_typed/2` (nesting 3→1)
- Fix credo issues: alias ordering, `Enum.map_join`, `unless`→`if`, nested module refs→aliases
- Run `mix format` on all files

## 0.9.3

- Add comprehensive ExDoc documentation with `@moduledoc` and `@doc` for all modules/functions
- Add quickstart guides: `bot-quickstart.md`, `tdl-quickstart.md`
- Add hex.pm packaging config (description, package, docs)
- Filter auto-generated TDL.Object/Method submodules from docs
- Expand README with Object struct, send options, and notify options documentation
- Add MIT LICENSE

## 0.9.2

- Flatten `lib/` structure: remove redundant `lib/sexy/` nesting

## 0.9.1

- Move all Bot modules under `Sexy.Bot` namespace:
  - `Sexy.Api` → `Sexy.Bot.Api`
  - `Sexy.Sender` → `Sexy.Bot.Sender`
  - `Sexy.Notification` → `Sexy.Bot.Notification`
  - `Sexy.Poller` → `Sexy.Bot.Poller`
  - `Sexy.Screen` → `Sexy.Bot.Screen`
  - `Sexy.Session` → `Sexy.Bot.Session`

## 0.9.0

**Breaking**: `Sexy` split into `Sexy.Bot` + `Sexy.TDL`.

- `Sexy` module is now a namespace only — use `Sexy.Bot` as supervisor entry point
- All Bot API functions moved: `Sexy.*` → `Sexy.Bot.*`
- New `Sexy.TDL` — TDLib integration for userbot sessions
  - `Sexy.TDL.open/3`, `close/1`, `transmit/2` — session management
  - `Sexy.TDL.Backend` — port to tdlib_json_cli binary
  - `Sexy.TDL.Handler` — JSON deserialization + event routing
  - `Sexy.TDL.Registry` — ETS-based session storage
  - `Sexy.TDL.Riser` — per-account supervisor
  - 2558 auto-generated Method/Object structs from TDLib API
- New mix tasks: `mix sexy.tdl.setup`, `mix sexy.tdl.generate_types`
- Migration guide: see MIGRATION.md

## 0.8.2

- Add Telegram Stars payment methods (send_invoice, answer_pre_checkout, refund_star_payment)
- Add wallet_init for Wallet.tg integration

## 0.8.1

- Built-in /_transit route for navigation between screens

## 0.8.0

**Breaking**: Sexy is now a Supervisor, not an Application.

- Start with `{Sexy, token: "...", session: MySession}` in supervision tree
- `Sexy.Session` behaviour — single integration point (persistence + dispatch)
- Removed `Sexy.Application`, `Sexy.Visor`, config-based dispatch
- Config replaced with `persistent_term` (no more `:sexycon`)
- Added `build/1`, `send/1-2`, `notify/2-3` facade functions
- Hardcoded dismiss_text default to "OK"
- Removed `wrap_text` from Sender

## 0.7.4

- Add `Sexy.Notification` — overlay/replace notification pattern
- Dismiss button with built-in `/_delete` route in Poller
- Configurable `dismiss_text` via `Application.get_env(:sexy, :dismiss_text)`

## 0.7.3

- Remove 26 legacy short-name delegates (pm, dm, um, ui, etc.)
- Remove unused `api_url/1`

## 0.7.2

- Add `Sexy.Session` behaviour (`get_message_id/1`, `on_message_sent/4`)
- Add `Sexy.Sender` — delivers Object, manages mid lifecycle via Session callbacks
- Add `Sexy.Screen` — converts app maps to Object structs
- `Sexy.Utils.Object`: add `chat_id`, rename `update_user` → `update_data`

## 0.7.1

- Create `Sexy.Api` with readable method names (send_message, delete_message, etc.)
- Consolidate HTTP into `do_request/3` and `do_multipart/3`
- Rewrite `Sexy` as facade with `defdelegate` to `Sexy.Api`
- Legacy short names kept as backward-compat delegates

## 0.7.0

- Initial extraction from OrderMachine into standalone library
- Modules: Sexy (API client), Sexy.Poller, Sexy.Visor, Sexy.Utils, Sexy.Utils.Bot, Sexy.Utils.Object
- Dependencies: HTTPoison, Jason, Base62