Issue user disconnected and kick/ban as events and restructure table.

This commit is contained in:
flash 2024-05-24 16:01:11 +00:00
parent 09d5bfef82
commit 2eae48325a
11 changed files with 176 additions and 98 deletions

View file

@ -1,4 +1,5 @@
using SharpChat.Misuzu;
using SharpChat.Events;
using SharpChat.Misuzu;
using SharpChat.SockChat.PacketsS2C;
using System;
using System.Linq;
@ -53,7 +54,7 @@ namespace SharpChat.SockChat.Commands {
}
if(duration <= TimeSpan.Zero) {
ctx.Chat.BanUser(banUser, duration);
ctx.Chat.Events.Dispatch("user:kickban", banUser, new UserKickBanEventData(UserDisconnectReason.Kicked, TimeSpan.Zero));
return;
}
@ -78,7 +79,7 @@ namespace SharpChat.SockChat.Commands {
duration, banReason
);
ctx.Chat.BanUser(banUser, duration);
ctx.Chat.Events.Dispatch("user:kickban", banUser, new UserKickBanEventData(UserDisconnectReason.Kicked, duration));
}).Wait();
}
}

View file

@ -4,11 +4,9 @@ namespace SharpChat.SockChat.PacketsS2C {
public class ForceDisconnectS2CPacket : ISockChatS2CPacket {
private readonly long Expires;
public ForceDisconnectS2CPacket() {
}
public ForceDisconnectS2CPacket(DateTimeOffset expires) {
Expires = expires.Year >= 2100 ? -1 : expires.ToUnixTimeSeconds();
Expires = expires <= DateTimeOffset.UtcNow
? 0 : (expires.Year >= 2100 ? -1 : expires.ToUnixTimeSeconds());
}
public string Pack() {

View file

@ -29,6 +29,11 @@ namespace SharpChat {
// user status should be stored outside of the UserInfo class so we don't need to do this:
UserInfo? userInfo = Users.Get(info.SenderId);
if(!string.IsNullOrWhiteSpace(info.ChannelName))
ChannelsUsers.SetUserLastChannel(info.SenderId, info.ChannelName);
// TODO: should user:connect and user:disconnect be channel agnostic?
switch(info.Type) {
case "user:connect":
SendTo(info.ChannelName, new UserConnectS2CPacket(
@ -43,13 +48,61 @@ namespace SharpChat {
break;
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
));
if(userInfo != null)
UpdateUser(userInfo, status: UserStatus.Offline);
Users.Remove(info.SenderId);
ChannelInfo[] channels = Channels.GetMany(ChannelsUsers.GetUserChannelNames(info.SenderId));
ChannelsUsers.DeleteUser(info.SenderId);
if(channels.Length > 0) {
UserDisconnectS2CPacket udPacket = new(
info.Id,
info.Created,
info.SenderId,
userInfo == null ? SockChatUtility.GetUserName(info) : SockChatUtility.GetUserNameWithStatus(userInfo),
info.Data is UserDisconnectEventData userDisconnect ? userDisconnect.Reason : UserDisconnectReason.Leave
);
foreach(ChannelInfo chan in channels) {
if(chan.IsTemporary && chan.IsOwner(info.SenderId))
RemoveChannel(chan);
else
SendTo(chan, udPacket);
}
}
break;
case "user:kickban":
if(info.Data is not UserKickBanEventData userBaka)
break;
SendTo(info.SenderId, new ForceDisconnectS2CPacket(userBaka.Expires));
ConnectionInfo[] conns = Connections.GetUser(info.SenderId);
foreach(ConnectionInfo conn in conns) {
Connections.Remove(conn);
if(conn is SockChatConnectionInfo scConn)
scConn.Close(1000);
Logger.Write($"<{conn.RemoteEndPoint}> Nuked connection from banned user #{conn.UserId}.");
}
string bakaChannelName = ChannelsUsers.GetUserLastChannel(info.SenderId);
if(!string.IsNullOrWhiteSpace(bakaChannelName))
Events.Dispatch(new ChatEventInfo(
SharpId.Next(),
"user:disconnect",
info.Created,
bakaChannelName,
info.SenderId,
info.SenderName,
info.SenderColour,
info.SenderRank,
info.SenderNickName,
info.SenderPerms,
new UserDisconnectEventData(userBaka.Reason)
));
break;
case "chan:join":
@ -147,7 +200,12 @@ namespace SharpChat {
foreach(UserInfo user in Users.All)
if(!Connections.HasUser(user)) {
HandleDisconnect(user, UserDisconnectReason.TimeOut);
Events.Dispatch(
"user:disconnect",
ChannelsUsers.GetUserLastChannel(user),
user,
new UserDisconnectEventData(UserDisconnectReason.TimeOut)
);
Logger.Write($"Timed out {user} (no more connections).");
}
}
@ -241,25 +299,6 @@ namespace SharpChat {
}
}
public void BanUser(UserInfo user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
if(duration > TimeSpan.Zero) {
DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration;
SendTo(user, new ForceDisconnectS2CPacket(expires));
} else
SendTo(user, new ForceDisconnectS2CPacket());
ConnectionInfo[] conns = Connections.GetUser(user);
foreach(ConnectionInfo conn in conns) {
Connections.Remove(conn);
if(conn is SockChatConnectionInfo scConn)
scConn.Close(1000);
Logger.Write($"<{conn.RemoteEndPoint}> Nuked connection from banned user #{conn.UserId}.");
}
HandleDisconnect(user, reason);
}
public void HandleChannelEventLog(string channelName, Action<ISockChatS2CPacket> handler) {
foreach(ChatEventInfo info in EventStorage.GetChannelEventLog(channelName)) {
switch(info.Type) {
@ -355,21 +394,6 @@ namespace SharpChat {
ChannelsUsers.Join(chan.Name, user.UserId);
}
public void HandleDisconnect(UserInfo user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
UpdateUser(user, status: UserStatus.Offline);
Users.Remove(user.UserId);
ChannelInfo[] channels = GetUserChannels(user);
ChannelsUsers.DeleteUser(user);
foreach(ChannelInfo chan in channels) {
Events.Dispatch("user:disconnect", chan, user, new UserDisconnectEventData(reason));
if(chan.IsTemporary && chan.IsOwner(user))
RemoveChannel(chan);
}
}
public void SwitchChannel(UserInfo user, ChannelInfo chan, string password) {
if(ChannelsUsers.IsUserLastChannel(user, chan)) {
ForceChannel(user);
@ -434,15 +458,22 @@ namespace SharpChat {
}
public void SendTo(UserInfo user, ISockChatS2CPacket packet) {
string data = packet.Pack();
Connections.WithUser(user, conn => {
SendTo(user.UserId, packet.Pack());
}
public void SendTo(long userId, ISockChatS2CPacket packet) {
SendTo(userId, packet.Pack());
}
public void SendTo(long userId, string packet) {
Connections.WithUser(userId, conn => {
if(conn is SockChatConnectionInfo scConn)
scConn.Send(data);
scConn.Send(packet);
});
}
public void SendTo(ChannelInfo channel, ISockChatS2CPacket packet) {
SendTo(channel, packet.Pack());
SendTo(channel.Name, packet.Pack());
}
public void SendTo(ChannelInfo channel, string packet) {

View file

@ -1,5 +1,6 @@
using Fleck;
using SharpChat.Config;
using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.Misuzu;
using SharpChat.SockChat.Commands;
@ -156,7 +157,12 @@ namespace SharpChat.SockChat {
if(!Context.Connections.HasUser(conn.UserId)) {
UserInfo? userInfo = Context.Users.Get(conn.UserId);
if(userInfo != null)
Context.HandleDisconnect(userInfo);
Context.Events.Dispatch(
"user:disconnect",
Context.ChannelsUsers.GetUserLastChannel(userInfo),
userInfo,
new UserDisconnectEventData(UserDisconnectReason.Leave)
);
}
Context.Update();
@ -199,7 +205,7 @@ namespace SharpChat.SockChat {
if(banDuration == TimeSpan.MinValue) {
Context.SendTo(userInfo, new FloodWarningS2CPacket());
} else {
Context.BanUser(userInfo, banDuration, UserDisconnectReason.Flood);
Context.Events.Dispatch("user:kickban", userInfo, new UserKickBanEventData(UserDisconnectReason.Flood, banDuration));
if(banDuration > TimeSpan.Zero)
Misuzu.CreateBanAsync(

View file

@ -26,6 +26,10 @@
OwnerId = ownerId;
}
public bool IsOwner(long userId) {
return OwnerId > 0
&& OwnerId == userId;
}
public bool IsOwner(UserInfo user) {
return OwnerId > 0
&& user != null

View file

@ -4,6 +4,6 @@ using System.Collections.Generic;
namespace SharpChat.EventStorage {
public interface IEventStorage : IChatEventHandler {
ChatEventInfo? GetEvent(long eventId);
IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0);
IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int after = 0);
}
}

View file

@ -33,30 +33,27 @@ namespace SharpChat.EventStorage {
}
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)",
"INSERT INTO sqc_events (event_id, event_type, event_created, event_channel, event_data"
+ ", event_sender, event_sender_name, event_sender_colour, event_sender_rank, event_sender_nick, event_sender_perms"
+ ") VALUES (@id, @type, FROM_UNIXTIME(@created), @channel, @data"
+ ", @senderId, @senderName, @senderColour, @senderRank, @senderNickName, @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)
new MySqlParameter("created", info.Created.ToUnixTimeSeconds()),
new MySqlParameter("channel", string.IsNullOrWhiteSpace(info.ChannelName) ? null : info.ChannelName),
new MySqlParameter("data", JsonSerializer.Serialize(info.Data, info.Data.GetType(), jsonOpts)),
new MySqlParameter("senderId", info.SenderId < 1 ? null : info.SenderId),
new MySqlParameter("senderName", info.SenderName),
new MySqlParameter("senderColour", info.SenderColour.ToMisuzu()),
new MySqlParameter("senderRank", info.SenderRank),
new MySqlParameter("senderNickName", string.IsNullOrWhiteSpace(info.SenderNickName) ? null : info.SenderNickName),
new MySqlParameter("senderPerms", 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",
"UPDATE IGNORE sqc_events SET event_deleted = NOW() WHERE event_id = @id AND event_deleted IS NULL",
new MySqlParameter("id", msgDelete.MessageId)
);
}
@ -64,12 +61,10 @@ namespace SharpChat.EventStorage {
public ChatEventInfo? GetEvent(long eventId) {
try {
using MySqlDataReader? reader = RunQuery(
"SELECT `event_id`, `event_type`, `event_data`, `event_target`"
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`"
+ ", UNIX_TIMESTAMP(`event_created`) AS `event_created`"
+ ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`"
+ " FROM `sqc_events`"
+ " WHERE `event_id` = @id",
"SELECT event_id, event_type, event_channel, event_data"
+ ", event_sender, event_sender_name, event_sender_colour, event_sender_rank, event_sender_nick, event_sender_perms"
+ ", UNIX_TIMESTAMP(event_created) AS event_created, UNIX_TIMESTAMP(event_deleted) AS event_deleted"
+ " FROM sqc_events WHERE event_id = @id",
new MySqlParameter("id", eventId)
);
@ -87,43 +82,42 @@ namespace SharpChat.EventStorage {
}
private static ChatEventInfo ReadEvent(MySqlDataReader reader) {
string eventType = Encoding.ASCII.GetString((byte[])reader["event_type"]);
string eventType = reader.GetString("event_type");
ChatEventData eventData = EventDataTypes.ContainsKey(eventType)
? (ChatEventData)(JsonSerializer.Deserialize((byte[])reader["event_data"], EventDataTypes[eventType]) ?? ChatEventData.EmptyInstance)
? (ChatEventData)(JsonSerializer.Deserialize(reader.GetString("event_data"), EventDataTypes[eventType]) ?? ChatEventData.EmptyInstance)
: ChatEventData.EmptyInstance;
return new ChatEventInfo(
reader.GetInt64("event_id"),
eventType,
DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")),
reader.IsDBNull(reader.GetOrdinal("event_target")) ? string.Empty : Encoding.ASCII.GetString((byte[])reader["event_target"]),
reader.GetString("event_channel"),
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_rank")) ? 0 : reader.GetInt32("event_sender_rank"),
reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? null : reader.GetString("event_sender_nick"),
(UserPermissions)reader.GetInt32("event_sender_perms"),
(UserPermissions)(reader.IsDBNull(reader.GetOrdinal("event_sender_perms")) ? 0 : reader.GetInt32("event_sender_perms")),
eventData
);
}
public IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) {
public IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int after = 0) {
List<ChatEventInfo> events = new();
try {
using MySqlDataReader? reader = RunQuery(
"SELECT `event_id`, `event_type`, `event_data`, `event_target`"
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`"
+ ", UNIX_TIMESTAMP(`event_created`) AS `event_created`"
+ ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`"
+ " FROM `sqc_events`"
+ " WHERE `event_deleted` IS NULL AND (`event_target` = @target OR `event_target` IS NULL)"
+ " AND `event_id` > @offset"
+ " ORDER BY `event_id` DESC"
"SELECT event_id, event_type, event_channel, event_data"
+ ", event_sender, event_sender_name, event_sender_colour, event_sender_rank, event_sender_nick, event_sender_perms"
+ ", UNIX_TIMESTAMP(event_created) AS event_created"
+ " FROM sqc_events WHERE event_deleted IS NULL"
+ " AND (event_channel IS NULL OR event_channel = @channel)"
+ " AND event_id > @after"
+ " ORDER BY event_id DESC"
+ " LIMIT @amount",
new MySqlParameter("target", channelName),
new MySqlParameter("channel", channelName),
new MySqlParameter("amount", amount),
new MySqlParameter("offset", startAt)
new MySqlParameter("after", after)
);
if(reader != null)

View file

@ -32,6 +32,23 @@ namespace SharpChat.EventStorage {
DoMigration("allow_null_target", AllowNullTarget);
DoMigration("update_event_type_names", UpdateEventTypeNames);
DoMigration("deprecate_event_flags", DeprecateEventFlags);
DoMigration("update_collations_and_use_json_type", UpdateCollationsAndUseJsonType);
}
private void UpdateCollationsAndUseJsonType() {
RunCommand("UPDATE sqc_events SET event_target = LOWER(event_target)");
RunCommand("UPDATE sqc_events SET event_sender_nick = NULL WHERE event_sender_nick = ''");
RunCommand(
"ALTER TABLE `sqc_events` COLLATE='utf8mb4_unicode_520_ci',"
+ " CHANGE COLUMN `event_type` `event_type` VARCHAR(255) NOT NULL COLLATE 'ascii_general_ci' AFTER `event_id`,"
+ " CHANGE COLUMN `event_created` `event_created` TIMESTAMP NOT NULL DEFAULT current_timestamp() AFTER `event_type`,"
+ " CHANGE COLUMN `event_deleted` `event_deleted` TIMESTAMP NULL DEFAULT NULL AFTER `event_created`,"
+ " CHANGE COLUMN `event_target` `event_channel` VARCHAR(255) NULL DEFAULT NULL COLLATE 'ascii_general_ci' AFTER `event_deleted`,"
+ " CHANGE COLUMN `event_sender_name` `event_sender_name` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender`,"
+ " CHANGE COLUMN `event_sender_nick` `event_sender_nick` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender_rank`,"
+ " CHANGE COLUMN `event_data` `event_data` JSON NOT NULL DEFAULT '{}' AFTER `event_sender_perms`,"
+ " DROP INDEX `event_target`, ADD INDEX `event_channel` (`event_channel`)"
);
}
private void DeprecateEventFlags() {

View file

@ -17,10 +17,10 @@ namespace SharpChat.EventStorage {
return Events.TryGetValue(eventId.ToString(), out ChatEventInfo? evt) ? evt : null;
}
public IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int startAt = 0) {
public IEnumerable<ChatEventInfo> GetChannelEventLog(string channelName, int amount = 20, int after = 0) {
IEnumerable<ChatEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName);
int start = subset.Count() - startAt - amount;
int start = subset.Count() - after - amount;
if(start < 0) {
amount += start;
start = 0;

View file

@ -0,0 +1,26 @@
using System;
using System.Text.Json.Serialization;
namespace SharpChat.Events {
[ChatEventDataFor("user:kickban")]
public class UserKickBanEventData : ChatEventData {
[JsonPropertyName("reason")]
public UserDisconnectReason Reason { get; }
[JsonPropertyName("expires")]
public DateTimeOffset Expires { get; }
public UserKickBanEventData(
UserDisconnectReason reason,
DateTimeOffset expires
) {
Reason = reason;
Expires = expires;
}
public UserKickBanEventData(
UserDisconnectReason reason,
TimeSpan duration
) : this(reason, DateTimeOffset.UtcNow.Add(duration)) {}
}
}

View file

@ -1,4 +1,5 @@
using System;
using SharpChat.SockChat;
using System;
using System.Collections.Generic;
using System.Linq;