using Microsoft.Extensions.Logging; using MySqlConnector; using SharpChat.Data; using SharpChat.Messages; using System.Data.Common; using System.Text; using System.Text.Json; using ZLogger; namespace SharpChat.MariaDB; public class MariaDBMessageStorage(MariaDBStorage storage, ILogger logger) : MessageStorage { public async Task LogMessage(Message msg) { try { using MariaDBConnection conn = await storage.CreateConnection(); await conn.RunCommand( "INSERT IGNORE INTO sqc_events (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" + ", event_created, event_deleted)" + " VALUES (@id, @type, @channel, @data" + ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms" + ", FROM_UNIXTIME(@created), FROM_UNIXTIME(@deleted))", new MySqlParameter("id", msg.Id), new MySqlParameter("type", msg.Type), new MySqlParameter("channel", string.IsNullOrWhiteSpace(msg.ChannelName) ? null : msg.ChannelName), new MySqlParameter("data", JsonSerializer.SerializeToUtf8Bytes(msg.Data)), new MySqlParameter("sender", long.TryParse(msg.SenderId, out long senderId64) && senderId64 > 0 ? senderId64 : null), new MySqlParameter("sender_name", msg.SenderName), new MySqlParameter("sender_colour", msg.SenderColour.ToMisuzu()), new MySqlParameter("sender_rank", msg.SenderRank), new MySqlParameter("sender_nick", string.IsNullOrWhiteSpace(msg.SenderNickName) ? null : msg.SenderNickName), new MySqlParameter("sender_perms", MariaDBUserPermissionsConverter.To(msg.SenderPermissions)), new MySqlParameter("created", msg.Created.ToUnixTimeSeconds()), new MySqlParameter("deleted", msg.Deleted?.ToUnixTimeSeconds()) ); } catch(MySqlException ex) { logger.ZLogError($"Error in LogMessage(Message): {ex}"); } } 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 { 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.ZLogError($"Error in LogMessage(long, ...): {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.ZLogError($"Error in DeleteMessage(): {ex}"); } } private static Message ReadMessage(DbDataReader reader) { using Stream data = reader.GetStream(reader.GetOrdinal("event_data")); return new Message( reader.GetInt64(reader.GetOrdinal("event_id")), reader.GetString(reader.GetOrdinal("event_type")), reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : reader.GetString(reader.GetOrdinal("event_sender")), reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString(reader.GetOrdinal("event_sender_name")), ColourInheritable.FromMisuzu(reader.GetInt32(reader.GetOrdinal("event_sender_colour"))), reader.GetInt32(reader.GetOrdinal("event_sender_rank")), MariaDBUserPermissionsConverter.From((MariaDBUserPermissions)reader.GetInt32(reader.GetOrdinal("event_sender_perms"))), reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? string.Empty : reader.GetString(reader.GetOrdinal("event_sender_nick")), DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32(reader.GetOrdinal("event_created"))), reader.IsDBNull(reader.GetOrdinal("event_deleted")) ? null : DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32(reader.GetOrdinal("event_deleted"))), reader.IsDBNull(reader.GetOrdinal("event_channel")) ? null : reader.GetString(reader.GetOrdinal("event_channel")), JsonDocument.Parse(data) ); } public async Task<Message?> GetMessage(long id) { try { 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", id) ); if(reader is null) return null; while(reader.Read()) { Message evt = ReadMessage(reader); if(evt != null) return evt; } } catch(MySqlException ex) { logger.ZLogError($"Error in GetMessage(): {ex}"); } return null; } public async Task<long> CountMessages( string? channelName = null, bool includeDeleted = false ) { List<MySqlParameter> parameters = []; bool firstParam = true; StringBuilder qb = new(); qb.Append("SELECT COUNT(*) FROM sqc_events"); if(!includeDeleted) { firstParam = false; qb.Append(" WHERE event_deleted IS NULL"); } if(!string.IsNullOrEmpty(channelName)) { qb.AppendFormat(" {0} (event_channel = @channel OR event_channel IS NULL)", firstParam ? "WHERE" : "AND"); parameters.Add(new MySqlParameter("channel", channelName)); } try { using MariaDBConnection conn = await storage.CreateConnection(); return await conn.RunQueryValue<long>(qb.ToString(), [.. parameters]); } catch(MySqlException ex) { logger.ZLogError($"Error in CountMessages({channelName}, {includeDeleted}): {ex}"); return 0; } } public async Task<IEnumerable<Message>> GetMessages( string? channelName = null, int? take = 20, long? beforeId = null, bool includeDeleted = false ) { List<MySqlParameter> parameters = []; bool firstParam = true; StringBuilder qb = new(); qb.Append("SELECT event_id, event_type, event_data, event_channel"); qb.Append(", event_sender, event_sender_name, event_sender_colour, event_sender_rank, event_sender_nick, event_sender_perms"); qb.Append(", UNIX_TIMESTAMP(event_created) AS event_created"); qb.Append(", UNIX_TIMESTAMP(event_deleted) AS event_deleted"); qb.Append(" FROM sqc_events"); if(!includeDeleted) { firstParam = false; qb.Append(" WHERE event_deleted IS NULL"); } if(!string.IsNullOrEmpty(channelName)) { qb.AppendFormat(" {0} (event_channel = @channel OR event_channel IS NULL)", firstParam ? "WHERE" : "AND"); parameters.Add(new MySqlParameter("channel", channelName)); firstParam = false; } if(beforeId.HasValue) { qb.AppendFormat(" {0} event_id < @before", firstParam ? "WHERE" : "AND"); parameters.Add(new MySqlParameter("before", beforeId.Value)); } qb.Append(" ORDER BY event_id DESC"); if(take.HasValue) { qb.Append(" LIMIT @take"); parameters.Add(new MySqlParameter("take", take.Value)); } string query = string.Format("SELECT * FROM ({0}) AS _ ORDER BY event_id ASC", qb); try { MariaDBConnection conn = await storage.CreateConnection(); DbDataReader? reader = await conn.RunQuery(query, [.. parameters]); if(reader is null) { conn.Dispose(); return []; } return new DbObjectEnumerable<Message>(reader, ReadMessage, () => conn.Dispose()); } catch(MySqlException ex) { logger.ZLogError($"Error in GetMessages({channelName}, {take}, {beforeId}, {includeDeleted}): {ex}"); return []; } } }