diff --git a/.gitignore b/.gitignore index 16b1c56..215570f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ http-motd.txt _webdb.txt msz_url.txt sharpchat.cfg +sharpchat.db +sharpchat.db-wal +sharpchat.db-shm SharpChat/version.txt # User-specific files diff --git a/SharpChat.MariaDB/MariaDBConnection.cs b/SharpChat.MariaDB/MariaDBConnection.cs new file mode 100644 index 0000000..9af5840 --- /dev/null +++ b/SharpChat.MariaDB/MariaDBConnection.cs @@ -0,0 +1,52 @@ +using MySqlConnector; +using System.Data; + +namespace SharpChat.MariaDB; + +public class MariaDBConnection(MySqlConnection conn) : IDisposable { + public async Task<int> RunCommand(string command, params MySqlParameter[] parameters) { + using MySqlCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + return await cmd.ExecuteNonQueryAsync(); + } + + public async Task<MySqlDataReader?> RunQuery(string command, params MySqlParameter[] parameters) { + using MySqlCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + return await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection); + } + + public async Task<T> RunQueryValue<T>(string command, params MySqlParameter[] parameters) + where T : struct { + using MySqlCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + await cmd.PrepareAsync(); + + object? raw = await cmd.ExecuteScalarAsync(); + return raw is T value ? value : default; + } + + private bool disposed = false; + + ~MariaDBConnection() { + DoDispose(); + } + + public void Dispose() { + DoDispose(); + GC.SuppressFinalize(this); + } + + private void DoDispose() { + if(disposed) + return; + disposed = true; + conn.Dispose(); + } +} diff --git a/SharpChat.MariaDB/MariaDBMessageStorage.cs b/SharpChat.MariaDB/MariaDBMessageStorage.cs index 8c8cbb4..b9b1ad6 100644 --- a/SharpChat.MariaDB/MariaDBMessageStorage.cs +++ b/SharpChat.MariaDB/MariaDBMessageStorage.cs @@ -4,9 +4,7 @@ using System.Text.Json; namespace SharpChat.MariaDB; -public partial class MariaDBMessageStorage(string connString) : MessageStorage { - private string ConnectionString { get; } = connString ?? throw new ArgumentNullException(nameof(connString)); - +public class MariaDBMessageStorage(MariaDBStorage storage) : MessageStorage { public async Task LogMessage( long id, string type, @@ -19,41 +17,77 @@ public partial class MariaDBMessageStorage(string connString) : MessageStorage { UserPermissions senderPerms, object? data = null ) { - await RunCommand( - "INSERT INTO sqc_events (event_id, event_created, event_type, event_channel, event_data" - + ", event_sender, event_sender_name, event_sender_colour, event_sender_rank, event_sender_nick, event_sender_perms)" - + " VALUES (@id, NOW(), @type, @channel, @data" - + ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms)", - new MySqlParameter("id", id), - new MySqlParameter("type", type), - new MySqlParameter("channel", string.IsNullOrWhiteSpace(channelName) ? null : channelName), - 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)) + try { + using MariaDBConnection conn = await storage.CreateConnection(); + await conn.RunCommand( + "INSERT INTO sqc_events (event_id, event_created, event_type, event_channel, event_data" + + ", event_sender, event_sender_name, event_sender_colour, event_sender_rank, event_sender_nick, event_sender_perms)" + + " VALUES (@id, NOW(), @type, @channel, @data" + + ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms)", + new MySqlParameter("id", id), + new MySqlParameter("type", type), + new MySqlParameter("channel", string.IsNullOrWhiteSpace(channelName) ? null : channelName), + 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", MariaDBUserPermissionsConverter.To(senderPerms)) + ); + } catch(MySqlException ex) { + Logger.Write(ex); + } + } + + public async Task DeleteMessage(Message msg) { + try { + using MariaDBConnection conn = await storage.CreateConnection(); + await conn.RunCommand( + "UPDATE IGNORE sqc_events SET event_deleted = NOW() WHERE event_id = @id AND event_deleted IS NULL", + new MySqlParameter("id", msg.Id) + ); + } catch(MySqlException ex) { + Logger.Write(ex); + } + } + + private static Message ReadMessage(MySqlDataReader reader) { + using Stream data = reader.GetStream("event_data"); + return new Message( + reader.GetInt64("event_id"), + reader.GetString("event_type"), + reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : reader.GetString("event_sender"), + 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"), + MariaDBUserPermissionsConverter.From((MariaDBUserPermissions)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_channel")) ? null : reader.GetString("event_channel"), + JsonDocument.Parse(data) ); } - public async Task<Message?> GetMessage(long seqId) { + public async Task<Message?> GetMessage(long id) { try { - using MySqlDataReader? reader = await RunQuery( + using MariaDBConnection conn = await storage.CreateConnection(); + using MySqlDataReader? reader = await conn.RunQuery( "SELECT event_id, event_type, event_data, event_channel" + ", 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) + new MySqlParameter("id", id) ); if(reader is null) return null; while(reader.Read()) { - Message evt = ReadEvent(reader); + Message evt = ReadMessage(reader); if(evt != null) return evt; } @@ -64,29 +98,12 @@ public partial class MariaDBMessageStorage(string connString) : MessageStorage { return null; } - private static Message ReadEvent(MySqlDataReader reader) { - using Stream data = reader.GetStream("event_data"); - return new Message( - reader.GetInt64("event_id"), - reader.GetString("event_type"), - reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : reader.GetString("event_sender"), - 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"), - FromMessagePermissions((MariaDBUserPermissions)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_channel")) ? null : reader.GetString("event_channel"), - JsonDocument.Parse(data) - ); - } - public async Task<IEnumerable<Message>> GetMessages(string channelName, int amount = 20, int offset = 0) { - List<Message> events = []; + List<Message> msgs = []; try { - using MySqlDataReader? reader = await RunQuery( + using MariaDBConnection conn = await storage.CreateConnection(); + using MySqlDataReader? reader = await conn.RunQuery( "SELECT event_id, event_type, event_data, event_channel" + ", event_sender, event_sender_name, event_sender_colour, event_sender_rank, event_sender_nick, event_sender_perms" + ", UNIX_TIMESTAMP(event_created) AS event_created" @@ -101,26 +118,19 @@ public partial class MariaDBMessageStorage(string connString) : MessageStorage { new MySqlParameter("offset", offset) ); if(reader is null) - return events; + return msgs; while(reader.Read()) { - Message evt = ReadEvent(reader); + Message evt = ReadMessage(reader); if(evt != null) - events.Add(evt); + msgs.Add(evt); } } catch(MySqlException ex) { Logger.Write(ex); } - events.Reverse(); + msgs.Reverse(); - return events; - } - - public async Task DeleteMessage(Message 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) - ); + return msgs; } } diff --git a/SharpChat.MariaDB/MariaDBMessageStorage_Database.cs b/SharpChat.MariaDB/MariaDBMessageStorage_Database.cs deleted file mode 100644 index 6f83f80..0000000 --- a/SharpChat.MariaDB/MariaDBMessageStorage_Database.cs +++ /dev/null @@ -1,87 +0,0 @@ -using MySqlConnector; -using SharpChat.Configuration; - -namespace SharpChat.MariaDB; - -public partial class MariaDBMessageStorage { - public static string BuildConnString(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; - } -} diff --git a/SharpChat.MariaDB/MariaDBMessageStorage_Migrations.cs b/SharpChat.MariaDB/MariaDBMigrations.cs similarity index 76% rename from SharpChat.MariaDB/MariaDBMessageStorage_Migrations.cs rename to SharpChat.MariaDB/MariaDBMigrations.cs index 312fbf7..ddae353 100644 --- a/SharpChat.MariaDB/MariaDBMessageStorage_Migrations.cs +++ b/SharpChat.MariaDB/MariaDBMigrations.cs @@ -2,16 +2,16 @@ using MySqlConnector; namespace SharpChat.MariaDB; -public partial class MariaDBMessageStorage { +public class MariaDBMigrations(MariaDBConnection conn) { private async Task DoMigration(string name, Func<Task> action) { - bool done = await RunQueryValue<long>( + bool done = await conn.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( + await conn.RunCommand( "INSERT INTO sqc_migrations (migration_name) VALUES (@name)", new MySqlParameter("name", name) ); @@ -19,7 +19,7 @@ public partial class MariaDBMessageStorage { } public async Task RunMigrations() { - await RunCommand( + await conn.RunCommand( "CREATE TABLE IF NOT EXISTS sqc_migrations (" + "migration_name VARCHAR(255) NOT NULL," + "migration_completed TIMESTAMP NOT NULL DEFAULT current_timestamp()," @@ -38,9 +38,9 @@ public partial class MariaDBMessageStorage { } private async Task UpdateCollationsAndUseJsonType() { - await RunCommand("UPDATE sqc_events SET event_target = LOWER(CONVERT(event_target USING ascii))"); - await RunCommand("UPDATE sqc_events SET event_sender_nick = NULL WHERE event_sender_nick = ''"); - await RunCommand( + await conn.RunCommand("UPDATE sqc_events SET event_target = LOWER(CONVERT(event_target USING ascii))"); + await conn.RunCommand("UPDATE sqc_events SET event_sender_nick = NULL WHERE event_sender_nick = ''"); + await conn.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," @@ -55,27 +55,27 @@ public partial class MariaDBMessageStorage { } private async Task UpdateEventTypeNames() { - await RunCommand(@"UPDATE sqc_events SET event_type = ""msg:add"" WHERE event_type = ""SharpChat.Events.ChatMessage"""); - await RunCommand(@"UPDATE sqc_events SET event_type = ""user:connect"" WHERE event_type = ""SharpChat.Events.UserConnectEvent"""); - await RunCommand(@"UPDATE sqc_events SET event_type = ""user:disconnect"" WHERE event_type = ""SharpChat.Events.UserDisconnectEvent"""); - await RunCommand(@"UPDATE sqc_events SET event_type = ""chan:join"" WHERE event_type = ""SharpChat.Events.UserChannelJoinEvent"""); - await RunCommand(@"UPDATE sqc_events SET event_type = ""chan:leave"" WHERE event_type = ""SharpChat.Events.UserChannelLeaveEvent"""); + await conn.RunCommand(@"UPDATE sqc_events SET event_type = ""msg:add"" WHERE event_type = ""SharpChat.Events.ChatMessage"""); + await conn.RunCommand(@"UPDATE sqc_events SET event_type = ""user:connect"" WHERE event_type = ""SharpChat.Events.UserConnectEvent"""); + await conn.RunCommand(@"UPDATE sqc_events SET event_type = ""user:disconnect"" WHERE event_type = ""SharpChat.Events.UserDisconnectEvent"""); + await conn.RunCommand(@"UPDATE sqc_events SET event_type = ""chan:join"" WHERE event_type = ""SharpChat.Events.UserChannelJoinEvent"""); + await conn.RunCommand(@"UPDATE sqc_events SET event_type = ""chan:leave"" WHERE event_type = ""SharpChat.Events.UserChannelLeaveEvent"""); } private async Task NoMoreFlagsField() { // MessageFlags.Action is just a field in the data object - await RunCommand("UPDATE sqc_events SET event_data = JSON_MERGE_PATCH(event_data, JSON_OBJECT('act', true)) WHERE event_flags & 1"); + await conn.RunCommand("UPDATE sqc_events SET event_data = JSON_MERGE_PATCH(event_data, JSON_OBJECT('act', true)) WHERE event_flags & 1"); // MessageFlags.Broadcast can be implied by just having a NULL as the channel name - await RunCommand("UPDATE sqc_events SET event_target = NULL WHERE event_flags & 2"); + await conn.RunCommand("UPDATE sqc_events SET event_target = NULL WHERE event_flags & 2"); // MessageFlags.Log was never meaningfully used by anything and basically just meant "not-msg:add" // MessageFlags.Private was also never meaningfully used, can be determined by checking if the channel name starts with @ - await RunCommand("ALTER TABLE sqc_events DROP COLUMN event_flags"); + await conn.RunCommand("ALTER TABLE sqc_events DROP COLUMN event_flags"); } private async Task EventUserAndNickNameTo1000() { - await RunCommand( + await conn.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;" @@ -83,21 +83,21 @@ public partial class MariaDBMessageStorage { } private async Task EventDataAsMediumBlob() { - await RunCommand( + await conn.RunCommand( "ALTER TABLE sqc_events" + " CHANGE COLUMN event_data event_data MEDIUMBLOB NULL DEFAULT NULL AFTER event_flags;" ); } private async Task AllowNullTarget() { - await RunCommand( + await conn.RunCommand( "ALTER TABLE sqc_events" + " CHANGE COLUMN event_target event_target VARBINARY(255) NULL AFTER event_type;" ); } private async Task CreateEventsTable() { - await RunCommand( + await conn.RunCommand( "CREATE TABLE sqc_events (" + "event_id BIGINT(20) NOT NULL," + "event_sender BIGINT(20) UNSIGNED NULL DEFAULT NULL," diff --git a/SharpChat.MariaDB/MariaDBStorage.cs b/SharpChat.MariaDB/MariaDBStorage.cs new file mode 100644 index 0000000..71ccea5 --- /dev/null +++ b/SharpChat.MariaDB/MariaDBStorage.cs @@ -0,0 +1,47 @@ +using MySqlConnector; +using SharpChat.Configuration; +using SharpChat.Messages; + +namespace SharpChat.MariaDB; + +public class MariaDBStorage(string connString) : Storage { + public async Task<MariaDBConnection> CreateConnection() { + MySqlConnection conn = new(connString); + await conn.OpenAsync(); + return new MariaDBConnection(conn); + } + + public MessageStorage CreateMessageStorage() { + return new MariaDBMessageStorage(this); + } + + public async Task UpgradeStorage() { + using MariaDBConnection conn = await CreateConnection(); + await new MariaDBMigrations(conn).RunMigrations(); + } + + public static string BuildConnectionString(Config config) { + return BuildConnectionString( + config.ReadValue("host", "localhost"), + config.ReadValue("user", string.Empty), + config.ReadValue("pass", string.Empty), + config.ReadValue("db", "sharpchat") + ); + } + + public static string BuildConnectionString(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 = false, + ConnectionTimeout = 5, + DefaultCommandTimeout = 900, // fuck it, 15 minutes + }.ToString(); + } +} diff --git a/SharpChat.MariaDB/MariaDBMessageStorage_Permissions.cs b/SharpChat.MariaDB/MariaDBUserPermissionsConverter.cs similarity index 95% rename from SharpChat.MariaDB/MariaDBMessageStorage_Permissions.cs rename to SharpChat.MariaDB/MariaDBUserPermissionsConverter.cs index 9854583..428fa1c 100644 --- a/SharpChat.MariaDB/MariaDBMessageStorage_Permissions.cs +++ b/SharpChat.MariaDB/MariaDBUserPermissionsConverter.cs @@ -1,7 +1,7 @@ namespace SharpChat.MariaDB; -public partial class MariaDBMessageStorage { - public static UserPermissions FromMessagePermissions(MariaDBUserPermissions mup) { +public static class MariaDBUserPermissionsConverter { + public static UserPermissions From(MariaDBUserPermissions mup) { UserPermissions perms = 0; if(mup.HasFlag(MariaDBUserPermissions.KickUser)) @@ -50,7 +50,7 @@ public partial class MariaDBMessageStorage { return perms; } - public static MariaDBUserPermissions ToStoredPermissions(UserPermissions up) { + public static MariaDBUserPermissions To(UserPermissions up) { MariaDBUserPermissions perms = 0; if(up.HasFlag(UserPermissions.KickUser)) diff --git a/SharpChat.SQLite/SQLiteConnection.cs b/SharpChat.SQLite/SQLiteConnection.cs new file mode 100644 index 0000000..e611c04 --- /dev/null +++ b/SharpChat.SQLite/SQLiteConnection.cs @@ -0,0 +1,56 @@ +using System.Data; +using System.Data.Common; +using System.Data.SQLite; +using NativeSQLiteConnection = System.Data.SQLite.SQLiteConnection; + +namespace SharpChat.SQLite; + +public class SQLiteConnection(NativeSQLiteConnection conn) : IDisposable { + public async Task<int> RunCommand(string command, params SQLiteParameter[] parameters) { + using SQLiteCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + return await cmd.ExecuteNonQueryAsync(); + } + + public async Task<DbDataReader?> RunQuery(string command, params SQLiteParameter[] parameters) { + using SQLiteCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + return await cmd.ExecuteReaderAsync(); + } + + public async Task<T> RunQueryValue<T>(string command, params SQLiteParameter[] parameters) + where T : struct { + using SQLiteCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + await cmd.PrepareAsync(); + + object? raw = await cmd.ExecuteScalarAsync(); + return raw is T value ? value : default; + } + + private bool disposed = false; + + ~SQLiteConnection() { + DoDispose(); + } + + public void Dispose() { + DoDispose(); + GC.SuppressFinalize(this); + } + + private void DoDispose() { + if(disposed) + return; + disposed = true; + + RunCommand("VACUUM").Wait(); + conn.Dispose(); + } +} diff --git a/SharpChat.SQLite/SQLiteMessageStorage.cs b/SharpChat.SQLite/SQLiteMessageStorage.cs new file mode 100644 index 0000000..7e10924 --- /dev/null +++ b/SharpChat.SQLite/SQLiteMessageStorage.cs @@ -0,0 +1,126 @@ +using SharpChat.Messages; +using System.Data; +using System.Data.Common; +using System.Data.SQLite; +using System.Text; +using System.Text.Json; + +namespace SharpChat.SQLite; + +public class SQLiteMessageStorage(SQLiteConnection conn) : MessageStorage { + public async Task LogMessage( + long id, + string type, + string channelName, + string senderId, + string senderName, + ColourInheritable senderColour, + int senderRank, + string senderNick, + UserPermissions senderPerms, + object? data = null + ) { + try { + await conn.RunCommand( + "INSERT INTO messages (msg_id, msg_type, msg_created, msg_channel, msg_sender, msg_sender_name, msg_sender_colour, msg_sender_rank, msg_sender_nick, msg_sender_perms, msg_data)" + + " VALUES (@id, @type, @created, @channel, @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms, @data)", + new SQLiteParameter("id", id), + new SQLiteParameter("type", type), + new SQLiteParameter("created", $"{DateTimeOffset.UtcNow:s}Z"), + new SQLiteParameter("channel", string.IsNullOrWhiteSpace(channelName) ? null : channelName), + new SQLiteParameter("sender", long.TryParse(senderId, out long senderId64) && senderId64 > 0 ? senderId64 : null), + new SQLiteParameter("sender_name", senderName), + new SQLiteParameter("sender_colour", senderColour.Rgb.HasValue ? senderColour.Rgb.Value.Raw : null), + new SQLiteParameter("sender_rank", senderRank), + new SQLiteParameter("sender_nick", string.IsNullOrWhiteSpace(senderNick) ? null : senderNick), + new SQLiteParameter("sender_perms", SQLiteUserPermissionsConverter.To(senderPerms)), + new SQLiteParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)) + ); + } catch(SQLiteException ex) { + Logger.Write(ex); + } + } + + public async Task DeleteMessage(Message msg) { + try { + await conn.RunCommand( + "UPDATE IGNORE messages SET msg_deleted = NOW() WHERE msg_id = @id AND msg_deleted IS NULL", + new SQLiteParameter("id", msg.Id) + ); + } catch(SQLiteException ex) { + Logger.Write(ex); + } + } + + private static Message ReadMessage(DbDataReader reader) { + return new Message( + reader.GetInt64("msg_id"), + reader.GetString("msg_type"), + reader.IsDBNull(reader.GetOrdinal("msg_sender")) ? null : reader.GetString("msg_sender"), + reader.IsDBNull(reader.GetOrdinal("msg_sender_name")) ? string.Empty : reader.GetString("msg_sender_name"), + ColourInheritable.FromMisuzu(reader.GetInt32("msg_sender_colour")), + reader.GetInt32("msg_sender_rank"), + SQLiteUserPermissionsConverter.From((SQLiteUserPermissions)reader.GetInt32("msg_sender_perms")), + reader.IsDBNull(reader.GetOrdinal("msg_sender_nick")) ? string.Empty : reader.GetString("msg_sender_nick"), + DateTimeOffset.Parse(reader.GetString("msg_created")), + reader.IsDBNull(reader.GetOrdinal("msg_deleted")) ? null : DateTimeOffset.Parse(reader.GetString("msg_deleted")), + reader.IsDBNull(reader.GetOrdinal("msg_channel")) ? null : reader.GetString("msg_channel"), + JsonDocument.Parse(reader.GetString("msg_data")) + ); + } + + public async Task<Message?> GetMessage(long id) { + try { + using DbDataReader? reader = await conn.RunQuery( + "SELECT msg_id, msg_type, msg_created, msg_deleted, msg_channel, msg_sender, msg_sender_name, msg_sender_colour, msg_sender_rank, msg_sender_nick, msg_sender_perms, msg_data" + + " FROM messages WHERE msg_id = @id", + new SQLiteParameter("id", id) + ); + + if(reader is null) + return null; + + while(reader.Read()) { + Message evt = ReadMessage(reader); + if(evt != null) + return evt; + } + } catch(SQLiteException ex) { + Logger.Write(ex); + } + + return null; + } + + public async Task<IEnumerable<Message>> GetMessages(string channelName, int amount = 20, int offset = 0) { + List<Message> msgs = []; + + try { + using DbDataReader? reader = await conn.RunQuery( + "SELECT msg_id, msg_type, msg_created, msg_deleted, msg_channel, msg_sender, msg_sender_name, msg_sender_colour, msg_sender_rank, msg_sender_nick, msg_sender_perms, msg_data" + + " FROM messages" + + " WHERE msg_deleted IS NULL AND (msg_channel = @channel OR msg_channel IS NULL)" + + " AND msg_id > @offset" + + " ORDER BY msg_id DESC" + + " LIMIT @amount", + new SQLiteParameter("channel", channelName), + new SQLiteParameter("amount", amount), + new SQLiteParameter("offset", offset) + ); + if(reader is null) + return msgs; + + while(reader.Read()) { + Message evt = ReadMessage(reader); + if(evt != null) + msgs.Add(evt); + } + } catch(SQLiteException ex) { + Logger.Write(ex); + } + + msgs.Reverse(); + + return msgs; + } +} diff --git a/SharpChat.SQLite/SQLiteMigrations.cs b/SharpChat.SQLite/SQLiteMigrations.cs new file mode 100644 index 0000000..1dcc464 --- /dev/null +++ b/SharpChat.SQLite/SQLiteMigrations.cs @@ -0,0 +1,45 @@ +namespace SharpChat.SQLite; + +public class SQLiteMigrations(SQLiteConnection conn) { + public async Task RunMigrations() { + long currentVersion = await conn.RunQueryValue<long>("PRAGMA user_version"); + long version = currentVersion; + + async Task doMigration(int expect, Func<Task> action) { + if(version < expect) { + await action(); + ++version; + } + }; + + await doMigration(1, CreateMessagesTable); + + if(currentVersion != version) + await conn.RunCommand($"PRAGMA user_version = {version}"); + } + + private async Task CreateMessagesTable() { + await conn.RunCommand( + @"CREATE TABLE ""messages"" (" + + @"""msg_id"" INTEGER NOT NULL," + + @"""msg_type"" TEXT NOT NULL COLLATE NOCASE," + + @"""msg_created"" TEXT NOT NULL COLLATE NOCASE," + + @"""msg_deleted"" TEXT DEFAULT NULL COLLATE NOCASE," + + @"""msg_channel"" TEXT DEFAULT NULL COLLATE NOCASE," + + @"""msg_sender"" TEXT DEFAULT NULL COLLATE BINARY," + + @"""msg_sender_name"" TEXT DEFAULT NULL COLLATE NOCASE," + + @"""msg_sender_colour"" INTEGER DEFAULT NULL," + + @"""msg_sender_rank"" INTEGER DEFAULT NULL," + + @"""msg_sender_nick"" TEXT DEFAULT NULL COLLATE NOCASE," + + @"""msg_sender_perms"" INTEGER DEFAULT NULL," + + @"""msg_data"" BLOB DEFAULT NULL CHECK(JSON_VALID(""msg_data"") AND JSON_TYPE(""msg_data"") = ""object"") COLLATE BINARY," + + @"PRIMARY KEY(""msg_id"")" + + @");" + ); + await conn.RunCommand(@"CREATE INDEX ""messages_channel_index"" ON ""messages"" (""msg_channel"");"); + await conn.RunCommand(@"CREATE INDEX ""messages_created_index"" ON ""messages"" (""msg_created"");"); + await conn.RunCommand(@"CREATE INDEX ""messages_deleted_index"" ON ""messages"" (""msg_deleted"");"); + await conn.RunCommand(@"CREATE INDEX ""messages_event_type"" ON ""messages"" (""msg_type"");"); + await conn.RunCommand(@"CREATE INDEX ""messages_sender_index"" ON ""messages"" (""msg_sender"");"); + } +} diff --git a/SharpChat.SQLite/SQLiteStorage.cs b/SharpChat.SQLite/SQLiteStorage.cs new file mode 100644 index 0000000..b0eec3f --- /dev/null +++ b/SharpChat.SQLite/SQLiteStorage.cs @@ -0,0 +1,61 @@ +using SharpChat.Configuration; +using SharpChat.Messages; +using System.Data.SQLite; +using NativeSQLiteConnection = System.Data.SQLite.SQLiteConnection; + +namespace SharpChat.SQLite; + +public class SQLiteStorage(string connString) : Storage, IDisposable { + public const string MEMORY = "file::memory:?cache=shared"; + public const string DEFAULT = "sharpchat.db"; + + public SQLiteConnection Connection { get; } = new SQLiteConnection(new NativeSQLiteConnection(connString).OpenAndReturn()); + + public MessageStorage CreateMessageStorage() { + return new SQLiteMessageStorage(Connection); + } + + public async Task UpgradeStorage() { + await new SQLiteMigrations(Connection).RunMigrations(); + } + + public static string BuildConnectionString(Config config) { + return BuildConnectionString( + config.ReadValue("path", DEFAULT)!, + config.ReadValue("pass") + ); + } + + public static string BuildConnectionString(string path, string? password) { + return new SQLiteConnectionStringBuilder { + DataSource = string.IsNullOrWhiteSpace(path) ? MEMORY : path, + DateTimeFormat = SQLiteDateFormats.ISO8601, + DateTimeKind = DateTimeKind.Utc, + FailIfMissing = false, + ForeignKeys = true, + JournalMode = SQLiteJournalModeEnum.Wal, + LegacyFormat = false, + Password = string.IsNullOrWhiteSpace(password) ? null : password, + ReadOnly = false, + UseUTF16Encoding = false, + }.ToString(); + } + + private bool disposed = false; + + ~SQLiteStorage() { + DoDispose(); + } + + public void Dispose() { + DoDispose(); + GC.SuppressFinalize(this); + } + + private void DoDispose() { + if(disposed) + return; + disposed = true; + Connection.Dispose(); + } +} diff --git a/SharpChat.SQLite/SQLiteUserPermissions.cs b/SharpChat.SQLite/SQLiteUserPermissions.cs new file mode 100644 index 0000000..c09c01b --- /dev/null +++ b/SharpChat.SQLite/SQLiteUserPermissions.cs @@ -0,0 +1,26 @@ +namespace SharpChat.SQLite; + +[Flags] +public enum SQLiteUserPermissions : long { + SendMessage = 0x1L, + DeleteOwnMessage = 0x2L, + DeleteAnyMessage = 0x4L, + EditOwnMessage = 0x8L, + EditAnyMessage = 0x10L, + SendBroadcast = 0x20L, + ViewLogs = 0x40L, + KickUser = 0x80L, + BanUser = 0x100L, + PardonUser = 0x200L, + PardonIPAddress = 0x400L, + ViewIPAddress = 0x800L, + ViewBanList = 0x1000L, + CreateChannel = 0x2000L, + SetChannelPermanent = 0x4000L, + SetChannelPassword = 0x8000L, + SetChannelMinimumRank = 0x10000L, + DeleteChannel = 0x20000L, + JoinAnyChannel = 0x40000L, + SetOwnNickname = 0x80000L, + SetOthersNickname = 0x100000L, +} diff --git a/SharpChat.SQLite/SQLiteUserPermissionsConverter.cs b/SharpChat.SQLite/SQLiteUserPermissionsConverter.cs new file mode 100644 index 0000000..8ddfcfb --- /dev/null +++ b/SharpChat.SQLite/SQLiteUserPermissionsConverter.cs @@ -0,0 +1,101 @@ +namespace SharpChat.SQLite; + +public static class SQLiteUserPermissionsConverter { + public static UserPermissions From(SQLiteUserPermissions sup) { + UserPermissions up = 0; + + if(sup.HasFlag(SQLiteUserPermissions.SendMessage)) + up |= UserPermissions.SendMessage; + if(sup.HasFlag(SQLiteUserPermissions.DeleteOwnMessage)) + up |= UserPermissions.DeleteOwnMessage; + if(sup.HasFlag(SQLiteUserPermissions.DeleteAnyMessage)) + up |= UserPermissions.DeleteAnyMessage; + if(sup.HasFlag(SQLiteUserPermissions.EditOwnMessage)) + up |= UserPermissions.EditOwnMessage; + if(sup.HasFlag(SQLiteUserPermissions.EditAnyMessage)) + up |= UserPermissions.EditAnyMessage; + if(sup.HasFlag(SQLiteUserPermissions.SendBroadcast)) + up |= UserPermissions.SendBroadcast; + if(sup.HasFlag(SQLiteUserPermissions.ViewLogs)) + up |= UserPermissions.ViewLogs; + if(sup.HasFlag(SQLiteUserPermissions.KickUser)) + up |= UserPermissions.KickUser; + if(sup.HasFlag(SQLiteUserPermissions.BanUser)) + up |= UserPermissions.BanUser; + if(sup.HasFlag(SQLiteUserPermissions.PardonUser)) + up |= UserPermissions.PardonUser; + if(sup.HasFlag(SQLiteUserPermissions.PardonIPAddress)) + up |= UserPermissions.PardonIPAddress; + if(sup.HasFlag(SQLiteUserPermissions.ViewIPAddress)) + up |= UserPermissions.ViewIPAddress; + if(sup.HasFlag(SQLiteUserPermissions.ViewBanList)) + up |= UserPermissions.ViewBanList; + if(sup.HasFlag(SQLiteUserPermissions.CreateChannel)) + up |= UserPermissions.CreateChannel; + if(sup.HasFlag(SQLiteUserPermissions.SetChannelPermanent)) + up |= UserPermissions.SetChannelPermanent; + if(sup.HasFlag(SQLiteUserPermissions.SetChannelPassword)) + up |= UserPermissions.SetChannelPassword; + if(sup.HasFlag(SQLiteUserPermissions.SetChannelMinimumRank)) + up |= UserPermissions.SetChannelMinimumRank; + if(sup.HasFlag(SQLiteUserPermissions.DeleteChannel)) + up |= UserPermissions.DeleteChannel; + if(sup.HasFlag(SQLiteUserPermissions.JoinAnyChannel)) + up |= UserPermissions.JoinAnyChannel; + if(sup.HasFlag(SQLiteUserPermissions.SetOwnNickname)) + up |= UserPermissions.SetOwnNickname; + if(sup.HasFlag(SQLiteUserPermissions.SetOthersNickname)) + up |= UserPermissions.SetOthersNickname; + + return up; + } + + public static SQLiteUserPermissions To(UserPermissions up) { + SQLiteUserPermissions sup = 0; + + if(up.HasFlag(UserPermissions.SendMessage)) + sup |= SQLiteUserPermissions.SendMessage; + if(up.HasFlag(UserPermissions.DeleteOwnMessage)) + sup |= SQLiteUserPermissions.DeleteOwnMessage; + if(up.HasFlag(UserPermissions.DeleteAnyMessage)) + sup |= SQLiteUserPermissions.DeleteAnyMessage; + if(up.HasFlag(UserPermissions.EditOwnMessage)) + sup |= SQLiteUserPermissions.EditOwnMessage; + if(up.HasFlag(UserPermissions.EditAnyMessage)) + sup |= SQLiteUserPermissions.EditAnyMessage; + if(up.HasFlag(UserPermissions.SendBroadcast)) + sup |= SQLiteUserPermissions.SendBroadcast; + if(up.HasFlag(UserPermissions.ViewLogs)) + sup |= SQLiteUserPermissions.ViewLogs; + if(up.HasFlag(UserPermissions.KickUser)) + sup |= SQLiteUserPermissions.KickUser; + if(up.HasFlag(UserPermissions.BanUser)) + sup |= SQLiteUserPermissions.BanUser; + if(up.HasFlag(UserPermissions.PardonUser)) + sup |= SQLiteUserPermissions.PardonUser; + if(up.HasFlag(UserPermissions.PardonIPAddress)) + sup |= SQLiteUserPermissions.PardonIPAddress; + if(up.HasFlag(UserPermissions.ViewIPAddress)) + sup |= SQLiteUserPermissions.ViewIPAddress; + if(up.HasFlag(UserPermissions.ViewBanList)) + sup |= SQLiteUserPermissions.ViewBanList; + if(up.HasFlag(UserPermissions.CreateChannel)) + sup |= SQLiteUserPermissions.CreateChannel; + if(up.HasFlag(UserPermissions.SetChannelPermanent)) + sup |= SQLiteUserPermissions.SetChannelPermanent; + if(up.HasFlag(UserPermissions.SetChannelPassword)) + sup |= SQLiteUserPermissions.SetChannelPassword; + if(up.HasFlag(UserPermissions.SetChannelMinimumRank)) + sup |= SQLiteUserPermissions.SetChannelMinimumRank; + if(up.HasFlag(UserPermissions.DeleteChannel)) + sup |= SQLiteUserPermissions.DeleteChannel; + if(up.HasFlag(UserPermissions.JoinAnyChannel)) + sup |= SQLiteUserPermissions.JoinAnyChannel; + if(up.HasFlag(UserPermissions.SetOwnNickname)) + sup |= SQLiteUserPermissions.SetOwnNickname; + if(up.HasFlag(UserPermissions.SetOthersNickname)) + sup |= SQLiteUserPermissions.SetOthersNickname; + + return sup; + } +} diff --git a/SharpChat.SQLite/SharpChat.SQLite.csproj b/SharpChat.SQLite/SharpChat.SQLite.csproj new file mode 100644 index 0000000..b47e6ba --- /dev/null +++ b/SharpChat.SQLite/SharpChat.SQLite.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" /> + </ItemGroup> + +</Project> diff --git a/SharpChat.sln b/SharpChat.sln index f92ff48..c4c8520 100644 --- a/SharpChat.sln +++ b/SharpChat.sln @@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Storage", "Storage", "{D5EB EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChat.MariaDB", "SharpChat.MariaDB\SharpChat.MariaDB.csproj", "{5B760B2D-F0AD-46E5-B701-8C53D25E2355}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChat.SQLite", "SharpChat.SQLite\SharpChat.SQLite.csproj", "{6D74CAE7-200D-44C8-B950-9F45B843E133}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +57,10 @@ Global {5B760B2D-F0AD-46E5-B701-8C53D25E2355}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B760B2D-F0AD-46E5-B701-8C53D25E2355}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B760B2D-F0AD-46E5-B701-8C53D25E2355}.Release|Any CPU.Build.0 = Release|Any CPU + {6D74CAE7-200D-44C8-B950-9F45B843E133}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D74CAE7-200D-44C8-B950-9F45B843E133}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D74CAE7-200D-44C8-B950-9F45B843E133}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D74CAE7-200D-44C8-B950-9F45B843E133}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -63,6 +69,7 @@ Global {A9B0B652-C20F-4C62-A96A-EF7ACD2079E9} = {5BB7CDAA-06BB-4746-BA07-7EF9090774D8} {FEDDC565-B784-4D6F-BEF5-121C383D7AB2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {5B760B2D-F0AD-46E5-B701-8C53D25E2355} = {D5EB4BD7-7C69-41F5-9D94-AA5E25B26BDA} + {6D74CAE7-200D-44C8-B950-9F45B843E133} = {D5EB4BD7-7C69-41F5-9D94-AA5E25B26BDA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {42279FE1-5980-440A-87F8-25338DFE54CF} diff --git a/SharpChat/Program.cs b/SharpChat/Program.cs index 8cc8f32..6612bc2 100644 --- a/SharpChat/Program.cs +++ b/SharpChat/Program.cs @@ -1,9 +1,10 @@ using SharpChat; using SharpChat.Configuration; -using SharpChat.Messages; using SharpChat.Flashii; -using System.Text; using SharpChat.MariaDB; +using SharpChat.Messages; +using SharpChat.SQLite; +using System.Text; const string CONFIG = "sharpchat.cfg"; @@ -124,18 +125,23 @@ FlashiiClient flashii = new(httpClient, config.ScopeTo("msz")); if(hasCancelled) return; -MessageStorage msgStore; -if(string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty))) { - msgStore = new VirtualMessageStorage(); -} else { - MariaDBMessageStorage mdbes = new(MariaDBMessageStorage.BuildConnString(config.ScopeTo("mariadb"))); - msgStore = mdbes; - await mdbes.RunMigrations(); +Storage storage = string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty)) + ? new SQLiteStorage(SQLiteStorage.BuildConnectionString(config.ScopeTo("sqlite"))) + : new MariaDBStorage(MariaDBStorage.BuildConnectionString(config.ScopeTo("mariadb"))); + +try { + if(hasCancelled) return; + + await storage.UpgradeStorage(); + + if(hasCancelled) return; + + using SockChatServer scs = new(flashii, flashii, storage.CreateMessageStorage(), config.ScopeTo("chat")); + scs.Listen(mre); + + mre.WaitOne(); +} finally { + if(storage is IDisposable disp) + disp.Dispose(); } -if(hasCancelled) return; - -using SockChatServer scs = new(flashii, flashii, msgStore, config.ScopeTo("chat")); -scs.Listen(mre); - -mre.WaitOne(); diff --git a/SharpChat/SharpChat.csproj b/SharpChat/SharpChat.csproj index 16cd97b..e530e41 100644 --- a/SharpChat/SharpChat.csproj +++ b/SharpChat/SharpChat.csproj @@ -35,6 +35,7 @@ <ProjectReference Include="..\SharpChat.Flashii\SharpChat.Flashii.csproj" /> <ProjectReference Include="..\SharpChat.MariaDB\SharpChat.MariaDB.csproj" /> <ProjectReference Include="..\SharpChat.SockChat\SharpChat.SockChat.csproj" /> + <ProjectReference Include="..\SharpChat.SQLite\SharpChat.SQLite.csproj" /> <ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" /> </ItemGroup> diff --git a/SharpChatCommon/ColourInheritable.cs b/SharpChatCommon/ColourInheritable.cs index fa4558b..5820c98 100644 --- a/SharpChatCommon/ColourInheritable.cs +++ b/SharpChatCommon/ColourInheritable.cs @@ -1,14 +1,16 @@ namespace SharpChat; -public readonly record struct ColourInheritable(ColourRgb? rgb) { +public readonly record struct ColourInheritable(ColourRgb? Rgb) { public static readonly ColourInheritable None = new(null); - public override string ToString() => rgb.HasValue ? rgb.Value.ToString() : "inherit"; + public override string ToString() => Rgb.HasValue ? Rgb.Value.ToString() : "inherit"; + + public bool Inherits => !Rgb.HasValue; public static ColourInheritable FromRaw(int? raw) => raw is null ? None : new(new ColourRgb(raw.Value)); public static ColourInheritable FromRgb(byte red, byte green, byte blue) => new(ColourRgb.FromRgb(red, green, blue)); // these should go Away private const int MSZ_INHERIT = 0x40000000; - public int ToMisuzu() => rgb.HasValue ? rgb.Value.Raw : MSZ_INHERIT; + public int ToMisuzu() => Rgb.HasValue ? Rgb.Value.Raw : MSZ_INHERIT; public static ColourInheritable FromMisuzu(int msz) => (msz & MSZ_INHERIT) > 0 ? None : new(new ColourRgb(msz & 0xFFFFFF)); } diff --git a/SharpChatCommon/Messages/MessageStorage.cs b/SharpChatCommon/Messages/MessageStorage.cs index e8dce95..5024b02 100644 --- a/SharpChatCommon/Messages/MessageStorage.cs +++ b/SharpChatCommon/Messages/MessageStorage.cs @@ -2,7 +2,7 @@ namespace SharpChat.Messages; public interface MessageStorage { Task LogMessage( - long msgId, + long id, string type, string channelName, string senderId, @@ -13,7 +13,7 @@ public interface MessageStorage { UserPermissions senderPerms, object? data = null ); - Task DeleteMessage(Message evt); - Task<Message?> GetMessage(long msgId); + Task DeleteMessage(Message msg); + Task<Message?> GetMessage(long id); Task<IEnumerable<Message>> GetMessages(string channelName, int amount = 20, int offset = 0); } diff --git a/SharpChatCommon/Messages/VirtualMessageStorage.cs b/SharpChatCommon/Messages/VirtualMessageStorage.cs deleted file mode 100644 index a985c3e..0000000 --- a/SharpChatCommon/Messages/VirtualMessageStorage.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Text.Json; - -namespace SharpChat.Messages; - -public class VirtualMessageStorage : MessageStorage { - private readonly Dictionary<long, Message> Messages = []; - - public Task LogMessage( - long id, - string type, - string channelName, - string senderId, - string senderName, - ColourInheritable senderColour, - int senderRank, - string senderNick, - UserPermissions senderPerms, - object? data = null - ) { - Messages.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)) - ) - ); - - return Task.CompletedTask; - } - - public Task<Message?> GetMessage(long seqId) { - return Task.FromResult(Messages.TryGetValue(seqId, out Message? evt) ? evt : null); - } - - public Task DeleteMessage(Message evt) { - Messages.Remove(evt.Id); - return Task.CompletedTask; - } - - public Task<IEnumerable<Message>> GetMessages(string channelName, int amount = 20, int offset = 0) { - IEnumerable<Message> subset = Messages.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<Message>); - } -} diff --git a/SharpChatCommon/Storage.cs b/SharpChatCommon/Storage.cs new file mode 100644 index 0000000..df3b6a1 --- /dev/null +++ b/SharpChatCommon/Storage.cs @@ -0,0 +1,8 @@ +using SharpChat.Messages; + +namespace SharpChat; + +public interface Storage { + MessageStorage CreateMessageStorage(); + Task UpgradeStorage(); +}