simplex-chat 0.3.0 → 0.5.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: 3f038bfb3c5cc7308a7c53d4a19e4152cfcc4e3f54088093d66b1947d2816f7c
4
- data.tar.gz: 4e4fa59d7fa8c29225b64bf7856ce9dec9a5551df0038158e4c53e4a60c25b7c
3
+ metadata.gz: a5abf1b7b550f998760cb68e89636e031dcf96aff3449e9422903db050521bd2
4
+ data.tar.gz: 4602313dfbf09bfd9b650c5668f16389d885cf16202b1896fc81f4dd71dfa869
5
5
  SHA512:
6
- metadata.gz: c3d3091b7fdbc2ff66ed648dc27729860fd79b2434dbb3bbe3f3ec7473042975a8a8aa981b853e302259137aa7b88b1ad5233f4459f9ec1d09aab7d8eadfd165
7
- data.tar.gz: c690575e7ad589694966f0ed69f3e62b2ce7d9e562cd419b49bed1bc8bcfb07d5dfefc43fed5b2905ceca1d9520d6c5a90153a67ebc52df853a3f3886ea5fe07
6
+ metadata.gz: 5f77b4edf376118de56ff6dde9a99ee06fc49c12f6ef348053a8ba1ec807b885fac6c2ef4c5c6fe48c21d95a10d7b3351a6ad07a8429603f0ec1ee997ea2923e
7
+ data.tar.gz: f59fb84f87c57f491eda4e3ab58c30451a376c1c8b79c8e6b190df433f53d4394c4c41b459e8e18940bea91c95517ebd82643cf1e3e65c6093462be3910455c1
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleXChat
4
+ # All SimpleX-related errors will inherit from GenericError
5
+ # These errors should be recoverable
6
+ class GenericError < StandardError
7
+ end
8
+
9
+ class SendCommandError < GenericError
10
+ def initialize(cmd)
11
+ super "Failed to send command: #{cmd}"
12
+ end
13
+ end
14
+
15
+ class UnexpectedResponseError < GenericError
16
+ def initialize(type, expected_type)
17
+ super "Unexpected response type: #{type} (expected: #{expected_type})"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleXChat
4
+ require 'net/http'
5
+
6
+ # Fixes regex match for status line in HTTPResponse
7
+ class HTTPResponse < Net::HTTPResponse
8
+ class << self
9
+ def read_status_line(sock)
10
+ str = sock.readline
11
+ m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\Z/in.match(str) or
12
+ raise Net::HTTPBadResponse, "wrong status line: #{str.dump}"
13
+ m.captures
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleXChat
4
+ module ChatType
5
+ DIRECT = '@'
6
+ GROUP = '#'
7
+ CONTACT_REQUEST = '<@'
8
+ end
9
+
10
+ module GroupMemberRole
11
+ AUTHOR = 'author' # reserved and unused as of now, but added anyways
12
+ OWNER = 'owner'
13
+ ADMIN = 'admin'
14
+ MEMBER = 'member'
15
+ OBSERVER = 'observer'
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleXChat
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/simplex-chat.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'simplex-chat/version'
4
+ require_relative 'simplex-chat/errors'
5
+ require_relative 'simplex-chat/patches'
6
+ require_relative 'simplex-chat/types'
4
7
 
5
8
  module SimpleXChat
6
9
  require 'net/http'
@@ -10,36 +13,10 @@ module SimpleXChat
10
13
  require 'concurrent'
11
14
  require 'time'
12
15
 
13
- # Fixes regex match for status line in HTTPResponse
14
- class HTTPResponse < Net::HTTPResponse
15
- class << self
16
- def read_status_line(sock)
17
- str = sock.readline
18
- m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\Z/in.match(str) or
19
- raise Net::HTTPBadResponse, "wrong status line: #{str.dump}"
20
- m.captures
21
- end
22
- end
23
- end
24
-
25
- module ChatType
26
- DIRECT = '@'
27
- GROUP = '#'
28
- CONTACT_REQUEST = '<@'
29
- end
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
-
39
16
  class ClientAgent
40
17
  attr_accessor :on_message
41
18
 
42
- def initialize client_uri, connect: true, log_level: Logger::INFO, timeout_ms: 30_000, interval_ms: 100
19
+ def initialize(client_uri, connect: true, log_level: Logger::INFO, timeout_ms: 10_000, interval_ms: 100)
43
20
  @uri = client_uri
