matrix_sdk 2.1.3 → 2.5.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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MatrixSdk::Rooms
4
+ class Space < MatrixSdk::Room
5
+ TYPE = 'm.space'
6
+
7
+ def tree(suggested_only: nil, max_rooms: nil)
8
+ begin
9
+ data = client.api.request :get, :client_unstable, "/org.matrix.msc2946/rooms/#{id}/spaces", query: {
10
+ suggested_only: suggested_only,
11
+ max_rooms_per_space: max_rooms
12
+ }.compact
13
+ rescue MatrixRequestError
14
+ data = client.api.request :get, :client_r0, "/rooms/#{id}/spaces", query: {
15
+ suggested_only: suggested_only,
16
+ max_rooms_per_space: max_rooms
17
+ }.compact
18
+ end
19
+
20
+ rooms = data.rooms.map do |r|
21
+ next if r[:room_id] == id
22
+
23
+ room = client.ensure_room(r[:room_id])
24
+ room.instance_variable_set :@room_type, r[:room_type] if r.key? :room_type
25
+ room = room.to_space if room.space?
26
+
27
+ # Inject available room information
28
+ r.each do |k, v|
29
+ if room.respond_to?("#{k}_cached?".to_sym) && send("#{k}_cached?".to_sym)
30
+ room.send(:tinycache_adapter).write(k, v)
31
+ elsif room.instance_variable_defined? "@#{k}"
32
+ room.instance_variable_set("@#{k}", v)
33
+ end
34
+ end
35
+ room
36
+ end
37
+ rooms.compact!
38
+
39
+ grouping = {}
40
+ data.events.each do |ev|
41
+ next unless ev[:type] == 'm.space.child'
42
+ next unless ev[:content].key? :via
43
+
44
+ d = (grouping[ev[:room_id]] ||= [])
45
+ d << ev[:state_key]
46
+ end
47
+
48
+ build_tree = proc do |entry|
49
+ next if entry.nil?
50
+
51
+ room = self if entry == id
52
+ room ||= rooms.find { |r| r.id == entry }
53
+ puts "Unable to find room for entry #{entry}" unless room
54
+ # next if room.nil?
55
+
56
+ ret = {
57
+ room => []
58
+ }
59
+
60
+ grouping[entry]&.each do |child|
61
+ if grouping.key?(child)
62
+ ret[room] << build_tree.call(child)
63
+ else
64
+ child_r = self if child == id
65
+ child_r ||= rooms.find { |r| r.id == child }
66
+
67
+ ret[room] << child_r
68
+ end
69
+ end
70
+
71
+ ret[room].compact!
72
+
73
+ ret
74
+ end
75
+
76
+ build_tree.call(id)
77
+ end
78
+ end
79
+ end
@@ -59,7 +59,7 @@ module MatrixSdk
59
59
  # Only works for the current user object, as requested by
60
60
  # client.get_user(:self)
61
61
  #
62
- # @param url [String,URI::MATRIX] the new avatar URL
62
+ # @param url [String,URI::MXC] the new avatar URL
63
63
  # @note Requires a mxc:// URL, check example on
64
64
  # {MatrixSdk::Protocols::CS#set_avatar_url} for how this can be done
65
65
  # @see MatrixSdk::Protocols::CS#set_avatar_url
@@ -74,7 +74,7 @@ module MatrixSdk
74
74
  # @see MatrixSdk::Protocols::CS#get_presence_status
75
75
  # @note This information is not cached in the abstraction layer
76
76
  def presence
77
- raw_presence[:presence].to_sym
77
+ raw_presence[:presence]&.to_sym
78
78
  end
79
79
 
80
80
  # Sets the user's current presence status
@@ -122,10 +122,18 @@ module MatrixSdk
122
122
  Time.now - (since / 1000)
123
123
  end
124
124
 
