matrix_sdk 0.0.1 → 0.0.2

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,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
@@ -1,15 +1,31 @@
1
1
  module MatrixSdk
2
- class MatrixError
3
- attr_reader :errcode, :error, :httpstatus
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
- @errcode = error[:errcode]
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
- "#{errcode}: #{error}"
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