matrix_sdk 1.5.0 → 2.0.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: c3aa6522f1b8b49ed1ea60dc27c67fefb9479b5aa27d87c4597edf97e76d4eb6
4
- data.tar.gz: b2a6d3b5ff58c037092515ccf42e0a2683f1ef024a8b6731c824faade7649a0a
3
+ metadata.gz: 2e0e5819346e56fd4eac1564434e88399500adefe0def16bc706a25ec7a254b2
4
+ data.tar.gz: 2d74cd2d5203f5431396b03717c4302f9a0c7816c47eaa969e2395ab2eb46586
5
5
  SHA512:
6
- metadata.gz: b55690dbf6fe96eeb24a6831d390e122962b1c68d5e0031deb32641eaeb7f38568d47ec728d0adbd4eb9aea5d1f2064ff926667588b9b77893314b999366245b
7
- data.tar.gz: 73eb136cd9285ffc1415b79c25ac05807eb0b17f52865df329ebff1a83962a5005802a3f914b7ebfe8f6a2371097967955dcdcd6de01d4148c5fd9d3c96dafbd
6
+ metadata.gz: 7eb8ef8277b0bda1191915e5be4dcf93856cd876f611b77974b8dc88e5e230ba5c3300995157f9717a4b00d1a7cba7f7a618844cc436ad0b6a3ea5865367049d
7
+ data.tar.gz: bd888c36cf3a082360dbecc408c0a67e16ddb337a721105be7abf3a9662d81bfa564a1093a64d1f934d484af6a22a36e5b91bafde70f02a882e6d092e81d5459
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 2.0.0 - 2020-02-14
2
+
3
+ **NB**, this release includes backwards-incompatible changes;
4
+ - Changes room state lookup to separate specific state lookups from full state retrieval.
5
+ This will require changes in client code where `#get_room_state` is called to retrieve
6
+ all state, as it now requires a state key. For retrieving full room state,
7
+ `#get_room_state_all` is now the method to use.
8
+ - Changes some advanced parameters to named parameters, ensure your code is updated if it makes use of them
9
+ - Fixes SSL verification to actually verify certs (#9)
10
+
11
+ - Adds multiple CS API endpoints
12
+ - Adds `:room_id` key to all room events
13
+ - Adds `:self` as a valid option to the client abstraction's `#get_user` method
14
+ - Separates homeserver part stringification for MXIDs
15
+ - Exposes some previously private client abstraction methods (`#ensure_room`, `#next_batch`) for easier bot usage
16
+ - Changes room abstraction member lookups to use `#get_room_joined_members`, reducing transferred data amounts
17
+ - Fixes debug print of methods that return arrays (e.g. CS `/room/{id}/state`)
18
+
1
19
  ## 1.5.0 - 2019-10-25
2
20
 
3
21
  - Adds error event to the client abstraction, for handling errors in the background listener
@@ -98,6 +98,7 @@ module MatrixSdk
98
98
  target_uri = nil
99
99
 
100
100
  if !port.nil? && !port.empty?
101
+ # If the domain is fully qualified according to Matrix (FQDN and port) then skip discovery
101
102
  target_uri = URI("https://#{domain}:#{port}")
102
103
  elsif target == :server
103
104
  # Attempt SRV record discovery
@@ -110,6 +111,7 @@ module MatrixSdk
110
111
  end
111
112
 
112
113
  if target_uri.nil?
114
+ # Attempt .well-known discovery for server-to-server
113
115
  well_known = begin
114
116
  data = Net::HTTP.get("https://#{domain}/.well-known/matrix/server")
115
117
  JSON.parse(data)
@@ -153,6 +155,14 @@ module MatrixSdk
153
155
  ))
154
156
  end
155
157
 
158
+ # Check if a protocol is enabled on the API connection
159
+ #
160
+ # @example Checking for identity server API support
161
+ # api.protocol? :IS
162
+ # # => false
163
+ #
164
+ # @param protocol [Symbol] The protocol to check
165
+ # @return [Boolean] Is the protocol enabled
156
166
  def protocol?(protocol)
157
167
  protocols.include? protocol
158
168
  end
@@ -201,6 +211,28 @@ module MatrixSdk
201
211
  @proxy_uri = proxy_uri
202
212
  end
203
213
 
