deathbycaptcha 4.1.5 → 5.0.0

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.
@@ -0,0 +1,135 @@
1
+ module DeathByCaptcha
2
+
3
+ # HTTP client for DeathByCaptcha API.
4
+ #
5
+ class Client::HTTP < Client
6
+
7
+ BASE_URL = 'http://api.dbcapi.me/api'
8
+
9
+ # Retrieve information from an uploaded captcha.
10
+ #
11
+ # @param [Integer] captcha_id Numeric ID of the captcha.
12
+ #
13
+ # @return [DeathByCaptcha::Captcha] The captcha object.
14
+ #
15
+ def captcha(captcha_id)
16
+ response = perform("captcha/#{captcha_id}")
17
+ DeathByCaptcha::Captcha.new(response)
18
+ end
19
+
20
+ # Report incorrectly solved captcha for refund.
21
+ #
22
+ # @param [Integer] captcha_id Numeric ID of the captcha.
23
+ #
24
+ # @return [DeathByCaptcha::Captcha] The captcha object.
25
+ #
26
+ def report!(captcha_id)
27
+ response = perform("captcha/#{captcha_id}/report", :post)
28
+ DeathByCaptcha::Captcha.new(response)
29
+ end
30
+
31
+ # Retrieve your user information (which has the current credit balance)
32
+ #
33
+ # @return [DeathByCaptcha::User] The user object.
34
+ #
35
+ def user
36
+ response = perform('user')
37
+ DeathByCaptcha::User.new(response)
38
+ end
39
+
40
+ # Retrieve DeathByCaptcha server status.
41
+ #
42
+ # @return [DeathByCaptcha::ServerStatus] The server status object.
43
+ #
44
+ def status
45
+ response = perform('status')
46
+ DeathByCaptcha::ServerStatus.new(response)
47
+ end
48
+
49
+ # Upload a captcha to DeathByCaptcha.
50
+ #
51
+ # This method will not return the solution. It's only useful if you want to
52
+ # implement your own "decode" function.
53
+ #
54
+ # @return [DeathByCaptcha::Captcha] The captcha object (not solved yet).
55
+ #
56
+ def upload(raw64)
57
+ response = perform('captcha', :post_multipart, captchafile: "base64:#{raw64}")
58
+ DeathByCaptcha::Captcha.new(response)
59
+ end
60
+
61
+ private
62
+
63
+ # Perform an HTTP request to the DeathByCaptcha API.
64
+ #
65
+ # @param [String] action API method name.
66
+ # @param [Symbol] method HTTP method (:get, :post, :post_multipart).
67
+ # @param [Hash] payload Data to be sent through the HTTP request.
68
+ #
69
+ # @return [Hash] Response from the DeathByCaptcha API.
70
+ #
71
+ def perform(action, method = :get, payload = {})
72
+ payload.merge!(username: self.username, password: self.password)
73
+
74
+ headers = { 'User-Agent' => DeathByCaptcha::API_VERSION }
75
+
76
+ if method == :post
77
+ uri = URI("#{BASE_URL}/#{action}")
78
+ req = Net::HTTP::Post.new(uri.request_uri, headers)
79
+ req.set_form_data(payload)
80
+
81
+ elsif method == :post_multipart
82
+ uri = URI("#{BASE_URL}/#{action}")
83
+ req = Net::HTTP::Post.new(uri.request_uri, headers)
84
+ boundary, body = prepare_multipart_data(payload)
85
+ req.content_type = "multipart/form-data; boundary=#{boundary}"
86
+ req.body = body
87
+
88
+ else
89
+ uri = URI("#{BASE_URL}/#{action}?#{URI.encode_www_form(payload)}")
90
+ req = Net::HTTP::Get.new(uri.request_uri, headers)
91
+ end
92
+
93
+ res = Net::HTTP.start(uri.hostname, uri.port) do |http|
94
+ http.request(req)
95
+ end
96
+
97
+ case res
98
+ when Net::HTTPSuccess, Net::HTTPSeeOther
99
+ Hash[URI.decode_www_form(res.body)]
100
+ when Net::HTTPForbidden
101
+ raise DeathByCaptcha::APIForbidden
102
+ when Net::HTTPBadRequest
103
+ raise DeathByCaptcha::APIBadRequest
104
+ when Net::HTTPRequestEntityTooLarge
105
+ raise DeathByCaptcha::APICaptchaTooLarge
106
+ when Net::HTTPServiceUnavailable
107
+ raise DeathByCaptcha::APIServiceUnavailable
108
+ else
109
+ raise DeathByCaptcha::APIResponseError.new(res.body)
110
+ end
111
+ end
112
+
113
+ # Prepare the multipart data to be sent via a :post_multipart request.
114
+ #
115
+ # @param [Hash] payload Data to be prepared via a multipart post.
116
+ #
117
+ # @return [String,String] Boundary and body for the multipart post.
118
+ #
119
+ def prepare_multipart_data(payload)
120
+ boundary = "infosimples" + rand(1_000_000).to_s # a random unique string
121
+
122
+ content = []
123
+ payload.each do |param, value|
124
+ content << '--' + boundary
125
+ content << "Content-Disposition: form-data; name=\"#{param}\""
126
+ content << ''
127
+ content << value
128
+ end
129
+ content << '--' + boundary + '--'
130
+ content << ''
131
+
132
+ [boundary, content.join("\r\n")]
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,113 @@
1
+ module DeathByCaptcha
2
+
3
+ # Socket client for DeathByCaptcha API.
4
+ #
5
+ class Client::Socket < Client
6
+
7
+ HOST = 'api.dbcapi.me'
8
+ PORTS = (8123..8130).to_a
9
+
10
+ # Retrieve information from an uploaded captcha.
11
+ #
12
+ # @param [Integer] captcha_id Numeric ID of the captcha.
13
+ #
14
+ # @return [DeathByCaptcha::Captcha] The captcha object.
15
+ #
16
+ def captcha(captcha_id)
17
+ response = perform('captcha', captcha: captcha_id)
18
+ DeathByCaptcha::Captcha.new(response)
19
+ end
20
+
21
+ # Report incorrectly solved captcha for refund.
22
+ #
23
+ # @param [Integer] captcha_id Numeric ID of the captcha.
24
+ #
25
+ # @return [DeathByCaptcha::Captcha] The captcha object.
26
+ #
27
+ def report!(captcha_id)
28
+ response = perform('report', captcha: captcha_id)
29
+ DeathByCaptcha::Captcha.new(response)
30
+ end
31
+
32
+ # Retrieve your user information (which has the current credit balance)
33
+ #
34
+ # @return [DeathByCaptcha::User] The user object.
35
+ #
36
+ def user
37
+ response = perform('user')
38
+ DeathByCaptcha::User.new(response)
39
+ end
40
+
41
+ # Retrieve DeathByCaptcha server status. This method won't use a Socket
42
+ # connection, it will use an HTTP connection.
43
+ #
44
+ # @return [DeathByCaptcha::ServerStatus] The server status object.
45
+ #
46
+ def status
47
+ http_client.status
48
+ end
49
+
50
+ # Upload a captcha to DeathByCaptcha.
51
+ #
52
+ # This method will not return the solution. It's only useful if you want to
53
+ # implement your own "decode" function.
54
+ #
55
+ # @return [DeathByCaptcha::Captcha] The captcha object (not solved yet).
56
+ #
57
+ def upload(raw64)
58
+ response = perform('upload', captcha: raw64)
59
+ DeathByCaptcha::Captcha.new(response)
60
+ end
61
+
62
+ private
63
+
64
+ # Perform a Socket communication with the DeathByCaptcha API.
65
+ #
66
+ # @param [String] action API method name.
67
+ # @param [Hash] payload Data to be exchanged in the communication.
68
+ #
69
+ # @return [Hash] Response from the DeathByCaptcha API.
70
+ #
71
+ def perform(action, payload = {})
72
+ payload.merge!(
73
+ cmd: action,
74
+ version: DeathByCaptcha::API_VERSION,
75
+ username: self.username,
76
+ password: self.password
77
+ )
78
+
79
+ response = ::Socket.tcp(HOST, PORTS.sample) do |socket|
80
+ socket.puts payload.to_json
81
+ socket.read
82
+ end
83
+
84
+ begin
85
+ response = JSON.parse(response)
86
+ rescue
87
+ raise DeathByCaptcha::APIResponseError.new("invalid JSON: #{response}")
88
+ end
89
+
90
+ if !(error = response['error'].to_s).empty?
91
+ case error
92
+ when 'not-logged-in', 'invalid-credentials', 'banned', 'insufficient-funds'
93
+ raise DeathByCaptcha::APIForbidden
94
+ when 'invalid-captcha'
95
+ raise DeathByCaptcha::APIBadRequest
96
+ when 'service-overload'
97
+ raise DeathByCaptcha::APIServiceUnavailable
98
+ else
99
+ raise DeathByCaptcha::APIResponseError.new(error)
100
+ end
101
+ end
102
+
103
+ response
104
+ end
105
+
106
+ # Return a cached http client for methods that doesn't work with sockets.
107
+ #
108
+ def http_client
109
+ @http_client ||= DeathByCaptcha.new(self.username, self.password, :http)
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,62 @@
1
+ module DeathByCaptcha
2
+
3
+ # This is the base DeathByCaptcha exception class. Rescue it if you want to
4
+ # catch any exception that might be raised.
5
+ #
6
+ class Error < Exception
7
+ end
8
+
9
+ class InvalidClientConnection < Error
10
+ def initialize
11
+ super('You have specified an invalid client connection (valid connections are :socket, :http)')
12
+ end
13
+ end
14
+
15
+ class InvalidCaptcha < Error
16
+ def initialize
17
+ super('The captcha is empty or invalid')
18
+ end
19
+ end
20
+
21
+ class Timeout < Error
22
+ def initialize
23
+ super('The captcha was not solved in the expected time')
24
+ end
25
+ end
26
+
27
+ class IncorrectSolution < Error
28
+ def initialize
29
+ super('CAPTCHA was not solved by DeathByCaptcha. Try again.')
30
+ end
31
+ end
32
+
33
+ class APIResponseError < Error
34
+ def initialize(info)
35
+ super("Invalid API response: #{info}")
36
+ end
37
+ end
38
+
39
+ class APIForbidden < Error
40
+ def initialize
41
+ super('Access denied, please check your credentials and/or balance')
42
+ end
43
+ end
44
+
45
+ class APIServiceUnavailable < Error
46
+ def initialize
47
+ super('CAPTCHA was rejected due to service overload, try again later')
48
+ end
49
+ end
50
+
51
+ class APIBadRequest < Error
52
+ def initialize
53
+ super('CAPTCHA was rejected by the service, check if it\'s a valid image')
54
+ end
55
+ end
56
+
57
+ class APICaptchaTooLarge < Error
58
+ def initialize
59
+ super('CAPTCHA image size is too large')
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,16 @@
1
+ module DeathByCaptcha
2
+
3
+ # Base class of a model object returned by DeathByCaptcha API.
4
+ #
5
+ class Model
6
+ def initialize(values = {})
7
+ values.each do |key, value|
8
+ self.send("#{key}=", value) if self.respond_to?("#{key}=")
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ require 'deathbycaptcha/models/captcha'
15
+ require 'deathbycaptcha/models/server_status'
16
+ require 'deathbycaptcha/models/user'
@@ -0,0 +1,20 @@
1
+ module DeathByCaptcha
2
+
3
+ # Model of a Captcha returned by DeathByCaptcha API.
4
+ #
5
+ class Captcha < DeathByCaptcha::Model
6
+ attr_accessor :captcha, :is_correct, :text
7
+
8
+ def id
9
+ @captcha
10
+ end
11
+
12
+ def is_correct=(value)
13
+ @is_correct = ['1', true].include?(value)
14
+ end
15
+
16
+ def captcha=(value)
17
+ @captcha = value.to_i
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module DeathByCaptcha
2
+
3
+ # Model of a server status returned by DeathByCaptcha API.
4
+ #
5
+ class ServerStatus < DeathByCaptcha::Model
6
+ attr_accessor :todays_accuracy, :solved_in, :is_service_overloaded
7
+
8
+ def todays_accuracy=(value)
9
+ @todays_accuracy = value.to_f
10
+ end
11
+
12
+ def solved_in=(value)
13
+ @solved_in = value.to_i
14
+ end
15
+
16
+ def is_service_overloaded=(value)
17
+ @is_service_overloaded = ['1', true].include?(value)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ module DeathByCaptcha
2
+
3
+ # Model of a User returned by DeathByCaptcha API.
4
+ #
5
+ class User < DeathByCaptcha::Model
6
+ attr_accessor :is_banned, :balance, :rate, :user
7
+
8
+ def id
9
+ @user
10
+ end
11
+
12
+ def is_banned=(value)
13
+ @is_banned = ['1', true].include?(value)
14
+ end
15
+
16
+ def balance=(value)
17
+ @balance = value.to_f
18
+ end
19
+
20
+ def rate=(value)
21
+ @rate = value.to_f
22
+ end
23
+
24
+ def user=(value)
25
+ @user = value.to_i
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ # Patches are added so older versions of Ruby work with this gem
2
+ #
3
+ unless ''.respond_to?(:empty?)
4
+ class String
5
+ def empty?
6
+ self.length == 0
7
+ end
8
+ end
9
+ end
@@ -1,4 +1,4 @@
1
1
  module DeathByCaptcha
