Split MariaDB message storage out into its own library.

This commit is contained in:
flash 2025-04-27 01:54:46 +00:00
parent 8eff4127b5
commit f1d4051fb5
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
23 changed files with 255 additions and 228 deletions

View file

@ -20,10 +20,10 @@ public class Channel(
return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase);
}
public bool IsOwner(User user) {
return string.IsNullOrEmpty(OwnerId)
&& user != null
&& OwnerId == user.UserId;
public bool IsOwner(string userId) {
return !string.IsNullOrEmpty(OwnerId)
&& !string.IsNullOrEmpty(userId)
&& OwnerId == userId;
}
public override int GetHashCode() {

View file

@ -26,7 +26,7 @@ public class DeleteChannelClientCommand : ClientCommand {
return;
}
if(!ctx.User.Permissions.HasFlag(UserPermissions.DeleteChannel) && delChan.IsOwner(ctx.User)) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.DeleteChannel) && delChan.IsOwner(ctx.User.UserId)) {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_DELETE_FAILED, true, delChan.Name));
return;
}

View file

@ -1,4 +1,4 @@
using SharpChat.EventStorage;
using SharpChat.Messages;
using SharpChat.SockChat.S2CPackets;
namespace SharpChat.ClientCommands;
@ -27,14 +27,14 @@ public class DeleteMessageClientCommand : ClientCommand {
return;
}
StoredEventInfo? delMsg = await ctx.Chat.Events.GetEvent(delSeqId);
Message? delMsg = await ctx.Chat.Messages.GetMessage(delSeqId);
if(delMsg?.SenderId is null || delMsg.SenderRank > ctx.User.Rank || (!deleteAnyMessage && delMsg.SenderId != ctx.User.UserId)) {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.MESSAGE_DELETE_ERROR));
return;
}
await ctx.Chat.Events.RemoveEvent(delMsg);
await ctx.Chat.Messages.DeleteMessage(delMsg);
await ctx.Chat.Send(new ChatMessageDeleteS2CPacket(delMsg.Id));
}
}

View file

@ -11,7 +11,7 @@ public class PasswordChannelClientCommand : ClientCommand {
public async Task Dispatch(ClientCommandContext ctx) {
long msgId = ctx.Chat.RandomSnowflake.Next();
if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPassword) || ctx.Channel.IsOwner(ctx.User)) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPassword) || ctx.Channel.IsOwner(ctx.User.UserId)) {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
return;
}

View file

@ -12,7 +12,7 @@ public class RankChannelClientCommand : ClientCommand {
public async Task Dispatch(ClientCommandContext ctx) {
long msgId = ctx.Chat.RandomSnowflake.Next();
if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelMinimumRank) || ctx.Channel.IsOwner(ctx.User)) {
if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelMinimumRank) || ctx.Channel.IsOwner(ctx.User.UserId)) {
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
return;
}

View file

