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 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "lib" << "test"
8
+ t.pattern = "test/**/*_test.rb"
9
+ t.verbose = true
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class AwsWaf < Base
5
+ def initialize(captcha_type:, api_key: nil, **kwargs)
6
+ super(api_key: api_key, captcha_type: captcha_type, **kwargs)
7
+ end
8
+ end
9
+ end
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class Cloudflare < Base
5
+ def initialize(api_key: nil, captcha_type: "AntiTurnstileTaskProxyLess", **kwargs)
6
+ super(api_key: api_key, captcha_type: captcha_type, **kwargs)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class Configuration
5
+ attr_accessor :api_key, :sleep_time, :request_url
6
+
7
+ def initialize
8
+ @sleep_time = 5
9
+ @request_url = "https://api.capsolver.com"
10
+ end
11
+ end
12
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class DatadomeSlider < Base
5
+ def initialize(api_key: nil, captcha_type: "DatadomeSliderTask", **kwargs)
6
+ super(api_key: api_key, captcha_type: captcha_type, **kwargs)
7
+ end
8
+ end
9
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class FriendlyCaptcha < Base
5
+ def initialize(api_key: nil, captcha_type: "FriendlyCaptchaTaskProxyless", **kwargs)
6
+ super(api_key: api_key, captcha_type: captcha_type, **kwargs)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class GeeTest < Base
5
+ def initialize(api_key: nil, captcha_type: "GeeTestTaskProxyLess", **kwargs)
6
+ super(api_key: api_key, captcha_type: captcha_type, **kwargs)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class ImageToText < Base
5
+ def initialize(api_key: nil, captcha_type: "ImageToTextTask", **kwargs)
6
+ super(api_key: api_key, captcha_type: captcha_type, **kwargs)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class MtCaptcha < Base
5
+ def initialize(captcha_type:, api_key: nil, **kwargs)
6
+ super(api_key: api_key, captcha_type: captcha_type, **kwargs)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class ReCaptcha < Base
5
+ def initialize(captcha_type:, api_key: nil, **kwargs)
6
+ super(api_key: api_key, captcha_type: captcha_type, **kwargs)
7
+ end
8
+ end
9
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsolver
4
+ class VisionEngine < Base
5
+ def initialize(api_key: nil, captcha_type: "VisionEngine", **kwargs)
6
+ super(api_key: api_key, captcha_type: captcha_type, **kwargs)
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,4 @@
1
+ module Capsolver
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []