matrix_sdk 2.0.1 → 2.2.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: e1de46ea35a8763e17d834c9395dc95a12c3084557085a0eb6b8e5f1fa25979a
4
- data.tar.gz: 13a55698bf72695e8e690d4d5f6618d7efe595f06c32c65580b10e9bf550724e
3
+ metadata.gz: 4f16ccd1ed10b02a963b9dbd736e577375ca41b26fb45743bc0344f219b1d70f
4
+ data.tar.gz: 3d8eef54f71ccf9e04c0da896e6189fd8d4c2c34c269e2890de37dd4cec023a6
5
5
  SHA512:
6
- metadata.gz: 919a27ee7fd758c382eafe3f8aad89c13da2a6cc6542ccd8bfe52d09186d93453b12406dfe6b03b6aa63d1898dbacfb6dbced4f4e9606722ad04df4f237cf600
7
- data.tar.gz: a8f5c0bc4fb07a089e0d1daebeb8996c6d36157550e73e3869a8bb55f25cc2c405a064654fc49c8bdb323f9d8c168b3c785e52b9f88fbc168ee29c152f7472a5
6
+ metadata.gz: 01de0cccbcf0724a101526190ea4e164ec1235daaa0e0c3d42ed445e760246b2b81a054ba4bd7e88229fdb0833e4fe3a8624a4de3dfd032bfa50b4f23946caf1
7
+ data.tar.gz: 1b177862e3b01aef4c10de6a4318b8348a5c6e00885f0b137ce13a269bf0d5579e6a72b93a87a9366b8d75ec2549518a7d21f2da5db4d7f1d60fa464ff11d83b
@@ -1,6 +1,41 @@
1
+ ## 2.2.0 - 2020-11-20
2
+
3
+ - Adds direct message (1:1) room mapping to client abstraction
4
+ - Adds Api#get_room_event_context (#13)
5
+ - Improves support for JRuby
6
+
7
+ ## 2.1.3 - 2020-09-18
8
+
9
+ - Adds separate state event handler as Client#on_state_event
10
+ - Changes Client sync interval to by-default run at full speed
11
+ - Fixes state events being sent twice if included in both timeline and state of a sync
12
+ - Improves error reporting of broken 200 responses
13
+ - Improves event handlers for rooms, to not depend on a specific room object instance anymore
14
+
15
+ ## 2.1.2 - 2020-09-10
16
+
17
+ - Adds method for reading complete member lists for rooms, improves the CS spec adherence
18
+ - Adds test for state events
19
+ - Fixes state event handler for rooms not actually passing events
20
+ - Fixes Api#new_for_domain using a faulty URI in certain cases
21
+
22
+ ## 2.1.1 - 2020-08-21
23
+
24
+ - Fixes crash if state event content is null (#11)
25
+ - Fixes an uninitialized URI constant exception when requiring only the main library file
26
+ - Fixes the Api#get_pushrules method missing an ending slash in the request URI
27
+ - Fixes discovery code for client/server connections based on domain
28
+
29
+ ## 2.1.0 - 2020-05-22
30
+
31
+ - Adds unique query IDs as well as duration in API debug output, to make it easier to track long requests
32
+ - Finishes up MSC support, get sync over SSE working flawlessly
33
+ - Exposes the #listen_forever method in the client abstraction
34
+ - Fixes room access methods
35
+
1
36
  ## 2.0.1 - 2020-03-13
2
37
 
3
- - Add code for handling non-final MSC's in protocols
38
+ - Adds code for handling non-final MSC's in protocols
4
39
  - Currently implementing clients parts of MSC2018 for Sync over Server Sent Events
5
40
 
6
41
  ## 2.0.0 - 2020-02-14
@@ -92,6 +92,8 @@ module MatrixSdk
92
92
  uri = URI("http#{ssl ? 's' : ''}://#{domain}")
93
93
  well_known = nil
94
94
  target_uri = nil
95
+ logger = ::Logging.logger[self]
96
+ logger.debug "Resolving #{domain}"
95
97
 
96
98
  if !port.nil? && !port.empty?
97
99
  # If the domain is fully qualified according to Matrix (FQDN and port) then skip discovery
@@ -99,21 +101,30 @@ module MatrixSdk
99
101
  elsif target == :server
100
102
  # Attempt SRV record discovery
101
103
  target_uri = begin
102
- require 'resolv'
103
- resolver = Resolv::DNS.new
104
- resolver.getresource("_matrix._tcp.#{domain}")
105
- rescue StandardError
106
- nil
107
- end
104
+ require 'resolv'
105
+ resolver = Resolv::DNS.new
106
+ srv = "_matrix._tcp.#{domain}"
107
+ logger.debug "Trying DNS #{srv}..."
108
+ d = resolver.getresource(srv, Resolv::DNS::Resource::IN::SRV)
109
+ d
110
+ rescue StandardError => e
111
+ logger.debug "DNS lookup failed with #{e.class}: #{e.message}"
112
+ nil
113
+ end
108
114
 
109
115
  if target_uri.nil?
110
116
  # Attempt .well-known discovery for server-to-server
111
117
  well_known = begin
112
- data = Net::HTTP.get("https://#{domain}/.well-known/matrix/server")
113
- JSON.parse(data)
114
- rescue StandardError
115
- nil
116
- end
118
+ wk_uri = URI("https://#{domain}/.well-known/matrix/server")
119
+ logger.debug "Trying #{wk_uri}..."
120
+ data = Net::HTTP.start(wk_uri.host, wk_uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
121
+ http.get(wk_uri.path).body
122
+ end
123
+ JSON.parse(data)
124
+ rescue StandardError => e
125
+ logger.debug "Well-known failed with #{e.class}: #{e.message}"
126
+ nil
127
+ end
117
128
 
118
129
  target_uri = well_known['m.server'] if well_known&.key?('m.server')
119
130
  else
@@ -122,11 +133,16 @@ module MatrixSdk
122
133
  elsif %i[client identity].include? target
123
134
  # Attempt .well-known discovery
124
135
  well_known = begin
125
- data = Net::HTTP.get("https://#{domain}/.well-known/matrix/client")
126
- JSON.parse(data)
127
- rescue StandardError
128
- nil
129
- end
136
+ wk_uri = URI("https://#{domain}/.well-known/matrix/client")
137
+ logger.debug "Trying #{wk_uri}..."
138
+ data = Net::HTTP.start(wk_uri.host, wk_uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
139
+ http.get(wk_uri.path).body
140
+ end
141
+ JSON.parse(data)
142
+ rescue StandardError => e
143
+ logger.debug "Well-known failed with #{e.class}: #{e.message}"
144
+ nil
145
+ end
130
146
 
131
147
  if well_known
132
148
  key = 'm.homeserver'
@@ -138,6 +154,7 @@ module MatrixSdk
138
154
  end
139
155
  end
140
156
  end
157
+ logger.debug "Using #{target_uri.inspect}"
141
158
 
142
159
  # Fall back to direct domain connection
143
160
  target_uri ||= URI("https://#{domain}:8448")
@@ -161,6 +178,7 @@ module MatrixSdk
161
178
  def protocols
162
179
  self
163
180
  .class.included_modules
181
+ .reject { |m| m&.name.nil? }
164
182
  .select { |m| m.name.start_with? 'MatrixSdk::Protocols::' }
165
183
  .map { |m| m.name.split('::').last.to_sym }
166
184
  end
@@ -250,37 +268,32 @@ module MatrixSdk
250
268
  u.query = [u.query, URI.encode_www_form(options.fetch(:query))].flatten.compact.join('&') if options[:query]
251
269
  u.query = nil if u.query.nil? || u.query.empty?
252
270
  end
253
- request = Net::HTTP.const_get(method.to_s.capitalize.to_sym).new url.request_uri
254
- request.body = options[:body] if options.key? :body
255
- request.body = request.body.to_json if options.key?(:body) && !request.body.is_a?(String)
256
- request.body_stream = options[:body_stream] if options.key? :body_stream
257
-
258
- global_headers.each { |h, v| request[h] = v }
259
- if request.body || request.body_stream
260
- request.content_type = 'application/json'
261
- request.content_length = (request.body || request.body_stream).size
262
- end
263
-
264
- request['authorization'] = "Bearer #{access_token}" if access_token && !options.fetch(:skip_auth, false)
265
- if options.key? :headers
266
- options[:headers].each do |h, v|
267
- request[h.to_s.downcase] = v
268
- end
269
- end
270
271
 
271
272
  failures = 0
272
273
  loop do
273
274
  raise MatrixConnectionError, "Server still too busy to handle request after #{failures} attempts, try again later" if failures >= 10
274
275
 
275
- print_http(request)
276
+ req_id = ('A'..'Z').to_a.sample(4).join
277
+
278
+ req_obj = construct_request(url: url, method: method, **options)
279
+ print_http(req_obj, id: req_id)
276
280
  begin
277
- response = http.request request
278
- rescue EOFError => e
281
+ dur_start = Time.now
282
+ response = http.request req_obj
283
+ dur_end = Time.now
284
+ duration = dur_end - dur_start
285
+ rescue EOFError
279
286
  logger.error 'Socket closed unexpectedly'
280
- raise e
287
+ raise
288
+ end
289
+ print_http(response, duration: duration, id: req_id)
290
+
291
+ begin
292
+ data = JSON.parse(response.body, symbolize_names: true)
293
+ rescue JSON::JSONError => e
294
+ logger.debug "#{e.class} error when parsing response. #{e}"
295
+ data = nil
281
296
  end
282
- print_http(response)
283
- data = JSON.parse(response.body, symbolize_names: true) rescue nil
284
297
 
285
298
  if response.is_a? Net::HTTPTooManyRequests
286
299
  raise MatrixRequestError.new_by_code(data, response.code) unless autoretry
@@ -291,24 +304,63 @@ module MatrixSdk
291
304
  next
292
305
  end
293
306
 
294
- return MatrixSdk::Response.new self, data if response.is_a? Net::HTTPSuccess
307
+ if response.is_a? Net::HTTPSuccess
308
+ unless data
309
+ logger.error "Received non-parsable data in 200 response; #{response.body.inspect}"
310
+ raise MatrixConnectionError, response
311
+ end
312
+ return MatrixSdk::Response.new self, data
313
+ end
295
314
  raise MatrixRequestError.new_by_code(data, response.code) if data
296
315
 
297
316
  raise MatrixConnectionError.class_by_code(response.code), response
298
317
  end
299
318
  end
300
319
 
320
+ # Generate a transaction ID
321
+ #
322
+ # @return [String] An arbitrary transaction ID
323
+ def transaction_id
324
+ ret = @transaction_id ||= 0
325
+ @transaction_id = @transaction_id.succ
326
+ ret
327
+ end
328
+
301
329
  private
302
330
 
303
- def print_http(http, body: true)
331
+ def construct_request(method:, url:, **options)
332
+ request = Net::HTTP.const_get(method.to_s.capitalize.to_sym).new url.request_uri
333
+
334
+ # FIXME: Handle bodies better, avoid duplicating work
335
+ request.body = options[:body] if options.key? :body
336
+ request.body = request.body.to_json if options.key?(:body) && !request.body.is_a?(String)
337
+ request.body_stream = options[:body_stream] if options.key? :body_stream
338
+
339
+ global_headers.each { |h, v| request[h] = v }
340
+ if request.body || request.body_stream
341
+ request.content_type = 'application/json'
342
+ request.content_length = (request.body || request.body_stream).size
343
+ end
344
+
345
+ request['authorization'] = "Bearer #{access_token}" if access_token && !options.fetch(:skip_auth, false)
346
+ if options.key? :headers
347
+ options[:headers].each do |h, v|
348
+ request[h.to_s.downcase] = v
349
+ end
350
+ end
351
+
352
+ request
353
+ end
354
+
355
+ def print_http(http, body: true, duration: nil, id: nil)
304
356
  return unless logger.debug?
305
357
 
306
358
  if http.is_a? Net::HTTPRequest
307
- dir = '>'
359
+ dir = "#{id ? "#{id} : " : nil}>"
308
360
  logger.debug "#{dir} Sending a #{http.method} request to `#{http.path}`:"
309
361
  else
310
- dir = '<'
311
- logger.debug "#{dir} Received a #{http.code} #{http.message} response:"
362
+ dir = "#{id ? "#{id} : " : nil}<"
363
+ logger.debug "#{dir} Received a #{http.code} #{http.message} response:#{duration ? " [#{(duration * 1000).to_i}ms]" : nil}"
312
364
  end
313
365
  http.to_hash.map { |k, v| "#{k}: #{k == 'authorization' ? '[ REDACTED ]' : v.join(', ')}" }.each do |h|
314
366
  logger.debug "#{dir} #{h}"
@@ -316,7 +368,7 @@ module MatrixSdk
316
368
  logger.debug dir
317
369
  if body
318
370
  clean_body = JSON.parse(http.body) rescue nil if http.body
319
- clean_body.keys.each { |k| clean_body[k] = '[ REDACTED ]' if %w[password access_token].include?(k) }.to_json if clean_body.is_a? Hash
371
+ clean_body.each_key { |k| clean_body[k] = '[ REDACTED ]' if %w[password access_token].include?(k) }.to_json if clean_body.is_a? Hash
320
372
  clean_body = clean_body.to_s if clean_body
321
373
  logger.debug "#{dir} #{clean_body.length < 200 ? clean_body : clean_body.slice(0..200) + "... [truncated, #{clean_body.length} Bytes]"}" if clean_body
322
374
  end
@@ -324,12 +376,6 @@ module MatrixSdk
324
376
  logger.warn "#{e.class} occured while printing request debug; #{e.message}\n#{e.backtrace.join "\n"}"
325
377
  end
326
378
 
327
- def transaction_id
328
- ret = @transaction_id ||= 0
329
- @transaction_id = @transaction_id.succ
330
- ret
331
- end
332
-
333
379
  def api_to_path(api)
334
380
  # TODO: <api>_current / <api>_latest
335
381
  "/_matrix/#{api.to_s.split('_').join('/')}"
@@ -23,7 +23,7 @@ module MatrixSdk
23
23
  attr_reader :api, :next_batch
24
24
  attr_accessor :cache, :sync_filter
25
25
 
26
- events :error, :event, :presence_event, :invite_event, :leave_event, :ephemeral_event
26
+ events :error, :event, :presence_event, :invite_event, :leave_event, :ephemeral_event, :state_event
27
27
  ignore_inspect :api,
28
28
  :on_event, :on_presence_event, :on_invite_event, :on_leave_event, :on_ephemeral_event
29
29
 
@@ -44,7 +44,7 @@ module MatrixSdk
44
44
  # @see #initialize
45
45
  def self.new_for_domain(domain, **params)
46
46
  api = MatrixSdk::Api.new_for_domain(domain, keep_wellknown: true)
47
- return new(api, params) unless api.well_known.key? 'm.identity_server'
47
+ return new(api, params) unless api.well_known&.key?('m.identity_server')
48
48
 
49
49
  identity_server = MatrixSdk::Api.new(api.well_known['m.identity_server']['base_url'], protocols: %i[IS])
50
50
  new(api, params.merge(identity_server: identity_server))
@@ -70,8 +70,6 @@ module MatrixSdk
70
70
  @api = Api.new hs_url, params
71
71
  end
72
72
 
73
- @rooms = {}
74
- @users = {}
75
73
  @cache = client_cache
76
74
  @identity_server = nil
77
75
 
@@ -79,7 +77,6 @@ module MatrixSdk
79
77
  @sync_thread = nil
80
78
  @sync_filter = { room: { timeline: { limit: params.fetch(:sync_filter_limit, 20) }, state: { lazy_load_members: true } } }
81
79
 
82
- @should_listen = false
83
80
  @next_batch = nil
84
81
 
85
82
  @bad_sync_timeout_limit = 60 * 60
@@ -88,6 +85,11 @@ module MatrixSdk
88
85
  instance_variable_set("@#{k}", v) if instance_variable_defined? "@#{k}"
89
86
  end
90
87
 
88
+ @rooms = {}
89
+ @room_handlers = {}
90
+ @users = {}
91
+ @should_listen = false
92
+
91
93
  raise ArgumentError, 'Cache value must be one of of [:all, :some, :none]' unless %i[all some none].include? @cache
92
94
 
93
95
  return unless params[:user_id]
@@ -154,6 +156,27 @@ module MatrixSdk
154
156
  rooms
155
157
  end
156
158
 
159
+ # Gets a list of all direct chat rooms (1:1 chats / direct message chats) for the currenct user
160
+ #
161
+ # @return [Hash[String,Array[String]]] A mapping of MXIDs to a list of direct rooms with that user
162
+ def direct_rooms
163
+ Hash[api.get_account_data(mxid, 'm.direct').map do |mxid, rooms|
164
+ [mxid.to_s, rooms]
165
+ end]
166
+ end
167
+
168
+ # Gets a direct message room for the given user if one exists
169
+ #
170
+ # @note Will return the oldest room if multiple exist
171
+ # @return [Room,nil] A direct message room if one exists
172
+ def direct_room(mxid)
173
+ mxid = MatrixSdk::MXID.new mxid.to_s unless mxid.is_a? MatrixSdk::MXID
174
+ raise ArgumentError, 'Must be a valid user ID' unless mxid.user?
175
+
176
+ room_id = direct_rooms[mxid.to_s]&.first
177
+ ensure_room room_id if room_id
178
+ end
179
+
157
180
  # Gets a list of all relevant rooms, either the ones currently handled by
158
181
  # the client, or the list of currently joined ones if no rooms are handled
159
182
  #
@@ -343,7 +366,7 @@ module MatrixSdk
343
366
 
344
367
  # Joins an already created room
345
368
  #
346
- # @param room_id_or_alias [String,MXID] A room alias (#room:exmaple.com) or a room ID (!id:example.com)
369
+ # @param room_id_or_alias [String,MXID] A room alias (#room:example.com) or a room ID (!id:example.com)
347
370
  # @param server_name [Array[String]] A list of servers to attempt the join through, required for IDs
348
371
  # @return [Room] The resulting room
349
372
  # @see Protocols::CS#join_room
@@ -424,12 +447,27 @@ module MatrixSdk
424
447
 
425
448
  @should_listen = true
426
449
  if api.protocol?(:MSC) && api.msc2108?
450
+ params[:filter] = sync_filter unless params.key? :filter
451
+ params[:filter] = params[:filter].to_json unless params[:filter].nil? || params[:filter].is_a?(String)
427
452
  params[:since] = @next_batch if @next_batch
428
- thread = api.msc2108_sync_sse(params) do |data, event:, id:|
429
- logger.debug "Handling SSE event '#{event}' from '#{id}'"
453
+
454
+ errors = 0
455
+ thread, cancel_token = api.msc2108_sync_sse(params) do |data, event:, id:|
430
456
  @next_batch = id if id
431
- handle_sync_response(data) if event.to_sym == :sync
457
+ case event.to_sym
458
+ when :sync
459
+ handle_sync_response(data)
460
+ errors = 0
461
+ when :sync_error
462
+ logger.error "SSE Sync error received; #{data.type}: #{data.message}"
463
+ errors += 1
464
+
465
+ # TODO: Allow configuring
466
+ raise 'Aborting due to excessive errors' if errors >= 5
467
+ end
432
468
  end
469
+
470
+ @should_listen = cancel_token
433
471
  else
434
472
  thread = Thread.new { listen_forever(params) }
435
473
  end
@@ -441,8 +479,15 @@ module MatrixSdk
441
479
  def stop_listener_thread
442
480
  return unless @sync_thread
443
481
 
444
- @should_listen = false
445
- @sync_thread.join if @sync_thread.alive?
482
+ if @should_listen.is_a? Hash
483
+ @should_listen[:run] = false
484
+ else
485
+ @should_listen = false
486
+ end
487
+ if @sync_thread.alive?
488
+ ret = @sync_thread.join(2)
489
+ @sync_thread.kill unless ret
490
+ end
446
491
  @sync_thread = nil
447
492
  end
448
493
 
@@ -503,9 +548,7 @@ module MatrixSdk
503
548
  end
504
549
  end
505
550
 
506
- private
507
-
508
- def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 30, **params)
551
+ def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 0, **params)
509
552
  orig_bad_sync_timeout = bad_sync_timeout + 0
510
553
  while @should_listen
511
554
  begin
@@ -528,6 +571,8 @@ module MatrixSdk
528
571
  fire_error(ErrorEvent.new(e, :listener_thread))
529
572
  end
530
573
 
574
+ private
575
+
531
576
  def post_authentication(data)
532
577
  @mxid = data[:user_id]
533
578
  @api.access_token = data[:access_token]
@@ -539,7 +584,10 @@ module MatrixSdk
539
584
  def handle_state(room_id, state_event)
540
585
  return unless state_event.key? :type
541
586
 
587
+ on_state_event.fire(MatrixEvent.new(self, state_event), state_event[:type])
588
+
542
589
  room = ensure_room(room_id)
590
+ room.send :put_state_event, state_event
543
591
  content = state_event[:content]
544
592
  case state_event[:type]
545
593
  when 'm.room.name'
@@ -553,9 +601,9 @@ module MatrixSdk
553
601
  when 'm.room.aliases'
554
602
  room.instance_variable_get('@aliases').concat content[:aliases]
555
603
  when 'm.room.join_rules'
556
- room.instance_variable_set '@join_rule', content[:join_rule].to_sym
604
+ room.instance_variable_set '@join_rule', content[:join_rule].nil? ? nil : content[:join_rule].to_sym
557
605
  when 'm.room.guest_access'
558
- room.instance_variable_set '@guest_access', content[:guest_access].to_sym
606
+ room.instance_variable_set '@guest_access', content[:guest_access].nil? ? nil : content[:guest_access].to_sym
559
607
  when 'm.room.member'
560
608
  return unless cache == :all
561
609
 
@@ -596,7 +644,12 @@ module MatrixSdk
596
644
 
597
645
  join[:timeline][:events].each do |event|
598
646
  event[:room_id] = room_id.to_s
599
- handle_state(room_id, event) unless event[:type] == 'm.room.message'
647
+ # Avoid sending two identical state events if it's both in state and timeline
648
+ if event.key?(:state_key)
649
+ state_event = join.dig(:state, :events).find { |ev| ev[:event_id] == event[:event_id] }
650
+
651
+ handle_state(room_id, event) unless event == state_event
652
+ end
600
653
  room.send :put_event, event
601
654
 
602
655
  fire_event(MatrixEvent.new(self, event), event[:type])
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+
3
5
  module URI
4
6
  # A mxc:// Matrix content URL
5
7
  class MATRIX < Generic
@@ -189,7 +191,9 @@ module MatrixSdk
189
191
  end
190
192
 
191
193
  def respond_to_missing?(method, *)
192
- event.key? method
194
+ return true if event.key? method
195
+
196
+ super
193
197
  end
194
198
  end
195
199
  end
@@ -32,13 +32,13 @@ module MatrixSdk
32
32
  #
33
33
  # @return [String]
34
34
  def homeserver
35
- port_s = port ? ':' + port.to_s : ''
35
+ port_s = port ? ":#{port}" : ''
36
36
  domain ? domain + port_s : ''
37
37
  end
38
38
 
39
39
  # Gets the homserver part of the ID as a suffix (':homeserver')
40
40
  def homeserver_suffix
41
- ':' + homeserver if domain
41
+ ":#{homeserver}" if domain
42
42
  end
43
43
 
44
44
  def to_s
@@ -49,18 +49,13 @@ module MatrixSdk
49
49
  #
50
50
  # @return [Symbol] The MXID type, one of (:user_id, :room_id, :event_id, :group_id, or :room_alias)
51
51
  def type
52
- case sigil
53
- when '@'
54
- :user_id
55
- when '!'
56
- :room_id
57
- when '$'
58
- :event_id
59
- when '+'
60
- :group_id
61
- when '#'
62
- :room_alias
63
- end
52
+ {
53
+ '@' => :user_id,
54
+ '!' => :room_id,
55
+ '$' => :event_id,
56
+ '+' => :group_id,
57
+ '#' => :room_alias
58
+ }[sigil]
64
59
  end
65
60
 
66
61
  # Checks if the ID is valid
@@ -767,6 +767,30 @@ module MatrixSdk::Protocols::CS
767
767
  request(:get, :client_r0, "/rooms/#{room_id}/state", query: query)
768
768
  end
769
769
 
770
+ # Retrieves number of events that happened just before and after the specified event
771
+ #
772
+ # @param room_id [MXID,String] The room to get events from.
773
+ # @param event_id [MXID,String] The event to get context around.
774
+ # @option params [Integer] :limit (10) The limit of messages to retrieve
775
+ # @option params [String] :filter A filter to limit the retrieval to
776
+ # @return [Response] A response hash with contextual event information
777
+ # @see https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-context-eventid
778
+ # The Matrix Spec, for more information about the call and response
779
+ # @example Find event context with filter and limit specified
780
+ # api.get_room_event_context('#room:example.com', '$event_id:example.com', filter: { types: ['m.room.message'] }.to_json, limit: 20)
781
+ def get_room_event_context(room_id, event_id, **params)
782
+ query = {}
783
+ query[:user_id] = params.delete(:user_id) if protocol?(:AS) && params.key?(:user_id)
784
+
785
+ query[:limit] = params.fetch(:limit) if params.key? :limit
786
+ query[:filter] = params.fetch(:filter) if params.key? :filter
787
+
788
+ room_id = ERB::Util.url_encode room_id.to_s
789
+ event_id = ERB::Util.url_encode event_id.to_s
790
+
791
+ request(:get, :client_r0, "/rooms/#{room_id}/context/#{event_id}", query: query)
792
+ end
793
+
770
794
  ## Specialized getters for specced state
771
795
  #
772
796
 
@@ -1085,7 +1109,7 @@ module MatrixSdk::Protocols::CS
1085
1109
  # @return [Response] The resulting state event
1086
1110
  # @see https://matrix.org/docs/spec/client_server/latest.html#m-room-guest-server-acl
1087
1111
  # The Matrix Spec, for more information about the event and data
1088
- def set_room_server_acl(room_id, allow_ip_literals: false, allow:, deny:, **params)
1112
+ def set_room_server_acl(room_id, allow:, deny:, allow_ip_literals: false, **params)
1089
1113
  content = {
1090
1114
  allow_ip_literals: allow_ip_literals,
1091
1115
  allow: allow,
@@ -1637,7 +1661,7 @@ module MatrixSdk::Protocols::CS
1637
1661
 
1638
1662
  room_id = ERB::Util.url_encode room_id.to_s
1639
1663
 
1640
- request(:get, :client_r0, "/rooms/#{room_id}/members", query: query)
1664
+ request(:get, :client_r0, "/rooms/#{room_id}/members", query: query.merge(params))
1641
1665
  end
1642
1666
 
1643
1667
  # Gets a list of the joined members in a room
@@ -1712,7 +1736,7 @@ module MatrixSdk::Protocols::CS
1712
1736
  # # => { :device_keys => { :'@alice:example.com' => { :ABCDEFGHIJ => { ...
1713
1737
  # @see https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-query
1714
1738
  # The Matrix Spec, for more information about the parameters and data
1715
- def keys_query(timeout: nil, device_keys:, token: nil, **params)
1739
+ def keys_query(device_keys:, timeout: nil, token: nil, **params)
1716
1740
  body = {
1717
1741
  timeout: (timeout || 10) * 1000,
1718
1742
  device_keys: device_keys
@@ -1823,7 +1847,7 @@ module MatrixSdk::Protocols::CS
1823
1847
  # @see https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules
1824
1848
  # The Matrix Spec, for more information about the parameters and data
1825
1849
  def get_pushrules
1826
- request(:get, :client_r0, '/pushrules')
1850
+ request(:get, :client_r0, '/pushrules/')
1827
1851
  end
1828
1852
 
1829
1853
  # Retrieves a single registered push rule for the current user
@@ -1834,7 +1858,7 @@ module MatrixSdk::Protocols::CS
1834
1858
  # @return [Response] A response hash containing the full data of the requested push rule
1835
1859
  # @see https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules-scope-kind-ruleid
1836
1860
  # The Matrix Spec, for more information about the parameters and data
1837
- def get_pushrule(scope: 'global', kind:, id:)
1861
+ def get_pushrule(kind:, id:, scope: 'global')
1838
1862
  scope = ERB::Util.url_encode scope.to_s
1839
1863
  kind = ERB::Util.url_encode kind.to_s
1840
1864
  id = ERB::Util.url_encode id.to_s
@@ -1850,7 +1874,7 @@ module MatrixSdk::Protocols::CS
1850
1874
  # @return [Response] A response hash containing an :enabled key for if the rule is enabled or not
1851
1875
  # @see https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules-scope-kind-ruleid-enabled
1852
1876
  # The Matrix Spec, for more information about the parameters and data
1853
- def get_pushrule_enabled(scope: 'global', kind:, id:)
1877
+ def get_pushrule_enabled(kind:, id:, scope: 'global')
1854
1878
  scope = ERB::Util.url_encode scope.to_s
1855
1879
  kind = ERB::Util.url_encode kind.to_s
1856
1880
  id = ERB::Util.url_encode id.to_s
@@ -1867,7 +1891,7 @@ module MatrixSdk::Protocols::CS
1867
1891
  # @return [Response] An empty response hash if the push rule was enabled/disabled successfully
1868
1892
  # @see https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-pushrules-scope-kind-ruleid-enabled
1869
1893
  # The Matrix Spec, for more information about the parameters and data
1870
- def set_pushrule_enabled(enabled, scope: 'global', kind:, id:)
1894
+ def set_pushrule_enabled(enabled, kind:, id:, scope: 'global')
1871
1895
  scope = ERB::Util.url_encode scope.to_s
1872
1896
  kind = ERB::Util.url_encode kind.to_s
1873
1897
  id = ERB::Util.url_encode id.to_s
@@ -1887,7 +1911,7 @@ module MatrixSdk::Protocols::CS
1887
1911
  # @return [Response] A response hash containing an :enabled key for if the rule is enabled or not
1888
1912
  # @see https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules-scope-kind-ruleid-actions
1889
1913
  # The Matrix Spec, for more information about the parameters and data
1890
- def get_pushrule_actions(scope: 'global', kind:, id:)
1914
+ def get_pushrule_actions(kind:, id:, scope: 'global')
1891
1915
  scope = ERB::Util.url_encode scope.to_s
1892
1916
  kind = ERB::Util.url_encode kind.to_s
1893
1917
  id = ERB::Util.url_encode id.to_s
@@ -1904,7 +1928,7 @@ module MatrixSdk::Protocols::CS
1904
1928
  # @return [Response] An empty response hash if the push rule actions were modified successfully
1905
1929
  # @see https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-pushrules-scope-kind-ruleid-actions
1906
1930
  # The Matrix Spec, for more information about the parameters and data
1907
- def set_pushrule_actions(actions, scope: 'global', kind:, id:)
1931
+ def set_pushrule_actions(actions, kind:, id:, scope: 'global')
1908
1932
  scope = ERB::Util.url_encode scope.to_s
1909
1933
  kind = ERB::Util.url_encode kind.to_s
1910
1934
  id = ERB::Util.url_encode id.to_s
@@ -2,16 +2,13 @@
2
2
 
3
3
  # Preliminary support for unmerged MSCs (Matrix Spec Changes)
4
4
  module MatrixSdk::Protocols::MSC
5
- def self.included(_)
6
- @msc = {}
7
- end
8
-
9
5
  def refresh_mscs
10
6
  @msc = {}
11
7
  end
12
8
 
13
9
  # Check if there's support for MSC2108 - Sync over Server Sent Events
14
10
  def msc2108?
11
+ @msc ||= {}
15
12
  @msc[2108] ||= \
16
13
  begin
17
14
  request(:get, :client_r0, '/sync/sse', skip_auth: true, headers: { accept: 'text/event-stream' })
@@ -20,13 +17,13 @@ module MatrixSdk::Protocols::MSC
20
17
  rescue MatrixSdk::MatrixRequestError
21
18
  false
22
19
  end
20
+ rescue StandardError => e
21
+ logger.debug "Failed to check MSC2108 status;\n#{e.inspect}"
22
+ false
23
23
  end
24
24
 
25
25
  # Sync over Server Sent Events - MSC2108
26
26
  #
27
- # @note With the default Ruby Net::HTTP server, body fragments are cached up to 16kB,
28
- # which will result in large batches and delays if your filters trim a lot of data.
29
- #
30
27
  # @example Syncing over SSE
31
28
  # @since = 'some token'
32
29
  # api.msc2108_sync_sse(since: @since) do |data, event:, id:|
@@ -38,10 +35,11 @@ module MatrixSdk::Protocols::MSC
38
35
  #
39
36
  # @see Protocols::CS#sync
40
37
  # @see https://github.com/matrix-org/matrix-doc/pull/2108/
38
+ # rubocop:disable Metrics/MethodLength
41
39
  def msc2108_sync_sse(since: nil, **params, &on_data)
42
40
  raise ArgumentError, 'Must be given a block accepting two args - data and { event:, id: }' \
43
41
  unless on_data.is_a?(Proc) && on_data.arity == 2
44
- raise MatrixNotAuthorizedError unless access_token
42
+ raise 'Needs to be logged in' unless access_token # TODO: Better error
45
43
 
46
44
  query = params.select do |k, _v|
47
45
  %i[filter full_state set_presence].include? k
@@ -49,24 +47,56 @@ module MatrixSdk::Protocols::MSC
49
47
  query[:user_id] = params.delete(:user_id) if protocol?(:AS) && params.key?(:user_id)
50
48
 
51
49
  req = Net::HTTP::Get.new(homeserver.dup.tap do |u|
52
- u.path = api_to_path(:client_r0) + '/sync/sse'
50
+ u.path = "#{api_to_path :client_r0}/sync/sse"
53
51
  u.query = URI.encode_www_form(query)
54
52
  end)
55
53
  req['accept'] = 'text/event-stream'
54
+ req['accept-encoding'] = 'identity' # Disable compression on the SSE stream
56
55
  req['authorization'] = "Bearer #{access_token}"
57
56
  req['last-event-id'] = since if since
58
57
 
58
+ cancellation_token = { run: true }
59
+
59
60
  # rubocop:disable Metrics/BlockLength
60
- thread = Thread.new do
61
+ thread = Thread.new(cancellation_token) do |ctx|
61
62
  print_http(req)
62
63
  http.request req do |response|
64
+ break unless ctx[:run]
65
+
63
66
  print_http(response, body: false)
64
67
  raise MatrixRequestError.new_by_code(JSON.parse(response.body, symbolize_names: true), response.code) unless response.is_a? Net::HTTPSuccess
65
68
 
69
+ # Override buffer size for BufferedIO
70
+ socket = response.instance_variable_get :@socket
71
+ if socket.is_a? Net::BufferedIO
72
+ socket.instance_eval do
73
+ def rbuf_fill
74
+ bufsize_override = 1024
75
+ loop do
76
+ case rv = @io.read_nonblock(bufsize_override, exception: false)
77
+ when String
78
+ @rbuf << rv
79
+ rv.clear
80
+ return
81
+ when :wait_readable
82
+ @io.to_io.wait_readable(@read_timeout) || raise(Net::ReadTimeout)
83
+ when :wait_writable
84
+ @io.to_io.wait_writable(@read_timeout) || raise(Net::ReadTimeout)
85
+ when nil
86
+ raise EOFError, 'end of file reached'
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ stream_id = ('A'..'Z').to_a.sample(4).join
94
+
95
+ logger.debug "MSC2108 : #{stream_id} : Starting SSE stream."
96
+
66
97
  buffer = ''
67
98
  response.read_body do |chunk|
68
99
  buffer += chunk
69
- logger.debug "< MSC2108: Received #{chunk.length}B of data."
70
100
 
71
101
  while (index = buffer.index(/\r\n\r\n|\n\n/))
72
102
  stream = buffer.slice!(0..index)
@@ -86,20 +116,32 @@ module MatrixSdk::Protocols::MSC
86
116
  /^id:(.+)$/.match(part) do |m_id|
87
117
  id = m_id[1].strip
88
118
  end
119
+ /^:(.+)$/.match(part) do |m_comment|
120
+ logger.debug "MSC2108 : #{stream_id} : Received comment '#{m_comment[1].strip}'"
121
+ end
89
122
  end
90
123
 
91
- data = JSON.parse(data, symbolize_names: true)
124
+ if %w[sync sync_error].include? event
125
+ data = JSON.parse(data, symbolize_names: true)
126
+ yield((MatrixSdk::Response.new self, data), event: event, id: id)
127
+ elsif event
128
+ logger.info "MSC2108 : #{stream_id} : Received unknown event '#{event}'; #{data}"
129
+ end
130
+ end
92
131
 
93
- yield((MatrixSdk::Response.new self, data), event: event, id: id)
132
+ unless ctx[:run]
133
+ socket.close
134
+ break
94
135
  end
95
136
  end
137
+ break unless ctx[:run]
96
138
  end
97
139
  end
98
140
  # rubocop:enable Metrics/BlockLength
99
141
 
100
- thread.abort_on_exception = true
101
142
  thread.run
102
143
 
103
- thread
144
+ [thread, cancellation_token]
104
145
  end
146
+ # rubocop:enable Metrics/MethodLength
105
147
  end
@@ -46,7 +46,9 @@ module MatrixSdk
46
46
  attr_reader :api
47
47
 
48
48
  def respond_to_missing?(name, *_args)
49
- key? name
49
+ return true if key? name
50
+
51
+ super
50
52
  end
51
53
 
52
54
  def method_missing(name, *args)
@@ -42,19 +42,11 @@ module MatrixSdk
42
42
  # The timeline events are what will end up in here
43
43
  attr_reader :id, :client, :topic, :aliases, :members, :events
44
44
 
45
- # @!attribute [r] on_event
46
- # @return [EventHandlerArray] The list of event handlers for all events
47
- # @!attribute [r] on_state_event
48
- # @return [EventHandlerArray] The list of event handlers for only state events
49
- # @!attribute [r] on_ephemeral_event
50
- # @return [EventHandlerArray] The list of event handlers for only ephemeral events
51
- events :event, :state_event, :ephemeral_event
52
45
  # @!method inspect
53
46
  # An inspect method that skips a handful of instance variables to avoid
54
47
  # flooding the terminal with debug data.
55
48
  # @return [String] a regular inspect string without the data for some variables
56
- ignore_inspect :client, :members, :events, :prev_batch, :logger,
57
- :on_event, :on_state_event, :on_ephemeral_event
49
+ ignore_inspect :client, :members, :events, :prev_batch, :logger
58
50
 
59
51
  alias room_id id
60
52
 
@@ -85,8 +77,6 @@ module MatrixSdk
85
77
  room_id = MXID.new room_id unless room_id.is_a?(MXID)
86
78
  raise ArgumentError, 'room_id must be a valid Room ID' unless room_id.room_id?
87
79
 
88
- event_initialize
89
-
90
80
  @name = nil
91
81
  @topic = nil
92
82
  @canonical_alias = nil
@@ -114,6 +104,28 @@ module MatrixSdk
114
104
  logger.debug "Created room #{room_id}"
115
105
  end
116
106
 
107
+ #
108
+ # Event handlers
109
+ #
110
+
111
+ # @!attribute [r] on_event
112
+ # @return [EventHandlerArray] The list of event handlers for all events
113
+ def on_event
114
+ ensure_room_handlers[:event]
115
+ end
116
+
117
+ # @!attribute [r] on_state_event
118
+ # @return [EventHandlerArray] The list of event handlers for only state events
119
+ def on_state_event
120
+ ensure_room_handlers[:state_event]
121
+ end
122
+
123
+ # @!attribute [r] on_ephemeral_event
124
+ # @return [EventHandlerArray] The list of event handlers for only ephemeral events
125
+ def on_ephemeral_event
126
+ ensure_room_handlers[:ephemeral_event]
127
+ end
128
+
117
129
  #
118
130
  # State readers
119
131
  #
@@ -157,6 +169,17 @@ module MatrixSdk
157
169
  members
158
170
  end
159
171
 
172
+ # Get all members (member events) in the room
173
+ #
174
+ # @note This will also count members who've knocked, been invited, have left, or have been banned.
175
+ #
176
+ # @param params [Hash] Additional query parameters to pass to the room member listing - e.g. for filtering purposes.
177
+ #
178
+ # @return [Array(User)] The complete list of members in the room, regardless of membership state
179
+ def all_members(**params)
180
+ client.api.get_room_members(id, **params)[:chunk].map { |ch| client.get_user(ch[:state_key]) }
181
+ end
182
+
160
183
  # Gets the current name of the room, querying the API if necessary
161
184
  #
162
185
  # @note Will cache the current name for 15 minutes
@@ -340,7 +363,7 @@ module MatrixSdk
340
363
  # @param reverse [Boolean] whether to fill messages in reverse or not
341
364
  # @param limit [Integer] the maximum number of messages to backfill
342
365
  # @note This will trigger the `on_event` events as messages are added
343
- def backfill_messages(reverse = false, limit = 10)
366
+ def backfill_messages(reverse = false, limit = 10) # rubocop:disable Style/OptionalBooleanParameter
344
367
  data = client.api.get_room_messages(id, @prev_batch, direction: :b, limit: limit)
345
368
 
346
369
  events = data[:chunk]
@@ -512,10 +535,10 @@ module MatrixSdk
512
535
  # @return [Boolean] if the name was changed or not
513
536
  def reload_name!
514
537
  data = begin
515
- client.api.get_room_name(id)
516
- rescue MatrixNotFoundError
517
- nil
518
- end
538
+ client.api.get_room_name(id)
539
+ rescue MatrixNotFoundError
540
+ nil
541
+ end
519
542
  changed = data[:name] != @name
520
543
  @name = data[:name] if changed
521
544
  changed
@@ -535,10 +558,10 @@ module MatrixSdk
535
558
  # @return [Boolean] if the topic was changed or not
536
559
  def reload_topic!
537
560
  data = begin
538
- client.api.get_room_topic(id)
539
- rescue MatrixNotFoundError
540
- nil
541
- end
561
+ client.api.get_room_topic(id)
562
+ rescue MatrixNotFoundError
563
+ nil
564
+ end
542
565
  changed = data[:topic] != @topic
543
566
  @topic = data[:topic] if changed
544
567
  changed
@@ -590,7 +613,7 @@ module MatrixSdk
590
613
  #
591
614
  # @param join_rule [:invite,:public] The join rule of the room
592
615
  def join_rule=(join_rule)
593
- client.api.set_join_rule(id, join_rule)
616
+ client.api.set_room_join_rules(id, join_rule)
594
617
  @join_rule = join_rule
595
618
  end
596
619
 
@@ -606,7 +629,7 @@ module MatrixSdk
606
629
  #
607
630
  # @param guest_access [:can_join,:forbidden] The new guest access status of the room
608
631
  def guest_access=(guest_access)
609
- client.api.set_guest_access(id, guest_access)
632
+ client.api.set_room_guest_access(id, guest_access)
610
633
  @guest_access = guest_access
611
634
  end
612
635
 
@@ -670,15 +693,35 @@ module MatrixSdk
670
693
  members << member unless members.any? { |m| m.id == member.id }
671
694
  end
672
695
 
696
+ def room_handlers?
697
+ client.instance_variable_get(:@room_handlers).key? id
698
+ end
699
+
700
+ def ensure_room_handlers
701
+ client.instance_variable_get(:@room_handlers)[id] ||= {
702
+ event: MatrixSdk::EventHandlerArray.new,
703
+ state_event: MatrixSdk::EventHandlerArray.new,
704
+ ephemeral_event: MatrixSdk::EventHandlerArray.new
705
+ }
706
+ end
707
+
673
708
  def put_event(event)
709
+ ensure_room_handlers[:event].fire(MatrixEvent.new(self, event), event[:type]) if room_handlers?
710
+
674
711
  @events.push event
675
712
  @events.shift if @events.length > @event_history_limit
676
-
677
- fire_event MatrixEvent.new(self, event)
678
713
  end
679
714
 
680
715
  def put_ephemeral_event(event)
681
- fire_ephemeral_event MatrixEvent.new(self, event)
716
+ return unless room_handlers?
717
+
718
+ ensure_room_handlers[:ephemeral_event].fire(MatrixEvent.new(self, event), event[:type])
719
+ end
720
+
721
+ def put_state_event(event)
722
+ return unless room_handlers?
723
+
724
+ ensure_room_handlers[:state_event].fire(MatrixEvent.new(self, event), event[:type])
682
725
  end
683
726
  end
684
727
  end
@@ -122,6 +122,14 @@ module MatrixSdk
122
122
  Time.now - (since / 1000)
123
123
  end
124
124
 
125
+ # Gets a direct message room with the user if one exists
126
+ #
127
+ # @return [Room,nil] A direct message room if one exists
128
+ # @see MatrixSdk::Client#direct_room
129
+ def direct_room
130
+ client.direct_room(id)
131
+ end
132
+
125
133
  # Returns all the current device keys for the user, retrieving them if necessary
126
134
  def device_keys
127
135
  @device_keys ||= client.api.keys_query(device_keys: { id => [] }).yield_self do |resp|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MatrixSdk
4
- VERSION = '2.0.1'
4
+ VERSION = '2.2.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: matrix_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Olofsson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-13 00:00:00.000000000 Z
11
+ date: 2020-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mocha