@ -1,5 +1,5 @@
using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.Messages;
using SharpChat.Snowflake;
using SharpChat.SockChat;
using SharpChat.SockChat.S2CPackets;
@ -18,13 +18,13 @@ public class Context {
public HashSet<Channel> Channels { get; } = [];
public HashSet<Connection> Connections { get; } = [];
public HashSet<User> Users { get; } = [];
public EventStorage.EventStorage Events { get; }
public MessageStorage Messages { get; }
public HashSet<ChannelUserAssoc> ChannelUsers { get; } = [];
public Dictionary<string, RateLimiter> UserRateLimiters { get; } = [];
public Dictionary<string, Channel> UserLastChannel { get; } = [];
public Context(EventStorage.EventStorage evtStore) {
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
public Context(MessageStorage msgs) {
Messages = msgs;
RandomSnowflake = new(SnowflakeGenerator);
}
@ -71,14 +71,14 @@ public class Context {
));
}
await Events.AddEvent(
await Messages.LogMessage(
mce.MessageId, "msg:add",
mce.ChannelName,
mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms,
new { text = mce.MessageText },
(mce.IsBroadcast ? StoredEventFlags.Broadcast : 0)
| (mce.IsAction ? StoredEventFlags.Action : 0)
| (mce.IsPrivate ? StoredEventFlags.Private : 0)
(mce.IsBroadcast ? MessageFlags.Broadcast : 0)
| (mce.IsAction ? MessageFlags.Action : 0)
| (mce.IsPrivate ? MessageFlags.Private : 0)
);
return;
}
@ -212,7 +212,7 @@ public class Context {
if(!IsInChannel(user, chan)) {
long msgId = RandomSnowflake.Next();
await SendTo(chan, new UserConnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
await Events.AddEvent(msgId, "user:connect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
await Messages.LogMessage(msgId, "user:connect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, MessageFlags.Log);
}
await conn.Send(new AuthSuccessS2CPacket(
@ -236,8 +236,8 @@ public class Context {
))
));
IEnumerable<StoredEventInfo> msgs = await Events.GetChannelEventLog(chan.Name);
foreach(StoredEventInfo msg in msgs)
IEnumerable<Message> msgs = await Messages.GetMessages(chan.Name);
foreach(Message msg in msgs)
await conn.Send(new ContextMessageS2CPacket(msg));
await conn.Send(new ContextChannelsS2CPacket(
@ -263,9 +263,9 @@ public class Context {
long msgId = RandomSnowflake.Next();
await SendTo(chan, new UserDisconnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, reason));
await Events.AddEvent(msgId, "user:disconnect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, new { reason = (int)reason }, StoredEventFlags.Log);
await Messages.LogMessage(msgId, "user:disconnect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, new { reason = (int)reason }, MessageFlags.Log);
if(chan.IsTemporary && chan.IsOwner(user))
if(chan.IsTemporary && chan.IsOwner(user.UserId))
await RemoveChannel(chan);
}
}
@ -276,7 +276,7 @@ public class Context {
return;
}
if(!user.Permissions.HasFlag(UserPermissions.JoinAnyChannel) && chan.IsOwner(user)) {
if(!user.Permissions.HasFlag(UserPermissions.JoinAnyChannel) && chan.IsOwner(user.UserId)) {
if(chan.Rank > user.Rank) {
await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name));
await ForceChannel(user);
@ -294,18 +294,18 @@ public class Context {
}
public async Task ForceChannelSwitch(User user, Channel chan) {
if(!Channels.Contains(chan))
if(!Channels.Any(c => c.NameEquals(chan.Name)))
return;
Channel oldChan = UserLastChannel[user.UserId];
long leaveId = RandomSnowflake.Next();
await SendTo(oldChan, new UserChannelLeaveS2CPacket(leaveId, user.UserId));
await Events.AddEvent(leaveId, "chan:leave", oldChan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
await Messages.LogMessage(leaveId, "chan:leave", oldChan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, MessageFlags.Log);
long joinId = RandomSnowflake.Next();
await SendTo(chan, new UserChannelJoinS2CPacket(joinId, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
await Events.AddEvent(joinId, "chan:join", chan.Name, user.UserId, user.LegacyName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
await Messages.LogMessage(joinId, "chan:join", chan.Name, user.UserId, user.LegacyName, user.Colour, user.Rank, user.NickName, user.Permissions, null, MessageFlags.Log);
await SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.Mode.MessagesUsers));
await SendTo(user, new ContextUsersS2CPacket(
@ -320,8 +320,8 @@ public class Context {
))
));
IEnumerable<StoredEventInfo> msgs = await Events.GetChannelEventLog(chan.Name);
foreach(StoredEventInfo msg in msgs)
IEnumerable<Message> msgs = await Messages.GetMessages(chan.Name);
foreach(Message msg in msgs)
await SendTo(user, new ContextMessageS2CPacket(msg));
await ForceChannel(user, chan);
@ -330,7 +330,7 @@ public class Context {
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
UserLastChannel[user.UserId] = chan;
if(oldChan.IsTemporary && oldChan.IsOwner(user))
if(oldChan.IsTemporary && oldChan.IsOwner(user.UserId))
await RemoveChannel(oldChan);
}
@ -374,8 +374,7 @@ public class Context {
}
public async Task UpdateChannel(Channel channel, bool? temporary = null, int? hierarchy = null, string? password = null) {
ArgumentNullException.ThrowIfNull(channel);
if(!Channels.Contains(channel))
if(!Channels.Any(c => c.NameEquals(channel.Name)))
throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel));
if(temporary.HasValue)

View file

@ -1,20 +0,0 @@
namespace SharpChat.EventStorage;
public interface EventStorage {
Task AddEvent(
long id,
string type,
string channelName,
string senderId,
string senderName,
ColourInheritable senderColour,
int senderRank,
string senderNick,
UserPermissions senderPerms,
object? data = null,
StoredEventFlags flags = StoredEventFlags.None
);
Task RemoveEvent(StoredEventInfo evt);
Task<StoredEventInfo?> GetEvent(long seqId);
Task<IEnumerable<StoredEventInfo>> GetChannelEventLog(string channelName, int amount = 20, int offset = 0);
}

View file

@ -1,128 +0,0 @@
using MySqlConnector;
using System.Text;
using System.Text.Json;
namespace SharpChat.EventStorage;
public partial class MariaDBEventStorage(string connString) : EventStorage {
private string ConnectionString { get; } = connString ?? throw new ArgumentNullException(nameof(connString));
public async Task AddEvent(
long id,
string type,
string channelName,
string senderId,
string senderName,
ColourInheritable senderColour,
int senderRank,
string senderNick,
UserPermissions senderPerms,
object? data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
await RunCommand(
"INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_flags`, `event_data`"
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`)"
+ " VALUES (@id, NOW(), @type, @target, @flags, @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("flags", (byte)flags),
new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)),
new MySqlParameter("sender", long.TryParse(senderId, out long senderId64) && senderId64 > 0 ? senderId64 : null),
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", ToStoredPermissions(senderPerms))
);
}
public async Task<StoredEventInfo?> GetEvent(long seqId) {
try {
using MySqlDataReader? reader = await RunQuery(
"SELECT `event_id`, `event_type`, `event_flags`, `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",
new MySqlParameter("id", seqId)
);
if(reader is null)
return null;
while(reader.Read()) {
StoredEventInfo evt = ReadEvent(reader);
if(evt != null)
return evt;
}
} catch(MySqlException ex) {
Logger.Write(ex);
}
return null;
}
private static StoredEventInfo ReadEvent(MySqlDataReader reader) {
return new StoredEventInfo(
reader.GetInt64("event_id"),
Encoding.ASCII.GetString((byte[])reader["event_type"]),
reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : reader.GetInt64("event_sender").ToString(),
reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"),
ColourInheritable.FromMisuzu(reader.GetInt32("event_sender_colour")),
reader.GetInt32("event_sender_rank"),
FromStoredPermissions((StoredUserPermissions)reader.GetInt32("event_sender_perms")),
reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? string.Empty : reader.GetString("event_sender_nick"),
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"])),
(StoredEventFlags)reader.GetByte("event_flags")
);
}
public async Task<IEnumerable<StoredEventInfo>> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) {
List<StoredEventInfo> events = [];
try {
using MySqlDataReader? reader = await RunQuery(
"SELECT `event_id`, `event_type`, `event_flags`, `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"
+ " LIMIT @amount",
new MySqlParameter("target", channelName),
new MySqlParameter("amount", amount),
new MySqlParameter("offset", offset)
);
if(reader is null)
return events;
while(reader.Read()) {
StoredEventInfo evt = ReadEvent(reader);
if(evt != null)
events.Add(evt);
}
} catch(MySqlException ex) {
Logger.Write(ex);
}
events.Reverse();
return events;
}
public async Task RemoveEvent(StoredEventInfo evt) {
await 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,87 +0,0 @@
using MySqlConnector;
using SharpChat.Configuration;
namespace SharpChat.EventStorage;
public partial class MariaDBEventStorage {
public static string BuildConnString(Configuration.Config config) {
return BuildConnString(
config.ReadValue("host", "localhost"),
config.ReadValue("user", string.Empty),
config.ReadValue("pass", string.Empty),
config.ReadValue("db", "sharpchat")
);
}
public static string BuildConnString(string? host, string? username, string? password, string? database) {
return new MySqlConnectionStringBuilder {
Server = host,
UserID = username,
Password = password,
Database = database,
OldGuids = false,
TreatTinyAsBoolean = false,
CharacterSet = "utf8mb4",
SslMode = MySqlSslMode.None,
ForceSynchronous = true,
ConnectionTimeout = 5,
DefaultCommandTimeout = 900, // fuck it, 15 minutes
}.ToString();
}
private async Task<MySqlConnection> GetConnection() {
MySqlConnection conn = new(ConnectionString);
await conn.OpenAsync();
return conn;
}
private async Task<int> RunCommand(string command, params MySqlParameter[] parameters) {
try {
using MySqlConnection conn = await GetConnection();
using MySqlCommand cmd = conn.CreateCommand();
if(parameters?.Length > 0)
cmd.Parameters.AddRange(parameters);
cmd.CommandText = command;
return await cmd.ExecuteNonQueryAsync();
} catch(MySqlException ex) {
Logger.Write(ex);
}
return 0;
}
private async Task<MySqlDataReader?> RunQuery(string command, params MySqlParameter[] parameters) {
try {
MySqlConnection conn = await GetConnection();
MySqlCommand cmd = conn.CreateCommand();
if(parameters?.Length > 0)
cmd.Parameters.AddRange(parameters);
cmd.CommandText = command;
return await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.CloseConnection);
} catch(MySqlException ex) {
Logger.Write(ex);
}
return null;
}
private async Task<T> RunQueryValue<T>(string command, params MySqlParameter[] parameters)
where T : struct {
try {
using MySqlConnection conn = await GetConnection();
using MySqlCommand cmd = conn.CreateCommand();
if(parameters?.Length > 0)
cmd.Parameters.AddRange(parameters);
cmd.CommandText = command;
await cmd.PrepareAsync();
object? raw = await cmd.ExecuteScalarAsync();
if(raw is T value)
return value;
} catch(MySqlException ex) {
Logger.Write(ex);
}
return default;
}
}

View file

@ -1,84 +0,0 @@
using MySqlConnector;
namespace SharpChat.EventStorage;
public partial class MariaDBEventStorage {
private async Task DoMigration(string name, Func<Task> action) {
bool done = await RunQueryValue<long>(
"SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name",
new MySqlParameter("name", name)
) > 0;
if(!done) {
Logger.Write($"Running migration '{name}'...");
await action();
await RunCommand(
"INSERT INTO `sqc_migrations` (`migration_name`) VALUES (@name)",
new MySqlParameter("name", name)
);
}
}
public async Task RunMigrations() {
await RunCommand(
"CREATE TABLE IF NOT EXISTS `sqc_migrations` ("
+ "`migration_name` VARCHAR(255) NOT NULL,"
+ "`migration_completed` TIMESTAMP NOT NULL DEFAULT current_timestamp(),"
+ "UNIQUE INDEX `migration_name` (`migration_name`),"
+ "INDEX `migration_completed` (`migration_completed`)"
+ ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;"
);
await DoMigration("create_events_table", CreateEventsTable);
await DoMigration("allow_null_target", AllowNullTarget);
await DoMigration("event_data_as_medium_blob", EventDataAsMediumBlob);
await DoMigration("event_user_and_nick_name_to_1000", EventUserAndNickNameTo1000);
}
private async Task EventUserAndNickNameTo1000() {
await RunCommand(
"ALTER TABLE `sqc_events`"
+ " CHANGE COLUMN `event_sender_name` `event_sender_name` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender`,"
+ " CHANGE COLUMN `event_sender_nick` `event_sender_nick` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender_rank`;"
);
}
private async Task EventDataAsMediumBlob() {
await RunCommand(
"ALTER TABLE `sqc_events`"
+ " CHANGE COLUMN `event_data` `event_data` MEDIUMBLOB NULL DEFAULT NULL AFTER `event_flags`;"
);
}
private async Task AllowNullTarget() {
await RunCommand(
"ALTER TABLE `sqc_events`"
+ " CHANGE COLUMN `event_target` `event_target` VARBINARY(255) NULL AFTER `event_type`;"
);
}
private async Task CreateEventsTable() {
await RunCommand(
"CREATE TABLE `sqc_events` ("
+ "`event_id` BIGINT(20) NOT NULL,"
+ "`event_sender` BIGINT(20) UNSIGNED NULL DEFAULT NULL,"
+ "`event_sender_name` VARCHAR(255) NULL DEFAULT NULL,"
+ "`event_sender_colour` INT(11) NULL DEFAULT NULL,"
+ "`event_sender_rank` INT(11) NULL DEFAULT NULL,"
+ "`event_sender_nick` VARCHAR(255) NULL DEFAULT NULL,"
+ "`event_sender_perms` INT(11) NULL DEFAULT NULL,"
+ "`event_created` TIMESTAMP NOT NULL DEFAULT current_timestamp(),"
+ "`event_deleted` TIMESTAMP NULL DEFAULT NULL,"
+ "`event_type` VARBINARY(255) NOT NULL,"
+ "`event_target` VARBINARY(255) NOT NULL,"
+ "`event_flags` TINYINT(3) UNSIGNED NOT NULL,"
+ "`event_data` BLOB NULL DEFAULT NULL,"
+ "PRIMARY KEY (`event_id`),"
+ "INDEX `event_target` (`event_target`),"
+ "INDEX `event_type` (`event_type`),"
+ "INDEX `event_sender` (`event_sender`),"
+ "INDEX `event_datetime` (`event_created`),"
+ "INDEX `event_deleted` (`event_deleted`)"
+ ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;"
);
}
}

View file

@ -1,100 +0,0 @@
namespace SharpChat.EventStorage;
public partial class MariaDBEventStorage {
public static UserPermissions FromStoredPermissions(StoredUserPermissions sup) {
UserPermissions perms = 0;
if(sup.HasFlag(StoredUserPermissions.KickUser))
perms |= UserPermissions.KickUser;
if(sup.HasFlag(StoredUserPermissions.BanUser))
perms |= UserPermissions.BanUser;
if(sup.HasFlag(StoredUserPermissions.Broadcast))
perms |= UserPermissions.SendBroadcast;
if(sup.HasFlag(StoredUserPermissions.SetOwnNickname))
perms |= UserPermissions.SetOwnNickname;
if(sup.HasFlag(StoredUserPermissions.SetOthersNickname))
perms |= UserPermissions.SetOthersNickname;
if(sup.HasFlag(StoredUserPermissions.CreateChannel))
perms |= UserPermissions.CreateChannel;
if(sup.HasFlag(StoredUserPermissions.DeleteChannel))
perms |= UserPermissions.DeleteChannel;
if(sup.HasFlag(StoredUserPermissions.SetChannelPermanent))
perms |= UserPermissions.SetChannelPermanent;
if(sup.HasFlag(StoredUserPermissions.SetChannelPassword))
perms |= UserPermissions.SetChannelPassword;
if(sup.HasFlag(StoredUserPermissions.SetChannelHierarchy))
perms |= UserPermissions.SetChannelMinimumRank;
if(sup.HasFlag(StoredUserPermissions.JoinAnyChannel))
perms |= UserPermissions.JoinAnyChannel;
if(sup.HasFlag(StoredUserPermissions.SendMessage))
perms |= UserPermissions.SendMessage;
if(sup.HasFlag(StoredUserPermissions.DeleteOwnMessage))
perms |= UserPermissions.DeleteOwnMessage;
if(sup.HasFlag(StoredUserPermissions.DeleteAnyMessage))
perms |= UserPermissions.DeleteAnyMessage;
if(sup.HasFlag(StoredUserPermissions.EditOwnMessage))
perms |= UserPermissions.EditOwnMessage;
if(sup.HasFlag(StoredUserPermissions.EditAnyMessage))
perms |= UserPermissions.EditAnyMessage;
if(sup.HasFlag(StoredUserPermissions.SeeIPAddress))
perms |= UserPermissions.ViewIPAddress;
if(sup.HasFlag(StoredUserPermissions.ViewLogs))
perms |= UserPermissions.ViewLogs;
if(sup.HasFlag(StoredUserPermissions.ViewBanList))
perms |= UserPermissions.ViewBanList;
if(sup.HasFlag(StoredUserPermissions.PardonUser))
perms |= UserPermissions.PardonUser;
if(sup.HasFlag(StoredUserPermissions.PardonIPAddress))
perms |= UserPermissions.PardonIPAddress;
return perms;
}
public static StoredUserPermissions ToStoredPermissions(UserPermissions up) {
StoredUserPermissions perms = 0;
if(up.HasFlag(UserPermissions.KickUser))
perms |= StoredUserPermissions.KickUser;
if(up.HasFlag(UserPermissions.BanUser))
perms |= StoredUserPermissions.BanUser;
if(up.HasFlag(UserPermissions.SendBroadcast))
perms |= StoredUserPermissions.Broadcast;
if(up.HasFlag(UserPermissions.SetOwnNickname))
perms |= StoredUserPermissions.SetOwnNickname;
if(up.HasFlag(UserPermissions.SetOthersNickname))
perms |= StoredUserPermissions.SetOthersNickname;
if(up.HasFlag(UserPermissions.CreateChannel))
perms |= StoredUserPermissions.CreateChannel;
if(up.HasFlag(UserPermissions.DeleteChannel))
perms |= StoredUserPermissions.DeleteChannel;
if(up.HasFlag(UserPermissions.SetChannelPermanent))
perms |= StoredUserPermissions.SetChannelPermanent;
if(up.HasFlag(UserPermissions.SetChannelPassword))
perms |= StoredUserPermissions.SetChannelPassword;
if(up.HasFlag(UserPermissions.SetChannelMinimumRank))
perms |= StoredUserPermissions.SetChannelHierarchy;
if(up.HasFlag(UserPermissions.JoinAnyChannel))
perms |= StoredUserPermissions.JoinAnyChannel;
if(up.HasFlag(UserPermissions.SendMessage))
perms |= StoredUserPermissions.SendMessage;
if(up.HasFlag(UserPermissions.DeleteOwnMessage))
perms |= StoredUserPermissions.DeleteOwnMessage;
if(up.HasFlag(UserPermissions.DeleteAnyMessage))
perms |= StoredUserPermissions.DeleteAnyMessage;
if(up.HasFlag(UserPermissions.EditOwnMessage))
perms |= StoredUserPermissions.EditOwnMessage;
if(up.HasFlag(UserPermissions.EditAnyMessage))
perms |= StoredUserPermissions.EditAnyMessage;
if(up.HasFlag(UserPermissions.ViewIPAddress))
perms |= StoredUserPermissions.SeeIPAddress;
if(up.HasFlag(UserPermissions.ViewLogs))
perms |= StoredUserPermissions.ViewLogs;
if(up.HasFlag(UserPermissions.ViewBanList))
perms |= StoredUserPermissions.ViewBanList;
if(up.HasFlag(UserPermissions.PardonUser))
perms |= StoredUserPermissions.PardonUser;
if(up.HasFlag(UserPermissions.PardonIPAddress))
perms |= StoredUserPermissions.PardonIPAddress;
return perms;
}
}

View file

@ -1,10 +0,0 @@
namespace SharpChat.EventStorage;
[Flags]
public enum StoredEventFlags {
None = 0,
Action = 1,
Broadcast = 1 << 1,
Log = 1 << 2,
Private = 1 << 3,
}

View file

@ -1,35 +0,0 @@
using System.Text.Json;
namespace SharpChat.EventStorage;
public class StoredEventInfo(
long id,
string type,
string? senderId,
string senderName,
ColourInheritable senderColour,
int senderRank,
UserPermissions senderPermissions,
string senderNickName,
DateTimeOffset created,
DateTimeOffset? deleted,
string? channelName,
JsonDocument data,
StoredEventFlags flags
) {
public long Id { get; } = id;
public string Type { get; } = type;
public string? SenderId { get; } = senderId;
public string SenderName { get; } = senderName;
public ColourInheritable SenderColour { get; } = senderColour;
public int SenderRank { get; } = senderRank;
public UserPermissions SenderPermissions { get; } = senderPermissions;
public string SenderNickName { get; } = senderNickName;
public DateTimeOffset Created { get; } = created;
public DateTimeOffset? Deleted { get; } = deleted;
public string? ChannelName { get; } = channelName;
public StoredEventFlags Flags { get; } = flags;
public JsonDocument Data { get; } = data;
public string SenderLegacyName => string.IsNullOrWhiteSpace(SenderNickName) ? SenderName : $"~{SenderNickName}";
}

View file

@ -1,27 +0,0 @@
namespace SharpChat.EventStorage;
[Flags]
public enum StoredUserPermissions : int {
KickUser = 0x1,
BanUser = 0x2,
//SilenceUser = 0x4,
Broadcast = 0x8,
SetOwnNickname = 0x10,
SetOthersNickname = 0x20,
CreateChannel = 0x40,
SetChannelPermanent = 0x80,
SetChannelPassword = 0x100,
SetChannelHierarchy = 0x200,
SendMessage = 0x400,
DeleteOwnMessage = 0x800,
DeleteAnyMessage = 0x1000,
EditOwnMessage = 0x2000,
EditAnyMessage = 0x4000,
SeeIPAddress = 0x8000,
DeleteChannel = 0x10000,
JoinAnyChannel = 0x20000,
ViewLogs = 0x40000,
ViewBanList = 0x80000,
PardonUser = 0x100000,
PardonIPAddress = 0x200000,
}

View file

@ -1,63 +0,0 @@
using System.Text.Json;
namespace SharpChat.EventStorage;
public class VirtualEventStorage : EventStorage {
private readonly Dictionary<long, StoredEventInfo> Events = [];
public Task AddEvent(
long id,
string type,
string channelName,
string senderId,
string senderName,
ColourInheritable senderColour,
int senderRank,
string senderNick,
UserPermissions senderPerms,
object? data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
Events.Add(
id,
new(
id,
type,
long.TryParse(senderId, out long senderId64) && senderId64 > 0 ? senderId : null,
senderName,
senderColour,
senderRank,
senderPerms,
senderNick,
DateTimeOffset.Now,
null,
channelName,
JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data)),
flags
)
);
return Task.CompletedTask;
}
public Task<StoredEventInfo?> GetEvent(long seqId) {
return Task.FromResult(Events.TryGetValue(seqId, out StoredEventInfo? evt) ? evt : null);
}
public Task RemoveEvent(StoredEventInfo evt) {
Events.Remove(evt.Id);
return Task.CompletedTask;
}
public Task<IEnumerable<StoredEventInfo>> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) {
IEnumerable<StoredEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName);
int start = subset.Count() - offset - amount;
if(start < 0) {
amount += start;
start = 0;
}
return Task.FromResult(subset.Skip(start).Take(amount).ToArray() as IEnumerable<StoredEventInfo>);
}
}

View file

@ -1,8 +1,9 @@
using SharpChat;
using SharpChat.Configuration;
using SharpChat.EventStorage;
using SharpChat.Messages;
using SharpChat.Flashii;
using System.Text;
using SharpChat.MariaDB;
const string CONFIG = "sharpchat.cfg";
@ -123,18 +124,18 @@ FlashiiClient flashii = new(httpClient, config.ScopeTo("msz"));
if(hasCancelled) return;
EventStorage evtStore;
MessageStorage msgStore;
if(string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty))) {
evtStore = new VirtualEventStorage();
msgStore = new VirtualMessageStorage();
} else {
MariaDBEventStorage mdbes = new(MariaDBEventStorage.BuildConnString(config.ScopeTo("mariadb")));
evtStore = mdbes;
MariaDBMessageStorage mdbes = new(MariaDBMessageStorage.BuildConnString(config.ScopeTo("mariadb")));
msgStore = mdbes;
await mdbes.RunMigrations();
}
if(hasCancelled) return;
using SockChatServer scs = new(flashii, flashii, evtStore, config.ScopeTo("chat"));
using SockChatServer scs = new(flashii, flashii, msgStore, config.ScopeTo("chat"));
scs.Listen(mre);
mre.WaitOne();

