matrix_sdk 0.0.4 → 0.1.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,210 @@
1
+ require 'matrix_sdk'
2
+
3
+ module MatrixSdk
4
+ class ApplicationService
5
+ include MatrixSdk::Logging
6
+ attr_reader :api, :port
7
+
8
+ def_delegators :@api,
9
+ :access_token, :access_token=, :device_id, :device_id=, :homeserver, :homeserver=,
10
+ :validate_certificate, :validate_certificate=
11
+
12
+ def initialize(hs_url, as_token:, hs_token:, default_routes: true, **params)
13
+ logger.warning 'This abstraction is still under HEAVY development, expect errors'
14
+
15
+ params = { protocols: %i[AS CS] }.merge(params).merge(access_token: as_token)
16
+ if hs_url.is_a? Api
17
+ @api = hs_url
18
+ params.each do |k, v|
19
+ api.instance_variable_set("@#{k}", v) if api.instance_variable_defined? "@#{k}"
20
+ end
21
+ else
22
+ @api = Api.new hs_url, params
23
+ end
24
+
25
+ @id = params.fetch(:id, MatrixSdk::Api::USER_AGENT)
26
+ @port = params.fetch(:port, 8888)
27
+ @url = params.fetch(:url, URI("http://localhost:#{@port}"))
28
+ @as_token = as_token
29
+ @hs_token = hs_token
30
+
31
+ @method_map = {}
32
+
33
+ if default_routes
34
+ add_method(:GET, '/_matrix/app/v1/users/', %r{^/_matrix/app/v1/users/(?<user>[^/]+)$}, :do_get_user)
35
+ add_method(:GET, '/_matrix/app/v1/rooms/', %r{^/_matrix/app/v1/rooms/(?<room>[^/]+)$}, :do_get_room)
36
+
37
+ add_method(:GET, '/_matrix/app/v1/thirdparty/protocol/', %r{^/_matrix/app/v1/thirdparty/protocol/(?<protocol>[^/]+)$}, :do_get_3p_protocol_p)
38
+ add_method(:GET, '/_matrix/app/v1/thirdparty/user/', %r{^/_matrix/app/v1/thirdparty/user/(?<protocol>[^/]+)$}, :do_get_3p_user_p)
39
+ add_method(:GET, '/_matrix/app/v1/thirdparty/location/', %r{^/_matrix/app/v1/thirdparty/location/(?<protocol>[^/]+)$}, :do_get_3p_location_p)
40
+ add_method(:GET, '/_matrix/app/v1/thirdparty/user', %r{^/_matrix/app/v1/thirdparty/user$}, :do_get_3p_user)
41
+ add_method(:GET, '/_matrix/app/v1/thirdparty/location', %r{^/_matrix/app/v1/thirdparty/location$}, :do_get_3p_location)
42
+
43
+ add_method(:PUT, '/_matrix/app/v1/transactions/', %r{^/_matrix/app/v1/transactions/(?<txn_id>[^/]+)$}, :do_put_transaction)
44
+
45
+ if params.fetch(:legacy_routes, false)
46
+ add_method(:GET, '/users/', %r{^/users/(?<user>[^/]+)$}, :do_get_user)
47
+ add_method(:GET, '/rooms/', %r{^/rooms/(?<room>[^/]+)$}, :do_get_room)
48
+
49
+ add_method(:GET, '/_matrix/app/unstable/thirdparty/protocol/', %r{^/_matrix/app/unstable/thirdparty/protocol/(?<protocol>[^/]+)$}, :do_get_3p_protocol_p)
50
+ add_method(:GET, '/_matrix/app/unstable/thirdparty/user/', %r{^/_matrix/app/unstable/thirdparty/user/(?<protocol>[^/]+)$}, :do_get_3p_user_p)
51
+ add_method(:GET, '/_matrix/app/unstable/thirdparty/location/', %r{^/_matrix/app/unstable/thirdparty/location/(?<protocol>[^/]+)$}, :do_get_3p_location_p)
52
+ add_method(:GET, '/_matrix/app/unstable/thirdparty/user', %r{^/_matrix/app/unstable/thirdparty/user$}, :do_get_3p_user)
53
+ add_method(:GET, '/_matrix/app/unstable/thirdparty/location', %r{^/_matrix/app/unstable/thirdparty/location$}, :do_get_3p_location)
54
+
55
+ add_method(:PUT, '/transactions/', %r{^/transactions/(?<txn_id>[^/]+)$}, :do_put_transaction)
56
+ end
57
+ end
58
+
59
+ start_server
60
+ end
61
+
62
+ def registration
63
+ {
64
+ id: @id,
65
+ url: @url,
66
+ as_token: @as_token,
67
+ hs_token: @hs_token,
68
+ sender_localpart: '',
69
+ namespaces: {
70
+ users: [],
71
+ aliases: [],
72
+ rooms: []
73
+ },
74
+ rate_limited: false,
75
+ protocols: []
76
+ }
77
+ end
78
+
79
+ def port=(port)
80
+ raise ArgumentError, 'Port must be a number' unless port.is_a? Numeric
81
+
82
+ raise NotImplementedError, "Can't change port of a running server" if server.status != :Stop
83
+
84
+ @port = port
85
+ end
86
+
87
+ protected
88
+
89
+ def add_method(verb, prefix, regex, proc = nil, &block)
90
+ proc ||= block
91
+ raise ArgumentError, 'No method specified' if proc.nil?
92
+
93
+ method_entry = (@method_map[verb] ||= {})[regex] = {
94
+ verb: verb,
95
+ prefix: prefix,
96
+ proc: proc
97
+ }
98
+ return true unless @server
99
+
100
+ server.mount_proc(method.prefix) { |req, res| _handle_proc(verb, method_entry, req, res) }
101
+ end
102
+
103
+ def do_get_user(user:, **params)
104
+ [user, params]
105
+ raise NotImplementedError
106
+ end
107
+
108
+ def do_get_room(room:, **params)
109
+ [room, params]
110
+ raise NotImplementedError
111
+ end
112
+
113
+ def do_get_3p_protocol_p(protocol:, **params)
114
+ [protocol, params]
115
+ raise NotImplementedError
116
+ end
117
+
118
+ def do_get_3p_user_p(protocol:, **params)
119
+ [protocol, params]
120
+ raise NotImplementedError
121
+ end
122
+
123
+ def do_get_3p_location_p(protocol:, **params)
124
+ [protocol, params]
125
+ raise NotImplementedError
126
+ end
127
+
128
+ def do_get_3p_location(**params)
129
+ [protocol, params]
130
+ raise NotImplementedError
131
+ end
132
+
133
+ def do_get_3p_user(**params)
134
+ [protocol, params]
135
+ raise NotImplementedError
136
+ end
137
+
138
+ def do_put_transaction(txn_id:, **params)
139
+ [txn_id, params]
140
+ raise NotImplementedError
141
+ end
142
+
143
+ def start_server
144
+ server.start
145
+
146
+ @method_map.each do |verb, method_entry|
147
+ # break if verb != method_entry[:verb]
148
+
149
+ method = method_entry[:proc]
150
+ server.mount_proc(method.prefix) { |req, res| _handle_proc(verb, method_entry, req, res) }
151
+ end
152
+
153
+ logger.info "Application Service is now running on port #{port}"
154
+ end
155
+
156
+ def stop_server
157
+ @server.shutdown if @server
158
+ @server = nil
159
+ end
160
+
161
+ private
162
+
163
+ def _handle_proc(verb, method_entry, req, res)
164
+ logger.debug "Received request for #{verb} #{method_entry}"
165
+ match = regex.match(req.request_uri.path)
166
+ match_hash = Hash[match.names.zip(match.captures)].merge(
167
+ request: req,
168
+ response: res
169
+ )
170
+
171
+ if method.is_a? Symbol
172
+ send method, match_hash
173
+ else
174
+ method.call match_hash
175
+ end
176
+ end
177
+
178
+ def server
179
+ @server ||= WEBrick::HTTPServer.new(Port: port, ServerSoftware: "#{MatrixSdk::Api::USER_AGENT} (Ruby #{RUBY_VERSION})").tap do |server|
180
+ server.mount_proc '/', &:handle_request
181
+ end
182
+ end
183
+
184
+ def handle_request(request, response)
185
+ logger.debug "Received request #{request.inspect}"
186
+
187
+ req_method = request.request_method.to_s.to_sym
188
+ req_uri = request.request_uri
189
+
190
+ map = @method_map[req_method]
191
+ raise WEBrick::HTTPStatus[405], { message: 'Unsupported verb' }.to_json if map.nil?
192
+
193
+ discovered = map.find { |k, _v| k =~ req_uri.path }
194
+ raise WEBrick::HTTPStatus[404], { message: 'Unknown request' }.to_json if discovered.nil?
195
+
196
+ method = discovered.last
197
+ match = Regexp.last_match
198
+ match_hash = Hash[match.names.zip(match.captures)].merge(
199
+ request: request,
200
+ response: response
201
+ )
202
+
203
+ if method.is_a? Symbol
204
+ send method, match_hash
205
+ else
206
+ method.call match_hash
207
+ end
208
+ end
209
+ end
210
+ end
@@ -4,19 +4,28 @@ require 'forwardable'
4
4
 
