From 7ce0ec320c8bb22edda2cf3f6605d18f14afe010 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Fri, 14 Feb 2025 09:59:21 +0100 Subject: [PATCH 01/11] Added Imperva task --- .../IntegrationTests.cs | 46 +++++++++++++++++++ .../ObjectGen.cs | 31 +++++++++++++ .../Sut.cs | 3 ++ .../CapMonsterCloud.Client.csproj | 4 +- ...CapMonsterCloudClient_GetResultTimeouts.cs | 11 ++++- .../Requests/DataDomeCustomTaskRequest.cs | 23 +++++++++- .../Requests/DataDomeCustomTaskRequestBase.cs | 30 ------------ .../Requests/ImpervaCustomTaskRequest.cs | 28 +++++++++++ .../Requests/TenDiCustomTaskRequest.cs | 19 ++++++-- .../Requests/TenDiCustomTaskRequestBase.cs | 22 --------- 10 files changed, 157 insertions(+), 60 deletions(-) delete mode 100644 CapMonsterCloud.Client/Requests/DataDomeCustomTaskRequestBase.cs create mode 100644 CapMonsterCloud.Client/Requests/ImpervaCustomTaskRequest.cs delete mode 100644 CapMonsterCloud.Client/Requests/TenDiCustomTaskRequestBase.cs diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index d6f6fbb..adb7c57 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -465,6 +465,52 @@ public async Task DataDome_ShouldSolve() actual.Should().BeEquivalentTo(expectedResult); } + [Test] + public async Task Imperva_ShouldSolve() + { + var clientKey = Gen.RandomString(); + var taskId = Gen.RandomInt(); + + var captchaRequest = ObjectGen.CustomTask.CreateImpervaTask(); + var expectedResult = ObjectGen.CustomTask.CreateImpervaSolution(); + + var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> + { + ( + Type: RequestType.CreateTask, + ExpectedRequest: JsonConvert.SerializeObject(new + { clientKey = clientKey, task = captchaRequest, softId = 53 }) + ), + ( + Type: RequestType.GetTaskResult, + ExpectedRequest: JsonConvert.SerializeObject(new { clientKey = clientKey, taskId = taskId }) + ), + }; + + var captchaResults = new List + { + new { taskId = taskId, errorId = 0, errorCode = (string)null! }, + new + { + status = "ready", + solution = new + { + domains = expectedResult.Solution.Domains + }, + errorId = 0, + errorCode = (string)null! + } + }; + + var sut = new Sut(clientKey); + sut.SetupHttpServer(captchaResults); + + var actual = await sut.SolveAsync(captchaRequest); + + sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); + actual.Should().BeEquivalentTo(expectedResult); + } + [Test] public async Task RecaptchaComplexImageTask_ShouldSolve() { diff --git a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs index 2873173..a19526b 100644 --- a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs @@ -409,6 +409,37 @@ public static CaptchaResult CreateBasiliskSolution() } }; } + + public static ImpervaCustomTaskRequest CreateImpervaTask() + { + return new ImpervaCustomTaskRequest(Gen.RandomString(), Gen.RandomString(), Gen.RandomString()) + { + WebsiteUrl = Gen.RandomUri().ToString(), + UserAgent = Gen.UserAgent(), + Proxy = new ProxyContainer(Gen.RandomString(), Gen.RandomInt(0, 65535), Gen.RandomEnum(), Gen.RandomString(), Gen.RandomString()) + }; + } + + public static CaptchaResult CreateImpervaSolution() + { + return new CaptchaResult + { + Error = null, + Solution = new CustomTaskResponse + { + Domains = new Dictionary + { + { + Gen.RandomString(), + new CustomTaskResponse.DomainInfo() + { + Cookies = new Dictionary { { Gen.RandomString(), Gen.RandomString() } }, + } + } + } + } + }; + } } public static class AmazonWaf diff --git a/CapMonsterCloud.Client.IntegrationTests/Sut.cs b/CapMonsterCloud.Client.IntegrationTests/Sut.cs index 22b48c1..0c8296f 100644 --- a/CapMonsterCloud.Client.IntegrationTests/Sut.cs +++ b/CapMonsterCloud.Client.IntegrationTests/Sut.cs @@ -92,6 +92,9 @@ public async Task> SolveAsync( public async Task> SolveAsync( BinanceTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task> SolveAsync( + ImpervaCustomTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task GetBalanceAsync() { return await _cloudClient.GetBalanceAsync(); diff --git a/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj b/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj index 8662f22..627b839 100644 --- a/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj +++ b/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj @@ -15,8 +15,8 @@ README.md logo.png https://github.com/ZennoLab/capmonstercloud-client-dotnet - 2.0.0 - Changed way of using proxy + 2.1.0 + Added Imperva CustomTask diff --git a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs index 307ffda..a65c449 100644 --- a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs +++ b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs @@ -172,7 +172,16 @@ private static GetResultTimeouts GetTimeouts(Type type) RequestsInterval = TimeSpan.FromSeconds(1), Timeout = TimeSpan.FromSeconds(20) } - } + }, + { + typeof(ImpervaCustomTaskRequest), + new GetResultTimeouts + { + FirstRequestDelay = TimeSpan.FromSeconds(1), + RequestsInterval = TimeSpan.FromSeconds(3), + Timeout = TimeSpan.FromSeconds(180) + } + }, }; } } diff --git a/CapMonsterCloud.Client/Requests/DataDomeCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/DataDomeCustomTaskRequest.cs index 32301d3..584be88 100644 --- a/CapMonsterCloud.Client/Requests/DataDomeCustomTaskRequest.cs +++ b/CapMonsterCloud.Client/Requests/DataDomeCustomTaskRequest.cs @@ -3,9 +3,28 @@ /// /// DataDome CustomTask recognition request /// - public sealed class DataDomeCustomTaskRequest : DataDomeCustomTaskRequestBase + public sealed class DataDomeCustomTaskRequest : CustomTaskRequestBase { /// - public DataDomeCustomTaskRequest(string datadomeCookie, string captchaUrl = null, string htmlPageBase64 = null) : base(datadomeCookie, captchaUrl, htmlPageBase64) { } + public override string Class => "DataDome"; + + /// + /// + /// These values will be set to Metadata property. + /// + /// - captchaUrl: "captchaUrl": "..." + /// Field is required if metadata.htmlPageBase64 is not filled. + /// You can take the link from the page with the captcha. + /// Often it looks like https://geo.captcha-delivery.com/captcha/?initialCid=... + /// + /// - htmlPageBase64: "htmlPageBase64": "PGh0bWw+PGhlYWQ+PHRpdGxlPmJs...N0E5QTA1" + /// Field is required if 'captchaUrl' is not filled. + /// A base64 encoded html page that comes with a 403 code and a Set-Cookie: datadome="..." header in response to a get request to the target site. + /// + /// - datadomeCookie: "datadomeCookie": "datadome=6BvxqELMoorFNoo7GT1...JyfP_mhz" + /// Field is required. Your cookies from datadome. You can get it on the page using "document.cookie" or in the Set-Cookie request header: "datadome=..." + /// + /// + public DataDomeCustomTaskRequest(string datadomeCookie, string captchaUrl, string htmlPageBase64) => Metadata = new { datadomeCookie, captchaUrl, htmlPageBase64 }; } } diff --git a/CapMonsterCloud.Client/Requests/DataDomeCustomTaskRequestBase.cs b/CapMonsterCloud.Client/Requests/DataDomeCustomTaskRequestBase.cs deleted file mode 100644 index a79939c..0000000 --- a/CapMonsterCloud.Client/Requests/DataDomeCustomTaskRequestBase.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Zennolab.CapMonsterCloud.Requests -{ - /// - /// DataDome CustomTask recognition request base - /// - public abstract class DataDomeCustomTaskRequestBase : CustomTaskRequestBase - { - /// - public override string Class => "DataDome"; - - /// - /// - /// These values will be set to Metadata property. - /// - /// - captchaUrl: "captchaUrl": "..." - /// Field is required if metadata.htmlPageBase64 is not filled. - /// You can take the link from the page with the captcha. - /// Often it looks like https://geo.captcha-delivery.com/captcha/?initialCid=... - /// - /// - htmlPageBase64: "htmlPageBase64": "PGh0bWw+PGhlYWQ+PHRpdGxlPmJs...N0E5QTA1" - /// Field is required if 'captchaUrl' is not filled. - /// A base64 encoded html page that comes with a 403 code and a Set-Cookie: datadome="..." header in response to a get request to the target site. - /// - /// - datadomeCookie: "datadomeCookie": "datadome=6BvxqELMoorFNoo7GT1...JyfP_mhz" - /// Field is required. Your cookies from datadome. You can get it on the page using "document.cookie" or in the Set-Cookie request header: "datadome=..." - /// - /// - protected DataDomeCustomTaskRequestBase(string datadomeCookie, string captchaUrl, string htmlPageBase64) => Metadata = new { datadomeCookie, captchaUrl, htmlPageBase64 }; - } -} diff --git a/CapMonsterCloud.Client/Requests/ImpervaCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/ImpervaCustomTaskRequest.cs new file mode 100644 index 0000000..22309e4 --- /dev/null +++ b/CapMonsterCloud.Client/Requests/ImpervaCustomTaskRequest.cs @@ -0,0 +1,28 @@ +namespace Zennolab.CapMonsterCloud.Requests +{ + /// + /// Imperva CustomTask recognition request + /// + public class ImpervaCustomTaskRequest : CustomTaskRequestBase + { + /// + public override string Class => "Imperva"; + + /// + /// + /// These values will be set to Metadata property. + /// + /// - incapsulaScriptBase64: "incapsulaScriptBase64": "..." + /// the base64-encoded content of the Incapsula JavaScript script. To obtain this value, you need to go to the script page. + /// The script content loaded by this src must be encoded in base64 format. This will be the value for the parameter incapsulaScriptBase64. + /// + /// - incapsulaSessionCookie: "incapsulaSessionCookie": "l/LsGnrvyB9lNhXI8borDKa2IGcAAAAAX0qAEHheCWuNDquzwb44cw=" + /// Your cookies from Incapsula. You can obtain them on the page using "document.cookie" or in the request header Set-Cookie: "incap_sess_*=..." + /// + /// - reese84UrlEndpoint: "reese84UrlEndpoint": "Built-with-the-For-hopence-Hurleysurfecting-the-" + /// The name of the endpoint where the reese84 fingerprint is sent can be found among the requests and ends with ?d=site.com + /// + /// + public ImpervaCustomTaskRequest(string incapsulaScriptBase64, string incapsulaSessionCookie, string reese84UrlEndpoint) => Metadata = new { incapsulaScriptBase64, incapsulaSessionCookie, reese84UrlEndpoint }; + } +} diff --git a/CapMonsterCloud.Client/Requests/TenDiCustomTaskRequest.cs b/CapMonsterCloud.Client/Requests/TenDiCustomTaskRequest.cs index 6d5369d..ef9efb5 100644 --- a/CapMonsterCloud.Client/Requests/TenDiCustomTaskRequest.cs +++ b/CapMonsterCloud.Client/Requests/TenDiCustomTaskRequest.cs @@ -1,9 +1,22 @@ -namespace Zennolab.CapMonsterCloud.Requests +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; + +namespace Zennolab.CapMonsterCloud.Requests { /// /// TenDi CustomTask recognition request /// - public sealed class TenDiCustomTaskRequest : TenDiCustomTaskRequestBase - { + public sealed class TenDiCustomTaskRequest : CustomTaskRequestBase + { + /// + public override string Class => "TenDI"; + + /// + /// captchaAppId. For example "websiteKey": "189123456" - is a unique parameter for your site. You can take it from an html page with a captcha or from traffic. + /// + /// 189123456 + [JsonProperty("websiteKey", Required = Required.Always)] + [StringLength(int.MaxValue, MinimumLength = 1)] + public string WebsiteKey { get; set; } } } diff --git a/CapMonsterCloud.Client/Requests/TenDiCustomTaskRequestBase.cs b/CapMonsterCloud.Client/Requests/TenDiCustomTaskRequestBase.cs deleted file mode 100644 index d1baef1..0000000 --- a/CapMonsterCloud.Client/Requests/TenDiCustomTaskRequestBase.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; -using System.ComponentModel.DataAnnotations; - -namespace Zennolab.CapMonsterCloud.Requests -{ - /// - /// TenDi CustomTask recognition request base - /// - public abstract class TenDiCustomTaskRequestBase : CustomTaskRequestBase - { - /// - public override string Class => "TenDI"; - - /// - /// captchaAppId. For example "websiteKey": "189123456" - is a unique parameter for your site. You can take it from an html page with a captcha or from traffic (see description below). - /// - /// 189123456 - [JsonProperty("websiteKey", Required = Required.Always)] - [StringLength(int.MaxValue, MinimumLength = 1)] - public string WebsiteKey { get; set; } - } -} From 31bf05cdda3d5c39a0455334b47ac5bb17f48e69 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Mon, 17 Feb 2025 10:12:26 +0100 Subject: [PATCH 02/11] Added RecognitionComplexImageTask --- .../Gen.cs | 9 ++ .../IntegrationTests.cs | 100 +++++++++++++++++- .../ObjectGen.cs | 43 +++++++- .../Sut.cs | 3 + ...CapMonsterCloudClient_GetResultTimeouts.cs | 3 +- .../Json/RecognitionAnswerJsonConverter.cs | 97 +++++++++++++++++ .../Requests/ComplexImageTaskBase.cs | 2 +- .../Requests/FuncaptchaComplexImageTask.cs | 3 +- .../Requests/HCaptchaComplexImageTask.cs | 3 +- .../Requests/RecaptchaComplexImageTask.cs | 3 +- .../RecognitionComplexImageTaskRequest.cs | 47 ++++++++ .../DynamicGridComplexImageTaskResponse.cs | 23 ++++ .../Responses/RecognitionAnswer.cs | 18 ++++ 13 files changed, 344 insertions(+), 10 deletions(-) create mode 100644 CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs create mode 100644 CapMonsterCloud.Client/Requests/RecognitionComplexImageTaskRequest.cs create mode 100644 CapMonsterCloud.Client/Responses/DynamicGridComplexImageTaskResponse.cs create mode 100644 CapMonsterCloud.Client/Responses/RecognitionAnswer.cs diff --git a/CapMonsterCloud.Client.IntegrationTests/Gen.cs b/CapMonsterCloud.Client.IntegrationTests/Gen.cs index 8c40a38..50b6c98 100644 --- a/CapMonsterCloud.Client.IntegrationTests/Gen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/Gen.cs @@ -96,6 +96,15 @@ public static List ListOfValues(Func createFunc, int count) return Enumerable.Repeat(0, count).Select(x => createFunc()).ToList(); } + public static T[] ArrayOfValues(Func createFunc) => + ArrayOfValues(createFunc, 5); + + public static T[] ArrayOfValues(Func createFunc, int count) + { + if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count)); + return Enumerable.Repeat(0, count).Select(x => createFunc()).ToArray(); + } + public static bool RandomBool() => Rnd.NextDouble() >= 0.5; public static string RandomGuid() diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index d6f6fbb..f94ce00 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -472,7 +472,7 @@ public async Task RecaptchaComplexImageTask_ShouldSolve() var taskId = Gen.RandomInt(); var captchaRequest = ObjectGen.ComplexImageTask.CreateRecaptchaComplexImageTask(); - var expectedResult = ObjectGen.ComplexImageTask.CreateSolution(); + var expectedResult = ObjectGen.ComplexImageTask.CreateGridComplexImageTaskSolution(); var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> { @@ -518,7 +518,7 @@ public async Task HCaptchaComplexImageTask_ShouldSolve() var taskId = Gen.RandomInt(); var captchaRequest = ObjectGen.ComplexImageTask.CreateHCaptchaComplexImageTask(); - var expectedResult = ObjectGen.ComplexImageTask.CreateSolution(); + var expectedResult = ObjectGen.ComplexImageTask.CreateGridComplexImageTaskSolution(); var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> { @@ -564,7 +564,7 @@ public async Task FunCaptchaComplexImageTask_ShouldSolve() var taskId = Gen.RandomInt(); var captchaRequest = ObjectGen.ComplexImageTask.CreateFunCaptchaComplexImageTask(); - var expectedResult = ObjectGen.ComplexImageTask.CreateSolution(); + var expectedResult = ObjectGen.ComplexImageTask.CreateGridComplexImageTaskSolution(); var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> { @@ -602,7 +602,99 @@ public async Task FunCaptchaComplexImageTask_ShouldSolve() sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); actual.Should().BeEquivalentTo(expectedResult); } - + + [Test] + public async Task RecognitionComplexImageTaskWithBoolValues_ShouldSolve() + { + var clientKey = Gen.RandomString(); + var taskId = Gen.RandomInt(); + + var captchaRequest = ObjectGen.ComplexImageTask.CreateRecognitionComplexImageTask(); + var expectedResult = ObjectGen.ComplexImageTask.CreateDynamicGridComplexImageTaskSolutionWithBoolValues(); + + var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> + { + ( + Type: RequestType.CreateTask, + ExpectedRequest: JsonConvert.SerializeObject(new + { clientKey = clientKey, task = captchaRequest, softId = 53 }) + ), + ( + Type: RequestType.GetTaskResult, + ExpectedRequest: JsonConvert.SerializeObject(new { clientKey = clientKey, taskId = taskId }) + ), + }; + + var captchaResults = new List + { + new { taskId = taskId, errorId = 0, errorCode = (string)null! }, + new + { + status = "ready", + solution = new + { + answer = expectedResult.Solution.Answer + }, + errorId = 0, + errorCode = (string)null! + } + }; + + var sut = new Sut(clientKey); + sut.SetupHttpServer(captchaResults); + + var actual = await sut.SolveAsync(captchaRequest); + + sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); + actual.Should().BeEquivalentTo(expectedResult); + } + + [Test] + public async Task RecognitionComplexImageTaskWithDecimalValues_ShouldSolve() + { + var clientKey = Gen.RandomString(); + var taskId = Gen.RandomInt(); + + var captchaRequest = ObjectGen.ComplexImageTask.CreateRecognitionComplexImageTask(); + var expectedResult = ObjectGen.ComplexImageTask.CreateDynamicGridComplexImageTaskSolutionWithDecimalValues(); + + var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> + { + ( + Type: RequestType.CreateTask, + ExpectedRequest: JsonConvert.SerializeObject(new + { clientKey = clientKey, task = captchaRequest, softId = 53 }) + ), + ( + Type: RequestType.GetTaskResult, + ExpectedRequest: JsonConvert.SerializeObject(new { clientKey = clientKey, taskId = taskId }) + ), + }; + + var captchaResults = new List + { + new { taskId = taskId, errorId = 0, errorCode = (string)null! }, + new + { + status = "ready", + solution = new + { + answer = expectedResult.Solution.Answer + }, + errorId = 0, + errorCode = (string)null! + } + }; + + var sut = new Sut(clientKey); + sut.SetupHttpServer(captchaResults); + + var actual = await sut.SolveAsync(captchaRequest); + + sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); + actual.Should().BeEquivalentTo(expectedResult); + } + [Test] public async Task RecaptchaV2_IncorrectWebsiteUrl_ShouldThrowValidationException() { diff --git a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs index 2873173..92779f9 100644 --- a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs @@ -311,7 +311,24 @@ public static FunCaptchaComplexImageTaskRequest CreateFunCaptchaComplexImageTask }; } - public static CaptchaResult CreateSolution() + + public static RecognitionComplexImageTaskRequest CreateRecognitionComplexImageTask() + { + return new RecognitionComplexImageTaskRequest + { + WebsiteUrl = Gen.RandomUri().ToString(), + Metadata = new RecognitionComplexImageTaskRequest.RecognitionMetadata + { + Task = Gen.RandomString(), + TaskArgument = Gen.RandomString() + }, + ImageUrls = Gen.ListOfValues(Gen.RandomUri().ToString), + ImagesBase64 = Gen.ListOfValues(Gen.RandomString), + UserAgent = Gen.UserAgent() + }; + } + + public static CaptchaResult CreateGridComplexImageTaskSolution() { return new CaptchaResult { @@ -322,6 +339,30 @@ public static CaptchaResult CreateSolution() } }; } + + public static CaptchaResult CreateDynamicGridComplexImageTaskSolutionWithBoolValues() + { + return new CaptchaResult + { + Error = null, + Solution = new DynamicGridComplexImageTaskResponse + { + Answer = new RecognitionAnswer{ BoolValues = Gen.ArrayOfValues(Gen.RandomBool) } + } + }; + } + + public static CaptchaResult CreateDynamicGridComplexImageTaskSolutionWithDecimalValues() + { + return new CaptchaResult + { + Error = null, + Solution = new DynamicGridComplexImageTaskResponse + { + Answer = new RecognitionAnswer { DecimalValues = Gen.ArrayOfValues(Gen.RandomDecimal) } + } + }; + } } public static class CustomTask diff --git a/CapMonsterCloud.Client.IntegrationTests/Sut.cs b/CapMonsterCloud.Client.IntegrationTests/Sut.cs index 22b48c1..92584ea 100644 --- a/CapMonsterCloud.Client.IntegrationTests/Sut.cs +++ b/CapMonsterCloud.Client.IntegrationTests/Sut.cs @@ -92,6 +92,9 @@ public async Task> SolveAsync( public async Task> SolveAsync( BinanceTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task> SolveAsync( + RecognitionComplexImageTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task GetBalanceAsync() { return await _cloudClient.GetBalanceAsync(); diff --git a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs index 307ffda..3748a0b 100644 --- a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs +++ b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Zennolab.CapMonsterCloud.Requests; +using Zennolab.CapMonsterCloud.Responses; namespace Zennolab.CapMonsterCloud { @@ -78,7 +79,7 @@ private static GetResultTimeouts GetTimeouts(Type type) } }, { - typeof(ComplexImageTaskRequestBase), + typeof(ComplexImageTaskRequestBase<>), new GetResultTimeouts { FirstRequestDelay = TimeSpan.FromMilliseconds(350), diff --git a/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs new file mode 100644 index 0000000..228af4f --- /dev/null +++ b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Zennolab.CapMonsterCloud.Responses; + +namespace Zennolab.CapMonsterCloud.Json +{ + public class RecognitionAnswerJsonConverter : JsonConverter + { + public override RecognitionAnswer ReadJson(JsonReader reader, Type objectType, RecognitionAnswer existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JToken token = JToken.Load(reader); + + JArray array; + if (token.Type == JTokenType.Array) + { + array = (JArray)token; + } + else if (token.Type == JTokenType.Object) + { + JObject obj = (JObject)token; + JToken innerToken = obj["DecimalValues"]; + + if (innerToken == null) + innerToken = obj["BoolValues"]; + + if (innerToken == null) + throw new JsonSerializationException("Expected an array in the object, but could not find an 'answer' (or 'data') property."); + + if (innerToken.Type != JTokenType.Array) + throw new JsonSerializationException("Expected the 'answer' (or 'data') property to be an array."); + + array = (JArray)innerToken; + } + else + { + throw new JsonSerializationException("Unexpected token type. Expected an array or an object containing an array."); + } + + return ProcessArray(array); + } + + private RecognitionAnswer ProcessArray(JArray array) + { + if (array.Count == 0) + return new RecognitionAnswer { DecimalValues = new decimal[0] }; + + bool isBoolArray = array[0].Type == JTokenType.Boolean; + + var boolList = new List(); + var decimalList = new List(); + + foreach (var item in array) + { + if (isBoolArray) + { + if (item.Type != JTokenType.Boolean) + throw new JsonSerializationException("Inconsistent array types: Expected all booleans."); + boolList.Add(item.Value()); + } + else + { + if (item.Type != JTokenType.Float && item.Type != JTokenType.Integer) + throw new JsonSerializationException("Inconsistent array types: Expected all numbers."); + decimalList.Add(item.Value()); + } + } + + return isBoolArray + ? new RecognitionAnswer { BoolValues = boolList.ToArray() } + : new RecognitionAnswer { DecimalValues = decimalList.ToArray() }; + } + + public override void WriteJson(JsonWriter writer, RecognitionAnswer value, JsonSerializer serializer) + { + writer.WriteStartArray(); + + if (value.IsBool) + { + foreach (var item in value.BoolValues) + { + writer.WriteValue(item); + } + } + else if (value.IsDecimal) + { + foreach (var item in value.DecimalValues) + { + writer.WriteValue(item); + } + } + + writer.WriteEndArray(); + } + } +} \ No newline at end of file diff --git a/CapMonsterCloud.Client/Requests/ComplexImageTaskBase.cs b/CapMonsterCloud.Client/Requests/ComplexImageTaskBase.cs index 2bd9719..56f6f18 100644 --- a/CapMonsterCloud.Client/Requests/ComplexImageTaskBase.cs +++ b/CapMonsterCloud.Client/Requests/ComplexImageTaskBase.cs @@ -8,7 +8,7 @@ namespace Zennolab.CapMonsterCloud.Requests /// /// ComplexImageTask recognition request /// - public abstract class ComplexImageTaskRequestBase : CaptchaRequestBase + public abstract class ComplexImageTaskRequestBase : CaptchaRequestBase where TResponse : CaptchaResponseBase { /// /// Recognition task type diff --git a/CapMonsterCloud.Client/Requests/FuncaptchaComplexImageTask.cs b/CapMonsterCloud.Client/Requests/FuncaptchaComplexImageTask.cs index 2da4322..8e4a6fe 100644 --- a/CapMonsterCloud.Client/Requests/FuncaptchaComplexImageTask.cs +++ b/CapMonsterCloud.Client/Requests/FuncaptchaComplexImageTask.cs @@ -1,12 +1,13 @@ using Newtonsoft.Json; using System.ComponentModel.DataAnnotations; +using Zennolab.CapMonsterCloud.Responses; namespace Zennolab.CapMonsterCloud.Requests { /// /// ComplexImageTask recognition request for funcaptcha images /// - public sealed class FunCaptchaComplexImageTaskRequest : ComplexImageTaskRequestBase + public sealed class FunCaptchaComplexImageTaskRequest : ComplexImageTaskRequestBase { /// /// Metadata for recognition diff --git a/CapMonsterCloud.Client/Requests/HCaptchaComplexImageTask.cs b/CapMonsterCloud.Client/Requests/HCaptchaComplexImageTask.cs index 4e6d8f7..ffbe08f 100644 --- a/CapMonsterCloud.Client/Requests/HCaptchaComplexImageTask.cs +++ b/CapMonsterCloud.Client/Requests/HCaptchaComplexImageTask.cs @@ -1,13 +1,14 @@ using Newtonsoft.Json; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Zennolab.CapMonsterCloud.Responses; namespace Zennolab.CapMonsterCloud.Requests { /// /// ComplexImageTask recognition request for hcaptcha images /// - public sealed class HCaptchaComplexImageTaskRequest : ComplexImageTaskRequestBase + public sealed class HCaptchaComplexImageTaskRequest : ComplexImageTaskRequestBase { /// /// Metadata for recognition diff --git a/CapMonsterCloud.Client/Requests/RecaptchaComplexImageTask.cs b/CapMonsterCloud.Client/Requests/RecaptchaComplexImageTask.cs index 257d004..3b633b3 100644 --- a/CapMonsterCloud.Client/Requests/RecaptchaComplexImageTask.cs +++ b/CapMonsterCloud.Client/Requests/RecaptchaComplexImageTask.cs @@ -1,13 +1,14 @@ using System; using System.ComponentModel.DataAnnotations; using Newtonsoft.Json; +using Zennolab.CapMonsterCloud.Responses; namespace Zennolab.CapMonsterCloud.Requests { /// /// ComplexImageTask recognition request for recaptcha images /// - public sealed class RecaptchaComplexImageTaskRequest : ComplexImageTaskRequestBase + public sealed class RecaptchaComplexImageTaskRequest : ComplexImageTaskRequestBase { /// /// Metadata for recognition diff --git a/CapMonsterCloud.Client/Requests/RecognitionComplexImageTaskRequest.cs b/CapMonsterCloud.Client/Requests/RecognitionComplexImageTaskRequest.cs new file mode 100644 index 0000000..ffdd83f --- /dev/null +++ b/CapMonsterCloud.Client/Requests/RecognitionComplexImageTaskRequest.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; +using Zennolab.CapMonsterCloud.Responses; + +namespace Zennolab.CapMonsterCloud.Requests +{ + /// + /// ComplexImageTask recognition request for Recognition images + /// + public sealed class RecognitionComplexImageTaskRequest : ComplexImageTaskRequestBase + { + /// + public override string Class => "recognition"; + + /// + /// Metadata for recognition + /// + public sealed class RecognitionMetadata + { + /// + /// Task definition. Required. + /// + /// + /// oocl_rotate_new + /// + [Required] + [JsonProperty("Task")] + public string Task { get; set; } + + /// + /// Additional task argument definition. Optional. + /// + /// + /// 546 + /// + [JsonProperty("TaskArgument")] + public string TaskArgument { get; set; } + } + + /// + /// Metadata for recognition + /// + [JsonProperty("metadata")] + [Required] + public RecognitionMetadata Metadata { get; set; } + } +} diff --git a/CapMonsterCloud.Client/Responses/DynamicGridComplexImageTaskResponse.cs b/CapMonsterCloud.Client/Responses/DynamicGridComplexImageTaskResponse.cs new file mode 100644 index 0000000..6d6c94d --- /dev/null +++ b/CapMonsterCloud.Client/Responses/DynamicGridComplexImageTaskResponse.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using Zennolab.CapMonsterCloud.Json; + +namespace Zennolab.CapMonsterCloud.Responses +{ + /// + /// Response for Recognition grid-like tasks + /// + public class DynamicGridComplexImageTaskResponse : CaptchaResponseBase + { + /// + /// Bool or decimal collection with answers + /// + /// + /// [false,true,false,true,false,false,true,false,false] + /// [4,4,4,4,4,3,1,2,2] + /// [130.90909] + /// + [JsonProperty("answer")] + [JsonConverter(typeof(RecognitionAnswerJsonConverter))] + public RecognitionAnswer Answer { get; set; } + } +} diff --git a/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs b/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs new file mode 100644 index 0000000..24717fd --- /dev/null +++ b/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs @@ -0,0 +1,18 @@ +namespace Zennolab.CapMonsterCloud.Responses +{ + public class RecognitionAnswer + { + public decimal[] DecimalValues { get; set; } + + public bool[] BoolValues { get; set; } + + public bool IsDecimal => DecimalValues != null; + + public bool IsBool => BoolValues != null; + + public override string ToString() + { + return IsDecimal ? string.Join(", ", DecimalValues) : string.Join(", ", BoolValues); + } + } +} From 85389be27bb6ef28dc99fe856c208fc6e86cc62e Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Mon, 17 Feb 2025 10:14:19 +0100 Subject: [PATCH 03/11] Up version --- CapMonsterCloud.Client/CapMonsterCloud.Client.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj b/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj index 8662f22..d34a81a 100644 --- a/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj +++ b/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj @@ -15,8 +15,8 @@ README.md logo.png https://github.com/ZennoLab/capmonstercloud-client-dotnet - 2.0.0 - Changed way of using proxy + 2.2.0 + Added RecognitionComplexImageTask From cf87babbcb8163838a81e292d95ff31bef9750c4 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Mon, 17 Feb 2025 12:52:16 +0100 Subject: [PATCH 04/11] Added xml comments --- .../Json/RecognitionAnswerJsonConverter.cs | 2 +- .../Responses/CaptchaResponseBase.cs | 3 +++ .../Responses/RecognitionAnswer.cs | 16 +++++++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs index 228af4f..b1024a0 100644 --- a/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs +++ b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs @@ -6,7 +6,7 @@ namespace Zennolab.CapMonsterCloud.Json { - public class RecognitionAnswerJsonConverter : JsonConverter + internal class RecognitionAnswerJsonConverter : JsonConverter { public override RecognitionAnswer ReadJson(JsonReader reader, Type objectType, RecognitionAnswer existingValue, bool hasExistingValue, JsonSerializer serializer) { diff --git a/CapMonsterCloud.Client/Responses/CaptchaResponseBase.cs b/CapMonsterCloud.Client/Responses/CaptchaResponseBase.cs index 72d472c..2b2be64 100644 --- a/CapMonsterCloud.Client/Responses/CaptchaResponseBase.cs +++ b/CapMonsterCloud.Client/Responses/CaptchaResponseBase.cs @@ -1,5 +1,8 @@ namespace Zennolab.CapMonsterCloud.Responses { + /// + /// CaptchaResponse base class + /// public abstract class CaptchaResponseBase { } diff --git a/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs b/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs index 24717fd..821e007 100644 --- a/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs +++ b/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs @@ -1,18 +1,24 @@ namespace Zennolab.CapMonsterCloud.Responses { + /// + /// Recognition captcha answer + /// public class RecognitionAnswer { + /// + /// Decimal answer + /// public decimal[] DecimalValues { get; set; } + /// + /// Bool answer + /// public bool[] BoolValues { get; set; } + /// public bool IsDecimal => DecimalValues != null; + /// public bool IsBool => BoolValues != null; - - public override string ToString() - { - return IsDecimal ? string.Join(", ", DecimalValues) : string.Join(", ", BoolValues); - } } } From 74d51d083c96e74ab871468fa61a84950789ea2d Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Mon, 17 Feb 2025 13:17:32 +0100 Subject: [PATCH 05/11] Fixed failed test --- .../Json/RecognitionAnswerJsonConverter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs index b1024a0..61fa332 100644 --- a/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs +++ b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs @@ -22,14 +22,14 @@ public override RecognitionAnswer ReadJson(JsonReader reader, Type objectType, R JObject obj = (JObject)token; JToken innerToken = obj["DecimalValues"]; - if (innerToken == null) + if (innerToken == null || !innerToken.HasValues) innerToken = obj["BoolValues"]; if (innerToken == null) - throw new JsonSerializationException("Expected an array in the object, but could not find an 'answer' (or 'data') property."); + throw new JsonSerializationException("Expected an array in the object, but could not find an 'DecimalValues' (or 'BoolValues') property."); if (innerToken.Type != JTokenType.Array) - throw new JsonSerializationException("Expected the 'answer' (or 'data') property to be an array."); + throw new JsonSerializationException("Expected the 'DecimalValues' (or 'BoolValues') property to be an array."); array = (JArray)innerToken; } From ad7e9ec93ba26a97db8029561c55e1e7677506a4 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Mon, 17 Feb 2025 15:30:51 +0100 Subject: [PATCH 06/11] Changed get result timeout --- .../CapMonsterCloudClient_GetResultTimeouts.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs index a65c449..932c50e 100644 --- a/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs +++ b/CapMonsterCloud.Client/CapMonsterCloudClient_GetResultTimeouts.cs @@ -178,8 +178,8 @@ private static GetResultTimeouts GetTimeouts(Type type) new GetResultTimeouts { FirstRequestDelay = TimeSpan.FromSeconds(1), - RequestsInterval = TimeSpan.FromSeconds(3), - Timeout = TimeSpan.FromSeconds(180) + RequestsInterval = TimeSpan.FromSeconds(1), + Timeout = TimeSpan.FromSeconds(15) } }, }; From dab80907d2ab5514f54128208a0b20b40095be2c Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Thu, 20 Feb 2025 18:10:18 +0100 Subject: [PATCH 07/11] Fixed converter --- .../IntegrationTests.cs | 68 +++++++++++--- .../ObjectGen.cs | 16 ++-- .../Sut.cs | 6 +- .../Json/RecognitionAnswerJsonConverter.cs | 89 +++++-------------- .../Requests/HCaptchaComplexImageTask.cs | 2 +- .../RecognitionComplexImageTaskRequest.cs | 2 +- ....cs => DynamicComplexImageTaskResponse.cs} | 2 +- .../Responses/RecognitionAnswer.cs | 8 +- 8 files changed, 98 insertions(+), 95 deletions(-) rename CapMonsterCloud.Client/Responses/{DynamicGridComplexImageTaskResponse.cs => DynamicComplexImageTaskResponse.cs} (89%) diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index f94ce00..808c461 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -512,13 +512,13 @@ public async Task RecaptchaComplexImageTask_ShouldSolve() } [Test] - public async Task HCaptchaComplexImageTask_ShouldSolve() + public async Task HCaptchaComplexImageTaskWithNumericAnswer_ShouldSolve() { var clientKey = Gen.RandomString(); var taskId = Gen.RandomInt(); var captchaRequest = ObjectGen.ComplexImageTask.CreateHCaptchaComplexImageTask(); - var expectedResult = ObjectGen.ComplexImageTask.CreateGridComplexImageTaskSolution(); + var expectedResult = ObjectGen.ComplexImageTask.CreateDynamicComplexImageTaskSolutionWithNumericAnswer(); var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> { @@ -541,7 +541,7 @@ public async Task HCaptchaComplexImageTask_ShouldSolve() status = "ready", solution = new { - answer = expectedResult.Solution.Answer + answer = expectedResult.Solution.Answer.NumericAnswer }, errorId = 0, errorCode = (string)null! @@ -555,8 +555,54 @@ public async Task HCaptchaComplexImageTask_ShouldSolve() sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); actual.Should().BeEquivalentTo(expectedResult); - } - + } + + [Test] + public async Task HCaptchaComplexImageTaskWithGridAnswer_ShouldSolve() + { + var clientKey = Gen.RandomString(); + var taskId = Gen.RandomInt(); + + var captchaRequest = ObjectGen.ComplexImageTask.CreateHCaptchaComplexImageTask(); + var expectedResult = ObjectGen.ComplexImageTask.CreateDynamicComplexImageTaskSolutionWithGridAnswer(); + + var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> + { + ( + Type: RequestType.CreateTask, + ExpectedRequest: JsonConvert.SerializeObject(new + { clientKey = clientKey, task = captchaRequest, softId = 53 }) + ), + ( + Type: RequestType.GetTaskResult, + ExpectedRequest: JsonConvert.SerializeObject(new { clientKey = clientKey, taskId = taskId }) + ), + }; + + var captchaResults = new List + { + new { taskId = taskId, errorId = 0, errorCode = (string)null! }, + new + { + status = "ready", + solution = new + { + answer = expectedResult.Solution.Answer.GridAnswer + }, + errorId = 0, + errorCode = (string)null! + } + }; + + var sut = new Sut(clientKey); + sut.SetupHttpServer(captchaResults); + + var actual = await sut.SolveAsync(captchaRequest); + + sut.GetActualRequests().Should().BeEquivalentTo(expectedRequests); + actual.Should().BeEquivalentTo(expectedResult); + } + [Test] public async Task FunCaptchaComplexImageTask_ShouldSolve() { @@ -604,13 +650,13 @@ public async Task FunCaptchaComplexImageTask_ShouldSolve() } [Test] - public async Task RecognitionComplexImageTaskWithBoolValues_ShouldSolve() + public async Task RecognitionComplexImageTaskWithGridAnswer_ShouldSolve() { var clientKey = Gen.RandomString(); var taskId = Gen.RandomInt(); var captchaRequest = ObjectGen.ComplexImageTask.CreateRecognitionComplexImageTask(); - var expectedResult = ObjectGen.ComplexImageTask.CreateDynamicGridComplexImageTaskSolutionWithBoolValues(); + var expectedResult = ObjectGen.ComplexImageTask.CreateDynamicComplexImageTaskSolutionWithGridAnswer(); var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> { @@ -633,7 +679,7 @@ public async Task RecognitionComplexImageTaskWithBoolValues_ShouldSolve() status = "ready", solution = new { - answer = expectedResult.Solution.Answer + answer = expectedResult.Solution.Answer.GridAnswer }, errorId = 0, errorCode = (string)null! @@ -650,13 +696,13 @@ public async Task RecognitionComplexImageTaskWithBoolValues_ShouldSolve() } [Test] - public async Task RecognitionComplexImageTaskWithDecimalValues_ShouldSolve() + public async Task RecognitionComplexImageTaskWithNumericAnswer_ShouldSolve() { var clientKey = Gen.RandomString(); var taskId = Gen.RandomInt(); var captchaRequest = ObjectGen.ComplexImageTask.CreateRecognitionComplexImageTask(); - var expectedResult = ObjectGen.ComplexImageTask.CreateDynamicGridComplexImageTaskSolutionWithDecimalValues(); + var expectedResult = ObjectGen.ComplexImageTask.CreateDynamicComplexImageTaskSolutionWithNumericAnswer(); var expectedRequests = new List<(RequestType Type, string ExpectedRequest)> { @@ -679,7 +725,7 @@ public async Task RecognitionComplexImageTaskWithDecimalValues_ShouldSolve() status = "ready", solution = new { - answer = expectedResult.Solution.Answer + answer = expectedResult.Solution.Answer.NumericAnswer }, errorId = 0, errorCode = (string)null! diff --git a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs index 92779f9..8443832 100644 --- a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs @@ -340,26 +340,26 @@ public static CaptchaResult CreateGridComplexImage }; } - public static CaptchaResult CreateDynamicGridComplexImageTaskSolutionWithBoolValues() + public static CaptchaResult CreateDynamicComplexImageTaskSolutionWithGridAnswer() { - return new CaptchaResult + return new CaptchaResult { Error = null, - Solution = new DynamicGridComplexImageTaskResponse + Solution = new DynamicComplexImageTaskResponse { - Answer = new RecognitionAnswer{ BoolValues = Gen.ArrayOfValues(Gen.RandomBool) } + Answer = new RecognitionAnswer{ GridAnswer = Gen.ArrayOfValues(Gen.RandomBool) } } }; } - public static CaptchaResult CreateDynamicGridComplexImageTaskSolutionWithDecimalValues() + public static CaptchaResult CreateDynamicComplexImageTaskSolutionWithNumericAnswer() { - return new CaptchaResult + return new CaptchaResult { Error = null, - Solution = new DynamicGridComplexImageTaskResponse + Solution = new DynamicComplexImageTaskResponse { - Answer = new RecognitionAnswer { DecimalValues = Gen.ArrayOfValues(Gen.RandomDecimal) } + Answer = new RecognitionAnswer { NumericAnswer = Gen.ArrayOfValues(Gen.RandomDecimal) } } }; } diff --git a/CapMonsterCloud.Client.IntegrationTests/Sut.cs b/CapMonsterCloud.Client.IntegrationTests/Sut.cs index 92584ea..c8fb92e 100644 --- a/CapMonsterCloud.Client.IntegrationTests/Sut.cs +++ b/CapMonsterCloud.Client.IntegrationTests/Sut.cs @@ -71,8 +71,8 @@ public async Task> SolveAsync ( public async Task> SolveAsync ( RecaptchaComplexImageTaskRequest request) => await _cloudClient.SolveAsync(request); - public async Task> SolveAsync ( - HCaptchaComplexImageTaskRequest request) => await _cloudClient.SolveAsync(request); + public async Task> SolveAsync ( + HCaptchaComplexImageTaskRequest request) => await _cloudClient.SolveAsync(request); public async Task> SolveAsync ( FunCaptchaComplexImageTaskRequest request) => await _cloudClient.SolveAsync(request); @@ -92,7 +92,7 @@ public async Task> SolveAsync( public async Task> SolveAsync( BinanceTaskRequest request) => await _cloudClient.SolveAsync(request); - public async Task> SolveAsync( + public async Task> SolveAsync( RecognitionComplexImageTaskRequest request) => await _cloudClient.SolveAsync(request); public async Task GetBalanceAsync() diff --git a/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs index 61fa332..c8b82c6 100644 --- a/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs +++ b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System; using Zennolab.CapMonsterCloud.Responses; namespace Zennolab.CapMonsterCloud.Json @@ -12,86 +11,44 @@ public override RecognitionAnswer ReadJson(JsonReader reader, Type objectType, R { JToken token = JToken.Load(reader); - JArray array; - if (token.Type == JTokenType.Array) - { - array = (JArray)token; - } - else if (token.Type == JTokenType.Object) - { - JObject obj = (JObject)token; - JToken innerToken = obj["DecimalValues"]; + if (token.Type != JTokenType.Array) + throw new JsonSerializationException("Expected an array for answer field"); - if (innerToken == null || !innerToken.HasValues) - innerToken = obj["BoolValues"]; + if (!token.HasValues) + throw new JsonSerializationException("Empty answer array"); - if (innerToken == null) - throw new JsonSerializationException("Expected an array in the object, but could not find an 'DecimalValues' (or 'BoolValues') property."); + RecognitionAnswer answer = new RecognitionAnswer(); - if (innerToken.Type != JTokenType.Array) - throw new JsonSerializationException("Expected the 'DecimalValues' (or 'BoolValues') property to be an array."); - - array = (JArray)innerToken; + if (token.First.Type == JTokenType.Boolean) + { + answer.GridAnswer = token.ToObject(); } - else + else if (token.First.Type == JTokenType.Integer || token.First.Type == JTokenType.Float) { - throw new JsonSerializationException("Unexpected token type. Expected an array or an object containing an array."); + answer.NumericAnswer = token.ToObject(); } - - return ProcessArray(array); - } - - private RecognitionAnswer ProcessArray(JArray array) - { - if (array.Count == 0) - return new RecognitionAnswer { DecimalValues = new decimal[0] }; - - bool isBoolArray = array[0].Type == JTokenType.Boolean; - - var boolList = new List(); - var decimalList = new List(); - - foreach (var item in array) + else { - if (isBoolArray) - { - if (item.Type != JTokenType.Boolean) - throw new JsonSerializationException("Inconsistent array types: Expected all booleans."); - boolList.Add(item.Value()); - } - else - { - if (item.Type != JTokenType.Float && item.Type != JTokenType.Integer) - throw new JsonSerializationException("Inconsistent array types: Expected all numbers."); - decimalList.Add(item.Value()); - } + throw new JsonSerializationException("Unexpected answer format"); } - return isBoolArray - ? new RecognitionAnswer { BoolValues = boolList.ToArray() } - : new RecognitionAnswer { DecimalValues = decimalList.ToArray() }; + return answer; } public override void WriteJson(JsonWriter writer, RecognitionAnswer value, JsonSerializer serializer) { - writer.WriteStartArray(); - - if (value.IsBool) + if (value.IsGrid) { - foreach (var item in value.BoolValues) - { - writer.WriteValue(item); - } + JToken.FromObject(value.GridAnswer).WriteTo(writer); } - else if (value.IsDecimal) + else if (value.IsNumeric) { - foreach (var item in value.DecimalValues) - { - writer.WriteValue(item); - } + JToken.FromObject(value.NumericAnswer).WriteTo(writer); + } + else + { + throw new JsonSerializationException("Invalid RecognitionAnswer state"); } - - writer.WriteEndArray(); } } } \ No newline at end of file diff --git a/CapMonsterCloud.Client/Requests/HCaptchaComplexImageTask.cs b/CapMonsterCloud.Client/Requests/HCaptchaComplexImageTask.cs index ffbe08f..e2f62eb 100644 --- a/CapMonsterCloud.Client/Requests/HCaptchaComplexImageTask.cs +++ b/CapMonsterCloud.Client/Requests/HCaptchaComplexImageTask.cs @@ -8,7 +8,7 @@ namespace Zennolab.CapMonsterCloud.Requests /// /// ComplexImageTask recognition request for hcaptcha images /// - public sealed class HCaptchaComplexImageTaskRequest : ComplexImageTaskRequestBase + public sealed class HCaptchaComplexImageTaskRequest : ComplexImageTaskRequestBase { /// /// Metadata for recognition diff --git a/CapMonsterCloud.Client/Requests/RecognitionComplexImageTaskRequest.cs b/CapMonsterCloud.Client/Requests/RecognitionComplexImageTaskRequest.cs index ffdd83f..679f946 100644 --- a/CapMonsterCloud.Client/Requests/RecognitionComplexImageTaskRequest.cs +++ b/CapMonsterCloud.Client/Requests/RecognitionComplexImageTaskRequest.cs @@ -7,7 +7,7 @@ namespace Zennolab.CapMonsterCloud.Requests /// /// ComplexImageTask recognition request for Recognition images /// - public sealed class RecognitionComplexImageTaskRequest : ComplexImageTaskRequestBase + public sealed class RecognitionComplexImageTaskRequest : ComplexImageTaskRequestBase { /// public override string Class => "recognition"; diff --git a/CapMonsterCloud.Client/Responses/DynamicGridComplexImageTaskResponse.cs b/CapMonsterCloud.Client/Responses/DynamicComplexImageTaskResponse.cs similarity index 89% rename from CapMonsterCloud.Client/Responses/DynamicGridComplexImageTaskResponse.cs rename to CapMonsterCloud.Client/Responses/DynamicComplexImageTaskResponse.cs index 6d6c94d..a24d91a 100644 --- a/CapMonsterCloud.Client/Responses/DynamicGridComplexImageTaskResponse.cs +++ b/CapMonsterCloud.Client/Responses/DynamicComplexImageTaskResponse.cs @@ -6,7 +6,7 @@ namespace Zennolab.CapMonsterCloud.Responses /// /// Response for Recognition grid-like tasks /// - public class DynamicGridComplexImageTaskResponse : CaptchaResponseBase + public class DynamicComplexImageTaskResponse : CaptchaResponseBase { /// /// Bool or decimal collection with answers diff --git a/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs b/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs index 821e007..5f31f31 100644 --- a/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs +++ b/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs @@ -8,17 +8,17 @@ public class RecognitionAnswer /// /// Decimal answer /// - public decimal[] DecimalValues { get; set; } + public decimal[] NumericAnswer { get; set; } /// /// Bool answer /// - public bool[] BoolValues { get; set; } + public bool[] GridAnswer { get; set; } /// - public bool IsDecimal => DecimalValues != null; + public bool IsNumeric => NumericAnswer != null; /// - public bool IsBool => BoolValues != null; + public bool IsGrid => NumericAnswer != null; } } From aa5e613428cd3b65ff50c5e8762cdc1302f7875a Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Mon, 24 Feb 2025 19:14:30 +0100 Subject: [PATCH 08/11] Fixed converter --- .../IntegrationTests.cs | 6 +- .../Json/RecognitionAnswerJsonConverter.cs | 78 +++++++++++++------ .../DynamicComplexImageTaskResponse.cs | 20 ++++- .../Responses/RecognitionAnswer.cs | 2 +- 4 files changed, 76 insertions(+), 30 deletions(-) diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index 808c461..4b214c7 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -679,7 +679,8 @@ public async Task RecognitionComplexImageTaskWithGridAnswer_ShouldSolve() status = "ready", solution = new { - answer = expectedResult.Solution.Answer.GridAnswer + answer = expectedResult.Solution.Answer.GridAnswer, + metadata = new { AnswerType = "Grid" } }, errorId = 0, errorCode = (string)null! @@ -725,7 +726,8 @@ public async Task RecognitionComplexImageTaskWithNumericAnswer_ShouldSolve() status = "ready", solution = new { - answer = expectedResult.Solution.Answer.NumericAnswer + answer = expectedResult.Solution.Answer.NumericAnswer, + metadata = new { AnswerType = "NumericArray" } }, errorId = 0, errorCode = (string)null! diff --git a/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs index c8b82c6..18eef12 100644 --- a/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs +++ b/CapMonsterCloud.Client/Json/RecognitionAnswerJsonConverter.cs @@ -5,50 +5,78 @@ namespace Zennolab.CapMonsterCloud.Json { - internal class RecognitionAnswerJsonConverter : JsonConverter + internal class RecognitionAnswerJsonConverter : JsonConverter { - public override RecognitionAnswer ReadJson(JsonReader reader, Type objectType, RecognitionAnswer existingValue, bool hasExistingValue, JsonSerializer serializer) + public override DynamicComplexImageTaskResponse ReadJson(JsonReader reader, Type objectType, DynamicComplexImageTaskResponse existingValue, bool hasExistingValue, JsonSerializer serializer) { - JToken token = JToken.Load(reader); + JObject obj = JObject.Load(reader); - if (token.Type != JTokenType.Array) - throw new JsonSerializationException("Expected an array for answer field"); + if (!obj.ContainsKey("metadata") || !obj.ContainsKey("answer")) + throw new JsonSerializationException("Missing 'metadata' or 'answer' field in response"); - if (!token.HasValues) - throw new JsonSerializationException("Empty answer array"); + string answerType = obj["metadata"]?["AnswerType"]?.ToString(); + if (string.IsNullOrEmpty(answerType)) + throw new JsonSerializationException("AnswerType is missing in metadata"); - RecognitionAnswer answer = new RecognitionAnswer(); - - if (token.First.Type == JTokenType.Boolean) - { - answer.GridAnswer = token.ToObject(); - } - else if (token.First.Type == JTokenType.Integer || token.First.Type == JTokenType.Float) + JToken answerToken = obj["answer"]; + DynamicComplexImageTaskResponse response = new DynamicComplexImageTaskResponse { - answer.NumericAnswer = token.ToObject(); - } - else + Metadata = new DynamicComplexImageTaskResponse.RecognitionMetadata + { + AnswerType = answerType + } + }; + + switch (answerType) { - throw new JsonSerializationException("Unexpected answer format"); + case "NumericArray": + response.Answer = new RecognitionAnswer + { + NumericAnswer = answerToken.ToObject() + }; + break; + case "Grid": + response.Answer = new RecognitionAnswer + { + GridAnswer = answerToken.ToObject() + }; + break; + default: + throw new JsonSerializationException($"Unknown AnswerType: {answerType}"); } - return answer; + return response; } - public override void WriteJson(JsonWriter writer, RecognitionAnswer value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, DynamicComplexImageTaskResponse value, JsonSerializer serializer) { - if (value.IsGrid) + if (value == null) { - JToken.FromObject(value.GridAnswer).WriteTo(writer); + writer.WriteNull(); + return; } - else if (value.IsNumeric) + + writer.WriteStartObject(); + + writer.WritePropertyName("answer"); + + if (value.Metadata?.AnswerType == "Grid" && value.Answer?.GridAnswer != null) + { + serializer.Serialize(writer, value.Answer.GridAnswer); + } + else if (value.Metadata?.AnswerType == "NumericArray" && value.Answer?.NumericAnswer != null) { - JToken.FromObject(value.NumericAnswer).WriteTo(writer); + serializer.Serialize(writer, value.Answer.NumericAnswer); } else { - throw new JsonSerializationException("Invalid RecognitionAnswer state"); + throw new JsonSerializationException("Invalid or missing answer data for the specified AnswerType."); } + + writer.WritePropertyName("metadata"); + serializer.Serialize(writer, value.Metadata); + + writer.WriteEndObject(); } } } \ No newline at end of file diff --git a/CapMonsterCloud.Client/Responses/DynamicComplexImageTaskResponse.cs b/CapMonsterCloud.Client/Responses/DynamicComplexImageTaskResponse.cs index a24d91a..a448d0f 100644 --- a/CapMonsterCloud.Client/Responses/DynamicComplexImageTaskResponse.cs +++ b/CapMonsterCloud.Client/Responses/DynamicComplexImageTaskResponse.cs @@ -1,13 +1,24 @@ using Newtonsoft.Json; using Zennolab.CapMonsterCloud.Json; +using static Zennolab.CapMonsterCloud.Requests.RecognitionComplexImageTaskRequest; namespace Zennolab.CapMonsterCloud.Responses -{ +{ /// /// Response for Recognition grid-like tasks /// + [JsonConverter(typeof(RecognitionAnswerJsonConverter))] public class DynamicComplexImageTaskResponse : CaptchaResponseBase { + /// + /// Metadata class containing AnswerType + /// + public class RecognitionMetadata + { + [JsonProperty("AnswerType")] + public string AnswerType { get; set; } + } + /// /// Bool or decimal collection with answers /// @@ -17,7 +28,12 @@ public class DynamicComplexImageTaskResponse : CaptchaResponseBase /// [130.90909] /// [JsonProperty("answer")] - [JsonConverter(typeof(RecognitionAnswerJsonConverter))] public RecognitionAnswer Answer { get; set; } + + /// + /// Metadata containing the answer type + /// + [JsonProperty("metadata")] + public RecognitionMetadata Metadata { get; set; } } } diff --git a/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs b/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs index 5f31f31..13fb70d 100644 --- a/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs +++ b/CapMonsterCloud.Client/Responses/RecognitionAnswer.cs @@ -19,6 +19,6 @@ public class RecognitionAnswer public bool IsNumeric => NumericAnswer != null; /// - public bool IsGrid => NumericAnswer != null; + public bool IsGrid => GridAnswer != null; } } From b7ad136bfb548da68488ba19b4afe289a1697973 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Mon, 24 Feb 2025 19:16:23 +0100 Subject: [PATCH 09/11] Fixed tests --- CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs index 8443832..300350a 100644 --- a/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs +++ b/CapMonsterCloud.Client.IntegrationTests/ObjectGen.cs @@ -347,7 +347,8 @@ public static CaptchaResult CreateDynamicComple Error = null, Solution = new DynamicComplexImageTaskResponse { - Answer = new RecognitionAnswer{ GridAnswer = Gen.ArrayOfValues(Gen.RandomBool) } + Answer = new RecognitionAnswer{ GridAnswer = Gen.ArrayOfValues(Gen.RandomBool) }, + Metadata = new DynamicComplexImageTaskResponse.RecognitionMetadata { AnswerType = "Grid" } } }; } @@ -359,7 +360,8 @@ public static CaptchaResult CreateDynamicComple Error = null, Solution = new DynamicComplexImageTaskResponse { - Answer = new RecognitionAnswer { NumericAnswer = Gen.ArrayOfValues(Gen.RandomDecimal) } + Answer = new RecognitionAnswer { NumericAnswer = Gen.ArrayOfValues(Gen.RandomDecimal) }, + Metadata = new DynamicComplexImageTaskResponse.RecognitionMetadata { AnswerType = "NumericArray" } } }; } From c6c0f8f6163d1e471605ce543a1f33dfc7f51b90 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Tue, 25 Feb 2025 13:46:19 +0100 Subject: [PATCH 10/11] Fixed hcaptcha tests --- CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs index 4b214c7..2a1fb15 100644 --- a/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs +++ b/CapMonsterCloud.Client.IntegrationTests/IntegrationTests.cs @@ -541,7 +541,8 @@ public async Task HCaptchaComplexImageTaskWithNumericAnswer_ShouldSolve() status = "ready", solution = new { - answer = expectedResult.Solution.Answer.NumericAnswer + answer = expectedResult.Solution.Answer.NumericAnswer, + metadata = new { AnswerType = "NumericArray" } }, errorId = 0, errorCode = (string)null! @@ -587,7 +588,8 @@ public async Task HCaptchaComplexImageTaskWithGridAnswer_ShouldSolve() status = "ready", solution = new { - answer = expectedResult.Solution.Answer.GridAnswer + answer = expectedResult.Solution.Answer.GridAnswer, + metadata = new { AnswerType = "Grid" } }, errorId = 0, errorCode = (string)null! From 340b7dcbe9a002d464137e986931b15bd0f43420 Mon Sep 17 00:00:00 2001 From: Andrei Morgan Date: Mon, 3 Mar 2025 10:47:01 +0100 Subject: [PATCH 11/11] Up version --- CapMonsterCloud.Client/CapMonsterCloud.Client.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj b/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj index d34a81a..047f46f 100644 --- a/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj +++ b/CapMonsterCloud.Client/CapMonsterCloud.Client.csproj @@ -15,8 +15,8 @@ README.md logo.png https://github.com/ZennoLab/capmonstercloud-client-dotnet - 2.2.0 - Added RecognitionComplexImageTask + 3.0.0 + Added RecognitionComplexImageTask. Added dynamic responses for HCaptchaComplexImageTask