chatrix 1.0.0.pre

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f4d840405e59e950232bf4a82a906c7395d3c581
4
+ data.tar.gz: 26d630e954f5cc66b8bdd9dd5f01132ef6ca84af
5
+ SHA512:
6
+ metadata.gz: bd3aa274eb0b3da53e24f8cfb95b354025513a421fd094172a912791de0962e171f9817906bb811bdc125047f415173c78a612ca7f9179bc1fc34ed710cf9e51
7
+ data.tar.gz: 61275fb3232b31c24b3e0d215107655ecbd65d4836b04d97b3d8c6a79c7896100428f59a509eb1e27c863597c08dc2455e265e75d53b7543b67ef48cecb39306
@@ -0,0 +1,12 @@
1
+ [*]
2
+ charset = utf-8
3
+ indent_size = 2
4
+ indent_style = space
5
+ insert_final_newline = true
6
+ trim_trailing_whitespace = true
7
+
8
+ [*.json]
9
+ indent_size = 2
10
+
11
+ [*.{yaml,yml}]
12
+ indent_size = 2
@@ -0,0 +1,167 @@
1
+ # Created by https://www.gitignore.io/api/ruby,git,windows,linux,osx,vim,sublimetext
2
+
3
+ ### Ruby ###
4
+ *.gem
5
+ *.rbc
6
+ /.config
7
+ /coverage/
8
+ /InstalledFiles
9
+ /pkg/
10
+ /spec/reports/
11
+ /spec/examples.txt
12
+ /test/tmp/
13
+ /test/version_tmp/
14
+ /tmp/
15
+
16
+ # Used by dotenv library to load environment variables.
17
+ # .env
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ Gemfile.lock
49
+ .ruby-version
50
+ .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+
56
+ ### Git ###
57
+ *.orig
58
+
59
+
60
+ ### Windows ###
61
+ # Windows image file caches
62
+ Thumbs.db
63
+ ehthumbs.db
64
+
65
+ # Folder config file
66
+ Desktop.ini
67
+
68
+ # Recycle Bin used on file shares
69
+ $RECYCLE.BIN/
70
+
71
+ # Windows Installer files
72
+ *.cab
73
+ *.msi
74
+ *.msm
75
+ *.msp
76
+
77
+ # Windows shortcuts
78
+ *.lnk
79
+
80
+
81
+ ### Linux ###
82
+ *~
83
+
84
+ # temporary files which can be created if a process still has a handle open of a deleted file
85
+ .fuse_hidden*
86
+
87
+ # KDE directory preferences
88
+ .directory
89
+
90
+ # Linux trash folder which might appear on any partition or disk
91
+ .Trash-*
92
+
93
+
94
+ ### OSX ###
95
+ *.DS_Store
96
+ .AppleDouble
97
+ .LSOverride
98
+
99
+ # Icon must end with two \r
100
+ Icon
101
+
102
+
103
+ # Thumbnails
104
+ ._*
105
+
106
+ # Files that might appear in the root of a volume
107
+ .DocumentRevisions-V100
108
+ .fseventsd
109
+ .Spotlight-V100
110
+ .TemporaryItems
111
+ .Trashes
112
+ .VolumeIcon.icns
113
+ .com.apple.timemachine.donotpresent
114
+
115
+ # Directories potentially created on remote AFP share
116
+ .AppleDB
117
+ .AppleDesktop
118
+ Network Trash Folder
119
+ Temporary Items
120
+ .apdisk
121
+
122
+
123
+ ### Vim ###
124
+ # swap
125
+ [._]*.s[a-w][a-z]
126
+ [._]s[a-w][a-z]
127
+ # session
128
+ Session.vim
129
+ # temporary
130
+ .netrwhist
131
+ *~
132
+ # auto-generated tag files
133
+ tags
134
+
135
+
136
+ ### SublimeText ###
137
+ # cache files for sublime text
138
+ *.tmlanguage.cache
139
+ *.tmPreferences.cache
140
+ *.stTheme.cache
141
+
142
+ # workspace files are user-specific
143
+ *.sublime-workspace
144
+
145
+ # project files should be checked into the repository, unless a significant
146
+ # proportion of contributors will probably not be using SublimeText
147
+ # *.sublime-project
148
+
149
+ # sftp configuration file
150
+ sftp-config.json
151
+
152
+ # Package control specific files
153
+ Package Control.last-run
154
+ Package Control.ca-list
155
+ Package Control.ca-bundle
156
+ Package Control.system-ca-bundle
157
+ Package Control.cache/
158
+ Package Control.ca-certs/
159
+ bh_unicode_properties.cache
160
+
161
+ # Sublime-github package stores a github token in this file
162
+ # https://packagecontrol.io/packages/sublime-github
163
+ GitHub.sublime-settings
164
+
165
+ ### Custom rules ###
166
+ # Ignore access token file
167
+ access_token.txt
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,8 @@
1
+ sudo: false
2
+
3
+ language: ruby
4
+
5
+ rvm:
6
+ - 2.3.1
7
+
8
+ before_install: gem install bundler -v 1.12.5
@@ -0,0 +1,5 @@
1
+ --protected
2
+ --private
3
+ --markup-provider=redcarpet
4
+ --markup=markdown
5
+ 'lib/**/*.rb' - README.md LICENSE
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ratrix.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 by Adam Hellberg.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,63 @@
1
+ chatrix
2
+ =======
3
+
4
+ A Ruby implementation of the [Matrix][matrix] API.
5
+
6
+ ## License
7
+
8
+ Copyright (c) 2016 by Adam Hellberg.
9
+
10
+ chatrix is licensed under the [MIT License][license-url], see the file
11
+ `LICENSE` for more information.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'chatrix'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install chatrix
28
+
29
+ ## Usage
30
+
31
+ This implementation is currently very basic and exposes all the endpoints
32
+ in the `Matrix` class. Example usage:
33
+
34
+ ```ruby
35
+ # Uses the standard matrix.org homeserver
36
+ api = Chatrix::Matrix.new 'my secret token'
37
+
38
+ # Join may raise ForbiddenError if client does not have permission
39
+ # to join the room
40
+ if id = api.join '#myroom:myserver.org'
41
+ api.send_message id, 'Hello everyone!'
42
+ end
43
+ ```
44
+
45
+ Currently there is no asynchronous calls or built-in handling of
46
+ rate-limiting.
47
+
48
+ ## Development
49
+
50
+ After checking out the repo, run `bin/setup` to install dependencies.
51
+ Then, run `rake spec` to run the tests. You can also run `bin/console`
52
+ for an interactive prompt that will allow you to experiment.
53
+
54
+ To install this gem onto your local machine, run `bundle exec rake install`.
55
+
56
+ ## Contributing
57
+
58
+ Bug reports and pull requests are welcome on [GitHub][issues].
59
+
60
+ [project]: https://github.com/Sharparam/chatrix
61
+ [issues]: https://github.com/Sharparam/chatrix/issues
62
+ [matrix]: http://matrix.org
63
+ [license-url]: http://opensource.org/licenses/MIT
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ require 'rubocop/rake_task'
7
+
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: [:spec, :rubocop]
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'chatrix'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'pry'
14
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chatrix/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'chatrix'
8
+ spec.version = Chatrix::VERSION
9
+ spec.authors = ['Adam Hellberg']
10
+ spec.email = ['sharparam@sharparam.com']
11
+
12
+ spec.summary = 'Ruby implementation of the Matrix API'
13
+ # spec.description = %q{}
14
+ spec.homepage = 'https://github.com/Sharparam/chatrix'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+
21
+ spec.bindir = 'exe'
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_runtime_dependency 'httparty', '~> 0.13'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.12'
28
+ spec.add_development_dependency 'rake', '~> 10.0'
29
+ spec.add_development_dependency 'rspec', '~> 3.0'
30
+ spec.add_development_dependency 'pry', '~> 0.10'
31
+ spec.add_development_dependency 'yard', '~> 0.8'
32
+ spec.add_development_dependency 'redcarpet', '~> 3.3'
33
+ spec.add_development_dependency 'rubocop', '~> 0.40.0'
34
+ end
@@ -0,0 +1,7 @@
1
+ require 'chatrix/version'
2
+ require 'chatrix/errors'
3
+ require 'chatrix/matrix'
4
+
5
+ # Encapsulates all gem functionality.
6
+ module Chatrix
7
+ end
@@ -0,0 +1,54 @@
1
+ module Chatrix
2
+ # Generic faults from the library.
3
+ class ChatrixError < StandardError
4
+ end
5
+
6
+ # Errors that stem from an API call.
7
+ class ApiError < ChatrixError
8
+ end
9
+
10
+ # Error raised when a request is badly formatted.
11
+ class RequestError < ApiError
12
+ attr_reader :code, :api_message
13
+
14
+ def initialize(error)
15
+ @code = error['errcode']
16
+ @api_message = error['error']
17
+ end
18
+ end
19
+
20
+ # Raised when a resource is requested that the user does not have access to.
21
+ class ForbiddenError < ApiError
22
+ end
23
+
24
+ # Raised when a resource is not found.
25
+ class NotFoundError < ApiError
26
+ end
27
+
28
+ # Raised when a user is not found.
29
+ class UserNotFoundError < NotFoundError
30
+ attr_reader :username
31
+
32
+ def initialize(username)
33
+ @username = username
34
+ end
35
+ end
36
+
37
+ # Raised when a user's avatar is not found.
38
+ class AvatarNotFoundError < NotFoundError
39
+ attr_reader :username
40
+
41
+ def initialize(username)
42
+ @username = username
43
+ end
44
+ end
45
+
46
+ # Raised when a room is not found.
47
+ class RoomNotFoundError < NotFoundError
48
+ attr_reader :room
49
+
50
+ def initialize(room)
51
+ @room = room
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,730 @@
1
+ require 'chatrix/errors'
2
+
3
+ require 'httparty'
4
+
5
+ module Chatrix
6
+ # Provides an interface to the Matrix API on a homeserver.
7
+ #
8
+ # Detailed information about the data structures is not included here and
9
+ # can be found on the
10
+ # {http://matrix.org/docs/api/client-server Matrix API page}.
11
+ #
12
+ # @note Any of the methods may raise the errors listed in {#parse_response}.
13
+ # Consider this when calling the methods.
14
+ # @note Endpoints that require a room ID in the official API can be passed
15
+ # a room alias in this implementation, the room ID will be automatically
16
+ # looked up from the homeserver.
17
+ #
18
+ # @todo Try to extract functionality to make this class smaller.
19
+ #
20
+ # rubocop:disable ClassLength
21
+ class Matrix
22
+ include HTTParty
23
+
24
+ headers('User-Agent' => "chatrix/#{Chatrix::VERSION}",
25
+ 'Content-Type' => 'application/json',
26
+ 'Accept' => 'application/json')
27
+
28
+ # Maps HTTP methods to their respective HTTParty method.
29
+ METHODS = {
30
+ get: -> (path, options, &block) { get path, options, &block },
31
+ put: -> (path, options, &block) { put path, options, &block },
32
+ post: -> (path, options, &block) { post path, options, &block },
33
+ delete: -> (path, options, &block) { delete path, options, &block }
34
+ }.freeze
35
+
36
+ # Default homeserver used if none is specified.
37
+ DEFAULT_HOMESERVER = 'https://matrix.org'.freeze
38
+
39
+ # API path used.
40
+ API_PATH = '/_matrix/client/r0'.freeze
41
+
42
+ # @!attribute access_token
43
+ # @return [String] The access token used when performing requests
44
+ # to the homeserver.
45
+ attr_accessor :access_token
46
+
47
+ # @!attribute [r] homeserver
48
+ # @return [String] The homeserver for this API object.
49
+ attr_reader :homeserver
50
+
51
+ # Initializes a new instance of Matrix.
52
+ #
53
+ # @param token [String] The access token to use.
54
+ # @param homeserver [String] The homeserver to make requests to.
55
+ def initialize(token = nil, homeserver = DEFAULT_HOMESERVER)
56
+ @homeserver = homeserver
57
+ @base_uri = @homeserver + API_PATH
58
+ @transaction_id = 0
59
+ @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
+
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']
187
+ end
188
+
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? '#'
199
+ make_request(
200
+ :get,
201
+ "/rooms/#{room}/messages",
202
+ params: {
203
+ from: from,
204
+ dir: direction,
205
+ limit: limit
206
+ }
207
+ ).parsed_response
208
+ end
209
+
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
359
+ end
360
+
361
+ # Synchronize with the latest state on the server.
362
+ #
363
+ # For initial sync, call this method with the `since` parameter
364
+ # set to `nil`.
365
+ #
366
+ # @param filter [String,Hash] The ID of a filter to use, or provided
367
+ # directly as a hash.
368
+ # @param since [String,nil] A point in time to continue sync from.
369
+ # Will retrieve a snapshot of the state if not set, which will also
370
+ # provide a `next_batch` value to use for `since` in the next call.
371
+ # @param full_state [Boolean] If `true`, all state events will be returned
372
+ # for all rooms the user is a member of.
373
+ # @param set_presence [Boolean] If `true`, the user performing this request
374
+ # will have their presence updated to show them as being online.
375
+ # @param timeout [Fixnum] Maximum time (in milliseconds) to wait before
376
+ # the request is aborted.
377
+ # @return [Hash] The initial snapshot of the state (if no `since` value
378
+ # was provided), or a delta to use for updating state.
379
+ def sync(filter: nil, since: nil, full_state: false,
380
+ set_presence: true, timeout: 30_000)
381
+ options = { full_state: full_state }
382
+
383
+ options[:since] = since if since
384
+ options[:set_presence] = 'offline' unless set_presence
385
+ 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
392
+
393
+ make_request(:get, '/sync', params: options).parsed_response
394
+ end
395
+
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
413
+ end
414
+
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
432
+ end
433
+
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? '#'
484
+ 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
559
+ end
560
+
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
570
+ end
571
+
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`.
583
+ #
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.
601
+ #
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.
611
+ #
612
+ # @note Only the user for whom the {#access_token access_token} is
613
+ # valid for can have their presence updated.
614
+ #
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
632
+
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
655
+ end
656
+
657
+ private
658
+
659
+ # Create an options Hash to pass to a server request.
660
+ #
661
+ # This method embeds the {#access_token access_token} into the
662
+ # query parameters.
663
+ #
664
+ # @param params [Hash{String=>String},nil] Query parameters to add to
665
+ # the options hash.
666
+ # @param content [Hash,nil] Request content to add to the options hash.
667
+ # @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
677
+ end
678
+
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)
697
+ end
698
+
699
+ # Parses a HTTParty Response object and returns it if it was successful.
700
+ #
701
+ # @param response [HTTParty::Response] The response object to parse.
702
+ # @return [HTTParty::Response] The same response object that was passed
703
+ # in, if the request was successful.
704
+ #
705
+ # @raise [ForbiddenError] If a `403` response code was returned from the
706
+ # request.
707
+ # @raise [NotFoundError] If a `404` response code was returned from the
708
+ # 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
713
+ def parse_response(response)
714
+ case response.code
715
+ when 200 # OK
716
+ 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
+ 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'
727
+ end
728
+ end
729
+ end
730
+ end
@@ -0,0 +1,4 @@
1
+ module Chatrix
2
+ # The current version of this gem.
3
+ VERSION = '1.0.0.pre'.freeze
4
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chatrix
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.pre
5
+ platform: ruby
6
+ authors:
7
+ - Adam Hellberg
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-06-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.13'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.12'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.10'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.10'
83
+ - !ruby/object:Gem::Dependency
84
+ name: yard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.8'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.8'
97
+ - !ruby/object:Gem::Dependency
98
+ name: redcarpet
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.40.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.40.0
125
+ description:
126
+ email:
127
+ - sharparam@sharparam.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".editorconfig"
133
+ - ".gitignore"
134
+ - ".rspec"
135
+ - ".travis.yml"
136
+ - ".yardopts"
137
+ - Gemfile
138
+ - LICENSE
139
+ - README.md
140
+ - Rakefile
141
+ - bin/console
142
+ - bin/setup
143
+ - chatrix.gemspec
144
+ - lib/chatrix.rb
145
+ - lib/chatrix/errors.rb
146
+ - lib/chatrix/matrix.rb
147
+ - lib/chatrix/version.rb
148
+ homepage: https://github.com/Sharparam/chatrix
149
+ licenses:
150
+ - MIT
151
+ metadata: {}
152
+ post_install_message:
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">"
164
+ - !ruby/object:Gem::Version
165
+ version: 1.3.1
166
+ requirements: []
167
+ rubyforge_project:
168
+ rubygems_version: 2.5.1
169
+ signing_key:
170
+ specification_version: 4
171
+ summary: Ruby implementation of the Matrix API
172
+ test_files: []
173
+ has_rdoc: