diff --git a/.env.example b/.env.example index 131e8bc..b9df749 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,7 @@ CAPSOLVER_KEY="" ANTICAPTCHA_KEY="" # API key for capmonster -CAPMONSTER_KEY="" \ No newline at end of file +CAPMONSTER_KEY="" + +# API key for captchaai +CAPTCHAAI_KEY="" \ No newline at end of file diff --git a/captchatools-go/README.md b/captchatools-go/README.md index 50ac50b..24d30ef 100644 --- a/captchatools-go/README.md +++ b/captchatools-go/README.md @@ -261,18 +261,19 @@ func addtional_data() { - **[2Captcha](https://www.2captcha.com/)** - **[Anticaptcha](https://www.anti-captcha.com/)** - **[Capsolver](https://capsolver.com/)** +- **[CaptchaAI](https://captchaai.com/)** ### Site-Specific Support: -| Captcha Type |2Captcha | Anticaptcha | Capmonster| Capsolver | -| :-------------: |:-------------:| :-----:| :-----:| :-----:| -| Recaptcha V2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Recaptcha V3 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Hcaptcha | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Image Captcha | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Cloudflare Turnstile | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | -| Funcaptcha |:x: | :x: | :x: | :x: | -| GeeTest |:x: | :x: | :x: | :x: | -| Amazon WAF |:x: | :x: | :x: | :x: | +| Captcha Type |2Captcha | Anticaptcha | Capmonster| Capsolver | CaptchaAI| +| :-------------: |:-------------:| :-----:| :-----:| :-----:| :-----:| +| Recaptcha V2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Recaptcha V3 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Hcaptcha | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Image Captcha | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Cloudflare Turnstile | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | +| Funcaptcha |:x: | :x: | :x: | :x: | :x: | +| GeeTest |:x: | :x: | :x: | :x: | :x: | +| Amazon WAF |:x: | :x: | :x: | :x: | :x: | diff --git a/captchatools-go/captchaai.go b/captchatools-go/captchaai.go new file mode 100644 index 0000000..6d04f7e --- /dev/null +++ b/captchatools-go/captchaai.go @@ -0,0 +1,220 @@ +package captchatoolsgo + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" +) + +type CaptchaAi struct { + *Config +} + +func (t CaptchaAi) GetToken(additional ...*AdditionalData) (*CaptchaAnswer, error) { + return t.getCaptchaAnswer(context.Background(), additional...) +} + +func (t CaptchaAi) GetTokenWithContext(ctx context.Context, additional ...*AdditionalData) (*CaptchaAnswer, error) { + return t.getCaptchaAnswer(ctx, additional...) +} + +func (t CaptchaAi) GetBalance() (float32, error) { + return t.getBalance() +} + +// Method to get Queue ID from the API. +func (t CaptchaAi) getID(data *AdditionalData) (string, error) { + // Get Payload + uri, err := t.createUrl(data) + if err != nil { + return "", err + } + + // Make request to get answer + response := &struct { + Status int `json:"status"` + Request int `json:"request"` + }{} + for i := 0; i < 100; i++ { + resp, err := http.Get(uri) + if err != nil { + time.Sleep(3 * time.Second) + continue + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + json.Unmarshal(body, response) + + // Parse the response + if response.Status != 1 { // Means there was an error + // Have to read the error into an interface + temp := make(map[string]string) + json.Unmarshal(body, &temp) + return "", errCodeToError(temp["request"]) + } + return strconv.Itoa(response.Request), nil + } + return "", ErrMaxAttempts +} + +// This method gets the captcha token from the Capmonster API +func (t CaptchaAi) getCaptchaAnswer(ctx context.Context, additional ...*AdditionalData) (*CaptchaAnswer, error) { + var data *AdditionalData = nil + if len(additional) > 0 { + data = additional[0] + } + + // Get Queue ID + queueID, err := t.getID(data) + if err != nil { + return nil, err + } + + // Get Captcha Answer + response := &twocaptchaResponse{} + urlToAnswer := fmt.Sprintf( + "https://ocr.captchaai.com/res.php?key=%v&action=get&id=%v&json=1", + t.Api_key, + queueID, + ) + for i := 0; i < 100; i++ { + req, _ := http.NewRequestWithContext(ctx, "GET", urlToAnswer, nil) + resp, err := makeRequest(req) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return nil, fmt.Errorf("getCaptchaAnswer error: %w", err) + } + time.Sleep(3 * time.Second) + continue + } + + // Parse Response + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + json.Unmarshal(body, response) + + // Check for any errors + if response.Status == 0 && response.Request != "CAPCHA_NOT_READY" { + return nil, errCodeToError(response.Request) + } + + // Check if captcha is ready + if response.Request == "CAPCHA_NOT_READY" { + time.Sleep(3 * time.Second) + continue + } + return newCaptchaAnswer( + queueID, + response.Request, + t.Api_key, + t.CaptchaType, + CaptchaAiSite, + "", + ), nil + } + return nil, ErrMaxAttempts +} + +func (t CaptchaAi) getBalance() (float32, error) { + // Attempt to get the balance from the API + // Max attempts is 5 + url := fmt.Sprintf("https://ocr.captchaai.com/res.php?key=%v&action=getbalance&json=1", t.Api_key) + response := &twocaptchaResponse{} + for i := 0; i < 5; i++ { + resp, err := http.Get(url) + if err != nil { + time.Sleep(1 * time.Second) + continue + } + + // Parse Response + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + json.Unmarshal(body, response) + if response.Status == 0 { + return 0, errCodeToError(response.Request) + } + + // Convert to float32 + var balance float32 + value, err := strconv.ParseFloat(response.Request, 32) + if err != nil { + return 0, errors.New("unable to convert balance") + } + balance = float32(value) + return balance, nil + } + return 0, ErrMaxAttempts +} + +/* +createUrl creates the Uri needed to submit data to CaptchaAi + +Possible errors that can be returned: +1) ErrIncorrectCapType +*/ +func (t CaptchaAi) createUrl(data *AdditionalData) (string, error) { + + // Create base uri + u, err := url.Parse("https://ocr.captchaai.com/in.php") + if err != nil { + return "", fmt.Errorf("createUrl error: %w", err) + } + + // Dynamically add queries + query := u.Query() + query.Add("key", t.Api_key) + query.Add("json", "1") + query.Add("pageurl", t.CaptchaURL) + switch t.CaptchaType { + case ImageCaptcha: + query.Add("method", "base64") + if data != nil && data.B64Img != "" { + query.Add("body", data.B64Img) + } + case V2Captcha: + query.Add("method", "userrecaptcha") + query.Add("googlekey", t.Sitekey) + if t.IsInvisibleCaptcha { + query.Add("invisible", "1") + } + case V3Captcha: + query.Add("method", "userrecaptcha") + query.Add("version", "v3") + query.Add("googlekey", t.Sitekey) + if t.Action != "" { + query.Add("action", t.Action) + } + if t.MinScore > 0 { + query.Add("min_score", fmt.Sprintf("%v", t.MinScore)) + } + case HCaptcha: + query.Add("method", "hcaptcha") + query.Add("sitekey", t.Sitekey) + + case CFTurnstile: + return "", ErrNotSupported + default: + return "", ErrIncorrectCapType + } + if data != nil && t.CaptchaType != ImageCaptcha { + if data.UserAgent != "" { + query.Add("userAgent", data.UserAgent) + } + if data.Proxy != nil { + query.Add("proxy", data.Proxy.StringFormatted()) + } + if data.ProxyType != "" { + query.Add("proxytype", data.ProxyType) + } + } + + u.RawQuery = query.Encode() + return u.String(), nil +} diff --git a/captchatools-go/captchaai_test.go b/captchatools-go/captchaai_test.go new file mode 100644 index 0000000..4b5c07e --- /dev/null +++ b/captchatools-go/captchaai_test.go @@ -0,0 +1,150 @@ +package captchatoolsgo + +import ( + "fmt" + "os" + "testing" + + "github.com/joho/godotenv" +) + +// Test getting a recap V2 token +// go test -v -run ^TestCaptchaAiV2$ github.com/Matthew17-21/Captcha-Tools/captchatools-go +func TestCaptchaAiV2(t *testing.T) { + // Load ENV + if err := godotenv.Load("../.env"); err != nil { + t.Fatalf("Failed to load .env file: %v", err) + } + + // Create tests + configs := []Config{ + {Api_key: os.Getenv("CAPTCHAAI_KEY"), Sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", CaptchaURL: "https://www.google.com/recaptcha/api2/demo", CaptchaType: V2Captcha}, + {Api_key: os.Getenv("CAPTCHAAI_KEY"), Sitekey: "6LcmDCcUAAAAAL5QmnMvDFnfPTP4iCUYRk2MwC0-", CaptchaURL: "https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php", CaptchaType: V2Captcha, IsInvisibleCaptcha: true}, + } + + // Run tests + for testNum, config := range configs { + t.Run(fmt.Sprintf("Test #%v", testNum+1), func(t *testing.T) { + h := CaptchaAi{&config} + answer, err := h.GetToken() + if err != nil { + t.Fatalf("Error getting token: %v", err) + } + fmt.Println(answer) + + }) + } + +} + +// Test getting a recap V3 token +// go test -v -run ^TestCaptchaAiV3$ github.com/Matthew17-21/Captcha-Tools/captchatools-go +func TestCaptchaAiV3(t *testing.T) { + // Load ENV + if err := godotenv.Load("../.env"); err != nil { + t.Fatalf("Failed to load .env file: %v", err) + } + + // Create tests + configs := []Config{ + {Api_key: os.Getenv("CAPTCHAAI_KEY"), Sitekey: "6LcR_okUAAAAAPYrPe-HK_0RULO1aZM15ENyM-Mf", CaptchaURL: "https://antcpt.com/score_detector/", CaptchaType: V3Captcha, Action: "homepage", MinScore: 0.7}, + } + + // Run tests + for testNum, config := range configs { + t.Run(fmt.Sprintf("Test #%v", testNum+1), func(t *testing.T) { + h := CaptchaAi{&config} + answer, err := h.GetToken() + if err != nil { + t.Fatalf("Error getting token: %v", err) + } + fmt.Println(answer) + + }) + } + +} + +// Test getting a recap V3 token +// go test -v -run ^TestCaptchaAiHCap$ github.com/Matthew17-21/Captcha-Tools/captchatools-go +func TestCaptchaAiHCap(t *testing.T) { + // Load ENV + if err := godotenv.Load("../.env"); err != nil { + t.Fatalf("Failed to load .env file: %v", err) + } + + // Create tests + configs := []Config{ + {Api_key: os.Getenv("CAPTCHAAI_KEY"), Sitekey: "a5f74b19-9e45-40e0-b45d-47ff91b7a6c2", CaptchaURL: "https://accounts.hcaptcha.com/demo", CaptchaType: HCaptcha}, + } + + // Run tests + for testNum, config := range configs { + t.Run(fmt.Sprintf("Test #%v", testNum+1), func(t *testing.T) { + h := CaptchaAi{&config} + answer, err := h.GetToken() + if err != nil { + t.Fatalf("Error getting token: %v", err) + } + fmt.Println(answer) + + }) + } + +} + +// Test getting a Image captcha +// go test -v -run ^TestCaptchaAiNormalCap$ github.com/Matthew17-21/Captcha-Tools/captchatools-go +func TestCaptchaAiNormalCap(t *testing.T) { + // Load ENV + if err := godotenv.Load("../.env"); err != nil { + t.Fatalf("Failed to load .env file: %v", err) + } + + // Create tests + configs := []Config{ + {Api_key: os.Getenv("CAPTCHAAI_KEY"), CaptchaType: ImageCaptcha}, + } + + // Run tests + for testNum, config := range configs { + t.Run(fmt.Sprintf("Test #%v", testNum+1), func(t *testing.T) { + h := CaptchaAi{&config} + answer, err := h.GetToken(&AdditionalData{ + B64Img: "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", + }) + if err != nil { + t.Fatalf("Error getting token: %v", err) + } + fmt.Println(answer) + }) + } + +} + +// Test getting balance info +// go test -v -run ^TestCaptchaAiGetBalance$ github.com/Matthew17-21/Captcha-Tools/captchatools-go +func TestCaptchaAiGetBalance(t *testing.T) { + // Load ENV + if err := godotenv.Load("../.env"); err != nil { + t.Fatalf("Failed to load .env file: %v", err) + } + + // Create tests + configs := []Config{ + {Api_key: os.Getenv("CAPTCHAAI_KEY"), CaptchaType: ImageCaptcha}, + } + + // Run tests + for testNum, config := range configs { + t.Run(fmt.Sprintf("Test #%v", testNum+1), func(t *testing.T) { + h := CaptchaAi{&config} + balance, err := h.GetBalance() + if err != nil { + t.Fatalf("Error getting balance: %v", err) + } + fmt.Println(balance) + }) + } + +} diff --git a/captchatools-go/common.go b/captchatools-go/common.go index bd03585..0230e0e 100644 --- a/captchatools-go/common.go +++ b/captchatools-go/common.go @@ -6,6 +6,7 @@ const ( AnticaptchaSite // The int 2 will represent Anticaptcha TwoCaptchaSite // The int 3 will represent 2captcha CapsolverSite + CaptchaAiSite ) const ( diff --git a/captchatools-go/errors.go b/captchatools-go/errors.go index b8cf441..c8366e7 100644 --- a/captchatools-go/errors.go +++ b/captchatools-go/errors.go @@ -28,6 +28,7 @@ var ( ErrMissingValues = errors.New("some of the required values for successive user emulation are missing") ErrAddionalDataMissing = errors.New("additional data is missing. Refer to guide") ErrProxyEmpty = errors.New("proxy is blank") + ErrNotSupported = errors.New("captcha type not supported") ) // errCodeToError converts an error ID returned from the site and diff --git a/captchatools-go/harvester.go b/captchatools-go/harvester.go index ea6f9af..76a96e7 100644 --- a/captchatools-go/harvester.go +++ b/captchatools-go/harvester.go @@ -76,6 +76,8 @@ func NewHarvester(solving_site site, config *Config) (Harvester, error) { h = &Twocaptcha{config: config} case CapsolverSite: h = &Capsolver{config} + case CaptchaAiSite: + h = &CaptchaAi{config} default: return nil, ErrNoHarvester }