chatrix 1.0.0.pre → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,15 +5,41 @@ module Chatrix
5
5
 
6
6
  # Errors that stem from an API call.
7
7
  class ApiError < ChatrixError
8
+ # @return [Hash] the raw error response object.
9
+ attr_reader :error
10
+
11
+ # @return [String] the type of error. `'E_UNKNOWN'` if the server
12
+ # did not give an error code.
13
+ attr_reader :code
14
+
15
+ # @return [String] the error message returned from the server.
16
+ # `'Unknown error'` if the server did not give any message.
17
+ attr_reader :api_message
18
+
19
+ # Initializes a new RequestError instance.
20
+ # @param error [Hash{String=>String}] The error response object.
21
+ def initialize(error = {})
22
+ @error = error
23
+ @code = error['errcode'] || 'E_UNKNOWN'
24
+ @api_message = error['error'] || 'Unknown error'
25
+ end
8
26
  end
9
27
 
10
28
  # Error raised when a request is badly formatted.
11
29
  class RequestError < ApiError
12
- attr_reader :code, :api_message
30
+ end
31
+
32
+ # Error raised when the API request limit is reached.
33
+ class RateLimitError < ApiError
34
+ # @return [Fixnum,nil] number of milliseconds to wait before attempting
35
+ # this request again. If no delay was provided this will be `nil`.
36
+ attr_reader :retry_delay
13
37
 
14
- def initialize(error)
15
- @code = error['errcode']
16
- @api_message = error['error']
38
+ # Initializes a new RateLimitError instance.
39
+ # @param error [Hash] The error response object.
40
+ def initialize(error = {})
41
+ super
42
+ @retry_delay = error['retry_after_ms'] if error.key? 'retry_after_ms'
17
43
  end
18
44
  end
19
45
 
@@ -27,28 +53,70 @@ module Chatrix
27
53
 
28
54
  # Raised when a user is not found.
29
55
  class UserNotFoundError < NotFoundError
56
+ # @return [String] the name of the user that was not found.
30
57
  attr_reader :username
31
58
 
32
- def initialize(username)
59
+ # Initializes a new UserNotFoundError instance.
60
+ # @param username [String] The user that wasn't found.
61
+ # @param error [Hash] The error response from the server.
62
+ def initialize(username, error = {})
63
+ super error
33
64
  @username = username
34
65
  end
35
66
  end
36
67
 
37
68
  # Raised when a user's avatar is not found.
38
69
  class AvatarNotFoundError < NotFoundError
70
+ # @return [String] the user whose avatar was not found.
39
71
  attr_reader :username
40
72
 
41
- def initialize(username)
73
+ # Initializes a new AvatarNotFoundError instance.
74
+ # @param username [String] Name of the user whose avatar was not found.
75
+ # @param error [Hash] The error response from the server.
76
+ def initialize(username, error = {})
77
+ super error
42
78
  @username = username
43
79
  end
44
80
  end
45
81
 
46
82
  # Raised when a room is not found.
47
83
  class RoomNotFoundError < NotFoundError
84
+ # @return [String] the room that was not found.
48
85
  attr_reader :room
49
86
 
50
- def initialize(room)
87
+ # Initializes a new RoomNotFoundError instance.
88
+ # @param room [String] Name of the room that was not found.
89
+ # @param error [Hash] The error response from the server.
90
+ def initialize(room, error = {})
91
+ super error
51
92
  @room = room
52
93
  end
53
94
  end
95
+
96
+ # Raised when there is an issue with authentication.
97
+ #
98
+ # This can either be because authentication failed outright or because
99
+ # more information is required by the server to successfully authenticate.
100
+ #
101
+ # If authentication failed then the `data` attribute will be an empty hash.
102
+ #
103
+ # If more information is required the `data` hash will contain information
104
+ # about what additional information is needed to authenticate.
105
+ class AuthenticationError < ApiError
106
+ # @return [Hash] a hash with information about the additional information
107
+ # required by the server for authentication, if any. If the
108
+ # authentication request failed, this will be an empty hash or `nil`.
109
+ attr_reader :data
110
+
111
+ # Initializes a new AuthenticationError instance.
112
+ # @param error [Hash] The error response from the server.
113
+ def initialize(error = {})
114
+ super
115
+
116
+ # Set data to be the error response hash WITHOUT the error code and
117
+ # error values. This will leave it with only the data relevant for
118
+ # handling authentication.
119
+ @data = error.select { |key| !%w{(errcode), (error)}.include? key }
120
+ end
121
+ end
54
122
  end