214
+ # Perform a raw Matrix API request
215
+ #
216
+ # @example Simple API query
217
+ # api.request(:get, :client_r0, '/account/whoami')
218
+ # # => { :user_id => "@alice:matrix.org" }
219
+ #
220
+ # @example Advanced API request
221
+ # api.request(:post,
222
+ # :media_r0,
223
+ # '/upload',
224
+ # body_stream: open('./file'),
225
+ # headers: { 'content-type' => 'image/png' })
226
+ # # => { :content_uri => "mxc://example.com/AQwafuaFswefuhsfAFAgsw" }
227
+ #
228
+ # @param method [Symbol] The method to use, can be any of the ones under Net::HTTP
229
+ # @param api [Symbol] The API symbol to use, :client_r0 is the current CS one
230
+ # @param path [String] The API path to call, this is the part that comes after the API definition in the spec
231
+ # @param options [Hash] Additional options to pass along to the request
232
+ # @option options [Hash] :query Query parameters to set on the URL
233
+ # @option options [Hash,String] :body The body to attach to the request, will be JSON-encoded if sent as a hash
234
+ # @option options [IO] :body_stream A body stream to attach to the request
235
+ # @option options [Hash] :headers Additional headers to set on the request
204
236
  def request(method, api, path, **options)
205
237
  url = homeserver.dup.tap do |u|
206
238
  u.path = api_to_path(api) + path
@@ -272,7 +304,8 @@ module MatrixSdk
272
304
  end
273
305
  logger.debug dir
274
306
  clean_body = JSON.parse(http.body) rescue nil if http.body