125
+ # Gets a direct message room with the user if one exists
126
+ #
127
+ # @return [Room,nil] A direct message room if one exists
128
+ # @see MatrixSdk::Client#direct_room
129
+ def direct_room
130
+ client.direct_room(id)
131
+ end
132
+
125
133
  # Returns all the current device keys for the user, retrieving them if necessary
126
134
  def device_keys
127
135
  @device_keys ||= client.api.keys_query(device_keys: { id => [] }).yield_self do |resp|
128
- resp[:device_keys][id.to_sym]
136
+ resp.dig(:device_keys, id.to_sym)
129
137
  end
130
138
  end
131
139
 
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MatrixSdk
4
+ class EventHandlerArray < Hash
5
+ include MatrixSdk::Logging
6
+ attr_accessor :reraise_exceptions
7
+
8
+ def initialize(*args)
9
+ @reraise_exceptions = false
10
+
11
+ super(*args)
12
+ end
13
+
14
+ def add_handler(filter = nil, id = nil, &block)
15
+ id ||= block.hash
16
+ self[id] = { filter: filter, id: id, block: block }
17
+ end
18
+
19
+ def remove_handler(id)
20
+ delete id
21
+ end
22
+
23
+ def fire(event, filter = nil)
24
+ reverse_each do |_k, h|
25
+ h[:block].call(event) if !h[:filter] || event.matches?(h[:filter], filter)
26
+ rescue StandardError => e
27
+ logger.error "#{e.class.name} occurred when firing event (#{event})\n#{e}"
28
+
29
+ raise e if @reraise_exceptions
30
+ end
31
+ end
32
+ end
33
+
34
+ class Event
35
+ extend MatrixSdk::Extensions
36
+
37
+ attr_writer :handled
38
+
39
+ ignore_inspect :sender
40
+
41
+ def initialize(sender)
42
+ @sender = sender
43
+ @handled = false
44
+ end
45
+
46
+ def handled?
47
+ @handled
48
+ end
49
+
50
+ def matches?(_filter)
51
+ true
52
+ end
53
+ end
54
+
55
+ class ErrorEvent < Event
56
+ attr_accessor :error
57
+
58
+ def initialize(error, source)
59
+ @error = error
60
+ super source
61
+ end
62
+
63
+ def source
64
+ @sender
65
+ end
66
+ end
67
+
68
+ class MatrixEvent < Event
69
+ attr_accessor :event, :filter
70
+ alias data event
71
+
72
+ ignore_inspect :sender
73
+
74
+ def initialize(sender, event = nil, filter = nil)
75
+ @event = event
76
+ @filter = filter || @event[:type]
77
+ super sender
78
+ end
79
+
80
+ def matches?(filter, filter_override = nil)
81
+ return true if filter_override.nil? && (@filter.nil? || filter.nil?)
82
+
83
+ to_match = filter_override || @filter
84
+ if filter.is_a? Regexp
85
+ filter.match(to_match) { true } || false
86
+ else
87
+ to_match == filter
88
+ end
89
+ end
90
+
91
+ def [](key)
92
+ event[key]
93
+ end
94
+
95
+ def to_s
96
+ "#{event[:type]}: #{event.reject { |k, _v| k == :type }.to_json}"
97
+ end
98
+
99
+ def method_missing(method, *args)
100
+ return event[method] if event.key? method
101
+
102
+ super
103
+ end
104
+
105
+ def respond_to_missing?(method, *)
106
+ return true if event.key? method
107
+
108
+ super
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless Object.respond_to? :yield_self
4
+ class Object
5
+ def yield_self
6
+ yield(self)
7
+ end
8
+ end
9
+ end
10
+
11
+ module MatrixSdk
12
+ module Extensions
13
+ def events(*symbols)
14
+ module_name = "#{name}Events"
15
+
16
+ initializers = []
17
+ readers = []
18
+ methods = []
19
+
20
+ symbols.each do |sym|
21
+ name = sym.to_s
22
+
23
+ initializers << "
24
+ @on_#{name} = MatrixSdk::EventHandlerArray.new
25
+ "
26
+ readers << ":on_#{name}"
27
+ methods << "
28
+ def fire_#{name}(ev, filter = nil)
29
+ @on_#{name}.fire(ev, filter)
30
+ when_#{name}(ev) if !ev.handled?
31
+ end
32
+
33
+ def when_#{name}(ev); end
34
+ "
35
+ end
36
+
37
+ class_eval "
38
+ module #{module_name}
39
+ attr_reader #{readers.join ', '}
40
+
41
+ def event_initialize
42
+ #{initializers.join}
43
+ end
44
+
45
+ #{methods.join}
46
+ end
47
+
48
+ include #{module_name}
49
+ ", __FILE__, __LINE__ - 12
50
+ end
51
+
52
+ def ignore_inspect(*symbols)
53
+ class_eval %*
54
+ def inspect
55
+ reentrant = caller_locations.any? { |l| l.absolute_path == __FILE__ && l.label == 'inspect' }
56
+ "\\\#<\#{self.class} \#{instance_variables
57
+ .reject { |f| %i[#{symbols.map { |s| "@#{s}" }.join ' '}].include? f }
58
+ .map { |f| "\#{f}=\#{reentrant ? instance_variable_get(f) : instance_variable_get(f).inspect}" }.join " " }}>"
59
+ end
60
+ *, __FILE__, __LINE__ - 7
61
+ end
62
+ end
63
+
64
+ module Logging
65
+ def logger
66
+ return MatrixSdk.logger if MatrixSdk.global_logger?
67
+
68
+ @logger ||= ::Logging.logger[self]
69
+ end
70
+
71
+ def logger=(logger)
72
+ @logger = logger
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'matrix_sdk/util/tinycache_adapter'
4
+
5
+ module MatrixSdk::Util
6
+ module Tinycache
7
+ CACHE_LEVELS = {
8
+ none: 0,
9
+ some: 1,
10
+ all: 2
11
+ }.freeze
12
+
13
+ def self.adapter
14
+ @adapter ||= TinycacheAdapter
15
+ end
16
+
17
+ def self.adapter=(adapter)
18
+ @adapter = adapter
19
+ end
20
+
21
+ def self.extended(base)
22
+ helper_name = base.send(:cache_helper_module_name)
23
+ base.send :remove_const, helper_name if base.const_defined?(helper_name)
24
+ base.prepend base.const_set(helper_name, Module.new)
25
+
26
+ base.include InstanceMethods
27
+ end
28
+
29
+ def cached(*methods, **opts)
30
+ methods.each { |method| build_cache_methods(method, **opts) }
31
+ end
32
+
33
+ module InstanceMethods
34
+ def tinycache_adapter
35
+ @tinycache_adapter ||= Tinycache.adapter.new.tap do |adapter|
36
+ adapter.config = self.class.tinycache_adapter_config if adapter.respond_to? :config=
37
+ adapter.client = client if respond_to?(:client) && adapter.respond_to?(:client=)
38
+ end
39
+ end
40
+ end
41
+
42
+ def tinycache_adapter_config
43
+ @tinycache_adapter_config ||= {}
44
+ end
45
+
46
+ private
47
+
48
+ def default_cache_key
49
+ proc do |method_name, _method_args|
50
+ method_name.to_sym
51
+ end
52
+ end
53
+
54
+ def cache_helper_module_name
55
+ class_name = name&.gsub(/:/, '') || to_s.gsub(/[^a-zA-Z_0-9]/, '')
56
+ "#{class_name}Tinycache"
57
+ end
58
+
59
+ def build_cache_methods(method_name, cache_key: default_cache_key, cache_level: :none, expires_in: nil, **opts)
60
+ raise ArgumentError, 'Cache key must be a three-arg proc' unless cache_key.is_a? Proc
61
+
62
+ method_names = build_method_names(method_name)
63
+ tinycache_adapter_config[method_name] = {
64
+ level: cache_level,
65
+ expires: expires_in || (1 * 365 * 24 * 60 * 60) # 1 year
66
+ }
67
+
68
+ helper = const_get(cache_helper_module_name)
69
+ return if method_names.any? { |k, _| helper.respond_to? k }
70
+
71
+ helper.class_eval do
72
+ define_method(method_names[:cache_key]) do |*args|
73
+ cache_key.call(method_name, args)
74
+ end
75
+
76
+ define_method(method_names[:with_cache]) do |*args|
77
+ tinycache_adapter.fetch(__send__(method_names[:cache_key], *args), expires_in: expires_in) do
78
+ __send__(method_names[:without_cache], *args)
79
+ end
80
+ end
81
+
82
+ define_method(method_names[:without_cache]) do |*args|
83
+ orig = method(method_name).super_method
84
+ orig.call(*args)
85
+ end
86
+
87
+ define_method(method_names[:clear_cache]) do |*args|
88
+ tinycache_adapter.delete(__send__(method_names[:cache_key], *args))
89
+ end
90
+
91
+ define_method(method_names[:cached]) do
92
+ true
93
+ end
94
+
95
+ define_method(method_names[:has_value]) do |*args|
96
+ tinycache_adapter.valid?(__send__(method_names[:cache_key], *args))
97
+ end
98
+
99
+ define_method(method_name) do |*args|
100
+ unless_proc = opts[:unless].is_a?(Symbol) ? opts[:unless].to_proc : opts[:unless]
101
+
102
+ raise ArgumentError, 'Invalid proc provided (must have arity between 1..3)' if unless_proc && !(1..3).include?(unless_proc.arity)
103
+
104
+ skip_cache = false
105
+ skip_cache ||= unless_proc.call(self, method_name, args) if unless_proc&.arity == 3
106
+ skip_cache ||= unless_proc.call(method_name, args) if unless_proc&.arity == 2
107
+ skip_cache ||= unless_proc.call(args) if unless_proc&.arity == 1
108
+ skip_cache ||= CACHE_LEVELS[client&.cache || :all] < CACHE_LEVELS[cache_level]
109
+
110
+ if skip_cache
111
+ __send__(method_names[:without_cache], *args)
112
+ else
113
+ __send__(method_names[:with_cache], *args)
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def build_method_names(method)
120
+ # Clean up method name (split any suffix)
121
+ method_name = method.to_s.sub(/([?!=])$/, '')
122
+ punctuation = Regexp.last_match(-1)
123
+
124
+ {
125
+ cache_key: "#{method_name}_cache_key#{punctuation}",
126
+ with_cache: "#{method_name}_with_cache#{punctuation}",
127
+ without_cache: "#{method_name}_without_cache#{punctuation}",
128
+ clear_cache: "clear_#{method_name}_cache#{punctuation}",
129
+ cached: "#{method}_cached?",
130
+ has_value: "#{method}_has_value?"
131
+ }
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MatrixSdk::Util
4
+ class TinycacheAdapter
5
+ attr_accessor :config, :client
6
+
7
+ def initialize
8
+ @config = {}
9
+
10
+ clear
11
+ end
12
+
13
+ def read(key)
14
+ cache[key]&.value
15
+ end
16
+
17
+ def write(key, value, expires_in: nil, cache_level: nil)
18
+ expires_in ||= config.dig(key, :expires)
19
+ expires_in ||= 24 * 60 * 60
20
+ cache_level ||= client&.cache
21
+ cache_level ||= :all
22
+ cache_level = Tinycache::CACHE_LEVELS[cache_level] unless cache_level.is_a? Integer
23
+
24
+ return value if cache_level < Tinycache::CACHE_LEVELS[config.dig(key, :level) || :none]
25
+
26
+ cache[key] = Value.new(value, Time.now, Time.now + expires_in)
27
+ value
28
+ end
29
+
30
+ def exist?(key)
31
+ cache.key?(key)
32
+ end
33
+
34
+ def valid?(key)
35
+ exist?(key) && !cache[key].expired?
36
+ end
37
+
38
+ def fetch(key, expires_in: nil, cache_level: nil, **_opts)
39
+ expires_in ||= config.dig(key, :expires)
40
+ cache_level ||= client&.cache
41
+ cache_level ||= :all
42
+ cache_level = Tinycache::CACHE_LEVELS[cache_level]
43
+
44
+ return read(key) if exist?(key) && !cache[key].expired?
45
+
46
+ value = yield
47
+ write(key, value, expires_in: expires_in, cache_level: cache_level)
48
+ end
49
+
50
+ def delete(key)
51
+ return false unless exist?(key)
52
+
53
+ cache.delete key
54
+ true
55
+ end
56
+
57
+ def clear
58
+ @cache = {}
59
+ end
60
+
61
+ def cleanup
62
+ @cache.delete_if { |_, v| v.expired? }
63
+ end
64
+
65
+ private
66
+
67
+ Value = Struct.new(:value, :timestamp, :expires_at) do
68
+ def expired?
69
+ return false if expires_at.nil?
70
+
71
+ Time.now > expires_at
72
+ end
73
+ end
74
+
75
+ attr_reader :cache
76
+ end
77
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module URI
6
+ # A mxc:// Matrix content URL
7
+ class MXC < Generic
8
+ def full_path
9
+ select(:host, :port, :path, :query, :fragment)
10
+ .reject(&:nil?)
11
+ .join
12
+ end
13
+ end
14
+
15
+ @@schemes['MXC'] = MXC
16
+
17
+ unless @@schemes.key? 'MATRIX'
18
+ # A matrix: URI according to MSC2312
19
+ class MATRIX < Generic
20
+ attr_reader :authority, :action, :mxid, :mxid2, :via
21
+
22
+ def initialize(*args)
23
+ super(*args)
24
+
25
+ @action = nil
26
+ @authority = nil
27
+ @mxid = nil
28
+ @mxid2 = nil
29
+ @via = nil
30
+
31
+ raise InvalidComponentError, 'missing opaque part for matrix URL' if !@opaque && !@path
32
+
33
+ if @path
34
+ @authority = @host
35
+ @authority += ":#{@port}" if @port
36
+ else
37
+ @path, @query = @opaque.split('?')
38
+ @query, @fragment = @query.split('#') if @query&.include? '#'
39
+ @path, @fragment = @path.split('#') if @path&.include? '#'
40
+ @path = "/#{path}"
41
+ @opaque = nil
42
+ end
43
+
44
+ components = @path.delete_prefix('/').split('/', -1)
45
+ raise InvalidComponentError, 'component count must be 2 or 4' if components.size != 2 && components.size != 4
46
+
47
+ sigil = case components.shift
48
+ when 'u', 'user'
49
+ '@'
50
+ when 'r', 'room'
51
+ '#'
52
+ when 'roomid'
53
+ '!'
54
+ else
55
+ raise InvalidComponentError, 'invalid component in path'
56
+ end
57
+
58
+ component = components.shift
59
+ raise InvalidComponentError, "component can't be empty" if component.nil? || component.empty?
60
+
61
+ @mxid = MatrixSdk::MXID.new("#{sigil}#{component}")
62
+
63
+ if components.size == 2
64
+ sigil2 = case components.shift
65
+ when 'e', 'event'
66
+ '$'
67
+ else
68
+ raise InvalidComponentError, 'invalid component in path'
69
+ end
70
+ component = components.shift
71
+ raise InvalidComponentError, "component can't be empty" if component.nil? || component.empty?
72
+
73
+ @mxid2 = MatrixSdk::MXID.new("#{sigil2}#{component}")
74
+ end
75
+
76
+ return unless @query
77
+
78
+ @action = @query.match(/action=([^&]+)/)&.captures&.first&.to_sym
79
+ @via = @query.scan(/via=([^&]+)/)&.flatten&.compact
80
+ end
81
+
82
+ def mxid2?
83
+ !@mxid2.nil?
84
+ end
85
+ end
86
+
87
+ @@schemes['MATRIX'] = MATRIX
88
+ end
89
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MatrixSdk
4
- VERSION = '2.1.3'
4
+ VERSION = '2.5.0'
5
5
  end
