mixin_bot 2.6.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +3 -3
- data/API_COVERAGE.md +1 -0
- data/CHANGELOG.md +22 -1
- data/README.md +116 -5
- data/docs/agent/cookbook.md +49 -0
- data/examples/blaze_async.rb +6 -4
- data/lib/generators/mixin_bot/authentication/USAGE +29 -0
- data/lib/generators/mixin_bot/authentication/authentication_generator.rb +82 -0
- data/lib/generators/mixin_bot/authentication/templates/app/controllers/concerns/authentication.rb.tt +52 -0
- data/lib/generators/mixin_bot/authentication/templates/app/controllers/sessions_controller.rb.tt +24 -0
- data/lib/generators/mixin_bot/authentication/templates/app/models/current.rb.tt +4 -0
- data/lib/generators/mixin_bot/authentication/templates/app/models/session.rb.tt +3 -0
- data/lib/generators/mixin_bot/authentication/templates/app/models/user.rb.tt +12 -0
- data/lib/generators/mixin_bot/authentication/templates/app/views/sessions/new.html.erb.tt +3 -0
- data/lib/generators/mixin_bot/authentication/templates/config/initializers/omniauth.rb.tt +21 -0
- data/lib/generators/mixin_bot/blaze/USAGE +19 -0
- data/lib/generators/mixin_bot/blaze/blaze_generator.rb +43 -0
- data/lib/generators/mixin_bot/blaze/templates/app/mixin/handlers/text_handler.rb +14 -0
- data/lib/generators/mixin_bot/blaze/templates/config/initializers/mixin_bot_blaze.rb +23 -0
- data/lib/generators/mixin_bot/install/USAGE +24 -0
- data/lib/generators/mixin_bot/install/install_generator.rb +50 -0
- data/lib/generators/mixin_bot/notifications/USAGE +13 -0
- data/lib/generators/mixin_bot/notifications/notifications_generator.rb +49 -0
- data/lib/generators/mixin_bot/notifications/templates/app/notifications/payment_received_notification.rb +24 -0
- data/lib/generators/mixin_bot/outputs/USAGE +29 -0
- data/lib/generators/mixin_bot/outputs/outputs_generator.rb +50 -0
- data/lib/generators/mixin_bot/outputs/templates/app/mixin/processors/deposit_processor.rb +21 -0
- data/lib/generators/mixin_bot/outputs/templates/app/models/mixin_output.rb +8 -0
- data/lib/generators/mixin_bot/outputs/templates/db/migrate/create_mixin_outputs.rb.tt +26 -0
- data/lib/generators/mixin_bot/transfers/USAGE +37 -0
- data/lib/generators/mixin_bot/transfers/templates/app/jobs/mixin_transfers/perform_job.rb +13 -0
- data/lib/generators/mixin_bot/transfers/templates/app/jobs/mixin_transfers/reconcile_job.rb +14 -0
- data/lib/generators/mixin_bot/transfers/templates/app/models/mixin_transfer.rb +11 -0
- data/lib/generators/mixin_bot/transfers/templates/db/migrate/create_mixin_transfers.rb.tt +23 -0
- data/lib/generators/mixin_bot/transfers/transfers_generator.rb +67 -0
- data/lib/generators/mixin_bot.rb +10 -0
- data/lib/mixin_bot/api/app.rb +20 -0
- data/lib/mixin_bot/api/blaze_async.rb +5 -1
- data/lib/mixin_bot/api/snapshot.rb +5 -0
- data/lib/mixin_bot/blaze/reactor.rb +249 -0
- data/lib/mixin_bot/blaze/router.rb +333 -0
- data/lib/mixin_bot/cache_store_adapter.rb +3 -1
- data/lib/mixin_bot/client.rb +2 -1
- data/lib/mixin_bot/computer.rb +3 -1
- data/lib/mixin_bot/configuration.rb +52 -0
- data/lib/mixin_bot/json_decoder.rb +14 -0
- data/lib/mixin_bot/notifications/mixin_channel.rb +80 -0
- data/lib/mixin_bot/outputs/cursor_model.rb +33 -0
- data/lib/mixin_bot/outputs/envelope.rb +160 -0
- data/lib/mixin_bot/outputs/memory_stores.rb +97 -0
- data/lib/mixin_bot/outputs/poller.rb +178 -0
- data/lib/mixin_bot/outputs/processing_job.rb +54 -0
- data/lib/mixin_bot/outputs/processors.rb +124 -0
- data/lib/mixin_bot/outputs/receipt_model.rb +86 -0
- data/lib/mixin_bot/outputs.rb +42 -0
- data/lib/mixin_bot/rails/railtie.rb +44 -0
- data/lib/mixin_bot/rails/tasks/poller.rake +33 -0
- data/lib/mixin_bot/rails.rb +34 -0
- data/lib/mixin_bot/registry.rb +79 -0
- data/lib/mixin_bot/transfers/model.rb +112 -0
- data/lib/mixin_bot/transfers/performer.rb +64 -0
- data/lib/mixin_bot/transfers/reconcile.rb +53 -0
- data/lib/mixin_bot/transfers.rb +23 -0
- data/lib/mixin_bot/version.rb +1 -1
- data/lib/mixin_bot.rb +4 -0
- data/lib/puma/plugin/mixin_blaze.rb +263 -0
- data/llms.txt +1 -1
- metadata +69 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 520bf980215da4e70c9fa6ae1d9297874fc16fc8264a43bb019a126f0fb0d555
|
|
4
|
+
data.tar.gz: 1d5cf99a0e645d4ae110e73148b501bd5f942b3f2c0e9ac0a4338d2d38699a7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b73f1bd21043e018410555d93a537b2314f3398a174bf7ce443cce1652b578e425569faa9118d413789607f106863dbf08b1b79ef4da643f28c40fdbad065479
|
|
7
|
+
data.tar.gz: 49b66d6300397db22af3f28688eca9999a68ad47f07689a84f94a6c43831d96e0ad42c88d7b95cb3a12f531ab4bf4d5540f84cb0f070508687027f20337ff36a
|
data/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md — MixinBot
|
|
2
2
|
|
|
3
|
-
Ruby gem (
|
|
3
|
+
Ruby gem (v3.0.2): Mixin Network REST SDK + `mixinbot` CLI. Parity targets: [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client), [bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client).
|
|
4
4
|
|
|
5
5
|
## Commands
|
|
6
6
|
|
|
@@ -34,13 +34,13 @@ docs/agent/ # LLM-oriented CLI and cookbook docs
|
|
|
34
34
|
|
|
35
35
|
## CI and release
|
|
36
36
|
|
|
37
|
-
- **CI** (`.github/workflows/ci.yml`): `pull_request` and `push` to `main` — `rake test` on Ruby
|
|
37
|
+
- **CI** (`.github/workflows/ci.yml`): `pull_request` and `push` to `main` — `rake test` on Ruby 4.0, `rake rubocop` (4.0), `rake mixin_bot:api_coverage`.
|
|
38
38
|
- **Release** (`.github/workflows/release.yml`): push tag `v*` (must match `MixinBot::VERSION`, e.g. tag `v2.1.0` for `VERSION = '2.1.0'`) → `rake build` → RubyGems via [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (OIDC; workflow `release.yml`, no repo secret) → GitHub Release (notes from `CHANGELOG.md`, `.gem` attached).
|
|
39
39
|
- **Dependabot** (`.github/dependabot.yml`): weekly Bundler and GitHub Actions updates; Dependabot PRs use the same CI workflow.
|
|
40
40
|
|
|
41
41
|
## Conventions
|
|
42
42
|
|
|
43
|
-
- **Ruby** >=
|
|
43
|
+
- **Ruby** >= 4.0 (CI: 4.0)
|
|
44
44
|
- **HTTP responses**: `MixinBot::Models::ApiEnvelope` — use `res['data']` or delegated keys
|
|
45
45
|
- **Safe transfers**: require `spend_key`; prefer `create_safe_transfer` / `create_transfer` (Safe pipeline)
|
|
46
46
|
- **Legacy APIs**: `create_legacy_transfer`, `POST /transfers` — deprecated, warns once
|
data/API_COVERAGE.md
CHANGED
|
@@ -81,6 +81,7 @@ Status values: `done` | `alias` | `n/a` (CLI-only / config / platform-specific)
|
|
|
81
81
|
| **Snapshots** |
|
|
82
82
|
| SafeSnapshots* | `API#safe_snapshots` | GET `/safe/snapshots` | done |
|
|
83
83
|
| SafeSnapshotById | `API#safe_snapshot` | GET `/safe/snapshots/:id` | done |
|
|
84
|
+
| SafeSnapshotByTrace | `API#safe_snapshot_by_trace` | GET `/safe/snapshots/trace/:trace_id` | done |
|
|
84
85
|
| SafeNotifySnapshot | `API#create_safe_snapshot_notification` | POST notifications | done |
|
|
85
86
|
| Snapshots / SnapshotById / SnapshotByTraceId | `API#snapshots`, `#snapshot`, `#snapshot_by_trace_id` | legacy | done |
|
|
86
87
|
| NetworkSnapshot* | `API#network_snapshot(s)` | legacy | done |
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [3.0.2] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`MixinBot::API#update_app_security`** — `POST /apps/:id/security`, the endpoint that persists an app's `allowed_ips` and `resource_patterns`. The generic `update_app` endpoint silently ignores both fields, so code that tried to manage the resource-pattern whitelist through it was a no-op.
|
|
13
|
+
|
|
14
|
+
## [3.0.1] - 2026-09-19
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Blaze reconnect backoff no longer resets on short-lived connections.** `Reactor::STABLE_PERIOD` (the lifetime a connection needs for the backoff to reset) was exactly `MAX_BACKOFF` (30s), so any failure mode that closes sessions at a ~30s lifetime (e.g. a gateway throttling abusive reconnect patterns) read as "stable" on every cycle — the backoff pinned at the initial 1s and the reactor reconnected hot indefinitely, which is itself the abuse pattern that triggers such throttling. `STABLE_PERIOD` is now 120s (well above every single backoff step) and `MAX_BACKOFF` grows the ceiling to 300s so a sustained failure mode backs off hard while a single transient blip still reconnects in ~1s.
|
|
19
|
+
|
|
20
|
+
## [3.0.0] - 2026-09-15
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Puma plugin hosting the Blaze connection** (`plugin :mixin_blaze`) — Solid Queue-style in-process hosting, removing the standalone Blaze deployment artifact. Dual run modes via the `mixin_blaze_mode` Puma DSL option: `:fork` (default) forks a dedicated Blaze child on `on_booted` with a monitor thread that stops Puma if the child dies, terminated on `on_stopped`/`on_restart`; `:async` runs the reactor on a Puma background thread in-process. Handler resolution comes from `MixinBot` configuration (resolved lazily after app boot) with an explicit ack policy (on-receipt vs after-handler); Puma 6/7 lifecycle-event compatibility; cluster mode requires `preload_app!` and fails loudly without it. Puma remains a host-app concern — no new gem dependency.
|
|
25
|
+
- **`MixinBot::Blaze::Reactor`** — reusable Blaze loop (connect, keepalive ping, read → handler → ack, reconnect with backoff) wrapping `API#blaze_async`; extracted from `examples/blaze_async.rb` so the plugin shell stays thin and the loop is unit-testable offline.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Breaking**: Ruby >= 4.0 is now required — `required_ruby_version` bumped from `>= 3.2.0` to `>= 4.0.0`. The CI matrix, RuboCop target, and Release workflow all run Ruby 4.0; the dev-only `parallel` version pin (kept for the old Ruby 3.2 floor) was dropped.
|
|
9
30
|
|
|
10
31
|
## [2.6.0] - 2026-09-07
|
|
11
32
|
|
data/README.md
CHANGED
|
@@ -6,12 +6,12 @@ Ruby SDK and CLI for [Mixin Network](https://developers.mixin.one/docs): authent
|
|
|
6
6
|
|
|
7
7
|
The gem aims for **parity with the official [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client)** Go SDK and **[bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client)** Node SDK. See [API_COVERAGE.md](API_COVERAGE.md) for the full mapping; run `rake mixin_bot:api_coverage` to confirm no gaps are marked missing.
|
|
8
8
|
|
|
9
|
-
Current gem version: **
|
|
9
|
+
Current gem version: **3.0.2** (see [CHANGELOG.md](CHANGELOG.md) for breaking changes and deprecations).
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
|
-
- **Ruby** ≥
|
|
14
|
-
- **Bundler** 2.
|
|
13
|
+
- **Ruby** ≥ 4.0 (CI runs 4.0).
|
|
14
|
+
- **Bundler** 2.6+ recommended.
|
|
15
15
|
- Optional: the **`mixin`** CLI in `PATH` if you use `MixinBot::API#encode_raw_transaction_native` / `#decode_raw_transaction_native` or the experimental `MixinBot::NodeCLI` helpers.
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
@@ -290,6 +290,117 @@ ensure
|
|
|
290
290
|
end
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
+
### Running Blaze inside Puma (`plugin :mixin_blaze`)
|
|
294
|
+
|
|
295
|
+
For Rails/Puma apps, the gem ships a Puma plugin (Solid Queue-style) that hosts the Blaze connection inside the web process tree — no standalone Blaze process to deploy or supervise:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
# config/puma.rb
|
|
299
|
+
plugin :mixin_blaze
|
|
300
|
+
mixin_blaze_mode :fork # default; see table below
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# config/initializers/mixin_bot.rb
|
|
305
|
+
MixinBot.configure do
|
|
306
|
+
# ... credentials ...
|
|
307
|
+
self.blaze_handler = ->(envelope) { MyBot.process! envelope }
|
|
308
|
+
self.blaze_ack_policy = :on_receipt # default; or :after_handler
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The handler receives each decoded message envelope (a Hash with `action` and `data` keys) and runs serially on the hosting thread — safe for ActiveRecord. Handlers run one message at a time; the keepalive ping runs separately.
|
|
313
|
+
|
|
314
|
+
| Mode | Where the connection lives | Notes |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| `:fork` (default) | Dedicated child process forked from the Puma launcher | Crash-isolated; if the child dies, Puma is stopped so the process manager restarts the unit |
|
|
317
|
+
| `:async` | Background thread inside the Puma process itself | In cluster mode that process is the master; no child process |
|
|
318
|
+
|
|
319
|
+
Ack policy: `:on_receipt` acknowledges each message immediately (at-most-once dispatch — a handler crash loses that message); `:after_handler` acknowledges only after the handler completes without raising (at-least-once — handlers must be idempotent, since unacknowledged messages are redelivered on reconnect).
|
|
320
|
+
|
|
321
|
+
Receipt confirmations (`ACKNOWLEDGE_MESSAGE_RECEIPT` frames the server pushes for your own outgoing/acknowledged messages) are consumed internally: the handler never sees them and they are never re-acknowledged.
|
|
322
|
+
|
|
323
|
+
Operational notes:
|
|
324
|
+
|
|
325
|
+
- **Remove any standalone Blaze process when enabling the plugin** — two connections double your handler invocations (or split deliveries).
|
|
326
|
+
- **Cluster mode requires `preload_app!`** — plugins run in the launcher process, which holds no application code without preloading; the plugin reports an error and stays off otherwise.
|
|
327
|
+
- **Phased restarts keep the old handler code** in the hosting process until a full restart; the same applies to Solid Queue's plugin.
|
|
328
|
+
- Workers that need to *send* messages should use the REST API (`create_message` / `create_messages`), not the socket.
|
|
329
|
+
|
|
330
|
+
## Rails integration
|
|
331
|
+
|
|
332
|
+
The gem ships Rails generators under the `mixin_bot:` namespace. The first one sets up **"Sign in with Mixin"** (OAuth login via [`omniauth-mixin`](https://github.com/an-lee/omniauth-mixin)), modeled on Rails' built-in `rails g authentication`:
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
rails generate mixin_bot:authentication
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
This creates an identity-only Mixin login: OmniAuth initializer, `User` / `Session` / `Current` models, `SessionsController` with the OAuth callback, an `Authentication` controller concern (injected into `ApplicationController`), a sign-in view, routes, and the `CreateUsers` / `CreateSessions` migrations. It also adds `omniauth-mixin` and `omniauth-rails_csrf_protection` to the Gemfile (uncommenting existing entries, otherwise `bundle add`) and runs migrations generation. Run `bin/rails db:migrate` afterwards.
|
|
339
|
+
|
|
340
|
+
Then:
|
|
341
|
+
|
|
342
|
+
- Set `MIXIN_CLIENT_ID` and `MIXIN_CLIENT_SECRET` from the [Mixin Developer Dashboard](https://developers.mixin.one/dashboard).
|
|
343
|
+
- Register `https://<your-host>/auth/mixin/callback` as the OAuth redirect URL of your Mixin app.
|
|
344
|
+
|
|
345
|
+
Notes:
|
|
346
|
+
|
|
347
|
+
- **Identity only** — the OAuth exchange is used to establish identity (Mixin user id + name + avatar). Access/refresh tokens are not persisted; a future generator may add API-access wiring on top.
|
|
348
|
+
- **Fail loud at sign-in, not at boot** — the app boots fine without credentials (deploys, CI, `assets:precompile`); attempting to sign in raises an error naming the missing `MIXIN_CLIENT_ID` / `MIXIN_CLIENT_SECRET`.
|
|
349
|
+
- **CSRF-protected request phase** — the sign-in button POSTs to `/auth/mixin` (OmniAuth 2.x default request method, protected by `omniauth-rails_csrf_protection`); the provider redirects back to the `/auth/mixin/callback` GET route.
|
|
350
|
+
- **Existing apps beware** — like `rails g authentication`, the generator templates over target files (e.g. an existing `User` model) and will prompt on conflicts; reconcile manually if you already have users.
|
|
351
|
+
- Sessions are database-backed (`sessions` table with `ip_address` / `user_agent`) with a signed httponly cookie, exactly like Rails 8's `authentication` generator; `Current.user` is available after sign-in.
|
|
352
|
+
|
|
353
|
+
The generated code depends only on Rails and the two OmniAuth gems — not on `mixin_bot` itself — so the login keeps working even if you later drop the gem from the app.
|
|
354
|
+
|
|
355
|
+
### The full Rails integration layer (`require 'mixin_bot/rails'`)
|
|
356
|
+
|
|
357
|
+
Beyond the generators, the gem ships an optional runtime integration layer. It is opt-in and Rails stays a soft dependency:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# config/application.rb, after Bundler.require
|
|
361
|
+
require 'mixin_bot/rails'
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Or scaffold everything in one command and skip pieces as needed:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
rails generate mixin_bot:install
|
|
368
|
+
rails generate mixin_bot:install --skip-notifications --skip-transfers
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
What each piece gives you:
|
|
372
|
+
|
|
373
|
+
- **Blaze message routing** — `MixinBot::Blaze::Router` is a callable that plugs into `config.blaze_handler`. Handlers live in `app/mixin/handlers`, receive a decoded `Message`, and reply over the HTTP message API (no WebSocket access needed):
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
# config/initializers/mixin_bot_blaze.rb (generated by `rails g mixin_bot:blaze`)
|
|
377
|
+
Rails.application.config.to_prepare do
|
|
378
|
+
MixinBot.configure do
|
|
379
|
+
self.blaze_handler = MixinBot::Blaze::Router.new do
|
|
380
|
+
on 'text', Mixin::Handlers::TextHandler # 'text' => PLAIN_TEXT
|
|
381
|
+
on 'transfer', Mixin::Handlers::TransferHandler
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Pair with the Puma plugin (`plugin :mixin_blaze`) so messages are received inside the web process tree. Per-bot: `MixinBot::Blaze::Router.new(api: MixinBot.bot(:shop))`.
|
|
388
|
+
|
|
389
|
+
- **Outputs polling** — `rails g mixin_bot:outputs` creates a receipt table (unique per `bot_app_id` + `output_id` — the dedup gate) and an example processor in `app/mixin/processors`. The poller's resume cursor is derived from the receipts themselves (newest recorded output `sequence` per bot — unique, monotonic, and the key the outputs API paginates on) — no separate cursor table. Run `rails mixin_bot:poller` (standalone process; pass `[bot_name]` for a registered bot). Every output — inbound *and* outbound — is delivered to matching processors through your job queue; memos bridge lazily via the snapshot notification API. Processors implement `self.matches?(envelope)` + `#process` and should be idempotent (dispatch is at-least-once).
|
|
390
|
+
|
|
391
|
+
- **Outbound transfers** — `rails g mixin_bot:transfers` creates a `MixinTransfer` ledger with the state machine (`pending → broadcast → confirmed | failed | reconciling`). Creating a record enqueues the Safe pipeline with the record's `trace_id` as the request id — retries are network-deduplicated. A timeout after submission lands in `reconciling`; the recurring `MixinTransfers.ReconcileJob` confirms from the trace snapshot or re-sends safely.
|
|
392
|
+
|
|
393
|
+
- **Mixin notifications** — `rails g mixin_bot:notifications` adds `noticed` and an example notification with `deliver_by :mixin`. The channel (`MixinBot::Notifications::MixinChannel`) sends text/post/app-card messages with the configured bot's credentials; it loads only when Noticed is present.
|
|
394
|
+
|
|
395
|
+
Multiple bots in one process: register named bots, then reference them everywhere (`MixinBot.bot(:shop)`, `mixin_bot:poller[shop]`, `Router.new(api: ...)`, `deliver_by :mixin, bot: :shop`):
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
MixinBot.register_bot :shop,
|
|
399
|
+
app_id: '...', session_id: '...',
|
|
400
|
+
session_private_key: '...', server_public_key: '...',
|
|
401
|
+
spend_key: '...'
|
|
402
|
+
```
|
|
403
|
+
|
|
293
404
|
## Deep links and bot auth
|
|
294
405
|
|
|
295
406
|
```ruby
|
|
@@ -396,8 +507,8 @@ Examples under `examples/` expect `examples/config.yml` (copy from `examples/con
|
|
|
396
507
|
|
|
397
508
|
GitHub Actions runs on every pull request and on pushes to `main`:
|
|
398
509
|
|
|
399
|
-
- **Test** — Ruby
|
|
400
|
-
- **RuboCop** — Ruby
|
|
510
|
+
- **Test** — Ruby 4.0: `bundle exec rake test`
|
|
511
|
+
- **RuboCop** — Ruby 4.0: `bundle exec rake rubocop`
|
|
401
512
|
- **API coverage** — `bundle exec rake mixin_bot:api_coverage`
|
|
402
513
|
|
|
403
514
|
### Release
|
data/docs/agent/cookbook.md
CHANGED
|
@@ -130,6 +130,55 @@ end
|
|
|
130
130
|
|
|
131
131
|
See [examples/blaze.rb](../../examples/blaze.rb).
|
|
132
132
|
|
|
133
|
+
## Blaze inside Puma (`plugin :mixin_blaze`)
|
|
134
|
+
|
|
135
|
+
For Rails/Puma apps the gem ships a Puma plugin (Solid Queue-style) that hosts the Blaze connection inside the web process tree — no standalone Blaze process. Requires a configured `blaze_handler`; without one the plugin logs an error and stays off.
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# config/puma.rb
|
|
139
|
+
plugin :mixin_blaze
|
|
140
|
+
mixin_blaze_mode :fork # default (:fork = supervised child process); :async = in-process thread
|
|
141
|
+
|
|
142
|
+
# config/initializers/mixin_bot.rb
|
|
143
|
+
MixinBot.configure do
|
|
144
|
+
# ... credentials ...
|
|
145
|
+
self.blaze_handler = ->(envelope) { MyBot.process! envelope } # required
|
|
146
|
+
self.blaze_ack_policy = :on_receipt # default; :after_handler = ack after handler success
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Behavior: reconnect with bounded backoff, keepalive pings, serial handler dispatch (ActiveRecord-safe), handler exceptions logged without ending delivery, graceful shutdown with Puma (child killed on stop; in `:fork` mode a dead child stops Puma so the unit restarts). `ACKNOWLEDGE_MESSAGE_RECEIPT` confirmations are consumed internally — the handler never sees them and they are never re-acknowledged.
|
|
151
|
+
|
|
152
|
+
Operational notes:
|
|
153
|
+
|
|
154
|
+
- **Remove any standalone Blaze process when enabling the plugin** — duplicate connections duplicate handler invocations.
|
|
155
|
+
- **Puma cluster mode requires `preload_app!`** — plugins run in the launcher process; without preloading there is no app code to resolve the handler and the plugin reports an error.
|
|
156
|
+
- **Phased restarts keep old handler code** in the hosting process until a full restart.
|
|
157
|
+
- Workers that need to *send* messages use the REST API (`create_message`), not the socket.
|
|
158
|
+
|
|
159
|
+
## Mixin login for a Rails app (`rails g mixin_bot:authentication`)
|
|
160
|
+
|
|
161
|
+
Task: add "Sign in with Mixin" (OAuth identity login) to an existing Rails app.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
bundle add mixin_bot # if not already present
|
|
165
|
+
rails generate mixin_bot:authentication
|
|
166
|
+
bin/rails db:migrate
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Generated: `config/initializers/omniauth.rb` (OmniAuth `:mixin` provider), `app/models/{user,session,current}.rb`, `app/controllers/sessions_controller.rb` + `app/controllers/concerns/authentication.rb` (injected into `ApplicationController`), `app/views/sessions/new.html.erb`, `resource :session` + `/auth/mixin/callback` routes, and `CreateUsers` (`mixin_user_id` unique, `name`, `avatar_url`) / `CreateSessions` migrations. The Gemfile gains `omniauth-mixin` and `omniauth-rails_csrf_protection`.
|
|
170
|
+
|
|
171
|
+
Required environment: `MIXIN_CLIENT_ID`, `MIXIN_CLIENT_SECRET` (from the Mixin Developer Dashboard); register `https://<host>/auth/mixin/callback` as the OAuth redirect URL.
|
|
172
|
+
|
|
173
|
+
Behavior notes for agents:
|
|
174
|
+
|
|
175
|
+
- **Identity only** — sign-in stores the Mixin user id + name + avatar (`User.find_or_create_by_mixin_auth!`, matched by uid). OAuth access/refresh tokens are never persisted; do not add token columns to the generated models.
|
|
176
|
+
- **Boot is safe without credentials** (CI / `assets:precompile`); a sign-in attempt without them raises `ArgumentError` naming the missing env vars (OmniAuth setup phase).
|
|
177
|
+
- **Request phase is POST + CSRF** (`button_to` → `/auth/mixin`, `omniauth-rails_csrf_protection`); callback is the GET route → `sessions#create`. A GET to `/auth/mixin` is rejected (OmniAuth 2.x).
|
|
178
|
+
- **Sessions** are DB-backed (`sessions` rows, `ip_address`/`user_agent`) remembered in a signed httponly cookie; `Current.user` after sign-in; unauthenticated requests redirect to the sign-in page and return after login; `terminate_session` on sign-out.
|
|
179
|
+
- **Self-contained** — generated code references no `mixin_bot` APIs; the login survives removing the gem. Re-running the generator does not duplicate routes or Gemfile entries; template conflicts prompt like any Rails generator (an existing `User` model is overwritten only if you choose so).
|
|
180
|
+
- To call Mixin APIs on behalf of a signed-in user afterwards, use the gem's OAuth machinery (`MixinBot::API` with delegated authorization) separately from the generated authentication code.
|
|
181
|
+
|
|
133
182
|
## Discover API methods
|
|
134
183
|
|
|
135
184
|
```bash
|
data/examples/blaze_async.rb
CHANGED
|
@@ -40,10 +40,12 @@ def send_frame(connection, bytes)
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
Async do |task|
|
|
43
|
-
# endpoint_options: is forwarded to Async::HTTP::Endpoint.parse
|
|
44
|
-
# timeout here
|
|
45
|
-
#
|
|
46
|
-
|
|
43
|
+
# endpoint_options: is forwarded to Async::HTTP::Endpoint.parse. No
|
|
44
|
+
# timeout: here — it would become the socket's per-read timeout and kill
|
|
45
|
+
# quiet-but-healthy connections; liveness is the keepalive ping below.
|
|
46
|
+
# handler: stays default; pass an Async::WebSocket::Connection subclass to
|
|
47
|
+
# hook the raw frame events (e.g. PONG correlation).
|
|
48
|
+
connection = API.blaze_async
|
|
47
49
|
p [Time.now.to_s, :connected]
|
|
48
50
|
|
|
49
51
|
# liveness is the caller's policy: nothing on the wire goes stale quietly
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Adds "Sign in with Mixin" to this Rails application using OmniAuth
|
|
3
|
+
(omniauth-mixin): User/Session/Current models, a sessions controller with
|
|
4
|
+
the OAuth callback, an authentication concern, routes, migrations, and
|
|
5
|
+
the OmniAuth initializer.
|
|
6
|
+
|
|
7
|
+
The generated login is identity-only: the OAuth access token is used to
|
|
8
|
+
establish identity and is not persisted.
|
|
9
|
+
|
|
10
|
+
After generating, set MIXIN_CLIENT_ID and MIXIN_CLIENT_SECRET, and register
|
|
11
|
+
https://<your-host>/auth/mixin/callback as the OAuth redirect URL in the
|
|
12
|
+
Mixin developer dashboard.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
rails generate mixin_bot:authentication
|
|
16
|
+
|
|
17
|
+
This will create:
|
|
18
|
+
config/initializers/omniauth.rb
|
|
19
|
+
app/models/user.rb
|
|
20
|
+
app/models/session.rb
|
|
21
|
+
app/models/current.rb
|
|
22
|
+
app/controllers/sessions_controller.rb
|
|
23
|
+
app/controllers/concerns/authentication.rb
|
|
24
|
+
app/views/sessions/new.html.erb
|
|
25
|
+
db/migrate/xxx_create_users.rb
|
|
26
|
+
db/migrate/xxx_create_sessions.rb
|
|
27
|
+
|
|
28
|
+
It will also add `omniauth-mixin` and `omniauth-rails_csrf_protection` to
|
|
29
|
+
the Gemfile and inject `include Authentication` into ApplicationController.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module MixinBot
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate mixin_bot:authentication` — sets up "Sign in with Mixin"
|
|
8
|
+
# via OmniAuth, modeled on Rails' built-in `authentication` generator.
|
|
9
|
+
# Identity only: the OAuth tokens are used to establish identity and are
|
|
10
|
+
# not persisted.
|
|
11
|
+
class AuthenticationGenerator < Rails::Generators::Base
|
|
12
|
+
# Rails 8.1 moved bundle_command out of Actions into BundleHelper.
|
|
13
|
+
unless method_defined?(:bundle_command) || private_method_defined?(:bundle_command)
|
|
14
|
+
begin
|
|
15
|
+
require 'rails/generators/bundle_helper'
|
|
16
|
+
include Rails::Generators::BundleHelper
|
|
17
|
+
rescue LoadError
|
|
18
|
+
# Older Rails ships bundle_command in Rails::Generators::Actions.
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
source_root File.expand_path('templates', __dir__)
|
|
23
|
+
|
|
24
|
+
SESSION_ROUTE = 'resource :session, only: [:new, :destroy]'
|
|
25
|
+
CALLBACK_ROUTE = "get '/auth/mixin/callback', to: 'sessions#create'"
|
|
26
|
+
OMNIAUTH_GEMS = %w[omniauth-mixin omniauth-rails_csrf_protection].freeze
|
|
27
|
+
|
|
28
|
+
def create_authentication_files
|
|
29
|
+
template 'config/initializers/omniauth.rb'
|
|
30
|
+
template 'app/models/user.rb'
|
|
31
|
+
template 'app/models/session.rb'
|
|
32
|
+
template 'app/models/current.rb'
|
|
33
|
+
template 'app/controllers/sessions_controller.rb'
|
|
34
|
+
template 'app/controllers/concerns/authentication.rb'
|
|
35
|
+
template 'app/views/sessions/new.html.erb'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def configure_application_controller
|
|
39
|
+
inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', " include Authentication\n"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def configure_authentication_routes
|
|
43
|
+
route SESSION_ROUTE
|
|
44
|
+
route CALLBACK_ROUTE
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_migrations
|
|
48
|
+
generate 'migration', 'CreateUsers', 'mixin_user_id:string!:uniq name:string avatar_url:string', '--force'
|
|
49
|
+
generate 'migration', 'CreateSessions', 'user:references ip_address:string user_agent:string', '--force'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_omniauth_gems
|
|
53
|
+
gemfile = File.read(gemfile_path)
|
|
54
|
+
uncommented = []
|
|
55
|
+
|
|
56
|
+
OMNIAUTH_GEMS.each do |name|
|
|
57
|
+
pattern = /gem\s+["']#{Regexp.escape(name)}["']/
|
|
58
|
+
next if gemfile.match?(/^\s*#{pattern}/) # already present
|
|
59
|
+
|
|
60
|
+
if gemfile.match?(/^\s*#\s*#{pattern}/)
|
|
61
|
+
uncomment_lines 'Gemfile', pattern
|
|
62
|
+
uncommented << name
|
|
63
|
+
else
|
|
64
|
+
# Single positional call: bundle_command signatures differ across
|
|
65
|
+
# railties versions (8.1 BundleHelper is (command, env, params);
|
|
66
|
+
# older Actions is (command, options)) — only this shape is valid
|
|
67
|
+
# on all of them, and bundler handles --quiet itself.
|
|
68
|
+
bundle_command("add #{name} --quiet")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
bundle_command('install --quiet') unless uncommented.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def gemfile_path
|
|
78
|
+
File.expand_path('Gemfile', destination_root)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/generators/mixin_bot/authentication/templates/app/controllers/concerns/authentication.rb.tt
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Authentication
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
before_action :require_authentication
|
|
6
|
+
helper_method :authenticated?
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def allow_unauthenticated_access(**options)
|
|
11
|
+
skip_before_action :require_authentication, **options
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
def authenticated?
|
|
17
|
+
resume_session
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def require_authentication
|
|
21
|
+
resume_session || request_authentication
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def resume_session
|
|
25
|
+
Current.session ||= find_session_by_cookie
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def find_session_by_cookie
|
|
29
|
+
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def request_authentication
|
|
33
|
+
session[:return_to_after_authenticating] = request.url
|
|
34
|
+
redirect_to new_session_path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def after_authentication_url
|
|
38
|
+
session.delete(:return_to_after_authenticating) || root_url
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def start_new_session_for(user)
|
|
42
|
+
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
|
43
|
+
Current.session = session
|
|
44
|
+
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def terminate_session
|
|
49
|
+
Current.session&.destroy
|
|
50
|
+
cookies.delete(:session_id)
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/generators/mixin_bot/authentication/templates/app/controllers/sessions_controller.rb.tt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class SessionsController < ApplicationController
|
|
2
|
+
allow_unauthenticated_access only: %i[new create]
|
|
3
|
+
|
|
4
|
+
def new
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def create
|
|
8
|
+
auth = request.env["omniauth.auth"]
|
|
9
|
+
|
|
10
|
+
unless auth
|
|
11
|
+
redirect_to new_session_path, alert: "Sign in with Mixin failed. Please try again."
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
user = User.find_or_create_by_mixin_auth!(auth)
|
|
16
|
+
start_new_session_for user
|
|
17
|
+
redirect_to after_authentication_url
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy
|
|
21
|
+
terminate_session
|
|
22
|
+
redirect_to new_session_path, status: :see_other
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class User < ApplicationRecord
|
|
2
|
+
has_many :sessions, dependent: :destroy
|
|
3
|
+
|
|
4
|
+
# Finds or creates the user for an OmniAuth auth hash and refreshes the
|
|
5
|
+
# stored Mixin profile (name, avatar). The OAuth exchange is used only to
|
|
6
|
+
# establish identity; credentials are not stored.
|
|
7
|
+
def self.find_or_create_by_mixin_auth!(auth)
|
|
8
|
+
user = find_or_create_by!(mixin_user_id: auth.uid)
|
|
9
|
+
user.update!(name: auth.info.name, avatar_url: auth.info.image)
|
|
10
|
+
user
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "omniauth"
|
|
4
|
+
require "omniauth/strategies/mixin"
|
|
5
|
+
|
|
6
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
7
|
+
provider :mixin,
|
|
8
|
+
ENV.fetch("MIXIN_CLIENT_ID", nil),
|
|
9
|
+
ENV.fetch("MIXIN_CLIENT_SECRET", nil),
|
|
10
|
+
setup: lambda { |env|
|
|
11
|
+
client_id = ENV["MIXIN_CLIENT_ID"]
|
|
12
|
+
client_secret = ENV["MIXIN_CLIENT_SECRET"]
|
|
13
|
+
|
|
14
|
+
if client_id.blank? || client_secret.blank?
|
|
15
|
+
raise ArgumentError, "Missing Mixin OAuth credentials: set MIXIN_CLIENT_ID and MIXIN_CLIENT_SECRET"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
env["omniauth.strategy"].options[:client_id] = client_id
|
|
19
|
+
env["omniauth.strategy"].options[:client_secret] = client_secret
|
|
20
|
+
}
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Adds Mixin Blaze message routing to this Rails application: a routes
|
|
3
|
+
initializer wired to MixinBot's `blaze_handler`, an example text handler
|
|
4
|
+
under app/mixin/handlers, and the `mixin_blaze` Puma plugin line in
|
|
5
|
+
config/puma.rb so messages are received inside the web process tree.
|
|
6
|
+
|
|
7
|
+
Requires MixinBot bot credentials (app_id, session_id,
|
|
8
|
+
session_private_key, server_public_key) to be configured, e.g. from
|
|
9
|
+
environment variables.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
rails generate mixin_bot:blaze
|
|
13
|
+
|
|
14
|
+
This will create:
|
|
15
|
+
config/initializers/mixin_bot_blaze.rb
|
|
16
|
+
app/mixin/handlers/text_handler.rb
|
|
17
|
+
|
|
18
|
+
It will also add `plugin :mixin_blaze` to config/puma.rb (creating it
|
|
19
|
+
when missing) without duplicating an existing line.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module MixinBot
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate mixin_bot:blaze` — scaffolds Mixin Blaze message
|
|
8
|
+
# routing: a routes initializer wired to `config.blaze_handler`, an
|
|
9
|
+
# example text handler under `app/mixin/handlers`, and the `mixin_blaze`
|
|
10
|
+
# Puma plugin line in `config/puma.rb` so the app receives messages in
|
|
11
|
+
# its own process tree.
|
|
12
|
+
class BlazeGenerator < Rails::Generators::Base
|
|
13
|
+
source_root File.expand_path('templates', __dir__)
|
|
14
|
+
|
|
15
|
+
PUMA_PLUGIN_LINE = 'plugin :mixin_blaze'
|
|
16
|
+
PUMA_COMMENT = '# Receive Mixin Blaze messages in the web process tree (MixinBot).'
|
|
17
|
+
|
|
18
|
+
def create_routes_initializer
|
|
19
|
+
template 'config/initializers/mixin_bot_blaze.rb'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_example_handler
|
|
23
|
+
template 'app/mixin/handlers/text_handler.rb'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def configure_puma_plugin
|
|
27
|
+
if File.exist?(puma_path)
|
|
28
|
+
return if File.read(puma_path).match?(/^\s*plugin\s+:mixin_blaze\b/)
|
|
29
|
+
|
|
30
|
+
append_to_file 'config/puma.rb', "\n#{PUMA_COMMENT}\n#{PUMA_PLUGIN_LINE}\n"
|
|
31
|
+
else
|
|
32
|
+
create_file 'config/puma.rb', "#{PUMA_COMMENT}\n#{PUMA_PLUGIN_LINE}\n"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def puma_path
|
|
39
|
+
File.expand_path('config/puma.rb', destination_root)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mixin
|
|
4
|
+
module Handlers
|
|
5
|
+
# Example Blaze handler: echoes text messages back to the sender.
|
|
6
|
+
# Handlers are instantiated per message; `message` exposes decoded data,
|
|
7
|
+
# conversation/sender ids, and the `reply` helper (HTTP message API).
|
|
8
|
+
class TextHandler < MixinBot::Blaze::Router::Base
|
|
9
|
+
def handle
|
|
10
|
+
reply("You said: #{message.data}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Mixin Blaze message routing.
|
|
4
|
+
#
|
|
5
|
+
# Requires bot credentials in MixinBot.configure (app_id, session_id,
|
|
6
|
+
# session_private_key, server_public_key, spend_key) — typically from
|
|
7
|
+
# environment variables — and the `mixin_blaze` Puma plugin in
|
|
8
|
+
# config/puma.rb to actually receive messages.
|
|
9
|
+
#
|
|
10
|
+
# Routes dispatch decoded envelopes in registration order (first match
|
|
11
|
+
# wins); handlers live under app/mixin/handlers and are re-resolved on
|
|
12
|
+
# every code reload in development.
|
|
13
|
+
|
|
14
|
+
Rails.application.config.to_prepare do
|
|
15
|
+
MixinBot.configure do
|
|
16
|
+
self.blaze_handler = MixinBot::Blaze::Router.new do
|
|
17
|
+
on 'text', Mixin::Handlers::TextHandler
|
|
18
|
+
# on 'transfer', Mixin::Handlers::TransferHandler
|
|
19
|
+
# on category: 'APP_CARD', handler: Mixin::Handlers::AppCardHandler
|
|
20
|
+
# on action: 'CREATE_MESSAGE', handler: ->(message) { ... }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|