275
- clean_body.keys.each { |k| clean_body[k] = '[ REDACTED ]' if %w[password access_token].include?(k) }.to_json if clean_body
307
+ clean_body.keys.each { |k| clean_body[k] = '[ REDACTED ]' if %w[password access_token].include?(k) }.to_json if clean_body.is_a? Hash
308
+ clean_body = clean_body.to_s if clean_body
276
309
  logger.debug "#{dir} #{clean_body.length < 200 ? clean_body : clean_body.slice(0..200) + "... [truncated, #{clean_body.length} Bytes]"}" if clean_body
277
310
  rescue StandardError => e
278
311
  logger.warn "#{e.class} occured while printing request debug; #{e.message}\n#{e.backtrace.join "\n"}"
@@ -303,7 +336,7 @@ module MatrixSdk
303
336
  @http.open_timeout = open_timeout
304
337
  @http.read_timeout = read_timeout
305
338
  @http.use_ssl = homeserver.scheme == 'https'
306
- @http.verify_mode = validate_certificate ? ::OpenSSL::SSL::VERIFY_NONE : nil
339
+ @http.verify_mode = validate_certificate ? ::OpenSSL::SSL::VERIFY_PEER : ::OpenSSL::SSL::VERIFY_NONE
307
340
  @http.start
308
341
  @http
309
342
  end
@@ -11,7 +11,16 @@ module MatrixSdk
11
11
  include MatrixSdk::Logging
12
12
  extend Forwardable
13
13
 
14
- attr_reader :api
14
+ # @!attribute api [r] The underlying API connection
15
+ # @return [Api] The underlying API connection
16
+ # @!attribute next_batch [r] The batch token for a running sync
17
+ # @return [String] The opaque batch token
18
+ # @!attribute cache [rw] The cache level
19
+ # @return [:all,:some,:none] The level of caching to do
20
+ # @!attribute sync_filter [rw] The global sync filter
21
+ # @return [Hash,String] A filter definition, either as defined by the
22
+ # Matrix spec, or as an identifier returned by a filter creation request
23
+ attr_reader :api, :next_batch
15
24
  attr_accessor :cache, :sync_filter
16
25
 
17
26
  events :error, :event, :presence_event, :invite_event, :leave_event, :ephemeral_event
@@ -22,6 +31,17 @@ module MatrixSdk
22
31
  :access_token, :access_token=, :device_id, :device_id=, :homeserver, :homeserver=,
23
32
  :validate_certificate, :validate_certificate=
24
33
 
34
+ # Create a new client instance from only a Matrix HS domain
35
+ #
36
+ # This will use the well-known delegation lookup to find the correct client URL
37
+ #
38
+ # @note This method will not verify that the created client has a valid connection,
39
+ # it will only perform the necessary lookups to build a connection URL.
40
+ # @return [Client] The new client instance
41
+ # @param domain [String] The domain name to look up
42
+ # @param params [Hash] Additional parameters to pass along to {Api.new_for_domain} as well as {initialize}
43
+ # @see Api.new_for_domain
44
+ # @see #initialize
25
45
  def self.new_for_domain(domain, **params)
26
46
  api = MatrixSdk::Api.new_for_domain(domain, keep_wellknown: true)
27
47
  return new(api, params) unless api.well_known.key? 'm.identity_server'
@@ -53,7 +73,7 @@ module MatrixSdk
53
73
  @rooms = {}
54
74
  @users = {}
55
75
  @cache = client_cache
56
- @identity_server = params.fetch(:identity_server, nil)
76
+ @identity_server = nil
57
77
 
58
78
  @sync_token = nil
59
79
  @sync_thread = nil
@@ -75,22 +95,42 @@ module MatrixSdk
75
95
  @mxid = params[:user_id]
76
96
  end
77
97
 
98
+ # Gets the currently logged in user's MXID
99
+ #
100
+ # @return [MXID] The MXID of the current user
78
101
  def mxid
79
102
  @mxid ||= begin
80
- api.whoami?[:user_id] if api&.access_token
103
+ MXID.new api.whoami?[:user_id] if api&.access_token
81
104
  end
82
105
  end
83
106
 
84
- def mxid=(id)
85
- id = MXID.new id.to_s unless id.is_a? MXID
86
- raise ArgumentError, 'Must be a User ID' unless id.user?
107
+ alias user_id mxid
87
108
 
88
- @mxid = id
109
+ # Gets the current user presence status object
110
+ #
111
+ # @return [Response] The user presence
112
+ # @see User#presence
113
+ # @see Protocols::CS#get_presence_status
114
+ def presence
115
+ api.get_presence_status(mxid).tap { |h| h.delete :user_id }
89
116
  end
90
117
 
91
- alias user_id mxid
92
- alias user_id= mxid=
118
+ # Sets the current user's presence status
119
+ #
120
+ # @param status [:online,:offline,:unavailable] The new status to use
121
+ # @param message [String] A custom status message to set
122
+ # @see User#presence=
123
+ # @see Protocols::CS#set_presence_status
124
+ def set_presence(status, message: nil)
125
+ raise ArgumentError, 'Presence must be one of :online, :offline, :unavailable' unless %i[online offline unavailable].include?(status)
126
+
127
+ api.set_presence_status(mxid, status, message: message)
128
+ end
93
129
 
130
+ # Gets a list of all the public rooms on the connected HS
131
+ #
132
+ # @note This will try to list all public rooms on the HS, and may take a while on larger instances
133
+ # @return [Array[Room]] The public rooms
94
134
  def public_rooms
95
135
  rooms = []
96
136
  since = nil
@@ -114,6 +154,12 @@ module MatrixSdk
114
154
  rooms
115
155
  end
116
156
 
157
+ # Gets a list of all relevant rooms, either the ones currently handled by
158
+ # the client, or the list of currently joined ones if no rooms are handled
159
+ #
160
+ # @return [Array[Room]] All the currently handled rooms
161
+ # @note This will always return the empty array if the cache level is set
162
+ # to :none
117
163
  def rooms
118
164
  if @rooms.empty? && cache != :none
119
165
  api.get_joined_rooms.joined_rooms.each do |id|
@@ -124,6 +170,11 @@ module MatrixSdk
124
170
  @rooms.values
125
171
  end
126
172
 
173
+ # Refresh the list of currently handled rooms, replacing it with the user's
174
+ # currently joined rooms.
175
+ #
176
+ # @note This will be a no-op if the cache level is set to :none
177
+ # @return [Boolean] If the refresh succeeds
127
178
  def reload_rooms!
128
179
  return true if cache == :none
129
180
 
@@ -137,12 +188,25 @@ module MatrixSdk
137
188
  end
138
189
  alias refresh_rooms! reload_rooms!
139
190
 
191
+ # Register - and log in - on the connected HS as a guest
192
+ #
193
+ # @note This feature is not commonly supported by many HSes
140
194
  def register_as_guest
141
195
  data = api.register(kind: :guest)
142
196
  post_authentication(data)
143
197
  end
144
198
 
145
- def register_with_password(username, password, full_state: true, **params)
199
+ # Register a new user account on the connected HS
200
+ #
201
+ # This will also trigger an initial sync unless no_sync is set
202
+ #
203
+ # @note This method will currently always use auth type 'm.login.dummy'
204
+ # @param username [String] The new user's name
205
+ # @param password [String] The new user's password
206
+ # @param params [Hash] Additional options
207
+ # @option params [Boolean] :no_sync Skip the initial sync on registering
208
+ # @option params [Boolean] :allow_sync_retry Allow sync to retry on failure
209
+ def register_with_password(username, password, **params)
146
210
  username = username.to_s unless username.is_a?(String)
147
211
  password = password.to_s unless password.is_a?(String)
148
212
 
@@ -154,10 +218,21 @@ module MatrixSdk
154
218
 
155
219
  return if params[:no_sync]
156
220
 
157
- sync full_state: full_state,
221
+ sync full_state: true,
158
222
  allow_sync_retry: params.fetch(:allow_sync_retry, nil)
159
223
  end
160
224
 
225
+ # Logs in as a user on the connected HS
226
+ #
227
+ # This will also trigger an initial sync unless no_sync is set
228
+ #
229
+ # @param username [String] The username of the user
230
+ # @param password [String] The password of the user
231
+ # @param sync_timeout [Numeric] The timeout of the initial sync on login
232
+ # @param full_state [Boolean] Should the initial sync retrieve full state
233
+ # @param params [Hash] Additional options
234
+ # @option params [Boolean] :no_sync Skip the initial sync on registering
235
+ # @option params [Boolean] :allow_sync_retry Allow sync to retry on failure
161
236
  def login(username, password, sync_timeout: 15, full_state: false, **params)
162
237
  username = username.to_s unless username.is_a?(String)
163
238
  password = password.to_s unless password.is_a?(String)
@@ -175,6 +250,17 @@ module MatrixSdk
175
250
  allow_sync_retry: params.fetch(:allow_sync_retry, nil)
176
251
  end
177
252
 
253
+ # Logs in as a user on the connected HS
254
+ #
255
+ # This will also trigger an initial sync unless no_sync is set
256
+ #
257
+ # @param username [String] The username of the user
258
+ # @param token [String] The token to log in with
259
+ # @param sync_timeout [Numeric] The timeout of the initial sync on login
260
+ # @param full_state [Boolean] Should the initial sync retrieve full state
261
+ # @param params [Hash] Additional options
262
+ # @option params [Boolean] :no_sync Skip the initial sync on registering
263
+ # @option params [Boolean] :allow_sync_retry Allow sync to retry on failure
178
264
  def login_with_token(username, token, sync_timeout: 15, full_state: false, **params)
179
265
  username = username.to_s unless username.is_a?(String)
180
266
  token = token.to_s unless token.is_a?(String)
@@ -192,30 +278,90 @@ module MatrixSdk
192
278
  allow_sync_retry: params.fetch(:allow_sync_retry, nil)
193
279
  end
194
280
 
281
+ # Logs out of the current session
195
282
  def logout
196
283
  api.logout
197
284
  @api.access_token = nil
198
285
  @mxid = nil
199
286
  end
200
287
 
288
+ # Check if there's a currently logged in session
289
+ #
290
+ # @note This will not check if the session is valid, only if it exists
291
+ # @return [Boolean] If there's a current session
201
292
  def logged_in?
202
- !(mxid.nil? || @api.access_token.nil?)
293
+ !@api.access_token.nil?
203
294
  end
204
295
 
296
+ # Retrieve a list of all registered third-party IDs for the current user
297
+ #
298
+ # @return [Response] A response hash containing the key :threepids
299
+ # @see Protocols::CS#get_3pids
300
+ def registered_3pids
301
+ data = api.get_3pids
302
+ data.threepids.each do |obj|
303
+ obj.instance_eval do
304
+ def added_at
305
+ Time.at(self[:added_at] / 1000)
306
+ end
307
+
308
+ def validated_at
309
+ return unless validated?
310
+
311
+ Time.at(self[:validated_at] / 1000)
312
+ end
313
+
314
+ def validated?
315
+ key? :validated_at
316
+ end
317
+
318
+ def to_s
319
+ "#{self[:medium]}:#{self[:address]}"
320
+ end
321
+
322
+ def inspect
323
+ "#<MatrixSdk::Response 3pid=#{to_s.inspect} added_at=\"#{added_at}\"#{validated? ? " validated_at=\"#{validated_at}\"" : ''}>"
324
+ end
325
+ end
326
+ end
327
+ data
328
+ end
329
+
330
+ # Creates a new room
331
+ #
332
+ # @example Creating a room with an alias
333
+ # client.create_room('myroom')
334
+ # #<MatrixSdk::Room ... >
335
+ #
336
+ # @param room_alias [String] A default alias to set on the room, should only be the localpart
337
+ # @return [Room] The resulting room
338
+ # @see Protocols::CS#create_room
205
339
  def create_room(room_alias = nil, **params)
