From 5daad52aba6749db6f07adba93a8a62580e7c358 Mon Sep 17 00:00:00 2001 From: flashwave Date: Fri, 24 May 2024 03:44:20 +0000 Subject: [PATCH] Events system overhaul. --- .../Commands/MessageActionCommand.cs | 10 +- .../Commands/MessageBroadcastCommand.cs | 12 +- .../Commands/MessageDeleteCommand.cs | 14 +- .../Commands/MessageWhisperCommand.cs | 11 +- .../PacketsC2S/SendMessageC2SPacketHandler.cs | 10 +- .../PacketsS2C/MessageDeleteS2CPacket.cs | 4 +- SharpChat.SockChat/SockChatContext.cs | 327 +++++++++--------- SharpChat.SockChat/SockChatUtility.cs | 7 +- SharpChatCommon/EventStorage/IEventStorage.cs | 23 +- .../EventStorage/MariaDBEventStorage.cs | 112 +++--- .../EventStorage/StoredEventInfo.cs | 32 -- .../EventStorage/VirtualEventStorage.cs | 44 +-- SharpChatCommon/Events/ChatEventData.cs | 8 + .../Events/ChatEventDataForAttribute.cs | 12 + SharpChatCommon/Events/ChatEventDispatcher.cs | 88 +++++ SharpChatCommon/Events/ChatEventInfo.cs | 45 +++ SharpChatCommon/Events/IChatEvent.cs | 4 - SharpChatCommon/Events/IChatEventHandler.cs | 5 + SharpChatCommon/Events/MessageAddEventData.cs | 17 + SharpChatCommon/Events/MessageCreateEvent.cs | 94 ----- .../Events/MessageDeleteEventData.cs | 13 + .../Events/UserDisconnectEventData.cs | 13 + SharpChatCommon/UserDisconnectReason.cs | 10 +- SharpChatCommon/UserPermissions.cs | 2 +- 24 files changed, 469 insertions(+), 448 deletions(-) delete mode 100644 SharpChatCommon/EventStorage/StoredEventInfo.cs create mode 100644 SharpChatCommon/Events/ChatEventData.cs create mode 100644 SharpChatCommon/Events/ChatEventDataForAttribute.cs create mode 100644 SharpChatCommon/Events/ChatEventDispatcher.cs create mode 100644 SharpChatCommon/Events/ChatEventInfo.cs delete mode 100644 SharpChatCommon/Events/IChatEvent.cs create mode 100644 SharpChatCommon/Events/IChatEventHandler.cs create mode 100644 SharpChatCommon/Events/MessageAddEventData.cs delete mode 100644 SharpChatCommon/Events/MessageCreateEvent.cs create mode 100644 SharpChatCommon/Events/MessageDeleteEventData.cs create mode 100644 SharpChatCommon/Events/UserDisconnectEventData.cs diff --git a/SharpChat.SockChat/Commands/MessageActionCommand.cs b/SharpChat.SockChat/Commands/MessageActionCommand.cs index 5f665c7..dcc71cd 100644 --- a/SharpChat.SockChat/Commands/MessageActionCommand.cs +++ b/SharpChat.SockChat/Commands/MessageActionCommand.cs @@ -1,5 +1,4 @@ using SharpChat.Events; -using System; using System.Linq; namespace SharpChat.SockChat.Commands { @@ -17,14 +16,7 @@ namespace SharpChat.SockChat.Commands { if(string.IsNullOrWhiteSpace(actionStr)) return; - ctx.Chat.DispatchEvent(new MessageCreateEvent( - SharpId.Next(), - ctx.Channel, - ctx.User, - DateTimeOffset.Now, - actionStr, - false, true, false - )); + ctx.Chat.Events.Dispatch("msg:add", ctx.Channel, ctx.User, new MessageAddEventData(actionStr, true)); } } } diff --git a/SharpChat.SockChat/Commands/MessageBroadcastCommand.cs b/SharpChat.SockChat/Commands/MessageBroadcastCommand.cs index 5870c2d..de73673 100644 --- a/SharpChat.SockChat/Commands/MessageBroadcastCommand.cs +++ b/SharpChat.SockChat/Commands/MessageBroadcastCommand.cs @@ -1,6 +1,5 @@ using SharpChat.Events; using SharpChat.SockChat.PacketsS2C; -using System; namespace SharpChat.SockChat.Commands { public class MessageBroadcastCommand : ISockChatClientCommand { @@ -15,14 +14,11 @@ namespace SharpChat.SockChat.Commands { return; } - ctx.Chat.DispatchEvent(new MessageCreateEvent( - SharpId.Next(), - string.Empty, + ctx.Chat.Events.Dispatch( + "msg:add", ctx.User, - DateTimeOffset.Now, - string.Join(' ', ctx.Args), - false, false, true - )); + new MessageAddEventData(string.Join(' ', ctx.Args)) + ); } } } diff --git a/SharpChat.SockChat/Commands/MessageDeleteCommand.cs b/SharpChat.SockChat/Commands/MessageDeleteCommand.cs index 553d497..ff2b2cb 100644 --- a/SharpChat.SockChat/Commands/MessageDeleteCommand.cs +++ b/SharpChat.SockChat/Commands/MessageDeleteCommand.cs @@ -1,4 +1,4 @@ -using SharpChat.EventStorage; +using SharpChat.Events; using SharpChat.SockChat.PacketsS2C; using System.Linq; @@ -21,20 +21,22 @@ namespace SharpChat.SockChat.Commands { string? firstArg = ctx.Args.FirstOrDefault(); - if(string.IsNullOrWhiteSpace(firstArg) || !firstArg.All(char.IsDigit) || !long.TryParse(firstArg, out long delSeqId)) { + if(string.IsNullOrWhiteSpace(firstArg) || !firstArg.All(char.IsDigit) || !long.TryParse(firstArg, out long eventId)) { ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket()); return; } - StoredEventInfo? delMsg = ctx.Chat.Events.GetEvent(delSeqId); + ChatEventInfo? eventInfo = ctx.Chat.EventStorage.GetEvent(eventId); - if(delMsg == null || delMsg.Sender?.Rank > ctx.User.Rank || (!deleteAnyMessage && delMsg.Sender?.UserId != ctx.User.UserId)) { + if(eventInfo == null + || !eventInfo.Type.Equals("msg:add") + || eventInfo.SenderRank > ctx.User.Rank + || (!deleteAnyMessage && eventInfo.SenderId != ctx.User.UserId)) { ctx.Chat.SendTo(ctx.User, new MessageDeleteNotAllowedErrorS2CPacket()); return; } - ctx.Chat.Events.RemoveEvent(delMsg); - ctx.Chat.Send(new MessageDeleteS2CPacket(delMsg.Id)); + ctx.Chat.Events.Dispatch("msg:delete", eventInfo.ChannelName, ctx.User, new MessageDeleteEventData(eventInfo.Id.ToString())); } } } diff --git a/SharpChat.SockChat/Commands/MessageWhisperCommand.cs b/SharpChat.SockChat/Commands/MessageWhisperCommand.cs index 7180968..5ef2f7c 100644 --- a/SharpChat.SockChat/Commands/MessageWhisperCommand.cs +++ b/SharpChat.SockChat/Commands/MessageWhisperCommand.cs @@ -1,6 +1,5 @@ using SharpChat.Events; using SharpChat.SockChat.PacketsS2C; -using System; using System.Linq; namespace SharpChat.SockChat.Commands { @@ -28,14 +27,12 @@ namespace SharpChat.SockChat.Commands { if(whisperUser == ctx.User) return; - ctx.Chat.DispatchEvent(new MessageCreateEvent( - SharpId.Next(), + ctx.Chat.Events.Dispatch( + "msg:add", UserInfo.GetDMChannelName(ctx.User, whisperUser), ctx.User, - DateTimeOffset.Now, - string.Join(' ', ctx.Args.Skip(1)), - true, false, false - )); + new MessageAddEventData(string.Join(' ', ctx.Args.Skip(1))) + ); } } } diff --git a/SharpChat.SockChat/PacketsC2S/SendMessageC2SPacketHandler.cs b/SharpChat.SockChat/PacketsC2S/SendMessageC2SPacketHandler.cs index 9a02e3c..1960980 100644 --- a/SharpChat.SockChat/PacketsC2S/SendMessageC2SPacketHandler.cs +++ b/SharpChat.SockChat/PacketsC2S/SendMessageC2SPacketHandler.cs @@ -1,7 +1,6 @@ using SharpChat.Config; using SharpChat.Events; using SharpChat.SockChat.Commands; -using System; using System.Collections.Generic; using System.Linq; @@ -80,14 +79,7 @@ namespace SharpChat.SockChat.PacketsC2S { } } - ctx.Chat.DispatchEvent(new MessageCreateEvent( - SharpId.Next(), - channelInfo, - user, - DateTimeOffset.Now, - messageText, - false, false, false - )); + ctx.Chat.Events.Dispatch("msg:add", channelInfo, user, new MessageAddEventData(messageText)); } finally { ctx.Chat.ContextAccess.Release(); } diff --git a/SharpChat.SockChat/PacketsS2C/MessageDeleteS2CPacket.cs b/SharpChat.SockChat/PacketsS2C/MessageDeleteS2CPacket.cs index e61d024..05cba05 100644 --- a/SharpChat.SockChat/PacketsS2C/MessageDeleteS2CPacket.cs +++ b/SharpChat.SockChat/PacketsS2C/MessageDeleteS2CPacket.cs @@ -1,8 +1,8 @@ namespace SharpChat.SockChat.PacketsS2C { public class MessageDeleteS2CPacket : ISockChatS2CPacket { - private readonly long DeletedMessageId; + private readonly string DeletedMessageId; - public MessageDeleteS2CPacket(long deletedMessageId) { + public MessageDeleteS2CPacket(string deletedMessageId) { DeletedMessageId = deletedMessageId; } diff --git a/SharpChat.SockChat/SockChatContext.cs b/SharpChat.SockChat/SockChatContext.cs index e5723e2..38eaaef 100644 --- a/SharpChat.SockChat/SockChatContext.cs +++ b/SharpChat.SockChat/SockChatContext.cs @@ -5,79 +5,133 @@ using SharpChat.SockChat.PacketsS2C; using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading; namespace SharpChat { - public class SockChatContext { + public class SockChatContext : IChatEventHandler { public readonly SemaphoreSlim ContextAccess = new(1, 1); public ChannelsContext Channels { get; } = new(); public ConnectionsContext Connections { get; } = new(); public UsersContext Users { get; } = new(); - public IEventStorage Events { get; } + public IEventStorage EventStorage { get; } + public ChatEventDispatcher Events { get; } = new(); public ChannelsUsersContext ChannelsUsers { get; } = new(); public Dictionary UserRateLimiters { get; } = new(); public SockChatContext(IEventStorage evtStore) { - Events = evtStore; + EventStorage = evtStore; + Events.Subscribe(evtStore); + Events.Subscribe(this); } - public void DispatchEvent(IChatEvent eventInfo) { - if(eventInfo is MessageCreateEvent mce) { - if(mce.IsBroadcast) { - Send(new MessageBroadcastS2CPacket(mce.MessageId, mce.MessageCreated, mce.MessageText)); - } else if(mce.IsPrivate) { - // The channel name returned by GetDMChannelName should not be exposed to the user, instead @ should be displayed - // e.g. nook sees @Arysil and Arysil sees @nook + public void HandleEvent(ChatEventInfo info) { + // user status should be stored outside of the UserInfo class so we don't need to do this: + UserInfo? userInfo = Users.Get(info.SenderId); - // this entire routine is garbage, channels should probably in the db - if(mce.ChannelName?.StartsWith("@") != true) - return; + switch(info.Type) { + case "user:connect": + SendTo(info.ChannelName, new UserConnectS2CPacket( + info.Id, + info.Created, + info.SenderId, + userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo), + info.SenderColour, + info.SenderRank, + info.SenderPerms + )); + break; - long[] targetIds = mce.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1).ToArray(); - if(targetIds.Length != 2) - return; + case "user:disconnect": + SendTo(info.ChannelName, new UserDisconnectS2CPacket( + info.Id, + info.Created, + info.SenderId, + userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo), + info.Data is UserDisconnectEventData userDisconnect ? userDisconnect.Reason : UserDisconnectReason.Leave + )); + break; - UserInfo[] users = Users.GetMany(targetIds); - UserInfo? target = users.FirstOrDefault(u => u.UserId != mce.SenderId); - if(target == null) - return; + case "chan:join": + SendTo(info.ChannelName, new UserChannelJoinS2CPacket( + info.SenderId, + userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo), + info.SenderColour, + info.SenderRank, + info.SenderPerms + )); + break; - foreach(UserInfo user in users) - SendTo(user, new MessageAddS2CPacket( - mce.MessageId, - DateTimeOffset.Now, - mce.SenderId, - mce.SenderId == user.UserId ? $"{SockChatUtility.GetUserName(target)} {mce.MessageText}" : mce.MessageText, - mce.IsAction, - true - )); - } else { - ChannelInfo? channel = Channels.Get(mce.ChannelName, SockChatUtility.SanitiseChannelName); - if(channel != null) - SendTo(channel, new MessageAddS2CPacket( - mce.MessageId, - DateTimeOffset.Now, - mce.SenderId, - mce.MessageText, - mce.IsAction, - false - )); - } + case "chan:leave": + SendTo(info.ChannelName, new UserChannelLeaveS2CPacket(info.SenderId)); + break; - object msgData = mce.IsAction - ? new { text = mce.MessageText, action = true } - : new { text = mce.MessageText }; + case "msg:delete": + if(info.Data is not MessageDeleteEventData msgDelete) + break; - Events.AddEvent( - mce.MessageId, - "msg:add", - mce.IsBroadcast ? null : mce.ChannelName, - mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms, - msgData - ); - return; + MessageDeleteS2CPacket msgDelPacket = new(msgDelete.MessageId); + + if(info.IsBroadcast) { + Send(msgDelPacket); + } else if(info.ChannelName.StartsWith('@')) { + long[] targetIds = info.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1).ToArray(); + if(targetIds.Length != 2) + return; + + UserInfo[] users = Users.GetMany(targetIds); + UserInfo? target = users.FirstOrDefault(u => u.UserId != info.SenderId); + if(target == null) + return; + + foreach(UserInfo user in users) + SendTo(user, msgDelPacket); + } else { + SendTo(info.ChannelName, msgDelPacket); + } + break; + + case "msg:add": + if(info.Data is not MessageAddEventData msgAdd) + break; + + if(info.IsBroadcast) { + Send(new MessageBroadcastS2CPacket(info.Id, info.Created, msgAdd.Text)); + } else if(info.ChannelName.StartsWith('@')) { + // The channel name returned by GetDMChannelName should not be exposed to the user, instead @ should be displayed + // e.g. nook sees @Arysil and Arysil sees @nook + + long[] targetIds = info.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1).ToArray(); + if(targetIds.Length != 2) + return; + + UserInfo[] users = Users.GetMany(targetIds); + UserInfo? target = users.FirstOrDefault(u => u.UserId != info.SenderId); + if(target == null) + return; + + foreach(UserInfo user in users) + SendTo(user, new MessageAddS2CPacket( + info.Id, + DateTimeOffset.Now, + info.SenderId, + info.SenderId == user.UserId ? $"{SockChatUtility.GetUserName(target)} {msgAdd.Text}" : msgAdd.Text, + msgAdd.IsAction, + true + )); + } else { + ChannelInfo? channel = Channels.Get(info.ChannelName, SockChatUtility.SanitiseChannelName); + if(channel != null) + SendTo(channel, new MessageAddS2CPacket( + info.Id, + DateTimeOffset.Now, + info.SenderId, + msgAdd.Text, + msgAdd.IsAction, + false + )); + } + break; } } @@ -207,105 +261,81 @@ namespace SharpChat { } public void HandleChannelEventLog(string channelName, Action handler) { - foreach(StoredEventInfo msg in Events.GetChannelEventLog(channelName)) { - ISockChatS2CPacket packet; + foreach(ChatEventInfo info in EventStorage.GetChannelEventLog(channelName)) { + ISockChatS2CPacket? packet; - switch(msg.Type) { + switch(info.Type) { case "msg:add": string maText = string.Empty; - if(msg.Data.RootElement.TryGetProperty("text", out JsonElement maTextElem)) - maText = maTextElem.ToString(); - bool maAction = false; - if(msg.Data.RootElement.TryGetProperty("action", out JsonElement maIsActionElem)) - maAction = maIsActionElem.ValueKind == JsonValueKind.True; + + if(info.Data is MessageAddEventData messageAdd) { + maText = messageAdd.Text; + maAction = messageAdd.IsAction; + } packet = new MessageAddLogS2CPacket( - msg.Id, - msg.Created, - msg.Sender?.UserId ?? -1, - msg.Sender == null ? "ChatBot" : SockChatUtility.GetUserName(msg.Sender), - msg.Sender?.Colour ?? Colour.None, - msg.Sender?.Rank ?? 0, - msg.Sender?.Permissions ?? 0, + info.Id, + info.Created, + info.SenderId, + info.SenderName, + info.SenderColour, + info.SenderRank, + info.SenderPerms, maText, maAction, - msg.ChannelName?.StartsWith('@') == true, - msg.ChannelName == null, + info.ChannelName.StartsWith('@'), + info.IsBroadcast, false ); break; case "user:connect": packet = new UserConnectLogS2CPacket( - msg.Id, - msg.Created, - msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender) + info.Id, + info.Created, + SockChatUtility.GetUserName(info) ); break; case "user:disconnect": - UserDisconnectReason udReason = 0; - if(msg.Data.RootElement.TryGetProperty("reason", out JsonElement udElem) && udElem.TryGetByte(out byte udReasonRaw)) - udReason = (UserDisconnectReason)udReasonRaw; - packet = new UserDisconnectLogS2CPacket( - msg.Id, - msg.Created, - msg.Sender == null ? string.Empty : SockChatUtility.GetUserNameWithStatus(msg.Sender), - udReason + info.Id, + info.Created, + SockChatUtility.GetUserName(info), + info.Data is UserDisconnectEventData userDisconnect ? userDisconnect.Reason : UserDisconnectReason.Leave ); break; case "chan:join": packet = new UserChannelJoinLogS2CPacket( - msg.Id, - msg.Created, - msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender) + info.Id, + info.Created, + SockChatUtility.GetUserName(info) ); break; case "chan:leave": packet = new UserChannelLeaveLogS2CPacket( - msg.Id, - msg.Created, - msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender) + info.Id, + info.Created, + SockChatUtility.GetUserName(info) ); break; default: - throw new Exception($"Unsupported backlog type: {msg.Type}"); + packet = null; + break; } - handler(packet); + if(packet != null) + handler(packet); } } public void HandleJoin(UserInfo user, ChannelInfo chan, SockChatConnectionInfo conn, int maxMsgLength) { - if(!ChannelsUsers.Has(chan, user)) { - long eventId = SharpId.Next(); - SendTo(chan, new UserConnectS2CPacket( - eventId, - DateTimeOffset.UtcNow, - user.UserId, - SockChatUtility.GetUserNameWithStatus(user), - user.Colour, - user.Rank, - user.Permissions - )); - Events.AddEvent( - eventId, - "user:connect", - chan.Name, - user.UserId, - user.UserName, - user.Colour, - user.Rank, - user.NickName, - user.Permissions, - null - ); - } + if(!ChannelsUsers.Has(chan, user)) + Events.Dispatch("user:connect", chan, user); conn.Send(new AuthSuccessS2CPacket( user.UserId, @@ -347,26 +377,7 @@ namespace SharpChat { ChannelsUsers.DeleteUser(user); foreach(ChannelInfo chan in channels) { - long eventId = SharpId.Next(); - SendTo(chan, new UserDisconnectS2CPacket( - eventId, - DateTimeOffset.UtcNow, - user.UserId, - SockChatUtility.GetUserNameWithStatus(user), - reason - )); - Events.AddEvent( - eventId, - "user:disconnect", - chan.Name, - user.UserId, - user.UserName, - user.Colour, - user.Rank, - user.NickName, - user.Permissions, - new { reason = (int)reason } - ); + Events.Dispatch("user:disconnect", chan, user, new UserDisconnectEventData(reason)); if(chan.IsTemporary && chan.IsOwner(user)) RemoveChannel(chan); @@ -397,45 +408,13 @@ namespace SharpChat { } public void ForceChannelSwitch(UserInfo user, ChannelInfo chan) { + DateTimeOffset now = DateTimeOffset.UtcNow; ChannelInfo? oldChan = Channels.Get(ChannelsUsers.GetUserLastChannel(user)); - if(oldChan != null) { - SendTo(oldChan, new UserChannelLeaveS2CPacket(user.UserId)); - Events.AddEvent( - SharpId.Next(), - "chan:leave", - oldChan.Name, - user.UserId, - user.UserName, - user.Colour, - user.Rank, - user.NickName, - user.Permissions, - null - ); - } - - SendTo(chan, new UserChannelJoinS2CPacket( - user.UserId, - SockChatUtility.GetUserNameWithStatus(user), - user.Colour, - user.Rank, - user.Permissions - )); - if(oldChan != null) - Events.AddEvent( - SharpId.Next(), - "chan:join", - oldChan.Name, - user.UserId, - user.UserName, - user.Colour, - user.Rank, - user.NickName, - user.Permissions, - null - ); + Events.Dispatch("chan:leave", now, oldChan, user); + + Events.Dispatch("chan:join", now, chan, user); SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.ClearMode.MessagesUsers)); SendTo(user, new UsersPopulateS2CPacket(GetChannelUsers(chan).Except(new[] { user }).Select( @@ -481,7 +460,15 @@ namespace SharpChat { } public void SendTo(ChannelInfo channel, string packet) { - long[] userIds = ChannelsUsers.GetChannelUserIds(channel); + SendTo(channel.Name, packet); + } + + public void SendTo(string channelName, ISockChatS2CPacket packet) { + SendTo(channelName, packet.Pack()); + } + + public void SendTo(string channelName, string packet) { + long[] userIds = ChannelsUsers.GetChannelUserIds(channelName); foreach(long userId in userIds) Connections.WithUser(userId, conn => { if(conn is SockChatConnectionInfo scConn) diff --git a/SharpChat.SockChat/SockChatUtility.cs b/SharpChat.SockChat/SockChatUtility.cs index d6fe317..ac6c4d4 100644 --- a/SharpChat.SockChat/SockChatUtility.cs +++ b/SharpChat.SockChat/SockChatUtility.cs @@ -1,4 +1,5 @@ -using System; +using SharpChat.Events; +using System; using System.Text.RegularExpressions; namespace SharpChat.SockChat { @@ -37,6 +38,10 @@ namespace SharpChat.SockChat { return name; } + public static string GetUserName(ChatEventInfo info) { + return string.IsNullOrWhiteSpace(info.SenderNickName) ? info.SenderName : $"~{info.SenderNickName}"; + } + public static (string, UsersContext.NameTarget) ExplodeUserName(string name) { UsersContext.NameTarget target = UsersContext.NameTarget.UserName; diff --git a/SharpChatCommon/EventStorage/IEventStorage.cs b/SharpChatCommon/EventStorage/IEventStorage.cs index 1fea29d..38d3171 100644 --- a/SharpChatCommon/EventStorage/IEventStorage.cs +++ b/SharpChatCommon/EventStorage/IEventStorage.cs @@ -1,22 +1,9 @@ -using System.Collections.Generic; +using SharpChat.Events; +using System.Collections.Generic; namespace SharpChat.EventStorage { - public interface IEventStorage { - void AddEvent( - long id, - string type, - string? channelName, - long senderId, - string? senderName, - Colour senderColour, - int senderRank, - string? senderNick, - UserPermissions senderPerms, - object? data = null - ); - - void RemoveEvent(StoredEventInfo evt); - StoredEventInfo? GetEvent(long seqId); - IEnumerable GetChannelEventLog(string channelName, int amount = 20, int startAt = 0); + public interface IEventStorage : IChatEventHandler { + ChatEventInfo? GetEvent(long eventId); + IEnumerable GetChannelEventLog(string channelName, int amount = 20, int startAt = 0); } } diff --git a/SharpChatCommon/EventStorage/MariaDBEventStorage.cs b/SharpChatCommon/EventStorage/MariaDBEventStorage.cs index cb7e5e2..57a0a32 100644 --- a/SharpChatCommon/EventStorage/MariaDBEventStorage.cs +++ b/SharpChatCommon/EventStorage/MariaDBEventStorage.cs @@ -1,6 +1,8 @@ using MySqlConnector; +using SharpChat.Events; using System; using System.Collections.Generic; +using System.Reflection; using System.Text; using System.Text.Json; @@ -8,41 +10,58 @@ namespace SharpChat.EventStorage { public partial class MariaDBEventStorage : IEventStorage { private string ConnectionString { get; } + private readonly JsonSerializerOptions jsonOpts = new() { + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault, + }; + + private static readonly Dictionary EventDataTypes = new(); + + static MariaDBEventStorage() { + foreach(Type type in Assembly.GetExecutingAssembly().GetTypes()) { + ChatEventDataForAttribute? forAttr = type.GetCustomAttribute(); + if(forAttr == null) + continue; + if(EventDataTypes.ContainsKey(forAttr.EventName)) + throw new InvalidOperationException($"Attempted to register more than one data type for {forAttr.EventName}!"); + + EventDataTypes.Add(forAttr.EventName, type); + } + } + public MariaDBEventStorage(string connString) { ConnectionString = connString; } - public void AddEvent( - long id, - string type, - string? channelName, - long senderId, - string? senderName, - Colour senderColour, - int senderRank, - string? senderNick, - UserPermissions senderPerms, - object? data = null - ) { + public void HandleEvent(ChatEventInfo info) { + MySqlParameter dataParam = new("data", MySqlDbType.Blob) { + Value = JsonSerializer.SerializeToUtf8Bytes(info.Data, info.Data.GetType(), jsonOpts) + }; + RunCommand( "INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_data`" + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`)" + " VALUES (@id, NOW(), @type, @target, @data" + ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms)", - new MySqlParameter("id", id), - new MySqlParameter("type", type), - new MySqlParameter("target", string.IsNullOrWhiteSpace(channelName) ? null : channelName), - new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)), - new MySqlParameter("sender", senderId < 1 ? null : senderId), - new MySqlParameter("sender_name", senderName), - new MySqlParameter("sender_colour", senderColour.ToMisuzu()), - new MySqlParameter("sender_rank", senderRank), - new MySqlParameter("sender_nick", string.IsNullOrWhiteSpace(senderNick) ? null : senderNick), - new MySqlParameter("sender_perms", senderPerms) + new MySqlParameter("id", info.Id), + new MySqlParameter("type", info.Type), + new MySqlParameter("target", string.IsNullOrWhiteSpace(info.ChannelName) ? null : info.ChannelName), + dataParam, + new MySqlParameter("sender", info.SenderId < 1 ? null : info.SenderId), + new MySqlParameter("sender_name", info.SenderName), + new MySqlParameter("sender_colour", info.SenderColour.ToMisuzu()), + new MySqlParameter("sender_rank", info.SenderRank), + new MySqlParameter("sender_nick", info.SenderNickName), + new MySqlParameter("sender_perms", info.SenderPerms) ); + + if(info.Type.Equals("msg:delete") && info.Data is MessageDeleteEventData msgDelete) + RunCommand( + "UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL", + new MySqlParameter("id", msgDelete.MessageId) + ); } - public StoredEventInfo? GetEvent(long seqId) { + public ChatEventInfo? GetEvent(long eventId) { try { using MySqlDataReader? reader = RunQuery( "SELECT `event_id`, `event_type`, `event_data`, `event_target`" @@ -51,12 +70,12 @@ namespace SharpChat.EventStorage { + ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`" + " FROM `sqc_events`" + " WHERE `event_id` = @id", - new MySqlParameter("id", seqId) + new MySqlParameter("id", eventId) ); if(reader != null) while(reader.Read()) { - StoredEventInfo evt = ReadEvent(reader); + ChatEventInfo evt = ReadEvent(reader); if(evt != null) return evt; } @@ -67,27 +86,29 @@ namespace SharpChat.EventStorage { return null; } - private static StoredEventInfo ReadEvent(MySqlDataReader reader) { - return new StoredEventInfo( + private static ChatEventInfo ReadEvent(MySqlDataReader reader) { + string eventType = Encoding.ASCII.GetString((byte[])reader["event_type"]); + ChatEventData eventData = EventDataTypes.ContainsKey(eventType) + ? (ChatEventData)(JsonSerializer.Deserialize((byte[])reader["event_data"], EventDataTypes[eventType]) ?? ChatEventData.EmptyInstance) + : ChatEventData.EmptyInstance; + + return new ChatEventInfo( reader.GetInt64("event_id"), - Encoding.ASCII.GetString((byte[])reader["event_type"]), - reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : new UserInfo( - reader.GetInt64("event_sender"), - reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"), - Colour.FromMisuzu(reader.GetInt32("event_sender_colour")), - reader.GetInt32("event_sender_rank"), - (UserPermissions)reader.GetInt32("event_sender_perms"), - reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? null : reader.GetString("event_sender_nick") - ), + eventType, DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")), - reader.IsDBNull(reader.GetOrdinal("event_deleted")) ? null : DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_deleted")), - reader.IsDBNull(reader.GetOrdinal("event_target")) ? null : Encoding.ASCII.GetString((byte[])reader["event_target"]), - JsonDocument.Parse(Encoding.ASCII.GetString((byte[])reader["event_data"])) + reader.IsDBNull(reader.GetOrdinal("event_target")) ? string.Empty : Encoding.ASCII.GetString((byte[])reader["event_target"]), + reader.IsDBNull(reader.GetOrdinal("event_sender")) ? -1 : reader.GetInt64("event_sender"), + reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"), + reader.IsDBNull(reader.GetOrdinal("event_sender_colour")) ? Colour.None : Colour.FromMisuzu(reader.GetInt32("event_sender_colour")), + reader.GetInt32("event_sender_rank"), + reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? null : reader.GetString("event_sender_nick"), + (UserPermissions)reader.GetInt32("event_sender_perms"), + eventData ); } - public IEnumerable GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) { - List events = new(); + public IEnumerable GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) { + List events = new(); try { using MySqlDataReader? reader = RunQuery( @@ -107,7 +128,7 @@ namespace SharpChat.EventStorage { if(reader != null) while(reader.Read()) { - StoredEventInfo evt = ReadEvent(reader); + ChatEventInfo evt = ReadEvent(reader); if(evt != null) events.Add(evt); } @@ -119,12 +140,5 @@ namespace SharpChat.EventStorage { return events; } - - public void RemoveEvent(StoredEventInfo evt) { - RunCommand( - "UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL", - new MySqlParameter("id", evt.Id) - ); - } } } diff --git a/SharpChatCommon/EventStorage/StoredEventInfo.cs b/SharpChatCommon/EventStorage/StoredEventInfo.cs deleted file mode 100644 index 378e731..0000000 --- a/SharpChatCommon/EventStorage/StoredEventInfo.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Text.Json; - -namespace SharpChat.EventStorage { - public class StoredEventInfo { - public long Id { get; set; } - public string Type { get; set; } - public UserInfo? Sender { get; set; } - public DateTimeOffset Created { get; set; } - public DateTimeOffset? Deleted { get; set; } - public string? ChannelName { get; set; } - public JsonDocument Data { get; set; } - - public StoredEventInfo( - long id, - string type, - UserInfo? sender, - DateTimeOffset created, - DateTimeOffset? deleted, - string? channelName, - JsonDocument data - ) { - Id = id; - Type = type; - Sender = sender; - Created = created; - Deleted = deleted; - ChannelName = channelName; - Data = data; - } - } -} diff --git a/SharpChatCommon/EventStorage/VirtualEventStorage.cs b/SharpChatCommon/EventStorage/VirtualEventStorage.cs index 2bae155..58f7ac7 100644 --- a/SharpChatCommon/EventStorage/VirtualEventStorage.cs +++ b/SharpChatCommon/EventStorage/VirtualEventStorage.cs @@ -1,46 +1,24 @@ -using System; +using SharpChat.Events; using System.Collections.Generic; using System.Linq; -using System.Text.Json; namespace SharpChat.EventStorage { public class VirtualEventStorage : IEventStorage { - private readonly Dictionary Events = new(); + private readonly Dictionary Events = new(); - public void AddEvent( - long id, - string type, - string? channelName, - long senderId, - string? senderName, - Colour senderColour, - int senderRank, - string? senderNick, - UserPermissions senderPerms, - object? data = null - ) { - // VES is meant as an emergency fallback but this is something else - JsonDocument hack = JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data)); - Events.Add(id, new(id, type, senderId < 1 ? null : new UserInfo( - senderId, - senderName ?? string.Empty, - senderColour, - senderRank, - senderPerms, - senderNick - ), DateTimeOffset.Now, null, channelName, hack)); + public void HandleEvent(ChatEventInfo info) { + Events.Add(info.Id.ToString(), info); + + if(info.Type.Equals("msg:delete") && info.Data is MessageDeleteEventData msgDelete) + Events.Remove(msgDelete.MessageId); } - public StoredEventInfo? GetEvent(long seqId) { - return Events.TryGetValue(seqId, out StoredEventInfo? evt) ? evt : null; + public ChatEventInfo? GetEvent(long eventId) { + return Events.TryGetValue(eventId.ToString(), out ChatEventInfo? evt) ? evt : null; } - public void RemoveEvent(StoredEventInfo evt) { - Events.Remove(evt.Id); - } - - public IEnumerable GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) { - IEnumerable subset = Events.Values.Where(ev => ev.ChannelName == channelName); + public IEnumerable GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) { + IEnumerable subset = Events.Values.Where(ev => ev.ChannelName == channelName); int start = subset.Count() - startAt - amount; if(start < 0) { diff --git a/SharpChatCommon/Events/ChatEventData.cs b/SharpChatCommon/Events/ChatEventData.cs new file mode 100644 index 0000000..06e82cf --- /dev/null +++ b/SharpChatCommon/Events/ChatEventData.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace SharpChat.Events { + public class ChatEventData { + [JsonIgnore] + public static readonly ChatEventData EmptyInstance = new(); + } +} diff --git a/SharpChatCommon/Events/ChatEventDataForAttribute.cs b/SharpChatCommon/Events/ChatEventDataForAttribute.cs new file mode 100644 index 0000000..8075c61 --- /dev/null +++ b/SharpChatCommon/Events/ChatEventDataForAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace SharpChat.Events { + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class ChatEventDataForAttribute : Attribute { + public string EventName { get; } + + public ChatEventDataForAttribute(string eventName) { + EventName = eventName; + } + } +} diff --git a/SharpChatCommon/Events/ChatEventDispatcher.cs b/SharpChatCommon/Events/ChatEventDispatcher.cs new file mode 100644 index 0000000..0d6a909 --- /dev/null +++ b/SharpChatCommon/Events/ChatEventDispatcher.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; + +namespace SharpChat.Events { + public class ChatEventDispatcher { + private readonly object HandlersAccess = new(); + private readonly List Handlers = new(); + + public void Subscribe(IChatEventHandler handler) { + lock(HandlersAccess) + if(!Handlers.Contains(handler)) + Handlers.Add(handler); + } + + public void Unsubscribe(IChatEventHandler handler) { + lock(HandlersAccess) + Handlers.Remove(handler); + } + + public ChatEventInfo Dispatch(ChatEventInfo info) { + lock(HandlersAccess) + foreach(IChatEventHandler handler in Handlers) + handler.HandleEvent(info); + + return info; + } + + public ChatEventInfo Dispatch( + string eventType, + DateTimeOffset eventCreated, + ChannelInfo channelInfo, + UserInfo userInfo, + ChatEventData? eventData = null + ) { + return Dispatch(new ChatEventInfo( + SharpId.Next(), + eventType, + eventCreated, + channelInfo.Name, + userInfo.UserId, + userInfo.UserName, + userInfo.Colour, + userInfo.Rank, + userInfo.NickName, + userInfo.Permissions, + eventData + )); + } + + public ChatEventInfo Dispatch( + string eventType, + string channelName, + UserInfo userInfo, + ChatEventData? eventData = null + ) { + return Dispatch(new ChatEventInfo( + SharpId.Next(), + eventType, + DateTimeOffset.UtcNow, + channelName, + userInfo.UserId, + userInfo.UserName, + userInfo.Colour, + userInfo.Rank, + userInfo.NickName, + userInfo.Permissions, + eventData + )); + } + + public ChatEventInfo Dispatch( + string eventType, + ChannelInfo channelInfo, + UserInfo userInfo, + ChatEventData? eventData = null + ) { + return Dispatch(eventType, channelInfo.Name, userInfo, eventData); + } + + public ChatEventInfo Dispatch( + string eventType, + UserInfo userInfo, + ChatEventData? eventData = null + ) { + return Dispatch(eventType, string.Empty, userInfo, eventData); + } + } +} diff --git a/SharpChatCommon/Events/ChatEventInfo.cs b/SharpChatCommon/Events/ChatEventInfo.cs new file mode 100644 index 0000000..7d8ad22 --- /dev/null +++ b/SharpChatCommon/Events/ChatEventInfo.cs @@ -0,0 +1,45 @@ +using System; + +namespace SharpChat.Events { + public class ChatEventInfo { + public long Id { get; } + public string Type { get; } + public DateTimeOffset Created { get; } + public string ChannelName { get; } + public long SenderId { get; } + public string SenderName { get; } + public Colour SenderColour { get; } + public int SenderRank { get; } + public string? SenderNickName { get; } + public UserPermissions SenderPerms { get; } + public ChatEventData Data { get; } + + public bool IsBroadcast => string.IsNullOrWhiteSpace(ChannelName); + + public ChatEventInfo( + long id, + string type, + DateTimeOffset created, + string channelName, + long senderId, + string senderName, + Colour senderColour, + int senderRank, + string? senderNickName, + UserPermissions senderPerms, + ChatEventData? data = null + ) { + Id = id; + Type = type; + Created = created; + ChannelName = channelName; + SenderId = senderId; + SenderName = senderName; + SenderColour = senderColour; + SenderRank = senderRank; + SenderNickName = senderNickName; + SenderPerms = senderPerms; + Data = data ?? ChatEventData.EmptyInstance; + } + } +} diff --git a/SharpChatCommon/Events/IChatEvent.cs b/SharpChatCommon/Events/IChatEvent.cs deleted file mode 100644 index 318e6f8..0000000 --- a/SharpChatCommon/Events/IChatEvent.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace SharpChat.Events { - public interface IChatEvent { - } -} diff --git a/SharpChatCommon/Events/IChatEventHandler.cs b/SharpChatCommon/Events/IChatEventHandler.cs new file mode 100644 index 0000000..f7bb752 --- /dev/null +++ b/SharpChatCommon/Events/IChatEventHandler.cs @@ -0,0 +1,5 @@ +namespace SharpChat.Events { + public interface IChatEventHandler { + void HandleEvent(ChatEventInfo info); + } +} diff --git a/SharpChatCommon/Events/MessageAddEventData.cs b/SharpChatCommon/Events/MessageAddEventData.cs new file mode 100644 index 0000000..973c909 --- /dev/null +++ b/SharpChatCommon/Events/MessageAddEventData.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace SharpChat.Events { + [ChatEventDataFor("msg:add")] + public class MessageAddEventData : ChatEventData { + [JsonPropertyName("text")] + public string Text { get; } + + [JsonPropertyName("action")] + public bool IsAction { get; } + + public MessageAddEventData(string text, bool isAction = false) { + Text = text; + IsAction = isAction; + } + } +} diff --git a/SharpChatCommon/Events/MessageCreateEvent.cs b/SharpChatCommon/Events/MessageCreateEvent.cs deleted file mode 100644 index 9cc3ee2..0000000 --- a/SharpChatCommon/Events/MessageCreateEvent.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; - -namespace SharpChat.Events { - public class MessageCreateEvent : IChatEvent { - public long MessageId { get; } - public string? ChannelName { get; } - public long SenderId { get; } - public string? SenderName { get; } - public Colour SenderColour { get; } - public int SenderRank { get; } - public string? SenderNickName { get; } - public UserPermissions SenderPerms { get; } - public DateTimeOffset MessageCreated { get; } - public string MessageText { get; } - public bool IsPrivate { get; } - public bool IsAction { get; } - public bool IsBroadcast { get; } - - public MessageCreateEvent( - long msgId, - string? channelName, - long senderId, - string? senderName, - Colour senderColour, - int senderRank, - string? senderNickName, - UserPermissions senderPerms, - DateTimeOffset msgCreated, - string msgText, - bool isPrivate, - bool isAction, - bool isBroadcast - ) { - MessageId = msgId; - ChannelName = channelName; - SenderId = senderId; - SenderName = senderName; - SenderColour = senderColour; - SenderRank = senderRank; - SenderNickName = senderNickName; - SenderPerms = senderPerms; - MessageCreated = msgCreated; - MessageText = msgText; - IsPrivate = isPrivate; - IsAction = isAction; - IsBroadcast = isBroadcast; - } - - public MessageCreateEvent( - long msgId, - string? channelName, - UserInfo? sender, - DateTimeOffset msgCreated, - string msgText, - bool isPrivate, - bool isAction, - bool isBroadcast - ) : this( - msgId, - channelName, - sender?.UserId ?? -1, - sender?.UserName ?? null, - sender?.Colour ?? Colour.None, - sender?.Rank ?? 0, - sender?.NickName ?? null, - sender?.Permissions ?? 0, - msgCreated, - msgText, - isPrivate, - isAction, - isBroadcast - ) { } - - public MessageCreateEvent( - long msgId, - ChannelInfo channel, - UserInfo sender, - DateTimeOffset msgCreated, - string msgText, - bool isPrivate, - bool isAction, - bool isBroadcast - ) : this( - msgId, - channel?.Name ?? null, - sender, - msgCreated, - msgText, - isPrivate, - isAction, - isBroadcast - ) { } - } -} diff --git a/SharpChatCommon/Events/MessageDeleteEventData.cs b/SharpChatCommon/Events/MessageDeleteEventData.cs new file mode 100644 index 0000000..5361640 --- /dev/null +++ b/SharpChatCommon/Events/MessageDeleteEventData.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace SharpChat.Events { + [ChatEventDataFor("msg:delete")] + public class MessageDeleteEventData : ChatEventData { + [JsonPropertyName("msg")] + public string MessageId { get; } + + public MessageDeleteEventData(string messageId) { + MessageId = messageId; + } + } +} diff --git a/SharpChatCommon/Events/UserDisconnectEventData.cs b/SharpChatCommon/Events/UserDisconnectEventData.cs new file mode 100644 index 0000000..4b71adc --- /dev/null +++ b/SharpChatCommon/Events/UserDisconnectEventData.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace SharpChat.Events { + [ChatEventDataFor("user:disconnect")] + public class UserDisconnectEventData : ChatEventData { + [JsonPropertyName("reason")] + public UserDisconnectReason Reason { get; } + + public UserDisconnectEventData(UserDisconnectReason reason) { + Reason = reason; + } + } +} diff --git a/SharpChatCommon/UserDisconnectReason.cs b/SharpChatCommon/UserDisconnectReason.cs index af6f595..bf7efbd 100644 --- a/SharpChatCommon/UserDisconnectReason.cs +++ b/SharpChatCommon/UserDisconnectReason.cs @@ -1,8 +1,8 @@ namespace SharpChat { - public enum UserDisconnectReason { - Leave, - TimeOut, - Kicked, - Flood, + public enum UserDisconnectReason : int { + Leave = 0, + TimeOut = 1, + Kicked = 2, + Flood = 3, } } diff --git a/SharpChatCommon/UserPermissions.cs b/SharpChatCommon/UserPermissions.cs index 76ee177..7003d81 100644 --- a/SharpChatCommon/UserPermissions.cs +++ b/SharpChatCommon/UserPermissions.cs @@ -5,7 +5,7 @@ namespace SharpChat { public enum UserPermissions : int { KickUser = 0x00000001, BanUser = 0x00000002, - //SilenceUser = 0x00000004, + //SilenceUser = 0x00000004, Broadcast = 0x00000008, SetOwnNickname = 0x00000010, SetOthersNickname = 0x00000020,