instapaper 0.3.0 → 1.0.0.pre2
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/LICENSE.md +1 -1
- data/README.md +62 -38
- data/instapaper.gemspec +21 -30
- data/lib/instapaper.rb +0 -25
- data/lib/instapaper/api.rb +15 -0
- data/lib/instapaper/{client/account.rb → api/accounts.rb} +5 -5
- data/lib/instapaper/api/bookmarks.rb +77 -0
- data/lib/instapaper/{client/folder.rb → api/folders.rb} +12 -11
- data/lib/instapaper/api/highlights.rb +33 -0
- data/lib/instapaper/api/oauth.rb +17 -0
- data/lib/instapaper/bookmark.rb +21 -0
- data/lib/instapaper/bookmark_list.rb +20 -0
- data/lib/instapaper/client.rb +40 -28
- data/lib/instapaper/credentials.rb +12 -0
- data/lib/instapaper/error.rb +78 -0
- data/lib/instapaper/folder.rb +17 -0
- data/lib/instapaper/highlight.rb +16 -0
- data/lib/instapaper/http/headers.rb +45 -0
- data/lib/instapaper/http/qline_parser.rb +9 -0
- data/lib/instapaper/http/request.rb +84 -0
- data/lib/instapaper/http/utils.rb +67 -0
- data/lib/instapaper/user.rb +14 -0
- data/lib/instapaper/version.rb +1 -1
- metadata +63 -209
- data/.gemtest +0 -0
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.travis.yml +0 -8
- data/.yardopts +0 -3
- data/Gemfile +0 -7
- data/Rakefile +0 -13
- data/lib/faraday/response/raise_http_1xxx.rb +0 -65
- data/lib/instapaper/authentication.rb +0 -32
- data/lib/instapaper/client/bookmark.rb +0 -81
- data/lib/instapaper/client/user.rb +0 -19
- data/lib/instapaper/configuration.rb +0 -88
- data/lib/instapaper/connection.rb +0 -35
- data/lib/instapaper/request.rb +0 -22
- data/spec/faraday/response_spec.rb +0 -22
- data/spec/fixtures/access_token.qline +0 -1
- data/spec/fixtures/bookmarks_add.json +0 -1
- data/spec/fixtures/bookmarks_archive.json +0 -1
- data/spec/fixtures/bookmarks_get_text.txt +0 -299
- data/spec/fixtures/bookmarks_list.json +0 -5
- data/spec/fixtures/bookmarks_move.json +0 -1
- data/spec/fixtures/bookmarks_star.json +0 -1
- data/spec/fixtures/bookmarks_unarchive.json +0 -1
- data/spec/fixtures/bookmarks_unstar.json +0 -1
- data/spec/fixtures/bookmarks_update_read_progress.json +0 -1
- data/spec/fixtures/folders_add.json +0 -1
- data/spec/fixtures/folders_delete.json +0 -1
- data/spec/fixtures/folders_list.json +0 -1
- data/spec/fixtures/folders_set_order.json +0 -1
- data/spec/fixtures/invalid_credentials.qline +0 -1
- data/spec/fixtures/verify_credentials.json +0 -1
- data/spec/instapaper/client/account_spec.rb +0 -27
- data/spec/instapaper/client/bookmark_spec.rb +0 -234
- data/spec/instapaper/client/folder_spec.rb +0 -89
- data/spec/instapaper/client/user_spec.rb +0 -36
- data/spec/instapaper/client_spec.rb +0 -65
- data/spec/instapaper_spec.rb +0 -85
- data/spec/spec_helper.rb +0 -52
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'instapaper/highlight'
|
2
|
+
|
3
|
+
module Instapaper
|
4
|
+
module API
|
5
|
+
# Defines methods related to highlights
|
6
|
+
module Highlights
|
7
|
+
# List highlights for a bookmark
|
8
|
+
# @param bookmark_id [String, Integer]
|
9
|
+
def highlights(bookmark_id)
|
10
|
+
perform_get_with_objects("/api/1.1/bookmarks/#{bookmark_id}/highlights", {}, Instapaper::Highlight)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Create a new highlight
|
14
|
+
# @note Non-subscribers are limited to 5 highlights per month.
|
15
|
+
# @param bookmark_id [String, Integer]
|
16
|
+
# @param options [Hash]
|
17
|
+
# @option options [String] :text The text for the highlight (HTML tags in text parameter should be unescaped.)
|
18
|
+
# @option options [String, Integer] :posiiton The 0-indexed position of text in the content. Defaults to 0.
|
19
|
+
# @return [Instapaper::Highlight]
|
20
|
+
def add_highlight(bookmark_id, options = {})
|
21
|
+
perform_post_with_object("/api/1.1/bookmarks/#{bookmark_id}/highlight", options, Instapaper::Highlight)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Delete a highlight
|
25
|
+
# @param highlight_id [String, Integer]
|
26
|
+
# @return [Boolean]
|
27
|
+
def delete_highlight(highlight_id, options = {})
|
28
|
+
perform_post_with_unparsed_response("/api/1.1/highlights/#{highlight_id}/delete", options)
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'instapaper/credentials'
|
2
|
+
require 'instapaper/http/qline_parser'
|
3
|
+
|
4
|
+
module Instapaper
|
5
|
+
module API
|
6
|
+
# Defines methods related to OAuth
|
7
|
+
module OAuth
|
8
|
+
# Gets an OAuth access token for a user.
|
9
|
+
def access_token(username, password)
|
10
|
+
response = perform_post_with_unparsed_response('/api/1.1/oauth/access_token', x_auth_username: username, x_auth_password: password, x_auth_mode: 'client_auth')
|
11
|
+
parsed_response = QLineParser.parse(response)
|
12
|
+
fail Instapaper::Error::OAuthError, parsed_response['error'] if parsed_response.key?('error')
|
13
|
+
Instapaper::Credentials.new(parsed_response)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module Instapaper
|
4
|
+
class Bookmark
|
5
|
+
include Virtus.value_object
|
6
|
+
|
7
|
+
values do
|
8
|
+
attribute :instapaper_hash, String
|
9
|
+
attribute :description, String
|
10
|
+
attribute :bookmark_id, Integer
|
11
|
+
attribute :private_source, String
|
12
|
+
attribute :title, String
|
13
|
+
attribute :url, String
|
14
|
+
attribute :progress_timestamp, DateTime
|
15
|
+
attribute :time, DateTime
|
16
|
+
attribute :progress, String
|
17
|
+
attribute :starred, String
|
18
|
+
attribute :type, String
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'instapaper/bookmark'
|
2
|
+
require 'instapaper/highlight'
|
3
|
+
require 'instapaper/user'
|
4
|
+
|
5
|
+
module Instapaper
|
6
|
+
class BookmarkList
|
7
|
+
include Virtus.value_object
|
8
|
+
|
9
|
+
values do
|
10
|
+
attribute :user, Instapaper::User
|
11
|
+
attribute :bookmarks, Array[Instapaper::Bookmark]
|
12
|
+
attribute :highlights, Array[Instapaper::Highlight]
|
13
|
+
attribute :delete_ids, Array[Integer]
|
14
|
+
end
|
15
|
+
|
16
|
+
def each
|
17
|
+
bookmarks.each { |bookmark| yield(bookmark) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/instapaper/client.rb
CHANGED
@@ -1,43 +1,55 @@
|
|
1
|
-
require 'instapaper/
|
2
|
-
require 'instapaper/
|
3
|
-
require 'instapaper/
|
1
|
+
require 'instapaper/api'
|
2
|
+
require 'instapaper/http/utils'
|
3
|
+
require 'instapaper/version'
|
4
4
|
|
5
5
|
module Instapaper
|
6
6
|
# Wrapper for the Instapaper REST API
|
7
7
|
class Client
|
8
|
-
|
9
|
-
|
8
|
+
include Instapaper::API
|
9
|
+
include Instapaper::HTTP::Utils
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
attr_accessor :oauth_token, :oauth_token_secret, :consumer_key, :consumer_secret, :proxy
|
12
|
+
attr_writer :user_agent
|
13
13
|
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
# Initializes a new Client object
|
15
|
+
#
|
16
|
+
# @param options [Hash]
|
17
|
+
# @return [Instapaper::Client]
|
18
|
+
def initialize(options = {})
|
19
|
+
options.each do |key, value|
|
20
|
+
instance_variable_set("@#{key}", value)
|
19
21
|
end
|
22
|
+
yield(self) if block_given?
|
20
23
|
end
|
21
24
|
|
22
|
-
|
23
|
-
|
25
|
+
# @return [String]
|
26
|
+
def user_agent
|
27
|
+
@user_agent ||= "InstapaperRubyGem/#{Instapaper::VERSION}"
|
24
28
|
end
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
# Authentication hash
|
31
|
+
#
|
32
|
+
# @return [Hash]
|
33
|
+
def credentials
|
34
|
+
{
|
35
|
+
consumer_key: @consumer_key,
|
36
|
+
consumer_secret: @consumer_secret,
|
37
|
+
oauth_token: @oauth_token,
|
38
|
+
oauth_token_secret: @oauth_token_secret,
|
39
|
+
}
|
40
|
+
end
|
29
41
|
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
42
|
+
# @return [Hash]
|
43
|
+
def consumer_credentials
|
44
|
+
{
|
45
|
+
consumer_key: @consumer_key,
|
46
|
+
consumer_secret: @consumer_secret,
|
47
|
+
}
|
48
|
+
end
|
37
49
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
50
|
+
# @return [Boolean]
|
51
|
+
def credentials?
|
52
|
+
credentials.values.all?
|
53
|
+
end
|
42
54
|
end
|
43
55
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Instapaper
|
2
|
+
# Custom error class for rescuing from all Instapaper errors
|
3
|
+
class Error < StandardError
|
4
|
+
# @return [Integer]
|
5
|
+
attr_reader :code
|
6
|
+
|
7
|
+
ServiceUnavailableError = Class.new(self)
|
8
|
+
BookmarkError = Class.new(self)
|
9
|
+
FolderError = Class.new(self)
|
10
|
+
HighlightError = Class.new(self)
|
11
|
+
OAuthError = Class.new(self)
|
12
|
+
|
13
|
+
ERRORS = {
|
14
|
+
1040 => 'Rate-limit exceeded',
|
15
|
+
1041 => 'Premium account required',
|
16
|
+
1042 => 'Application is suspended',
|
17
|
+
1500 => 'Unexpected service error',
|
18
|
+
1550 => 'Error generating text version of this URL',
|
19
|
+
}
|
20
|
+
|
21
|
+
BOOKMARK_ERRORS = {
|
22
|
+
1220 => 'Domain requires full content to be supplied',
|
23
|
+
1221 => 'Domain has opted out of Instapaper compatibility',
|
24
|
+
1240 => 'Invalid URL specified',
|
25
|
+
1241 => 'Invalid or missing bookmark_id',
|
26
|
+
1242 => 'Invalid or missing folder_id',
|
27
|
+
1243 => 'Invalid or missing progress',
|
28
|
+
1244 => 'Invalid or missing progress_timestamp',
|
29
|
+
1245 => 'Private bookmarks require supplied content',
|
30
|
+
1250 => 'Unexpected error when saving bookmark',
|
31
|
+
}
|
32
|
+
|
33
|
+
FOLDER_ERRORS = {
|
34
|
+
1250 => 'Invalid or missing title',
|
35
|
+
1251 => 'User already has a folder with this title',
|
36
|
+
1252 => 'Cannot add bookmarks to this folder',
|
37
|
+
}
|
38
|
+
|
39
|
+
HIGHLIGHT_ERRORS = {
|
40
|
+
1600 => 'Cannot create highlight with empty text',
|
41
|
+
1601 => 'Duplicate highlight',
|
42
|
+
}
|
43
|
+
|
44
|
+
CODES = [
|
45
|
+
ERRORS,
|
46
|
+
BOOKMARK_ERRORS,
|
47
|
+
FOLDER_ERRORS,
|
48
|
+
HIGHLIGHT_ERRORS,
|
49
|
+
].collect(&:keys).flatten
|
50
|
+
|
51
|
+
# Create a new error from an HTTP response
|
52
|
+
#
|
53
|
+
# @param response [HTTP::Response]
|
54
|
+
# @return [Instapaper::Error]
|
55
|
+
def self.from_response(code, path)
|
56
|
+
if ERRORS.keys.include?(code)
|
57
|
+
new(ERRORS[code], code)
|
58
|
+
else
|
59
|
+
case path
|
60
|
+
when /highlights/ then HighlightError.new(HIGHLIGHT_ERRORS[code], code)
|
61
|
+
when /bookmarks/ then BookmarkError.new(BOOKMARK_ERRORS[code], code)
|
62
|
+
when /folders/ then FolderError.new(FOLDER_ERRORS[code], code)
|
63
|
+
else new('Unknown Error Code', code)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Initializes a new Error object
|
69
|
+
#
|
70
|
+
# @param message [Exception, String]
|
71
|
+
# @param code [Integer]
|
72
|
+
# @return [Instapaper::Error]
|
73
|
+
def initialize(message = '', code = nil)
|
74
|
+
super(message)
|
75
|
+
@code = code
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module Instapaper
|
4
|
+
class Folder
|
5
|
+
include Virtus.value_object
|
6
|
+
|
7
|
+
values do
|
8
|
+
attribute :title, String
|
9
|
+
attribute :display_title, String
|
10
|
+
attribute :sync_to_mobile, Axiom::Types::Boolean
|
11
|
+
attribute :folder_id, Integer
|
12
|
+
attribute :position, String
|
13
|
+
attribute :type, String
|
14
|
+
attribute :slug, String
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module Instapaper
|
4
|
+
class Highlight
|
5
|
+
include Virtus.value_object
|
6
|
+
|
7
|
+
values do
|
8
|
+
attribute :type, String
|
9
|
+
attribute :highlight_id, String
|
10
|
+
attribute :bookmark_id, String
|
11
|
+
attribute :text, String
|
12
|
+
attribute :position, String
|
13
|
+
attribute :time, String
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'base64'
|
3
|
+
require 'simple_oauth'
|
4
|
+
|
5
|
+
module Instapaper
|
6
|
+
module HTTP
|
7
|
+
class Headers
|
8
|
+
def initialize(client, request_method, url, options = {})
|
9
|
+
@client = client
|
10
|
+
@request_method = request_method.to_sym
|
11
|
+
@uri = Addressable::URI.parse(url)
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def request_headers
|
16
|
+
{
|
17
|
+
user_agent: @client.user_agent,
|
18
|
+
authorization: oauth_header,
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def oauth_header
|
25
|
+
SimpleOAuth::Header.new(@request_method, @uri, @options, credentials.merge(ignore_extra_keys: true))
|
26
|
+
end
|
27
|
+
|
28
|
+
# Authentication hash
|
29
|
+
#
|
30
|
+
# @return [Hash]
|
31
|
+
def credentials
|
32
|
+
if @client.credentials?
|
33
|
+
{
|
34
|
+
consumer_key: @client.consumer_key,
|
35
|
+
consumer_secret: @client.consumer_secret,
|
36
|
+
token: @client.oauth_token,
|
37
|
+
token_secret: @client.oauth_token_secret,
|
38
|
+
}
|
39
|
+
else
|
40
|
+
@client.consumer_credentials
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'http'
|
3
|
+
require 'json'
|
4
|
+
require 'net/https'
|
5
|
+
require 'openssl'
|
6
|
+
require 'instapaper/error'
|
7
|
+
require 'instapaper/http/headers'
|
8
|
+
|
9
|
+
module Instapaper
|
10
|
+
module HTTP
|
11
|
+
class Request
|
12
|
+
BASE_URL = 'https://www.instapaper.com'
|
13
|
+
attr_accessor :client, :headers, :multipart, :options, :path,
|
14
|
+
:rate_limit, :request_method, :uri
|
15
|
+
alias_method :verb, :request_method
|
16
|
+
|
17
|
+
# @param client [Instapaper::Client]
|
18
|
+
# @param request_method [String, Symbol]
|
19
|
+
# @param path [String]
|
20
|
+
# @param options [Hash]
|
21
|
+
# @return [Instapaper::HTTP::Request]
|
22
|
+
def initialize(client, request_method, path, options = {})
|
23
|
+
@client = client
|
24
|
+
@request_method = request_method
|
25
|
+
@uri = Addressable::URI.parse(path.start_with?('http') ? path : BASE_URL + path)
|
26
|
+
@path = uri.path
|
27
|
+
@options = options
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Array, Hash]
|
31
|
+
def perform
|
32
|
+
perform_request
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def perform_request
|
38
|
+
raw = @options.delete(:raw)
|
39
|
+
@headers = Instapaper::HTTP::Headers.new(@client, @request_method, @uri, @options).request_headers
|
40
|
+
options_key = @request_method == :get ? :params : :form
|
41
|
+
response = ::HTTP.with(@headers).public_send(@request_method, @uri.to_s, options_key => @options)
|
42
|
+
fail_if_error(response, raw)
|
43
|
+
raw ? response.to_s : parsed_response(response)
|
44
|
+
end
|
45
|
+
|
46
|
+
def fail_if_error(response, raw)
|
47
|
+
fail_if_error_unparseable_response(response) unless raw
|
48
|
+
fail_if_error_in_body(parsed_response(response))
|
49
|
+
fail_if_error_response_code(response)
|
50
|
+
end
|
51
|
+
|
52
|
+
def fail_if_error_response_code(response)
|
53
|
+
fail Instapaper::Error::ServiceUnavailableError if response.status != 200
|
54
|
+
end
|
55
|
+
|
56
|
+
def fail_if_error_unparseable_response(response)
|
57
|
+
response.parse(:json)
|
58
|
+
rescue JSON::ParserError
|
59
|
+
raise Instapaper::Error::ServiceUnavailableError
|
60
|
+
end
|
61
|
+
|
62
|
+
def fail_if_error_in_body(response)
|
63
|
+
error = error(response)
|
64
|
+
fail(error) if error
|
65
|
+
end
|
66
|
+
|
67
|
+
def error(response)
|
68
|
+
return unless response.is_a?(Array)
|
69
|
+
return unless response.size > 0
|
70
|
+
return unless response.first['type'] == 'error'
|
71
|
+
|
72
|
+
Instapaper::Error.from_response(response.first['error_code'], @path)
|
73
|
+
end
|
74
|
+
|
75
|
+
def parsed_response(response)
|
76
|
+
@parsed_response ||= begin
|
77
|
+
response.parse(:json)
|
78
|
+
rescue
|
79
|
+
response.body
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|