chatrix 1.0.0.pre → 1.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.
@@ -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