using System.Text.Json; using System.Text.Json.Serialization; using FinlyticCore.Util; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace FinlyticNews.Services; using FinlyticCore.Dtos.News; /// /// Defines integration operations with the external n8n AI workflow webhook. /// public interface IN8nService { /// /// Submits raw article content and pre-filtered assets to n8n, returning the parsed response metadata. /// Task AnalyzeArticleAsync(string content, List filteredAssets, CancellationToken ct = default); } /// public class N8nService : IN8nService { private readonly HttpClient _httpClient; private readonly IServiceScopeFactory _scopeFactory; private readonly IConfiguration _configuration; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// public N8nService( HttpClient httpClient, IServiceScopeFactory scopeFactory, IConfiguration configuration, ILogger logger) { _httpClient = httpClient; _scopeFactory = scopeFactory; _configuration = configuration; _logger = logger; } /// public async Task AnalyzeArticleAsync(string content, List filteredAssets, CancellationToken ct = default) { string? targetUrl = null; // 1. Dynamic Settings Resolution (DB Scope -> AppSettings Fallback) using (var scope = _scopeFactory.CreateScope()) { var settingsDb = scope.ServiceProvider.GetService(); if (settingsDb != null) { var settings = await settingsDb.GetSettingsAsync(); targetUrl = settings?.N8nWebhookUrl; } } if (string.IsNullOrWhiteSpace(targetUrl)) { targetUrl = _configuration["N8N:ArticleExtractionUrl"] ?? _configuration["N8N__ArticleExtractionUrl"]; } if (string.IsNullOrWhiteSpace(targetUrl)) { _logger.LogError("[{Channel}] N8nWebhookUrl is not configured in DB or application settings.", "NewsChannel"); return null; } _logger.LogInformation("[{Channel}] Posting article to n8n webhook pipeline at: {Url}", "NewsChannel", targetUrl); var payload = new N8nRequestPayload(content, filteredAssets ?? []); try { // Zero-Allocation / Source-Generated Request Serialization var jsonContent = JsonSerializer.Serialize(payload, FinlyticJsonSerializerContext.Default.N8nRequestPayload); using var requestContent = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json"); using var response = await _httpClient.PostAsync(targetUrl, requestContent, ct); if (!response.IsSuccessStatusCode) { var errorMsg = await response.Content.ReadAsStringAsync(ct); _logger.LogError("[{Channel}] n8n webhook returned status code {StatusCode}. Error payload: {Error}", "NewsChannel", response.StatusCode, errorMsg); return null; } using var responseStream = await response.Content.ReadAsStreamAsync(ct); using var doc = await JsonDocument.ParseAsync(responseStream, cancellationToken: ct); var root = doc.RootElement; // 2. Robust n8n Array-Unwrapping (Handles [{ "json": { ... } }]) if (root.ValueKind == JsonValueKind.Array) { if (root.GetArrayLength() == 0) { _logger.LogWarning("[{Channel}] n8n webhook returned an empty array.", "NewsChannel"); return null; } root = root[0]; } // 3. Dynamic Node Wrapper Unwrapping ("json", "output", "data", "body") if (root.ValueKind == JsonValueKind.Object) { if (root.TryGetProperty("json", out var jsonChild) && jsonChild.ValueKind == JsonValueKind.Object) root = jsonChild; else if (root.TryGetProperty("output", out var outChild) && outChild.ValueKind == JsonValueKind.Object) root = outChild; else if (root.TryGetProperty("data", out var dataChild) && dataChild.ValueKind == JsonValueKind.Object) root = dataChild; else if (root.TryGetProperty("body", out var bodyChild) && bodyChild.ValueKind == JsonValueKind.Object) root = bodyChild; } // 4. Source-Generated Deserialization directly from JsonElement var result = root.Deserialize(FinlyticJsonSerializerContext.Default.N8nResponsePayload); return result; } catch (Exception ex) { _logger.LogError(ex, "[{Channel}] Failed to communicate with or parse response from n8n webhook workflow.", "NewsChannel"); return null; } } }