View file

@ -17,7 +17,6 @@
<ItemGroup>
<PackageReference Include="Fleck" Version="1.2.0" />
<PackageReference Include="MySqlConnector" Version="2.4.0" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
@ -34,6 +33,7 @@
<ItemGroup>
<ProjectReference Include="..\SharpChat.Flashii\SharpChat.Flashii.csproj" />
<ProjectReference Include="..\SharpChat.MariaDB\SharpChat.MariaDB.csproj" />
<ProjectReference Include="..\SharpChat.SockChat\SharpChat.SockChat.csproj" />
<ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
</ItemGroup>

View file

@ -1,115 +0,0 @@
using SharpChat.EventStorage;
using System.Text;
namespace SharpChat.SockChat.S2CPackets;
public class ContextMessageS2CPacket(StoredEventInfo evt, bool notify = false) : S2CPacket {
public StoredEventInfo Event { get; private set; } = evt ?? throw new ArgumentNullException(nameof(evt));
public string Pack() {
bool isAction = Event.Flags.HasFlag(StoredEventFlags.Action);
bool isBroadcast = Event.Flags.HasFlag(StoredEventFlags.Broadcast);
bool isPrivate = Event.Flags.HasFlag(StoredEventFlags.Private);
StringBuilder sb = new();
sb.Append("7\t1\t");
sb.Append(Event.Created.ToUnixTimeSeconds());
sb.Append('\t');
switch(Event.Type) {
case "msg:add":
case "SharpChat.Events.ChatMessage":
if(isBroadcast || Event.SenderId is null) {
sb.Append("-1\tChatBot\tinherit\t\t0\fsay\f");
} else {
sb.Append(Event.SenderId);
sb.Append('\t');
sb.Append(Event.SenderLegacyName);
sb.Append('\t');
sb.Append(Event.SenderColour);
sb.Append('\t');
sb.Append(Event.SenderRank);
sb.Append(' ');
sb.Append(Event.SenderPermissions.HasFlag(UserPermissions.KickUser) ? '1' : '0');
sb.Append(' ');
sb.Append(Event.SenderPermissions.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
sb.Append(' ');
sb.Append(Event.SenderPermissions.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
sb.Append(' ');
sb.Append(Event.SenderPermissions.HasFlag(UserPermissions.CreateChannel) ? (Event.SenderPermissions.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
sb.Append('\t');
}
if(isAction)
sb.Append("<i>");
sb.Append(
(Event.Data.RootElement.GetProperty("text").GetString()?
.Replace("<", "&lt;")
.Replace(">", "&gt;")
.Replace("\n", " <br/> ")
.Replace("\t", " ")) ?? string.Empty
);
if(isAction)
sb.Append("</i>");
break;
case "user:connect":
case "SharpChat.Events.UserConnectEvent":
sb.Append("-1\tChatBot\tinherit\t\t0\fjoin\f");
sb.Append(Event.SenderLegacyName);
break;
case "chan:join":
case "SharpChat.Events.UserChannelJoinEvent":
sb.Append("-1\tChatBot\tinherit\t\t0\fjchan\f");
sb.Append(Event.SenderLegacyName);
break;
case "chan:leave":
case "SharpChat.Events.UserChannelLeaveEvent":
sb.Append("-1\tChatBot\tinherit\t\t0\flchan\f");
sb.Append(Event.SenderLegacyName);
break;
case "user:disconnect":
case "SharpChat.Events.UserDisconnectEvent":
sb.Append("-1\tChatBot\tinherit\t\t0\f");
switch((UserDisconnectS2CPacket.Reason)Event.Data.RootElement.GetProperty("reason").GetByte()) {
case UserDisconnectS2CPacket.Reason.Flood:
sb.Append("flood");
break;
case UserDisconnectS2CPacket.Reason.Kicked:
sb.Append("kick");
break;
case UserDisconnectS2CPacket.Reason.TimeOut:
sb.Append("timeout");
break;
case UserDisconnectS2CPacket.Reason.Leave:
default:
sb.Append("leave");
break;
}
sb.Append('\f');
sb.Append(Event.SenderLegacyName);
break;
}
sb.Append('\t');
sb.Append(Event.Id);
sb.Append('\t');
sb.Append(notify ? '1' : '0');
sb.AppendFormat(
"\t1{0}0{1}{2}",
isAction ? '1' : '0',
isAction ? '0' : '1',
isPrivate ? '1' : '0'
);
return sb.ToString();
}
}

View file

@ -4,6 +4,7 @@ using SharpChat.Bans;
using SharpChat.C2SPacketHandlers;
using SharpChat.ClientCommands;
using SharpChat.Configuration;
using SharpChat.Messages;
using SharpChat.SockChat.S2CPackets;
using System.Net;
@ -39,7 +40,7 @@ public class SockChatServer : IDisposable {
public SockChatServer(
AuthClient authClient,
BansClient bansClient,
EventStorage.EventStorage evtStore,
MessageStorage msgStorage,
Config config
) {
Logger.Write("Initialising Sock Chat server...");
@ -51,7 +52,7 @@ public class SockChatServer : IDisposable {
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
FloodKickExemptRank = config.ReadCached("floodKickExemptRank", DEFAULT_FLOOD_KICK_EXEMPT_RANK);
Context = new Context(evtStore);
Context = new Context(msgStorage ?? throw new ArgumentNullException(nameof(msgStorage)));
string[]? channelNames = config.ReadValue("channels", DEFAULT_CHANNELS);
if(channelNames is not null)