| | | 1 | | using Newtonsoft.Json.Linq; |
| | | 2 | | using Services.ApiServices.ApiSupplierRequestLog; |
| | | 3 | | |
| | | 4 | | namespace Services.ApiServices.ForumAutoApi; |
| | | 5 | | |
| | | 6 | | /// <summary>Клиент read-only API Forum-Auto v2. Аутентификация — query login/pass в каждом запросе. |
| | | 7 | | /// Один и тот же API обслуживает несколько независимых аккаунтов (см. ForumAutoInterpartsHttpClient) — |
| | | 8 | | /// httpClientName/providerKey разводят именованный HttpClient и ключ лога между ними.</summary> |
| | | 9 | | public class ForumAutoHttpClient |
| | | 10 | | { |
| | | 11 | | private readonly IHttpClientFactory _factory; |
| | | 12 | | private readonly ForumAutoConfig _config; |
| | | 13 | | private readonly ApiSupplierRequestLogStore _logs; |
| | | 14 | | private readonly string _httpClientName; |
| | | 15 | | private readonly string _providerKey; |
| | | 16 | | |
| | 78 | 17 | | public ForumAutoHttpClient(IHttpClientFactory factory, ForumAutoConfig config, ApiSupplierRequestLogStore logs, |
| | 78 | 18 | | string httpClientName = "ForumAutoApi", string providerKey = "ForumAuto") |
| | | 19 | | { |
| | 78 | 20 | | _factory = factory; |
| | 78 | 21 | | _config = config; |
| | 78 | 22 | | _logs = logs; |
| | 78 | 23 | | _httpClientName = httpClientName; |
| | 78 | 24 | | _providerKey = providerKey; |
| | 78 | 25 | | } |
| | | 26 | | |
| | | 27 | | public async Task<JToken> GetAsync(string path, IReadOnlyDictionary<string, string?>? query = null, CancellationToke |
| | | 28 | | { |
| | 23 | 29 | | if (string.IsNullOrWhiteSpace(_config.Login) || string.IsNullOrWhiteSpace(_config.Password)) |
| | 0 | 30 | | throw new ForumAutoApiException($"Не настроены {_providerKey}:Login и/или {_providerKey}:Password."); |
| | | 31 | | |
| | 23 | 32 | | var merged = new Dictionary<string, string?>(query ?? new Dictionary<string, string?>()) |
| | 23 | 33 | | { |
| | 23 | 34 | | ["login"] = _config.Login, |
| | 23 | 35 | | ["pass"] = _config.Password |
| | 23 | 36 | | }; |
| | 23 | 37 | | var uri = BuildUri(path, merged); |
| | | 38 | | |
| | 23 | 39 | | var timer = System.Diagnostics.Stopwatch.StartNew(); |
| | | 40 | | try |
| | | 41 | | { |
| | 23 | 42 | | using var response = await _factory.CreateClient(_httpClientName).GetAsync(uri, ct); |
| | 23 | 43 | | var text = await response.Content.ReadAsStringAsync(ct); |
| | 23 | 44 | | _logs.Add(_providerKey, HttpMethod.Get.Method, path, (int)response.StatusCode, timer, uri.Query, text); |
| | | 45 | | |
| | 23 | 46 | | if (!response.IsSuccessStatusCode) |
| | 2 | 47 | | throw new ForumAutoApiException($"Forum-Auto вернул HTTP {(int)response.StatusCode}.", (int)response.Sta |
| | | 48 | | |
| | | 49 | | JToken token; |
| | 42 | 50 | | try { token = JToken.Parse(text); } |
| | 0 | 51 | | catch (Exception ex) { throw new ForumAutoApiException("Forum-Auto вернул невалидный JSON.", requestPath: pa |
| | | 52 | | |
| | | 53 | | // Forum-Auto возвращает SOAP-Fault-подобную ошибку в теле даже при HTTP 200 — |
| | | 54 | | // проверяем "errors" независимо от статус-кода ответа. |
| | 21 | 55 | | if (token is JObject obj && obj["errors"] is JObject errors) |
| | | 56 | | { |
| | 2 | 57 | | var faultCode = errors.Value<int?>("FaultCode"); |
| | 2 | 58 | | var faultString = errors.Value<string>("FaultString"); |
| | 2 | 59 | | var faultDetail = errors.Value<string>("FaultDetail"); |
| | 2 | 60 | | var message = string.IsNullOrWhiteSpace(faultString) ? "Forum-Auto вернул ошибку." : faultString; |
| | 4 | 61 | | if (!string.IsNullOrWhiteSpace(faultDetail)) message += $" ({faultDetail})"; |
| | 2 | 62 | | throw new ForumAutoApiException(message, (int)response.StatusCode, faultCode, path); |
| | | 63 | | } |
| | | 64 | | |
| | 19 | 65 | | return token; |
| | | 66 | | } |
| | 8 | 67 | | catch (ForumAutoApiException) { throw; } |
| | 0 | 68 | | catch (Exception ex) |
| | | 69 | | { |
| | 0 | 70 | | _logs.Add(_providerKey, HttpMethod.Get.Method, path, null, timer, uri.Query, ex.Message); |
| | 0 | 71 | | throw new ForumAutoApiException("Не удалось выполнить запрос к Forum-Auto.", requestPath: path, innerExcepti |
| | | 72 | | } |
| | 19 | 73 | | } |
| | | 74 | | |
| | | 75 | | private Uri BuildUri(string path, IReadOnlyDictionary<string, string?> query) |
| | | 76 | | { |
| | 23 | 77 | | var builder = new UriBuilder(_config.BaseUrl.TrimEnd('/') + "/" + path.TrimStart('/')); |
| | 131 | 78 | | builder.Query = string.Join("&", query.Where(x => !string.IsNullOrWhiteSpace(x.Value)) |
| | 117 | 79 | | .Select(x => $"{Uri.EscapeDataString(x.Key)}={Uri.EscapeDataString(x.Value!)}")); |
| | 23 | 80 | | return builder.Uri; |
| | | 81 | | } |
| | | 82 | | } |