5
5
  module MatrixSdk
6
6
  class Client
7
+ include MatrixSdk::Logging
7
8
  extend Forwardable
8
9
 
9
10
  attr_reader :api
10
11
  attr_accessor :cache, :sync_filter
11
12
 
12
- events :event, :presence_event, :invite_event, :left_event, :ephemeral_event
13
+ events :event, :presence_event, :invite_event, :leave_event, :ephemeral_event
13
14
  ignore_inspect :api,
14
- :on_event, :on_presence_event, :on_invite_event, :on_left_event, :on_ephemeral_event
15
+ :on_event, :on_presence_event, :on_invite_event, :on_leave_event, :on_ephemeral_event
15
16
 
16
17
  def_delegators :@api,
17
18
  :access_token, :access_token=, :device_id, :device_id=, :homeserver, :homeserver=,
18
19
  :validate_certificate, :validate_certificate=
19
20
 
21
+ def self.new_for_domain(domain, **params)
22
+ api = MatrixSdk::Api.new_for_domain(domain, keep_wellknown: true)
23
+ return new(api, params) unless api.well_known.key? 'm.identity_server'
24
+
25
+ identity_server = MatrixSdk::Api.new(api.well_known['m.identity_server']['base_url'], protocols: %i[IS])
26
+ new(api, params.merge(identity_server: identity_server))
27
+ end
28
+
20
29
  # @param hs_url [String,URI,Api] The URL to the Matrix homeserver, without the /_matrix/ part, or an existing Api instance
