instapaper 0.3.0 → 1.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|