Prac2026_Email/ListmonkIntegration/Services/ListmonkService.cs
2026-07-15 17:17:03 +03:00

812 lines
33 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using ListmonkIntegration.Models;
using TemplateInfo = ListmonkIntegration.Models.TemplateInfo;
namespace ListmonkIntegration.Services
{
public class ListmonkService : IListmonkService
{
private readonly HttpClient _httpClient;
private readonly ListmonkSettings _settings;
private readonly ILogger<ListmonkService> _logger;
public ListmonkService(
HttpClient httpClient,
ListmonkSettings settings,
ILogger<ListmonkService> logger)
{
_httpClient = httpClient;
_settings = settings;
_logger = logger;
// Настройка базового адреса
_httpClient.BaseAddress = new Uri(_settings.BaseUrl);
// Listmonk использует Basic Auth с API credentials
// Username: логин пользователя (например, "admin")
// Password: API токен (d1qPRY5jl1LsD1LskI4zWu4ukYWTyNLxjot05CjgRin4nQy3)
var username = _settings.Username;
var password = !string.IsNullOrEmpty(_settings.ApiToken)
? _settings.ApiToken
: _settings.Password;
var authToken = Convert.ToBase64String(
Encoding.ASCII.GetBytes($"{username}:{password}"));
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", authToken);
_logger.LogInformation($"Using Basic Auth with username: {username}");
}
public async Task<bool> TestConnectionAsync()
{
try
{
_logger.LogInformation($"Testing connection to Listmonk...");
_logger.LogInformation($"Base URL: {_settings.BaseUrl}");
_logger.LogInformation($"Username: {_settings.Username}");
var response = await _httpClient.GetAsync("/api/config");
_logger.LogInformation($"Response status: {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Success! Connected to Listmonk");
return true;
}
else
{
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogWarning($"Failed! Status: {response.StatusCode}, Content: {errorContent}");
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Exception during connection test: {ex.Message}");
return false;
}
}
public async Task<int> CreateListAsync(string listName)
{
_logger.LogInformation($"Creating list: {listName}");
// Сначала проверим, нет ли уже списка с таким именем
var existingListId = await FindListIdByNameAsync(listName);
if (existingListId.HasValue)
{
_logger.LogInformation($"List '{listName}' already exists with ID {existingListId.Value}");
return existingListId.Value;
}
var content = new StringContent(
JsonSerializer.Serialize(new
{
name = listName,
type = "private",
optin = "single"
}),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync("/api/lists", content);
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Create list response: {responseBody}");
response.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(responseBody);
// Listmonk возвращает {"data": {"id": 1, ...}}
var listId = json.RootElement
.GetProperty("data")
.GetProperty("id")
.GetInt32();
_logger.LogInformation($"List created with ID: {listId}");
return listId;
}
private async Task<int?> FindListIdByNameAsync(string listName)
{
try
{
var response = await _httpClient.GetAsync("/api/lists");
if (response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Lists response: {responseBody.Substring(0, Math.Min(200, responseBody.Length))}...");
var json = JsonDocument.Parse(responseBody);
// Структура: {"data": {"results": [...]}}
if (json.RootElement.TryGetProperty("data", out var data))
{
JsonElement results;
if (data.ValueKind == JsonValueKind.Object &&
data.TryGetProperty("results", out results))
{
if (results.ValueKind == JsonValueKind.Array)
{
foreach (var list in results.EnumerateArray())
{
var name = list.GetProperty("name").GetString();
if (name == listName)
{
var id = list.GetProperty("id").GetInt32();
_logger.LogInformation($"Found existing list '{listName}' with ID {id}");
return id;
}
}
}
}
else if (data.ValueKind == JsonValueKind.Array)
{
foreach (var list in data.EnumerateArray())
{
var name = list.GetProperty("name").GetString();
if (name == listName)
{
var id = list.GetProperty("id").GetInt32();
return id;
}
}
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Error searching for list '{listName}'");
}
return null;
}
public async Task<bool> ImportSubscribersAsync(List<int> listIds, List<Subscriber> subscribers)
{
_logger.LogInformation($"Importing {subscribers.Count} subscribers to lists: {string.Join(", ", listIds)}");
int successCount = 0;
int failCount = 0;
int updatedCount = 0;
int createdCount = 0;
foreach (var sub in subscribers)
{
try
{
// Проверяем, существует ли подписчик
var existingSubscriber = await GetSubscriberByEmailAsync(sub.Email);
if (existingSubscriber != null)
{
// Подписчик существует - обновляем его
_logger.LogInformation($"Subscriber {sub.Email} exists, updating...");
// Получаем ID существующего подписчика
var subscriberId = await GetSubscriberIdByEmailAsync(sub.Email);
if (subscriberId.HasValue)
{
// Получаем текущие списки подписчика
var currentListIds = await GetSubscriberListIdsAsync(subscriberId.Value);
// Добавляем новые списки к существующим
var allListIds = new List<int>(currentListIds);
foreach (var listId in listIds)
{
if (!allListIds.Contains(listId))
{
allListIds.Add(listId);
}
}
// Объединяем атрибуты
var mergedAttributes = new Dictionary<string, string>(existingSubscriber.Attributes);
foreach (var attr in sub.Attributes)
{
mergedAttributes[attr.Key] = attr.Value;
}
// Обновляем подписчика
var updated = await UpdateSubscriberAsync(
subscriberId.Value,
sub.Email,
sub.FullName,
mergedAttributes,
allListIds
);
if (updated)
{
updatedCount++;
successCount++;
}
else
{
failCount++;
}
}
else
{
failCount++;
}
}
else
{
// Создаём нового подписчика
await AddSubscriberToList(listIds, sub);
createdCount++;
successCount++;
}
}
catch (Exception ex)
{
failCount++;
_logger.LogWarning(ex, $"Failed to process subscriber {sub.Email}");
}
}
_logger.LogInformation($"Import completed: {successCount} success ({createdCount} created, {updatedCount} updated), {failCount} failed");
return true;
}
private async Task<List<int>> GetSubscriberListIdsAsync(int subscriberId)
{
try
{
var response = await _httpClient.GetAsync($"/api/subscribers/{subscriberId}");
if (!response.IsSuccessStatusCode)
return new List<int>();
var responseBody = await response.Content.ReadAsStringAsync();
var json = JsonDocument.Parse(responseBody);
var listIds = new List<int>();
if (json.RootElement.TryGetProperty("data", out var data))
{
if (data.TryGetProperty("lists", out var lists))
{
foreach (var list in lists.EnumerateArray())
{
if (list.TryGetProperty("id", out var id))
{
listIds.Add(id.GetInt32());
}
}
}
}
return listIds;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting list IDs for subscriber {subscriberId}");
return new List<int>();
}
}
private async Task AddSubscriberToList(List<int> listIds, Subscriber subscriber)
{
var subscriberData = new
{
email = subscriber.Email,
name = subscriber.FullName,
lists = listIds, // Массив ID списков
attribs = subscriber.Attributes,
status = "enabled",
mode = "subscribe"
};
var jsonContent = JsonSerializer.Serialize(subscriberData);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/api/subscribers", content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogWarning($"Failed to add subscriber {subscriber.Email}: {response.StatusCode} - {errorContent}");
throw new Exception($"Failed to add subscriber: {errorContent}");
}
}
public async Task<bool> CreateCampaignAsync(string name, int listId, int templateId)
{
var content = new StringContent(
JsonSerializer.Serialize(new
{
name = name,
subject = "Тема письма",
lists = new[] { listId },
template_id = templateId,
type = "regular",
content_type = "html"
}),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync("/api/campaigns", content);
return response.IsSuccessStatusCode;
}
public async Task<List<ListInfo>> GetListsAsync()
{
_logger.LogInformation("Fetching lists from Listmonk");
var response = await _httpClient.GetAsync("/api/lists");
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogError($"Failed to fetch lists: {response.StatusCode} - {errorContent}");
throw new Exception($"Failed to fetch lists: {response.StatusCode}");
}
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Raw response: {responseBody.Substring(0, Math.Min(500, responseBody.Length))}");
var json = JsonDocument.Parse(responseBody);
var lists = new List<ListInfo>();
// Структура: {"data": {"results": [...]}}
if (json.RootElement.TryGetProperty("data", out var data))
{
if (data.ValueKind == JsonValueKind.Object &&
data.TryGetProperty("results", out var results))
{
if (results.ValueKind == JsonValueKind.Array)
{
foreach (var list in results.EnumerateArray())
{
var listInfo = new ListInfo
{
Id = list.GetProperty("id").GetInt32(),
Name = list.GetProperty("name").GetString() ?? string.Empty,
SubscriberCount = list.TryGetProperty("subscriber_count", out var countProp)
? countProp.GetInt32()
: 0
};
lists.Add(listInfo);
_logger.LogInformation($"Parsed list: ID={listInfo.Id}, Name={listInfo.Name}, Subscribers={listInfo.SubscriberCount}");
}
}
}
else if (data.ValueKind == JsonValueKind.Array)
{
// На случай, если Listmonk вернёт массив напрямую
foreach (var list in data.EnumerateArray())
{
var listInfo = new ListInfo
{
Id = list.GetProperty("id").GetInt32(),
Name = list.GetProperty("name").GetString() ?? string.Empty,
SubscriberCount = list.TryGetProperty("subscriber_count", out var countProp)
? countProp.GetInt32()
: 0
};
lists.Add(listInfo);
}
}
}
_logger.LogInformation($"Total lists found: {lists.Count}");
return lists;
}
public async Task<List<string>> GetListAttributesAsync(int listId)
{
_logger.LogInformation($"Fetching attributes for list {listId}");
// Получаем ТОЛЬКО подписчиков из конкретного списка
var response = await _httpClient.GetAsync($"/api/subscribers?list_id={listId}&per_page=1000");
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning($"Failed to fetch subscribers for list {listId}: {response.StatusCode}");
return new List<string>();
}
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Subscribers response for list {listId}: {responseBody.Substring(0, Math.Min(500, responseBody.Length))}");
var json = JsonDocument.Parse(responseBody);
var attributes = new HashSet<string>();
// Структура: {"data": {"results": [...]}}
if (json.RootElement.TryGetProperty("data", out var data))
{
JsonElement results;
if (data.ValueKind == JsonValueKind.Object && data.TryGetProperty("results", out results))
{
if (results.ValueKind == JsonValueKind.Array)
{
foreach (var subscriber in results.EnumerateArray())
{
if (subscriber.TryGetProperty("attribs", out var attribs))
{
foreach (var prop in attribs.EnumerateObject())
{
attributes.Add(prop.Name);
_logger.LogDebug($"Found attribute '{prop.Name}' for list {listId}");
}
}
}
}
}
else if (data.ValueKind == JsonValueKind.Array)
{
foreach (var subscriber in data.EnumerateArray())
{
if (subscriber.TryGetProperty("attribs", out var attribs))
{
foreach (var prop in attribs.EnumerateObject())
{
attributes.Add(prop.Name);
}
}
}
}
}
var result = attributes.ToList();
_logger.LogInformation($"Found {result.Count} unique attributes for list {listId}: {string.Join(", ", result)}");
return result;
}
public async Task<int> CreateTemplateAsync(string name, string subject, string body)
{
_logger.LogInformation($"Creating template: {name}");
// Listmonk требует плейсхолдер {{ template "content" . }} в теле шаблона
// Если его нет — добавляем автоматически
if (!body.Contains("{{ template \"content\" . }}"))
{
body = body.TrimEnd() + "\n\n{{ template \"content\" . }}";
_logger.LogInformation("Added required placeholder to template body");
}
var templateData = new
{
name = name,
subject = subject,
body = body,
type = "campaign"
};
var jsonContent = JsonSerializer.Serialize(templateData);
_logger.LogInformation($"Request body: {jsonContent}");
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/api/templates", content);
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Response status: {response.StatusCode}");
_logger.LogInformation($"Response body: {responseBody}");
if (!response.IsSuccessStatusCode)
{
_logger.LogError($"Failed to create template: {response.StatusCode} - {responseBody}");
throw new Exception($"Failed to create template: {responseBody}");
}
var json = JsonDocument.Parse(responseBody);
int templateId;
if (json.RootElement.TryGetProperty("data", out var data))
{
templateId = data.GetProperty("id").GetInt32();
}
else
{
templateId = json.RootElement.GetProperty("id").GetInt32();
}
_logger.LogInformation($"Template created with ID: {templateId}");
return templateId;
}
public async Task<List<TemplateInfo>> GetTemplatesAsync()
{
_logger.LogInformation("Fetching templates from Listmonk");
var response = await _httpClient.GetAsync("/api/templates");
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
_logger.LogError($"Failed to fetch templates: {response.StatusCode} - {error}");
return new List<TemplateInfo>();
}
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Templates response: {responseBody.Substring(0, Math.Min(500, responseBody.Length))}");
var json = JsonDocument.Parse(responseBody);
var templates = new List<TemplateInfo>();
if (json.RootElement.TryGetProperty("data", out var data))
{
JsonElement results;
if (data.ValueKind == JsonValueKind.Object && data.TryGetProperty("results", out results))
{
if (results.ValueKind == JsonValueKind.Array)
{
foreach (var template in results.EnumerateArray())
{
templates.Add(new TemplateInfo
{
Id = template.GetProperty("id").GetInt32(),
Name = template.GetProperty("name").GetString() ?? string.Empty,
Subject = template.TryGetProperty("subject", out var subj)
? subj.GetString() ?? string.Empty
: string.Empty
});
}
}
}
else if (data.ValueKind == JsonValueKind.Array)
{
foreach (var template in data.EnumerateArray())
{
templates.Add(new TemplateInfo
{
Id = template.GetProperty("id").GetInt32(),
Name = template.GetProperty("name").GetString() ?? string.Empty,
Subject = template.TryGetProperty("subject", out var subj)
? subj.GetString() ?? string.Empty
: string.Empty
});
}
}
}
_logger.LogInformation($"Found {templates.Count} templates");
return templates;
}
public async Task<string> GetTemplateBodyAsync(int templateId)
{
_logger.LogInformation($"Fetching template {templateId}");
var response = await _httpClient.GetAsync($"/api/templates/{templateId}");
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
var json = JsonDocument.Parse(responseBody);
if (json.RootElement.TryGetProperty("data", out var data))
{
if (data.TryGetProperty("body", out var body))
{
return body.GetString() ?? string.Empty;
}
}
return string.Empty;
}
public async Task<int> CreateCampaignAsync(string name, int listId, int templateId, string subject)
{
_logger.LogInformation($"Creating campaign: {name}");
_logger.LogInformation($"List ID: {listId}, Template ID: {templateId}, Subject: {subject}");
// Убираем send_at - кампания создастся как черновик, потом запустим отдельно
var campaignData = new
{
name = name,
subject = subject,
lists = new[] { listId },
template_id = templateId,
type = "regular",
content_type = "html",
messenger = "email"
};
var jsonContent = JsonSerializer.Serialize(campaignData);
_logger.LogInformation($"Request body: {jsonContent}");
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/api/campaigns", content);
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Response status: {response.StatusCode}");
_logger.LogInformation($"Response body: {responseBody}");
if (!response.IsSuccessStatusCode)
{
_logger.LogError($"Failed to create campaign: {response.StatusCode} - {responseBody}");
throw new Exception($"Failed to create campaign: {responseBody}");
}
var json = JsonDocument.Parse(responseBody);
int campaignId;
if (json.RootElement.TryGetProperty("data", out var data))
{
campaignId = data.GetProperty("id").GetInt32();
}
else
{
campaignId = json.RootElement.GetProperty("id").GetInt32();
}
_logger.LogInformation($"Campaign created with ID: {campaignId}");
return campaignId;
}
public async Task<bool> StartCampaignAsync(int campaignId)
{
_logger.LogInformation($"Starting campaign {campaignId}");
var statusData = new
{
status = "running"
};
var jsonContent = JsonSerializer.Serialize(statusData);
_logger.LogInformation($"Request body: {jsonContent}");
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
// ВАЖНО: PUT, не POST!
var request = new HttpRequestMessage(HttpMethod.Put, $"/api/campaigns/{campaignId}/status")
{
Content = content
};
var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Response status: {response.StatusCode}");
_logger.LogInformation($"Response body: {responseBody}");
if (!response.IsSuccessStatusCode)
{
_logger.LogError($"Failed to start campaign: {response.StatusCode} - {responseBody}");
return false;
}
_logger.LogInformation($"Campaign {campaignId} started successfully");
return true;
}
public async Task<Subscriber?> GetSubscriberByEmailAsync(string email)
{
_logger.LogInformation($"Searching for subscriber by email: {email}");
try
{
// Ищем подписчика по email
var response = await _httpClient.GetAsync($"/api/subscribers?query=LOWER(email)=LOWER('{email}')&per_page=1");
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning($"Failed to search subscriber: {response.StatusCode}");
return null;
}
var responseBody = await response.Content.ReadAsStringAsync();
var json = JsonDocument.Parse(responseBody);
if (json.RootElement.TryGetProperty("data", out var data))
{
JsonElement results;
if (data.ValueKind == JsonValueKind.Object && data.TryGetProperty("results", out results))
{
if (results.ValueKind == JsonValueKind.Array && results.GetArrayLength() > 0)
{
var subscriberData = results[0];
// Парсим атрибуты
var attributes = new Dictionary<string, string>();
if (subscriberData.TryGetProperty("attribs", out var attribs))
{
foreach (var prop in attribs.EnumerateObject())
{
attributes[prop.Name] = prop.Value.GetString() ?? string.Empty;
}
}
// Получаем списки
var listIds = new List<int>();
if (subscriberData.TryGetProperty("lists", out var lists))
{
foreach (var list in lists.EnumerateArray())
{
if (list.TryGetProperty("id", out var id))
{
listIds.Add(id.GetInt32());
}
}
}
return new Subscriber
{
Email = subscriberData.GetProperty("email").GetString() ?? string.Empty,
FullName = subscriberData.GetProperty("name").GetString() ?? string.Empty,
Attributes = attributes
};
}
}
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error searching subscriber {email}");
return null;
}
}
public async Task<bool> UpdateSubscriberAsync(int subscriberId, string email, string name, Dictionary<string, string> attributes, List<int> listIds)
{
_logger.LogInformation($"Updating subscriber {subscriberId} ({email})");
var updateData = new
{
email = email,
name = name,
attribs = attributes,
lists = listIds,
status = "enabled"
};
var jsonContent = JsonSerializer.Serialize(updateData);
_logger.LogInformation($"Update data: {jsonContent}");
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await _httpClient.PutAsync($"/api/subscribers/{subscriberId}", content);
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Update response: {response.StatusCode} - {responseBody}");
return response.IsSuccessStatusCode;
}
private async Task<int?> GetSubscriberIdByEmailAsync(string email)
{
try
{
var response = await _httpClient.GetAsync($"/api/subscribers?query=LOWER(email)=LOWER('{email}')&per_page=1");
if (!response.IsSuccessStatusCode)
return null;
var responseBody = await response.Content.ReadAsStringAsync();
var json = JsonDocument.Parse(responseBody);
if (json.RootElement.TryGetProperty("data", out var data))
{
JsonElement results;
if (data.ValueKind == JsonValueKind.Object && data.TryGetProperty("results", out results))
{
if (results.ValueKind == JsonValueKind.Array && results.GetArrayLength() > 0)
{
return results[0].GetProperty("id").GetInt32();
}
}
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting subscriber ID for {email}");
return null;
}
}
}
}