@@ -0,0 +1,39 @@
1
+ module Chatrix
2
+ # Keeps track of what events have already been processed.
3
+ module Events
4
+ @processed = []
5
+
6
+ # Marks an event as having been processed.
7
+ # @param event [String,Hash] The affected event.
8
+ def self.processed(event)
9
+ @processed.push parse_event event
10
+ end
11
+
12
+ # Checks if an event has been processed.
13
+ #
14
+ # @param event [String,Hash] The event to check.
15
+ # @return [Boolean] `true` if the event has been processed,
16
+ # otherwise `false`.
17
+ def self.processed?(event)
18
+ @processed.member? parse_event event
19
+ end
20
+
21
+ # Extract the event ID from an event object.
22
+ # If this is a string, it's returned verbatim.
23
+ #
24
+ # @param event [String,Hash] The event object to extract information from.
25
+ # @return [String] The event ID.
26
+ #
27
+ # @raise ArgumentError if the event object is of an invalid type or does
28
+ # not contain an ID.
29
+ def self.parse_event(event)
30
+ if event.is_a? String
31
+ event
32
+ elsif event.is_a?(Hash) && event.key?('event_id')
33
+ event['event_id']
34
+ else
35
+ raise ArgumentError, 'Invalid event object'
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,11 @@
1
1
  require 'chatrix/errors'
2
2
 
3
+ require 'chatrix/api/session'
4
+ require 'chatrix/api/users'
5
+ require 'chatrix/api/rooms'
6
+ require 'chatrix/api/media'
7
+ require 'chatrix/api/push'
8
+
3
9
  require 'httparty'
4
10
 
5
11
  module Chatrix
@@ -14,10 +20,6 @@ module Chatrix
14
20
  # @note Endpoints that require a room ID in the official API can be passed
15
21
  # a room alias in this implementation, the room ID will be automatically
16
22
  # looked up from the homeserver.
17
- #
18
- # @todo Try to extract functionality to make this class smaller.
19
- #
20
- # rubocop:disable ClassLength
21
23
  class Matrix
22
24
  include HTTParty
23
25
 
@@ -33,21 +35,50 @@ module Chatrix
33
35
  delete: -> (path, options, &block) { delete path, options, &block }
34
36
  }.freeze
35
37
 
38
+ # Registered request error handlers.
39
+ ERROR_HANDLERS = {
40
+ 400 => [RequestError, 'Request failed'],
41
+ 401 => [AuthenticationError, 'Server requests additional authentication'],
42
+ 403 => [ForbiddenError, 'You do not have access to that resource'],
43
+ 404 => [NotFoundError, 'The resource was not found'],
44
+ 429 => [RateLimitError, 'The request was rate limited']
45
+ }.tap do |h|
46
+ h.default = [ApiError, 'An unknown API error occurred.']
47
+ end.freeze
48
+
36
49
  # Default homeserver used if none is specified.
37
50
  DEFAULT_HOMESERVER = 'https://matrix.org'.freeze
38
51
 
39
52
  # API path used.
40
53
  API_PATH = '/_matrix/client/r0'.freeze
41
54
 
42
- # @!attribute access_token
43
- # @return [String] The access token used when performing requests
44
- # to the homeserver.
55
+ # @return [String] the access token used when performing requests
56
+ # to the homeserver.
45
57
  attr_accessor :access_token
46
58
 
47
- # @!attribute [r] homeserver
48
- # @return [String] The homeserver for this API object.
59
+ # @return [String] the homeserver for this API object.
49
60
  attr_reader :homeserver
50
61
 
62
+ # @return [Api::Session] the instance of Api::Session to perform
63
+ # session-related API calls with.
64
+ attr_reader :session
65
+
66
+ # @return [Api::Users] the instance of Api::Users to perform user-related
67
+ # API calls with.
68
+ attr_reader :users
69
+
70
+ # @return [Api::Rooms] the instance of Api::Rooms to perform room-related
71
+ # API calls with.
72
+ attr_reader :rooms
73
+
74
+ # @return [Api::Media] the instance of Api::Media to perform media-related
75
+ # API calls with.
76
+ attr_reader :media
77
+
78
+ # @return [Api::Push] the instance of Api::Push to perform push-related
79
+ # API calls with.
80
+ attr_reader :push
81
+
51
82
  # Initializes a new instance of Matrix.
52
83
  #
53
84
  # @param token [String] The access token to use.
@@ -55,307 +86,28 @@ module Chatrix
55
86
  def initialize(token = nil, homeserver = DEFAULT_HOMESERVER)
56
87
  @homeserver = homeserver
57
88
  @base_uri = @homeserver + API_PATH
58
- @transaction_id = 0
59
89
  @access_token = token