206
340
  data = api.create_room(params.merge(room_alias: room_alias))
207
341
  ensure_room(data.room_id)
208
342
  end
209
343
 
344
+ # Joins an already created room
345
+ #
346
+ # @param room_id_or_alias [String,MXID] A room alias (#room:exmaple.com) or a room ID (!id:example.com)
347
+ # @param server_name [Array[String]] A list of servers to attempt the join through, required for IDs
348
+ # @return [Room] The resulting room
349
+ # @see Protocols::CS#join_room
210
350
  def join_room(room_id_or_alias, server_name: [])
211
351
  server_name = [server_name] unless server_name.is_a? Array
212
352
  data = api.join_room(room_id_or_alias, server_name: server_name)
213
353
  ensure_room(data.fetch(:room_id, room_id_or_alias))
214
354
  end
215
355
 
356
+ # Find a room in the locally cached list of rooms that the current user is part of
357
+ #
358
+ # @param room_id_or_alias [String,MXID] A room ID or alias
359
+ # @param only_canonical [Boolean] Only match alias against the canonical alias
360
+ # @return [Room] The found room
361
+ # @return [nil] If no room was found
216
362
  def find_room(room_id_or_alias, only_canonical: false)
217
363
  room_id_or_alias = MXID.new(room_id_or_alias.to_s) unless room_id_or_alias.is_a? MXID
