glottis 0.3.1 → 0.4.1
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/CHANGELOG.md +13 -0
- data/README.md +2 -1
- data/lib/glottis/client.rb +13 -6
- data/lib/glottis/handlers/remote_input_handler.rb +117 -86
- data/lib/glottis/handlers/remote_output_handler.rb +126 -96
- data/lib/glottis/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbc357e1258fd3dafb205cf7516918290a83f168a50bea6ea27b251ee5fff89d
|
|
4
|
+
data.tar.gz: 7390b269ea50726f631f1315cad2620e06192c0769efc24d95da09c735e7ed48
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1779ab3291678ed3b9fb52e537f2362f2d70b8ca83132db41685676df001f3d632fb858ed3408274ecf78dda20f64969824dd1dca793a1dc2dedb216489b59e2
|
|
7
|
+
data.tar.gz: 81de67b3295200ac4108f4b691c78e5f41a308171bfce76facdabe7c31c64ec98bb908e17dff76025483b4afd7bd5b7c365ca192e76d62f66950e5fab9f4f075
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semmle.com/semver/1.0.0/).
|
|
7
7
|
|
|
8
|
+
## [0.4.1] - 2026-07-24
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Production release with Ruby 3.4+ compatibility, GitHub Actions CI, and live Valyx server integration.
|
|
12
|
+
- Added GitHub Release badge and explicit `Content-Type: application/json` headers.
|
|
13
|
+
- Enhanced `RemoteInputHandler` with Server-Sent Events (SSE) stream parsing (`data: {...}`).
|
|
14
|
+
- Enhanced `RemoteOutputHandler` with automatic session cookie forwarding (`SID`) and strict JSON payload formatting (`{"to": "*", "body": "..."}`).
|
|
15
|
+
|
|
16
|
+
## [0.4.0] - 2026-07-24
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Minor version release for full Ruby 3.4+ compatibility and modernized dependency stack.
|
|
20
|
+
|
|
8
21
|
## [0.3.1] - 2026-07-24
|
|
9
22
|
|
|
10
23
|
### Changed
|
data/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
A CLI for the [valyx](https://github.com/maxdeliso/valyx) message passing server.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/maxdeliso/glottis/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/maxdeliso/glottis/releases/latest)
|
|
6
7
|
[](https://badge.fury.io/rb/glottis)
|
|
7
8
|
|
|
8
9
|
## Requirements
|
|
@@ -17,7 +18,7 @@ Install the gem by executing:
|
|
|
17
18
|
|
|
18
19
|
Or add it to your application's Gemfile:
|
|
19
20
|
|
|
20
|
-
$ gem 'glottis', '~> 0.
|
|
21
|
+
$ gem 'glottis', '~> 0.4'
|
|
21
22
|
|
|
22
23
|
## Usage
|
|
23
24
|
|
data/lib/glottis/client.rb
CHANGED
|
@@ -42,14 +42,21 @@ module Glottis
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
44
|
def start_handlers
|
|
45
|
+
remote_output = Handlers::RemoteOutputHandler.new(@outgoing, @host, @port)
|
|
46
|
+
remote_output.start
|
|
47
|
+
|
|
48
|
+
sleep 0.1 until remote_output.cookie || remote_output.sid
|
|
49
|
+
|
|
50
|
+
cookie = remote_output.cookie
|
|
51
|
+
remote_input = Handlers::RemoteInputHandler.new(@incoming, @host, @port, cookie)
|
|
52
|
+
remote_input.start
|
|
53
|
+
|
|
45
54
|
@handlers = [
|
|
46
|
-
Handlers::ConsoleInputHandler.new(@outgoing),
|
|
47
|
-
Handlers::ConsoleOutputHandler.new(@incoming),
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
Handlers::ConsoleInputHandler.new(@outgoing).start,
|
|
56
|
+
Handlers::ConsoleOutputHandler.new(@incoming).start,
|
|
57
|
+
remote_input,
|
|
58
|
+
remote_output
|
|
50
59
|
]
|
|
51
|
-
|
|
52
|
-
@handlers.each(&:start)
|
|
53
60
|
end
|
|
54
61
|
|
|
55
62
|
def stop_handlers
|
|
@@ -1,86 +1,117 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'logger'
|
|
4
|
-
require 'uri'
|
|
5
|
-
require 'net/http'
|
|
6
|
-
require 'json'
|
|
7
|
-
|
|
8
|
-
module Glottis
|
|
9
|
-
module Handlers
|
|
10
|
-
# Manages stream input from the remote valyx server.
|
|
11
|
-
class RemoteInputHandler
|
|
12
|
-
PROTOCOL = 'http'
|
|
13
|
-
STREAM_DELIMITER = "\0"
|
|
14
|
-
READ_TIMEOUT = 3600
|
|
15
|
-
REMOTE_PATHS = {
|
|
16
|
-
get_message_stream: '/api/messages/stream'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@
|
|
22
|
-
@
|
|
23
|
-
@
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@http
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module Glottis
|
|
9
|
+
module Handlers
|
|
10
|
+
# Manages stream input from the remote valyx server.
|
|
11
|
+
class RemoteInputHandler
|
|
12
|
+
PROTOCOL = 'http'
|
|
13
|
+
STREAM_DELIMITER = "\0"
|
|
14
|
+
READ_TIMEOUT = 3600
|
|
15
|
+
REMOTE_PATHS = {
|
|
16
|
+
get_message_stream: '/api/v1/messages/stream',
|
|
17
|
+
legacy_message_stream: '/api/messages/stream'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(incoming, host, port, cookie = nil)
|
|
21
|
+
@incoming = incoming
|
|
22
|
+
@host = host
|
|
23
|
+
@port = port
|
|
24
|
+
@cookie = cookie
|
|
25
|
+
@running = false
|
|
26
|
+
@thread = nil
|
|
27
|
+
@buffer = String.new
|
|
28
|
+
setup_http
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def start
|
|
32
|
+
@running = true
|
|
33
|
+
@thread = Thread.new do
|
|
34
|
+
@http.start
|
|
35
|
+
read_loop
|
|
36
|
+
end
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def join
|
|
41
|
+
@thread&.join
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def cleanup
|
|
45
|
+
@running = false
|
|
46
|
+
@http.finish if @http.started?
|
|
47
|
+
@thread&.kill if @thread&.alive?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def setup_http
|
|
53
|
+
@http = Net::HTTP.new(@host, @port)
|
|
54
|
+
@http.open_timeout = 2
|
|
55
|
+
@http.read_timeout = READ_TIMEOUT
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def read_loop
|
|
59
|
+
Client.logger.info('reading stream...')
|
|
60
|
+
headers = @cookie ? { 'Cookie' => @cookie } : {}
|
|
61
|
+
|
|
62
|
+
stream_request(headers) do |chunk|
|
|
63
|
+
break unless @running
|
|
64
|
+
|
|
65
|
+
process_chunk(chunk)
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
Client.logger.error("Error reading message stream: #{e.message}") if @running
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stream_request(headers, &block)
|
|
72
|
+
req = Net::HTTP::Get.new(REMOTE_PATHS.fetch(:get_message_stream), headers)
|
|
73
|
+
|
|
74
|
+
@http.request(req) do |response|
|
|
75
|
+
if Integer(response.code) == 404
|
|
76
|
+
legacy_req = Net::HTTP::Get.new(REMOTE_PATHS.fetch(:legacy_message_stream), headers)
|
|
77
|
+
@http.request(legacy_req) { |legacy_resp| legacy_resp.read_body(&block) }
|
|
78
|
+
else
|
|
79
|
+
response.read_body(&block)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def process_chunk(chunk)
|
|
85
|
+
@buffer << chunk
|
|
86
|
+
|
|
87
|
+
# Process SSE format ("data: {...}\n\n")
|
|
88
|
+
while (line_end = @buffer.index("\n"))
|
|
89
|
+
line = @buffer.slice!(0..line_end).strip
|
|
90
|
+
next if line.empty? || line.start_with?(':')
|
|
91
|
+
|
|
92
|
+
if line.start_with?('data:')
|
|
93
|
+
json_str = line.sub(/^data:\s*/, '').chomp
|
|
94
|
+
parse_and_push(json_str)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Process null-delimited format
|
|
99
|
+
while (delimiter_index = @buffer.index(STREAM_DELIMITER))
|
|
100
|
+
msg_raw = @buffer.slice!(0..delimiter_index)
|
|
101
|
+
msg_data = msg_raw.chomp(STREAM_DELIMITER)
|
|
102
|
+
parse_and_push(msg_data)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_and_push(raw)
|
|
107
|
+
return if raw.empty?
|
|
108
|
+
|
|
109
|
+
parsed_msg = JSON.parse(raw)
|
|
110
|
+
parsed_msg['msg'] ||= parsed_msg['body']
|
|
111
|
+
@incoming.push(parsed_msg)
|
|
112
|
+
rescue JSON::ParserError => e
|
|
113
|
+
Client.logger.warn("Failed to parse JSON payload: #{e.message}")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -1,96 +1,126 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'logger'
|
|
4
|
-
require 'uri'
|
|
5
|
-
require 'net/http'
|
|
6
|
-
require 'json'
|
|
7
|
-
|
|
8
|
-
module Glottis
|
|
9
|
-
module Handlers
|
|
10
|
-
# Manages posting outgoing messages to the remote valyx server.
|
|
11
|
-
class RemoteOutputHandler
|
|
12
|
-
POLL_INTERVAL = 0.1
|
|
13
|
-
PROTOCOL = 'http'
|
|
14
|
-
|
|
15
|
-
REMOTE_PATHS = {
|
|
16
|
-
get_session: '/api/session',
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module Glottis
|
|
9
|
+
module Handlers
|
|
10
|
+
# Manages posting outgoing messages to the remote valyx server.
|
|
11
|
+
class RemoteOutputHandler
|
|
12
|
+
POLL_INTERVAL = 0.1
|
|
13
|
+
PROTOCOL = 'http'
|
|
14
|
+
|
|
15
|
+
REMOTE_PATHS = {
|
|
16
|
+
get_session: '/api/v1/session',
|
|
17
|
+
legacy_session: '/api/session',
|
|
18
|
+
post_message: '/api/v1/messages',
|
|
19
|
+
legacy_post_message: '/api/message'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :cookie, :sid
|
|
23
|
+
|
|
24
|
+
def initialize(outgoing, host, port)
|
|
25
|
+
@outgoing = outgoing
|
|
26
|
+
@host = host
|
|
27
|
+
@port = port
|
|
28
|
+
@running = false
|
|
29
|
+
@thread = nil
|
|
30
|
+
@sid = nil
|
|
31
|
+
@cookie = nil
|
|
32
|
+
setup_http
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def start
|
|
36
|
+
@running = true
|
|
37
|
+
@thread = Thread.new do
|
|
38
|
+
@http.start
|
|
39
|
+
request_session
|
|
40
|
+
|
|
41
|
+
send_queued while @running
|
|
42
|
+
end
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def join
|
|
47
|
+
@thread&.join
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cleanup
|
|
51
|
+
@running = false
|
|
52
|
+
@http.finish if @http.started?
|
|
53
|
+
@thread&.kill if @thread&.alive?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def setup_http
|
|
59
|
+
@http = Net::HTTP.new(@host, @port)
|
|
60
|
+
@http.open_timeout = 2
|
|
61
|
+
@http.read_timeout = 2
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def request_session
|
|
65
|
+
Client.logger.info('making initial request for session...')
|
|
66
|
+
|
|
67
|
+
headers = { 'Content-Type' => 'application/json' }
|
|
68
|
+
session_req = fetch_session(headers)
|
|
69
|
+
|
|
70
|
+
code = Integer(session_req.code)
|
|
71
|
+
raise "failed to retrieve session: #{session_req.body}" unless [200, 201].include?(code)
|
|
72
|
+
|
|
73
|
+
parse_session_response(session_req)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def fetch_session(headers)
|
|
77
|
+
req = @http.post(REMOTE_PATHS.fetch(:get_session), '', headers)
|
|
78
|
+
return req if Integer(req.code) != 404
|
|
79
|
+
|
|
80
|
+
@http.get(REMOTE_PATHS.fetch(:legacy_session))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_session_response(session_req)
|
|
84
|
+
if session_req['set-cookie']
|
|
85
|
+
raw_cookie = session_req['set-cookie']
|
|
86
|
+
@cookie = raw_cookie.split(';').first
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
response = begin
|
|
90
|
+
JSON.parse(session_req.body)
|
|
91
|
+
rescue StandardError
|
|
92
|
+
{}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@sid = response['username'] || response['sid'] || @cookie
|
|
96
|
+
Client.logger.info("current session id: #{@sid}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def if_not_ok(http_req, ok_codes = [200, 201])
|
|
100
|
+
yield http_req.body unless ok_codes.include?(Integer(http_req.code))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def send_queued
|
|
104
|
+
raw_msg = @outgoing.pop
|
|
105
|
+
return if raw_msg.nil? || raw_msg == :shutdown
|
|
106
|
+
|
|
107
|
+
Client.logger.info("processing message with #{@outgoing.size} messages outgoing")
|
|
108
|
+
|
|
109
|
+
headers = { 'Content-Type' => 'application/json' }
|
|
110
|
+
headers['Cookie'] = @cookie if @cookie
|
|
111
|
+
|
|
112
|
+
modern_data = { to: '*', body: raw_msg }.to_json
|
|
113
|
+
post_req = @http.post(REMOTE_PATHS.fetch(:post_message), modern_data, headers)
|
|
114
|
+
|
|
115
|
+
if Integer(post_req.code) == 404
|
|
116
|
+
legacy_data = { from: @sid, to: '*', msg: raw_msg }.to_json
|
|
117
|
+
post_req = @http.post(REMOTE_PATHS.fetch(:legacy_post_message), legacy_data, headers)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if_not_ok(post_req) do |err|
|
|
121
|
+
Client.logger.warn("failed to post message: #{raw_msg} #{err}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
data/lib/glottis/version.rb
CHANGED