simplex-chat 0.1.0 → 0.3.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: d837e851181b5797fc07d870cbb4170030de614570d1b960ff44a16272e4d765
4
- data.tar.gz: 9c031346b853a2a82a3b11314be2f56b65a381f15cdfddc0e12efd2084f899b6
3
+ metadata.gz: 3f038bfb3c5cc7308a7c53d4a19e4152cfcc4e3f54088093d66b1947d2816f7c
4
+ data.tar.gz: 4e4fa59d7fa8c29225b64bf7856ce9dec9a5551df0038158e4c53e4a60c25b7c
5
5
  SHA512:
6
- metadata.gz: 32b2980044aeeb7a216ae9d8fa4a9f1c0dd075635740a71b3160a83d9e481f126b180397b6d0cae74562434149e3a0f7d60595eebcaacba66ec2c80640cfef65
7
- data.tar.gz: fee2870de270b270750f1e82421ba24ed1689df71a40c1113b1386f907beb11e582a2e3ddb2027c139ba273ff48e6ac4e8f66a12bcefe2d792382e3c0eb4f1c1
6
+ metadata.gz: c3d3091b7fdbc2ff66ed648dc27729860fd79b2434dbb3bbe3f3ec7473042975a8a8aa981b853e302259137aa7b88b1ad5233f4459f9ec1d09aab7d8eadfd165
7
+ data.tar.gz: c690575e7ad589694966f0ed69f3e62b2ce7d9e562cd419b49bed1bc8bcfb07d5dfefc43fed5b2905ceca1d9520d6c5a90153a67ebc52df853a3f3886ea5fe07
data/README.md CHANGED
@@ -5,3 +5,52 @@ A port for the SimpleX Chat client API for Ruby
5
5
  This project is licensed under the GNU AGPL-3.0 (no later versions).
6
6
 
7
7
  Read `LICENSE` for more information.
8
+
9
+ ## Showcase
10
+
11
+ ![showcase](showcase.png)
12
+
13
+ ## Usage
14
+
15
+ 1. Install the Gem from RubyGems
16
+ ```shell
17
+ gem install simplex-chat
18
+ ```
19
+
20
+
21
+ 2. Start your local simplex-chat client on port 5225 (or any port you wish)
22
+
23
+ ```shell
24
+ simplex-chat -p 5225
25
+ ```
26
+
27
+ 3. Connect the `SimpleXChat::ClientAgent` to your local client
28
+
29
+ ```rb
30
+ require 'simplex-chat'
31
+ require 'net/http'
32
+
33
+ client = SimpleXChat::ClientAgent.new URI('ws://localhost:5225')
34
+ ```
35
+
36
+
37
+ 4. Now the client is connected and you can start using the APIs
38
+
39
+ ```rb
40
+ # Get version
41
+ version = client.api_version
42
+ puts "SimpleX Chat version: #{version}"
43
+
44
+ # Listen to incoming client messages
45
+ loop do
46
+ chat_msg = client.next_chat_message
47
+ break if chat_msg == nil
48
+
49
+ # Reply if user sends '/say_hello'
50
+ if chat_msg[:msg_text] == "/say_hello"
51
+ client.api_send_text_message chat_msg[:chat_type], chat_msg[:sender], "Hello! This was sent automagically"
52
+ end
53
+ end
54
+
55
+ # Much more... Read the examples for more information
56
+ ```
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleXChat
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/simplex-chat.rb CHANGED
@@ -8,6 +8,7 @@ module SimpleXChat
8
8
  require 'json'
9
9
  require 'websocket'
10
10
  require 'concurrent'
11
+ require 'time'
11
12
 
12
13
  # Fixes regex match for status line in HTTPResponse
13
14
  class HTTPResponse < Net::HTTPResponse
@@ -27,12 +28,21 @@ module SimpleXChat
27
28
  CONTACT_REQUEST = '<@'
28
29
  end
29
30
 
31
+ module GroupMemberRole
32
+ AUTHOR = 'author' # reserved and unused as of now, but added anyways
33
+ OWNER = 'owner'
34
+ ADMIN = 'admin'
35
+ MEMBER = 'member'
36
+ OBSERVER = 'observer'
37
+ end
38
+
30
39
  class ClientAgent