60
- end
61
-
62
- # Gets third-party IDs associated with the current account.
63
- #
64
- # @return [Array] A list of 3rd party IDs.
65
- def threepids
66
- make_request(:get, '/account/3pid')['threepids']
67
- end
68
-
69
- # Gets information about a specific user.
70
- #
71
- # @param user [String] The user to query (`@user:host.tld`).
72
- # @return [Hash] The user information.
73
- # @raise [UserNotFoundError] If the user could not be found.
74
- #
75
- # @example Print a user's display name
76
- # puts get_user('@foo:matrix.org')['displayname']
77
- def get_user(user)
78
- make_request(:get, "/profile/#{user}").parsed_response
79
- rescue NotFoundError
80
- raise UserNotFoundError.new(user), 'The specified user could not be found'
81
- end
82
-
83
- # Get the URL to a user's avatar (an `mxp://` URL).
84
- #
85
- # @param (see #get_user)
86
- # @return [String] The avatar URL.
87
- # @raise [AvatarNotFoundError] If the avatar or user could not be found.
88
- def get_avatar(user)
89
- make_request(:get, "/profile/#{user}/avatar_url")['avatar_url']
90
- rescue NotFoundError
91
- raise AvatarNotFoundError.new(user), 'Avatar or user could not be found'
92
- end
93
-
94
- # Get a user's display name (**not** username).
95
- #
96
- # @param (see #get_user)
97
- # @raise (see #get_user)
98
- def get_displayname(user)
99
- make_request(:get, "/profile/#{user}/displayname")['displayname']
100
- rescue NotFoundError
101
- raise UserNotFoundError.new(user), 'The specified user could not be found'
102
- end
103
-
104
- # Sets a new display name for a user.
105
- #
106
- # @note Can only be used on the user who possesses the
107
- # {#access_token access_token} currently in use.
108
- #
109
- # @param user [String] The user to modify (`@user:host.tld`).
110
- # @param displayname [String] The new displayname to set.
111
- # @return [Boolean] `true` if the new display name was successfully set,
112
- # otherwise `false`.
113
- def set_displayname(user, displayname)
114
- make_request(
115
- :put,
116
- "/profile/#{user}/displayname",
117
- content: {
118
- displayname: displayname
119
- }
120
- ).code == 200
121
- end
122
-
123
- # Get tags that a specific user has set on a room.
124
- #
125
- # @param user [String] The user whose settings to retrieve
126
- # (`@user:host.tld`).
127
- # @param room [String] The room to get tags from (ID or alias).
128
- # @return [Hash{String=>Hash}] A hash with tag data. The tag name is
129
- # the key and any additional metadata is contained in the Hash value.
130
- def get_user_room_tags(user, room)
131
- room = get_room_id room if room.start_with? '#'
132
- make_request(:get, "/user/#{user}/rooms/#{room}/tags")['tags']
133
- end
134
-
135
- # Get information about a room alias.
136
- #
137
- # This can be used to get the room ID that an alias points to.
138
- #
139
- # @param room_alias [String] The room alias to query, this **must** be an
140
- # alias and not an ID.
141
- # @return [Hash] Returns information about the alias in a Hash.
142
- #
143
- # @see #get_room_id #get_room_id is an example of how this method could be
144
- # used to get a room's ID.
145
- def get_room_alias_info(room_alias)
146
- make_request(:get, "/directory/room/#{room_alias}").parsed_response
147
- rescue NotFoundError
148
- raise RoomNotFoundError.new(room_alias),
149
- 'The specified room alias could not be found'
150
- end
151
-
152
- # Get a room's ID from its alias.
153
- #
154
- # @param room_alias [String] The room alias to query.
155
- # @return [String] The actual room ID for the room.
156
- def get_room_id(room_alias)
157
- get_room_alias_info(room_alias)['room_id']
158
- end
159
-
160
- # Gets context for an event in a room.
161
- #
162
- # The method will return events that happened before and after the
163
- # specified event.
164
- #
165
- # @param room [String] The room to query.
166
- # @param event [String] The event to get context for.
167
- # @param limit [Fixnum] Maximum number of events to retrieve.
168
- # @return [Hash] The returned hash contains information about the events
169
- # happening before and after the specified event, as well as start and
170
- # end timestamps and state information for the event.
171
- def get_event_context(room, event, limit = 10)
172
- room = get_room_id room if room.start_with? '#'
173
- make_request(
174
- :get,
175
- "/rooms/#{room}/context/#{event}",
176
- params: { limit: limit }
177
- ).parsed_response
178
- end
179
90
 
180
- # Get the members of a room.
181
- #
182
- # @param room [String] The room to query.
183
- # @return [Array] An array of users that are in this room.
184
- def get_room_members(room)
185
- room = get_room_id room if room.start_with? '#'
186
- make_request(:get, "/rooms/#{room}/members")['chunk']
91
+ @session = Api::Session.new self
92
+ @users = Api::Users.new self
93
+ @rooms = Api::Rooms.new self
94
+ @media = Api::Media.new self
95
+ @push = Api::Push.new self
187
96
  end
