using Backend.DatabaseHandler; using Backend.Interface; using System.Security.Cryptography; using System.Text; using System.Xml.Linq; namespace Backend.ServiceHandler { public sealed class NewsSentimentService { private readonly INewsSentimentStore _store; private readonly FinBertScoringService _scorer; private static readonly HttpClient Http = new() { DefaultRequestHeaders = { { "User-Agent", "SoftTrader/1.0" } } }; public NewsSentimentService(INewsSentimentStore store, FinBertScoringService scorer) { _store = store; _scorer = scorer; } public async Task> AnalyzeAsync( string keyword, CancellationToken cancellationToken = default) { keyword = keyword.Trim(); if (string.IsNullOrEmpty(keyword)) throw new ArgumentException("Keyword cannot be empty."); var articles = await FetchRssAsync(keyword, cancellationToken); var existingHashes = await _store.GetExistingHashesAsync(keyword, cancellationToken); var newRecords = new List(); foreach (var (title, url, publishedAt) in articles) { string titleHash = ComputeHash(title); if (existingHashes.Contains(titleHash)) continue; var (headline, publisher) = ParsePublisher(title); var (score, label) = _scorer.Score(headline); newRecords.Add(new NewsSentimentRecord { Source = "google_rss", Keyword = keyword, PublishedAt = publishedAt, Title = headline, Score = score, SentimentLabel = label, TitleHash = titleHash, Publisher = publisher, Url = url }); } if (newRecords.Count > 0) await _store.InsertAsync(newRecords, cancellationToken); return await _store.GetByKeywordAsync(keyword, cancellationToken); } private static async Task> FetchRssAsync( string keyword, CancellationToken cancellationToken) { string encodedKeyword = Uri.EscapeDataString(keyword); string url = $"https://news.google.com/rss/search?q={encodedKeyword}&hl=en-US&gl=US&ceid=US:en"; string xml = await Http.GetStringAsync(url, cancellationToken); var doc = XDocument.Parse(xml); var items = new List<(string, string, DateTimeOffset)>(); foreach (var item in doc.Descendants("item")) { string? title = item.Element("title")?.Value; string? link = item.Element("link")?.Value; string? pubDate = item.Element("pubDate")?.Value; if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(link)) continue; DateTimeOffset published = DateTimeOffset.TryParse(pubDate, out var dt) ? dt : DateTimeOffset.UtcNow; items.Add((title, link, published)); } return items; } private static (string Headline, string Publisher) ParsePublisher(string title) { int idx = title.LastIndexOf(" - "); if (idx < 0) return (title, "Unknown"); return (title[..idx].Trim(), title[(idx + 3)..].Trim()); } private static string ComputeHash(string text) { byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(text)); return Convert.ToHexStringLower(bytes); } } }