Files
Finlytic/FinlyticBackend/Controllers/NewsController.cs
T

326 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FinlyticBackend.Util;
using FinlyticCore.Dtos;
using FinlyticCore.Dtos.News;
using FinlyticCore.Dtos.Sentiment;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace FinlyticBackend.Controllers;
[ApiController]
[Authorize]
[Route("api/v1/news")]
public class NewsController : ControllerBase
{
private readonly WebMqttClient _mqttClient;
private readonly ILogger<NewsController> _logger;
public NewsController(WebMqttClient mqttClient, ILogger<NewsController> logger)
{
_mqttClient = mqttClient;
_logger = logger;
}
/// <summary>
/// Retrieves paginated news articles with optional filters and enriched sentiment data.
/// </summary>
[HttpGet]
public async Task<IActionResult> GetNews(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? symbol = null,
[FromQuery] string? isin = null,
[FromQuery] string? date = null,
[FromQuery] string? status = null,
[FromQuery] string? query = null,
[FromQuery] bool? hasSentiment = null)
{
string? activeSymbol = !string.IsNullOrWhiteSpace(symbol) ? symbol.Trim() : isin?.Trim();
string? effectiveStatus = status;
if (hasSentiment == true && string.IsNullOrEmpty(effectiveStatus))
{
effectiveStatus = "Analyzed";
}
try
{
if (_mqttClient.IsConnected)
{
var payload = new DailyNewsRequest(
Limit: pageSize,
Offset: (page - 1) * pageSize,
Isin: activeSymbol,
Date: DateTime.TryParse(date, out var dateTime) ? dateTime : null,
Status: effectiveStatus,
Query: query,
HasSentiment: hasSentiment
);
_logger.LogInformation("[NewsController] Fetching news. Date filter: {Date}", payload.Date?.ToString("o") ?? "None");
var articles = await _mqttClient.SendRpcRequestAsync<List<NewsArticleDto>, DailyNewsRequest>(
"news_Get",
payload,
TimeSpan.FromSeconds(10)
);
if (articles != null && articles.Count > 0)
{
// Parallelisierte Anreichung fehlender Sentiment-Daten
if (hasSentiment == true || string.Equals(effectiveStatus, "Analyzed", StringComparison.OrdinalIgnoreCase))
{
var enrichmentTasks = articles.Select(async article =>
{
if (article.FinbertResult != null)
{
return article;
}
try
{
var sentimentEntry = await _mqttClient.SendRpcRequestAsync<IsinAnalysisEntry, ArticleRequest>(
"sentiment_GetArticle",
new ArticleRequest(article.Id.ToString(), article.Id.ToString()),
TimeSpan.FromSeconds(2)
);
if (sentimentEntry?.FinbertResult != null)
{
var result = sentimentEntry.FinbertResult;
return article with
{
Sentiment = result.Label,
SentimentScore = result.CompoundScore,
Confidence = result.Confidence,
FinbertResult = result
};
}
}
catch
{
// RPC Timeout oder nicht analysiert -> Un-angereicherten Artikel zurückgeben
}
return article;
});
var enrichedArticles = await Task.WhenAll(enrichmentTasks);
return Ok(enrichedArticles);
}
return Ok(articles);
}
}
else
{
_logger.LogWarning("[GetNews] Cannot send MQTT RPC - MQTT client is NOT connected to broker.");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[GetNews] MQTT RPC news_Get request failed for symbol '{Symbol}'", activeSymbol);
}
return Ok(new List<NewsArticleDto>());
}
/// <summary>
/// Retrieves sentiment summary for a specific ISIN.
/// </summary>
[HttpGet("sentiment/isin/{isin}")]
public async Task<IActionResult> GetIsinSentimentSummary(string isin)
{
if (string.IsNullOrWhiteSpace(isin)) return NotFound(new { message = "ISIN is required." });
var isinCode = isin.Trim().ToUpperInvariant();
try
{
if (_mqttClient.IsConnected)
{
var rpcResult = await _mqttClient.SendRpcRequestAsync<IsinSentimentSummaryDto, IsinRequest>(
"sentiment_GetIsin",
new IsinRequest(isinCode),
TimeSpan.FromSeconds(5)
);
if (rpcResult != null)
{
return Ok(rpcResult);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "MQTT RPC sentiment_GetIsin failed for ISIN '{Isin}'", isinCode);
}
// File-System Fallback
var candidates = new[]
{
Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "isin", $"{isinCode}.json"),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "summaries", "isin", $"{isinCode}.json"),
Path.Combine(Directory.GetCurrentDirectory(), "..", "data", "summaries", "isin", $"{isinCode}.json")
};
foreach (var path in candidates)
{
if (System.IO.File.Exists(path))
{
try
{
var json = await System.IO.File.ReadAllTextAsync(path);
return Content(json, "application/json");
}
catch { }
}
}
return NotFound(new { message = $"No sentiment summary found for ISIN {isinCode}." });
}
/// <summary>
/// Retrieves sentiment analysis for a specific article.
/// </summary>
[HttpGet("sentiment/article/{articleId}")]
public async Task<IActionResult> GetArticleSentimentAnalysis(string articleId)
{
if (string.IsNullOrWhiteSpace(articleId)) return NotFound(new { message = "ArticleId is required." });
var targetId = articleId.Trim();
try
{
if (_mqttClient.IsConnected)
{
var rpcResult = await _mqttClient.SendRpcRequestAsync<IsinAnalysisEntry, ArticleRequest>(
"sentiment_GetArticle",
new ArticleRequest(targetId, targetId),
TimeSpan.FromSeconds(5)
);
if (rpcResult != null)
{
return Ok(rpcResult);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "MQTT RPC sentiment_GetArticle failed for articleId '{ArticleId}'", targetId);
}
// File-System Fallback
var articleCandidates = new[]
{
Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "articles", $"{targetId}.json"),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "summaries", "articles", $"{targetId}.json")
};
foreach (var path in articleCandidates)
{
if (System.IO.File.Exists(path))
{
try
{
var json = await System.IO.File.ReadAllTextAsync(path);
return Content(json, "application/json");
}
catch { }
}
}
return NotFound(new { message = $"No sentiment analysis found for article {articleId}." });
}
/// <summary>
/// Triggers or renews sentiment analysis for a specific news article via FinlyticSentiment.
/// </summary>
[HttpPost("sentiment/article/{articleId}/analyze")]
[HttpPost("{articleId}/reanalyze")]
public async Task<IActionResult> ReanalyzeArticleSentiment(string articleId)
{
if (string.IsNullOrWhiteSpace(articleId)) return BadRequest(new { message = "ArticleId ist erforderlich." });
var targetId = articleId.Trim();
try
{
if (_mqttClient.IsConnected)
{
var rpcResult = await _mqttClient.SendRpcRequestAsync<IsinAnalysisEntry, AnalyzeSentimentRequest>(
"sentiment_Analyze",
new AnalyzeSentimentRequest(ArticleId: targetId, ForceReload: true),
TimeSpan.FromSeconds(20)
);
if (rpcResult != null)
{
// Fetch full article from FinlyticNews to return complete updated DTO
var article = await _mqttClient.SendRpcRequestAsync<NewsArticleDto, ArticleRequest>(
"news_GetById",
new ArticleRequest(targetId, targetId),
TimeSpan.FromSeconds(5)
);
if (article != null && rpcResult.FinbertResult != null)
{
var res = rpcResult.FinbertResult;
var enriched = article with
{
Sentiment = res.Label,
SentimentScore = res.CompoundScore,
Confidence = res.Confidence,
FinbertResult = res,
Status = "Analyzed"
};
return Ok(enriched);
}
if (article != null)
{
return Ok(article);
}
// Fallback to synthetic DTO from entry
var synthetic = new NewsArticleDto
{
Id = Guid.TryParse(targetId, out var g) ? g : Guid.NewGuid(),
Title = rpcResult.Article?.Title ?? "Artikel",
Author = rpcResult.Article?.Source ?? "FinlyticNews",
Summary = rpcResult.SummarySnippet,
ContentRaw = "",
SourceUrl = "",
ScrapedAt = DateTime.UtcNow,
PublishedAt = DateTime.TryParse(rpcResult.Article?.PublishedAt, out var pDate) ? pDate : DateTime.UtcNow,
Status = "Analyzed",
Sentiment = rpcResult.FinbertResult?.Label ?? "NEUTRAL",
SentimentScore = rpcResult.FinbertResult?.CompoundScore ?? 0.0,
Confidence = rpcResult.FinbertResult?.Confidence ?? 0.0,
FinbertResult = rpcResult.FinbertResult
};
return Ok(synthetic);
}
}
else
{
return StatusCode(503, new { message = "MQTT Broker nicht verbunden." });
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Fehler beim erneuten Analysieren des Artikels {ArticleId}", targetId);
return StatusCode(500, new { message = $"Analyse fehlgeschlagen: {ex.Message}" });
}
return NotFound(new { message = $"Artikel {targetId} konnte nicht analysiert werden." });
}
}