188
97
 
189
- # Get a list of messages from a room.
190
- #
191
- # @param room [String] The room to get messages from.
192
- # @param from [String] Token to return events from.
193
- # @param direction ['b', 'f'] Direction to return events from.
194
- # @param limit [Fixnum] Maximum number of events to return.
195
- # @return [Hash] A hash containing the messages, as well as `start` and
196
- # `end` tokens for pagination.
197
- def get_room_messages(room, from, direction, limit = 10)
198
- room = get_room_id room if room.start_with? '#'
98
+ # Gets supported API versions from the server.
99
+ # @return [Array<String>] an array with the supported versions.
100
+ def versions
199
101
  make_request(
200
- :get,
201
- "/rooms/#{room}/messages",
202
- params: {
203
- from: from,
204
- dir: direction,
205
- limit: limit
206
- }
207
- ).parsed_response
102
+ :get, '/versions', base: "#{@homeserver}/_matrix/client"
103
+ )['versions']
208
104
  end
209
105
 
210
- # Sends a message object to a room.
211
- #
212
- # @param room [String] The room to send to.
213
- # @param content [Hash] The message content to send.
214
- # @param type [String] The type of message to send.
215
- # @return [String] The event ID of the sent message is returned.
216
- # @see #send_message_type
217
- # @see #send_message
218
- # @see #send_emote
219
- # @see #send_notice
220
- # @see #send_html
221
- def send_message_raw(room, content, type = 'm.room.message')
222
- room = get_room_id room if room.start_with? '#'
223
- @transaction_id += 1
224
- make_request(
225
- :put,
226
- "/rooms/#{room}/send/#{type}/#{@transaction_id}",
227
- content: content
228
- )['event_id']
229
- end
230
-
231
- # A helper method to send a simple message construct.
232
- #
233
- # @param room [String] The room to send the message to.
234
- # @param content [String] The message to send.
235
- # @param type [String] The type of message this is.
236
- # @return (see #send_message_raw)
237
- # @see #send_message
238
- # @see #send_notice
239
- # @see #send_emote
240
- # @see #send_html
241
- def send_message_type(room, content, type = 'm.text')
242
- send_message_raw room, msgtype: type, body: content
243
- end
244
-
245
- # Sends a plaintext message to a room.
246
- #
247
- # @param room [String] The room to send to.
248
- # @param content [String] The message to send.
249
- # @return (see #send_message_raw)
250
- #
251
- # @example Sending a simple message
252
- # send_message('#party:matrix.org', 'Hello everyone!')
253
- def send_message(room, content)
254
- send_message_type room, content
255
- end
256
-
257
- # Sends a notice message to a room.
258
- #
259
- # @param room [String] The room to send to.
260
- # @param content [String] The message to send.
261
- # @return (see #send_message_raw)
262
- #
263
- # @example Sending a notice
264
- # send_notice('#stuff:matrix.org', 'This is a notice')
265
- def send_notice(room, content)
266
- send_message_type room, content, 'm.notice'
267
- end
268
-
269
- # Sends an emote to a room.
270
- #
271
- # `/me <message here>`
272
- #
273
- # @param room [String] The room to send to.
274
- # @param content [String] The emote to send.
275
- # @return (see #send_message_raw)
276
- #
277
- # @example Sending an emote
278
- # # Will show up as: "* <user> is having fun"
279
- # send_emote('#party:matrix.org', 'is having fun')
280
- def send_emote(room, content)
281
- send_message_type room, content, 'm.emote'
282
- end
283
-
284
- # Sends a message formatted using HTML markup.
285
- #
286
- # The `body` field in the content will have the HTML stripped out, and is
287
- # usually presented in clients that don't support the formatting.
288
- #
289
- # The `formatted_body` field in the content will contain the actual HTML
290
- # formatted message (as passed to the `html` parameter).
291
- #
292
- # @param room [String] The room to send to.
293
- # @param html [String] The HTML formatted text to send.
294
- # @return (see #send_message_raw)
295
- #
296
- # @example Sending an HTML message
297
- # send_html('#html:matrix.org', '<strong>Hello</strong> <em>world</em>!')
298
- def send_html(room, html)
299
- send_message_raw(
300
- room,
301
- msgtype: 'm.text',
302
- format: 'org.matrix.custom.html',
303
- body: html.gsub(%r{</?[^>]*?>}, ''), # TODO: Make this better
304
- formatted_body: html
305
- )
306
- end
307
-
308
- # @overload get_room_state(room)
309
- # Get state events for the current state of a room.
310
- # @param room [String] The room to get events for.
311
- # @return [Array] An array with state events for the room.
312
- # @overload get_room_state(room, type)
313
- # Get the contents of a specific kind of state in the room.
314
- # @param room [String] The room to get the data from.
315
- # @param type [String] The type of state to get.
316
- # @return [Hash] Information about the state type.
317
- # @overload get_room_state(room, type, key)
318
- # Get the contents of a specific kind of state including only the
319
- # specified key in the result.
320
- # @param room [String] The room to get the data from.
321
- # @param type [String] The type of state to get.
322
- # @param key [String] The key of the state to look up.
323
- # @return [Hash] Information about the requested state.
324
- def get_room_state(room, type = nil, key = nil)
325
- room = get_room_id room if room.start_with? '#'
326
-
327
- if type && key
328
- make_request(
329
- :get,
330
- "/rooms/#{room}/state/#{type}/#{key}"
331
- ).parsed_response
332
- elsif type
333
- make_request(:get, "/rooms/#{room}/state/#{type}").parsed_response
334
- else
335
- make_request(:get, "/rooms/#{room}/state").parsed_response
336
- end
337
- end
338
-
339
- # Sends a message to the server informing it about a user having started
340
- # or stopped typing.
341
- #
342
- # @param room [String] The affected room.
343
- # @param user [String] The user that started or stopped typing.
344
- # @param typing [Boolean] Whether the user is typing.
345
- # @param duration [Fixnum] How long the user will be typing for
346
- # (in milliseconds).
347
- # @return [Boolean] `true` if the message sent successfully, otherwise
348
- # `false`.
349
- def send_typing(room, user, typing = true, duration = 30_000)
350
- room = get_room_id room if room.start_with? '#'
351
-
352
- content = { typingState: { typing: typing, timeout: duration } }
353
-
354
- make_request(
355
- :put,
356
- "/rooms/#{room}/typing/#{user}",
357
- content: content
358
- ).code == 200
106
+ # Performs a whois lookup on the specified user.
107
+ # @param user [String] The user to look up.
108
+ # @return [Hash] Information about the user.
109
+ def whois(user)
110
+ make_request(:get, "/admin/whois/#{user}").parsed_response
359
111
  end
