chatrix 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: