matrix_sdk 2.1.0 → 2.3.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: 88e56c3775bf881fb720e4e262ff6073136204d26b72c03bb1bbd2a4138b90ee
4
- data.tar.gz: c8f5223c4c0c4febda391e8320a90667b6438b92806ce0b9b7c251a506678730
3
+ metadata.gz: fb2cd8492e977a8c63d80d3a250841111f872b45ef48a06ac610dd6c871c5704
4
+ data.tar.gz: b5932dc63938f86a4bec522f2d5736871f3a5461ad5d25581c2ba1cd79b20ab2
5
5
  SHA512:
6
- metadata.gz: 7ce44dc75ea446ff9f901d8309f58df4251c2c199540d27f086fbb77359974b5950da8c960540e3d7e89d1a40b7f264dfb9f1c5ad8462c4e94176fcb19fedc60
7
- data.tar.gz: 0b2acb16c36524bac00528fe322984a587f8f0c951754d316148fbd337a5f0dafec6ce5daf3cdc5829673115bb96453b60475702633ca1e11718d5e78c8e37b0
6
+ metadata.gz: 0ed57a7d4b3296a15c244c88ca88777d6ed3f9b67a6c4cdaae017f56c6e15cf94ba0781c6831a70f7adf9c55605073c84c451b0c1b0b8bffe323015330294f19
7
+ data.tar.gz: f77aaa05a45917b80aa296b7ff73f37d60ba5fb36f29a3e753cec1a730681a428c33ccdae9f4c26648952d6a7b05f9636e7d7ffbbe8757d23d2c435378d3466b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,40 @@
1
+ ## 2.3.0 - 2021-03-26
2
+
3
+ - Adds support for Ruby 3.0 (#15)
4
+ - Adds support for requests against the Synapse admin API
5
+ - Adds helper methods for checking and changing user power levels
6
+ - Adds a proper caching system for room data
7
+ - Fixes argument error in `#get_room_messages`
8
+ - Removes unfinished and broken AS abstraction
9
+
10
+ ## 2.2.0 - 2020-11-20
11
+
12
+ - Adds direct message (1:1) room mapping to client abstraction
13
+ - Adds Api#get_room_event_context (#13)
14
+ - Improves support for JRuby
15
+
16
+ ## 2.1.3 - 2020-09-18
17
+
18
+ - Adds separate state event handler as Client#on_state_event
19
+ - Changes Client sync interval to by-default run at full speed
20
+ - Fixes state events being sent twice if included in both timeline and state of a sync
21
+ - Improves error reporting of broken 200 responses
22
+ - Improves event handlers for rooms, to not depend on a specific room object instance anymore
23
+
24
+ ## 2.1.2 - 2020-09-10
25
+
26
+ - Adds method for reading complete member lists for rooms, improves the CS spec adherence
27
+ - Adds test for state events
28
+ - Fixes state event handler for rooms not actually passing events
29
+ - Fixes Api#new_for_domain using a faulty URI in certain cases
30
+
31
+ ## 2.1.1 - 2020-08-21
32
+
33
+ - Fixes crash if state event content is null (#11)
34
+ - Fixes an uninitialized URI constant exception when requiring only the main library file
35
+ - Fixes the Api#get_pushrules method missing an ending slash in the request URI
36
+ - Fixes discovery code for client/server connections based on domain
37
+
1
38
  ## 2.1.0 - 2020-05-22
2
39
 
3
40
  - Adds unique query IDs as well as duration in API debug output, to make it easier to track long requests
data/README.md CHANGED
@@ -4,7 +4,7 @@ A Ruby gem for easing the development of software that communicates with servers
4
4
 
5
5
  There is a Matrix room for the discussion about usage and development at [#ruby-matrix-sdk:kittenface.studio](https://matrix.to/#/#ruby-matrix-sdk:kittenface.studio).
6
6
 
7
- Live YARD documentation can be found at; http://aleol57.gitlab-pages.liu.se/ruby-matrix-sdk
7
+ Live YARD documentation can be found at; https://ruby-sdk.ananace.dev
8
8
 
9
9
  ## Example usage
10
10
 
data/lib/matrix_sdk.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'matrix_sdk/extensions'
3
+ require 'matrix_sdk/util/extensions'
4
4
  require 'matrix_sdk/version'
5
5
 
6
6
  require 'json'
@@ -22,6 +22,11 @@ module MatrixSdk
22
22
  autoload :MatrixTimeoutError, 'matrix_sdk/errors'
23
23
  autoload :MatrixUnexpectedResponseError, 'matrix_sdk/errors'
24
24
 
25
+ module Util
26
+ autoload :Tinycache, 'matrix_sdk/util/tinycache'
27
+ autoload :TinycacheAdapter, 'matrix_sdk/util/tinycache_adapter'
28
+ end
29
+
25
30
  module Protocols
26
31
  autoload :AS, 'matrix_sdk/protocols/as'
27
32
  autoload :CS, 'matrix_sdk/protocols/cs'
@@ -38,6 +38,7 @@ module MatrixSdk
38
38
  # @option params [Numeric] :read_timeout (240) The timeout in seconds for reading responses
39
39
  # @option params [Hash] :global_headers Additional headers to set for all requests
40
40
  # @option params [Boolean] :skip_login Should the API skip logging in if the HS URL contains user information
41
+ # @option params [Boolean] :synapse (true) Is the API connecting to a Synapse instance
41
42
  # @option params [Hash] :well_known The .well-known object that the server was discovered through, should not be set manually
42
43
  def initialize(homeserver, **params)
43
44
  @homeserver = homeserver
@@ -61,6 +62,7 @@ module MatrixSdk
61
62
  @well_known = params.fetch(:well_known, {})
62
63
  @global_headers = DEFAULT_HEADERS.dup
63
64
  @global_headers.merge!(params.fetch(:global_headers)) if params.key? :global_headers
65
+ @synapse = params.fetch(:synapse, true)
64
66
  @http = nil
65
67
 
66
68
  ([params.fetch(:protocols, [:CS])].flatten - protocols).each do |proto|
@@ -92,6 +94,8 @@ module MatrixSdk
92
94
  uri = URI("http#{ssl ? 's' : ''}://#{domain}")
93
95
  well_known = nil
94
96
  target_uri = nil
97
+ logger = ::Logging.logger[self]
98
+ logger.debug "Resolving #{domain}"
95
99
 
96
100
  if !port.nil? && !port.empty?
97
101
  # If the domain is fully qualified according to Matrix (FQDN and port) then skip discovery
@@ -99,21 +103,30 @@ module MatrixSdk
99
103
  elsif target == :server
100
104
  # Attempt SRV record discovery
101
105
  target_uri = begin
102
- require 'resolv'
103
- resolver = Resolv::DNS.new
104
- resolver.getresource("_matrix._tcp.#{domain}")
105
- rescue StandardError
106
- nil
107
- end
106
+ require 'resolv'
107
+ resolver = Resolv::DNS.new
108
+ srv = "_matrix._tcp.#{domain}"
109
+ logger.debug "Trying DNS #{srv}..."
110
+ d = resolver.getresource(srv, Resolv::DNS::Resource::IN::SRV)
111
+ d
112
+ rescue StandardError => e
113
+ logger.debug "DNS lookup failed with #{e.class}: #{e.message}"
114
+ nil
115
+ end
108
116
 
109
117
  if target_uri.nil?
110
118
  # Attempt .well-known discovery for server-to-server
111
119
  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
120
+ wk_uri = URI("https://#{domain}/.well-known/matrix/server")
121
+ logger.debug "Trying #{wk_uri}..."
122
+ data = Net::HTTP.start(wk_uri.host, wk_uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
123
+ http.get(wk_uri.path).body
124
+ end
125
+ JSON.parse(data)
126
+ rescue StandardError => e
127
+ logger.debug "Well-known failed with #{e.class}: #{e.message}"
128
+ nil
129
+ end
117
130
 
118
131
  target_uri = well_known['m.server'] if well_known&.key?('m.server')
119
132
  else
@@ -122,11 +135,16 @@ module MatrixSdk
122
135
  elsif %i[client identity].include? target
123
136
  # Attempt .well-known discovery
124
137
  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
138
+ wk_uri = URI("https://#{domain}/.well-known/matrix/client")
139
+ logger.debug "Trying #{wk_uri}..."
140
+ data = Net::HTTP.start(wk_uri.host, wk_uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
141
+ http.get(wk_uri.path).body
142
+ end
143
+ JSON.parse(data)
144
+ rescue StandardError => e
145
+ logger.debug "Well-known failed with #{e.class}: #{e.message}"
146
+ nil
147
+ end
130
148
 
131
149
  if well_known
132
150
  key = 'm.homeserver'
@@ -138,17 +156,20 @@ module MatrixSdk
138
156
  end
139
157
  end
140
158
  end
159
+ logger.debug "Using #{target_uri.inspect}"
141
160
 
142
161
  # Fall back to direct domain connection
143
162
  target_uri ||= URI("https://#{domain}:8448")
144
163
 
145
164
  params[:well_known] = well_known if keep_wellknown
146
165
 
147
- new(uri,
148
- params.merge(
149
- address: target_uri.host,
150
- port: target_uri.port
151
- ))
166
+ new(
167
+ uri,
168
+ **params.merge(
169
+ address: target_uri.host,
170
+ port: target_uri.port
171
+ )
172
+ )
152
173
  end
153
174
 
154
175
  # Get a list of enabled protocols on the API client
@@ -251,23 +272,6 @@ module MatrixSdk
251
272
  u.query = [u.query, URI.encode_www_form(options.fetch(:query))].flatten.compact.join('&') if options[:query]
252
273
  u.query = nil if u.query.nil? || u.query.empty?
253
274
  end
254
- request = Net::HTTP.const_get(method.to_s.capitalize.to_sym).new url.request_uri
255
- request.body = options[:body] if options.key? :body
256
- request.body = request.body.to_json if options.key?(:body) && !request.body.is_a?(String)
257
- request.body_stream = options[:body_stream] if options.key? :body_stream
258
-
259
- global_headers.each { |h, v| request[h] = v }
260
- if request.body || request.body_stream
261
- request.content_type = 'application/json'
262
- request.content_length = (request.body || request.body_stream).size
263
- end
264
-
265
- request['authorization'] = "Bearer #{access_token}" if access_token && !options.fetch(:skip_auth, false)
266
- if options.key? :headers
267
- options[:headers].each do |h, v|
268
- request[h.to_s.downcase] = v
269
- end
270
- end
271
275
 
272
276
  failures = 0
273
277
  loop do
@@ -275,10 +279,11 @@ module MatrixSdk
275
279
 
276
280
  req_id = ('A'..'Z').to_a.sample(4).join
277
281
 
278
- print_http(request, id: req_id)
282
+ req_obj = construct_request(url: url, method: method, **options)
283
+ print_http(req_obj, id: req_id)
279
284
  begin
280
285
  dur_start = Time.now
281
- response = http.request request
286
+ response = http.request req_obj
282
287
  dur_end = Time.now
283
288
  duration = dur_end - dur_start
284
289
  rescue EOFError
@@ -287,7 +292,12 @@ module MatrixSdk
287
292
  end
288
293
  print_http(response, duration: duration, id: req_id)
289
294
 
290
- data = JSON.parse(response.body, symbolize_names: true) rescue nil
295
+ begin
296
+ data = JSON.parse(response.body, symbolize_names: true)
297
+ rescue JSON::JSONError => e
298
+ logger.debug "#{e.class} error when parsing response. #{e}"
299
+ data = nil
300
+ end
291
301
 
292
302
  if response.is_a? Net::HTTPTooManyRequests
293
303
  raise MatrixRequestError.new_by_code(data, response.code) unless autoretry
@@ -298,7 +308,13 @@ module MatrixSdk
298
308
  next
299
309
  end
300
310
 
301
- return MatrixSdk::Response.new self, data if response.is_a? Net::HTTPSuccess
311
+ if response.is_a? Net::HTTPSuccess
312
+ unless data
313
+ logger.error "Received non-parsable data in 200 response; #{response.body.inspect}"
314
+ raise MatrixConnectionError, response
315
+ end
316
+ return MatrixSdk::Response.new self, data
317
+ end
302
318
  raise MatrixRequestError.new_by_code(data, response.code) if data
303
319
 
304
320
  raise MatrixConnectionError.class_by_code(response.code), response
@@ -316,14 +332,38 @@ module MatrixSdk
316
332
 
317
333
  private
318
334
 
335
+ def construct_request(method:, url:, **options)
336
+ request = Net::HTTP.const_get(method.to_s.capitalize.to_sym).new url.request_uri
337
+
338
+ # FIXME: Handle bodies better, avoid duplicating work
339
+ request.body = options[:body] if options.key? :body
340
+ request.body = request.body.to_json if options.key?(:body) && !request.body.is_a?(String)
341
+ request.body_stream = options[:body_stream] if options.key? :body_stream
342
+
343
+ global_headers.each { |h, v| request[h] = v }
344
+ if request.body || request.body_stream
345
+ request.content_type = 'application/json'
346
+ request.content_length = (request.body || request.body_stream).size
347
+ end
348
+
349
+ request['authorization'] = "Bearer #{access_token}" if access_token && !options.fetch(:skip_auth, false)
350
+ if options.key? :headers
351
+ options[:headers].each do |h, v|
352
+ request[h.to_s.downcase] = v
353
+ end
354
+ end
355
+
356
+ request
357
+ end
358
+
319
359
  def print_http(http, body: true, duration: nil, id: nil)
320
360
  return unless logger.debug?
321
361
 
322
362
  if http.is_a? Net::HTTPRequest
323
- dir = "#{id ? id + ' : ' : nil}>"
363
+ dir = "#{id ? "#{id} : " : nil}>"
324
364
  logger.debug "#{dir} Sending a #{http.method} request to `#{http.path}`:"
325
365
  else
326
- dir = "#{id ? id + ' : ' : nil}<"
366
+ dir = "#{id ? "#{id} : " : nil}<"
327
367
  logger.debug "#{dir} Received a #{http.code} #{http.message} response:#{duration ? " [#{(duration * 1000).to_i}ms]" : nil}"
328
368
  end
329
369
  http.to_hash.map { |k, v| "#{k}: #{k == 'authorization' ? '[ REDACTED ]' : v.join(', ')}" }.each do |h|
@@ -332,7 +372,7 @@ module MatrixSdk
332
372
  logger.debug dir
333
373
  if body
334
374
  clean_body = JSON.parse(http.body) rescue nil if http.body
335
- clean_body.keys.each { |k| clean_body[k] = '[ REDACTED ]' if %w[password access_token].include?(k) }.to_json if clean_body.is_a? Hash
375
+ clean_body.each_key { |k| clean_body[k] = '[ REDACTED ]' if %w[password access_token].include?(k) }.to_json if clean_body.is_a? Hash
336
376
  clean_body = clean_body.to_s if clean_body
337
377
  logger.debug "#{dir} #{clean_body.length < 200 ? clean_body : clean_body.slice(0..200) + "... [truncated, #{clean_body.length} Bytes]"}" if clean_body
338
378
  end
@@ -341,6 +381,8 @@ module MatrixSdk
341
381
  end
342
382
 
343
383
  def api_to_path(api)
384
+ return "/_synapse/#{api.to_s.split('_').join('/')}" if @synapse && api.to_s.start_with?('admin_')
385
+
344
386
  # TODO: <api>_current / <api>_latest
345
387
  "/_matrix/#{api.to_s.split('_').join('/')}"
346
388
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'matrix_sdk'
4
+ require 'matrix_sdk/util/events'
4
5
 
5
6
  require 'English'
6
7
  require 'forwardable'
@@ -23,7 +24,7 @@ module MatrixSdk
23
24
  attr_reader :api, :next_batch
24
25
  attr_accessor :cache, :sync_filter
25
26
 
26
- events :error, :event, :presence_event, :invite_event, :leave_event, :ephemeral_event
27
+ events :error, :event, :presence_event, :invite_event, :leave_event, :ephemeral_event, :state_event
27
28
  ignore_inspect :api,
28
29
  :on_event, :on_presence_event, :on_invite_event, :on_leave_event, :on_ephemeral_event
29
30
 
@@ -44,7 +45,7 @@ module MatrixSdk
44
45
  # @see #initialize
45
46
  def self.new_for_domain(domain, **params)
46
47
  api = MatrixSdk::Api.new_for_domain(domain, keep_wellknown: true)
47
- return new(api, params) unless api.well_known.key? 'm.identity_server'
48
+ return new(api, params) unless api.well_known&.key?('m.identity_server')
48
49
 
49
50
  identity_server = MatrixSdk::Api.new(api.well_known['m.identity_server']['base_url'], protocols: %i[IS])
50
51
  new(api, params.merge(identity_server: identity_server))
@@ -67,11 +68,9 @@ module MatrixSdk
67
68
  api.instance_variable_set("@#{k}", v) if api.instance_variable_defined? "@#{k}"
68
69
  end
69
70
  else
70
- @api = Api.new hs_url, params
71
+ @api = Api.new hs_url, **params
71
72
  end
72
73
 
73
- @rooms = {}
74
- @users = {}
75
74
  @cache = client_cache
76
75
  @identity_server = nil
77
76
 
@@ -79,7 +78,6 @@ module MatrixSdk
79
78
  @sync_thread = nil
80
79
  @sync_filter = { room: { timeline: { limit: params.fetch(:sync_filter_limit, 20) }, state: { lazy_load_members: true } } }
81
80
 
82
- @should_listen = false
83
81
  @next_batch = nil
84
82
 
85
83
  @bad_sync_timeout_limit = 60 * 60
@@ -88,6 +86,11 @@ module MatrixSdk
88
86
  instance_variable_set("@#{k}", v) if instance_variable_defined? "@#{k}"
89
87
  end
90
88
 
89
+ @rooms = {}
90
+ @room_handlers = {}
91
+ @users = {}
92
+ @should_listen = false
93
+
91
94
  raise ArgumentError, 'Cache value must be one of of [:all, :some, :none]' unless %i[all some none].include? @cache
92
95
 
93
96
  return unless params[:user_id]
@@ -154,6 +157,27 @@ module MatrixSdk
154
157
  rooms
155
158
  end
156
159
 
160
+ # Gets a list of all direct chat rooms (1:1 chats / direct message chats) for the currenct user
161
+ #
162
+ # @return [Hash[String,Array[String]]] A mapping of MXIDs to a list of direct rooms with that user
163
+ def direct_rooms
164
+ Hash[api.get_account_data(mxid, 'm.direct').map do |mxid, rooms|
165
+ [mxid.to_s, rooms]
166
+ end]
167
+ end
168
+
169
+ # Gets a direct message room for the given user if one exists
170
+ #
171
+ # @note Will return the oldest room if multiple exist
172
+ # @return [Room,nil] A direct message room if one exists
173
+ def direct_room(mxid)
174
+ mxid = MatrixSdk::MXID.new mxid.to_s unless mxid.is_a? MatrixSdk::MXID
175
+ raise ArgumentError, 'Must be a valid user ID' unless mxid.user?
176
+
177
+ room_id = direct_rooms[mxid.to_s]&.first
178
+ ensure_room room_id if room_id
179
+ end
180
+
157
181
  # Gets a list of all relevant rooms, either the ones currently handled by
158
182
  # the client, or the list of currently joined ones if no rooms are handled
159
183
  #
@@ -337,13 +361,13 @@ module MatrixSdk
337
361
  # @return [Room] The resulting room
338
362
  # @see Protocols::CS#create_room
339
363
  def create_room(room_alias = nil, **params)
340
- data = api.create_room(params.merge(room_alias: room_alias))
364
+ data = api.create_room(**params.merge(room_alias: room_alias))
341
365
  ensure_room(data.room_id)
342
366
  end
343
367
 
344
368
  # Joins an already created room
345
369
  #
346
- # @param room_id_or_alias [String,MXID] A room alias (#room:exmaple.com) or a room ID (!id:example.com)
370
+ # @param room_id_or_alias [String,MXID] A room alias (#room:example.com) or a room ID (!id:example.com)
347
371
  # @param server_name [Array[String]] A list of servers to attempt the join through, required for IDs
348
372
  # @return [Room] The resulting room
349
373
  # @see Protocols::CS#join_room
@@ -431,10 +455,11 @@ module MatrixSdk
431
455
  errors = 0
432
456
  thread, cancel_token = api.msc2108_sync_sse(params) do |data, event:, id:|
433
457
  @next_batch = id if id
434
- if event.to_sym == :sync
458
+ case event.to_sym
459
+ when :sync
435
460
  handle_sync_response(data)
436
461
  errors = 0
437
- elsif event.to_sym == :sync_error
462
+ when :sync_error
438
463
  logger.error "SSE Sync error received; #{data.type}: #{data.message}"
439
464
  errors += 1
440
465
 
@@ -445,7 +470,7 @@ module MatrixSdk
445
470
 
446
471
  @should_listen = cancel_token
447
472
  else
448
- thread = Thread.new { listen_forever(params) }
473
+ thread = Thread.new { listen_forever(**params) }
449
474
  end
450
475
  @sync_thread = thread
451
476
  thread.run
@@ -494,7 +519,7 @@ module MatrixSdk
494
519
  attempts = 0
495
520
  data = loop do
496
521
  begin
497
- break api.sync extra_params
522
+ break api.sync **extra_params
498
523
  rescue MatrixSdk::MatrixTimeoutError => e
499
524
  raise e if (attempts += 1) >= params.fetch(:allow_sync_retry, 0)
500
525
  end
@@ -524,11 +549,11 @@ module MatrixSdk
524
549
  end
525
550
  end
526
551
 
527
- def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 30, **params)
552
+ def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 0, **params)
528
553
  orig_bad_sync_timeout = bad_sync_timeout + 0
529
554
  while @should_listen
530
555
  begin
531
- sync(params.merge(timeout: timeout))
556
+ sync(**params.merge(timeout: timeout))
532
557
 
533
558
  bad_sync_timeout = orig_bad_sync_timeout
534
559
  sleep(sync_interval) if sync_interval.positive?
@@ -560,34 +585,10 @@ module MatrixSdk
560
585
  def handle_state(room_id, state_event)
561
586
  return unless state_event.key? :type
562
587
 
588
+ on_state_event.fire(MatrixEvent.new(self, state_event), state_event[:type])
589
+
563
590
  room = ensure_room(room_id)
564
- content = state_event[:content]
565
- case state_event[:type]
566
- when 'm.room.name'
567
- room.instance_variable_set '@name', content[:name]
568
- when 'm.room.canonical_alias'
569
- room.instance_variable_set '@canonical_alias', content[:alias]
570
- # Also add as a regular alias
571
- room.instance_variable_get('@aliases').concat [content[:alias]]
572
- when 'm.room.topic'
573
- room.instance_variable_set '@topic', content[:topic]
574
- when 'm.room.aliases'
575
- room.instance_variable_get('@aliases').concat content[:aliases]
576
- when 'm.room.join_rules'
577
- room.instance_variable_set '@join_rule', content[:join_rule].to_sym
578
- when 'm.room.guest_access'
579
- room.instance_variable_set '@guest_access', content[:guest_access].to_sym
580
- when 'm.room.member'
581
- return unless cache == :all
582
-
583
- if content[:membership] == 'join'
584
- room.send(:ensure_member, get_user(state_event[:state_key]).dup.tap do |u|
585
- u.instance_variable_set :@display_name, content[:displayname]
586
- end)
587
- elsif %w[leave kick invite].include? content[:membership]
588
- room.members.delete_if { |m| m.id == state_event[:state_key] }
589
- end
590
- end
591
+ room.send :put_state_event, state_event
591
592
  end
592
593
 
593
594
  def handle_sync_response(data)
@@ -617,7 +618,12 @@ module MatrixSdk
617
618
 
618
619
  join[:timeline][:events].each do |event|
619
620
  event[:room_id] = room_id.to_s
620
- handle_state(room_id, event) unless event[:type] == 'm.room.message'
621
+ # Avoid sending two identical state events if it's both in state and timeline
622
+ if event.key?(:state_key)
623
+ state_event = join.dig(:state, :events).find { |ev| ev[:event_id] == event[:event_id] }
624
+
625
+ handle_state(room_id, event) unless event == state_event
626
+ end
621
627
  room.send :put_event, event
622
628
 
623
629
  fire_event(MatrixEvent.new(self, event), event[:type])
@@ -631,6 +637,14 @@ module MatrixSdk
631
637
  end
632
638
  end
633
639
 
640
+ unless cache == :none
641
+ @rooms.each do |_id, room|
642
+ # Clean up old cache data after every sync
643
+ # TODO Run this in a thread?
644
+ room.tinycache_adapter.cleanup
645
+ end
646
+ end
647
+
634
648
  nil
635
649
  end
636
650
  end