360
112
 
361
113
  # Synchronize with the latest state on the server.
@@ -383,275 +135,71 @@ module Chatrix
383
135
  options[:since] = since if since
384
136
  options[:set_presence] = 'offline' unless set_presence
385
137
  options[:timeout] = timeout if timeout
386
-
387
- if filter.is_a? Integer
388
- options[:filter] = filter
389
- elsif filter.is_a? Hash
390
- options[:filter] = URI.encode filter.to_json
391
- end
138
+ options[:filter] = parse_filter filter
392
139
 
393
140
  make_request(:get, '/sync', params: options).parsed_response
394
141
  end
395
142
 
396
- # Joins a room on the homeserver.
397
- #
398
- # @param room [String] The room to join.
399
- # @param third_party_signed [Hash,nil] If provided, the homeserver must
400
- # verify that it matches a pending `m.room.third_party_invite` event in
401
- # the room, and perform key validity checking if required by the event.
402
- # @return [String] The ID of the room that was joined is returned.
403
- def join(room, third_party_signed = nil)
404
- if third_party_signed
405
- make_request(
406
- :post,
407
- "/join/#{room}",
408
- content: { third_party_signed: third_party_signed }
409
- )['room_id']
410
- else
411
- make_request(:post, "/join/#{room}")['room_id']
412
- end
143
+ # Gets the definition for a filter.
144
+ # @param user [String] The user that has the filter.
145
+ # @param filter [String] The ID of the filter to get.
146
+ # @return [Hash] The filter definition.
147
+ def get_filter(user, filter)
148
+ make_request(:get, "/user/#{user}/filter/#{filter}").parsed_response
413
149
  end
414
150
 
415
- # Kicks and bans a user from a room.
416
- #
417
- # @param room [String] The room to ban the user from.
418
- # @param user [String] The user to ban.
419
- # @param reason [String] Reason why the ban was made.
420
- # @return [Boolean] `true` if the ban was carried out successfully,
421
- # otherwise `false`.
422
- #
423
- # @example Banning a spammer
424
- # ban('#haven:matrix.org', '@spammer:spam.com', 'Spamming the room')
425
- def ban(room, user, reason)
426
- room = get_room_id room if room.start_with? '#'
427
- make_request(
428
- :post,
429
- "/rooms/#{room}/ban",
430
- content: { reason: reason, user_id: user }
431
- ).code == 200
151
+ # Uploads a filter to the server.
152
+ # @param user [String] The user to upload the filter as.
153
+ # @param filter [Hash] The filter definition.
154
+ # @return [String] The ID of the created filter.
155
+ def create_filter(user, filter)
156
+ make_request(:post, "/user/#{user}/filter", content: filter)['filter_id']
432
157
  end
433
158
 