21
30
  # @param client_cache [:all,:some,:none] (:all) How much data should be cached in the client
22
31
  # @param params [Hash] Additional parameters on creation
@@ -40,10 +49,11 @@ module MatrixSdk
40
49
  @rooms = {}
41
50
  @users = {}
42
51
  @cache = client_cache
52
+ @identity_server = params.fetch(:identity_server, nil)
43
53
 
44
54
  @sync_token = nil
45
55
  @sync_thread = nil
46
- @sync_filter = { room: { timeline: { limit: params.fetch(:sync_filter_limit, 20) } } }
56
+ @sync_filter = { room: { timeline: { limit: params.fetch(:sync_filter_limit, 20) }, state: { lazy_load_members: true } } }
47
57
 
48
58
  @should_listen = false
49
59
  @next_batch = nil
@@ -61,10 +71,6 @@ module MatrixSdk
61
71
  @mxid = params[:user_id]
62
72
  end
63
73
 
64
- def logger
65
- @logger ||= Logging.logger[self]
66
- end
67
-
68
74
  def mxid
69
75
  @mxid ||= begin
70
76
  api.whoami?[:user_id] if api && api.access_token
@@ -154,8 +160,15 @@ module MatrixSdk
154
160
  ensure_room(data.fetch(:room_id, room_id_or_alias))
155
161
  end
156
162
 
157
- def find_room(room_id_or_alias)
158
- @rooms.fetch(room_id_or_alias, @rooms.values.find { |r| r.canonical_alias == room_id_or_alias })
163
+ def find_room(room_id_or_alias, only_canonical: false)
164
+ room_id_or_alias = MXID.new(room_id_or_alias.to_s) unless room_id_or_alias.is_a? MXID
165
+ raise ArgumentError, 'Must be a room id or alias' unless %i[room_id room_alias].include? room_id_or_alias.type
166
+
167
+ return @rooms.fetch(room_id_or_alias, nil) if room_id_or_alias.room_id?
168
+
169
+ return @rooms.values.find { |r| r.canonical_alias == room_id_or_alias.to_s } if only_canonical
170
+
171
+ @rooms.values.find { |r| r.aliases.include? room_id_or_alias.to_s }
159
172
  end
160
173
 
161
174
  def get_user(user_id)
@@ -177,10 +190,6 @@ module MatrixSdk
177
190
  raise MatrixUnexpectedResponseError, 'Upload succeeded, but no media URI returned'
178
191
  end
179
192
 
180
- def listen_for_events(timeout: 30, **arguments)
181
- sync(arguments.merge(timeout: timeout))
182
- end
183
-
184
193
  def start_listener_thread(params = {})
185
194
  @should_listen = true
186
195
  thread = Thread.new { listen_forever(params) }
