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();
}
}