data/lib/matrix_sdk.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'matrix_sdk/extensions'
3
+ require 'matrix_sdk/util/extensions'
4
+ require 'matrix_sdk/util/uri'
4
5
  require 'matrix_sdk/version'
5
6
 
6
7
  require 'json'
@@ -18,10 +19,24 @@ module MatrixSdk
18
19
 
19
20
  autoload :MatrixError, 'matrix_sdk/errors'
20
21
  autoload :MatrixRequestError, 'matrix_sdk/errors'
22
+ autoload :MatrixNotAuthorizedError, 'matrix_sdk/errors'
23
+ autoload :MatrixForbiddenError, 'matrix_sdk/errors'
24
+ autoload :MatrixNotFoundError, 'matrix_sdk/errors'
25
+ autoload :MatrixConflictError, 'matrix_sdk/errors'
26
+ autoload :MatrixTooManyRequestsError, 'matrix_sdk/errors'
21
27
  autoload :MatrixConnectionError, 'matrix_sdk/errors'
22
28
  autoload :MatrixTimeoutError, 'matrix_sdk/errors'
23
29
  autoload :MatrixUnexpectedResponseError, 'matrix_sdk/errors'
24
30
 
31
+ module Rooms
32
+ autoload :Space, 'matrix_sdk/rooms/space'
33
+ end
34
+
35
+ module Util
36
+ autoload :Tinycache, 'matrix_sdk/util/tinycache'
37
+ autoload :TinycacheAdapter, 'matrix_sdk/util/tinycache_adapter'
38
+ end
39
+
25
40
  module Protocols