@@ -196,19 +205,44 @@ module MatrixSdk
196
205
  @sync_thread = nil
197
206
  end
198
207
 
208
+ def sync(skip_store_batch: false, **params)
209
+ extra_params = {
210
+ filter: sync_filter,
211
+ timeout: 30
212
+ }
213
+ extra_params[:since] = @next_batch unless @next_batch.nil?
214
+ extra_params.merge!(params)
215
+ extra_params[:filter] = extra_params[:filter].to_json unless extra_params[:filter].is_a? String
216
+
217
+ attempts = 0
218
+ data = loop do
219
+ begin
220
+ break api.sync extra_params
221
+ rescue MatrixSdk::MatrixTimeoutError => e
222
+ raise e if (attempts += 1) >= params.fetch(:allow_sync_retry, 0)
223
+ end
224
+ end
225
+
226
+ @next_batch = data[:next_batch] unless skip_store_batch
227
+
228
+ handle_sync_response(data)
229
+ end
230
+
231
+ alias listen_for_events sync
232
+
199
233
  private
200
234
 
201
235
  def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 30, **params)
202
- orig_bad_sync_timeout = bad_sync_timeout.dup
236
+ orig_bad_sync_timeout = bad_sync_timeout + 0
203
237
  while @should_listen
204
238
  begin
205
239
  sync(params.merge(timeout: timeout))
206
240
 
207
241
  bad_sync_timeout = orig_bad_sync_timeout
208
242
  sleep(sync_interval) if sync_interval > 0
209
- rescue MatrixRequestError => ex
210
- logger.warn("A #{ex.class} occurred during sync")
211
- if ex.httpstatus >= 500
243
+ rescue MatrixRequestError => e
244
+ logger.warn("A #{e.class} occurred during sync")
245
+ if e.httpstatus >= 500
212
246
  logger.warn("Serverside error, retrying in #{bad_sync_timeout} seconds...")
213
247
  sleep params[:bad_sync_timeout]
214
248
  bad_sync_timeout = [bad_sync_timeout * 2, @bad_sync_timeout_limit].min
@@ -227,7 +261,11 @@ module MatrixSdk
227
261
 
228
262
  def ensure_room(room_id)
229
263
  room_id = room_id.to_s unless room_id.is_a? String
230
- @rooms.fetch(room_id) { @rooms[room_id] = Room.new(self, room_id) }
264
+ @rooms.fetch(room_id) do
265
+ room = Room.new(self, room_id)
266
+ @rooms[room_id] = room unless cache == :none
267
+ room
268
+ end
231
269
  end
232
270
 
233
271
  def handle_state(room_id, state_event)
@@ -240,6 +278,8 @@ module MatrixSdk
240
278
  room.instance_variable_set '@name', content[:name]
241
279
  when 'm.room.canonical_alias'
242
280
  room.instance_variable_set '@canonical_alias', content[:alias]
281
+ # Also add as a regular alias
282
+ room.instance_variable_get('@aliases').concat [content[:alias]]
243
283
  when 'm.room.topic'
244
284
  room.instance_variable_set '@topic', content[:topic]
245
285
  when 'm.room.aliases'
@@ -252,63 +292,48 @@ module MatrixSdk
252
292
  return unless cache == :all
253
293
 
254
294
  if content[:membership] == 'join'
255
- room.send :ensure_member, User.new(self, state_event[:state_key], display_name: content[:displayname])
295
+ room.send(:ensure_member, get_user(state_event[:state_key]).dup.tap do |u|
296
+ u.instance_variable_set :@display_name, content[:displayname]
297
+ end)
256
298
  elsif %w[leave kick invite].include? content[:membership]
257
299
  room.members.delete_if { |m| m.id == state_event[:state_key] }
258
300
  end
259
301
  end
260
302
  end
261
303
 
262
- def sync(skip_store_batch: false, **params)
263
- extra_params = {
264
- filter: sync_filter.to_json
265
- }
266
- extra_params[:since] = @next_batch unless @next_batch.nil?
267
-
268
- attempts = 0
269
- data = loop do
270
- begin
271
- break api.sync extra_params.merge(params)
272
- rescue MatrixTimeoutError => ex
273
- raise ex if (attempts += 1) > params.fetch(:allow_sync_retry, 0)
274
- end
275
- end
276
-
277
- @next_batch = data[:next_batch] unless skip_store_batch
278
-
279
- handle_sync_response(data)
280
- end
281
-
282
304
  def handle_sync_response(data)
283
305
  data[:presence][:events].each do |presence_update|
284
306
  fire_presence_event(MatrixEvent.new(self, presence_update))
