using SameBeat.Domain.Wyjatki; namespace SameBeat.Domain.Encje; /// /// Pojedyncza wiadomosc czatu (ETAP 6). Usunieta to soft-delete -- host "usuwa" wiadomosc z widoku /// widzow (Usunieta=true, znika z GET /api/chat/historia i idzie ChatWiadomoscUsunieta przez SignalR), /// ale wiersz zostaje w bazie do moderacji/audytu, dokladnie jak wzorzec HistoriaMikrofonu z ETAPU 5 /// (zapisujemy fakt biznesowy, nie usuwamy go fizycznie). /// /// NadawcaTozsamoscId wskazuje na UczestnikCzatu (nie na UczestnikOczekujacy z poczekalni) -- to /// "kim jest ta przegladarka na czacie". Nick jest tu zdenormalizowany (kopia z chwili wyslania), zeby /// historia czatu nie zalezala od tego, czy tozsamosc nadawcy pozniej zostanie zbanowana. /// public class WiadomoscCzatu { public const int MaksymalnaDlugoscTresci = 500; public Guid Id { get; private set; } public Guid WydarzenieId { get; private set; } public Guid NadawcaTozsamoscId { get; private set; } public string Nick { get; private set; } = string.Empty; public string Tresc { get; private set; } = string.Empty; public DateTime WyslanoUtc { get; private set; } public bool Usunieta { get; private set; } // Prywatny konstruktor bez parametrow -- wymagany przez EF Core do materializacji encji z bazy. private WiadomoscCzatu() { } private WiadomoscCzatu(Guid id, Guid wydarzenieId, Guid nadawcaTozsamoscId, string nick, string tresc, DateTime wyslanoUtc) { Id = id; WydarzenieId = wydarzenieId; NadawcaTozsamoscId = nadawcaTozsamoscId; Nick = nick; Tresc = tresc; WyslanoUtc = wyslanoUtc; } /// /// Tworzy nowa wiadomosc. Walidacja tresci tutaj chroni przed powstaniem encji w niepoprawnym /// stanie niezaleznie od tego, ktory serwis aplikacji jej uzyje -- SerwisCzatu odpowiada za /// wczesniejsze sprawdzenie rate limitu/wyciszenia/bana, ta metoda pilnuje wylacznie ksztaltu tresci. /// public static WiadomoscCzatu Wyslij(Guid wydarzenieId, Guid nadawcaTozsamoscId, string nick, string tresc) { if (wydarzenieId == Guid.Empty) { throw new NieprawidlowyStanEncjiWyjatek("Wiadomosc czatu musi byc powiazana z konkretnym wydarzeniem."); } if (nadawcaTozsamoscId == Guid.Empty) { throw new NieprawidlowyStanEncjiWyjatek("Wiadomosc czatu musi miec nadawce."); } if (string.IsNullOrWhiteSpace(nick)) { throw new NieprawidlowyStanEncjiWyjatek("Wiadomosc czatu musi miec nick nadawcy."); } var trescOczyszczona = OczyscTresc(tresc); return new WiadomoscCzatu(Guid.NewGuid(), wydarzenieId, nadawcaTozsamoscId, nick.Trim(), trescOczyszczona, DateTime.UtcNow); } private static string OczyscTresc(string tresc) { if (string.IsNullOrWhiteSpace(tresc)) { throw new NieprawidlowyStanEncjiWyjatek("Tresc wiadomosci jest wymagana."); } var trescPrzycieta = tresc.Trim(); if (trescPrzycieta.Length > MaksymalnaDlugoscTresci) { throw new NieprawidlowyStanEncjiWyjatek($"Wiadomosc moze miec maksymalnie {MaksymalnaDlugoscTresci} znakow."); } // Podstawowa higiena wejscia -- ten sam wzorzec co przy Nick w UczestnikOczekujacy z ETAPU 2. // To NIE jest pelne XSS-proofing (dopiero ETAP 7 dolozy CSP), tylko minimalna sanityzacja juz teraz. if (trescPrzycieta.Contains('<') || trescPrzycieta.Contains('>')) { throw new NieprawidlowyStanEncjiWyjatek("Wiadomosc nie moze zawierac znakow '<' ani '>'."); } return trescPrzycieta; } /// Soft-delete -- wiersz zostaje w bazie do moderacji/audytu, tylko znika z widoku widzow. Idempotentne. public void Usun() => Usunieta = true; }