| | | 1 | | using System.Text; |
| | | 2 | | using System.Text.Json; |
| | | 3 | | using Microsoft.Extensions.Http; |
| | | 4 | | using AppRepository.ApiServices.OzonApi.Filters; |
| | | 5 | | |
| | | 6 | | namespace AppRepository.ApiServices.OzonApi |
| | | 7 | | { |
| | | 8 | | /// <summary> |
| | | 9 | | /// Простой HTTP клиент специально для работы с Ozon Chat API |
| | | 10 | | /// Без rate limiting и дополнительных ограничений |
| | | 11 | | /// </summary> |
| | | 12 | | public class OzonChatHttpClient |
| | | 13 | | { |
| | | 14 | | private readonly IHttpClientFactory _httpClientFactory; |
| | | 15 | | private readonly string _clientId; |
| | | 16 | | private readonly string _apiKey; |
| | | 17 | | private const string BaseUrl = "https://api-seller.ozon.ru"; |
| | | 18 | | |
| | 0 | 19 | | private static readonly JsonSerializerOptions _serializeOptions = new() |
| | 0 | 20 | | { |
| | 0 | 21 | | DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, |
| | 0 | 22 | | PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower |
| | 0 | 23 | | }; |
| | | 24 | | |
| | 0 | 25 | | private static readonly JsonSerializerOptions _deserializeOptions = new() |
| | 0 | 26 | | { |
| | 0 | 27 | | PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, |
| | 0 | 28 | | NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString |
| | 0 | 29 | | }; |
| | | 30 | | |
| | 0 | 31 | | public OzonChatHttpClient( |
| | 0 | 32 | | IHttpClientFactory httpClientFactory, |
| | 0 | 33 | | string clientId, |
| | 0 | 34 | | string apiKey) |
| | | 35 | | { |
| | 0 | 36 | | _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); |
| | 0 | 37 | | _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); |
| | 0 | 38 | | _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); |
| | 0 | 39 | | } |
| | | 40 | | |
| | | 41 | | /// <summary> |
| | | 42 | | /// Выполняет POST запрос к Ozon Chat API |
| | | 43 | | /// </summary> |
| | | 44 | | /// <typeparam name="TRequest">Тип запроса</typeparam> |
| | | 45 | | /// <typeparam name="TResponse">Тип ответа</typeparam> |
| | | 46 | | /// <param name="request">Объект запроса</param> |
| | | 47 | | /// <param name="endpoint">Endpoint API (например, "/v3/chat/list")</param> |
| | | 48 | | /// <returns>Десериализованный ответ или null в случае ошибки</returns> |
| | | 49 | | public async Task<TResponse?> PostAsync<TRequest, TResponse>(TRequest request, string endpoint) |
| | | 50 | | where TRequest : class |
| | | 51 | | where TResponse : class |
| | | 52 | | { |
| | | 53 | | try |
| | | 54 | | { |
| | 0 | 55 | | using var httpClient = _httpClientFactory.CreateClient(); |
| | 0 | 56 | | httpClient.Timeout = TimeSpan.FromSeconds(30); |
| | | 57 | | |
| | | 58 | | // Устанавливаем заголовки аутентификации |
| | 0 | 59 | | httpClient.DefaultRequestHeaders.Clear(); |
| | 0 | 60 | | httpClient.DefaultRequestHeaders.Add("Client-Id", _clientId); |
| | 0 | 61 | | httpClient.DefaultRequestHeaders.Add("Api-Key", _apiKey); |
| | | 62 | | |
| | | 63 | | // Сериализуем запрос в JSON (System.Text.Json для поддержки JsonPropertyName атрибутов) |
| | 0 | 64 | | var jsonRequest = JsonSerializer.Serialize(request, _serializeOptions); |
| | 0 | 65 | | using var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"); |
| | | 66 | | |
| | | 67 | | // Строим полный URL |
| | 0 | 68 | | var fullUrl = $"{BaseUrl}{endpoint}"; |
| | | 69 | | |
| | | 70 | | // Выполняем POST запрос |
| | 0 | 71 | | using var response = await httpClient.PostAsync(fullUrl, content).ConfigureAwait(false); |
| | | 72 | | |
| | | 73 | | // Читаем ответ |
| | 0 | 74 | | var jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); |
| | | 75 | | |
| | | 76 | | |
| | 0 | 77 | | if (!response.IsSuccessStatusCode) |
| | | 78 | | { |
| | | 79 | | // Проверяем на ошибку Premium Plus |
| | 0 | 80 | | if (!string.IsNullOrEmpty(jsonResponse) && jsonResponse.Contains("PermissionDenied") && jsonResponse |
| | | 81 | | { |
| | | 82 | | // Для ChatHistoryResponse — возвращаем специальный объект с флагом |
| | 0 | 83 | | if (typeof(TResponse).Name == "ChatHistoryResponse") |
| | | 84 | | { |
| | 0 | 85 | | var premiumResp = new Filters.ChatHistoryResponse(); |
| | | 86 | | // Добавим свойство через dynamic cast (или расширьте модель при необходимости) |
| | | 87 | | // Здесь используем хак через словарь, если потребуется |
| | | 88 | | // Можно добавить публичное свойство IsPremiumRequired в ChatHistoryResponse |
| | 0 | 89 | | typeof(Filters.ChatHistoryResponse).GetProperty("IsPremiumRequired")?.SetValue(premiumResp, |
| | 0 | 90 | | return premiumResp as TResponse; |
| | | 91 | | } |
| | | 92 | | } |
| | 0 | 93 | | Console.WriteLine($"Ozon Chat API Error: {response.StatusCode} - {jsonResponse}"); |
| | 0 | 94 | | return null; |
| | | 95 | | } |
| | | 96 | | |
| | 0 | 97 | | if (string.IsNullOrWhiteSpace(jsonResponse)) |
| | | 98 | | { |
| | 0 | 99 | | return null; |
| | | 100 | | } |
| | | 101 | | |
| | | 102 | | // Десериализуем ответ |
| | 0 | 103 | | return JsonSerializer.Deserialize<TResponse>(jsonResponse, _deserializeOptions); |
| | | 104 | | } |
| | 0 | 105 | | catch (Exception ex) |
| | | 106 | | { |
| | 0 | 107 | | Console.WriteLine($"Ozon Chat HTTP Client Error: {ex.Message}"); |
| | 0 | 108 | | return null; |
| | | 109 | | } |
| | 0 | 110 | | } |
| | | 111 | | |
| | | 112 | | /// <summary> |
| | | 113 | | /// Получает список чатов |
| | | 114 | | /// </summary> |
| | | 115 | | /// <param name="request">Параметры запроса</param> |
| | | 116 | | /// <returns>Ответ с чатами или null</returns> |
| | | 117 | | public async Task<ChatListResponse?> GetChatListAsync(ChatListRequest request) |
| | | 118 | | { |
| | 0 | 119 | | return await PostAsync<ChatListRequest, ChatListResponse>(request, "/v3/chat/list") |
| | 0 | 120 | | .ConfigureAwait(false); |
| | 0 | 121 | | } |
| | | 122 | | |
| | | 123 | | /// <summary> |
| | | 124 | | /// Получает историю сообщений чата |
| | | 125 | | /// </summary> |
| | | 126 | | /// <param name="request">Параметры запроса</param> |
| | | 127 | | /// <returns>Ответ с сообщениями или null</returns> |
| | | 128 | | public async Task<ChatHistoryResponse?> GetChatHistoryAsync(ChatHistoryRequest request) |
| | | 129 | | { |
| | 0 | 130 | | return await PostAsync<ChatHistoryRequest, ChatHistoryResponse>(request, "/v3/chat/history") |
| | 0 | 131 | | .ConfigureAwait(false); |
| | 0 | 132 | | } |
| | | 133 | | |
| | | 134 | | /// <summary> |
| | | 135 | | /// Отмечает сообщения в чате как прочитанные |
| | | 136 | | /// </summary> |
| | | 137 | | /// <param name="request">Параметры запроса (chat_id и from_message_id)</param> |
| | | 138 | | /// <returns>Ответ с количеством непрочитанных сообщений или null</returns> |
| | | 139 | | public async Task<ChatReadResponse?> MarkAsReadAsync(ChatReadRequest request) |
| | | 140 | | { |
| | 0 | 141 | | return await PostAsync<ChatReadRequest, ChatReadResponse>(request, "/v2/chat/read") |
| | 0 | 142 | | .ConfigureAwait(false); |
| | 0 | 143 | | } |
| | | 144 | | |
| | | 145 | | /// <summary> |
| | | 146 | | /// Отправляет сообщение в чат |
| | | 147 | | /// </summary> |
| | | 148 | | /// <param name="request">Параметры запроса (chat_id и text)</param> |
| | | 149 | | /// <returns>Ответ с результатом отправки или null</returns> |
| | | 150 | | public async Task<ChatSendMessageResponse?> SendMessageAsync(ChatSendMessageRequest request) |
| | | 151 | | { |
| | 0 | 152 | | return await PostAsync<ChatSendMessageRequest, ChatSendMessageResponse>(request, "/v1/chat/send/message") |
| | 0 | 153 | | .ConfigureAwait(false); |
| | 0 | 154 | | } |
| | | 155 | | |
| | | 156 | | /// <summary> |
| | | 157 | | /// Отправляет файл в чат |
| | | 158 | | /// </summary> |
| | | 159 | | /// <param name="request">Параметры запроса (chat_id, base64_content и name)</param> |
| | | 160 | | /// <returns>Ответ с результатом отправки или null</returns> |
| | | 161 | | public async Task<ChatSendFileResponse?> SendFileAsync(ChatSendFileRequest request) |
| | | 162 | | { |
| | 0 | 163 | | return await PostAsync<ChatSendFileRequest, ChatSendFileResponse>(request, "/v1/chat/send/file") |
| | 0 | 164 | | .ConfigureAwait(false); |
| | 0 | 165 | | } |
| | | 166 | | } |
| | | 167 | | } |