2
- VERSION = "4.1.5"
3
- API_VERSION = "DBC/Ruby v4.1.2"
4
- end
2
+ VERSION = "5.0.0"
3
+ API_VERSION = "DBC/Ruby v#{VERSION}"
4
+ end
@@ -0,0 +1,3 @@
1
+ username: myusername
2
+ password: mypassword
3
+ captcha_id: 12345 # use any ID of a captcha solved on your account
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ username = CREDENTIALS['username']
4
+ password = CREDENTIALS['password']
5
+ captcha_id = CREDENTIALS['captcha_id']
6
+ image64 = Base64.encode64(File.open('captchas/1.png', 'rb').read)
7
+
8
+ describe DeathByCaptcha::Client do
9
+ describe '.create' do
10
+ context 'http' do
11
+ let(:client) { DeathByCaptcha.new(username, password, :http) }
12
+ it { expect(client).to be_a(DeathByCaptcha::Client::HTTP) }
13
+ end
14
+
15
+ context 'socket' do
16
+ let(:client) { DeathByCaptcha.new(username, password, :socket) }
17
+ it { expect(client).to be_a(DeathByCaptcha::Client::Socket) }
18
+ end
19
+
20
+ context 'default' do
21
+ let(:client) { DeathByCaptcha.new(username, password) }
22
+ it { expect(client).to be_a(DeathByCaptcha::Client::Socket) }
23
+ end
24
+
25
+ context 'other' do
26
+ it { expect {
27
+ DeathByCaptcha.new(username, password, :other)
28
+ }.to raise_error(DeathByCaptcha::InvalidClientConnection)
29
+ }
30
+ end
31
+ end
32
+
33
+ context 'http' do
34
+ subject(:client) { DeathByCaptcha.new(username, password, :http) }
35
+
36
+ describe '#load_captcha' do
37
+ it { expect(client.send(:load_captcha, url: 'http://bit.ly/1xXZcKo')).to eq(image64) }
38
+ it { expect(client.send(:load_captcha, path: 'captchas/1.png')).to eq(image64) }
39
+ it { expect(client.send(:load_captcha, file: File.open('captchas/1.png', 'rb'))).to eq(image64) }
40
+ it { expect(client.send(:load_captcha, raw: File.open('captchas/1.png', 'rb').read)).to eq(image64) }
41
+ it { expect(client.send(:load_captcha, raw64: image64)).to eq(image64) }
42
+ it { expect(client.send(:load_captcha, other: nil)).to eq('') }
43
+ end
44
+ end
45
+ end
46
+
47
+ shared_examples 'a client' do
48
+ describe '#captcha' do
49
+ before(:all) { @captcha = @client.captcha(captcha_id) }
50
+ it { expect(@captcha).to be_a(DeathByCaptcha::Captcha) }
51
+ it { expect(@captcha.text.size).to be > 0 }
52
+ it { expect([true, false]).to include(@captcha.is_correct) }
53
+ it { expect(@captcha.id).to be > 0 }
54
+ it { expect(@captcha.id).to eq(@captcha.captcha) }
55
+ end
56
+
57
+ describe '#user' do
58
+ before(:all) { @user = @client.user() }
59
+ it { expect(@user).to be_a(DeathByCaptcha::User) }
60
+ it { expect([true, false]).to include(@user.is_banned) }
61
+ it { expect(@user.balance).to be > 0 }
62
+ it { expect(@user.rate).to be > 0 }
63
+ it { expect(@user.id).to eq(@user.user) }
64
+ end
65
+
66
+ describe '#status' do
67
+ before(:all) { @status = @client.status() }
68
+ it { expect(@status).to be_a(DeathByCaptcha::ServerStatus) }
69
+ it { expect(@status.todays_accuracy).to be > 0 }
70
+ it { expect(@status.solved_in).to be > 0 }
71
+ it { expect([true, false]).to include(@status.is_service_overloaded) }
72
+ end
73
+
74
+ describe '#decode!' do
75
+ before(:all) { @captcha = @client.decode!(raw64: image64) }
76
+ it { expect(@captcha).to be_a(DeathByCaptcha::Captcha) }
77
+ it { expect(@captcha.text).to eq 'infosimples' }
78
+ it { expect(@captcha.is_correct).to be true }
79
+ it { expect(@captcha.id).to be > 0 }
80
+ it { expect(@captcha.id).to eq(@captcha.captcha) }
81
+ end
82
+ end
83
+
84
+ describe DeathByCaptcha::Client::HTTP do
85
+ it_behaves_like 'a client' do
86
+ before(:all) { @client = DeathByCaptcha.new(username, password, :http) }
87
+ end
88
+ end
89
+
90
+ describe DeathByCaptcha::Client::Socket do
91
+ it_behaves_like 'a client' do
92
+ before(:all) { @client = DeathByCaptcha.new(username, password, :socket) }
93
+ end
94
+ end