diff --git a/Models/Channel.cs b/Models/Channel.cs new file mode 100644 index 0000000..835351c --- /dev/null +++ b/Models/Channel.cs @@ -0,0 +1,11 @@ +using HermesSocketServer.Store; + +namespace HermesSocketServer.Models +{ + public class Channel + { + public string Id { get; set; } + public User User { get; set; } + public ChatterStore Chatters { get; set; } + } +} \ No newline at end of file diff --git a/Models/ChatterVoice.cs b/Models/ChatterVoice.cs new file mode 100644 index 0000000..2a4f55d --- /dev/null +++ b/Models/ChatterVoice.cs @@ -0,0 +1,9 @@ +namespace HermesSocketServer.Models +{ + public class ChatterVoice + { + public string ChatterId { get; set; } + public string UserId { get; set; } + public string VoiceId { get; set; } + } +} \ No newline at end of file diff --git a/Models/User.cs b/Models/User.cs new file mode 100644 index 0000000..4eb3493 --- /dev/null +++ b/Models/User.cs @@ -0,0 +1,11 @@ +namespace HermesSocketServer.Models +{ + public class User + { + public string Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Role { get; set; } + public string DefaultVoice { get; set; } + } +} \ No newline at end of file diff --git a/Models/Voice.cs b/Models/Voice.cs new file mode 100644 index 0000000..b364a73 --- /dev/null +++ b/Models/Voice.cs @@ -0,0 +1,8 @@ +namespace HermesSocketServer.Models +{ + public class Voice + { + public string Id { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/Requests/CreateTTSUser.cs b/Requests/CreateTTSUser.cs index 756b6bc..f5d3121 100644 --- a/Requests/CreateTTSUser.cs +++ b/Requests/CreateTTSUser.cs @@ -1,6 +1,8 @@ using System.Text.Json; using HermesSocketLibrary.db; using HermesSocketLibrary.Requests; +using HermesSocketServer.Models; +using HermesSocketServer.Services; using HermesSocketServer.Store; using ILogger = Serilog.ILogger; @@ -9,14 +11,16 @@ namespace HermesSocketServer.Requests public class CreateTTSUser : IRequest { public string Name => "create_tts_user"; + private ChannelManager _channels; private Database _database; - private ChatterStore _chatters; + private readonly ServerConfiguration _configuration; private ILogger _logger; - public CreateTTSUser(ChatterStore chatters, Database database, ILogger logger) + public CreateTTSUser(ChannelManager channels, Database database, ServerConfiguration configuration, ILogger logger) { _database = database; - _chatters = chatters; + _channels = channels; + _configuration = configuration; _logger = logger; } @@ -41,10 +45,19 @@ namespace HermesSocketServer.Requests data["user"] = sender; var check = await _database.ExecuteScalar("SELECT state FROM \"TtsVoiceState\" WHERE \"userId\" = @user AND \"ttsVoiceId\" = @voice", data) ?? false; - if (check is not bool state || !state) + if ((check is not bool state || !state) && chatterId != _configuration.Tts.OwnerId) return new RequestResult(false, "Voice is disabled on this channel."); - _chatters.Set(sender, chatterId, data["voice"].ToString()); + var channel = _channels.Get(sender); + if (channel == null) + return new RequestResult(false, null); + + channel.Chatters.Set(chatterId.ToString(), new ChatterVoice() + { + UserId = sender, + ChatterId = chatterId.ToString(), + VoiceId = data["voice"].ToString()! + }); _logger.Information($"Selected a tts voice [voice: {data["voice"]}] for user [chatter: {data["chatter"]}] in channel [channel: {data["user"]}]"); return new RequestResult(true, null); } diff --git a/Requests/CreateTTSVoice.cs b/Requests/CreateTTSVoice.cs index 7e70c61..8e691f7 100644 --- a/Requests/CreateTTSVoice.cs +++ b/Requests/CreateTTSVoice.cs @@ -1,5 +1,6 @@ using System.Text.Json; using HermesSocketLibrary.Requests; +using HermesSocketServer.Models; using HermesSocketServer.Store; using ILogger = Serilog.ILogger; @@ -8,7 +9,7 @@ namespace HermesSocketServer.Requests public class CreateTTSVoice : IRequest { public string Name => "create_tts_voice"; - private IStore _voices; + private IStore _voices; private ILogger _logger; private Random _random; @@ -35,7 +36,11 @@ namespace HermesSocketServer.Requests string id = RandomString(25); - _voices.Set(id, data["voice"].ToString()); + _voices.Set(id, new Voice() + { + Id = id, + Name = data["voice"].ToString()! + }); _logger.Information($"Added a new voice [voice: {data["voice"]}][voice id: {id}]"); return new RequestResult(true, id); diff --git a/Requests/DeleteTTSVoice.cs b/Requests/DeleteTTSVoice.cs index db037bd..a77efe9 100644 --- a/Requests/DeleteTTSVoice.cs +++ b/Requests/DeleteTTSVoice.cs @@ -1,5 +1,6 @@ using System.Text.Json; using HermesSocketLibrary.Requests; +using HermesSocketServer.Models; using HermesSocketServer.Store; using ILogger = Serilog.ILogger; @@ -8,7 +9,7 @@ namespace HermesSocketServer.Requests public class DeleteTTSVoice : IRequest { public string Name => "delete_tts_voice"; - private IStore _voices; + private IStore _voices; private ILogger _logger; public DeleteTTSVoice(VoiceStore voices, ILogger logger) diff --git a/Requests/GetTTSUsers.cs b/Requests/GetTTSUsers.cs index c1c09ff..f037e01 100644 --- a/Requests/GetTTSUsers.cs +++ b/Requests/GetTTSUsers.cs @@ -1,5 +1,5 @@ using HermesSocketLibrary.Requests; -using HermesSocketServer.Store; +using HermesSocketServer.Services; using ILogger = Serilog.ILogger; namespace HermesSocketServer.Requests @@ -7,18 +7,22 @@ namespace HermesSocketServer.Requests public class GetTTSUsers : IRequest { public string Name => "get_tts_users"; - private ChatterStore _chatters; + private ChannelManager _channels; private ILogger _logger; - public GetTTSUsers(ChatterStore chatters, ILogger logger) + public GetTTSUsers(ChannelManager channels, ILogger logger) { - _chatters = chatters; + _channels = channels; _logger = logger; } public async Task Grant(string sender, IDictionary? data) { - var temp = _chatters.Get(sender); + var channel = _channels.Get(sender); + if (channel == null) + return new RequestResult(false, null, notifyClientsOnAccount: false); + + var temp = channel.Chatters.Get().ToDictionary(p => p.Key, p => p.Value.VoiceId); _logger.Information($"Fetched all chatters' selected tts voice for channel [channel: {sender}]"); return new RequestResult(true, temp, notifyClientsOnAccount: false); } diff --git a/Requests/GetTTSVoices.cs b/Requests/GetTTSVoices.cs index 362c339..79d9a6d 100644 --- a/Requests/GetTTSVoices.cs +++ b/Requests/GetTTSVoices.cs @@ -1,6 +1,7 @@ using HermesSocketLibrary.db; using HermesSocketLibrary.Requests; using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Store; using ILogger = Serilog.ILogger; namespace HermesSocketServer.Requests @@ -8,24 +9,23 @@ namespace HermesSocketServer.Requests public class GetTTSVoices : IRequest { public string Name => "get_tts_voices"; - private Database _database; + private VoiceStore _voices; private ILogger _logger; - public GetTTSVoices(Database database, ILogger logger) + public GetTTSVoices(VoiceStore voices, ILogger logger) { - _database = database; + _voices = voices; _logger = logger; } public async Task Grant(string sender, IDictionary? data) { - IList voices = new List(); - string sql = "SELECT id, name FROM \"TtsVoice\""; - await _database.Execute(sql, (IDictionary?) null, (r) => voices.Add(new VoiceDetails() + IEnumerable voices = _voices.Get().Select(v => new VoiceDetails() { - Id = r.GetString(0), - Name = r.GetString(1) - })); + Id = v.Value.Id, + Name = v.Value.Name + }); + _logger.Information($"Fetched all TTS voices for channel [channel: {sender}]"); return new RequestResult(true, voices, notifyClientsOnAccount: false); } diff --git a/Requests/UpdateTTSUser.cs b/Requests/UpdateTTSUser.cs index 6eba5b2..700ab5f 100644 --- a/Requests/UpdateTTSUser.cs +++ b/Requests/UpdateTTSUser.cs @@ -1,6 +1,9 @@ using System.Text.Json; +using System.Threading.Channels; using HermesSocketLibrary.db; using HermesSocketLibrary.Requests; +using HermesSocketServer.Models; +using HermesSocketServer.Services; using HermesSocketServer.Store; using ILogger = Serilog.ILogger; @@ -10,15 +13,15 @@ namespace HermesSocketServer.Requests { public string Name => "update_tts_user"; + private ChannelManager _channels; + private Database _database; private readonly ServerConfiguration _configuration; - private readonly Database _database; - private ChatterStore _chatters; private ILogger _logger; - public UpdateTTSUser(ChatterStore chatters, Database database, ServerConfiguration configuration, ILogger logger) + public UpdateTTSUser(ChannelManager channels, Database database, ServerConfiguration configuration, ILogger logger) { _database = database; - _chatters = chatters; + _channels = channels; _configuration = configuration; _logger = logger; } @@ -39,12 +42,19 @@ namespace HermesSocketServer.Requests data["user"] = sender; var check = await _database.ExecuteScalar("SELECT state FROM \"TtsVoiceState\" WHERE \"userId\" = @user AND \"ttsVoiceId\" = @voice", data) ?? false; - if ((check is not bool state || !state) && chatterId != _configuration.OwnerId) - { + if ((check is not bool state || !state) && chatterId != _configuration.Tts.OwnerId) return new RequestResult(false, null); - } - _chatters.Set(sender, chatterId, data["voice"].ToString()); + var channel = _channels.Get(sender); + if (channel == null) + return new RequestResult(false, null); + + channel.Chatters.Set(chatterId.ToString(), new ChatterVoice() + { + UserId = sender, + ChatterId = chatterId.ToString(), + VoiceId = data["voice"].ToString()! + }); _logger.Information($"Updated chatter's [chatter: {data["chatter"]}] selected tts voice [voice: {data["voice"]}] in channel [channel: {sender}]"); return new RequestResult(true, null); } diff --git a/Requests/UpdateTTSVoice.cs b/Requests/UpdateTTSVoice.cs index 357b979..db59524 100644 --- a/Requests/UpdateTTSVoice.cs +++ b/Requests/UpdateTTSVoice.cs @@ -1,5 +1,6 @@ using System.Text.Json; using HermesSocketLibrary.Requests; +using HermesSocketServer.Models; using HermesSocketServer.Store; using ILogger = Serilog.ILogger; @@ -8,7 +9,7 @@ namespace HermesSocketServer.Requests public class UpdateTTSVoice : IRequest { public string Name => "update_tts_voice"; - private IStore _voices; + private IStore _voices; private ILogger _logger; public UpdateTTSVoice(VoiceStore voices, ILogger logger) @@ -30,7 +31,11 @@ namespace HermesSocketServer.Requests if (data["voiceid"] is JsonElement id) data["voiceid"] = id.ToString(); - _voices.Set(data["voiceid"].ToString(), data["voice"].ToString()); + _voices.Set(data["voiceid"].ToString(), new Voice() + { + Id = data["voiceid"].ToString()!, + Name = data["voice"].ToString()! + }); _logger.Information($"Updated voice's [voice id: {data["voiceid"]}] name [new name: {data["voice"]}]"); return new RequestResult(true, null); } diff --git a/ServerConfiguration.cs b/ServerConfiguration.cs index e1fee71..15bf60c 100644 --- a/ServerConfiguration.cs +++ b/ServerConfiguration.cs @@ -5,7 +5,7 @@ namespace HermesSocketServer public string Environment; public WebsocketServerConfiguration WebsocketServer; public DatabaseConfiguration Database; - public long OwnerId; + public TTSConfiguration Tts; public string AdminPassword; } @@ -20,4 +20,10 @@ namespace HermesSocketServer public string ConnectionString; public int SaveDelayInSeconds; } + + public class TTSConfiguration + { + public long OwnerId; + public string DefaultTtsVoice; + } } \ No newline at end of file diff --git a/Services/ChannelManager.cs b/Services/ChannelManager.cs new file mode 100644 index 0000000..ea645d7 --- /dev/null +++ b/Services/ChannelManager.cs @@ -0,0 +1,56 @@ +using System.Collections.Concurrent; +using HermesSocketLibrary.db; +using HermesSocketServer.Models; +using HermesSocketServer.Store; + +namespace HermesSocketServer.Services +{ + public class ChannelManager + { + private readonly UserStore _users; + private readonly Database _database; + private readonly Serilog.ILogger _logger; + private readonly IDictionary _channels; + + public ChannelManager(UserStore users, Database database, Serilog.ILogger logger) + { + _users = users; + _database = database; + _logger = logger; + _channels = new ConcurrentDictionary(); + } + + + public async Task Add(string userId) + { + var user = _users.Get(userId); + if (user == null) + { + return; + } + if (_channels.ContainsKey(userId)) + { + return; + } + + var chatters = new ChatterStore(userId, _database, _logger); + await chatters.Load(); + + var channel = new Channel() + { + Id = userId, + User = user, + Chatters = chatters + }; + + _channels.Add(userId, channel); + } + + public Channel? Get(string channelId) + { + if (_channels.TryGetValue(channelId, out var channel)) + return channel; + return null; + } + } +} \ No newline at end of file diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 348ead0..c539bc5 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -5,13 +5,14 @@ namespace HermesSocketServer.Services public class DatabaseService : BackgroundService { private readonly VoiceStore _voices; - private readonly ChatterStore _chatters; + private readonly UserStore _users; private readonly ServerConfiguration _configuration; private readonly Serilog.ILogger _logger; - public DatabaseService(VoiceStore voices, ChatterStore chatters, ServerConfiguration configuration, Serilog.ILogger logger) { + public DatabaseService(VoiceStore voices, UserStore users, ServerConfiguration configuration, Serilog.ILogger logger) + { _voices = voices; - _chatters = chatters; + _users = users; _configuration = configuration; _logger = logger; } @@ -20,16 +21,19 @@ namespace HermesSocketServer.Services { _logger.Information("Loading TTS voices..."); await _voices.Load(); - _logger.Information("Loading TTS chatters' voice."); - await _chatters.Load(); + _logger.Information("Loading users..."); + await _users.Load(); await Task.Run(async () => { + var tasks = new List(); + await Task.Delay(TimeSpan.FromSeconds(_configuration.Database.SaveDelayInSeconds)); while (true) { - await Task.Delay(TimeSpan.FromSeconds(_configuration.Database.SaveDelayInSeconds)); - await _voices.Save(); - await _chatters.Save(); + tasks.Add(_voices.Save()); + tasks.Add(_users.Save()); + tasks.Add(Task.Delay(TimeSpan.FromSeconds(_configuration.Database.SaveDelayInSeconds))); + await Task.WhenAll(tasks); } }); } diff --git a/Socket/Handlers/HermesLoginHandler.cs b/Socket/Handlers/HermesLoginHandler.cs index d425099..ba4a5b7 100644 --- a/Socket/Handlers/HermesLoginHandler.cs +++ b/Socket/Handlers/HermesLoginHandler.cs @@ -1,6 +1,8 @@ using HermesSocketLibrary.db; using HermesSocketLibrary.Requests.Messages; using HermesSocketLibrary.Socket.Data; +using HermesSocketServer.Services; +using HermesSocketServer.Store; using ILogger = Serilog.ILogger; namespace HermesSocketServer.Socket.Handlers @@ -9,14 +11,18 @@ namespace HermesSocketServer.Socket.Handlers { public int OperationCode { get; } = 1; + private readonly ChannelManager _manager; + private readonly VoiceStore _voices; private readonly ServerConfiguration _configuration; private readonly Database _database; private readonly HermesSocketManager _sockets; private readonly ILogger _logger; private readonly object _lock; - public HermesLoginHandler(ServerConfiguration configuration, Database database, HermesSocketManager sockets, ILogger logger) + public HermesLoginHandler(ChannelManager manager, VoiceStore voices, ServerConfiguration configuration, Database database, HermesSocketManager sockets, ILogger logger) { + _manager = manager; + _voices = voices; _configuration = configuration; _database = database; _sockets = sockets; @@ -49,30 +55,31 @@ namespace HermesSocketServer.Socket.Handlers sender.WebLogin = data.WebLogin; } - var userIdDict = new Dictionary() { { "user", userId } }; - string? ttsDefaultVoice = null; - string sql2 = "select name, role, \"ttsDefaultVoice\" from \"User\" where id = @user"; - await _database.Execute(sql2, userIdDict, sql => - { - sender.Name = sql.GetString(0); - sender.Admin = sql.GetString(1) == "ADMIN"; - ttsDefaultVoice = sql.GetString(2); - }); + await _manager.Add(userId); + var channel = _manager.Get(userId); + if (channel == null) + return; + + sender.Name = channel.User.Name; + sender.Admin = channel.User.Role == "ADMIN"; if (string.IsNullOrEmpty(sender.Name)) { - _logger.Error($"Could not find username using the user id [user id: {userId}][api key: {data.ApiKey}]"); + _logger.Error($"Could not find username for a certain user [user id: {userId}][api key: {data.ApiKey}]"); return; } + if (string.IsNullOrEmpty(channel.User.DefaultVoice)) + _logger.Warning($"No default voice was set for an user [user id: {userId}][api key: {data.ApiKey}]"); var ack = new LoginAckMessage() { UserId = userId, - OwnerId = _configuration.OwnerId, + OwnerId = _configuration.Tts.OwnerId, Admin = sender.Admin, WebLogin = data.WebLogin, }; + var userIdDict = new Dictionary() { { "user", userId } }; ack.Connections = new List(); string sql3 = "select \"name\", \"type\", \"clientId\", \"accessToken\", \"grantType\", \"scope\", \"expiresAt\", \"default\" from \"Connection\" where \"userId\" = @user"; await _database.Execute(sql3, userIdDict, sql => @@ -89,9 +96,7 @@ namespace HermesSocketServer.Socket.Handlers }) ); - ack.TTSVoicesAvailable = new Dictionary(); - string sql4 = "SELECT id, name FROM \"TtsVoice\""; - await _database.Execute(sql4, (IDictionary?) null, (r) => ack.TTSVoicesAvailable.Add(r.GetString(0), r.GetString(1))); + ack.TTSVoicesAvailable = _voices.Get().ToDictionary(v => v.Key, v => v.Value.Name); ack.EnabledTTSVoices = new List(); string sql5 = $"SELECT v.name FROM \"TtsVoiceState\" s " @@ -108,8 +113,7 @@ namespace HermesSocketServer.Socket.Handlers Replace = r.GetString(2) })); - if (ttsDefaultVoice != null) - ack.DefaultTTSVoice = ttsDefaultVoice; + ack.DefaultTTSVoice = channel.User.DefaultVoice ?? _configuration.Tts.DefaultTtsVoice; await sender.Send(2, ack); @@ -120,7 +124,7 @@ namespace HermesSocketServer.Socket.Handlers { AnotherClient = true, UserId = userId, - OwnerId = _configuration.OwnerId, + OwnerId = _configuration.Tts.OwnerId, WebLogin = data.WebLogin }; diff --git a/Startup.cs b/Startup.cs index bbe069a..035c9c7 100644 --- a/Startup.cs +++ b/Startup.cs @@ -81,7 +81,7 @@ s.AddSingleton(); // Stores s.AddSingleton(); -s.AddSingleton(); +s.AddSingleton(); // Request handlers s.AddSingleton(); @@ -103,6 +103,8 @@ s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); +// Managers +s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); @@ -113,7 +115,7 @@ s.AddSingleton(new JsonSerializerOptions() }); s.AddSingleton(); - +// Background services s.AddHostedService(); var app = builder.Build(); diff --git a/Store/ChatterStore.cs b/Store/ChatterStore.cs index 047c4ef..8bc425d 100644 --- a/Store/ChatterStore.cs +++ b/Store/ChatterStore.cs @@ -1,282 +1,99 @@ -using System.Collections.Immutable; -using System.Text; using HermesSocketLibrary.db; +using HermesSocketServer.Models; namespace HermesSocketServer.Store { - public class ChatterStore : IStore + public class ChatterStore : GroupSaveStore { + private readonly string _userId; private readonly Database _database; private readonly Serilog.ILogger _logger; - private readonly IDictionary> _chatters; - private readonly IDictionary> _added; - private readonly IDictionary> _modified; - private readonly IDictionary> _deleted; - private readonly object _lock; + private readonly GroupSaveSqlGenerator _generator; - public ChatterStore(Database database, Serilog.ILogger logger) + public ChatterStore(string userId, Database database, Serilog.ILogger logger) : base(logger) { + _userId = userId; _database = database; _logger = logger; - _chatters = new Dictionary>(); - _added = new Dictionary>(); - _modified = new Dictionary>(); - _deleted = new Dictionary>(); - _lock = new object(); - } - public string? Get(string user, long key) - { - if (!_chatters.TryGetValue(user, out var broadcaster)) - return null; - if (broadcaster.TryGetValue(key, out var chatter)) - return chatter; - return null; - } - - public IEnumerable Get() - { - return _chatters.Select(c => c.Value).SelectMany(c => c.Values).ToImmutableList(); - } - - public IDictionary Get(string user) - { - if (_chatters.TryGetValue(user, out var chatters)) - return chatters.ToImmutableDictionary(); - return new Dictionary(); - } - - public async Task Load() - { - string sql = "SELECT \"chatterId\", \"ttsVoiceId\", \"userId\" FROM \"TtsChatVoice\";"; - await _database.Execute(sql, new Dictionary(), (reader) => + var ctp = new Dictionary { - var chatterId = reader.GetInt64(0); - var ttsVoiceId = reader.GetString(1); - var userId = reader.GetString(2); - if (!_chatters.TryGetValue(userId, out var chatters)) + { "chatterId", "ChatterId" }, + { "ttsVoiceId", "VoiceId" }, + { "userId", "UserId" }, + }; + _generator = new GroupSaveSqlGenerator(ctp); + } + + public override async Task Load() + { + var data = new Dictionary() { { "user", _userId } }; + string sql = $"SELECT \"chatterId\", \"ttsVoiceId\" FROM \"TtsChatVoice\" WHERE \"userId\" = @user"; + await _database.Execute(sql, data, (reader) => + { + string chatterId = reader.GetInt64(0).ToString(); + _store.Add(chatterId, new ChatterVoice() { - chatters = new Dictionary(); - _chatters.Add(userId, chatters); - } - chatters.Add(chatterId, ttsVoiceId); + UserId = _userId, + ChatterId = chatterId, + VoiceId = reader.GetString(1) + }); }); - _logger.Information($"Loaded {_chatters.Count} TTS voices from database."); + _logger.Information($"Loaded {_store.Count} TTS chatter voices from database."); } - public void Remove(string user, long? key) + public override void OnInitialAdd(string key, ChatterVoice value) { - if (key == null) - return; - - lock (_lock) - { - if (_chatters.TryGetValue(user, out var chatters) && chatters.Remove(key.Value)) - { - if (!_added.TryGetValue(user, out var added) || !added.Remove(key.Value)) - { - if (_modified.TryGetValue(user, out var modified)) - modified.Remove(key.Value); - if (!_deleted.TryGetValue(user, out var deleted)) - { - deleted = new List(); - _deleted.Add(user, deleted); - deleted.Add(key.Value); - } - else if (!deleted.Contains(key.Value)) - deleted.Add(key.Value); - } - } - } } - public void Remove(string? leftKey, long rightKey) + public override void OnInitialModify(string key, ChatterVoice value) { - throw new NotImplementedException(); } - public async Task Save() + public override void OnInitialRemove(string key) { - var changes = false; - var sb = new StringBuilder(); - var sql = ""; + } + + public override async Task Save() + { + int count = 0; + string sql = string.Empty; if (_added.Any()) { - int count = _added.Count; - sb.Append("INSERT INTO \"TtsChatVoice\" (\"chatterId\", \"ttsVoiceId\", \"userId\") VALUES "); lock (_lock) { - foreach (var broadcaster in _added) - { - var userId = broadcaster.Key; - var user = _chatters[userId]; - foreach (var chatterId in broadcaster.Value) - { - var voiceId = user[chatterId]; - sb.Append("(") - .Append(chatterId) - .Append(",'") - .Append(voiceId) - .Append("','") - .Append(userId) - .Append("'),"); - } - } - sb.Remove(sb.Length - 1, 1) - .Append(';'); - - sql = sb.ToString(); - sb.Clear(); + count = _added.Count; + sql = _generator.GenerateInsertSql("TtsChatVoice", _added.Select(a => _store[a]), ["userId", "chatterId", "ttsVoiceId"]); _added.Clear(); } - try - { - _logger.Debug($"About to save {count} voices to database."); - await _database.ExecuteScalar(sql); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to save TTS voices on database: " + sql); - } - changes = true; + _logger.Debug($"ADD {count} " + sql); + await _database.ExecuteScalar(sql); } - if (_modified.Any()) { - int count = _modified.Count; - sb.Append("UPDATE \"TtsChatVoice\" as t SET \"ttsVoiceId\" = c.\"ttsVoiceId\" FROM (VALUES "); lock (_lock) { - foreach (var broadcaster in _modified) - { - var userId = broadcaster.Key; - var user = _chatters[userId]; - foreach (var chatterId in broadcaster.Value) - { - var voiceId = user[chatterId]; - sb.Append("(") - .Append(chatterId) - .Append(",'") - .Append(voiceId) - .Append("','") - .Append(userId) - .Append("'),"); - } - } - sb.Remove(sb.Length - 1, 1) - .Append(") AS c(\"chatterId\", \"ttsVoiceId\", \"userId\") WHERE \"userId\" = c.\"userId\" AND \"chatterId\" = c.\"chatterId\";"); - - sql = sb.ToString(); - sb.Clear(); + count = _modified.Count; + sql = _generator.GenerateUpdateSql("TtsChatVoice", _modified.Select(m => _store[m]), ["userId", "chatterId"], ["ttsVoiceId"]); _modified.Clear(); } - - try - { - _logger.Debug($"About to update {count} voices on the database."); - await _database.ExecuteScalar(sql); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to modify TTS voices on database: " + sql); - } - changes = true; + _logger.Debug($"MOD {count} " + sql); + await _database.ExecuteScalar(sql); } - if (_deleted.Any()) { - int count = _deleted.Count; - sb.Append("DELETE FROM \"TtsChatVoice\" WHERE (\"chatterId\", \"userId\") IN ("); lock (_lock) { - foreach (var broadcaster in _deleted) - { - var userId = broadcaster.Key; - var user = _chatters[userId]; - foreach (var chatterId in broadcaster.Value) - { - sb.Append("(") - .Append(chatterId) - .Append(",'") - .Append(userId) - .Append("'),"); - } - } - sb.Remove(sb.Length - 1, 1) - .Append(");"); - - sql = sb.ToString(); - sb.Clear(); + count = _deleted.Count; + sql = _generator.GenerateDeleteSql("TtsChatVoice", _deleted, ["userId", "chatterId"]); _deleted.Clear(); } - - try - { - _logger.Debug($"About to delete {count} voices from the database."); - await _database.ExecuteScalar(sql); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to modify TTS voices on database: " + sql); - } - changes = true; + _logger.Debug($"DEL {count} " + sql); + await _database.ExecuteScalar(sql); } - return changes; - } - - public bool Set(string? user, long key, string? value) - { - if (user == null || value == null) - return false; - - lock (_lock) - { - if (!_chatters.TryGetValue(user, out var broadcaster)) - { - broadcaster = new Dictionary(); - _chatters.Add(user, broadcaster); - } - - if (broadcaster.TryGetValue(key, out var chatter)) - { - if (chatter != value) - { - broadcaster[key] = value; - if (!_added.TryGetValue(user, out var added) || !added.Contains(key)) - { - if (!_modified.TryGetValue(user, out var modified)) - { - modified = new List(); - _modified.Add(user, modified); - modified.Add(key); - } - else if (!modified.Contains(key)) - modified.Add(key); - } - } - } - else - { - broadcaster.Add(key, value); - _added.TryAdd(user, new List()); - - if (!_deleted.TryGetValue(user, out var deleted) || !deleted.Remove(key)) - { - if (!_added.TryGetValue(user, out var added)) - { - added = new List(); - _added.Add(user, added); - added.Add(key); - } - else if (!added.Contains(key)) - added.Add(key); - } - } - } - return true; } } diff --git a/Store/GroupSaveSqlGenerator.cs b/Store/GroupSaveSqlGenerator.cs new file mode 100644 index 0000000..e57100f --- /dev/null +++ b/Store/GroupSaveSqlGenerator.cs @@ -0,0 +1,138 @@ +using System.Reflection; +using System.Text; + +namespace HermesSocketServer.Store +{ + public class GroupSaveSqlGenerator + { + private readonly IDictionary columnPropertyRelations; + + public GroupSaveSqlGenerator(IDictionary columnsToProperties) + { + columnPropertyRelations = columnsToProperties.ToDictionary(p => p.Key, p => typeof(T).GetProperty(p.Value)); + + var nullProperties = columnPropertyRelations.Where(p => p.Value == null) + .Select(p => columnsToProperties[p.Key]); + if (nullProperties.Any()) + throw new ArgumentException("Some properties do not exist on the values given: " + string.Join(", ", nullProperties)); + } + + public string GenerateInsertSql(string table, IEnumerable values, IEnumerable columns) + { + if (string.IsNullOrWhiteSpace(table)) + throw new ArgumentException("Value is either null or whitespace-filled.", nameof(table)); + if (values == null) + throw new ArgumentNullException(nameof(values)); + if (!values.Any()) + throw new ArgumentException("Empty list given.", nameof(values)); + if (columns == null) + throw new ArgumentNullException(nameof(columns)); + if (!columns.Any()) + throw new ArgumentException("Empty list given.", nameof(columns)); + + var ctp = columns.ToDictionary(c => c, c => columnPropertyRelations[c]); + var sb = new StringBuilder(); + sb.Append($"INSERT INTO \"{table}\" (\"{string.Join("\", \"", columns)}\") VALUES "); + foreach (var value in values) + { + sb.Append("("); + foreach (var column in columns) + { + var propValue = columnPropertyRelations[column]!.GetValue(value); + var propType = columnPropertyRelations[column]!.PropertyType; + WriteValue(sb, propValue, propType); + sb.Append(","); + } + sb.Remove(sb.Length - 1, 1) + .Append("),"); + } + sb.Remove(sb.Length - 1, 1) + .Append(';'); + return sb.ToString(); + } + + public string GenerateUpdateSql(string table, IEnumerable values, IEnumerable keyColumns, IEnumerable updateColumns) + { + if (string.IsNullOrWhiteSpace(table)) + throw new ArgumentException("Value is either null or whitespace-filled.", nameof(table)); + if (values == null) + throw new ArgumentNullException(nameof(values)); + if (!values.Any()) + throw new ArgumentException("Empty list given.", nameof(values)); + if (keyColumns == null) + throw new ArgumentNullException(nameof(keyColumns)); + if (!keyColumns.Any()) + throw new ArgumentException("Empty list given.", nameof(keyColumns)); + if (updateColumns == null) + throw new ArgumentNullException(nameof(updateColumns)); + if (!updateColumns.Any()) + throw new ArgumentException("Empty list given.", nameof(updateColumns)); + + var columns = keyColumns.Union(updateColumns); + var ctp = columns.ToDictionary(c => c, c => columnPropertyRelations[c]); + var sb = new StringBuilder(); + sb.Append($"UPDATE \"{table}\" as t SET {string.Join(", ", updateColumns.Select(c => "\"" + c + "\" = c.\"" + c + "\""))} FROM (VALUES "); + foreach (var value in values) + { + sb.Append("("); + foreach (var column in columns) + { + var propValue = columnPropertyRelations[column]!.GetValue(value); + var propType = columnPropertyRelations[column]!.PropertyType; + WriteValue(sb, propValue, propType); + sb.Append(","); + } + sb.Remove(sb.Length - 1, 1) + .Append("),"); + } + sb.Remove(sb.Length - 1, 1) + .Append($") AS c(\"{string.Join("\", \"", columns)}\") WHERE id = c.id;"); + + return sb.ToString(); + } + + public string GenerateDeleteSql(string table, IEnumerable keys, IEnumerable keyColumns) + { + if (string.IsNullOrWhiteSpace(table)) + throw new ArgumentException("Value is either null or whitespace-filled.", nameof(table)); + if (keys == null) + throw new ArgumentNullException(nameof(keys)); + if (!keys.Any()) + throw new ArgumentException("Empty list given.", nameof(keys)); + if (keyColumns == null) + throw new ArgumentNullException(nameof(keyColumns)); + if (!keyColumns.Any()) + throw new ArgumentException("Empty list given.", nameof(keyColumns)); + + var ctp = keyColumns.ToDictionary(c => c, c => columnPropertyRelations[c]); + var sb = new StringBuilder(); + sb.Append($"DELETE FROM \"{table}\" WHERE (\"{string.Join("\", \"", keyColumns)}\") IN ("); + foreach (var k in keys) + { + sb.Append("("); + foreach (var column in keyColumns) + { + var propType = columnPropertyRelations[column]!.PropertyType; + WriteValue(sb, k, propType); + sb.Append(","); + } + sb.Remove(sb.Length - 1, 1) + .Append("),"); + } + sb.Remove(sb.Length - 1, 1) + .Append(");"); + + return sb.ToString(); + } + + private void WriteValue(StringBuilder sb, object? value, Type type) + { + if (type == typeof(string)) + sb.Append("'") + .Append(value) + .Append("'"); + else + sb.Append(value); + } + } +} \ No newline at end of file diff --git a/Store/GroupedSaveStore.cs b/Store/GroupedSaveStore.cs new file mode 100644 index 0000000..996010d --- /dev/null +++ b/Store/GroupedSaveStore.cs @@ -0,0 +1,112 @@ + + +using System.Collections.Immutable; + +namespace HermesSocketServer.Store +{ + public abstract class GroupSaveStore : IStore where K : class where V : class + { + private readonly Serilog.ILogger _logger; + protected readonly IDictionary _store; + protected readonly IList _added; + protected readonly IList _modified; + protected readonly IList _deleted; + protected readonly object _lock; + + + public GroupSaveStore(Serilog.ILogger logger) + { + _logger = logger; + _store = new Dictionary(); + _added = new List(); + _modified = new List(); + _deleted = new List(); + _lock = new object(); + } + + public abstract Task Load(); + public abstract void OnInitialAdd(K key, V value); + public abstract void OnInitialModify(K key, V value); + public abstract void OnInitialRemove(K key); + public abstract Task Save(); + + public V? Get(K key) + { + lock (_lock) + { + if (_store.TryGetValue(key, out var value)) + return value; + } + return null; + } + + public IDictionary Get() + { + lock (_lock) + { + return _store.ToImmutableDictionary(); + } + } + + public void Remove(K? key) + { + if (key == null) + return; + + lock (_lock) + { + if (_store.Remove(key)) + { + _logger.Information($"removed key from _deleted {key}"); + OnInitialRemove(key); + if (!_added.Remove(key)) + { + _modified.Remove(key); + _logger.Information($"removed key from _added & _modified {key}"); + if (!_deleted.Contains(key)) + { + _deleted.Add(key); + _logger.Information($"added key to _deleted {key}"); + } + } + } + } + } + + public bool Set(K? key, V? value) + { + if (key == null || value == null) + return false; + + lock (_lock) + { + if (_store.TryGetValue(key, out V? fetched)) + { + if (fetched != value) + { + OnInitialModify(key, value); + _store[key] = value; + if (!_added.Contains(key) && !_modified.Contains(key)) + { + _modified.Add(key); + _logger.Information($"added key to _modified {key}"); + } + return true; + } + } + else + { + OnInitialAdd(key, value); + _store.Add(key, value); + if (!_deleted.Remove(key) && !_added.Contains(key)) + { + _added.Add(key); + _logger.Information($"added key to _added {key}"); + } + return true; + } + } + return false; + } + } +} \ No newline at end of file diff --git a/Store/IStore.cs b/Store/IStore.cs index 4a4a4ee..42abf75 100644 --- a/Store/IStore.cs +++ b/Store/IStore.cs @@ -9,14 +9,4 @@ namespace HermesSocketServer.Store Task Save(); bool Set(K? key, V? value); } - - public interface IStore - { - V? Get(L leftKey, R rightKey); - IDictionary Get(L leftKey); - Task Load(); - void Remove(L? leftKey, R? rightKey); - Task Save(); - bool Set(L? leftKey, R? rightKey, V? value); - } } \ No newline at end of file diff --git a/Store/UserStore.cs b/Store/UserStore.cs new file mode 100644 index 0000000..ba7004c --- /dev/null +++ b/Store/UserStore.cs @@ -0,0 +1,94 @@ +using HermesSocketLibrary.db; +using HermesSocketServer.Models; + +namespace HermesSocketServer.Store +{ + public class UserStore : GroupSaveStore + { + private readonly Database _database; + private readonly Serilog.ILogger _logger; + private readonly GroupSaveSqlGenerator _generator; + + + public UserStore(Database database, Serilog.ILogger logger) : base(logger) + { + _database = database; + _logger = logger; + + var ctp = new Dictionary + { + { "id", "Id" }, + { "name", "Name" }, + { "email", "Email" }, + { "role", "Role" }, + { "ttsDefaultVoice", "DefaultVoice" } + }; + _generator = new GroupSaveSqlGenerator(ctp); + } + + public override async Task Load() + { + string sql = "SELECT id, name, email, role, \"ttsDefaultVoice\" FROM \"User\";"; + await _database.Execute(sql, new Dictionary(), (reader) => + { + string id = reader.GetString(0); + _store.Add(id, new User() + { + Id = id, + Name = reader.GetString(1), + Email = reader.GetString(2), + Role = reader.GetString(3), + DefaultVoice = reader.GetString(4), + }); + }); + _logger.Information($"Loaded {_store.Count} users from database."); + } + + public override void OnInitialAdd(string key, User value) + { + } + + public override void OnInitialModify(string key, User value) + { + } + + public override void OnInitialRemove(string key) + { + } + + public override async Task Save() + { + if (_added.Any()) + { + string sql = string.Empty; + lock (_lock) + { + sql = _generator.GenerateInsertSql("User", _added.Select(a => _store[a]), ["id", "name", "email", "role", "ttsDefaultVoice"]); + _added.Clear(); + } + await _database.ExecuteScalar(sql); + } + if (_modified.Any()) + { + string sql = string.Empty; + lock (_lock) + { + sql = _generator.GenerateUpdateSql("User", _modified.Select(m => _store[m]), ["id"], ["name", "email", "role", "ttsDefaultVoice"]); + _modified.Clear(); + } + await _database.ExecuteScalar(sql); + } + if (_deleted.Any()) + { + string sql = string.Empty; + lock (_lock) + { + sql = _generator.GenerateDeleteSql("User", _deleted, ["id"]); + _deleted.Clear(); + } + await _database.ExecuteScalar(sql); + } + return true; + } + } +} \ No newline at end of file diff --git a/Store/VoiceStore.cs b/Store/VoiceStore.cs index f6d5ed8..995178e 100644 --- a/Store/VoiceStore.cs +++ b/Store/VoiceStore.cs @@ -1,223 +1,102 @@ -using System.Collections.Immutable; -using System.Text; using HermesSocketLibrary.db; +using HermesSocketServer.Models; using HermesSocketServer.Validators; namespace HermesSocketServer.Store { - public class VoiceStore : IStore + public class VoiceStore : GroupSaveStore { + private readonly VoiceIdValidator _idValidator; + private readonly VoiceNameValidator _nameValidator; private readonly Database _database; - private readonly IValidator _voiceIdValidator; - private readonly IValidator _voiceNameValidator; private readonly Serilog.ILogger _logger; - private readonly IDictionary _voices; - private readonly IList _added; - private readonly IList _modified; - private readonly IList _deleted; - private readonly object _lock; - - public DateTime PreviousSave; + private readonly GroupSaveSqlGenerator _generator; - public VoiceStore(Database database, VoiceIdValidator voiceIdValidator, VoiceNameValidator voiceNameValidator, Serilog.ILogger logger) + public VoiceStore(VoiceIdValidator voiceIdValidator, VoiceNameValidator voiceNameValidator, Database database, Serilog.ILogger logger) : base(logger) { + _idValidator = voiceIdValidator; + _nameValidator = voiceNameValidator; _database = database; - _voiceIdValidator = voiceIdValidator; - _voiceNameValidator = voiceNameValidator; _logger = logger; - _voices = new Dictionary(); - _added = new List(); - _modified = new List(); - _deleted = new List(); - _lock = new object(); - PreviousSave = DateTime.UtcNow; + var ctp = new Dictionary + { + { "id", "Id" }, + { "name", "Name" } + }; + _generator = new GroupSaveSqlGenerator(ctp); } - public string? Get(string key) - { - if (_voices.TryGetValue(key, out var voice)) - return voice; - return null; - } - - public IDictionary Get() - { - return _voices.ToImmutableDictionary(); - } - - public async Task Load() + public override async Task Load() { string sql = "SELECT id, name FROM \"TtsVoice\";"; await _database.Execute(sql, new Dictionary(), (reader) => { - var id = reader.GetString(0); - var name = reader.GetString(1); - _voices.Add(id, name); - }); - _logger.Information($"Loaded {_voices.Count} TTS voices from database."); - } - - public void Remove(string? key) - { - if (key == null) - return; - - lock (_lock) - { - if (_voices.ContainsKey(key)) + string id = reader.GetString(0); + _store.Add(id, new Voice() { - _voices.Remove(key); - if (!_added.Remove(key)) - { - _modified.Remove(key); - if (!_deleted.Contains(key)) - _deleted.Add(key); - } - } - } + Id = id, + Name = reader.GetString(1), + }); + }); + _logger.Information($"Loaded {_store.Count} TTS voices from database."); } - public async Task Save() + public override void OnInitialAdd(string key, Voice value) { - var changes = false; - var sb = new StringBuilder(); - var sql = ""; + _idValidator.Check(value.Id); + _nameValidator.Check(value.Name); + } + + public override void OnInitialModify(string key, Voice value) + { + _nameValidator.Check(value.Name); + } + + public override void OnInitialRemove(string key) + { + } + + public override async Task Save() + { + int count = 0; + string sql = string.Empty; if (_added.Any()) { - int count = _added.Count; - sb.Append("INSERT INTO \"TtsVoice\" (id, name) VALUES "); lock (_lock) { - foreach (var voiceId in _added) - { - string voice = _voices[voiceId]; - sb.Append("('") - .Append(voiceId) - .Append("','") - .Append(voice) - .Append("'),"); - } - sb.Remove(sb.Length - 1, 1) - .Append(';'); - - sql = sb.ToString(); - sb.Clear(); + count = _added.Count; + sql = _generator.GenerateInsertSql("TtsVoice", _added.Select(a => _store[a]), ["id", "name"]); _added.Clear(); } - try - { - _logger.Debug($"About to save {count} voices to database."); - await _database.ExecuteScalar(sql); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to save TTS voices on database: " + sql); - } - changes = true; + _logger.Debug($"ADD {count} " + sql); + await _database.ExecuteScalar(sql); } - if (_modified.Any()) { - int count = _modified.Count; - sb.Append("UPDATE \"TtsVoice\" as t SET name = c.name FROM (VALUES "); lock (_lock) { - foreach (var voiceId in _modified) - { - string voice = _voices[voiceId]; - sb.Append("('") - .Append(voiceId) - .Append("','") - .Append(voice) - .Append("'),"); - } - sb.Remove(sb.Length - 1, 1) - .Append(") AS c(id, name) WHERE id = c.id;"); - - - sql = sb.ToString(); - sb.Clear(); + count = _modified.Count; + sql = _generator.GenerateUpdateSql("TtsVoice", _modified.Select(m => _store[m]), ["id"], ["name"]); _modified.Clear(); } - - try - { - _logger.Debug($"About to update {count} voices on the database."); - await _database.ExecuteScalar(sql); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to modify TTS voices on database: " + sql); - } - changes = true; + _logger.Debug($"MOD {count} " + sql); + await _database.ExecuteScalar(sql); } - if (_deleted.Any()) { - int count = _deleted.Count; - sb.Append("DELETE FROM \"TtsVoice\" WHERE id IN ("); lock (_lock) { - foreach (var voiceId in _deleted) - { - sb.Append("'") - .Append(voiceId) - .Append("',"); - } - sb.Remove(sb.Length - 1, 1) - .Append(");"); - - - sql = sb.ToString(); - sb.Clear(); + count = _deleted.Count; + sql = _generator.GenerateDeleteSql("TtsVoice", _deleted, ["id"]); _deleted.Clear(); } - - try - { - _logger.Debug($"About to delete {count} voices from the database."); - await _database.ExecuteScalar(sql); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to modify TTS voices on database: " + sql); - } - changes = true; + _logger.Debug($"DEL {count} " + sql); + await _database.ExecuteScalar(sql); } - return changes; - } - - public bool Set(string? key, string? value) - { - if (key == null || value == null) - return false; - _voiceNameValidator.Check(value); - - lock (_lock) - { - if (_voices.TryGetValue(key, out var voice)) - { - - if (voice != value) - { - _voices[key] = value; - if (!_added.Contains(key) && !_modified.Contains(key)) - _modified.Add(key); - } - } - else - { - _voiceIdValidator.Check(key); - _voices.Add(key, value); - if (!_deleted.Remove(key) && !_added.Contains(key)) - _added.Add(key); - } - } - return true; } }