31
40
  attr_accessor :on_message
32
41
 
33
- def initialize client_uri, connect: true, log_level: Logger::INFO
42
+ def initialize client_uri, connect: true, log_level: Logger::INFO, timeout_ms: 30_000, interval_ms: 100
34
43
  @uri = client_uri
35
44
  @message_queue = SizedQueue.new 4096
45
+ @chat_message_queue = Queue.new
36
46
  @socket = nil
37
47
  @handshake = nil
38
48
 
@@ -40,6 +50,8 @@ module SimpleXChat
40
50
  @listener_thread = nil
41
51
  @corr_id = Concurrent::AtomicFixnum.new(1) # Correlation ID for mapping client responses to command waiters
42
52
  @command_waiters = Concurrent::Hash.new
53
+ @timeout_ms = timeout_ms
54
+ @interval_ms = interval_ms
43
55
 
44
56
  @logger = Logger.new($stderr)
45
57
  @logger.level = log_level
@@ -48,9 +60,7 @@ module SimpleXChat
48
60
  "| [#{severity}] | #{datetime} | (#{progname}) :: #{msg}\n"
49
61
  }
50
62
 
51
- if connect
52
- self.connect
53
- end
63
+ self.connect if connect
54
64
 
55
65
  @logger.debug("Initialized ClientAgent")
56
66
  end
@@ -97,7 +107,7 @@ module SimpleXChat
97
107
  rescue => e
98
108
  # TODO: Verify if this way of stopping the execution
99
109
  # is graceful enough after implementing reconnects
100
- puts "Unhandled exception caught: #{e}"
110
+ @logger.error "Unhandled exception caught: #{e}"
101
111
  @message_queue.close
102
112
  raise e
103
113
  end
@@ -111,14 +121,82 @@ module SimpleXChat
111
121
  @message_queue.pop
112
122
  end
113
123
 
124
+ def next_chat_message
125
+ # NOTE: There can be more than one message per
126
+ # client message. Because of that, we use
127
+ # a chat message queue to insert one or
128
+ # more messages at a time, but poll just
129
+ # one at a time
130
+ return @chat_message_queue.pop if not @chat_message_queue.empty?
131
+
132
+ loop do
133
+ msg = next_message
134
+ break if msg == nil
135
+ next if not ["chatItemUpdated", "newChatItems"].include?(msg["type"])
136
+
137
+ chat_info_types = {
138
+ "direct" => ChatType::DIRECT,
139
+ "group" => ChatType::GROUP
140
+ }
141
+
142
+ # Handle one or more chat messages in a single client message
143
+ new_chat_messages = nil
144
+ if msg["type"] == "chatItemUpdated"
145
+ new_chat_messages = [msg["chatItem"]]
146
+ else
147
+ new_chat_messages = msg["chatItems"]
148
+ end
149
+
150
+ new_chat_messages.each do |chat_item|
151
+ chat_type = chat_info_types.dig(chat_item["chatInfo"]["type"])
152
+ group = nil
153
+ sender = nil
154
+ contact = nil
155
+ contact_role = nil
156
+ if chat_type == ChatType::GROUP
157
+ # NOTE: The group can "send messages" without a contact
158
+ # For example, when a member is removed, the group
159
+ # sends a message about his removal, with no contact
160
+ contact = chat_item.dig "chatItem", "chatDir", "groupMember", "localDisplayName"
161
+ contact_role = chat_item.dig "chatItem", "chatDir", "groupMember", "memberRole"
162
+ group = chat_item["chatInfo"]["groupInfo"]["localDisplayName"]
163
+ sender = group
164
+ else
165
+ contact = chat_item["chatInfo"]["contact"]["localDisplayName"]
166
+ sender = contact
167
+ end
168
+
169
+ msg_text = chat_item["chatItem"]["meta"]["itemText"]
170
+ timestamp = chat_item["chatItem"]["meta"]["updatedAt"]
171
+
172
+ chat_message = {
173
+ :chat_type => chat_type,
174
+ :sender => sender,
175
+ :contact_role => contact_role,
176
+ :contact => contact,
177
+ :group => group,
178
+ :msg_text => msg_text,
179
+ :msg_timestamp => Time.parse(timestamp)
180
+ }
181
+
182
+ @chat_message_queue.push chat_message
183
+ end
184
+
185
+ return @chat_message_queue.pop
186
+ end
187
+
188
+ nil
189
+ end
190
+
114
191
  def disconnect
