diff --git a/Requests/CreateTTSUser.cs b/Requests/CreateTTSUser.cs index f33686f..756b6bc 100644 --- a/Requests/CreateTTSUser.cs +++ b/Requests/CreateTTSUser.cs @@ -1,6 +1,7 @@ using System.Text.Json; using HermesSocketLibrary.db; using HermesSocketLibrary.Requests; +using HermesSocketServer.Store; using ILogger = Serilog.ILogger; namespace HermesSocketServer.Requests @@ -9,11 +10,13 @@ namespace HermesSocketServer.Requests { public string Name => "create_tts_user"; private Database _database; + private ChatterStore _chatters; private ILogger _logger; - public CreateTTSUser(Database database, ILogger logger) + public CreateTTSUser(ChatterStore chatters, Database database, ILogger logger) { _database = database; + _chatters = chatters; _logger = logger; } @@ -25,8 +28,8 @@ namespace HermesSocketServer.Requests return new RequestResult(false, null); } - if (long.TryParse(data["chatter"].ToString(), out long chatter)) - data["chatter"] = chatter; + if (long.TryParse(data["chatter"].ToString(), out long chatterId)) + data["chatter"] = chatterId; else return new RequestResult(false, "Invalid Twitch user id"); @@ -41,13 +44,9 @@ namespace HermesSocketServer.Requests if (check is not bool state || !state) return new RequestResult(false, "Voice is disabled on this channel."); - string sql = "INSERT INTO \"TtsChatVoice\" (\"userId\", \"chatterId\", \"ttsVoiceId\") VALUES (@user, @chatter, @voice)"; - var result = await _database.Execute(sql, data); - if (result == 0) - return new RequestResult(false, "Could not insert the user's voice properly."); - + _chatters.Set(sender, chatterId, 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(result == 1, null); + return new RequestResult(true, null); } } } \ No newline at end of file diff --git a/Requests/UpdateTTSUser.cs b/Requests/UpdateTTSUser.cs index e3613f9..6eba5b2 100644 --- a/Requests/UpdateTTSUser.cs +++ b/Requests/UpdateTTSUser.cs @@ -1,6 +1,7 @@ using System.Text.Json; using HermesSocketLibrary.db; using HermesSocketLibrary.Requests; +using HermesSocketServer.Store; using ILogger = Serilog.ILogger; namespace HermesSocketServer.Requests @@ -11,15 +12,18 @@ namespace HermesSocketServer.Requests private readonly ServerConfiguration _configuration; private readonly Database _database; - private readonly ILogger _logger; + private ChatterStore _chatters; + private ILogger _logger; - public UpdateTTSUser(ServerConfiguration configuration, Database database, ILogger logger) + public UpdateTTSUser(ChatterStore chatters, Database database, ServerConfiguration configuration, ILogger logger) { - _configuration = configuration; _database = database; + _chatters = chatters; + _configuration = configuration; _logger = logger; } + public async Task Grant(string sender, IDictionary? data) { if (data == null) @@ -40,10 +44,9 @@ namespace HermesSocketServer.Requests return new RequestResult(false, null); } - string sql = "UPDATE \"TtsChatVoice\" SET \"ttsVoiceId\" = @voice WHERE \"userId\" = @user AND \"chatterId\" = @chatter"; - var result = await _database.Execute(sql, data); + _chatters.Set(sender, chatterId, data["voice"].ToString()); _logger.Information($"Updated chatter's [chatter: {data["chatter"]}] selected tts voice [voice: {data["voice"]}] in channel [channel: {sender}]"); - return new RequestResult(result == 1, null); + return new RequestResult(true, null); } } } \ No newline at end of file diff --git a/Server.cs b/Server.cs index da27e8a..d5e0d6c 100644 --- a/Server.cs +++ b/Server.cs @@ -52,6 +52,19 @@ namespace HermesSocketLibrary if (obj.OpCode != 0) _logger.Information($"rxm: {message} [ip: {socket.IPAddress}][id: {socket.Id}][name: {socket.Name}][token: {socket.ApiKey}][uid: {socket.UID}]"); + int[] nonProtectedOps = { 0, 1 }; + if (string.IsNullOrEmpty(socket.Id) && !nonProtectedOps.Contains(obj.OpCode)) + { + _logger.Warning($"An attempt was made to use protected routes while not logged in [ip: {socket.IPAddress}][id: {socket.Id}][name: {socket.Name}][token: {socket.ApiKey}][uid: {socket.UID}]"); + return; + } + int[] protectedOps = { 0, 3, 5, 6, 7, 8 }; + if (!string.IsNullOrEmpty(socket.Id) && !protectedOps.Contains(obj.OpCode)) + { + _logger.Warning($"An attempt was made to use non-protected routes while logged in [ip: {socket.IPAddress}][id: {socket.Id}][name: {socket.Name}][token: {socket.ApiKey}][uid: {socket.UID}]"); + return; + } + /** * 0: Heartbeat * 1: Login RX diff --git a/ServerConfiguration.cs b/ServerConfiguration.cs index f2c30ed..e1fee71 100644 --- a/ServerConfiguration.cs +++ b/ServerConfiguration.cs @@ -18,5 +18,6 @@ namespace HermesSocketServer public class DatabaseConfiguration { public string ConnectionString; + public int SaveDelayInSeconds; } } \ No newline at end of file diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index a5b595e..348ead0 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -5,10 +5,14 @@ namespace HermesSocketServer.Services public class DatabaseService : BackgroundService { private readonly VoiceStore _voices; + private readonly ChatterStore _chatters; + private readonly ServerConfiguration _configuration; private readonly Serilog.ILogger _logger; - public DatabaseService(VoiceStore voices, Serilog.ILogger logger) { + public DatabaseService(VoiceStore voices, ChatterStore chatters, ServerConfiguration configuration, Serilog.ILogger logger) { _voices = voices; + _chatters = chatters; + _configuration = configuration; _logger = logger; } @@ -16,14 +20,16 @@ namespace HermesSocketServer.Services { _logger.Information("Loading TTS voices..."); await _voices.Load(); - _logger.Information("Loaded TTS voices."); + _logger.Information("Loading TTS chatters' voice."); + await _chatters.Load(); await Task.Run(async () => { while (true) { - await Task.Delay(TimeSpan.FromMinutes(1)); + await Task.Delay(TimeSpan.FromSeconds(_configuration.Database.SaveDelayInSeconds)); await _voices.Save(); + await _chatters.Save(); } }); } diff --git a/Startup.cs b/Startup.cs index 4542978..bbe069a 100644 --- a/Startup.cs +++ b/Startup.cs @@ -81,6 +81,7 @@ s.AddSingleton(); // Stores s.AddSingleton(); +s.AddSingleton(); // Request handlers s.AddSingleton(); diff --git a/Store/ChatterStore.cs b/Store/ChatterStore.cs new file mode 100644 index 0000000..047c4ef --- /dev/null +++ b/Store/ChatterStore.cs @@ -0,0 +1,283 @@ +using System.Collections.Immutable; +using System.Text; +using HermesSocketLibrary.db; + +namespace HermesSocketServer.Store +{ + public class ChatterStore : IStore + { + 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; + + + public ChatterStore(Database database, Serilog.ILogger logger) + { + _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 chatterId = reader.GetInt64(0); + var ttsVoiceId = reader.GetString(1); + var userId = reader.GetString(2); + if (!_chatters.TryGetValue(userId, out var chatters)) + { + chatters = new Dictionary(); + _chatters.Add(userId, chatters); + } + chatters.Add(chatterId, ttsVoiceId); + }); + _logger.Information($"Loaded {_chatters.Count} TTS voices from database."); + } + + public void Remove(string user, long? key) + { + 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) + { + throw new NotImplementedException(); + } + + public async Task Save() + { + var changes = false; + var sb = new StringBuilder(); + var sql = ""; + + 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(); + _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; + } + + 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(); + _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; + } + + 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(); + _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; + } + 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; + } + } +} \ No newline at end of file diff --git a/Store/IStore.cs b/Store/IStore.cs index e34b284..4a4a4ee 100644 --- a/Store/IStore.cs +++ b/Store/IStore.cs @@ -3,10 +3,20 @@ namespace HermesSocketServer.Store public interface IStore { V? Get(K key); - IEnumerable Get(); + IDictionary Get(); Task Load(); void Remove(K? key); 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/VoiceStore.cs b/Store/VoiceStore.cs index 68b6a82..f6d5ed8 100644 --- a/Store/VoiceStore.cs +++ b/Store/VoiceStore.cs @@ -42,9 +42,9 @@ namespace HermesSocketServer.Store return null; } - public IEnumerable Get() + public IDictionary Get() { - return _voices.Values.ToImmutableList(); + return _voices.ToImmutableDictionary(); } public async Task Load()