matrix_sdk 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +39 -0
- data/README.md +27 -2
- data/examples/simple_client.rb +93 -0
- data/lib/matrix_sdk/api.rb +402 -31
- data/lib/matrix_sdk/client.rb +234 -0
- data/lib/matrix_sdk/errors.rb +21 -5
- data/lib/matrix_sdk/extensions.rb +119 -0
- data/lib/matrix_sdk/room.rb +471 -0
- data/lib/matrix_sdk/user.rb +49 -0
- data/lib/matrix_sdk/version.rb +1 -1
- data/lib/matrix_sdk.rb +24 -2
- data/matrix-sdk.gemspec +9 -7
- metadata +27 -8
data/lib/matrix_sdk/client.rb
CHANGED
@@ -0,0 +1,234 @@
|
|
1
|
+
require 'matrix_sdk'
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module MatrixSdk
|
6
|
+
class Client
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :api
|
10
|
+
attr_accessor :cache, :mxid, :sync_filter
|
11
|
+
|
12
|
+
events :event, :presence_event, :invite_event, :left_event, :ephemeral_event
|
13
|
+
ignore_inspect :api,
|
14
|
+
:on_event, :on_presence_event, :on_invite_event, :on_left_event, :on_ephemeral_event
|
15
|
+
|
16
|
+
alias user_id mxid
|
17
|
+
alias user_id= mxid=
|
18
|
+
|
19
|
+
def_delegators :@api,
|
20
|
+
:access_token, :access_token=, :device_id, :device_id=, :homeserver, :homeserver=,
|
21
|
+
:validate_certificate, :validate_certificate=
|
22
|
+
|
23
|
+
def initialize(hs_url, params = {})
|
24
|
+
event_initialize
|
25
|
+
|
26
|
+
params[:user_id] = params[:mxid] if params[:mxid]
|
27
|
+
raise ArgumentError, 'Must provide user_id with access_token' if params[:access_token] && !params[:user_id]
|
28
|
+
|
29
|
+
@api = Api.new hs_url, params
|
30
|
+
|
31
|
+
@rooms = {}
|
32
|
+
@cache = :all
|
33
|
+
|
34
|
+
@sync_token = nil
|
35
|
+
@sync_thread = nil
|
36
|
+
@sync_filter = { room: { timeline: { limit: params.fetch(:sync_filter_limit, 20) } } }
|
37
|
+
|
38
|
+
@should_listen = false
|
39
|
+
|
40
|
+
@bad_sync_timeout_limit = 60 * 60
|
41
|
+
|
42
|
+
params.each do |k, v|
|
43
|
+
instance_variable_set("@#{k}", v) if instance_variable_defined? "@#{k}"
|
44
|
+
end
|
45
|
+
|
46
|
+
raise ArgumentError, 'Cache value must be one of of [:all, :some, :none]' unless %i[all some none].include? @cache
|
47
|
+
|
48
|
+
return unless params[:user_id]
|
49
|
+
@mxid = params[:user_id]
|
50
|
+
sync
|
51
|
+
end
|
52
|
+
|
53
|
+
def logger
|
54
|
+
@logger ||= Logging.logger[self.class.name]
|
55
|
+
end
|
56
|
+
|
57
|
+
def rooms
|
58
|
+
@rooms.values
|
59
|
+
end
|
60
|
+
|
61
|
+
def register_as_guest
|
62
|
+
data = api.register(kind: :guest)
|
63
|
+
post_authentication(data)
|
64
|
+
end
|
65
|
+
|
66
|
+
def register_with_password(username, password)
|
67
|
+
data = api.register(auth: { type: 'm.login.dummy' }, username: username, password: password)
|
68
|
+
post_authentication(data)
|
69
|
+
end
|
70
|
+
|
71
|
+
def login(username, password, params = {})
|
72
|
+
data = api.login(user: username, password: password)
|
73
|
+
post_authentication(data)
|
74
|
+
|
75
|
+
sync(timeout: params.fetch(:sync_timeout, 15)) unless params[:no_sync]
|
76
|
+
end
|
77
|
+
|
78
|
+
def logout
|
79
|
+
api.logout
|
80
|
+
@api.access_token = nil
|
81
|
+
@mxid = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def create_room(room_alias = nil, params = {})
|
85
|
+
api.create_room(params.merge(room_alias: room_alias))
|
86
|
+
end
|
87
|
+
|
88
|
+
def join_room(room_id_or_alias)
|
89
|
+
data = api.join_room(room_id_or_alias)
|
90
|
+
ensure_room(data.fetch(:room_id, room_id_or_alias))
|
91
|
+
end
|
92
|
+
|
93
|
+
def find_room(room_id_or_alias)
|
94
|
+
@rooms.fetch(room_id_or_alias, @rooms.values.find { |r| r.canonical_alias == room_id_or_alias })
|
95
|
+
end
|
96
|
+
|
97
|
+
def get_user(user_id)
|
98
|
+
User.new(self, user_id)
|
99
|
+
end
|
100
|
+
|
101
|
+
def remove_room_alias(room_alias)
|
102
|
+
api.remove_room_alias(room_alias)
|
103
|
+
end
|
104
|
+
|
105
|
+
def upload(content, content_type)
|
106
|
+
data = api.media_upload(content, content_type)
|
107
|
+
return data[:content_uri] if data.key? :content_uri
|
108
|
+
raise MatrixUnexpectedResponseError, 'Upload succeeded, but no media URI returned'
|
109
|
+
end
|
110
|
+
|
111
|
+
def listen_for_events(timeout = 30)
|
112
|
+
sync(timeout: timeout)
|
113
|
+
end
|
114
|
+
|
115
|
+
def start_listener_thread(params = {})
|
116
|
+
@should_listen = true
|
117
|
+
thread = Thread.new { listen_forever(params) }
|
118
|
+
@sync_thread = thread
|
119
|
+
thread.run
|
120
|
+
end
|
121
|
+
|
122
|
+
def stop_listener_thread
|
123
|
+
return unless @sync_thread
|
124
|
+
@should_listen = false
|
125
|
+
@sync_thread.join if @sync_thread.alive?
|
126
|
+
@sync_thread = nil
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def listen_forever(params = {})
|
132
|
+
timeout = params.fetch(:timeout, 30)
|
133
|
+
params[:bad_sync_timeout] = params.fetch(:bad_sync_timeout, 5)
|
134
|
+
|
135
|
+
bad_sync_timeout = params[:bad_sync_timeout]
|
136
|
+
while @should_listen
|
137
|
+
begin
|
138
|
+
sync(timeout: timeout)
|
139
|
+
bad_sync_timeout = params[:bad_sync_timeout]
|
140
|
+
rescue MatrixRequestError => ex
|
141
|
+
logger.warn("A #{ex.class} occurred during sync")
|
142
|
+
if ex.httpstatus >= 500
|
143
|
+
logger.warn("Serverside error, retrying in #{bad_sync_timeout} seconds...")
|
144
|
+
sleep params[:bad_sync_timeout]
|
145
|
+
bad_sync_timeout = [bad_sync_timeout * 2, @bad_sync_timeout_limit].min
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def post_authentication(data)
|
152
|
+
@mxid = data[:user_id]
|
153
|
+
@api.access_token = data[:access_token]
|
154
|
+
@api.device_id = data[:device_id]
|
155
|
+
@api.homeserver = data[:home_server]
|
156
|
+
access_token
|
157
|
+
end
|
158
|
+
|
159
|
+
def ensure_room(room_id)
|
160
|
+
room_id = room_id.to_s unless room_id.is_a? String
|
161
|
+
@rooms.fetch(room_id) { @rooms[room_id] = Room.new(self, room_id) }
|
162
|
+
end
|
163
|
+
|
164
|
+
def handle_state(room_id, state_event)
|
165
|
+
return unless state_event.key? :type
|
166
|
+
|
167
|
+
room = ensure_room(room_id)
|
168
|
+
content = state_event[:content]
|
169
|
+
case state_event[:type]
|
170
|
+
when 'm.room.name'
|
171
|
+
room.instance_variable_set '@name', content[:name]
|
172
|
+
when 'm.room.canonical_alias'
|
173
|
+
room.instance_variable_set '@canonical_alias', content[:alias]
|
174
|
+
when 'm.room.topic'
|
175
|
+
room.instance_variable_set '@topic', content[:topic]
|
176
|
+
when 'm.room.aliases'
|
177
|
+
room.instance_variable_get('@aliases').concat content[:aliases]
|
178
|
+
when 'm.room.join_rules'
|
179
|
+
room.instance_variable_set '@join_rule', content[:join_rule].to_sym
|
180
|
+
when 'm.room.guest_access'
|
181
|
+
room.instance_variable_set '@guest_access', content[:guest_access].to_sym
|
182
|
+
when 'm.room.member'
|
183
|
+
return unless cache == :all
|
184
|
+
|
185
|
+
if content[:membership] == 'join'
|
186
|
+
room.send :ensure_member, User.new(self, state_event[:state_key], display_name: content[:displayname])
|
187
|
+
elsif %w[leave kick invite].include? content[:membership]
|
188
|
+
room.members.delete_if { |m| m.id == state_event[:state_key] }
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def sync(params = {})
|
194
|
+
data = api.sync params.merge(filter: sync_filter.to_json)
|
195
|
+
|
196
|
+
data[:presence][:events].each do |presence_update|
|
197
|
+
fire_presence_event(MatrixEvent.new(self, presence_update))
|
198
|
+
end
|
199
|
+
|
200
|
+
data[:rooms][:invite].each do |room_id, invite|
|
201
|
+
fire_invite_event(MatrixEvent.new(self, invite), room_id)
|
202
|
+
end
|
203
|
+
|
204
|
+
data[:rooms][:leave].each do |room_id, left|
|
205
|
+
fire_leave_event(MatrixEvent.new(self, left), room_id)
|
206
|
+
end
|
207
|
+
|
208
|
+
data[:rooms][:join].each do |room_id, join|
|
209
|
+
room = ensure_room(room_id)
|
210
|
+
room.instance_variable_set '@prev_batch', join[:timeline][:prev_batch]
|
211
|
+
join[:state][:events].each do |event|
|
212
|
+
event[:room_id] = room_id
|
213
|
+
handle_state(room_id, event)
|
214
|
+
end
|
215
|
+
|
216
|
+
join[:timeline][:events].each do |event|
|
217
|
+
event[:room_id] = room_id
|
218
|
+
room.send :put_event, event
|
219
|
+
|
220
|
+
fire_event(MatrixEvent.new(self, event), event[:type])
|
221
|
+
end
|
222
|
+
|
223
|
+
join[:ephemeral][:events].each do |event|
|
224
|
+
event[:room_id] = room_id
|
225
|
+
room.send :put_ephemeral_event, event
|
226
|
+
|
227
|
+
fire_ephemeral_event(MatrixEvent.new(self, event), event[:type])
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
nil
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
data/lib/matrix_sdk/errors.rb
CHANGED
@@ -1,15 +1,31 @@
|
|
1
1
|
module MatrixSdk
|
2
|
-
|
3
|
-
|
2
|
+
# A generic error raised for issues in the MatrixSdk
|
3
|
+
class MatrixError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
# An error specialized and raised for failed requests
|
7
|
+
class MatrixRequestError < MatrixError
|
8
|
+
attr_reader :code, :httpstatus, :message
|
9
|
+
alias error message
|
4
10
|
|
5
11
|
def initialize(error, status)
|
6
|
-
@
|
7
|
-
@error = error[:error]
|
12
|
+
@code = error[:errcode]
|
8
13
|
@httpstatus = status
|
14
|
+
@message = error[:error]
|
15
|
+
|
16
|
+
super error[:error]
|
9
17
|
end
|
10
18
|
|
11
19
|
def to_s
|
12
|
-
"#{
|
20
|
+
"HTTP #{httpstatus} (#{code}): #{message}"
|
13
21
|
end
|
14
22
|
end
|
23
|
+
|
24
|
+
# An error raised when errors occur in the connection layer
|
25
|
+
class MatrixConnectionError < MatrixError
|
26
|
+
end
|
27
|
+
|
28
|
+
# An error raised when the homeserver returns an unexpected response to the client
|
29
|
+
class MatrixUnexpectedResponseError < MatrixError
|
30
|
+
end
|
15
31
|
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module URI
|
2
|
+
class MATRIX < Generic
|
3
|
+
def full_path
|
4
|
+
select(:host, :port, :path, :query, :fragment)
|
5
|
+
.reject(&:nil?)
|
6
|
+
.join
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
@@schemes['MXC'] = MATRIX
|
11
|
+
end
|
12
|
+
|
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
|
+
"#<\#{self.class.name}:\#{"%016x" % (object_id << 1)} \#{instance_variables.reject { |f| %i[#{symbols.map { |s| "@#{s}" }.join ' '}].include? f }.map { |f| "\#{f}=\#{instance_variable_get(f).inspect}" }.join ' ' }>"
|
56
|
+
end
|
57
|
+
*, __FILE__, __LINE__ - 4
|
58
|
+
end
|
59
|
+
|
60
|
+
module MatrixSdk
|
61
|
+
class EventHandlerArray < Hash
|
62
|
+
def add_handler(filter = nil, id = nil, &block)
|
63
|
+
id ||= block.hash
|
64
|
+
self[id] = { filter: filter, id: id, block: block }
|
65
|
+
end
|
66
|
+
|
67
|
+
def remove_handler(id)
|
68
|
+
delete id
|
69
|
+
end
|
70
|
+
|
71
|
+
def fire(event, filter = nil)
|
72
|
+
reverse_each do |_k, h|
|
73
|
+
h[:block].call(event) unless event.matches?(h[:filter], filter)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class Event
|
79
|
+
attr_writer :handled
|
80
|
+
|
81
|
+
ignore_inspect :sender
|
82
|
+
|
83
|
+
def initialize(sender)
|
84
|
+
@sender = sender
|
85
|
+
@handled = false
|
86
|
+
end
|
87
|
+
|
88
|
+
def handled?
|
89
|
+
@handled
|
90
|
+
end
|
91
|
+
|
92
|
+
def matches?(_filter)
|
93
|
+
true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class MatrixEvent < Event
|
98
|
+
attr_accessor :event, :filter
|
99
|
+
|
100
|
+
ignore_inspect :sender
|
101
|
+
|
102
|
+
def initialize(sender, event = nil, filter = nil)
|
103
|
+
@event = event
|
104
|
+
@filter = filter || @event[:type]
|
105
|
+
super sender
|
106
|
+
end
|
107
|
+
|
108
|
+
def matches?(filter, filter_override = nil)
|
109
|
+
return true if filter_override.nil? && (@filter.nil? || filter.nil?)
|
110
|
+
|
111
|
+
to_match = filter_override || @filter
|
112
|
+
if filter.is_a? Regexp
|
113
|
+
filter.match(to_match) { true } || false
|
114
|
+
else
|
115
|
+
to_match == filter
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|