matrix_sdk 2.0.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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