contextio-lite 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +114 -0
- data/Rakefile +2 -0
- data/contextio-lite.gemspec +27 -0
- data/lib/contextio.rb +14 -0
- data/lib/contextio/api/abstract_api.rb +213 -0
- data/lib/contextio/api/association_helpers.rb +17 -0
- data/lib/contextio/api/resource.rb +248 -0
- data/lib/contextio/api/resource_collection.rb +193 -0
- data/lib/contextio/lite.rb +43 -0
- data/lib/contextio/lite/api.rb +30 -0
- data/lib/contextio/lite/email_account.rb +48 -0
- data/lib/contextio/lite/email_account_collection.rb +44 -0
- data/lib/contextio/lite/folder.rb +18 -0
- data/lib/contextio/lite/folder_collection.rb +17 -0
- data/lib/contextio/lite/message.rb +77 -0
- data/lib/contextio/lite/message_collection.rb +15 -0
- data/lib/contextio/lite/url_builder.rb +95 -0
- data/lib/contextio/lite/user.rb +61 -0
- data/lib/contextio/lite/user_collection.rb +40 -0
- data/lib/contextio/lite/webhook.rb +43 -0
- data/lib/contextio/lite/webhook_collection.rb +28 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9a4c95db7436871ec9123eb36072dd02e24f5078
|
4
|
+
data.tar.gz: 55b65431a9f3f5b517992e5ed3cde2ac2aa4198a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 56066f305c8e1341b3f911f18a25bce82297c99c1b6697569704d954fae1503f9c57179e16a8cc54917d4924b6c0d1b2e133da7738b1786e71fd137989d42641
|
7
|
+
data.tar.gz: 4d245707e67caeac959882263e47dd5b121f7464bf01251b707259e4b463d8c2992bc402f6c776af395da4d1d6a690b17c14256a6910464ed6c6853e495b86ab
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 javi
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Contextio::Lite
|
2
|
+
|
3
|
+
This is API client for the [Context.IO Lite](https://context.io/) version based on [Context.IO Ruby](https://github.com/contextio/contextio-ruby) for the 2.0 version.
|
4
|
+
It works in the same way as the official, calling first a user object and then working with the collections...
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'contextio-lite'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install contextio-lite
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
### Get a client
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
require 'contextio/lite'
|
28
|
+
|
29
|
+
client = ContextIO::Lite.new(API_KEY, API_SECRET)
|
30
|
+
```
|
31
|
+
|
32
|
+
### Dealing with users
|
33
|
+
|
34
|
+
List all the registered lite users IDs
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
p client.users.map(&:id)
|
38
|
+
```
|
39
|
+
|
40
|
+
CRUD
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
user = client.users.create email: 'bob@gmail.com', first_name: 'Bob', server: 'imap.gmail.com' username: 'bob', 'use_sll':true, 'port': 993, 'type': 'IMAP
|
44
|
+
|
45
|
+
user_id = user.id
|
46
|
+
|
47
|
+
client.users[user_id].update :hash_attrs
|
48
|
+
|
49
|
+
client.users[user_id].delete
|
50
|
+
```
|
51
|
+
|
52
|
+
### Retrieving messages
|
53
|
+
|
54
|
+
To access the emails, first you need to select an email_account and the folder containing your email
|
55
|
+
|
56
|
+
So if you want to list the user email accounts or folders you can do like this
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
p client[user_id].email_accounts.map(&:label)
|
60
|
+
p client[user_id].email_accounts[label].folders.map(&:name) # can access email_accounts by number => email_accounts[0]
|
61
|
+
```
|
62
|
+
|
63
|
+
Listing all the email subjects inside a folder (limited by context IO to 100)
|
64
|
+
```ruby
|
65
|
+
client[user_id].email_accounts[0].folders['INBOX'].messages do
|
66
|
+
p messages.subject
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
And if you want to filter the emails
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
client[user_id].email_accounts[0].folder['INBOX'].messages.where(limit: 3)
|
74
|
+
```
|
75
|
+
|
76
|
+
You also may want to access the content of one message
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
client[user_id].email_accounts[0].folder['INBOX'].messages['<message_id>'].body_plain # or body_html
|
80
|
+
|
81
|
+
client[user_id].email_accounts[0].folder['INBOX'].messages['<message_id>'].with(include_body:true).body_plain
|
82
|
+
```
|
83
|
+
|
84
|
+
The first one calls https://api.context.io/lite/users/id/email_accounts/label/folders/folder/messages/message_id/body
|
85
|
+
and the second calls https://api.context.io/lite/users/id/email_accounts/label/folders/folder/messages/message_id?include_body=1
|
86
|
+
but they return the same.
|
87
|
+
|
88
|
+
And the above should works also with 'flags' or 'headers'.
|
89
|
+
|
90
|
+
### Webhooks
|
91
|
+
|
92
|
+
One of the main feature that this version has and 2.0 doesn't is the real-time webhooks.
|
93
|
+
So in order to work with them we can do
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
client[user_id].webhooks.map(&:webhook_id) # listing all the webhooks an user has
|
97
|
+
|
98
|
+
client[user_id].create callback_url, failure_url, filter_folder_added: 'INBOX', include_body: true
|
99
|
+
```
|
100
|
+
|
101
|
+
So it will call the callback_url every time there is a new message on the user INBOX folder, posting the message info and body included.
|
102
|
+
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
1. Fork it ( https://github.com/javijuol/contextio-lite/fork )
|
107
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
108
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
109
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
110
|
+
5. Create a new Pull Request
|
111
|
+
|
112
|
+
## Copyright
|
113
|
+
|
114
|
+
This gem is distributed under the MIT License. See LICENSE.md for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require File.expand_path('../lib/contextio', __FILE__)
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'contextio-lite'
|
8
|
+
spec.version = ContextIO.version
|
9
|
+
spec.authors = ['Javier Juan']
|
10
|
+
spec.email = ['javier@promivia.com']
|
11
|
+
spec.summary = %q{Provides interface to Context.IO Lite API}
|
12
|
+
spec.description = %q{Short implementation of a client rest API for the Context.IO Lite API}
|
13
|
+
spec.homepage = 'https://github.com/javijuol/contextio-lite'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
22
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
23
|
+
spec.add_development_dependency 'debase', '~> 0'
|
24
|
+
spec.add_development_dependency 'faraday', '~> 0.9.1'
|
25
|
+
spec.add_development_dependency 'faraday_middleware', '~> 0.9.1'
|
26
|
+
|
27
|
+
end
|
data/lib/contextio.rb
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
module ContextIO
|
7
|
+
module API
|
8
|
+
class AbstractAPI
|
9
|
+
|
10
|
+
# @private
|
11
|
+
BASE_URL = 'https://api.context.io'
|
12
|
+
|
13
|
+
# @return [String] The version of the Context.IO API this version of the
|
14
|
+
# gem is intended for use with.
|
15
|
+
def self.version
|
16
|
+
raise NotDefinedError, 'VERSION is not defined in your API subclassed model.' if self::VERSION.nil?
|
17
|
+
self::VERSION
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String] The base URL the API is served from.
|
21
|
+
def self.base_url
|
22
|
+
BASE_URL
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.user_agent_string
|
26
|
+
raise NotDefinedError, 'user_agent_string undefined in your API subclassed model.'
|
27
|
+
end
|
28
|
+
|
29
|
+
def user_agent_string
|
30
|
+
self.class.user_agent_string
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :base_url, :version
|
34
|
+
|
35
|
+
# @!attribute [r] key
|
36
|
+
# @return [String] The OAuth key for the user's Context.IO account.
|
37
|
+
# @!attribute [r] secret
|
38
|
+
# @return [String] The OAuth secret for the user's Context.IO account.
|
39
|
+
# @!attribute [r] opts
|
40
|
+
# @return [Hash] opts Optional options for OAuth connections.
|
41
|
+
attr_reader :key, :secret, :opts
|
42
|
+
|
43
|
+
# @param [String] key The user's OAuth key for their Context.IO account.
|
44
|
+
# @param [String] secret The user's OAuth secret for their Context.IO account.
|
45
|
+
# @param [Hash] opts Optional options for OAuth connections. ie. :timeout and :open_timeout are supported
|
46
|
+
def initialize(key, secret, opts={})
|
47
|
+
@key = key
|
48
|
+
@secret = secret
|
49
|
+
@opts = opts || {}
|
50
|
+
@base_url = self.class.base_url
|
51
|
+
@version = self.class.version
|
52
|
+
end
|
53
|
+
|
54
|
+
# Generates the path for a resource_path and params hash for use with the API.
|
55
|
+
#
|
56
|
+
# @param [String] resource_path The resource_path or full resource URL for
|
57
|
+
# the resource being acted on.
|
58
|
+
# @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
|
59
|
+
# A Hash of the query parameters for the action represented by this path.
|
60
|
+
def path(resource_path, params = {})
|
61
|
+
"/#{version}/#{strip_resource_path(resource_path)}#{self.class.hash_to_url_params(params)}"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Makes a request against the Context.IO API.
|
65
|
+
#
|
66
|
+
# @param [String, Symbol] method The HTTP verb for the request (lower case).
|
67
|
+
# @param [String] resource_path The path to the resource in question.
|
68
|
+
# @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
|
69
|
+
# A Hash of the query parameters for the action represented by this
|
70
|
+
# request.
|
71
|
+
#
|
72
|
+
# @raise [API::Error] if the response code isn't in the 200 or 300 range.
|
73
|
+
def request(method, resource_path, params = {})
|
74
|
+
response = oauth_request(method, resource_path, params, { 'Accept' => 'application/json' })
|
75
|
+
|
76
|
+
with_error_handling(response) do |response|
|
77
|
+
parse_json(response.body)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def raw_request(method, resource_path, params={})
|
82
|
+
response = oauth_request(method, resource_path, params)
|
83
|
+
|
84
|
+
with_error_handling(response) do |response|
|
85
|
+
response.body
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
# Makes a request signed for OAuth, encoding parameters correctly, etc.
|
92
|
+
#
|
93
|
+
# @param [String, Symbol] method The HTTP verb for the request (lower case).
|
94
|
+
# @param [String] resource_path The path to the resource in question.
|
95
|
+
# @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
|
96
|
+
# A Hash of the query parameters for the action represented by this
|
97
|
+
# request.
|
98
|
+
# @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] headers
|
99
|
+
# A Hash of headers to be merged with the default headers for making
|
100
|
+
# requests.
|
101
|
+
#
|
102
|
+
# @return [Faraday::Response] The response object from the request.
|
103
|
+
def oauth_request(method, resource_path, params, headers=nil)
|
104
|
+
normalized_params = params.inject({}) do |normalized_params, (key, value)|
|
105
|
+
normalized_params[key.to_sym] = value
|
106
|
+
normalized_params
|
107
|
+
end
|
108
|
+
|
109
|
+
connection.send(method, path(resource_path), normalized_params, headers) do |request|
|
110
|
+
if request.method == :put
|
111
|
+
request.params = normalized_params
|
112
|
+
request.body = {}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# So that we can accept full URLs, this strips the domain and version number
|
118
|
+
# out and returns just the resource path.
|
119
|
+
#
|
120
|
+
# @param [#to_s] resource_path The full URL or path for a resource.
|
121
|
+
#
|
122
|
+
# @return [String] The resource path.
|
123
|
+
def strip_resource_path(resource_path)
|
124
|
+
resource_path.to_s.gsub("#{base_url}/#{version}/", '')
|
125
|
+
end
|
126
|
+
|
127
|
+
# Context.IO's API expects query parameters that are arrays to be comma
|
128
|
+
# separated, rather than submitted more than once. This munges those arrays
|
129
|
+
# and then URL-encodes the whole thing into a query string.
|
130
|
+
#
|
131
|
+
# @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
|
132
|
+
# A Hash of the query parameters.
|
133
|
+
#
|
134
|
+
# @return [String] A URL-encoded version of the query parameters.
|
135
|
+
def self.hash_to_url_params(params = {})
|
136
|
+
return '' if params.empty?
|
137
|
+
|
138
|
+
params = params.inject({}) do |memo, (k, v)|
|
139
|
+
memo[k] = Array(v).join(',')
|
140
|
+
|
141
|
+
memo
|
142
|
+
end
|
143
|
+
|
144
|
+
"?#{URI.encode_www_form(params)}"
|
145
|
+
end
|
146
|
+
|
147
|
+
# @!attribute [r] connection
|
148
|
+
# @return [Faraday::Connection] A handle on the Faraday connection object.
|
149
|
+
def connection
|
150
|
+
@connection ||= Faraday::Connection.new(base_url) do |faraday|
|
151
|
+
faraday.headers['User-Agent'] = user_agent_string
|
152
|
+
|
153
|
+
faraday.request :oauth, consumer_key: key, consumer_secret: secret
|
154
|
+
faraday.request :url_encoded
|
155
|
+
|
156
|
+
faraday.adapter Faraday.default_adapter
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Errors can come in a few shapes and we want to detect them and extract the
|
161
|
+
# useful information. If no errors are found, it calls the provided block
|
162
|
+
# and passes the response through.
|
163
|
+
#
|
164
|
+
# @param [Faraday::Response] response A response object from making a request to the
|
165
|
+
# API with Faraday.
|
166
|
+
#
|
167
|
+
# @raise [API::Error] if the response code isn't in the 200 or 300 range.
|
168
|
+
def with_error_handling(response, &block)
|
169
|
+
return block.call(response) if response.success?
|
170
|
+
|
171
|
+
parsed_body = parse_json(response.body)
|
172
|
+
message = determine_best_error_message(parsed_body) || "HTTP #{response.status} Error"
|
173
|
+
|
174
|
+
raise ContextIO::API::Error, message
|
175
|
+
end
|
176
|
+
|
177
|
+
# Parses JSON if there's valid JSON passed in.
|
178
|
+
#
|
179
|
+
# @param [String] document A string you suspect may be a JSON document.
|
180
|
+
#
|
181
|
+
# @return [Hash, Array, Nil] Either a parsed version of the JSON document or
|
182
|
+
# nil, if the document wasn't valid JSON.
|
183
|
+
def parse_json(document)
|
184
|
+
return JSON.parse(document.to_s)
|
185
|
+
rescue JSON::ParserError => e
|
186
|
+
return nil
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
# Given a parsed JSON body from an error response, figures out if it can
|
191
|
+
# pull useful information therefrom.
|
192
|
+
#
|
193
|
+
# @param [Hash] parsed_body A Hash parsed from a JSON document that may
|
194
|
+
# describe an error condition.
|
195
|
+
#
|
196
|
+
# @return [String, Nil] If it can, it will return a human-readable
|
197
|
+
# error-describing String. Otherwise, nil.
|
198
|
+
def determine_best_error_message(parsed_body)
|
199
|
+
return unless parsed_body.respond_to?(:[])
|
200
|
+
|
201
|
+
if parsed_body['type'] == 'error'
|
202
|
+
return parsed_body['value']
|
203
|
+
elsif parsed_body.has_key?('success') && !parsed_body['success']
|
204
|
+
return [parsed_body['feedback_code'], parsed_body['connectionLog']].compact.join("\n")
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
class NotDefinedError < StandardError; end
|
210
|
+
class Error < StandardError; end
|
211
|
+
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ContextIO
|
2
|
+
module API
|
3
|
+
module AssociationHelpers
|
4
|
+
def self.class_for_association_name(association_name)
|
5
|
+
associations[association_name]
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.register_resource(klass, association_name)
|
9
|
+
associations[association_name] = klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.associations
|
13
|
+
@associations ||= {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|