218
- raise ArgumentError, 'Must be a room id or alias' unless %i[room_id room_alias].include? room_id_or_alias.type
364
+ raise ArgumentError, 'Must be a room id or alias' unless room_id_or_alias.room?
219
365
 
220
366
  return @rooms.fetch(room_id_or_alias.to_s, nil) if room_id_or_alias.room_id?
221
367
 
@@ -224,7 +370,21 @@ module MatrixSdk
224
370
  @rooms.values.find { |r| r.aliases.include? room_id_or_alias.to_s }
225
371
  end
226
372
 
373
+ # Get a User instance from a MXID
374
+ #
375
+ # @param user_id [String,MXID,:self] The MXID to look up, will also accept :self in order to get the currently logged-in user
376
+ # @return [User] The User instance for the specified user
377
+ # @raise [ArgumentError] If the input isn't a valid user ID
378
+ # @note The method doesn't perform any existence checking, so the returned User object may point to a non-existent user
227
379
  def get_user(user_id)
380
+ user_id = mxid if user_id == :self
381
+
382
+ user_id = MXID.new user_id.to_s unless user_id.is_a? MXID
383
+ raise ArgumentError, 'Must be a User ID' unless user_id.user?
384
+
385
+ # To still use regular string storage in the hash itself
386
+ user_id = user_id.to_s
387
+
228
388
  if cache == :all
229
389
  @users[user_id] ||= User.new(self, user_id)
230
390
  else
@@ -232,10 +392,23 @@ module MatrixSdk
232
392
  end
233
393
  end
234
394
 
395
+ # Remove a room alias
396
+ #
397
+ # @param room_alias [String,MXID] The room alias to remove
398
+ # @see Protocols::CS#remove_room_alias
235
399
  def remove_room_alias(room_alias)
400
+ room_alias = MXID.new room_alias.to_s unless room_alias.is_a? MXID
401
+ raise ArgumentError, 'Must be a room alias' unless room_alias.room_alias?
402
+
236
403
  api.remove_room_alias(room_alias)
237
404
  end
238
405
 
