using Backend.Interface; using Backend.ServiceHander; using Microsoft.AspNetCore.Mvc; namespace Backend.Controller { [ApiController] [Route("api/news-sentiment")] public sealed class NewsSentimentController : ControllerBase { private static DateTime _lastRefresh = DateTime.MinValue; private static readonly Lock _refreshLock = new(); private readonly NewsSentimentService _service; private readonly IUserInterestStore _interests; private readonly INewsSentimentStore _store; public NewsSentimentController( NewsSentimentService service, IUserInterestStore interests, INewsSentimentStore store) { _service = service; _interests = interests; _store = store; } [HttpGet] public async Task Analyze( [FromQuery] string keyword, CancellationToken cancellationToken) { try { var results = await _service.AnalyzeAsync(keyword, cancellationToken); return Ok(results.Select(r => new { source = r.Source, keyword = r.Keyword, publishedAt = r.PublishedAt, title = r.Title, score = r.Score, sentimentLabel = r.SentimentLabel, publisher = r.Publisher, url = r.Url })); } catch (ArgumentException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return StatusCode(500, new { error = ex.ToString() }); } } [HttpGet("feed")] public async Task Feed( [FromQuery] string user, [FromQuery] string? keyword = null, [FromQuery(Name = "request-amount")] int requestAmount = 20, CancellationToken cancellationToken = default) { try { if (string.IsNullOrWhiteSpace(user)) return BadRequest("User cannot be empty."); if (requestAmount < 1) requestAmount = 1; List keywords; if (!string.IsNullOrWhiteSpace(keyword)) { keywords = [keyword.Trim()]; } else { var symbols = await _interests.FetchAllAsync(user, "symbol"); var topics = await _interests.FetchAllAsync(user, "topic"); keywords = symbols.Concat(topics).Distinct().ToList(); } if (keywords.Count == 0) return Ok(Array.Empty()); var results = await _store.GetFeedAsync(keywords, requestAmount, cancellationToken); return Ok(results.Select(r => new { title = r.Title, publisher = r.Publisher, score = r.Score, publishedAt = r.PublishedAt.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ"), url = r.Url })); } catch (ArgumentException ex) { return BadRequest(ex.Message); } catch (Exception ex) { return StatusCode(500, new { error = ex.ToString() }); } } [HttpGet("refresh")] public async Task> Refresh( [FromQuery] string user, CancellationToken cancellationToken = default) { try { if (string.IsNullOrWhiteSpace(user)) return BadRequest("User cannot be empty."); // 5 second refresh cooldown - abuse protection lock (_refreshLock) { if ((DateTime.UtcNow - _lastRefresh).TotalSeconds < 5) return Ok(new BoolResponse { Success = false }); _lastRefresh = DateTime.UtcNow; } var symbols = await _interests.FetchAllAsync(user, "symbol"); var topics = await _interests.FetchAllAsync(user, "topic"); var keywords = symbols.Concat(topics).Distinct().ToList(); foreach (var keyword in keywords) { await _service.AnalyzeAsync(keyword, cancellationToken); } return Ok(new BoolResponse { Success = true }); } catch (Exception) { return Ok(new BoolResponse { Success = false }); } } } }