mixin_bot 2.5.0 → 2.6.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce614b6fab815a9d8dfb1e93914fdaf3ba6b7306b1f5d35ab594efcd0b46fe68
4
- data.tar.gz: 70b09c5ba22efc597a43b560a4257e2984b059b8334eee6efa19498934e87407
3
+ metadata.gz: 42fc9aa30981430bf8a4b32ac343994fedb94f3a318d4c519404f788f66d8c68
4
+ data.tar.gz: df1037544f1eb11ec4cf196aacbd076db8a3aa6c2dc11ed1d138751cdb37e073
5
5
  SHA512:
6
- metadata.gz: ab61a3850c702cb36a00794f5cfacb3aa63d18d6fca24104d9be336b97b04332430eb702bba50d6bbaa80f9ad94976fa5aaeefaae69f736bed88b21ffce7f849
7
- data.tar.gz: c2a237075b8c9399ef4f90d933d1bac288de35aefa11ff6fa55d3dd4e19a337d0a10f78173a9eef77a16476a63e208fa22b0c3250758ad7c2324490411ffa4b7
6
+ metadata.gz: a12c51222b50582cd31b4e80d17f3f95c73627845ec3500701a02de21d68de887e85ccf599e505be8ec81ce6c903485ec193ae6ee3e865dce9e2bd2b984ef023
7
+ data.tar.gz: 1dbb16ececb43acf6febcc42467ffb631b064fcb508f235c4a6e19148c15c328935a9ec1a297bffa45baebd70fc0b7458bf98d374fc2f5c0930734345bf67d29
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.6.0] - 2026-09-07
11
+
12
+ ### Added
13
+
14
+ - **`API#blaze_async`** — fiber-based sibling of `blaze`: returns a connected `async-websocket` client (same `wss://<blaze_host>/` URL, `Mixin-Blaze-1` subprotocol, Bearer JWT, User-Agent, and shared frame codec) for hosts driving Blaze outside EventMachine. `handler:` is yield-through for caller-supplied `Async::WebSocket::Connection` subclasses; `endpoint_options:` forwards to `Async::HTTP::Endpoint.parse`. Forces the HTTP/1 upgrade path via ALPN. Caller-side wire notes: send `write_ws_message` byte arrays as `BinaryMessage.new(ary.pack('C*'))`, feed `message.to_str` into `ws_message`, and own the keepalive ping (server pings are auto-replied).
15
+
10
16
  ## [2.5.0] - 2026-09-03
11
17
 
12
18
  ### Added
data/README.md CHANGED
@@ -6,7 +6,7 @@ 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: **2.3.0** (see [CHANGELOG.md](CHANGELOG.md) for breaking changes and deprecations).
9
+ Current gem version: **2.6.0** (see [CHANGELOG.md](CHANGELOG.md) for breaking changes and deprecations).
10
10
 
11
11
  ## Requirements
12
12
 
@@ -257,6 +257,39 @@ end
257
257
 
258
258
  For outbound messages over an open socket, use `blaze_send_plain_text`, `blaze_send_contact`, `blaze_send_app_card`, and related helpers (parity with Go `BlazeClient`).
259
259
 
260
+ ### Without EventMachine: `blaze_async`
261
+
262
+ `blaze_async` returns the connected `async-websocket` connection instead of a `Faye::WebSocket::Client` — same URL, `Mixin-Blaze-1` subprotocol, and frame codec; the caller drives the loop in an `Async` reactor. Wire notes: wrap `write_ws_message` byte arrays in `Protocol::WebSocket::BinaryMessage`, feed `message.to_str` into `ws_message`, and own the keepalive ping (see `examples/blaze_async.rb`).
263
+
264
+ ```ruby
265
+ require 'async'
266
+ require 'mixin_bot'
267
+
268
+ Async do |task|
269
+ connection = MixinBot.api.blaze_async
270
+
271
+ task.async do # keepalive is the caller's job
272
+ loop do
273
+ sleep 30
274
+ connection.send_ping
275
+ end
276
+ end
277
+
278
+ connection.write Protocol::WebSocket::BinaryMessage.new(MixinBot.api.list_pending_message.pack('C*'))
279
+ while (message = connection.read)
280
+ raw = JSON.parse MixinBot.api.ws_message(message.to_str)
281
+ # data is a Hash for message envelopes, an Array in LIST_PENDING_MESSAGES replies
282
+ data = raw['data'].is_a?(Hash) ? raw['data'] : {}
283
+ next unless (message_id = data['message_id'])
284
+
285
+ bytes = MixinBot.api.acknowledge_message_receipt(message_id)
286
+ connection.write Protocol::WebSocket::BinaryMessage.new(bytes.pack('C*'))
287
+ end
288
+ ensure
289
+ connection&.close
290
+ end
291
+ ```
292
+
260
293
  ## Deep links and bot auth
