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.
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