434
- # Forgets about a room.
435
- #
436
- # @param room [String] The room to forget about.
437
- # @return [Boolean] `true` if the room was forgotten successfully,
438
- # otherwise `false`.
439
- def forget(room)
440
- room = get_room_id room if room.start_with? '#'
441
- make_request(:post, "/rooms/#{room}/forget").code == 200
442
- end
443
-
444
- # Kicks a user from a room.
445
- #
446
- # This does not ban the user, they can rejoin unless the room is
447
- # invite-only, in which case they need a new invite to join back.
448
- #
449
- # @param room [String] The room to kick the user from.
450
- # @param user [String] The user to kick.
451
- # @param reason [String] The reason for the kick.
452
- # @return [Boolean] `true` if the user was successfully kicked,
453
- # otherwise `false`.
454
- #
455
- # @example Kicking an annoying user
456
- # kick('#fun:matrix.org', '@anon:4chan.org', 'Bad cropping')
457
- def kick(room, user, reason)
458
- room = get_room_id room if room.start_with? '#'
459
- make_request(
460
- :post,
461
- "/rooms/#{room}/kick",
462
- content: { reason: reason, user_id: user }
463
- ).code == 200
464
- end
465
-
466
- # Leaves a room (but does not forget about it).
467
- #
468
- # @param room [String] The room to leave.
469
- # @return [Boolean] `true` if the room was left successfully,
470
- # otherwise `false`.
471
- def leave(room)
472
- room = get_room_id room if room.start_with? '#'
473
- make_request(:post, "/rooms/#{room}/leave").code == 200
474
- end
475
-
476
- # Unbans a user from a room.
477
- #
478
- # @param room [String] The room to unban the user from.
479
- # @param user [String] The user to unban.
480
- # @return [Boolean] `true` if the user was successfully unbanned,
481
- # otherwise `false`.
482
- def unban(room, user)
483
- room = get_room_id room if room.start_with? '#'
159
+ # Performs a full text search on the server.
160
+ # @param from [String] Where to return events from, if given. This can be
161
+ # obtained from previous calls to {#search}.
162
+ # @param options [Hash] Search options, see the official documentation
163
+ # for details on how to structure this.
164
+ # @return [Hash] the search results.
165
+ def search(from: nil, options: {})
484
166
  make_request(
485
- :post,
486
- "/rooms/#{room}/unban",
487
- content: { user_id: user }
488
- ).code == 200
489
- end
490
-
491
- # Performs a login attempt.
492
- #
493
- # @note A successful login will update the {#access_token access_token}
494
- # to the new one returned from the login response.
495
- #
496
- # @param method [String] The method to use for logging in.
497
- # For user/password combination, this should be `m.login.password`.
498
- # @param options [Hash{String=>String}] Options to pass for logging in.
499
- # For a password login, this should contain a key `:user` for the
500
- # username, and a key `:password` for the password.
501
- # @return [Hash] The response from the server. A successful login will
502
- # return a hash containing the user id, access token, and homeserver.
503
- #
504
- # @example Logging in with username and password
505
- # login('m.login.password', user: '@snoo:reddit.com', password: 'hunter2')
506
- def login(method, options = {})
507
- response = make_request(
508
- :post,
509
- '/login',
510
- content: { type: method }.merge!(options)
511
- )
512
-
513
- # Update the local access token
514
- @access_token = response['access_token']
515
-
516
- response.parsed_response
517
- end
518
-
519
- # Logs out.
520
- #
521
- # @note This will **invalidate the access token**. It will no longer be
522
- # valid for API calls.
523
- #
524
- # @return [Hash] The response from the server (an empty hash).
525
- def logout
526
- response = make_request :post, '/logout'
527
-
528
- # A successful logout means the access token has been invalidated
529
- @access_token = nil
530
-
531
- response.parsed_response
532
- end
533
-
534
- # Gets a new access token to use for API calls when the current one
535
- # expires.
536
- #
537
- # @note On success, the internal {#access_token access_token} will be
538
- # updated automatically for use in subsequent API calls.
539
- #
540
- # @param token [String,nil] The `refresh_token` to provide for the server
541
- # when requesting a new token. If not set, the internal refresh and
542
- # access tokens will be used.
543
- # @return [Hash] The response hash from the server will contain the new
544
- # access token and a refresh token to use the next time a new access
545
- # token is needed.
546
- def refresh(token = nil)
547
- refresh_token = token || @refresh_token || @access_token
548
-
549
- response = make_request(
550
- :post,
551
- '/tokenrefresh',
552
- content: { refresh_token: refresh_token }
553
- )
554
-
555
- @access_token = response['access_token']
556
- @refresh_token = response['refresh_token']
557
-
558
- response.parsed_response
167
+ :post, '/search', params: { next_batch: from }, content: options
168
+ ).parsed_response
559
169
  end
