anticaptchaofficial 1.0.1
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 +21 -0
- data/README.md +442 -0
- data/lib/anticaptcha/client.rb +352 -0
- data/lib/anticaptcha/errors.rb +52 -0
- data/lib/anticaptcha/solution.rb +162 -0
- data/lib/anticaptcha/tasks.rb +318 -0
- data/lib/anticaptcha/version.rb +5 -0
- data/lib/anticaptcha.rb +33 -0
- metadata +57 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Anticaptcha
|
|
9
|
+
# Talks to the Anti-Captcha API.
|
|
10
|
+
#
|
|
11
|
+
# client = Anticaptcha::Client.new("API_KEY_HERE")
|
|
12
|
+
# solution = client.solve_recaptcha_v2(
|
|
13
|
+
# website_url: "https://www.website.com/",
|
|
14
|
+
# website_key: "SITE_KEY"
|
|
15
|
+
# )
|
|
16
|
+
# puts solution.g_recaptcha_response
|
|
17
|
+
class Client
|
|
18
|
+
API_HOST = "https://api.anti-captcha.com/"
|
|
19
|
+
|
|
20
|
+
# Your key from https://anti-captcha.com/clients/settings/apisetup
|
|
21
|
+
attr_accessor :api_key
|
|
22
|
+
|
|
23
|
+
# Specify a soft id to earn 10% commission with your app.
|
|
24
|
+
# Get yours at https://anti-captcha.com/clients/tools/devcenter
|
|
25
|
+
attr_accessor :soft_id
|
|
26
|
+
|
|
27
|
+
# Debug output goes to stderr. On by default.
|
|
28
|
+
attr_accessor :verbose
|
|
29
|
+
|
|
30
|
+
# Timeout of a single HTTP call to the API, in seconds.
|
|
31
|
+
attr_accessor :connection_timeout
|
|
32
|
+
|
|
33
|
+
# Seconds to wait before the first status request.
|
|
34
|
+
attr_accessor :first_attempt_waiting_interval
|
|
35
|
+
|
|
36
|
+
# Seconds to wait between the following status requests.
|
|
37
|
+
attr_accessor :normal_waiting_interval
|
|
38
|
+
|
|
39
|
+
# Seconds to keep polling before giving up.
|
|
40
|
+
attr_accessor :max_waiting_time
|
|
41
|
+
|
|
42
|
+
# Full getTaskResult response of the last solved task.
|
|
43
|
+
attr_reader :last_task_info
|
|
44
|
+
|
|
45
|
+
def initialize(api_key = nil, soft_id: 0, verbose: true)
|
|
46
|
+
@api_key = api_key
|
|
47
|
+
@soft_id = soft_id
|
|
48
|
+
@verbose = verbose
|
|
49
|
+
@connection_timeout = 30
|
|
50
|
+
@first_attempt_waiting_interval = 5
|
|
51
|
+
@normal_waiting_interval = 5
|
|
52
|
+
@max_waiting_time = 300
|
|
53
|
+
@last_task_info = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Account balance in US dollars.
|
|
57
|
+
def balance
|
|
58
|
+
json_request("getBalance")["balance"]
|
|
59
|
+
end
|
|
60
|
+
alias get_balance balance
|
|
61
|
+
|
|
62
|
+
# Number of prepaid captcha credits on the account.
|
|
63
|
+
def credits_balance
|
|
64
|
+
json_request("getBalance")["captchaCredits"] || 0
|
|
65
|
+
end
|
|
66
|
+
alias get_credits_balance credits_balance
|
|
67
|
+
|
|
68
|
+
# Calls any API method with your own payload. The client key is added for you.
|
|
69
|
+
def json_request(method, payload = {})
|
|
70
|
+
raise InvalidTaskError, "API key is not set" if api_key.to_s.empty?
|
|
71
|
+
|
|
72
|
+
url = URI.join(API_HOST, method)
|
|
73
|
+
log "POST #{url}"
|
|
74
|
+
|
|
75
|
+
body = post_json(url, payload.merge("clientKey" => api_key))
|
|
76
|
+
response = parse_json(body)
|
|
77
|
+
error_id = response["errorId"]
|
|
78
|
+
|
|
79
|
+
unless error_id.is_a?(Integer)
|
|
80
|
+
raise BadResponseError.new("the API response carries no errorId", shorten(body))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
return response if error_id.zero?
|
|
84
|
+
|
|
85
|
+
raise ApiError.new(error_id, response["errorCode"].to_s,
|
|
86
|
+
response["errorDescription"] || "(no error description)")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Submits a task and returns its id.
|
|
90
|
+
def create_task(task)
|
|
91
|
+
log "Creating task: #{JSON.generate(task)}"
|
|
92
|
+
|
|
93
|
+
response = json_request("createTask", "task" => task, "softId" => soft_id)
|
|
94
|
+
task_id = response["taskId"]
|
|
95
|
+
|
|
96
|
+
unless task_id.is_a?(Integer)
|
|
97
|
+
raise BadResponseError.new("API did not return a task id", shorten(JSON.generate(response)))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
log "Task id: #{task_id}"
|
|
101
|
+
|
|
102
|
+
task_id
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Polls until the task is solved or the waiting limit runs out.
|
|
106
|
+
def wait_for_result(task_id)
|
|
107
|
+
deadline = monotonic_now + max_waiting_time
|
|
108
|
+
interval = first_attempt_waiting_interval
|
|
109
|
+
|
|
110
|
+
loop do
|
|
111
|
+
log "Waiting #{interval} seconds..."
|
|
112
|
+
sleep interval
|
|
113
|
+
interval = normal_waiting_interval
|
|
114
|
+
|
|
115
|
+
response = json_request("getTaskResult", "taskId" => task_id)
|
|
116
|
+
|
|
117
|
+
case response["status"]
|
|
118
|
+
when "ready"
|
|
119
|
+
@last_task_info = response
|
|
120
|
+
solution = Solution.new(response["solution"], task_id: task_id, cost: response["cost"].to_f)
|
|
121
|
+
|
|
122
|
+
if solution.empty?
|
|
123
|
+
raise BadResponseError.new("the task is ready but carries no solution",
|
|
124
|
+
shorten(JSON.generate(response)))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
log "The task is complete"
|
|
128
|
+
|
|
129
|
+
return solution
|
|
130
|
+
when "processing"
|
|
131
|
+
if monotonic_now >= deadline
|
|
132
|
+
raise TimeoutError,
|
|
133
|
+
"the task was not solved in #{max_waiting_time} seconds"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
log "The task is still processing..."
|
|
137
|
+
else
|
|
138
|
+
raise BadResponseError.new(
|
|
139
|
+
"unknown task status #{response["status"].inspect}, please update the library",
|
|
140
|
+
shorten(JSON.generate(response))
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Sends a task type this library does not wrap yet, and waits for the result.
|
|
147
|
+
# See https://anti-captcha.com/apidoc/task-types for the "task" object shape.
|
|
148
|
+
def solve_task(task)
|
|
149
|
+
wait_for_result(create_task(task))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# ------------------------------------------------------------------ tasks
|
|
153
|
+
|
|
154
|
+
def solve_image(**options)
|
|
155
|
+
solve_task(Tasks.image(**options))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def solve_image_file(path, **options)
|
|
159
|
+
solve_image(body: self.class.file_to_base64(path), **options)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def solve_image_to_coordinates(**options)
|
|
163
|
+
solve_task(Tasks.image_to_coordinates(**options))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def solve_image_to_coordinates_file(path, **options)
|
|
167
|
+
solve_image_to_coordinates(body: self.class.file_to_base64(path), **options)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def solve_recaptcha_v2(**options)
|
|
171
|
+
solve_task(Tasks.recaptcha_v2(**proxyless(options)))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def solve_recaptcha_v2_proxy_on(proxy:, **options)
|
|
175
|
+
solve_task(Tasks.recaptcha_v2(proxy: proxy, **options))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def solve_recaptcha_v3(**options)
|
|
179
|
+
solve_task(Tasks.recaptcha_v3(**options))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def solve_hcaptcha(**options)
|
|
183
|
+
solve_task(Tasks.hcaptcha(**proxyless(options)))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def solve_hcaptcha_proxy_on(proxy:, **options)
|
|
187
|
+
solve_task(Tasks.hcaptcha(proxy: proxy, **options))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def solve_funcaptcha(**options)
|
|
191
|
+
solve_task(Tasks.funcaptcha(**proxyless(options)))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def solve_funcaptcha_proxy_on(proxy:, **options)
|
|
195
|
+
solve_task(Tasks.funcaptcha(proxy: proxy, **options))
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def solve_geetest(**options)
|
|
199
|
+
solve_task(Tasks.geetest(**proxyless(options)))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def solve_geetest_proxy_on(proxy:, **options)
|
|
203
|
+
solve_task(Tasks.geetest(proxy: proxy, **options))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def solve_turnstile(**options)
|
|
207
|
+
solve_task(Tasks.turnstile(**proxyless(options)))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def solve_turnstile_proxy_on(proxy:, **options)
|
|
211
|
+
solve_task(Tasks.turnstile(proxy: proxy, **options))
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def solve_prosopo(**options)
|
|
215
|
+
solve_task(Tasks.prosopo(**proxyless(options)))
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def solve_prosopo_proxy_on(proxy:, **options)
|
|
219
|
+
solve_task(Tasks.prosopo(proxy: proxy, **options))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def solve_friendly_captcha(**options)
|
|
223
|
+
solve_task(Tasks.friendly_captcha(**proxyless(options)))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def solve_friendly_captcha_proxy_on(proxy:, **options)
|
|
227
|
+
solve_task(Tasks.friendly_captcha(proxy: proxy, **options))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def solve_altcha(**options)
|
|
231
|
+
solve_task(Tasks.altcha(**proxyless(options)))
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def solve_altcha_proxy_on(proxy:, **options)
|
|
235
|
+
solve_task(Tasks.altcha(proxy: proxy, **options))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def solve_amazon(**options)
|
|
239
|
+
solve_task(Tasks.amazon(**proxyless(options)))
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def solve_amazon_proxy_on(proxy:, **options)
|
|
243
|
+
solve_task(Tasks.amazon(proxy: proxy, **options))
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def solve_antigate(**options)
|
|
247
|
+
solve_task(Tasks.antigate(**options))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def solve_antibot_cookie(**options)
|
|
251
|
+
solve_task(Tasks.antibot_cookie(**options))
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# ---------------------------------------------------------------- reports
|
|
255
|
+
|
|
256
|
+
def report_incorrect_image_captcha(task_id)
|
|
257
|
+
json_request("reportIncorrectImageCaptcha", "taskId" => task_id)
|
|
258
|
+
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def report_incorrect_recaptcha(task_id)
|
|
263
|
+
json_request("reportIncorrectRecaptcha", "taskId" => task_id)
|
|
264
|
+
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def report_correct_recaptcha(task_id)
|
|
269
|
+
json_request("reportCorrectRecaptcha", "taskId" => task_id)
|
|
270
|
+
|
|
271
|
+
nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def report_incorrect_hcaptcha(task_id)
|
|
275
|
+
json_request("reportIncorrectHcaptcha", "taskId" => task_id)
|
|
276
|
+
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------- helpers
|
|
281
|
+
|
|
282
|
+
# Reads a file and encodes it in base64.
|
|
283
|
+
def self.file_to_base64(path)
|
|
284
|
+
to_base64(File.binread(path))
|
|
285
|
+
rescue SystemCallError, IOError => e
|
|
286
|
+
raise InvalidTaskError, "could not read #{path}: #{e.message}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Encodes bytes in base64.
|
|
290
|
+
def self.to_base64(bytes)
|
|
291
|
+
[bytes].pack("m0")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
private
|
|
295
|
+
|
|
296
|
+
def proxyless(options)
|
|
297
|
+
raise InvalidTaskError, "pass the proxy to the *_proxy_on method instead" if options.key?(:proxy)
|
|
298
|
+
|
|
299
|
+
options
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def post_json(url, payload)
|
|
303
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
304
|
+
http.use_ssl = url.scheme == "https"
|
|
305
|
+
http.open_timeout = connection_timeout
|
|
306
|
+
http.read_timeout = connection_timeout
|
|
307
|
+
|
|
308
|
+
request = Net::HTTP::Post.new(url.request_uri,
|
|
309
|
+
"Content-Type" => "application/json",
|
|
310
|
+
"Accept" => "application/json",
|
|
311
|
+
"User-Agent" => "anticaptcha-ruby/#{Anticaptcha::VERSION}")
|
|
312
|
+
request.body = JSON.generate(payload)
|
|
313
|
+
|
|
314
|
+
response = http.request(request)
|
|
315
|
+
|
|
316
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
317
|
+
raise BadResponseError.new("API answered with HTTP #{response.code}", shorten(response.body))
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
response.body
|
|
321
|
+
rescue Timeout::Error => e
|
|
322
|
+
raise NetworkError, "request to #{url} timed out after #{connection_timeout} seconds: #{e.message}"
|
|
323
|
+
rescue SystemCallError, SocketError, IOError, OpenSSL::SSL::SSLError => e
|
|
324
|
+
raise NetworkError, "request to #{url} failed: #{e.message}"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def parse_json(body)
|
|
328
|
+
parsed = JSON.parse(body.to_s)
|
|
329
|
+
|
|
330
|
+
unless parsed.is_a?(Hash)
|
|
331
|
+
raise BadResponseError.new("the API response is not a JSON object",
|
|
332
|
+
shorten(body))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
parsed
|
|
336
|
+
rescue JSON::ParserError => e
|
|
337
|
+
raise BadResponseError.new("could not parse the API response as JSON: #{e.message}", shorten(body))
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def shorten(text)
|
|
341
|
+
text.to_s[0, 500]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def log(message)
|
|
345
|
+
warn "[anticaptcha] #{message}" if verbose
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def monotonic_now
|
|
349
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Anticaptcha
|
|
4
|
+
# Base class of every error this library raises.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# A task is missing a required parameter, or a value is out of range.
|
|
8
|
+
# Nothing was sent to the API.
|
|
9
|
+
class InvalidTaskError < Error; end
|
|
10
|
+
|
|
11
|
+
# The API could not be reached.
|
|
12
|
+
class NetworkError < Error; end
|
|
13
|
+
|
|
14
|
+
# The API answered with something that is not the JSON we expect.
|
|
15
|
+
class BadResponseError < Error
|
|
16
|
+
# First 500 characters of what the API actually sent.
|
|
17
|
+
attr_reader :raw
|
|
18
|
+
|
|
19
|
+
def initialize(message, raw = nil)
|
|
20
|
+
@raw = raw
|
|
21
|
+
super(raw ? "#{message}; raw response: #{raw}" : message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The API answered with a non-zero errorId.
|
|
26
|
+
# See https://anti-captcha.com/apidoc/errors for the list of codes.
|
|
27
|
+
class ApiError < Error
|
|
28
|
+
attr_reader :error_id, :error_code, :description
|
|
29
|
+
|
|
30
|
+
# Codes worth retrying the very same task on.
|
|
31
|
+
RETRYABLE = %w[
|
|
32
|
+
ERROR_NO_SLOT_AVAILABLE
|
|
33
|
+
ERROR_SERVICE_OVERLOAD
|
|
34
|
+
ERROR_RATE_LIMIT
|
|
35
|
+
ERROR_CAPTCHA_UNSOLVABLE
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
def initialize(error_id, error_code, description)
|
|
39
|
+
@error_id = error_id
|
|
40
|
+
@error_code = error_code
|
|
41
|
+
@description = description
|
|
42
|
+
super("API error #{error_id} #{error_code}: #{description}")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def retryable?
|
|
46
|
+
RETRYABLE.include?(error_code)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The task was still unsolved when the waiting limit ran out.
|
|
51
|
+
class TimeoutError < Error; end
|
|
52
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Anticaptcha
|
|
4
|
+
# The "solution" object of a completed task.
|
|
5
|
+
#
|
|
6
|
+
# Which readers return something depends on the task type; see
|
|
7
|
+
# https://anti-captcha.com/apidoc for the per-type description. Readers for
|
|
8
|
+
# fields a task type does not fill return nil, so nothing blows up.
|
|
9
|
+
class Solution
|
|
10
|
+
# The whole "solution" object, for fields this class does not expose yet.
|
|
11
|
+
attr_reader :raw
|
|
12
|
+
|
|
13
|
+
# Id of the task that produced this solution, for the report_* methods.
|
|
14
|
+
attr_reader :task_id
|
|
15
|
+
|
|
16
|
+
# What the task cost, in US dollars.
|
|
17
|
+
attr_reader :cost
|
|
18
|
+
|
|
19
|
+
def initialize(raw, task_id: nil, cost: nil)
|
|
20
|
+
@raw = raw || {}
|
|
21
|
+
@task_id = task_id
|
|
22
|
+
@cost = cost
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Any field by its API name.
|
|
26
|
+
def [](field)
|
|
27
|
+
raw[field.to_s]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Image captchas.
|
|
31
|
+
def text
|
|
32
|
+
self["text"]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# FunCaptcha, Turnstile, Prosopo, Friendly Captcha, Altcha, Amazon WAF.
|
|
36
|
+
def token
|
|
37
|
+
self["token"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Recaptcha and hCaptcha.
|
|
41
|
+
def g_recaptcha_response
|
|
42
|
+
self["gRecaptchaResponse"]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Recaptcha with isExtended.
|
|
46
|
+
def g_recaptcha_response_md5
|
|
47
|
+
self["gRecaptchaResponseMd5"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# hCaptcha.
|
|
51
|
+
def resp_key
|
|
52
|
+
self["respKey"]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# User-agent of the worker who solved the captcha. Submit the form with it.
|
|
56
|
+
def user_agent
|
|
57
|
+
self["userAgent"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# AntiGate and AntiBotCookie tasks.
|
|
61
|
+
def url
|
|
62
|
+
self["url"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Amazon WAF: the domain the aws-waf-token cookie belongs to.
|
|
66
|
+
def domain
|
|
67
|
+
self["domain"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# GeeTest v3.
|
|
71
|
+
def challenge
|
|
72
|
+
self["challenge"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# GeeTest v3.
|
|
76
|
+
def seccode
|
|
77
|
+
self["seccode"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# GeeTest v3.
|
|
81
|
+
def validate
|
|
82
|
+
self["validate"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# GeeTest v4.
|
|
86
|
+
def captcha_id
|
|
87
|
+
self["captcha_id"]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# GeeTest v4.
|
|
91
|
+
def lot_number
|
|
92
|
+
self["lot_number"]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# GeeTest v4.
|
|
96
|
+
def pass_token
|
|
97
|
+
self["pass_token"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# GeeTest v4.
|
|
101
|
+
def gen_time
|
|
102
|
+
self["gen_time"]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# GeeTest v4.
|
|
106
|
+
def captcha_output
|
|
107
|
+
self["captcha_output"]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# AntiGate and AntiBotCookie tasks.
|
|
111
|
+
def cookies
|
|
112
|
+
self["cookies"]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# AntiGate and AntiBotCookie tasks.
|
|
116
|
+
def local_storage
|
|
117
|
+
self["localStorage"]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# AntiGate and AntiBotCookie tasks.
|
|
121
|
+
def fingerprint
|
|
122
|
+
self["fingerprint"]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# AntiBotCookie tasks: the headers the worker's browser sent last.
|
|
126
|
+
def last_request_headers
|
|
127
|
+
self["lastRequestHeaders"]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# ImageToCoordinates tasks: [x, y] points or [x1, y1, x2, y2] boxes.
|
|
131
|
+
def coordinates
|
|
132
|
+
self["coordinates"]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The cookies formatted as a "name1=value1; name2=value2" header value.
|
|
136
|
+
def cookie_header
|
|
137
|
+
jar = cookies
|
|
138
|
+
|
|
139
|
+
return "" unless jar.is_a?(Hash)
|
|
140
|
+
|
|
141
|
+
jar.map { |name, value| "#{name}=#{value}" }.join("; ")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The worker's browser user-agent, taken from the AntiBotCookie fingerprint.
|
|
145
|
+
def fingerprint_user_agent
|
|
146
|
+
fingerprint.is_a?(Hash) ? fingerprint["self.navigator.userAgent"] : nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# True when the API returned a "solution" object without any usable field.
|
|
150
|
+
def empty?
|
|
151
|
+
raw.empty?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def to_h
|
|
155
|
+
raw
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def inspect
|
|
159
|
+
"#<Anticaptcha::Solution task_id=#{task_id.inspect} fields=#{raw.keys.inspect}>"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|