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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +1 -1
  3. data/README.md +62 -38
  4. data/instapaper.gemspec +21 -30
  5. data/lib/instapaper.rb +0 -25
  6. data/lib/instapaper/api.rb +15 -0
  7. data/lib/instapaper/{client/account.rb → api/accounts.rb} +5 -5
  8. data/lib/instapaper/api/bookmarks.rb +77 -0
  9. data/lib/instapaper/{client/folder.rb → api/folders.rb} +12 -11
  10. data/lib/instapaper/api/highlights.rb +33 -0
  11. data/lib/instapaper/api/oauth.rb +17 -0
  12. data/lib/instapaper/bookmark.rb +21 -0
  13. data/lib/instapaper/bookmark_list.rb +20 -0
  14. data/lib/instapaper/client.rb +40 -28
  15. data/lib/instapaper/credentials.rb +12 -0
  16. data/lib/instapaper/error.rb +78 -0
  17. data/lib/instapaper/folder.rb +17 -0
  18. data/lib/instapaper/highlight.rb +16 -0
  19. data/lib/instapaper/http/headers.rb +45 -0
  20. data/lib/instapaper/http/qline_parser.rb +9 -0
  21. data/lib/instapaper/http/request.rb +84 -0
  22. data/lib/instapaper/http/utils.rb +67 -0
  23. data/lib/instapaper/user.rb +14 -0
  24. data/lib/instapaper/version.rb +1 -1
  25. metadata +63 -209
  26. data/.gemtest +0 -0
  27. data/.gitignore +0 -11
  28. data/.rspec +0 -3
  29. data/.travis.yml +0 -8
  30. data/.yardopts +0 -3
  31. data/Gemfile +0 -7
  32. data/Rakefile +0 -13
  33. data/lib/faraday/response/raise_http_1xxx.rb +0 -65
  34. data/lib/instapaper/authentication.rb +0 -32
  35. data/lib/instapaper/client/bookmark.rb +0 -81
  36. data/lib/instapaper/client/user.rb +0 -19
  37. data/lib/instapaper/configuration.rb +0 -88
  38. data/lib/instapaper/connection.rb +0 -35
  39. data/lib/instapaper/request.rb +0 -22
  40. data/spec/faraday/response_spec.rb +0 -22
  41. data/spec/fixtures/access_token.qline +0 -1
  42. data/spec/fixtures/bookmarks_add.json +0 -1
  43. data/spec/fixtures/bookmarks_archive.json +0 -1
  44. data/spec/fixtures/bookmarks_get_text.txt +0 -299
  45. data/spec/fixtures/bookmarks_list.json +0 -5
  46. data/spec/fixtures/bookmarks_move.json +0 -1
  47. data/spec/fixtures/bookmarks_star.json +0 -1
  48. data/spec/fixtures/bookmarks_unarchive.json +0 -1
  49. data/spec/fixtures/bookmarks_unstar.json +0 -1
  50. data/spec/fixtures/bookmarks_update_read_progress.json +0 -1
  51. data/spec/fixtures/folders_add.json +0 -1
  52. data/spec/fixtures/folders_delete.json +0 -1
  53. data/spec/fixtures/folders_list.json +0 -1
  54. data/spec/fixtures/folders_set_order.json +0 -1
  55. data/spec/fixtures/invalid_credentials.qline +0 -1
  56. data/spec/fixtures/verify_credentials.json +0 -1
  57. data/spec/instapaper/client/account_spec.rb +0 -27
  58. data/spec/instapaper/client/bookmark_spec.rb +0 -234
  59. data/spec/instapaper/client/folder_spec.rb +0 -89
  60. data/spec/instapaper/client/user_spec.rb +0 -36
  61. data/spec/instapaper/client_spec.rb +0 -65
  62. data/spec/instapaper_spec.rb +0 -85
  63. 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
@@ -1,43 +1,55 @@
1
- require 'instapaper/connection'
2
- require 'instapaper/request'
3
- require 'instapaper/authentication'
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
- # @private
9
- attr_accessor *Configuration::VALID_OPTIONS_KEYS
8
+ include Instapaper::API
9
+ include Instapaper::HTTP::Utils
10
10
 
11
- alias :api_endpoint :endpoint
12
- alias :api_version :version
11
+ attr_accessor :oauth_token, :oauth_token_secret, :consumer_key, :consumer_secret, :proxy
12
+ attr_writer :user_agent
13
13
 
14
- # Creates a new API
15
- def initialize(options={})
16
- options = Instapaper.options.merge(options)
17
- Configuration::VALID_OPTIONS_KEYS.each do |key|
18
- send("#{key}=", options[key])
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
- def endpoint_with_prefix
23
- api_endpoint + path_prefix
25
+ # @return [String]
26
+ def user_agent
27
+ @user_agent ||= "InstapaperRubyGem/#{Instapaper::VERSION}"
24
28
  end
25
29
 
26
- include Connection
27
- include Request
28
- include Authentication
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
- # Require client method modules after initializing the Client class in
31
- # order to avoid a superclass mismatch error, allowing those modules to be
32
- # Client-namespaced.
33
- require 'instapaper/client/account'
34
- require 'instapaper/client/user'
35
- require 'instapaper/client/bookmark'
36
- require 'instapaper/client/folder'
42
+ # @return [Hash]
43
+ def consumer_credentials
44
+ {
45
+ consumer_key: @consumer_key,
46
+ consumer_secret: @consumer_secret,
47
+ }
48
+ end
37
49
 
38
- include Instapaper::Client::Account
39
- include Instapaper::Client::User
40
- include Instapaper::Client::Bookmark
41
- include Instapaper::Client::Folder
50
+ # @return [Boolean]
51
+ def credentials?
52
+ credentials.values.all?
53
+ end
42
54
  end
43
55
  end
@@ -0,0 +1,12 @@
1
+ require 'virtus'
2
+
3
+ module Instapaper
4
+ class Credentials
5
+ include Virtus.value_object
6
+
7
+ values do
8
+ attribute :oauth_token, String
9
+ attribute :oauth_token_secret, String
10
+ end
11
+ end
12
+ 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,9 @@
1
+ module Instapaper
2
+ class QLineParser
3
+ def self.parse(response)
4
+ values = response.split('&').map { |part| part.split('=') }.flatten
5
+ values.unshift('error') if values.length == 1
6
+ Hash[*values]
7
+ end
8
+ end
9
+ 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