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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<long, RateLimiter> 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 @<Target User> 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 @<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) {
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)

View file

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

View file

@ -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<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0);
public interface IEventStorage : IChatEventHandler {
ChatEventInfo? GetEvent(long eventId);
IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0);
}
}

View file

@ -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<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) {
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<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) {
List<StoredEventInfo> events = new();
public IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) {
List<ChatEventInfo> 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)
);
}
}
}

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.Linq;
using System.Text.Json;
namespace SharpChat.EventStorage {
public class VirtualEventStorage : IEventStorage {
private readonly Dictionary<long, StoredEventInfo> Events = new();
private readonly Dictionary<string, ChatEventInfo> 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<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) {
IEnumerable<StoredEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName);
public IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) {
IEnumerable<ChatEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName);
int start = subset.Count() - startAt - amount;
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 {
public enum UserDisconnectReason {
Leave,
TimeOut,
Kicked,
Flood,
public enum UserDisconnectReason : int {
Leave = 0,
TimeOut = 1,
Kicked = 2,
Flood = 3,
}
}

View file

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