44
21
  @message_queue = SizedQueue.new 4096
45
22
  @chat_message_queue = Queue.new
@@ -87,6 +64,7 @@ module SimpleXChat
87
64
 
88
65
  msg = JSON.parse obj.to_s
89
66
  # @logger.debug("New message: #{msg}")
67
+ # @logger.debug("Command waiters: #{@command_waiters}")
90
68
 
91
69
  corr_id = msg["corrId"]
92
70
  resp = msg["resp"]
@@ -96,6 +74,7 @@ module SimpleXChat
96
74
  single_use_queue.push(resp)
97
75
  @logger.debug("Message sent to waiter with corrId '#{corr_id}'")
98
76
  else
77
+ @logger.debug("Message put on message queue")
99
78
  @message_queue.push resp
100
79
  end
101
80
  rescue IO::WaitReadable
@@ -121,7 +100,9 @@ module SimpleXChat
121
100
  @message_queue.pop
122
101
  end
123
102
 
124
- def next_chat_message
103
+ def next_chat_message(
104
+ max_backlog_secs: 15.0 # if nil, it will process any incoming messages, including old ones
105
+ )
125
106
  # NOTE: There can be more than one message per
126
107
  # client message. Because of that, we use
127
108
  # a chat message queue to insert one or
@@ -134,11 +115,6 @@ module SimpleXChat
134
115
  break if msg == nil
135
116
  next if not ["chatItemUpdated", "newChatItems"].include?(msg["type"])
136
117
 
137
- chat_info_types = {
138
- "direct" => ChatType::DIRECT,
139
- "group" => ChatType::GROUP
140
- }
141
-
142
118
  # Handle one or more chat messages in a single client message
143
119
  new_chat_messages = nil
144
120
  if msg["type"] == "chatItemUpdated"
@@ -148,36 +124,13 @@ module SimpleXChat
148
124
  end
149
125
 
150
126
  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
127
+ chat_message = parse_chat_item chat_item
168
128
 
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
- }
129
+ time_diff = Time.now - chat_message[:msg_timestamp]
130
+ if max_backlog_secs != nil && time_diff > max_backlog_secs
131
+ @logger.debug("Skipped message (time diff: #{time_diff}, max allowed: #{max_backlog_secs}): #{chat_message}")
132
+ next
133
+ end
181
134
 
182
135
  @chat_message_queue.push chat_message
183
136
  end
@@ -212,9 +165,12 @@ module SimpleXChat
212
165
  # command response
213
166
  single_use_queue = SizedQueue.new 1
214
167
  @command_waiters[corr_id] = single_use_queue
168
+ @logger.debug("Created command waiter for command ##{corr_id}")
215
169
 
170
+ @logger.debug("Sending command ##{corr_id}: #{json.to_s}")
216
171
  @socket.write frame.to_s
217
172
 
173
+ @logger.debug("Waiting response for command ##{corr_id}...")
218
174
  msg = nil
219
175
  iterations = timeout_ms / interval_ms
220
176
  iterations.times do
@@ -223,31 +179,31 @@ module SimpleXChat
223
179
  break
224
180
  rescue ThreadError
225
181
  sleep(interval_ms / 1000.0)
226
- ensure
227
- # Clean up command_waiters
228
- @command_waiters.delete corr_id
229
182
  end
230
183
  end
231
184
 
232
185
  if msg == nil
233
- raise "Failed to send command"
186
+ raise SendCommandError.new(json.to_s)
234
187
  end
235
188
 
189
+ @logger.debug("Command ##{corr_id} finished successfully with response: #{msg}")
190
+
236
191
  msg
192
+ ensure
193
+ @command_waiters.delete corr_id
194
+ @logger.debug("Cleaned up command waiter ##{corr_id}")
237
195
  end
238
196
 
239
197
  def api_version
240
198
  resp = send_command '/version'
241
- resp_type = resp["type"]
242
- raise "Unexpected response: #{resp_type}" if resp_type != "versionInfo"
199
+ check_response_type(resp, "versionInfo")
243
200
 
244
201
  resp["versionInfo"]["version"]
245
202
  end
246
203
 
247
204
  def api_profile
248
205
  resp = send_command '/profile'
