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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +219 -0
- data/LICENSE.txt +21 -0
- data/README.md +82 -0
- data/lib/matrix_sdk/api.rb +451 -0
- data/lib/matrix_sdk/bot/base.rb +847 -0
- data/lib/matrix_sdk/bot/main.rb +79 -0
- data/lib/matrix_sdk/bot.rb +4 -0
- data/lib/matrix_sdk/client.rb +696 -0
- data/lib/matrix_sdk/errors.rb +68 -0
- data/lib/matrix_sdk/mxid.rb +146 -0
- data/lib/matrix_sdk/protocols/as.rb +7 -0
- data/lib/matrix_sdk/protocols/cs.rb +1982 -0
- data/lib/matrix_sdk/protocols/is.rb +35 -0
- data/lib/matrix_sdk/protocols/msc.rb +152 -0
- data/lib/matrix_sdk/protocols/ss.rb +14 -0
- data/lib/matrix_sdk/response.rb +63 -0
- data/lib/matrix_sdk/room.rb +1044 -0
- data/lib/matrix_sdk/rooms/space.rb +79 -0
- data/lib/matrix_sdk/user.rb +168 -0
- data/lib/matrix_sdk/util/account_data_cache.rb +91 -0
- data/lib/matrix_sdk/util/events.rb +111 -0
- data/lib/matrix_sdk/util/extensions.rb +85 -0
- data/lib/matrix_sdk/util/state_event_cache.rb +92 -0
- data/lib/matrix_sdk/util/tinycache.rb +140 -0
- data/lib/matrix_sdk/util/tinycache_adapter.rb +87 -0
- data/lib/matrix_sdk/util/uri.rb +101 -0
- data/lib/matrix_sdk/version.rb +5 -0
- data/lib/matrix_sdk.rb +75 -0
- metadata +172 -0
@@ -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
|