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.
@@ -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