112 lines
3.1 KiB
C#
112 lines
3.1 KiB
C#
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<List<NewsSentimentRecord>> 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<NewsSentimentRecord>();
|
|
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<List<(string Title, string Url, DateTimeOffset PublishedAt)>> 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);
|
|
}
|
|
}
|
|
}
|