26
41
  autoload :AS, 'matrix_sdk/protocols/as'
27
42
  autoload :CS, 'matrix_sdk/protocols/cs'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: matrix_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.3
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Olofsson
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-18 00:00:00.000000000 Z
11
+ date: 2022-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mocha
@@ -95,10 +95,8 @@ files:
95
95
  - README.md
96
96
  - lib/matrix_sdk.rb
97
97
  - lib/matrix_sdk/api.rb
98
- - lib/matrix_sdk/application_service.rb
99
98
  - lib/matrix_sdk/client.rb
100
99
  - lib/matrix_sdk/errors.rb
101
- - lib/matrix_sdk/extensions.rb
102
100
  - lib/matrix_sdk/mxid.rb
103
101
  - lib/matrix_sdk/protocols/as.rb
104
102
  - lib/matrix_sdk/protocols/cs.rb
@@ -107,13 +105,19 @@ files:
107
105
  - lib/matrix_sdk/protocols/ss.rb
108
106
  - lib/matrix_sdk/response.rb
109
107
  - lib/matrix_sdk/room.rb
108
+ - lib/matrix_sdk/rooms/space.rb
110
109
  - lib/matrix_sdk/user.rb
110
+ - lib/matrix_sdk/util/events.rb
111
+ - lib/matrix_sdk/util/extensions.rb
112
+ - lib/matrix_sdk/util/tinycache.rb
113
+ - lib/matrix_sdk/util/tinycache_adapter.rb
114
+ - lib/matrix_sdk/util/uri.rb
111
115
  - lib/matrix_sdk/version.rb
112
116
  homepage: https://github.com/ananace/ruby-matrix-sdk
113
117
  licenses:
114
118
  - MIT
115
119
  metadata: {}
116
- post_install_message:
120
+ post_install_message:
117
121
  rdoc_options: []
118
122
  require_paths:
119
123
  - lib
@@ -128,8 +132,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
132
  - !ruby/object:Gem::Version
129
133
  version: '0'
130
134
  requirements: []
131
- rubygems_version: 3.1.2
132
- signing_key:
135
+ rubygems_version: 3.2.22
136
+ signing_key:
133
137
  specification_version: 4
134
138
  summary: SDK for applications using the Matrix protocol
135
139
  test_files: []