matrix_sdk 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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