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.
- checksums.yaml +7 -0
- data/.gitignore +17 -4
- data/CHANGELOG.md +20 -0
- data/Gemfile +1 -1
- data/{MIT-LICENSE → LICENSE.txt} +4 -2
- data/README.md +132 -51
- data/Rakefile +2 -2
- data/captchas/1.png +0 -0
- data/deathbycaptcha.gemspec +21 -21
- data/lib/deathbycaptcha.rb +11 -6
- data/lib/deathbycaptcha/client.rb +143 -129
- data/lib/deathbycaptcha/client/http.rb +135 -0
- data/lib/deathbycaptcha/client/socket.rb +113 -0
- data/lib/deathbycaptcha/exceptions.rb +62 -0
- data/lib/deathbycaptcha/models.rb +16 -0
- data/lib/deathbycaptcha/models/captcha.rb +20 -0
- data/lib/deathbycaptcha/models/server_status.rb +20 -0
- data/lib/deathbycaptcha/models/user.rb +28 -0
- data/lib/deathbycaptcha/patches.rb +9 -0
- data/lib/deathbycaptcha/version.rb +3 -3
- data/spec/credentials.yml.example +3 -0
- data/spec/lib/client_spec.rb +94 -0
- data/spec/spec_helper.rb +21 -0
- metadata +59 -39
- data/lib/deathbycaptcha/config.rb +0 -42
- data/lib/deathbycaptcha/errors.rb +0 -69
- data/lib/deathbycaptcha/http_client.rb +0 -98
- data/lib/deathbycaptcha/socket_client.rb +0 -224
@@ -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
|
@@ -1,4 +1,4 @@
|
|
1
1
|
module DeathByCaptcha
|
2
|
-
VERSION = "
|
3
|
-
API_VERSION = "DBC/Ruby
|
4
|
-
end
|
2
|
+
VERSION = "5.0.0"
|
3
|
+
API_VERSION = "DBC/Ruby v#{VERSION}"
|
4
|
+
end
|
@@ -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
|