560
170
 
561
- # Gets the presence list for a user.
562
- #
563
- # @param user [String] The user whose list to get.
564
- # @return [Array] A list of presences for this user.
565
- #
566
- # @todo The official documentation on this endpoint is weird, what does
567
- # this really do?
568
- def get_presence_list(user)
569
- make_request(:get, "/presence/list/#{user}").parsed_response
171
+ # Gets information about the TURN server. Contains credentials and
172
+ # server URIs for connecting.
173
+ # @return [Hash] TURN server details.
174
+ def turn_server
175
+ make_request(:get, '/voip/turnServer').parsed_response
570
176
  end
571
177
 
572
- # Adds or removes users from a user's presence list.
573
- #
574
- # @param user [String] The user whose list to modify.
575
- # @param data [Hash{String=>Array<String>}] Contains two arrays,
576
- # `invite` and `drop`. Users listed in the `invite` array will be
577
- # invited to join the presence list. Users listed in the `drop` array
578
- # will be removed from the presence list.
579
- # Note that both arrays are not required but at least one must be
580
- # present.
581
- # @return [Boolean] `true` if the list was successfully updated,
582
- # otherwise `false`.
178
+ # Helper method for performing requests to the homeserver.
583
179
  #
584
- # @example Add and remove two users
585
- # update_presence_list(
586
- # '@me:home.org',
587
- # {
588
- # invite: ['@friend:home.org'],
589
- # drop: ['@enemy:other.org']
590
- # }
591
- # )
592
- def update_presence_list(user, data)
593
- make_request(
594
- :post,
595
- "/presence/list/#{user}",
596
- content: { presence_diff: data }
597
- ).code == 200
598
- end
599
-
600
- # Gets the presence status of a user.
180
+ # @param method [Symbol] HTTP request method to use. Use only symbols
181
+ # available as keys in {METHODS}.
182
+ # @param path [String] The API path to query, relative to the base
183
+ # API path, eg. `/login`.
184
+ # @param opts [Hash] Additional request options.
601
185
  #
602
- # @param user [String] The user to query.
603
- # @return [Hash] Hash with information about the user's presence,
604
- # contains information indicating if they are available and when
605
- # they were last active.
606
- def get_presence_status(user)
607
- make_request(:get, "/presence/#{user}/status").parsed_response
608
- end
609
-
610
- # Updates the presence status of a user.
186
+ # @option opts [Hash] :params Additional parameters to include in the
187
+ # query string (part of the URL, not put in the request body).
188
+ # @option opts [Hash,#read] :content Content to put in the request body.
189
+ # If set, must be a Hash or a stream object.
190
+ # @option opts [Hash{String => String}] :headers Additional headers to
191
+ # include in the request.
192
+ # @option opts [String,nil] :base If this is set, it will be used as the
193
+ # base URI for the request instead of the default (`@base_uri`).
611
194
  #
612
- # @note Only the user for whom the {#access_token access_token} is
613
- # valid for can have their presence updated.
195
+ # @yield [fragment] HTTParty will call the block during the request.
614
196
  #
615
- # @param user [String] The user to update.
616
- # @param status [String] The new status to set. Eg. `'online'`
617
- # or `'offline'`.
618
- # @param message [String,nil] If set, associates a message with the status.
619
- # @return [Boolean] `true` if the presence was updated successfully,
620
- # otherwise `false`.
621
- def update_presence_status(user, status, message = nil)
622
- content = { presenceState: { presence: status } }
623
-
624
- content[:presenceState][:status_msg] = message if message
625
-
626
- make_request(
627
- :put,
628
- "/presence/#{user}/status",
629
- content: content
630
- ).code == 200
631
- end
197
+ # @return [HTTParty::Response] The HTTParty response object.
198
+ def make_request(method, path, opts = {}, &block)
199
+ path = (opts[:base] || @base_uri) + URI.encode(path)
200
+ options = make_options opts[:params], opts[:content], opts[:headers]
632
201
 
633
- # Get the list of public rooms on the server.
634
- #
635
- # The `start` and `end` values returned in the result can be passed to
636
- # `from` and `to`, for pagination purposes.
637
- #
638
- # @param from [String] The stream token to start looking from.
639
- # @param to [String] The stream token to stop looking at.
640
- # @param limit [Fixnum] Maximum number of results to return in one request.
641
- # @param direction ['f', 'b'] Direction to look in.
642
- # @return [Hash] Hash containing the list of rooms (in the `chunk` value),
643
- # and pagination parameters `start` and `end`.
644
- def rooms(from: 'START', to: 'END', limit: 10, direction: 'b')
645
- make_request(
646
- :get,
647
- '/publicRooms',
648
- params: {
649
- from: start,
650
- to: to,
651
- limit: limit,
652
- dir: direction
653
- }
654
- ).parsed_response
202
+ parse_response METHODS[method].call(path, options, &block)
655
203
  end
