activematrix 0.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.
@@ -0,0 +1,696 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'matrix_sdk'
4
+ require 'matrix_sdk/util/events'
5
+
6
+ require 'English'
7
+ require 'forwardable'
8
+
9
+ module MatrixSdk
10
+ class Client
11
+ extend MatrixSdk::Extensions
12
+ include MatrixSdk::Logging
13
+ extend Forwardable
14
+
15
+ # @!attribute api [r] The underlying API connection
16
+ # @return [Api] The underlying API connection
17
+ # @!attribute next_batch [r] The batch token for a running sync
18
+ # @return [String] The opaque batch token
19
+ # @!attribute cache [rw] The cache level
20
+ # @return [:all,:some,:none] The level of caching to do
21
+ # @!attribute sync_filter [rw] The global sync filter
22
+ # @return [Hash,String] A filter definition, either as defined by the
23
+ # Matrix spec, or as an identifier returned by a filter creation request
24
+ attr_reader :api
25
+ attr_accessor :cache, :sync_filter, :next_batch
26
+
27
+ events :error, :event, :account_data, :presence_event, :invite_event, :leave_event, :ephemeral_event, :state_event
28
+ ignore_inspect :api,
29
+ :on_event, :on_account_data, :on_presence_event, :on_invite_event, :on_leave_event, :on_ephemeral_event
30
+
31
+ def_delegators :@api,
32
+ :access_token, :access_token=, :device_id, :device_id=, :homeserver, :homeserver=,
33
+ :validate_certificate, :validate_certificate=
34
+
35
+ # Create a new client instance from only a Matrix HS domain
36
+ #
37
+ # This will use the well-known delegation lookup to find the correct client URL
38
+ #
39
+ # @note This method will not verify that the created client has a valid connection,
40
+ # it will only perform the necessary lookups to build a connection URL.
41
+ # @return [Client] The new client instance
42
+ # @param domain [String] The domain name to look up
43
+ # @param params [Hash] Additional parameters to pass along to {Api.new_for_domain} as well as {initialize}
44
+ # @see Api.new_for_domain
45
+ # @see #initialize
46
+ def self.new_for_domain(domain, **params)
47
+ api = MatrixSdk::Api.new_for_domain(domain, keep_wellknown: true)
48
+ return new(api, **params) unless api.well_known&.key?('m.identity_server')
49
+
50
+ identity_server = MatrixSdk::Api.new(api.well_known['m.identity_server']['base_url'], protocols: %i[IS])
51
+ new(api, **params.merge(identity_server: identity_server))
52
+ end
53
+
54
+ # @param hs_url [String,URI,Api] The URL to the Matrix homeserver, without the /_matrix/ part, or an existing Api instance
55
+ # @param client_cache [:all,:some,:none] (:all) How much data should be cached in the client
56
+ # @param params [Hash] Additional parameters on creation
57
+ # @option params [String,MXID] :user_id The user ID of the logged-in user
58
+ # @option params [Integer] :sync_filter_limit (20) Limit of timeline entries in syncs
59
+ # @see MatrixSdk::Api.new for additional usable params
60
+ def initialize(hs_url, client_cache: :all, **params)
61
+ event_initialize
62
+
63
+ params[:user_id] ||= params[:mxid] if params[:mxid]
64
+
65
+ if hs_url.is_a? Api
66
+ @api = hs_url
67
+ params.each do |k, v|
68
+ api.instance_variable_set("@#{k}", v) if api.instance_variable_defined? "@#{k}"
69
+ end
70
+ else
71
+ @api = Api.new hs_url, **params
72
+ end
73
+
74
+ @cache = client_cache
75
+ @identity_server = nil
76
+ @mxid = nil
77
+
78
+ @sync_thread = nil
79
+ @sync_filter = { room: { timeline: { limit: params.fetch(:sync_filter_limit, 20) }, state: { lazy_load_members: true } } }
80
+
81
+ @next_batch = nil
82
+
83
+ @bad_sync_timeout_limit = 60 * 60
84
+
85
+ params.each do |k, v|
86
+ instance_variable_set("@#{k}", v) if instance_variable_defined? "@#{k}"
87
+ end
88
+
89
+ @rooms = {}
90
+ @room_handlers = {}
91
+ @users = {}
92
+ @should_listen = false
93
+
94
+ raise ArgumentError, 'Cache value must be one of of [:all, :some, :none]' unless %i[all some none].include? @cache
95
+
96
+ return unless params[:user_id]
97
+
98
+ @mxid = params[:user_id]
99
+ end
100
+
101
+ alias sync_token next_batch
102
+ alias sync_token= next_batch=
103
+
104
+ # Gets the currently logged in user's MXID
105
+ #
106
+ # @return [MXID] The MXID of the current user
107
+ def mxid
108
+ @mxid ||= MXID.new api.whoami?[:user_id] if api&.access_token
109
+ @mxid
110
+ end
111
+
112
+ alias user_id mxid
113
+
114
+ # Gets the current user presence status object
115
+ #
116
+ # @return [Response] The user presence
117
+ # @see User#presence
118
+ # @see Protocols::CS#get_presence_status
119
+ def presence
120
+ api.get_presence_status(mxid).tap { |h| h.delete :user_id }
121
+ end
122
+
123
+ # Sets the current user's presence status
124
+ #
125
+ # @param status [:online,:offline,:unavailable] The new status to use
126
+ # @param message [String] A custom status message to set
127
+ # @see User#presence=
128
+ # @see Protocols::CS#set_presence_status
129
+ def set_presence(status, message: nil)
130
+ raise ArgumentError, 'Presence must be one of :online, :offline, :unavailable' unless %i[online offline unavailable].include?(status)
131
+
132
+ api.set_presence_status(mxid, status, message: message)
133
+ end
134
+
135
+ # Gets a list of all the public rooms on the connected HS
136
+ #
137
+ # @note This will try to list all public rooms on the HS, and may take a while on larger instances
138
+ # @return [Array[Room]] The public rooms
139
+ def public_rooms
140
+ rooms = []
141
+ since = nil
142
+ loop do
143
+ data = api.get_public_rooms since: since
144
+
145
+ data[:chunk].each do |chunk|
146
+ rooms << Room.new(self, chunk[:room_id],
147
+ name: chunk[:name], topic: chunk[:topic], aliases: chunk[:aliases],
148
+ canonical_alias: chunk[:canonical_alias], avatar_url: chunk[:avatar_url],
149
+ join_rule: :public, world_readable: chunk[:world_readable]).tap do |r|
150
+ r.instance_variable_set :@guest_access, chunk[:guest_can_join] ? :can_join : :forbidden
151
+ end
152
+ end
153
+
154
+ break if data[:next_batch].nil?
155
+
156
+ since = data.next_batch
157
+ end
158
+
159
+ rooms
160
+ end
161
+
162
+ # Gets a list of all direct chat rooms (1:1 chats / direct message chats) for the currenct user
163
+ #
164
+ # @return [Hash[String,Array[String]]] A mapping of MXIDs to a list of direct rooms with that user
165
+ def direct_rooms
166
+ account_data['m.direct'].transform_keys(&:to_s)
167
+ end
168
+
169
+ # Retrieve an account data helper
170
+ def account_data
171
+ return MatrixSdk::Util::AccountDataCache.new self if cache == :none
172
+
173
+ @account_data ||= MatrixSdk::Util::AccountDataCache.new self
174
+ end
175
+
176
+ # Gets a direct message room for the given user if one exists
177
+ #
178
+ # @note Will return the oldest room if multiple exist
179
+ # @return [Room,nil] A direct message room if one exists
180
+ def direct_room(mxid)
181
+ mxid = MatrixSdk::MXID.new mxid.to_s unless mxid.is_a? MatrixSdk::MXID
182
+ raise ArgumentError, 'Must be a valid user ID' unless mxid.user?
183
+
184
+ room_id = direct_rooms[mxid.to_s]&.first
185
+ ensure_room room_id if room_id
186
+ end
187
+
188
+ # Gets a list of all relevant rooms, either the ones currently handled by
189
+ # the client, or the list of currently joined ones if no rooms are handled
190
+ #
191
+ # @return [Array[Room]] All the currently handled rooms
192
+ # @note This will always return the empty array if the cache level is set
193
+ # to :none
194
+ def rooms
195
+ if @rooms.empty? && cache != :none
196
+ api.get_joined_rooms.joined_rooms.each do |id|
197
+ ensure_room(id)
198
+ end
199
+ end
200
+
201
+ @rooms.values
202
+ end
203
+
204
+ # Get a list of all joined Matrix Spaces
205
+ #
206
+ # @return [Array[Room]] All the currently joined Spaces
207
+ def spaces
208
+ rooms = if cache == :none
209
+ api.get_joined_rooms.joined_rooms.map { |id| Room.new(self, id) }
210
+ else
211
+ self.rooms
212
+ end
213
+
214
+ rooms.select(&:space?)
215
+ end
216
+
217
+ # Refresh the list of currently handled rooms, replacing it with the user's
218
+ # currently joined rooms.
219
+ #
220
+ # @note This will be a no-op if the cache level is set to :none
221
+ # @return [Boolean] If the refresh succeeds
222
+ def reload_rooms!
223
+ return true if cache == :none
224
+
225
+ @rooms.clear
226
+ api.get_joined_rooms.joined_rooms.each do |id|
227
+ r = ensure_room(id)
228
+ r.reload!
229
+ end
230
+
231
+ true
232
+ end
233
+ alias refresh_rooms! reload_rooms!
234
+ alias reload_spaces! reload_rooms!
235
+
236
+ # Register - and log in - on the connected HS as a guest
237
+ #
238
+ # @note This feature is not commonly supported by many HSes
239
+ def register_as_guest
240
+ data = api.register(kind: :guest)
241
+ post_authentication(data)
242
+ end
243
+
244
+ # Register a new user account on the connected HS
245
+ #
246
+ # This will also trigger an initial sync unless no_sync is set
247
+ #
248
+ # @note This method will currently always use auth type 'm.login.dummy'
249
+ # @param username [String] The new user's name
250
+ # @param password [String] The new user's password
251
+ # @param params [Hash] Additional options
252
+ # @option params [Boolean] :no_sync Skip the initial sync on registering
253
+ # @option params [Boolean] :allow_sync_retry Allow sync to retry on failure
254
+ def register_with_password(username, password, **params)
255
+ username = username.to_s unless username.is_a?(String)
256
+ password = password.to_s unless password.is_a?(String)
257
+
258
+ raise ArgumentError, "Username can't be nil or empty" if username.nil? || username.empty?
259
+ raise ArgumentError, "Password can't be nil or empty" if password.nil? || username.empty?
260
+
261
+ data = api.register(auth: { type: 'm.login.dummy' }, username: username, password: password)
262
+ post_authentication(data)
263
+
264
+ return if params[:no_sync]
265
+
266
+ sync full_state: true,
267
+ allow_sync_retry: params.fetch(:allow_sync_retry, nil)
268
+ end
269
+
270
+ # Logs in as a user on the connected HS
271
+ #
272
+ # This will also trigger an initial sync unless no_sync is set
273
+ #
274
+ # @param username [String] The username of the user
275
+ # @param password [String] The password of the user
276
+ # @param sync_timeout [Numeric] The timeout of the initial sync on login
277
+ # @param full_state [Boolean] Should the initial sync retrieve full state
278
+ # @param params [Hash] Additional options
279
+ # @option params [Boolean] :no_sync Skip the initial sync on registering
280
+ # @option params [Boolean] :allow_sync_retry Allow sync to retry on failure
281
+ def login(username, password, sync_timeout: 15, full_state: false, **params)
282
+ username = username.to_s unless username.is_a?(String)
283
+ password = password.to_s unless password.is_a?(String)
284
+
285
+ raise ArgumentError, "Username can't be nil or empty" if username.nil? || username.empty?
286
+ raise ArgumentError, "Password can't be nil or empty" if password.nil? || password.empty?
287
+
288
+ data = api.login(user: username, password: password)
289
+ post_authentication(data)
290
+
291
+ return if params[:no_sync]
292
+
293
+ sync timeout: sync_timeout,
294
+ full_state: full_state,
295
+ allow_sync_retry: params.fetch(:allow_sync_retry, nil)
296
+ end
297
+
298
+ # Logs in as a user on the connected HS
299
+ #
300
+ # This will also trigger an initial sync unless no_sync is set
301
+ #
302
+ # @param username [String] The username of the user
303
+ # @param token [String] The token to log in with
304
+ # @param sync_timeout [Numeric] The timeout of the initial sync on login
305
+ # @param full_state [Boolean] Should the initial sync retrieve full state
306
+ # @param params [Hash] Additional options
307
+ # @option params [Boolean] :no_sync Skip the initial sync on registering
308
+ # @option params [Boolean] :allow_sync_retry Allow sync to retry on failure
309
+ def login_with_token(username, token, sync_timeout: 15, full_state: false, **params)
310
+ username = username.to_s unless username.is_a?(String)
311
+ token = token.to_s unless token.is_a?(String)
312
+
313
+ raise ArgumentError, "Username can't be nil or empty" if username.nil? || username.empty?
314
+ raise ArgumentError, "Token can't be nil or empty" if token.nil? || token.empty?
315
+
316
+ data = api.login(user: username, token: token, type: 'm.login.token')
317
+ post_authentication(data)
318
+
319
+ return if params[:no_sync]
320
+
321
+ sync timeout: sync_timeout,
322
+ full_state: full_state,
323
+ allow_sync_retry: params.fetch(:allow_sync_retry, nil)
324
+ end
325
+
326
+ # Logs out of the current session
327
+ def logout
328
+ api.logout
329
+ @api.access_token = nil
330
+ @mxid = nil
331
+ end
332
+
333
+ # Check if there's a currently logged in session
334
+ #
335
+ # @note This will not check if the session is valid, only if it exists
336
+ # @return [Boolean] If there's a current session
337
+ def logged_in?
338
+ !@api.access_token.nil?
339
+ end
340
+
341
+ # Retrieve a list of all registered third-party IDs for the current user
342
+ #
343
+ # @return [Response] A response hash containing the key :threepids
344
+ # @see Protocols::CS#get_3pids
345
+ def registered_3pids
346
+ data = api.get_3pids
347
+ data.threepids.each do |obj|
348
+ obj.instance_eval do
349
+ def added_at
350
+ Time.at(self[:added_at] / 1000)
351
+ end
352
+
353
+ def validated_at
354
+ return unless validated?
355
+
356
+ Time.at(self[:validated_at] / 1000)
357
+ end
358
+
359
+ def validated?
360
+ key? :validated_at
361
+ end
362
+
363
+ def to_s
364
+ "#{self[:medium]}:#{self[:address]}"
365
+ end
366
+
367
+ def inspect
368
+ "#<MatrixSdk::Response 3pid=#{to_s.inspect} added_at=\"#{added_at}\"#{validated? ? " validated_at=\"#{validated_at}\"" : ''}>"
369
+ end
370
+ end
371
+ end
372
+ data
373
+ end
374
+
375
+ # Creates a new room
376
+ #
377
+ # @example Creating a room with an alias
378
+ # client.create_room('myroom')
379
+ # #<MatrixSdk::Room ... >
380
+ #
381
+ # @param room_alias [String] A default alias to set on the room, should only be the localpart
382
+ # @return [Room] The resulting room
383
+ # @see Protocols::CS#create_room
384
+ def create_room(room_alias = nil, **params)
385
+ data = api.create_room(**params.merge(room_alias: room_alias))
386
+ ensure_room(data.room_id)
387
+ end
388
+
389
+ # Joins an already created room
390
+ #
391
+ # @param room_id_or_alias [String,MXID] A room alias (#room:example.com) or a room ID (!id:example.com)
392
+ # @param server_name [Array[String]] A list of servers to attempt the join through, required for IDs
393
+ # @return [Room] The resulting room
394
+ # @see Protocols::CS#join_room
395
+ def join_room(room_id_or_alias, server_name: [])
396
+ server_name = [server_name] unless server_name.is_a? Array
397
+ data = api.join_room(room_id_or_alias, server_name: server_name)
398
+ ensure_room(data.fetch(:room_id, room_id_or_alias))
399
+ end
400
+
401
+ # Find a room in the locally cached list of rooms that the current user is part of
402
+ #
403
+ # @param room_id_or_alias [String,MXID] A room ID or alias
404
+ # @param only_canonical [Boolean] Only match alias against the canonical alias
405
+ # @return [Room] The found room
406
+ # @return [nil] If no room was found
407
+ def find_room(room_id_or_alias, only_canonical: true)
408
+ room_id_or_alias = MXID.new(room_id_or_alias.to_s) unless room_id_or_alias.is_a? MXID
409
+ raise ArgumentError, 'Must be a room id or alias' unless room_id_or_alias.room?
410
+
411
+ return @rooms.fetch(room_id_or_alias.to_s, nil) if room_id_or_alias.room_id?
412
+
413
+ room = @rooms.values.find { |r| r.aliases.include? room_id_or_alias.to_s }
414
+ return room if only_canonical
415
+
416
+ room || @rooms.values.find { |r| r.aliases(canonical_only: false).include? room_id_or_alias.to_s }
417
+ end
418
+
419
+ # Get a User instance from a MXID
420
+ #
421
+ # @param user_id [String,MXID,:self] The MXID to look up, will also accept :self in order to get the currently logged-in user
422
+ # @return [User] The User instance for the specified user
423
+ # @raise [ArgumentError] If the input isn't a valid user ID
424
+ # @note The method doesn't perform any existence checking, so the returned User object may point to a non-existent user
425
+ def get_user(user_id)
426
+ user_id = mxid if user_id == :self
427
+
428
+ user_id = MXID.new user_id.to_s unless user_id.is_a? MXID
429
+ raise ArgumentError, 'Must be a User ID' unless user_id.user?
430
+
431
+ # To still use regular string storage in the hash itself
432
+ user_id = user_id.to_s
433
+
434
+ if cache == :all
435
+ @users[user_id] ||= User.new(self, user_id)
436
+ else
437
+ User.new(self, user_id)
438
+ end
439
+ end
440
+
441
+ # Remove a room alias
442
+ #
443
+ # @param room_alias [String,MXID] The room alias to remove
444
+ # @see Protocols::CS#remove_room_alias
445
+ def remove_room_alias(room_alias)
446
+ room_alias = MXID.new room_alias.to_s unless room_alias.is_a? MXID
447
+ raise ArgumentError, 'Must be a room alias' unless room_alias.room_alias?
448
+
449
+ api.remove_room_alias(room_alias)
450
+ end
451
+
452
+ # Upload a piece of data to the media repo
453
+ #
454
+ # @return [URI::MXC] A Matrix content (mxc://) URL pointing to the uploaded data
455
+ # @param content [String] The data to upload
456
+ # @param content_type [String] The MIME type of the data
457
+ # @see Protocols::CS#media_upload
458
+ def upload(content, content_type)
459
+ data = api.media_upload(content, content_type)
460
+ return URI(data[:content_uri]) if data.key? :content_uri
461
+
462
+ raise MatrixUnexpectedResponseError, 'Upload succeeded, but no media URI returned'
463
+ end
464
+
465
+ # Starts a background thread that will listen to new events
466
+ #
467
+ # @see sync For What parameters are accepted
468
+ def start_listener_thread(**params)
469
+ return if listening?
470
+
471
+ @should_listen = true
472
+ if api.protocol?(:MSC) && api.msc2108?
473
+ params[:filter] = sync_filter unless params.key? :filter
474
+ params[:filter] = params[:filter].to_json unless params[:filter].nil? || params[:filter].is_a?(String)
475
+ params[:since] = @next_batch if @next_batch
476
+
477
+ errors = 0
478
+ thread, cancel_token = api.msc2108_sync_sse(params) do |data, event:, id:|
479
+ @next_batch = id if id
480
+ case event.to_sym
481
+ when :sync
482
+ handle_sync_response(data)
483
+ errors = 0
484
+ when :sync_error
485
+ logger.error "SSE Sync error received; #{data.type}: #{data.message}"
486
+ errors += 1
487
+
488
+ # TODO: Allow configuring
489
+ raise 'Aborting due to excessive errors' if errors >= 5
490
+ end
491
+ end
492
+
493
+ @should_listen = cancel_token
494
+ else
495
+ thread = Thread.new { listen_forever(**params) }
496
+ end
497
+ @sync_thread = thread
498
+ thread.run
499
+ end
500
+
501
+ # Stops the running background thread if one is active
502
+ def stop_listener_thread
503
+ return unless @sync_thread
504
+
505
+ if @should_listen.is_a? Hash
506
+ @should_listen[:run] = false
507
+ else
508
+ @should_listen = false
509
+ end
510
+
511
+ if @sync_thread.alive?
512
+ ret = @sync_thread.join(0.1)
513
+ @sync_thread.kill unless ret
514
+ end
515
+ @sync_thread = nil
516
+ end
517
+
518
+ # Check if there's a thread listening for events
519
+ def listening?
520
+ @sync_thread&.alive? == true
521
+ end
522
+
523
+ # Run a message sync round, triggering events as necessary
524
+ #
525
+ # @param skip_store_batch [Boolean] Should this sync skip storing the returned next_batch token,
526
+ # doing this would mean the next sync re-runs from the same point. Useful with use of filters.
527
+ # @param params [Hash] Additional options
528
+ # @option params [String,Hash] :filter (#sync_filter) A filter to use for this sync
529
+ # @option params [Numeric] :timeout (30) A timeout value in seconds for the sync request
530
+ # @option params [Numeric] :allow_sync_retry (0) The number of retries allowed for this sync request
531
+ # @option params [String] :since An override of the "since" token to provide to the sync request
532
+ # @see Protocols::CS#sync
533
+ def sync(skip_store_batch: false, **params)
534
+ extra_params = {
535
+ filter: sync_filter,
536
+ timeout: 30
537
+ }
538
+ extra_params[:since] = @next_batch unless @next_batch.nil?
539
+ extra_params.merge!(params)
540
+ extra_params[:filter] = extra_params[:filter].to_json unless extra_params[:filter].is_a? String
541
+
542
+ attempts = 0
543
+ data = loop do
544
+ break api.sync(**extra_params)
545
+ rescue MatrixSdk::MatrixTimeoutError => e
546
+ raise e if (attempts += 1) >= params.fetch(:allow_sync_retry, 0)
547
+ end
548
+
549
+ @next_batch = data[:next_batch] unless skip_store_batch
550
+
551
+ handle_sync_response(data)
552
+ true
553
+ end
554
+
555
+ alias listen_for_events sync
556
+
557
+ # Ensures that a room exists in the cache
558
+ #
559
+ # @param room_id [String,MXID] The room ID to ensure
560
+ # @return [Room] The room object for the requested room
561
+ def ensure_room(room_id)
562
+ room_id = MXID.new room_id.to_s unless room_id.is_a? MXID
563
+ raise ArgumentError, 'Must be a room ID' unless room_id.room_id?
564
+
565
+ room_id = room_id.to_s
566
+ ret = @rooms.fetch(room_id) do
567
+ room = Room.new(self, room_id)
568
+ @rooms[room_id] = room unless cache == :none
569
+ room
570
+ end
571
+ # Need to figure out a way to handle multiple types
572
+ ret = @rooms[room_id] = ret.to_space if ret.instance_variable_get :@room_type
573
+ ret
574
+ end
575
+
576
+ def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 0, **params)
577
+ orig_bad_sync_timeout = bad_sync_timeout + 0
578
+ while @should_listen
579
+ begin
580
+ sync(**params.merge(timeout: timeout))
581
+ return unless @should_listen
582
+
583
+ bad_sync_timeout = orig_bad_sync_timeout
584
+ sleep(sync_interval) if sync_interval.positive?
585
+ rescue MatrixRequestError => e
586
+ return unless @should_listen
587
+
588
+ logger.warn("A #{e.class} occurred during sync")
589
+ if e.httpstatus >= 500
590
+ logger.warn("Serverside error, retrying in #{bad_sync_timeout} seconds...")
591
+ sleep(bad_sync_timeout) if bad_sync_timeout.positive? # rubocop:disable Metrics/BlockNesting
592
+ bad_sync_timeout = [bad_sync_timeout * 2, @bad_sync_timeout_limit].min
593
+ end
594
+ end
595
+ end
596
+ rescue StandardError => e
597
+ logger.error "Unhandled #{e.class} raised in background listener"
598
+ logger.error [e.message, *e.backtrace].join($RS)
599
+ fire_error(ErrorEvent.new(e, :listener_thread))
600
+ end
601
+
602
+ private
603
+
604
+ def post_authentication(data)
605
+ @mxid = MXID.new data[:user_id]
606
+ @api.access_token = data[:access_token]
607
+ @api.device_id = data[:device_id]
608
+ @api.homeserver = data[:home_server]
609
+ access_token
610
+ end
611
+
612
+ def handle_state(room_id, state_event)
613
+ return unless state_event.key? :type
614
+
615
+ on_state_event.fire(MatrixEvent.new(self, state_event), state_event[:type])
616
+
617
+ room = ensure_room(room_id)
618
+ room.send :put_state_event, state_event
619
+ end
620
+
621
+ def handle_sync_response(data)
622
+ data.dig(:account_data, :events)&.each do |account_data|
623
+ if cache != :none
624
+ adapter = self.account_data.tinycache_adapter
625
+ adapter.write(account_data[:type], account_data[:content], expires_in: self.account_data.cache_time)
626
+ end
627
+ fire_account_data(MatrixEvent.new(self, account_data))
628
+ end
629
+
630
+ data.dig(:presence, :events)&.each do |presence_update|
631
+ fire_presence_event(MatrixEvent.new(self, presence_update))
632
+ end
633
+
634
+ data.dig(:rooms, :invite)&.each do |room_id, invite|
635
+ invite[:room_id] = room_id.to_s
636
+ fire_invite_event(MatrixEvent.new(self, invite), room_id.to_s)
637
+ end
638
+
639
+ data.dig(:rooms, :leave)&.each do |room_id, left|
640
+ left[:room_id] = room_id.to_s
641
+ fire_leave_event(MatrixEvent.new(self, left), room_id.to_s)
642
+ end
643
+
644
+ data.dig(:rooms, :join)&.each do |room_id, join|
645
+ room = ensure_room(room_id)
646
+ room.instance_variable_set '@prev_batch', join.dig(:timeline, :prev_batch)
647
+ room.instance_variable_set :@members_loaded, true unless sync_filter.fetch(:room, {}).fetch(:state, {}).fetch(:lazy_load_members, false)
648
+
649
+ join.dig(:account_data, :events)&.each do |account_data|
650
+ account_data[:room_id] = room_id.to_s
651
+ room.send :put_account_data, account_data
652
+
653
+ fire_account_data(MatrixEvent.new(self, account_data))
654
+ end
655
+
656
+ join.dig(:state, :events)&.each do |event|
657
+ event[:room_id] = room_id.to_s
658
+ handle_state(room_id, event)
659
+ end
660
+
661
+ join.dig(:timeline, :events)&.each do |event|
662
+ event[:room_id] = room_id.to_s
663
+ # Avoid sending two identical state events if it's both in state and timeline
664
+ if event.key?(:state_key)
665
+ state_event = join.dig(:state, :events)&.find { |ev| ev[:event_id] == event[:event_id] }
666
+
667
+ handle_state(room_id, event) unless event == state_event
668
+ end
669
+ room.send :put_event, event
670
+
671
+ fire_event(MatrixEvent.new(self, event), event[:type])
672
+ end
673
+
674
+ join.dig(:ephemeral, :events)&.each do |event|
675
+ event[:room_id] = room_id.to_s
676
+ room.send :put_ephemeral_event, event
677
+
678
+ fire_ephemeral_event(MatrixEvent.new(self, event), event[:type])
679
+ end
680
+ end
681
+
682
+ unless cache == :none
683
+ account_data.tinycache_adapter.cleanup if instance_variable_defined?(:@account_data) && @account_data
684
+ @rooms.each do |_id, room|
685
+ # Clean up old cache data after every sync
686
+ # TODO Run this in a thread?
687
+ room.tinycache_adapter.cleanup
688
+ room.account_data.tinycache_adapter.cleanup
689
+ room.room_state.tinycache_adapter.cleanup
690
+ end
691
+ end
692
+
693
+ nil
694
+ end
695
+ end
696
+ end