Events system overhaul.

This commit is contained in:
flash 2024-05-24 03:44:20 +00:00
parent 454a460441
commit 5daad52aba
24 changed files with 469 additions and 448 deletions

View file

@ -1,5 +1,4 @@
using SharpChat.Events; using SharpChat.Events;
using System;
using System.Linq; using System.Linq;
namespace SharpChat.SockChat.Commands { namespace SharpChat.SockChat.Commands {
@ -17,14 +16,7 @@ namespace SharpChat.SockChat.Commands {
if(string.IsNullOrWhiteSpace(actionStr)) if(string.IsNullOrWhiteSpace(actionStr))
return; return;
ctx.Chat.DispatchEvent(new MessageCreateEvent( ctx.Chat.Events.Dispatch("msg:add", ctx.Channel, ctx.User, new MessageAddEventData(actionStr, true));
SharpId.Next(),
ctx.Channel,
ctx.User,
DateTimeOffset.Now,
actionStr,
false, true, false
));
} }
} }
} }

View file

@ -1,6 +1,5 @@
using SharpChat.Events; using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C; using SharpChat.SockChat.PacketsS2C;
using System;
namespace SharpChat.SockChat.Commands { namespace SharpChat.SockChat.Commands {
public class MessageBroadcastCommand : ISockChatClientCommand { public class MessageBroadcastCommand : ISockChatClientCommand {
@ -15,14 +14,11 @@ namespace SharpChat.SockChat.Commands {
return; return;
} }
ctx.Chat.DispatchEvent(new MessageCreateEvent( ctx.Chat.Events.Dispatch(
SharpId.Next(), "msg:add",
string.Empty,
ctx.User, ctx.User,
DateTimeOffset.Now, new MessageAddEventData(string.Join(' ', ctx.Args))
string.Join(' ', ctx.Args), );
false, false, true
));
} }
} }
} }

View file

@ -1,4 +1,4 @@
using SharpChat.EventStorage; using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C; using SharpChat.SockChat.PacketsS2C;
using System.Linq; using System.Linq;
@ -21,20 +21,22 @@ namespace SharpChat.SockChat.Commands {
string? firstArg = ctx.Args.FirstOrDefault(); 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()); ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
return; 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()); ctx.Chat.SendTo(ctx.User, new MessageDeleteNotAllowedErrorS2CPacket());
return; return;
} }
ctx.Chat.Events.RemoveEvent(delMsg); ctx.Chat.Events.Dispatch("msg:delete", eventInfo.ChannelName, ctx.User, new MessageDeleteEventData(eventInfo.Id.ToString()));
ctx.Chat.Send(new MessageDeleteS2CPacket(delMsg.Id));
} }
} }
} }

View file

@ -1,6 +1,5 @@
using SharpChat.Events; using SharpChat.Events;
using SharpChat.SockChat.PacketsS2C; using SharpChat.SockChat.PacketsS2C;
using System;
using System.Linq; using System.Linq;
namespace SharpChat.SockChat.Commands { namespace SharpChat.SockChat.Commands {
@ -28,14 +27,12 @@ namespace SharpChat.SockChat.Commands {
if(whisperUser == ctx.User) if(whisperUser == ctx.User)
return; return;
ctx.Chat.DispatchEvent(new MessageCreateEvent( ctx.Chat.Events.Dispatch(
SharpId.Next(), "msg:add",
UserInfo.GetDMChannelName(ctx.User, whisperUser), UserInfo.GetDMChannelName(ctx.User, whisperUser),
ctx.User, ctx.User,
DateTimeOffset.Now, new MessageAddEventData(string.Join(' ', ctx.Args.Skip(1)))
string.Join(' ', ctx.Args.Skip(1)), );
true, false, false
));
} }
} }
} }

View file

@ -1,7 +1,6 @@
using SharpChat.Config; using SharpChat.Config;
using SharpChat.Events; using SharpChat.Events;
using SharpChat.SockChat.Commands; using SharpChat.SockChat.Commands;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -80,14 +79,7 @@ namespace SharpChat.SockChat.PacketsC2S {
} }
} }
ctx.Chat.DispatchEvent(new MessageCreateEvent( ctx.Chat.Events.Dispatch("msg:add", channelInfo, user, new MessageAddEventData(messageText));
SharpId.Next(),
channelInfo,
user,
DateTimeOffset.Now,
messageText,
false, false, false
));
} finally { } finally {
ctx.Chat.ContextAccess.Release(); ctx.Chat.ContextAccess.Release();
} }

