capsolver 0.1.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/LICENSE.txt +21 -0
- data/README.md +150 -0
- data/Rakefile +12 -0
- data/lib/capsolver/aws_waf.rb +9 -0
- data/lib/capsolver/base.rb +105 -0
- data/lib/capsolver/client.rb +85 -0
- data/lib/capsolver/cloudflare.rb +9 -0
- data/lib/capsolver/configuration.rb +12 -0
- data/lib/capsolver/control.rb +57 -0
- data/lib/capsolver/datadome_slider.rb +9 -0
- data/lib/capsolver/error.rb +18 -0
- data/lib/capsolver/file_instrument.rb +67 -0
- data/lib/capsolver/friendly_captcha.rb +9 -0
- data/lib/capsolver/gee_test.rb +9 -0
- data/lib/capsolver/image_to_text.rb +9 -0
- data/lib/capsolver/mt_captcha.rb +9 -0
- data/lib/capsolver/recaptcha.rb +9 -0
- data/lib/capsolver/utils.rb +73 -0
- data/lib/capsolver/version.rb +5 -0
- data/lib/capsolver/vision_engine.rb +9 -0
- data/lib/capsolver/yandex.rb +12 -0
- data/lib/capsolver.rb +38 -0
- data/sig/capsolver.rbs +4 -0
- metadata +135 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 596a92f8123d830530be79c0ef68165aabea888839f39fecfa4468d2f160d875
|
|
4
|
+
data.tar.gz: cc6410676969e23ad942c2f30b9183a48ea7175952448d4ef5521667b35bd921
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2f0d0625e6a3f261816caa703e35e01db6ea066df73b04e5034c16a437ecea6cebaa5d441db097b786d802a3d40857526113407cc1b80edfc1aca00c5689153a
|
|
7
|
+
data.tar.gz: 46774bb007cf8cffc91cb7a7bfb84e7d11df2f90fd2554e98fa942f37fd49af59f35113794fc39318ca2f2d59d472714368d1eabef793bf831770fbfe074360e
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jorge Romussi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Capsolver
|
|
2
|
+
|
|
3
|
+
Ruby client library for the [Capsolver](https://www.capsolver.com/) service API. This gem is a Ruby port of the popular [python3-capsolver](https://github.com/AndreiDrang/python3-capsolver) library, providing a clean, robust, and idiomatic interface to solve various CAPTCHAs.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Broad Captcha Support**: ReCaptcha (V2/V3/Enterprise), Cloudflare (Turnstile/Challenge), DataDome Slider, GeeTest, MtCaptcha, AWS WAF, Yandex Captcha, Vision Engine, and standard Image-to-Text.
|
|
8
|
+
- **Sync & Async Ready**: Fully compatible with modern Ruby 3.x concurrency (Threads, Fibers, and Fiber Schedulers like the `async` gem).
|
|
9
|
+
- **Parity with Python API**: Includes `aio_*` method aliases and structure designed to feel familiar to developers migrating code from Python.
|
|
10
|
+
- **Robust Connection Handling**: Uses Faraday under the hood for clean, customizable HTTP request management.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this line to your application's Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "capsolver"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
And then execute:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install it directly:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
gem install capsolver
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### 1. Control Client (Generic Operations)
|
|
39
|
+
|
|
40
|
+
The `Capsolver::Control` class allows checking your balance, submitting custom tasks, getting status, and providing feedback.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require "capsolver"
|
|
44
|
+
|
|
45
|
+
# Initialize control client
|
|
46
|
+
control = Capsolver::Control.new(api_key: "YOUR_CAPSOLVER_API_KEY")
|
|
47
|
+
|
|
48
|
+
# Check balance
|
|
49
|
+
balance_info = control.get_balance
|
|
50
|
+
puts "Balance: #{balance_info['balance']}"
|
|
51
|
+
|
|
52
|
+
# Create custom tasks and poll for results manually if needed
|
|
53
|
+
response = control.create_task({
|
|
54
|
+
type: "AntiTurnstileTaskProxyLess",
|
|
55
|
+
websiteURL: "https://example.com",
|
|
56
|
+
websiteKey: "0x4AAAAAA..."
|
|
57
|
+
})
|
|
58
|
+
task_id = response["taskId"]
|
|
59
|
+
|
|
60
|
+
# Poll for result
|
|
61
|
+
result = control.get_task_result(task_id)
|
|
62
|
+
puts result
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### 2. Solving Cloudflare Turnstile
|
|
68
|
+
|
|
69
|
+
Use the `Capsolver::Cloudflare` class for an automatic retry-and-polling resolver:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
require "capsolver"
|
|
73
|
+
|
|
74
|
+
cloudflare = Capsolver::Cloudflare.new(api_key: "YOUR_API_KEY")
|
|
75
|
+
|
|
76
|
+
# Triggers createTask and automatically polls getTaskResult
|
|
77
|
+
result = cloudflare.captcha_handler({
|
|
78
|
+
websiteURL: "https://example.com",
|
|
79
|
+
websiteKey: "0x4AAAAAA..."
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if result["status"] == "ready"
|
|
83
|
+
token = result.dig("solution", "token")
|
|
84
|
+
puts "Solved! Token: #{token}"
|
|
85
|
+
else
|
|
86
|
+
puts "Failed to solve Turnstile: #{result['errorDescription']}"
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### 3. Solving Image-to-Text Captchas
|
|
93
|
+
|
|
94
|
+
To solve classic image captchas, use `Capsolver::ImageToText` combined with the `Capsolver::FileInstrument` helper to handle base64 encoding:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
require "capsolver"
|
|
98
|
+
|
|
99
|
+
# Read and encode local image file to base64
|
|
100
|
+
instrument = Capsolver::FileInstrument.new
|
|
101
|
+
base64_image = instrument.file_processing(captcha_file: "path/to/captcha.png")
|
|
102
|
+
|
|
103
|
+
image_solver = Capsolver::ImageToText.new(api_key: "YOUR_API_KEY")
|
|
104
|
+
result = image_solver.captcha_handler({
|
|
105
|
+
body: base64_image,
|
|
106
|
+
module: "common" # or specific capsolver module
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
if result["status"] == "ready"
|
|
110
|
+
puts "Result: #{result.dig('solution', 'text')}"
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### 4. Concurrency & Async Support
|
|
117
|
+
|
|
118
|
+
Since standard blocking I/O (like Faraday requests and `sleep`) in Ruby 3.x automatically becomes non-blocking when run within a Fiber Scheduler context, using this gem in `async` blocks is natively concurrent:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
require "async"
|
|
122
|
+
require "capsolver"
|
|
123
|
+
|
|
124
|
+
Async do
|
|
125
|
+
client = Capsolver::Cloudflare.new(api_key: "YOUR_API_KEY")
|
|
126
|
+
|
|
127
|
+
# This poll-and-sleep loop runs concurrently without blocking other Fibers!
|
|
128
|
+
task = Async { client.captcha_handler(websiteURL: "...", websiteKey: "...") }
|
|
129
|
+
|
|
130
|
+
puts task.wait
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For parity with Python codebases, the gem also exposes explicit `aio_*` method aliases (e.g. `aio_captcha_handler`, `aio_get_balance`, `aio_create_task`).
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to execute the Minitest test suite.
|
|
141
|
+
|
|
142
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
143
|
+
|
|
144
|
+
## Contributing
|
|
145
|
+
|
|
146
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/eljarpo/capsolver.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Capsolver
|
|
7
|
+
class Base
|
|
8
|
+
APP_ID = "3E36E3CD-7EB5-4CAF-AA15-91011E652321"
|
|
9
|
+
REQUEST_URL = "https://api.capsolver.com"
|
|
10
|
+
VALID_STATUS_CODES = [200, 202, 400, 401, 405]
|
|
11
|
+
CAPTCHA_UNSOLVABLE = "ERROR_CAPTCHA_UNSOLVABLE"
|
|
12
|
+
CAPTCHA_UNSOLVABLE_DESCRIPTION = "Captcha not recognized"
|
|
13
|
+
attr_accessor :api_key, :captcha_type, :sleep_time, :request_url
|
|
14
|
+
|
|
15
|
+
def initialize(api_key: nil, captcha_type:, sleep_time: nil, request_url: nil)
|
|
16
|
+
@api_key = api_key || Capsolver.configuration.api_key
|
|
17
|
+
raise ArgumentError, "api_key is required (either configure it globally via Capsolver.configure or pass it to initialize)" if @api_key.nil?
|
|
18
|
+
|
|
19
|
+
@captcha_type = captcha_type
|
|
20
|
+
@sleep_time = sleep_time || Capsolver.configuration.sleep_time
|
|
21
|
+
@request_url = request_url || Capsolver.configuration.request_url
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def captcha_handler(task_payload)
|
|
25
|
+
task = { type: @captcha_type }.merge(task_payload)
|
|
26
|
+
|
|
27
|
+
create_task_payload = {
|
|
28
|
+
clientKey: @api_key,
|
|
29
|
+
appId: APP_ID,
|
|
30
|
+
task: task
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
created_task_data = create_task(create_task_payload)
|
|
34
|
+
|
|
35
|
+
if created_task_data["errorId"] == 0
|
|
36
|
+
if created_task_data["status"]&.downcase == "ready"
|
|
37
|
+
created_task_data
|
|
38
|
+
else
|
|
39
|
+
get_result(created_task_data["taskId"])
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
created_task_data["status"] = "failed"
|
|
43
|
+
created_task_data
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias_method :aio_captcha_handler, :captcha_handler
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def connection
|
|
52
|
+
@connection ||= Faraday.new(url: @request_url) do |conn|
|
|
53
|
+
conn.request :json
|
|
54
|
+
conn.response :json, content_type: /\bjson$/
|
|
55
|
+
conn.adapter Faraday.default_adapter
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def post_request(endpoint, payload)
|
|
60
|
+
camelized_payload = Utils.camelize_keys(payload)
|
|
61
|
+
response = connection.post(endpoint, camelized_payload)
|
|
62
|
+
|
|
63
|
+
unless VALID_STATUS_CODES.include?(response.status)
|
|
64
|
+
raise Error, "HTTP request failed with status #{response.status}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Utils.normalize_response(response.body)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create_task(payload)
|
|
71
|
+
post_request("createTask", payload)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def get_result(task_id)
|
|
75
|
+
# initial waiting
|
|
76
|
+
sleep(@sleep_time)
|
|
77
|
+
|
|
78
|
+
payload = {
|
|
79
|
+
clientKey: @api_key,
|
|
80
|
+
taskId: task_id
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Up to 15 attempts (yielding 1 to 15)
|
|
84
|
+
15.times do
|
|
85
|
+
result = post_request("getTaskResult", payload)
|
|
86
|
+
|
|
87
|
+
status = result["status"]
|
|
88
|
+
if %w[ready failed].include?(status&.downcase)
|
|
89
|
+
return result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sleep(@sleep_time)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# default response if server is silent
|
|
96
|
+
{
|
|
97
|
+
"errorId" => 1,
|
|
98
|
+
"errorCode" => CAPTCHA_UNSOLVABLE,
|
|
99
|
+
"errorDescription" => CAPTCHA_UNSOLVABLE_DESCRIPTION,
|
|
100
|
+
"taskId" => task_id,
|
|
101
|
+
"status" => "failed"
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capsolver
|
|
4
|
+
class Client
|
|
5
|
+
attr_reader :api_key, :sleep_time, :request_url
|
|
6
|
+
|
|
7
|
+
def initialize(api_key: nil, sleep_time: nil, request_url: nil)
|
|
8
|
+
@api_key = api_key || Capsolver.configuration.api_key
|
|
9
|
+
raise ArgumentError, "api_key is required (either configure it globally via Capsolver.configure or pass it to initialize)" if @api_key.nil?
|
|
10
|
+
|
|
11
|
+
@sleep_time = sleep_time || Capsolver.configuration.sleep_time
|
|
12
|
+
@request_url = request_url || Capsolver.configuration.request_url
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def balance
|
|
16
|
+
control.get_balance
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
alias_method :get_balance, :balance
|
|
20
|
+
alias_method :aio_get_balance, :balance
|
|
21
|
+
|
|
22
|
+
def cloudflare(payload = {})
|
|
23
|
+
Cloudflare.new(api_key: @api_key, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def recaptcha(captcha_type, payload = {})
|
|
27
|
+
ReCaptcha.new(api_key: @api_key, captcha_type: captcha_type, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def image_to_text(payload = {})
|
|
31
|
+
ImageToText.new(api_key: @api_key, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def datadome_slider(payload = {})
|
|
35
|
+
DatadomeSlider.new(api_key: @api_key, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def friendly_captcha(payload = {})
|
|
39
|
+
FriendlyCaptcha.new(api_key: @api_key, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def gee_test(payload = {})
|
|
43
|
+
GeeTest.new(api_key: @api_key, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def mt_captcha(captcha_type, payload = {})
|
|
47
|
+
MtCaptcha.new(api_key: @api_key, captcha_type: captcha_type, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def aws_waf(captcha_type, payload = {})
|
|
51
|
+
AwsWaf.new(api_key: @api_key, captcha_type: captcha_type, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def yandex_captcha(payload = {})
|
|
55
|
+
YandexCaptcha.new(api_key: @api_key, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def vision_engine(payload = {})
|
|
59
|
+
VisionEngine.new(api_key: @api_key, sleep_time: @sleep_time, request_url: @request_url).captcha_handler(payload)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Low-level control delegates
|
|
63
|
+
def create_task(task_payload)
|
|
64
|
+
control.create_task(task_payload)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_task_result(task_id)
|
|
68
|
+
control.get_task_result(task_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def get_token(task_payload)
|
|
72
|
+
control.get_token(task_payload)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def feedback_task(task_id, result_payload)
|
|
76
|
+
control.feedback_task(task_id, result_payload)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def control
|
|
82
|
+
@control ||= Control.new(api_key: @api_key, sleep_time: @sleep_time, request_url: @request_url)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capsolver
|
|
4
|
+
class Control < Base
|
|
5
|
+
def initialize(api_key: nil, **kwargs)
|
|
6
|
+
super(api_key: api_key, captcha_type: "Control", **kwargs)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def get_balance
|
|
10
|
+
post_request("getBalance", { clientKey: @api_key })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
alias_method :aio_get_balance, :get_balance
|
|
14
|
+
|
|
15
|
+
def create_task(task_payload)
|
|
16
|
+
payload = {
|
|
17
|
+
clientKey: @api_key,
|
|
18
|
+
appId: APP_ID,
|
|
19
|
+
task: task_payload
|
|
20
|
+
}
|
|
21
|
+
post_request("createTask", payload)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
alias_method :aio_create_task, :create_task
|
|
25
|
+
|
|
26
|
+
def get_task_result(task_id)
|
|
27
|
+
payload = {
|
|
28
|
+
clientKey: @api_key,
|
|
29
|
+
taskId: task_id
|
|
30
|
+
}
|
|
31
|
+
post_request("getTaskResult", payload)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
alias_method :aio_get_task_result, :get_task_result
|
|
35
|
+
|
|
36
|
+
def get_token(task_payload)
|
|
37
|
+
payload = {
|
|
38
|
+
clientKey: @api_key,
|
|
39
|
+
task: task_payload
|
|
40
|
+
}
|
|
41
|
+
post_request("getToken", payload)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias_method :aio_get_token, :get_token
|
|
45
|
+
|
|
46
|
+
def feedback_task(task_id, result_payload)
|
|
47
|
+
payload = {
|
|
48
|
+
clientKey: @api_key,
|
|
49
|
+
taskId: task_id,
|
|
50
|
+
result: result_payload
|
|
51
|
+
}
|
|
52
|
+
post_request("feedbackTask", payload)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
alias_method :aio_feedback_task, :feedback_task
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capsolver
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ApiError < Error
|
|
7
|
+
attr_reader :error_id, :error_code, :error_description
|
|
8
|
+
|
|
9
|
+
def initialize(error_id:, error_code:, error_description:)
|
|
10
|
+
@error_id = error_id
|
|
11
|
+
@error_code = error_code
|
|
12
|
+
@error_description = error_description
|
|
13
|
+
super("[#{error_code}] #{error_description} (Error ID: #{error_id})")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class CaptchaUnsolvableError < Error; end
|
|
18
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "faraday"
|
|
7
|
+
|
|
8
|
+
module Capsolver
|
|
9
|
+
class FileInstrument
|
|
10
|
+
# Synchronously processes the file/link/base64 to a base64 encoded string.
|
|
11
|
+
def file_processing(
|
|
12
|
+
captcha_link: nil,
|
|
13
|
+
captcha_file: nil,
|
|
14
|
+
captcha_base64: nil,
|
|
15
|
+
save_format: "temp",
|
|
16
|
+
img_clearing: true,
|
|
17
|
+
file_path: "/tmp/",
|
|
18
|
+
file_extension: "png",
|
|
19
|
+
**kwargs
|
|
20
|
+
)
|
|
21
|
+
if captcha_file
|
|
22
|
+
content = File.binread(captcha_file)
|
|
23
|
+
Base64.strict_encode64(content)
|
|
24
|
+
elsif captcha_base64
|
|
25
|
+
Base64.strict_encode64(captcha_base64)
|
|
26
|
+
elsif captcha_link
|
|
27
|
+
content = url_read(captcha_link, **kwargs)
|
|
28
|
+
if save_format == "const"
|
|
29
|
+
full_path = file_const_saver(content, file_path, file_extension)
|
|
30
|
+
file_clean(full_path) if img_clearing
|
|
31
|
+
end
|
|
32
|
+
Base64.strict_encode64(content)
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "No valid captcha variant is set."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
alias_method :aio_file_processing, :file_processing
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def url_read(url, **kwargs)
|
|
43
|
+
# Extract headers and params if provided
|
|
44
|
+
headers = kwargs[:headers] || {}
|
|
45
|
+
params = kwargs[:params] || {}
|
|
46
|
+
|
|
47
|
+
# Use Faraday to fetch the URL
|
|
48
|
+
response = Faraday.get(url, params, headers)
|
|
49
|
+
unless response.success?
|
|
50
|
+
raise Error, "Failed to download captcha image from URL: #{url} (Status: #{response.status})"
|
|
51
|
+
end
|
|
52
|
+
response.body
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def file_const_saver(content, file_path, file_extension)
|
|
56
|
+
FileUtils.mkdir_p(file_path)
|
|
57
|
+
file_name = "file-#{SecureRandom.uuid}.#{file_extension}"
|
|
58
|
+
full_path = File.join(file_path, file_name)
|
|
59
|
+
File.binwrite(full_path, content)
|
|
60
|
+
full_path
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def file_clean(full_path)
|
|
64
|
+
FileUtils.rm_f(full_path)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capsolver
|
|
4
|
+
module Utils
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def camelize_keys(value)
|
|
8
|
+
case value
|
|
9
|
+
when Hash
|
|
10
|
+
value.each_with_object({}) do |(key, val), memo|
|
|
11
|
+
new_key = camelize_key(key.to_s)
|
|
12
|
+
memo[new_key] = camelize_keys(val)
|
|
13
|
+
end
|
|
14
|
+
when Array
|
|
15
|
+
value.map { |item| camelize_keys(item) }
|
|
16
|
+
else
|
|
17
|
+
value
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def camelize_key(str)
|
|
22
|
+
case str
|
|
23
|
+
when "website_url" then "websiteURL"
|
|
24
|
+
when "website_key" then "websiteKey"
|
|
25
|
+
when "aws_key" then "awsKey"
|
|
26
|
+
when "aws_challenge_js" then "awsChallengeJS"
|
|
27
|
+
when "client_key" then "clientKey"
|
|
28
|
+
when "task_id" then "taskId"
|
|
29
|
+
when "callback_url" then "callbackUrl"
|
|
30
|
+
when "app_id" then "appId"
|
|
31
|
+
when "error_id" then "errorId"
|
|
32
|
+
when "error_code" then "errorCode"
|
|
33
|
+
when "error_description" then "errorDescription"
|
|
34
|
+
when "captcha_url" then "captchaUrl"
|
|
35
|
+
when "user_agent" then "userAgent"
|
|
36
|
+
else
|
|
37
|
+
# Default snake_case to camelCase (e.g., foo_bar_baz -> fooBarBaz)
|
|
38
|
+
str.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize_response(value)
|
|
43
|
+
case value
|
|
44
|
+
when Hash
|
|
45
|
+
value.each_with_object({}) do |(key, val), memo|
|
|
46
|
+
norm_val = normalize_response(val)
|
|
47
|
+
|
|
48
|
+
# Compute key variations
|
|
49
|
+
snake = snakeize_key(key.to_s)
|
|
50
|
+
camel = camelize_key(snake)
|
|
51
|
+
|
|
52
|
+
memo[snake.to_sym] = norm_val
|
|
53
|
+
memo[snake] = norm_val
|
|
54
|
+
memo[camel.to_sym] = norm_val
|
|
55
|
+
memo[camel] = norm_val
|
|
56
|
+
end
|
|
57
|
+
when Array
|
|
58
|
+
value.map { |item| normalize_response(item) }
|
|
59
|
+
else
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def snakeize_key(str)
|
|
65
|
+
# Handlers for specific replacements
|
|
66
|
+
str = str.gsub("websiteURL", "website_url")
|
|
67
|
+
str = str.gsub("awsChallengeJS", "aws_challenge_js")
|
|
68
|
+
|
|
69
|
+
# Convert standard camelCase to snake_case
|
|
70
|
+
str.gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }.gsub(/^_/, "")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capsolver
|
|
4
|
+
class YandexCaptcha < Base
|
|
5
|
+
def initialize(api_key: nil, captcha_type: "YandexCaptchaTaskProxyLess", **kwargs)
|
|
6
|
+
super(api_key: api_key, captcha_type: captcha_type, **kwargs)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# For convenience, alias Yandex as YandexCaptcha
|
|
11
|
+
Yandex = YandexCaptcha
|
|
12
|
+
end
|
data/lib/capsolver.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "capsolver/version"
|
|
4
|
+
|
|
5
|
+
module Capsolver
|
|
6
|
+
autoload :Error, "capsolver/error"
|
|
7
|
+
autoload :ApiError, "capsolver/error"
|
|
8
|
+
autoload :CaptchaUnsolvableError, "capsolver/error"
|
|
9
|
+
autoload :Configuration, "capsolver/configuration"
|
|
10
|
+
autoload :Utils, "capsolver/utils"
|
|
11
|
+
autoload :Client, "capsolver/client"
|
|
12
|
+
autoload :FileInstrument, "capsolver/file_instrument"
|
|
13
|
+
autoload :Base, "capsolver/base"
|
|
14
|
+
autoload :Control, "capsolver/control"
|
|
15
|
+
autoload :ReCaptcha, "capsolver/recaptcha"
|
|
16
|
+
autoload :ImageToText, "capsolver/image_to_text"
|
|
17
|
+
autoload :Cloudflare, "capsolver/cloudflare"
|
|
18
|
+
autoload :DatadomeSlider, "capsolver/datadome_slider"
|
|
19
|
+
autoload :FriendlyCaptcha, "capsolver/friendly_captcha"
|
|
20
|
+
autoload :GeeTest, "capsolver/gee_test"
|
|
21
|
+
autoload :MtCaptcha, "capsolver/mt_captcha"
|
|
22
|
+
autoload :AwsWaf, "capsolver/aws_waf"
|
|
23
|
+
autoload :YandexCaptcha, "capsolver/yandex"
|
|
24
|
+
autoload :Yandex, "capsolver/yandex"
|
|
25
|
+
autoload :VisionEngine, "capsolver/vision_engine"
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
attr_writer :configuration
|
|
29
|
+
|
|
30
|
+
def configuration
|
|
31
|
+
@configuration ||= Configuration.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def configure
|
|
35
|
+
yield(configuration)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/sig/capsolver.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: capsolver
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jorge Romussi
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: base64
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: webmock
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rake
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '13.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '13.0'
|
|
82
|
+
description: A Ruby client library for the Capsolver API. Supports ReCaptcha (V2/V3),
|
|
83
|
+
Cloudflare (Turnstile), DataDome, GeeTest, MtCaptcha, AWS WAF, Yandex, and ImageToText.
|
|
84
|
+
email:
|
|
85
|
+
- jorge@rompe.cl
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- LICENSE.txt
|
|
91
|
+
- README.md
|
|
92
|
+
- Rakefile
|
|
93
|
+
- lib/capsolver.rb
|
|
94
|
+
- lib/capsolver/aws_waf.rb
|
|
95
|
+
- lib/capsolver/base.rb
|
|
96
|
+
- lib/capsolver/client.rb
|
|
97
|
+
- lib/capsolver/cloudflare.rb
|
|
98
|
+
- lib/capsolver/configuration.rb
|
|
99
|
+
- lib/capsolver/control.rb
|
|
100
|
+
- lib/capsolver/datadome_slider.rb
|
|
101
|
+
- lib/capsolver/error.rb
|
|
102
|
+
- lib/capsolver/file_instrument.rb
|
|
103
|
+
- lib/capsolver/friendly_captcha.rb
|
|
104
|
+
- lib/capsolver/gee_test.rb
|
|
105
|
+
- lib/capsolver/image_to_text.rb
|
|
106
|
+
- lib/capsolver/mt_captcha.rb
|
|
107
|
+
- lib/capsolver/recaptcha.rb
|
|
108
|
+
- lib/capsolver/utils.rb
|
|
109
|
+
- lib/capsolver/version.rb
|
|
110
|
+
- lib/capsolver/vision_engine.rb
|
|
111
|
+
- lib/capsolver/yandex.rb
|
|
112
|
+
- sig/capsolver.rbs
|
|
113
|
+
homepage: https://github.com/AndreiDrang/ruby-capsolver
|
|
114
|
+
licenses: []
|
|
115
|
+
metadata:
|
|
116
|
+
homepage_uri: https://github.com/AndreiDrang/ruby-capsolver
|
|
117
|
+
source_code_uri: https://github.com/AndreiDrang/ruby-capsolver
|
|
118
|
+
rdoc_options: []
|
|
119
|
+
require_paths:
|
|
120
|
+
- lib
|
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: 3.2.0
|
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
requirements: []
|
|
132
|
+
rubygems_version: 3.6.9
|
|
133
|
+
specification_version: 4
|
|
134
|
+
summary: Ruby client for the Capsolver service API.
|
|
135
|
+
test_files: []
|