261
294
 
262
295
  ```ruby
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fiber-based Blaze client — the async-websocket sibling of examples/blaze.rb.
4
+ #
5
+ # Drives API#blaze_async without EventMachine: the gem returns the connected
6
+ # connection and the caller runs the loop inside an Async reactor. The frame
7
+ # codec (write_ws_message / ws_message / list_pending_message /
8
+ # acknowledge_message_receipt) is shared with blaze verbatim; only the wire
9
+ # differs:
10
+ # - *Send*: write_ws_message returns an Array of byte integers (Faye framed
11
+ # that itself) — wrap it in a Protocol::WebSocket::BinaryMessage.
12
+ # - *Read*: connection.read yields a Protocol::WebSocket::Message (or nil
13
+ # after a clean close) — feed message.to_str into ws_message.
14
+ # - *Keepalive*: Faye's ping: 60 becomes the caller's job; server pings are
15
+ # auto-replied by the protocol layer.
16
+ #
17
+ # Run: ruby examples/blaze_async.rb
18
+
19
+ require './lib/mixin_bot'
20
+ require 'async'
21
+ require 'base64'
22
+ require 'json'
23
+ require 'securerandom'
24
+ require 'yaml'
25
+
26
+ CONFIG = YAML.load_file("#{File.dirname __FILE__}/config.yml")
27
+ MixinBot.configure do
28
+ self.app_id = CONFIG['app_id']
29
+ self.client_secret = CONFIG['client_secret']
30
+ self.session_id = CONFIG['session_id']
31
+ self.server_public_key = CONFIG['server_public_key']
32
+ self.session_private_key = CONFIG['session_private_key']
33
+ end
34
+
35
+ API = MixinBot.api
36
+
37
+ # gzip+JSON bytes from write_ws_message -> one binary WebSocket frame
38
+ def send_frame(connection, bytes)
39
+ connection.write Protocol::WebSocket::BinaryMessage.new(bytes.pack('C*'))
40
+ end
41
+
42
+ Async do |task|
43
+ # endpoint_options: is forwarded to Async::HTTP::Endpoint.parse (a connect
44
+ # timeout here). handler: stays default; pass an Async::WebSocket::Connection
45
+ # subclass to hook the raw frame events (e.g. PONG correlation).
46
+ connection = API.blaze_async(endpoint_options: { timeout: 10 })
47
+ p [Time.now.to_s, :connected]
48
+
49
+ # liveness is the caller's policy: nothing on the wire goes stale quietly
50
+ keepalive = task.async do
51
+ loop do
52
+ sleep 30
53
+ connection.send_ping
54
+ end
55
+ rescue Protocol::WebSocket::ProtocolError, IOError # IOError covers EOFError
56
+ # connection is gone; the read loop below is already winding down
57
+ end
58
+
59
+ # sends nothing on open — mirror blaze and request pending messages first
60
+ send_frame connection, API.list_pending_message
61
+
62
+ # read returns nil after a clean close; an abrupt one raises EOFError
63
+ while (message = connection.read)
64
+ raw = JSON.parse API.ws_message(message.to_str)
65
+ p [Time.now.to_s, :on_message, raw&.[]('action')]
66
+
67
+ # data is a Hash for message envelopes, but an Array (the pending list)
68
+ # in the LIST_PENDING_MESSAGES reply
69
+ data = raw['data'].is_a?(Hash) ? raw['data'] : {}
70
+ send_frame connection, API.acknowledge_message_receipt(data['message_id']) if data['message_id']
71
+
72
+ # echo PLAIN_TEXT back to its sender over the same connection
73
+ next unless data['category'] == 'PLAIN_TEXT'
74
+
75
+ send_frame connection, API.write_ws_message(
76
+ params: {
77
+ conversation_id: data['conversation_id'],
78
+ recipient_id: data['user_id'],
79
+ message_id: SecureRandom.uuid,
80
+ category: 'PLAIN_TEXT',
81
+ data_base64: Base64.urlsafe_encode64("echo: #{Base64.urlsafe_decode64(data['data'])}", padding: false)
82
+ }
83
+ )
84
+ end
85
+ p [Time.now.to_s, :closed]
86
+ ensure
87
+ keepalive&.stop
88
+ connection&.close
89
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async/http/endpoint'
4
+ require 'async/http/protocol/http11'
5
+ require 'async/websocket/client'
6
+
7
+ module MixinBot
8
+ class API
9
+ # Fiber-based sibling of +Blaze#blaze+: returns a connected
10
+ # +Async::WebSocket+ client instead of a +Faye::WebSocket::Client+.
11
+ #
12
+ # Same endpoint, +Mixin-Blaze-1+ subprotocol, Bearer JWT, and User-Agent as
13
+ # +blaze+. The frame codec (+ws_message+, +write_ws_message+,
14
+ # +list_pending_message+, +acknowledge_message_receipt+) is
15
+ # transport-agnostic and shared verbatim.
16
+ #
17
+ # Wire differences the caller must handle:
18
+ # - *Send*: +write_ws_message+ returns an +Array+ of byte integers (the
19
+ # Faye backend framed that automatically); here the caller wraps it:
20
+ # +connection.write(Protocol::WebSocket::BinaryMessage.new(bytes.pack('C*')))+.
21
+ # - *Read*: +connection.read+ returns a +Protocol::WebSocket::Message+;
22
+ # pass +message.to_str+ (the binary buffer) into +ws_message+.
23
+ # - *Keepalive*: Faye's +ping: 60+ becomes the caller's job (server pings
24
+ # are still auto-replied by the protocol layer).
25
+ #
26
+ # Sends nothing on open — the caller sends +list_pending_message+ first,
27
+ # mirroring +blaze+. No per-IO read timeout by default: quiet-but-healthy
28
+ # connections are legitimate; liveness is the caller's policy.
29
+ module BlazeAsync
30
+ # Returns the block-less +Client.connect+ connection, preserving
31
+ # +blaze+'s "returns a client the caller drives" contract.
32
+ #
33
+ # +handler:+ is yield-through so the caller can supply an
34
+ # +Async::WebSocket::Connection+ subclass (e.g. one that correlates
35
+ # PONG frames). +endpoint_options:+ are forwarded to
36
+ # +Async::HTTP::Endpoint.parse+ (e.g. +timeout:+ for the connect phase).
37
+ def blaze_async(handler: Async::WebSocket::Connection, endpoint_options: {})
38
+ access_token = access_token('GET', '/', '')
39
+ authorization = format('Bearer %<access_token>s', access_token:)
40
+
41
+ endpoint = Async::HTTP::Endpoint.parse(
42
+ format('wss://%<host>s/', host: config.blaze_host),
43
+ # Force the HTTP/1 upgrade path: avoids the RFC 8441 (h2 CONNECT)
44
+ # route, which not every gateway negotiates the same way.
45
+ alpn_protocols: Async::HTTP::Protocol::HTTP11.names,
46
+ **endpoint_options
47
+ )
48
+
49
+ Async::WebSocket::Client.connect(
50
+ endpoint,
51
+ protocols: ['Mixin-Blaze-1'],
52
+ headers: { 'Authorization' => authorization, 'User-Agent' => "mixin_bot/#{MixinBot::VERSION}" },
53
+ handler: handler
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/mixin_bot/api.rb CHANGED
@@ -8,6 +8,7 @@ require_relative 'api/asset'
8
8
  require_relative 'api/attachment'
9
9
  require_relative 'api/auth'
10
10
  require_relative 'api/blaze'
11
+ require_relative 'api/blaze_async'
11
12
  require_relative 'api/chain'
12
13
  require_relative 'api/code'
13
14
  require_relative 'api/circle'
@@ -338,6 +339,7 @@ module MixinBot
338
339
  include MixinBot::API::Attachment
339
340
  include MixinBot::API::Auth
340
341
  include MixinBot::API::Blaze
342
+ include MixinBot::API::BlazeAsync
341
343
  include MixinBot::API::Chain
342
344
  include MixinBot::API::Code
343
345
  include MixinBot::API::Circle
@@ -28,6 +28,7 @@ module MixinBot
28
28
  INTERACTIVE_API_METHODS = %i[
29
29
  start_blaze_connect
30
30
  blaze
31
+ blaze_async
31
32
  upload_attachment
32
33
  ].freeze
33
34
 
@@ -11,5 +11,5 @@ module MixinBot
11
11
  #
12
12
  # @see https://semver.org/
13
13
  #
14
- VERSION = '2.5.0'
14
+ VERSION = '2.6.0'
15
15
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mixin_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - an-lee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-02 00:00:00.000000000 Z
11
+ date: 2026-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: async-websocket
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.30'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.30'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: awesome_print
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -271,6 +285,7 @@ files:
271
285
  - docs/agent/cli.md
272
286
  - docs/agent/cookbook.md
273
287
  - examples/blaze.rb
288
+ - examples/blaze_async.rb
274
289
  - examples/config.yml.example
275
290
  - lib/mixin_bot.rb
276
291
  - lib/mixin_bot/address.rb
@@ -281,6 +296,7 @@ files:
281
296
  - lib/mixin_bot/api/attachment.rb
282
297
  - lib/mixin_bot/api/auth.rb
283
298
  - lib/mixin_bot/api/blaze.rb
299
+ - lib/mixin_bot/api/blaze_async.rb
284
300
  - lib/mixin_bot/api/chain.rb
285
301
  - lib/mixin_bot/api/circle.rb
286
302
  - lib/mixin_bot/api/code.rb