115
192
  @listener_thread.terminate
116
193
  @socket.close
117
194
  @message_queue.clear
195
+ @chat_message_queue.clear
118
196
  end
119
197
 
120
198
  # Sends a raw command to the SimpleX Chat client
121
- def send_command(cmd)
199
+ def send_command(cmd, timeout_ms: @timeout_ms, interval_ms: @interval_ms)
122
200
  corr_id = next_corr_id
123
201
  obj = {
124
202
  "corrId" => corr_id,
@@ -138,12 +216,16 @@ module SimpleXChat
138
216
  @socket.write frame.to_s
139
217
 
140
218
  msg = nil
141
- 100.times do
219
+ iterations = timeout_ms / interval_ms
220
+ iterations.times do
142
221
  begin
143
222
  msg = single_use_queue.pop(true)
144
223
  break
145
224
  rescue ThreadError
146
- sleep 0.1
225
+ sleep(interval_ms / 1000.0)
226
+ ensure
227
+ # Clean up command_waiters
228
+ @command_waiters.delete corr_id
147
229
  end
148
230
  end
149
231
 
@@ -244,6 +326,48 @@ module SimpleXChat
244
326
  }
245
327
  end
246
328
 
329
+ def api_auto_accept is_enabled
330
+ onoff = is_enabled && "on" || "off"
331
+
332
+ resp = send_command "/auto_accept #{onoff}"
333
+ resp_type = resp["type"]
334
+ raise "Unexpected response: #{resp_type}" if resp_type != "userContactLinkUpdated"
335
+
336
+ nil
337
+ end
338
+
339
+ def api_kick_group_member(group, member)
340
+ resp = send_command "/remove #{group} #{member}"
341
+ resp_type = resp["type"]
342
+ raise "Unexpected response: #{resp_type}" unless resp_type == "userDeletedMember"
343
+ end
344
+
345
+ # Parameters for /network:
346
+ # - socks: on/off/<[ipv4]:port>
347
+ # - socks-mode: always/onion
348
+ # - smp-proxy: always/unknown/unprotected/never
349
+ # - smp-proxy-fallback: no/protected/yes
350
+ # - timeout: <seconds>
351
+ def api_network(socks: nil, socks_mode: nil, smp_proxy: nil, smp_proxy_fallback: nil, timeout_secs: nil)
352
+ args = {
353
+ "socks" => socks,
354
+ "socks-mode" => socks_mode,
355
+ "smp-proxy" => smp_proxy,
356
+ "smp-proxy-fallback" => smp_proxy_fallback,
357
+ "timeout" => timeout_secs
358
+ }
359
+ command = '/network'
360
+ args.each do |param, value|
361
+ next if value == nil
362
+ command += " #{param}=#{value}"
363
+ end
364
+ resp = send_command command
365
+ resp_type = resp["type"]
366
+ raise "Unexpected response: #{resp_type}" if resp_type != "networkConfig"
367
+
368
+ resp["networkConfig"]
369
+ end
370
+
247
371
  private
248
372
 
249
373
  def next_corr_id
data/showcase.png ADDED
Binary file
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplex-chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rdbo
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-22 00:00:00.000000000 Z
10
+ date: 2025-03-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: websocket
@@ -49,6 +49,7 @@ files:
49
49
  - Rakefile
50
50
  - lib/simplex-chat.rb
51
51
  - lib/simplex-chat/version.rb
52
+ - showcase.png
52
53
  homepage: https://github.com/rdbo/simplex-chat-ruby
53
54
  licenses:
54
55
  - AGPL-3.0-only