285
307
  end
286
308
 
287
309
  data[:rooms][:invite].each do |room_id, invite|
288
- fire_invite_event(MatrixEvent.new(self, invite), room_id)
310
+ fire_invite_event(MatrixEvent.new(self, invite), room_id.to_s)
289
311
  end
290
312
 
291
313
  data[:rooms][:leave].each do |room_id, left|
292
- fire_leave_event(MatrixEvent.new(self, left), room_id)
314
+ fire_leave_event(MatrixEvent.new(self, left), room_id.to_s)
293
315
  end
294
316
 
295
317
  data[:rooms][:join].each do |room_id, join|
296
318
  room = ensure_room(room_id)
297
319
  room.instance_variable_set '@prev_batch', join[:timeline][:prev_batch]
320
+ room.instance_variable_set :@members_loaded, true unless sync_filter.fetch(:room, {}).fetch(:state, {}).fetch(:lazy_load_members, false)
321
+
298
322
  join[:state][:events].each do |event|
299
- event[:room_id] = room_id
323
+ event[:room_id] = room_id.to_s
300
324
  handle_state(room_id, event)
301
325
  end
302
326
 
303
327
  join[:timeline][:events].each do |event|
304
- event[:room_id] = room_id
328
+ event[:room_id] = room_id.to_s
329
+ handle_state(room_id, event) unless event[:type] == 'm.room.message'
305
330
  room.send :put_event, event
306
331
 
307
332
  fire_event(MatrixEvent.new(self, event), event[:type])
308
333
  end
309
334
 
310
335
  join[:ephemeral][:events].each do |event|
311
- event[:room_id] = room_id
336
+ event[:room_id] = room_id.to_s
312
337
  room.send :put_ephemeral_event, event
313
338
 
314
339
  fire_ephemeral_event(MatrixEvent.new(self, event), event[:type])
@@ -10,6 +10,14 @@ module URI
10
10
  @@schemes['MXC'] = MATRIX
11
11
  end
12
12
 
13
+ unless Object.respond_to? :yield_self
14
+ class Object
15
+ def yield_self
16
+ yield(self)
17
+ end
18
+ end
19
+ end
20
+
13
21
  def events(*symbols)
14
22
  module_name = "#{name}Events"
15
23
 
@@ -52,15 +60,23 @@ end
52
60
  def ignore_inspect(*symbols)
53
61
  class_eval %*
54
62
  def inspect
63
+ reentrant = caller_locations.any? { |l| l.absolute_path == __FILE__ && l.label == 'inspect' }
55
64
  "\#{to_s[0..-2]} \#{instance_variables
56
65
  .reject { |f| %i[#{symbols.map { |s| "@#{s}" }.join ' '}].include? f }
57
- .map { |f| "\#{f}=\#{instance_variable_get(f).inspect}" }.join " " }}>"
66
+ .map { |f| "\#{f}=\#{reentrant ? instance_variable_get(f) : instance_variable_get(f).inspect}" }.join " " }}>"
58
67
  end
59
- *, __FILE__, __LINE__ - 6
68
+ *, __FILE__, __LINE__ - 7
60
69
  end
61
70
 
62
71
  module MatrixSdk
72
+ module Logging
73
+ def logger
74
+ @logger ||= ::Logging.logger[self]
75
+ end
76
+ end
77
+
63
78
  class EventHandlerArray < Hash
79
+ include MatrixSdk::Logging
64
80
  attr_accessor :reraise_exceptions
65
81
 
66
82
  def initialize(*args)
@@ -82,17 +98,13 @@ module MatrixSdk
82
98
  reverse_each do |_k, h|
83
99
  begin
84
100
  h[:block].call(event) if event.matches?(h[:filter], filter)
85
- rescue StandardError => ex
86
- logger.error "#{ex.class.name} occurred when firing event (#{event})\n#{ex}"
101
+ rescue StandardError => e
102
+ logger.error "#{e.class.name} occurred when firing event (#{event})\n#{e}"
87
103
 
88
- raise ex if @reraise_exceptions
104
+ raise e if @reraise_exceptions
89
105
  end
90
106
  end
91
107
  end
92
-
93
- def logger
94
- @logger ||= Logging.logger[self]
95
- end
96
108
  end
97
109
 
98
110
  class Event
@@ -150,7 +162,7 @@ module MatrixSdk
150
162
  super
151
163
  end
152
164
 
153
- def respond_to_missing?(method)
165
+ def respond_to_missing?(method, *)
154
166
  event.key? method
155
167
  end
156
168
  end