using Microsoft.Extensions.Logging; using SharpChat.Data; using SharpChat.Messages; using SharpChat.Users; using System.Data; using System.Data.Common; using System.Data.SQLite; using System.Text; using System.Text.Json; using ZLogger; namespace SharpChat.SQLite; public class SQLiteMessageStorage(ILogger logger, SQLiteConnection conn) : MessageStorage { public async Task LogMessage(Message msg) { try { await conn.RunCommand( "INSERT OR IGNORE 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", msg.Id), new SQLiteParameter("type", msg.Type), new SQLiteParameter("channel", string.IsNullOrWhiteSpace(msg.ChannelName) ? null : msg.ChannelName), new SQLiteParameter("data", JsonSerializer.SerializeToUtf8Bytes(msg.Data)), new SQLiteParameter("sender", long.TryParse(msg.SenderId, out long senderId64) && senderId64 > 0 ? senderId64 : null), new SQLiteParameter("sender_name", msg.SenderName), new SQLiteParameter("sender_colour", msg.SenderColour.Rgb.HasValue ? msg.SenderColour.Rgb.Value.Raw : null), new SQLiteParameter("sender_rank", msg.SenderRank), new SQLiteParameter("sender_nick", string.IsNullOrWhiteSpace(msg.SenderNickName) ? null : msg.SenderNickName), new SQLiteParameter("sender_perms", SQLiteUserPermissionsConverter.To(msg.SenderPermissions)), new SQLiteParameter("created", $"{msg.Created:s}Z"), new SQLiteParameter("deleted", msg.Deleted is null ? null : $"{msg.Deleted:s}Z") ); } catch(SQLiteException 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 { 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.ZLogError($"Error in LogMessage(long, ...): {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.ZLogError($"Error in DeleteMessage(): {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"), reader.IsDBNull(reader.GetOrdinal("msg_sender_colour")) ? ColourInheritable.None : ColourInheritable.FromRaw((int)reader.GetInt64("msg_sender_colour")), (int)reader.GetInt64("msg_sender_rank"), SQLiteUserPermissionsConverter.From((SQLiteUserPermissions)reader.GetInt64("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) ); return reader?.Read() == true ? ReadMessage(reader) : null; } catch(SQLiteException ex) { logger.ZLogError($"Error in GetMessage(): {ex}"); return null; } } public async Task<long> CountMessages( string? channelName = null, bool includeDeleted = false ) { List<SQLiteParameter> parameters = []; bool firstParam = true; StringBuilder qb = new(); qb.Append("SELECT COUNT(*) FROM messages"); if(!includeDeleted) { firstParam = false; qb.Append(" WHERE msg_deleted IS NULL"); } if(!string.IsNullOrEmpty(channelName)) { qb.AppendFormat(" {0} (msg_channel = @channel OR msg_channel IS NULL)", firstParam ? "WHERE" : "AND"); parameters.Add(new SQLiteParameter("channel", channelName)); } try { return await conn.RunQueryValue<long>(qb.ToString(), [.. parameters]); } catch(SQLiteException 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<SQLiteParameter> parameters = []; bool firstParam = true; StringBuilder qb = new(); qb.Append("SELECT msg_id, msg_type, msg_created, msg_deleted, msg_channel, msg_data"); qb.Append(", msg_sender, msg_sender_name, msg_sender_colour, msg_sender_rank, msg_sender_nick, msg_sender_perms"); qb.Append(" FROM messages"); if(!includeDeleted) { firstParam = false; qb.Append(" WHERE msg_deleted IS NULL"); } if(!string.IsNullOrEmpty(channelName)) { qb.AppendFormat(" {0} (msg_channel = @channel OR msg_channel IS NULL)", firstParam ? "WHERE" : "AND"); parameters.Add(new SQLiteParameter("channel", channelName)); firstParam = false; } if(beforeId.HasValue) { qb.AppendFormat(" {0} msg_id < @before", firstParam ? "WHERE" : "AND"); parameters.Add(new SQLiteParameter("before", beforeId.Value)); } qb.Append(" ORDER BY msg_id DESC"); if(take.HasValue) { qb.Append(" LIMIT @take"); parameters.Add(new SQLiteParameter("take", take.Value)); } string query = string.Format("SELECT * FROM ({0}) AS _ ORDER BY msg_id ASC", qb); try { DbDataReader? reader = await conn.RunQuery(query, [.. parameters]); return reader is null ? [] : new DbObjectEnumerable<Message>(reader, ReadMessage); } catch(SQLiteException ex) { logger.ZLogError($"Error in GetMessages({channelName}, {take}, {beforeId}, {includeDeleted}): {ex}"); return []; } } }