406
+ # Upload a piece of data to the media repo
407
+ #
408
+ # @return [URI::MATRIX] A Matrix content (mxc://) URL pointing to the uploaded data
409
+ # @param content [String] The data to upload
410
+ # @param content_type [String] The MIME type of the data
411
+ # @see Protocols::CS#media_upload
239
412
  def upload(content, content_type)
240
413
  data = api.media_upload(content, content_type)
241
414
  return data[:content_uri] if data.key? :content_uri
@@ -243,13 +416,19 @@ module MatrixSdk
243
416
  raise MatrixUnexpectedResponseError, 'Upload succeeded, but no media URI returned'
244
417
  end
245
418
 
419
+ # Starts a background thread that will listen to new events
420
+ #
421
+ # @see sync For What parameters are accepted
246
422
  def start_listener_thread(**params)
423
+ return if listening?
424
+
247
425
  @should_listen = true
248
426
  thread = Thread.new { listen_forever(params) }
249
427
  @sync_thread = thread
250
428
  thread.run
251
429
  end
252
430
 
431
+ # Stops the running background thread if one is active
253
432
  def stop_listener_thread
254
433
  return unless @sync_thread
255
434
 
@@ -258,10 +437,21 @@ module MatrixSdk
258
437
  @sync_thread = nil
259
438
  end
260
439
 
440
+ # Check if there's a thread listening for events
261
441
  def listening?
262
442
  @sync_thread&.alive? == true
263
443
  end
264
444
 
445
+ # Run a message sync round, triggering events as necessary
446
+ #
447
+ # @param skip_store_batch [Boolean] Should this sync skip storing the returned next_batch token,
448
+ # doing this would mean the next sync re-runs from the same point. Useful with use of filters.
449
+ # @param params [Hash] Additional options
450
+ # @option params [String,Hash] :filter (#sync_filter) A filter to use for this sync
451
+ # @option params [Numeric] :timeout (30) A timeout value in seconds for the sync request
452
+ # @option params [Numeric] :allow_sync_retry (0) The number of retries allowed for this sync request
453
+ # @option params [String] :since An override of the "since" token to provide to the sync request
454
+ # @see Protocols::CS#sync
265
455
  def sync(skip_store_batch: false, **params)
266
456
  extra_params = {
267
457
  filter: sync_filter,
@@ -283,10 +473,27 @@ module MatrixSdk
283
473
  @next_batch = data[:next_batch] unless skip_store_batch
284
474
 
285
475
  handle_sync_response(data)
476
+ true
286
477
  end
287
478
 
288
479
  alias listen_for_events sync
289
480
 
481
+ # Ensures that a room exists in the cache
482
+ #
483
+ # @param room_id [String,MXID] The room ID to ensure
484
+ # @return [Room] The room object for the requested room
485
+ def ensure_room(room_id)
486
+ room_id = MXID.new room_id.to_s unless room_id.is_a? MXID
487
+ raise ArgumentError, 'Must be a room ID' unless room_id.room_id?
488
+
489
+ room_id = room_id.to_s
490
+ @rooms.fetch(room_id) do
491
+ room = Room.new(self, room_id)
492
+ @rooms[room_id] = room unless cache == :none
493
+ room
494
+ end
495
+ end
496
+
290
497
  private
291
498
 
292
499
  def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 30, **params)
@@ -320,15 +527,6 @@ module MatrixSdk
320
527
  access_token
321
528
  end
322
529
 
323
- def ensure_room(room_id)
324
- room_id = room_id.to_s unless room_id.is_a? String
325
- @rooms.fetch(room_id) do
326
- room = Room.new(self, room_id)
327
- @rooms[room_id] = room unless cache == :none
328
- room
329
- end
330
- end
331
-
332
530
  def handle_state(room_id, state_event)
333
531
  return unless state_event.key? :type
334
532
 
@@ -368,10 +566,12 @@ module MatrixSdk
368
566
  end
369
567
 
370
568
  data[:rooms][:invite].each do |room_id, invite|
569
+ invite[:room_id] = room_id.to_s
371
570
  fire_invite_event(MatrixEvent.new(self, invite), room_id.to_s)
372
571
  end
373
572
 
374
573
  data[:rooms][:leave].each do |room_id, left|
574
+ left[:room_id] = room_id.to_s
375
575
  fire_leave_event(MatrixEvent.new(self, left), room_id.to_s)
376
576
  end
377
577