using Microsoft.Extensions.Logging; using SameBeat.Application.DTO; using SameBeat.Application.Interfejsy; using SameBeat.Application.Komendy; using SameBeat.Domain.Encje; using SameBeat.Domain.Wyjatki; namespace SameBeat.Application.Serwisy; /// /// Logika czatu na zywo (ETAP 6). Rate limiting jest liczony z bazy (ostatnia wiadomosc danej /// tozsamosci), nie ze stanu w pamieci procesu -- backend moze kiedys dzialac na wielu instancjach, /// wiec stan w pamieci jednej instancji nie chronilby przed spamem idacym przez inna. /// public sealed class SerwisCzatu : ISerwisCzatu { private const string BladBrakWydarzenia = "Brak aktywnego wydarzenia, sprobuj pozniej."; /// Minimalny odstep miedzy wiadomosciami tej samej tozsamosci czatu. private static readonly TimeSpan OdstepMiedzyWiadomosciami = TimeSpan.FromSeconds(2); /// Ile ostatnich wiadomosci wraca z GET /api/chat/historia. private const int LiczbaWiadomosciHistorii = 50; private readonly IRepozytoriumWydarzen _repozytoriumWydarzen; private readonly IRepozytoriumCzatu _repozytoriumCzatu; private readonly IJednostkaPracy _jednostkaPracy; private readonly ILogger _logger; public SerwisCzatu( IRepozytoriumWydarzen repozytoriumWydarzen, IRepozytoriumCzatu repozytoriumCzatu, IJednostkaPracy jednostkaPracy, ILogger logger) { _repozytoriumWydarzen = repozytoriumWydarzen; _repozytoriumCzatu = repozytoriumCzatu; _jednostkaPracy = jednostkaPracy; _logger = logger; } public async Task> DolaczAsync(DolaczDoCzatuKomenda komenda, CancellationToken token = default) { // "Biezace" wydarzenie = najswiezsze utworzone, ta sama definicja co uzywa juz SerwisPoczekalni/SerwisWydarzen. var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token); if (wydarzenie is null) { _logger.LogInformation("Proba dolaczenia do czatu bez zadnego wydarzenia w bazie."); return WynikOperacji.Niepowodzenie(BladBrakWydarzenia); } UczestnikCzatu uczestnik; try { uczestnik = UczestnikCzatu.Dolacz(wydarzenie.Id, komenda.Nick); } catch (WyjatekDomenowy wyjatek) { return WynikOperacji.Niepowodzenie(wyjatek.Message); } _repozytoriumCzatu.DodajUczestnika(uczestnik); await _jednostkaPracy.ZapiszZmianyAsync(token); _logger.LogInformation("Uczestnik czatu {UczestnikCzatuId} dolaczyl do czatu wydarzenia {WydarzenieId}.", uczestnik.Id, wydarzenie.Id); return WynikOperacji.Ok(new OdpowiedzDolaczeniaDoCzatuDto(uczestnik.Id)); } public async Task> WyslijWiadomoscAsync(WyslijWiadomoscKomenda komenda, CancellationToken token = default) { var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token); if (wydarzenie is null) { return WynikOperacji.Niepowodzenie(BladBrakWydarzenia); } var uczestnik = await _repozytoriumCzatu.ZnajdzUczestnikaPoIdAsync(komenda.UczestnikCzatuId, token); if (uczestnik is null || uczestnik.WydarzenieId != wydarzenie.Id) { return WynikOperacji.Niepowodzenie("Nie znaleziono uczestnika czatu o podanym identyfikatorze."); } if (uczestnik.Zbanowany) { return WynikOperacji.Niepowodzenie("Zostales zbanowany na czacie i nie mozesz pisac wiadomosci."); } if (uczestnik.Wyciszony) { return WynikOperacji.Niepowodzenie("Zostales wyciszony na czacie i nie mozesz teraz pisac wiadomosci."); } // Rate limit liczony z bazy (ostatnia wiadomosc TEJ tozsamosci), nie ze stanu w pamieci -- // patrz komentarz przy klasie. var ostatniaWiadomosc = await _repozytoriumCzatu.PobierzOstatniaWiadomoscUczestnikaAsync(uczestnik.Id, token); if (ostatniaWiadomosc is not null && DateTime.UtcNow - ostatniaWiadomosc.WyslanoUtc < OdstepMiedzyWiadomosciami) { return WynikOperacji.Niepowodzenie("Piszesz za szybko, poczekaj chwile."); } WiadomoscCzatu wiadomosc; try { wiadomosc = WiadomoscCzatu.Wyslij(wydarzenie.Id, uczestnik.Id, uczestnik.Nick, komenda.Tresc); } catch (WyjatekDomenowy wyjatek) { return WynikOperacji.Niepowodzenie(wyjatek.Message); } _repozytoriumCzatu.DodajWiadomosc(wiadomosc); await _jednostkaPracy.ZapiszZmianyAsync(token); return WynikOperacji.Ok( new WiadomoscCzatuHostaDto(wiadomosc.Id, wiadomosc.Nick, wiadomosc.Tresc, wiadomosc.WyslanoUtc, uczestnik.Id)); } public async Task>> PobierzHistorieAsync(CancellationToken token = default) { var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token); if (wydarzenie is null) { // Brak wydarzenia to normalny stan (host jeszcze niczego nie utworzyl) -- pusta lista, nie blad. return WynikOperacji>.Ok(new List()); } var wiadomosci = await _repozytoriumCzatu.PobierzHistorieAsync(wydarzenie.Id, LiczbaWiadomosciHistorii, token); var dto = wiadomosci .Select(w => new WiadomoscCzatuDto(w.Id, w.Nick, w.Tresc, w.WyslanoUtc)) .ToList(); return WynikOperacji>.Ok(dto); } public async Task>> PobierzHistorieDlaHostaAsync(CancellationToken token = default) { var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token); if (wydarzenie is null) { return WynikOperacji>.Ok(new List()); } var wiadomosci = await _repozytoriumCzatu.PobierzHistorieAsync(wydarzenie.Id, LiczbaWiadomosciHistorii, token); var dto = wiadomosci .Select(w => new WiadomoscCzatuHostaDto(w.Id, w.Nick, w.Tresc, w.WyslanoUtc, w.NadawcaTozsamoscId)) .ToList(); return WynikOperacji>.Ok(dto); } public async Task UsunWiadomoscAsync(Guid wiadomoscId, CancellationToken token = default) { var wiadomosc = await _repozytoriumCzatu.ZnajdzWiadomoscPoIdAsync(wiadomoscId, token); if (wiadomosc is null) { return WynikOperacji.Niepowodzenie("Nie znaleziono wiadomosci o podanym identyfikatorze."); } wiadomosc.Usun(); await _jednostkaPracy.ZapiszZmianyAsync(token); return WynikOperacji.Ok(); } public async Task WyciszAsync(Guid uczestnikCzatuId, CancellationToken token = default) { var uczestnik = await _repozytoriumCzatu.ZnajdzUczestnikaPoIdAsync(uczestnikCzatuId, token); if (uczestnik is null) { return WynikOperacji.Niepowodzenie("Nie znaleziono uczestnika czatu o podanym identyfikatorze."); } uczestnik.Wycisz(); await _jednostkaPracy.ZapiszZmianyAsync(token); return WynikOperacji.Ok(); } public async Task OdciszAsync(Guid uczestnikCzatuId, CancellationToken token = default) { var uczestnik = await _repozytoriumCzatu.ZnajdzUczestnikaPoIdAsync(uczestnikCzatuId, token); if (uczestnik is null) { return WynikOperacji.Niepowodzenie("Nie znaleziono uczestnika czatu o podanym identyfikatorze."); } uczestnik.Odcisz(); await _jednostkaPracy.ZapiszZmianyAsync(token); return WynikOperacji.Ok(); } public async Task ZbanujAsync(Guid uczestnikCzatuId, CancellationToken token = default) { var uczestnik = await _repozytoriumCzatu.ZnajdzUczestnikaPoIdAsync(uczestnikCzatuId, token); if (uczestnik is null) { return WynikOperacji.Niepowodzenie("Nie znaleziono uczestnika czatu o podanym identyfikatorze."); } uczestnik.Zbanuj(); await _jednostkaPracy.ZapiszZmianyAsync(token); return WynikOperacji.Ok(); } }