View file

@ -1,8 +1,8 @@
namespace SharpChat.SockChat.PacketsS2C { namespace SharpChat.SockChat.PacketsS2C {
public class MessageDeleteS2CPacket : ISockChatS2CPacket { public class MessageDeleteS2CPacket : ISockChatS2CPacket {
private readonly long DeletedMessageId; private readonly string DeletedMessageId;
public MessageDeleteS2CPacket(long deletedMessageId) { public MessageDeleteS2CPacket(string deletedMessageId) {
DeletedMessageId = deletedMessageId; DeletedMessageId = deletedMessageId;
} }

View file

@ -5,79 +5,133 @@ using SharpChat.SockChat.PacketsS2C;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
namespace SharpChat { namespace SharpChat {
public class SockChatContext { public class SockChatContext : IChatEventHandler {
public readonly SemaphoreSlim ContextAccess = new(1, 1); public readonly SemaphoreSlim ContextAccess = new(1, 1);
public ChannelsContext Channels { get; } = new(); public ChannelsContext Channels { get; } = new();
public ConnectionsContext Connections { get; } = new(); public ConnectionsContext Connections { get; } = new();
public UsersContext Users { 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 ChannelsUsersContext ChannelsUsers { get; } = new();
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new(); public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new();
public SockChatContext(IEventStorage evtStore) { public SockChatContext(IEventStorage evtStore) {
Events = evtStore; EventStorage = evtStore;
Events.Subscribe(evtStore);
Events.Subscribe(this);
} }
public void DispatchEvent(IChatEvent eventInfo) { public void HandleEvent(ChatEventInfo info) {
if(eventInfo is MessageCreateEvent mce) { // user status should be stored outside of the UserInfo class so we don't need to do this:
if(mce.IsBroadcast) { UserInfo? userInfo = Users.Get(info.SenderId);
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 @<Target User> should be displayed
// e.g. nook sees @Arysil and Arysil sees @nook
// this entire routine is garbage, channels should probably in the db switch(info.Type) {
if(mce.ChannelName?.StartsWith("@") != true) case "user:connect":
return; 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(); case "user:disconnect":
if(targetIds.Length != 2) SendTo(info.ChannelName, new UserDisconnectS2CPacket(
return; 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); case "chan:join":
UserInfo? target = users.FirstOrDefault(u => u.UserId != mce.SenderId); SendTo(info.ChannelName, new UserChannelJoinS2CPacket(
if(target == null) info.SenderId,
return; userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo),
info.SenderColour,
info.SenderRank,
info.SenderPerms
));
break;
foreach(UserInfo user in users) case "chan:leave":
SendTo(user, new MessageAddS2CPacket( SendTo(info.ChannelName, new UserChannelLeaveS2CPacket(info.SenderId));
mce.MessageId, break;
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
));
}
object msgData = mce.IsAction case "msg:delete":
? new { text = mce.MessageText, action = true } if(info.Data is not MessageDeleteEventData msgDelete)
: new { text = mce.MessageText }; break;
Events.AddEvent( MessageDeleteS2CPacket msgDelPacket = new(msgDelete.MessageId);
mce.MessageId,
"msg:add", if(info.IsBroadcast) {
mce.IsBroadcast ? null : mce.ChannelName, Send(msgDelPacket);
mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms, } else if(info.ChannelName.StartsWith('@')) {
msgData long[] targetIds = info.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1).ToArray();
); if(targetIds.Length != 2)
return; 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 @<Target User> 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<ISockChatS2CPacket> handler) { public void HandleChannelEventLog(string channelName, Action<ISockChatS2CPacket> handler) {
foreach(StoredEventInfo msg in Events.GetChannelEventLog(channelName)) { foreach(ChatEventInfo info in EventStorage.GetChannelEventLog(channelName)) {
ISockChatS2CPacket packet; ISockChatS2CPacket? packet;
switch(msg.Type) { switch(info.Type) {
case "msg:add": case "msg:add":
string maText = string.Empty; string maText = string.Empty;
if(msg.Data.RootElement.TryGetProperty("text", out JsonElement maTextElem))
maText = maTextElem.ToString();
bool maAction = false; 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( packet = new MessageAddLogS2CPacket(
msg.Id, info.Id,
msg.Created, info.Created,
msg.Sender?.UserId ?? -1, info.SenderId,
msg.Sender == null ? "ChatBot" : SockChatUtility.GetUserName(msg.Sender), info.SenderName,
msg.Sender?.Colour ?? Colour.None, info.SenderColour,
msg.Sender?.Rank ?? 0, info.SenderRank,
msg.Sender?.Permissions ?? 0, info.SenderPerms,
maText, maText,
maAction, maAction,
msg.ChannelName?.StartsWith('@') == true, info.ChannelName.StartsWith('@'),
msg.ChannelName == null, info.IsBroadcast,
false false
); );
break; break;
case "user:connect": case "user:connect":
packet = new UserConnectLogS2CPacket( packet = new UserConnectLogS2CPacket(
msg.Id, info.Id,
msg.Created, info.Created,
msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender) SockChatUtility.GetUserName(info)
); );
break; break;
case "user:disconnect": 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( packet = new UserDisconnectLogS2CPacket(
msg.Id, info.Id,
msg.Created, info.Created,
msg.Sender == null ? string.Empty : SockChatUtility.GetUserNameWithStatus(msg.Sender), SockChatUtility.GetUserName(info),
udReason info.Data is UserDisconnectEventData userDisconnect ? userDisconnect.Reason : UserDisconnectReason.Leave
); );
break; break;
case "chan:join": case "chan:join":
packet = new UserChannelJoinLogS2CPacket( packet = new UserChannelJoinLogS2CPacket(
msg.Id, info.Id,
msg.Created, info.Created,
msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender) SockChatUtility.GetUserName(info)
); );
break; break;
case "chan:leave": case "chan:leave":
packet = new UserChannelLeaveLogS2CPacket( packet = new UserChannelLeaveLogS2CPacket(
msg.Id, info.Id,
msg.Created, info.Created,
msg.Sender == null ? string.Empty : SockChatUtility.GetUserName(msg.Sender) SockChatUtility.GetUserName(info)
); );
break; break;
default: 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) { public void HandleJoin(UserInfo user, ChannelInfo chan, SockChatConnectionInfo conn, int maxMsgLength) {
if(!ChannelsUsers.Has(chan, user)) { if(!ChannelsUsers.Has(chan, user))
long eventId = SharpId.Next(); Events.Dispatch("user:connect", chan, user);
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
);
}
conn.Send(new AuthSuccessS2CPacket( conn.Send(new AuthSuccessS2CPacket(
user.UserId, user.UserId,
@ -347,26 +377,7 @@ namespace SharpChat {
ChannelsUsers.DeleteUser(user); ChannelsUsers.DeleteUser(user);
foreach(ChannelInfo chan in channels) { foreach(ChannelInfo chan in channels) {
long eventId = SharpId.Next(); Events.Dispatch("user:disconnect", chan, user, new UserDisconnectEventData(reason));
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 }
);
if(chan.IsTemporary && chan.IsOwner(user)) if(chan.IsTemporary && chan.IsOwner(user))
RemoveChannel(chan); RemoveChannel(chan);
@ -397,45 +408,13 @@ namespace SharpChat {
} }
public void ForceChannelSwitch(UserInfo user, ChannelInfo chan) { public void ForceChannelSwitch(UserInfo user, ChannelInfo chan) {
DateTimeOffset now = DateTimeOffset.UtcNow;
ChannelInfo? oldChan = Channels.Get(ChannelsUsers.GetUserLastChannel(user)); 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) if(oldChan != null)
Events.AddEvent( Events.Dispatch("chan:leave", now, oldChan, user);
SharpId.Next(),
"chan:join", Events.Dispatch("chan:join", now, chan, user);
oldChan.Name,
user.UserId,
user.UserName,
user.Colour,
user.Rank,
user.NickName,
user.Permissions,
null
);
SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.ClearMode.MessagesUsers)); SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.ClearMode.MessagesUsers));
SendTo(user, new UsersPopulateS2CPacket(GetChannelUsers(chan).Except(new[] { user }).Select( SendTo(user, new UsersPopulateS2CPacket(GetChannelUsers(chan).Except(new[] { user }).Select(
@ -481,7 +460,15 @@ namespace SharpChat {
} }
public void SendTo(ChannelInfo channel, string packet) { 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) foreach(long userId in userIds)
Connections.WithUser(userId, conn => { Connections.WithUser(userId, conn => {
if(conn is SockChatConnectionInfo scConn) if(conn is SockChatConnectionInfo scConn)

View file

@ -1,4 +1,5 @@
using System; using SharpChat.Events;
using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SharpChat.SockChat { namespace SharpChat.SockChat {
@ -37,6 +38,10 @@ namespace SharpChat.SockChat {
return name; 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) { public static (string, UsersContext.NameTarget) ExplodeUserName(string name) {
UsersContext.NameTarget target = UsersContext.NameTarget.UserName; UsersContext.NameTarget target = UsersContext.NameTarget.UserName;

View file

@ -1,22 +1,9 @@
using System.Collections.Generic; using SharpChat.Events;
using System.Collections.Generic;
namespace SharpChat.EventStorage { namespace SharpChat.EventStorage {
public interface IEventStorage { public interface IEventStorage : IChatEventHandler {
void AddEvent( ChatEventInfo? GetEvent(long eventId);
long id, IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0);
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<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0);
} }
} }

View file

@ -1,6 +1,8 @@
using MySqlConnector; using MySqlConnector;
using SharpChat.Events;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -8,41 +10,58 @@ namespace SharpChat.EventStorage {
public partial class MariaDBEventStorage : IEventStorage { public partial class MariaDBEventStorage : IEventStorage {
private string ConnectionString { get; } private string ConnectionString { get; }
private readonly JsonSerializerOptions jsonOpts = new() {
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault,
};
private static readonly Dictionary<string, Type> EventDataTypes = new();
static MariaDBEventStorage() {
foreach(Type type in Assembly.GetExecutingAssembly().GetTypes()) {
ChatEventDataForAttribute? forAttr = type.GetCustomAttribute<ChatEventDataForAttribute>();
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) { public MariaDBEventStorage(string connString) {
ConnectionString = connString; ConnectionString = connString;
} }
public void AddEvent( public void HandleEvent(ChatEventInfo info) {
long id, MySqlParameter dataParam = new("data", MySqlDbType.Blob) {
string type, Value = JsonSerializer.SerializeToUtf8Bytes(info.Data, info.Data.GetType(), jsonOpts)
string? channelName, };
long senderId,
string? senderName,
Colour senderColour,
int senderRank,
string? senderNick,
UserPermissions senderPerms,
object? data = null
) {
RunCommand( RunCommand(
"INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_data`" "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`)" + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`)"
+ " VALUES (@id, NOW(), @type, @target, @data" + " VALUES (@id, NOW(), @type, @target, @data"
+ ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms)", + ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms)",
new MySqlParameter("id", id), new MySqlParameter("id", info.Id),
new MySqlParameter("type", type), new MySqlParameter("type", info.Type),
new MySqlParameter("target", string.IsNullOrWhiteSpace(channelName) ? null : channelName), new MySqlParameter("target", string.IsNullOrWhiteSpace(info.ChannelName) ? null : info.ChannelName),
new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)), dataParam,
new MySqlParameter("sender", senderId < 1 ? null : senderId), new MySqlParameter("sender", info.SenderId < 1 ? null : info.SenderId),
new MySqlParameter("sender_name", senderName), new MySqlParameter("sender_name", info.SenderName),
new MySqlParameter("sender_colour", senderColour.ToMisuzu()), new MySqlParameter("sender_colour", info.SenderColour.ToMisuzu()),
new MySqlParameter("sender_rank", senderRank), new MySqlParameter("sender_rank", info.SenderRank),
new MySqlParameter("sender_nick", string.IsNullOrWhiteSpace(senderNick) ? null : senderNick), new MySqlParameter("sender_nick", info.SenderNickName),
new MySqlParameter("sender_perms", senderPerms) 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 { try {
using MySqlDataReader? reader = RunQuery( using MySqlDataReader? reader = RunQuery(
"SELECT `event_id`, `event_type`, `event_data`, `event_target`" "SELECT `event_id`, `event_type`, `event_data`, `event_target`"
@ -51,12 +70,12 @@ namespace SharpChat.EventStorage {
+ ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`" + ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`"
+ " FROM `sqc_events`" + " FROM `sqc_events`"
+ " WHERE `event_id` = @id", + " WHERE `event_id` = @id",
new MySqlParameter("id", seqId) new MySqlParameter("id", eventId)
); );
if(reader != null) if(reader != null)
while(reader.Read()) { while(reader.Read()) {
StoredEventInfo evt = ReadEvent(reader); ChatEventInfo evt = ReadEvent(reader);
if(evt != null) if(evt != null)
return evt; return evt;
} }
@ -67,27 +86,29 @@ namespace SharpChat.EventStorage {
return null; return null;
} }
private static StoredEventInfo ReadEvent(MySqlDataReader reader) { private static ChatEventInfo ReadEvent(MySqlDataReader reader) {
return new StoredEventInfo( 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"), reader.GetInt64("event_id"),
Encoding.ASCII.GetString((byte[])reader["event_type"]), eventType,
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")
),
DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")), 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")) ? string.Empty : Encoding.ASCII.GetString((byte[])reader["event_target"]),
reader.IsDBNull(reader.GetOrdinal("event_target")) ? null : Encoding.ASCII.GetString((byte[])reader["event_target"]), reader.IsDBNull(reader.GetOrdinal("event_sender")) ? -1 : reader.GetInt64("event_sender"),
JsonDocument.Parse(Encoding.ASCII.GetString((byte[])reader["event_data"])) 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<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) { public IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) {
List<StoredEventInfo> events = new(); List<ChatEventInfo> events = new();
try { try {
using MySqlDataReader? reader = RunQuery( using MySqlDataReader? reader = RunQuery(
@ -107,7 +128,7 @@ namespace SharpChat.EventStorage {
if(reader != null) if(reader != null)
while(reader.Read()) { while(reader.Read()) {
StoredEventInfo evt = ReadEvent(reader); ChatEventInfo evt = ReadEvent(reader);
if(evt != null) if(evt != null)
events.Add(evt); events.Add(evt);
} }
@ -119,12 +140,5 @@ namespace SharpChat.EventStorage {
return events; 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)
);
}
} }
} }

View file

@ -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;
}
}
}

View file

@ -1,46 +1,24 @@
using System; using SharpChat.Events;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json;
namespace SharpChat.EventStorage { namespace SharpChat.EventStorage {
public class VirtualEventStorage : IEventStorage { public class VirtualEventStorage : IEventStorage {
private readonly Dictionary<long, StoredEventInfo> Events = new(); private readonly Dictionary<string, ChatEventInfo> Events = new();
public void AddEvent( public void HandleEvent(ChatEventInfo info) {
long id, Events.Add(info.Id.ToString(), info);
string type,
string? channelName, if(info.Type.Equals("msg:delete") && info.Data is MessageDeleteEventData msgDelete)
long senderId, Events.Remove(msgDelete.MessageId);
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 StoredEventInfo? GetEvent(long seqId) { public ChatEventInfo? GetEvent(long eventId) {
return Events.TryGetValue(seqId, out StoredEventInfo? evt) ? evt : null; return Events.TryGetValue(eventId.ToString(), out ChatEventInfo? evt) ? evt : null;
} }
public void RemoveEvent(StoredEventInfo evt) { public IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) {
Events.Remove(evt.Id); IEnumerable<ChatEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName);
}
public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) {
IEnumerable<StoredEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName);
int start = subset.Count() - startAt - amount; int start = subset.Count() - startAt - amount;
if(start < 0) { if(start < 0) {

View file

@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace SharpChat.Events {
public class ChatEventData {
[JsonIgnore]
public static readonly ChatEventData EmptyInstance = new();
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
namespace SharpChat.Events {
public class ChatEventDispatcher {
private readonly object HandlersAccess = new();
private readonly List<IChatEventHandler> 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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -1,4 +0,0 @@
namespace SharpChat.Events {
public interface IChatEvent {
}
}

View file

@ -0,0 +1,5 @@
namespace SharpChat.Events {
public interface IChatEventHandler {
void HandleEvent(ChatEventInfo info);
}
}

View file

@ -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;
}
}
}

View file

@ -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
) { }
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -1,8 +1,8 @@
namespace SharpChat { namespace SharpChat {
public enum UserDisconnectReason { public enum UserDisconnectReason : int {
Leave, Leave = 0,
TimeOut, TimeOut = 1,
Kicked, Kicked = 2,
Flood, Flood = 3,
} }
} }

View file

@ -5,7 +5,7 @@ namespace SharpChat {
public enum UserPermissions : int { public enum UserPermissions : int {
KickUser = 0x00000001, KickUser = 0x00000001,
BanUser = 0x00000002, BanUser = 0x00000002,
//SilenceUser = 0x00000004, //SilenceUser = 0x00000004,
Broadcast = 0x00000008, Broadcast = 0x00000008,
SetOwnNickname = 0x00000010, SetOwnNickname = 0x00000010,
SetOthersNickname = 0x00000020, SetOthersNickname = 0x00000020,