656
204
 
657
205
  private
@@ -663,37 +211,27 @@ module Chatrix
663
211
  #
664
212
  # @param params [Hash{String=>String},nil] Query parameters to add to
665
213
  # the options hash.
666
- # @param content [Hash,nil] Request content to add to the options hash.
214
+ # @param content [Hash,#read,nil] Request content. Can be a hash,
215
+ # stream, or `nil`.
667
216
  # @return [Hash] Options hash ready to be passed into a server request.
668
- def make_request_options(params, content)
669
- options = {
670
- query: @access_token ? { access_token: @access_token } : {}
671
- }
672
-
673
- options[:query].merge!(params) if params.is_a? Hash
674
- options[:body] = content.to_json if content.is_a? Hash
675
-
676
- options
217
+ def make_options(params, content, headers = {})
218
+ { headers: headers }.tap do |o|
219
+ o[:query] = @access_token ? { access_token: @access_token } : {}
220
+ o[:query].merge!(params) if params.is_a? Hash
221
+ o.merge! make_body content
222
+ end
677
223
  end
678
224
 
679
- # Helper method for performing requests to the homeserver.
680
- #
681
- # @param method [Symbol] HTTP request method to use. Use only symbols
682
- # available as keys in {METHODS}.
683
- # @param path [String] The API path to query, relative to the base
684
- # API path, eg. `/login`.
685
- # @param params [Hash{String=>String}] Additional parameters to include
686
- # in the query string (part of the URL, not put in the request body).
687
- # @param content [Hash] Content to put in the request body, must
688
- # be serializable to json via `#to_json`.
689
- # @yield [fragment] HTTParty will call the block during the request.
690
- #
691
- # @return [HTTParty::Response] The HTTParty response object.
692
- def make_request(method, path, params: nil, content: nil, &block)
693
- path = @base_uri + URI.encode(path)
694
- options = make_request_options params, content
695
-
696
- parse_response METHODS[method].call(path, options, &block)
225
+ # Create a hash with body content based on the type of `content`.
226
+ # @param content [Hash,#read,Object] Some kind of content to put into
227
+ # the request body. Can be a Hash, stream object, or other kind of
228
+ # object.
229
+ # @return [Hash{Symbol => Object}] A hash with the relevant body key
230
+ # and value.
231
+ def make_body(content)
232
+ key = content.respond_to?(:read) ? :body_stream : :body
233
+ value = content.is_a? Hash ? content.to_json : content
234
+ { key => value }
697
235
  end
698
236
 
699
237
  # Parses a HTTParty Response object and returns it if it was successful.
@@ -702,29 +240,34 @@ module Chatrix
702
240
  # @return [HTTParty::Response] The same response object that was passed
703
241
  # in, if the request was successful.
704
242
  #
243
+ # @raise [RequestError] If a `400` response code was returned from the
244
+ # request.
245
+ # @raise [AuthenticationError] If a `401` response code was returned
246
+ # from the request.
705
247
  # @raise [ForbiddenError] If a `403` response code was returned from the
706
248
  # request.
707
249
  # @raise [NotFoundError] If a `404` response code was returned from the
708
250
  # request.
709
- # @raise [RequestError] If an error object was returned from the server.
710
- # @raise [ApiError] If an unparsable error was returned from the server.
711
- #
712
- # rubocop:disable MethodLength
251
+ # @raise [RateLimitError] If a `429` response code was returned from the
252
+ # request.
253
+ # @raise [ApiError] If an unknown response code was returned from the
254
+ # request.
713
255
  def parse_response(response)
714
256
  case response.code
715
257
  when 200 # OK
716
258
  response
717
- when 403 # Forbidden
718
- raise ForbiddenError, 'You do not have access to that resource'
719
- when 404 # Not found
720
- raise NotFoundError, 'The specified resource could not be found'
721
259
  else
722
- if %w{(errcode), (error)}.all? { |k| response.include? k }
723
- raise RequestError.new(response.parsed_response), 'Request failed'
724
- end
725
-
726
- raise ApiError, 'Unknown API error occurred when carrying out request'
260
+ handler = ERROR_HANDLERS[response.code]
261
+ raise handler.first.new(response.parsed_response), handler.last
727
262
  end
728
263
  end
264
+
265
+ # Parses a filter object for use in a query string.
266
+ # @param filter [String,Hash] The filter object to parse.
267
+ # @return [String] Query-friendly filter object. Or the `filter`
268
+ # parameter as-is if it failed to parse.
269
+ def parse_filter(filter)
270
+ filter.is_a?(Hash) ? URI.encode(filter.to_json) : filter
271
+ end
729
272
  end
730
273
  end