matrix_sdk 1.5.0 → 2.0.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: 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