Структура
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Auth.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: изменился статус пользователя (онлайн/офлайн).
|
||||
/// </summary>
|
||||
public sealed record UserStatusChangedDomainEvent(Guid UserId, bool IsOnline, DateTime LastSeen) : IDomainEvent;
|
||||
@@ -0,0 +1,7 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Conversations.Application.Abstractions;
|
||||
|
||||
public interface IChatsUnitOfWork : IUnitOfWork
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Contracts.Conversations.Application.Abstractions;
|
||||
|
||||
public interface IUserStatusService
|
||||
{
|
||||
bool IsUserOnline(string userId);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Conversations.Domain;
|
||||
|
||||
public sealed record ChatCreatedDomainEvent(Chat Chat) : IDomainEvent;
|
||||
public sealed record ChatMemberAddedDomainEvent(Guid ChatId, Guid UserId) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Тип чата: личный или групповой.
|
||||
/// </summary>
|
||||
public enum ChatType
|
||||
{
|
||||
Personal,
|
||||
Group,
|
||||
Favorites
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Роль участника в чате.
|
||||
/// </summary>
|
||||
public static class ChatRole
|
||||
{
|
||||
public const string Owner = "owner";
|
||||
public const string Admin = "admin";
|
||||
public const string Member = "member";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сущность чата (Агрегат).
|
||||
/// </summary>
|
||||
public sealed class Chat : AggregateRoot<Guid>
|
||||
{
|
||||
public ChatType Type { get; private set; }
|
||||
public string? Name { get; private set; }
|
||||
public string? Description { get; private set; }
|
||||
public string? Avatar { get; private set; }
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public long LastMessageSequenceId { get; private set; }
|
||||
|
||||
private readonly List<ChatMember> _members = new();
|
||||
public IReadOnlyCollection<ChatMember> Members => _members.AsReadOnly();
|
||||
|
||||
private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null) : base(id)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
Avatar = avatar;
|
||||
Description = description;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public static Chat CreatePersonal()
|
||||
{
|
||||
var chat = new Chat(Guid.NewGuid(), ChatType.Personal, null, null);
|
||||
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
|
||||
return chat;
|
||||
}
|
||||
|
||||
public static Chat CreateGroup(string name, string? avatar = null)
|
||||
{
|
||||
var chat = new Chat(Guid.NewGuid(), ChatType.Group, name, avatar);
|
||||
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
|
||||
return chat;
|
||||
}
|
||||
|
||||
public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null)
|
||||
{
|
||||
var chat = new Chat(Guid.NewGuid(), type, name, avatar, description);
|
||||
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
|
||||
return chat;
|
||||
}
|
||||
|
||||
public void AddMember(Guid userId, string role = "member")
|
||||
{
|
||||
if (_members.Any(m => m.UserId == userId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_members.Add(new ChatMember(Id, userId, role));
|
||||
RaiseDomainEvent(new ChatMemberAddedDomainEvent(Id, userId));
|
||||
}
|
||||
|
||||
public void RemoveMember(Guid userId)
|
||||
{
|
||||
var member = _members.FirstOrDefault(m => m.UserId == userId);
|
||||
if (member != null)
|
||||
{
|
||||
_members.Remove(member);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateName(string name) => Name = name;
|
||||
|
||||
public void UpdateDescription(string? description) => Description = description;
|
||||
|
||||
public void UpdateAvatar(string? avatarUrl) => Avatar = avatarUrl;
|
||||
|
||||
public long IncrementSequenceId()
|
||||
{
|
||||
return ++LastMessageSequenceId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Участник чата.
|
||||
/// </summary>
|
||||
public sealed class ChatMember : Entity<Guid>
|
||||
{
|
||||
public Guid ChatId { get; private set; }
|
||||
public Guid UserId { get; private set; }
|
||||
public string Role { get; private set; }
|
||||
public DateTime JoinedAt { get; private set; }
|
||||
public bool IsPinned { get; private set; }
|
||||
public bool IsMuted { get; private set; }
|
||||
|
||||
public Guid? LastReadMessageId { get; private set; }
|
||||
public long LastReadSequenceId { get; private set; }
|
||||
public Guid? LastDeliveredMessageId { get; private set; }
|
||||
|
||||
private ChatMember() : base(Guid.Empty) { Role = "member"; }
|
||||
|
||||
internal ChatMember(Guid chatId, Guid userId, string role) : base(Guid.NewGuid())
|
||||
{
|
||||
ChatId = chatId;
|
||||
UserId = userId;
|
||||
Role = role;
|
||||
JoinedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void TogglePin() => IsPinned = !IsPinned;
|
||||
|
||||
public void UpdateReadCursor(Guid messageId, long sequenceId)
|
||||
{
|
||||
if (sequenceId > LastReadSequenceId)
|
||||
{
|
||||
LastReadMessageId = messageId;
|
||||
LastReadSequenceId = sequenceId;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDeliveredCursor(Guid messageId)
|
||||
{
|
||||
LastDeliveredMessageId = messageId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Conversations.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Сущность папки для группировки чатов.
|
||||
/// </summary>
|
||||
public sealed class Folder : AggregateRoot<Guid>
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
public string? Icon { get; private set; }
|
||||
public bool IsDefault { get; private set; }
|
||||
public FolderType Type { get; private set; }
|
||||
|
||||
public Folder(Guid id, string name, string? icon = null, bool isDefault = false, FolderType type = FolderType.Custom)
|
||||
: base(id)
|
||||
{
|
||||
Name = name;
|
||||
Icon = icon;
|
||||
IsDefault = isDefault;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public void Update(string name, string? icon)
|
||||
{
|
||||
if (IsDefault) throw new InvalidOperationException("Cannot rename default folders.");
|
||||
Name = name;
|
||||
Icon = icon;
|
||||
}
|
||||
}
|
||||
|
||||
public enum FolderType
|
||||
{
|
||||
All,
|
||||
New,
|
||||
Muted,
|
||||
Custom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Настройки конкретного чата для конкретного пользователя.
|
||||
/// </summary>
|
||||
public sealed class UserChatSettings : Entity<Guid>
|
||||
{
|
||||
public Guid UserId { get; private set; }
|
||||
public Guid ChatId { get; private set; }
|
||||
|
||||
private readonly List<Guid> _folderIds = new();
|
||||
public IReadOnlyCollection<Guid> FolderIds => _folderIds.AsReadOnly();
|
||||
|
||||
public bool IsMuted { get; private set; }
|
||||
|
||||
private UserChatSettings() : base(Guid.NewGuid()) { }
|
||||
|
||||
public UserChatSettings(Guid userId, Guid chatId) : base(Guid.NewGuid())
|
||||
{
|
||||
UserId = userId;
|
||||
ChatId = chatId;
|
||||
}
|
||||
|
||||
public static UserChatSettings Create(Guid userId, Guid chatId) => new(userId, chatId);
|
||||
|
||||
public void AddToFolder(Guid folderId)
|
||||
{
|
||||
if (!_folderIds.Contains(folderId)) _folderIds.Add(folderId);
|
||||
}
|
||||
|
||||
public void RemoveFromFolder(Guid folderId)
|
||||
{
|
||||
_folderIds.Remove(folderId);
|
||||
}
|
||||
|
||||
public void SetMute(bool isMuted) => IsMuted = isMuted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Глобальные настройки папок пользователя.
|
||||
/// </summary>
|
||||
public sealed class UserFolderSettings : AggregateRoot<Guid>
|
||||
{
|
||||
public Guid UserId { get; private set; }
|
||||
public List<Guid> HiddenDefaultFolderIds { get; private set; } = new();
|
||||
public List<Guid> CustomFolderIds { get; private set; } = new();
|
||||
|
||||
public UserFolderSettings(Guid userId) : base(Guid.NewGuid())
|
||||
{
|
||||
UserId = userId;
|
||||
}
|
||||
|
||||
public void HideFolder(Guid folderId)
|
||||
{
|
||||
if (!HiddenDefaultFolderIds.Contains(folderId)) HiddenDefaultFolderIds.Add(folderId);
|
||||
}
|
||||
|
||||
public void ShowFolder(Guid folderId)
|
||||
{
|
||||
HiddenDefaultFolderIds.Remove(folderId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Knot.Contracts.Conversations.Domain;
|
||||
|
||||
namespace Knot.Contracts.Conversations.Domain;
|
||||
|
||||
public interface IChatRepository
|
||||
{
|
||||
void Add(Chat chat);
|
||||
void Update(Chat chat);
|
||||
void Remove(Chat chat);
|
||||
Task<Chat?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
Task<Chat?> GetFavoritesAsync(Guid userId, CancellationToken cancellationToken);
|
||||
Task<List<Chat>> GetUserChatsAsync(Guid userId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IFolderRepository
|
||||
{
|
||||
void Add(Folder folder);
|
||||
void Update(Folder folder);
|
||||
void Remove(Folder folder);
|
||||
Task<Folder?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
Task<List<Folder>> GetUserFoldersAsync(Guid userId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IUserChatSettingsRepository
|
||||
{
|
||||
void Add(UserChatSettings settings);
|
||||
void Update(UserChatSettings settings);
|
||||
void Remove(UserChatSettings settings);
|
||||
void RemoveRange(IEnumerable<UserChatSettings> settings);
|
||||
Task<UserChatSettings?> GetAsync(Guid userId, Guid chatId, CancellationToken cancellationToken);
|
||||
Task<List<UserChatSettings>> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IUserFolderSettingsRepository
|
||||
{
|
||||
Task<UserFolderSettings?> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken);
|
||||
Task UpdateAsync(UserFolderSettings settings, CancellationToken cancellationToken);
|
||||
Task RemoveByUserIdAsync(Guid userId, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\Messaging\Knot.Contracts.Messaging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Application.Abstractions;
|
||||
|
||||
public interface IChatAccessProvider
|
||||
{
|
||||
Task<List<Guid>> GetValidChatIdsForUserAsync(Guid userId, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Knot.Contracts.Messaging.Application.Abstractions;
|
||||
|
||||
public interface IMessageNotifier
|
||||
{
|
||||
Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Application.Abstractions;
|
||||
|
||||
public interface IMessageRepository
|
||||
{
|
||||
void Add(Message message);
|
||||
Task<Message?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
Task<List<Message>> GetChatMessagesAsync(Guid chatId, int limit, int offset, CancellationToken cancellationToken);
|
||||
Task<Message?> GetLatestChatMessageAsync(Guid chatId, CancellationToken cancellationToken);
|
||||
Task<List<Message>> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken);
|
||||
|
||||
Task<List<Message>> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken);
|
||||
Task<Message?> GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken);
|
||||
|
||||
Task UpdateAsync(Message message, CancellationToken cancellationToken);
|
||||
Task<List<Message>> GetAllMessagesAsync(CancellationToken cancellationToken);
|
||||
Task DeleteChatMessagesAsync(Guid chatId, CancellationToken cancellationToken);
|
||||
Task DeleteUserMessagesAsync(Guid userId, CancellationToken cancellationToken);
|
||||
Task RemoveAsync(Guid id, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Application.Abstractions;
|
||||
|
||||
public record UserStats(int MessageCount, long StorageSize);
|
||||
|
||||
public interface IUserStatsService
|
||||
{
|
||||
Task<Dictionary<Guid, UserStats>> GetStatsForUsersAsync(IEnumerable<Guid> userIds, CancellationToken ct = default);
|
||||
Task<long> GetTotalStorageSizeAsync(CancellationToken ct = default);
|
||||
Task<int> GetCountOrphanedMessagesAsync(HashSet<Guid> activeChatIds, CancellationToken ct = default);
|
||||
Task<long> GetOrphanedMediaSizeAsync(HashSet<string> validFileIds, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Запись о том, что конкретный пользователь удалил у себя сообщение.
|
||||
/// </summary>
|
||||
public sealed class DeletedMessage : Entity<Guid>
|
||||
{
|
||||
public Guid MessageId { get; private set; }
|
||||
public Guid UserId { get; private set; }
|
||||
|
||||
internal DeletedMessage(Guid messageId, Guid userId) : base(Guid.NewGuid())
|
||||
{
|
||||
MessageId = messageId;
|
||||
UserId = userId;
|
||||
}
|
||||
|
||||
private DeletedMessage() : base(Guid.Empty) { }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Knot.Contracts.Messaging.Domain;
|
||||
|
||||
public interface IMessageReactionRepository
|
||||
{
|
||||
Task AddAsync(MessageReaction reaction, CancellationToken cancellationToken);
|
||||
Task RemoveAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken);
|
||||
Task<List<MessageReaction>> GetReactionsForMessageAsync(Guid messageId, CancellationToken cancellationToken);
|
||||
Task<List<MessageReaction>> GetReactionsForMessagesAsync(IEnumerable<Guid> messageIds, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Knot.Contracts.Messaging.Domain;
|
||||
|
||||
public enum MediaType
|
||||
{
|
||||
Image,
|
||||
Video,
|
||||
Voice,
|
||||
File
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Domain;
|
||||
|
||||
public abstract class Message : AggregateRoot<Guid>
|
||||
{
|
||||
public Guid ChatId { get; protected set; }
|
||||
public Guid SenderId { get; protected set; }
|
||||
public DateTime CreatedAt { get; protected set; }
|
||||
public long SequenceId { get; protected set; }
|
||||
public void SetSequenceId(long sequenceId) => SequenceId = sequenceId;
|
||||
public Guid? ReplyToId { get; protected set; }
|
||||
public Guid? ForwardedFromId { get; protected set; }
|
||||
public MessageState State { get; protected set; }
|
||||
public abstract string Type { get; }
|
||||
public abstract string? Content { get; protected set; }
|
||||
public virtual string? Quote { get; protected set; } = null;
|
||||
public virtual Guid? StoryId => null;
|
||||
public virtual string? StoryMediaUrl => null;
|
||||
public virtual string? StoryMediaType => null;
|
||||
public virtual IReadOnlyCollection<Media> Media => Array.Empty<Media>();
|
||||
public bool IsEdited => HasState(MessageState.IsEdited);
|
||||
public bool IsDeleted => HasState(MessageState.IsDeleted);
|
||||
public bool IsDeletedForUser(Guid userId) => _deletedFor.Exists(d => d.UserId == userId);
|
||||
protected List<DeletedMessage> _deletedFor = new();
|
||||
public IReadOnlyCollection<DeletedMessage> DeletedFor => _deletedFor.AsReadOnly();
|
||||
protected Message() : base(Guid.Empty) { }
|
||||
protected Message(Guid id, Guid chatId, Guid senderId, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported) : base(id)
|
||||
{
|
||||
ChatId = chatId;
|
||||
SenderId = senderId;
|
||||
ReplyToId = replyToId;
|
||||
ForwardedFromId = forwardedFromId;
|
||||
CreatedAt = createdAt;
|
||||
if (isImported) AddState(MessageState.IsImported);
|
||||
}
|
||||
public void AddState(MessageState state) => State |= state;
|
||||
public void RemoveState(MessageState state) => State &= ~state;
|
||||
public bool HasState(MessageState state) => (State & state) == state;
|
||||
public virtual void Delete() => AddState(MessageState.IsDeleted);
|
||||
public virtual void Edit(string newContent) { Content = newContent; AddState(MessageState.IsEdited); }
|
||||
public void DeleteForUser(Guid userId) { if (!_deletedFor.Exists(x => x.UserId == userId)) _deletedFor.Add(new DeletedMessage(Id, userId)); }
|
||||
}
|
||||
|
||||
public class Media
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string? Url { get; set; }
|
||||
public string? ThumbnailUrl { get; set; }
|
||||
public long? Size { get; set; }
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public string? FileId { get; set; }
|
||||
public string? Filename { get; set; }
|
||||
public string? Duration { get; set; }
|
||||
}
|
||||
|
||||
public class TextMessage : Message
|
||||
{
|
||||
public override string Type => "text";
|
||||
public override string? Content { get; protected set; }
|
||||
public TextMessage() : base() { }
|
||||
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
|
||||
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) => Content = content;
|
||||
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, string? quote, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
|
||||
: this(id, chatId, senderId, content, replyToId, forwardedFromId, createdAt, isImported) => Quote = quote;
|
||||
}
|
||||
|
||||
public class MediaMessage : Message
|
||||
{
|
||||
public override string Type => MediaType.ToString().ToLower();
|
||||
public override string? Content { get; protected set; }
|
||||
public string? Caption { get => Content; private set => Content = value; }
|
||||
public MediaType MediaType { get; private set; }
|
||||
private List<Media> _media = new();
|
||||
public override IReadOnlyCollection<Media> Media => _media.AsReadOnly();
|
||||
private MediaMessage() : base() { MediaType = MediaType.File; }
|
||||
public MediaMessage(Guid id, Guid chatId, Guid senderId, MediaType mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
||||
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) { MediaType = mediaType; Caption = caption; }
|
||||
public MediaMessage(Guid id, Guid chatId, Guid senderId, string mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
||||
: this(id, chatId, senderId, Enum.TryParse<MediaType>(mediaType, true, out var mt) ? mt : MediaType.File, caption, replyToId, forwardedFromId, createdAt, isImported) { }
|
||||
public void AddMedia(string type, string url, string? filename, long? size) => _media.Add(new Media { Type = type, Url = url, FileId = filename, Size = size });
|
||||
public override void Edit(string newCaption) => base.Edit(newCaption);
|
||||
public override void Delete() { Caption = null; base.Delete(); }
|
||||
}
|
||||
|
||||
public class StoryMessage : Message
|
||||
{
|
||||
public override string Type => "story";
|
||||
public override string? Content { get; protected set; }
|
||||
public override Guid? StoryId { get; }
|
||||
public string? InternalStoryMediaUrl { get; private set; }
|
||||
public override string? StoryMediaUrl => InternalStoryMediaUrl;
|
||||
public override string? StoryMediaType { get; }
|
||||
public StoryMessage() : base() { }
|
||||
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, DateTime createdAt)
|
||||
: base(id, chatId, senderId, null, null, createdAt, false) { StoryId = storyId; InternalStoryMediaUrl = storyMediaUrl; StoryMediaType = storyMediaType; }
|
||||
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, string? content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
||||
: this(id, chatId, senderId, storyId, storyMediaUrl, storyMediaType, createdAt) { Content = content; ReplyToId = replyToId; ForwardedFromId = forwardedFromId; if (isImported) AddState(MessageState.IsImported); }
|
||||
}
|
||||
|
||||
public class PollMessage : Message
|
||||
{
|
||||
public override string Type => "poll";
|
||||
public override string? Content { get; protected set; }
|
||||
public List<PollOption> Options { get; } = new();
|
||||
public List<PollVote> Votes { get; } = new();
|
||||
public bool IsMultipleChoice { get; set; }
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public bool IsClosed { get; set; }
|
||||
public PollMessage() : base() { }
|
||||
public PollMessage(Guid id, Guid chatId, Guid senderId, string? question, List<string>? options, bool isAnonymous, bool isMultiple, DateTime? expiresAt, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
|
||||
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported)
|
||||
{
|
||||
Content = question ?? "Poll";
|
||||
if (options != null) foreach (var opt in options) Options.Add(new PollOption { Text = opt });
|
||||
IsMultipleChoice = isMultiple;
|
||||
ExpiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
public class PollOption { public string Text { get; set; } = string.Empty; public int VoteCount { get; set; } }
|
||||
public class PollVote { public Guid OptionIndex { get; set; } public Guid UserId { get; set; } public DateTime VotedAt { get; set; } }
|
||||
@@ -0,0 +1,28 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: сообщение отправлено.
|
||||
/// </summary>
|
||||
public sealed record MessageSentDomainEvent(Guid MessageId, Guid ChatId, Guid SenderId, string? Content) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: сообщение удалено.
|
||||
/// </summary>
|
||||
public sealed record MessageDeletedDomainEvent(Guid MessageId, Guid ChatId) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: сообщение отредактировано.
|
||||
/// </summary>
|
||||
public sealed record MessageEditedDomainEvent(Guid MessageId, Guid ChatId, string NewContent) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: реакция добавлена.
|
||||
/// </summary>
|
||||
public sealed record MessageReactionAddedDomainEvent(Guid MessageId, Guid ChatId, Guid UserId, string Emoji) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: реакция удалена.
|
||||
/// </summary>
|
||||
public sealed record MessageReactionRemovedDomainEvent(Guid MessageId, Guid ChatId, Guid UserId, string Emoji) : IDomainEvent;
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Domain;
|
||||
|
||||
public sealed class MessageReaction : AggregateRoot<Guid>
|
||||
{
|
||||
public Guid MessageId { get; private set; }
|
||||
public Guid UserId { get; private set; }
|
||||
public string Emoji { get; private set; }
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
private MessageReaction() : base(Guid.Empty)
|
||||
{
|
||||
Emoji = default!;
|
||||
}
|
||||
|
||||
public MessageReaction(Guid messageId, Guid userId, string emoji) : base(Guid.NewGuid())
|
||||
{
|
||||
MessageId = messageId;
|
||||
UserId = userId;
|
||||
Emoji = emoji;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Knot.Contracts.Messaging.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Состояние сообщения
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MessageState
|
||||
{
|
||||
None = 0,
|
||||
IsEdited = 1,
|
||||
IsDeleted = 2,
|
||||
IsImported = 4,
|
||||
IsPinned = 8
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Domain;
|
||||
|
||||
public sealed class Reaction : Entity<Guid>
|
||||
{
|
||||
public Guid MessageId { get; private set; }
|
||||
public Guid UserId { get; private set; }
|
||||
public string Emoji { get; private set; }
|
||||
|
||||
internal Reaction(Guid messageId, Guid userId, string emoji) : base(Guid.NewGuid())
|
||||
{
|
||||
MessageId = messageId;
|
||||
UserId = userId;
|
||||
Emoji = emoji;
|
||||
}
|
||||
|
||||
private Reaction() : base(Guid.Empty)
|
||||
{
|
||||
Emoji = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Contracts.Settings.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: системные настройки обновлены.
|
||||
/// </summary>
|
||||
public sealed record SystemSettingsUpdatedDomainEvent(Application.DTOs.SystemSettingsDto Settings) : IDomainEvent;
|
||||
Reference in New Issue
Block a user