249
- resp_type = resp["type"]
250
- raise "Unexpected response: #{resp_type}" if resp_type != "userProfile"
206
+ check_response_type(resp, "userProfile")
251
207
 
252
208
  {
253
209
  "name" => resp["user"]["profile"]["displayName"],
@@ -263,32 +219,42 @@ module SimpleXChat
263
219
  if resp_type == "chatCmdError" && resp.dig("chatError", "storeError", "type") == "userContactLinkNotFound"
264
220
  return nil
265
221
  end
266
-
267
- raise "Unexpected response: #{resp_type}" if resp_type != "userContactLink"
222
+ check_response_type(resp, "userContactLink")
268
223
 
269
224
  resp["contactLink"]["connReqContact"]
270
225
  end
271
226
 
272
227
  def api_create_user_address
273
228
  resp = send_command '/address'
274
- resp_type = resp["type"]
275
- raise "Unexpected response: #{resp_type}" if resp_type != "userContactLinkCreated"
229
+ check_response_type(resp, "userContactLinkCreated")
276
230
 
277
231
  resp["connReqContact"]
278
232
  end
279
233
 
280
- def api_send_text_message(chat_type, contact, message)
281
- resp = send_command "#{chat_type}#{contact} #{message}"
282
- resp_type = resp["type"]
283
- raise "Unexpected response: #{resp_type}" if resp_type != "newChatItems"
234
+ def api_send_text_message(chat_type, receiver, message)
235
+ resp = send_command "#{chat_type}#{receiver} #{message}"
236
+ check_response_type(resp, "newChatItems")
237
+
238
+ resp["chatItems"]
239
+ end
240
+
241
+ def api_send_image(chat_type, receiver, file_path)
242
+ resp = send_command "/image #{chat_type}#{receiver} #{file_path}"
243
+ check_response_type(resp, "newChatItems")
244
+
245
+ resp["chatItems"]
246
+ end
247
+
248
+ def api_send_file(chat_type, receiver, file_path)
249
+ resp = send_command "/file #{chat_type}#{receiver} #{file_path}"
250
+ check_response_type(resp, "newChatItems")
284
251
 
285
252
  resp["chatItems"]
286
253
  end
287
254
 
288
255
  def api_contacts
289
256
  resp = send_command "/contacts"
290
- resp_type = resp["type"]
291
- raise "Unexpected response: #{resp_type}" if resp_type != "contactsList"
257
+ check_response_type(resp, "contactsList")
292
258
 
293
259
  contacts = resp["contacts"]
294
260
  contacts.map{ |c| {
@@ -303,8 +269,7 @@ module SimpleXChat
303
269
 
304
270
  def api_groups
305
271
  resp = send_command "/groups"
306
- resp_type = resp["type"]
307
- raise "Unexpected response: #{resp_type}" if resp_type != "groupsList"
272
+ check_response_type(resp, "groupsList")
308
273
 
309
274
  groups = resp["groups"]
310
275
  groups.map{ |entry|
@@ -330,16 +295,14 @@ module SimpleXChat
330
295
  onoff = is_enabled && "on" || "off"
331
296
 
332
297
  resp = send_command "/auto_accept #{onoff}"
333
- resp_type = resp["type"]
334
- raise "Unexpected response: #{resp_type}" if resp_type != "userContactLinkUpdated"
298
+ check_response_type(resp, "userContactLinkUpdated")
335
299
 
336
300
  nil
337
301
  end
338
302
 
339
303
  def api_kick_group_member(group, member)
340
304
  resp = send_command "/remove #{group} #{member}"
341
- resp_type = resp["type"]
342
- raise "Unexpected response: #{resp_type}" unless resp_type == "userDeletedMember"
305
+ check_response_type(resp, "userDeletedMember")
343
306
  end
344
307
 
345
308
  # Parameters for /network:
@@ -362,17 +325,100 @@ module SimpleXChat
362
325
  command += " #{param}=#{value}"
363
326
  end
364
327
  resp = send_command command
365
- resp_type = resp["type"]
366
- raise "Unexpected response: #{resp_type}" if resp_type != "networkConfig"
328
+ check_response_type(resp, "networkConfig")
367
329
 
368
330
  resp["networkConfig"]
369
331
  end
370
332
 
333
+ def api_tail(chat_type: nil, conversation: nil, message_count: nil)
334
+ cmd = "/tail"
335
+ cmd += " #{chat_type}#{conversation}" if chat_type != nil && conversation != nil
336
+ cmd += " #{message_count}" if message_count != nil
337
+ resp = send_command cmd
338
+ check_response_type(resp, "chatItems")
339
+
340
+ resp["chatItems"].map{|chat_item| parse_chat_item chat_item}
341
+ end
342
+
343
+ def api_chats(
344
+ chat_count=20 # if nil, will return all the chats
345
+ )
346
+ param = chat_count != nil ? "#{chat_count}" : "all"
347
+ cmd = "/chats #{param}"
348
+ resp = send_command cmd
349
+ check_response_type(resp, "chats")
350
+
351
+ resp["chats"].map do |chat|
352
+ chat_type = parse_chat_info_type chat["chatInfo"]["type"]
353
+ next if chat_type == nil # WARN: Chat type "local" is currently ignored
354
+ conversation = nil
355
+ if chat_type == ChatType::GROUP
356
+ conversation = chat["chatInfo"]["groupInfo"]["localDisplayName"]
357
+ else
358
+ conversation = chat["chatInfo"]["contact"]["localDisplayName"]
359
+ end
360
+
361
+ {
362
+ :chat_type => chat_type,
363
+ :conversation => conversation
364
+ }
365
+ end.filter { |x| x != nil }
366
+ end
367
+
371
368
  private
372
369
 
370
+ def check_response_type(resp, expected_resp_type)
371
+ resp_type = resp["type"]
372
+ raise UnexpectedResponseError.new(resp_type, expected_resp_type) unless resp_type == expected_resp_type
373
+ end
374
+
373
375
  def next_corr_id
374
376
  # The correlation ID has to be a string
375
377
  (@corr_id.update { |x| x + 1 } - 1).to_s(10)
376
378
  end
379
+
380
+ def parse_chat_info_type(type)
381
+ chat_info_types = {
382
+ "direct" => ChatType::DIRECT,
383
+ "group" => ChatType::GROUP
384
+ }
385
+
386
+ chat_info_types.dig(type)
387
+ end
388
+
389
+ def parse_chat_item(chat_item)
390
+ chat_type = parse_chat_info_type chat_item["chatInfo"]["type"]
391
+ group = nil
392
+ sender = nil
393
+ contact = nil
394
+ contact_role = nil
395
+ if chat_type == ChatType::GROUP
396
+ # NOTE: The group can "send messages" without a contact
397
+ # For example, when a member is removed, the group
398
+ # sends a message about his removal, with no contact
399
+ contact = chat_item.dig "chatItem", "chatDir", "groupMember", "localDisplayName"
400
+ contact_role = chat_item.dig "chatItem", "chatDir", "groupMember", "memberRole"
401
+ group = chat_item["chatInfo"]["groupInfo"]["localDisplayName"]
402
+ sender = group
403
+ else
404
+ contact = chat_item["chatInfo"]["contact"]["localDisplayName"]
405
+ sender = contact
406
+ end
407
+
408
+ msg_text = chat_item["chatItem"]["meta"]["itemText"]
409
+ timestamp = Time.parse(chat_item["chatItem"]["meta"]["updatedAt"])
410
+ image_preview = chat_item.dig "chatItem", "content", "msgContent", "image"
411
+
412
+ chat_message = {
413
+ :chat_type => chat_type,
414
+ :sender => sender,
415
+ :contact_role => contact_role,
416
+ :contact => contact,
417
+ :group => group,
418
+ :msg_text => msg_text,
419
+ :msg_timestamp => timestamp,
420
+ :img_preview => image_preview
421
+ }
422
+ end
377
423
  end
378
424
  end
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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rdbo
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-01 00:00:00.000000000 Z
10
+ date: 2025-03-04 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: websocket
@@ -48,6 +48,9 @@ files:
48
48
  - README.md
49
49
  - Rakefile
50
50
  - lib/simplex-chat.rb
51
+ - lib/simplex-chat/errors.rb
52
+ - lib/simplex-chat/patches.rb
53
+ - lib/simplex-chat/types.rb
51
54
  - lib/simplex-chat/version.rb
52
55
  - showcase.png
53
56
  homepage: https://github.com/rdbo/simplex-chat-ruby