diff --git a/Models/Channel.cs b/Models/Channel.cs index 6571be7..0659126 100644 --- a/Models/Channel.cs +++ b/Models/Channel.cs @@ -8,6 +8,7 @@ namespace HermesSocketServer.Models public required User User { get; set; } public required ChatterStore Chatters { get; set; } public required ConnectionStore Connections { get; set; } + public required EmoteStore Emotes { get; set; } public required GroupStore Groups { get; set; } public required GroupPermissionStore GroupPermissions { get; set; } public required PolicyStore Policies { get; set; } diff --git a/Models/User.cs b/Models/User.cs index 9696512..7bd9601 100644 --- a/Models/User.cs +++ b/Models/User.cs @@ -7,5 +7,6 @@ namespace HermesSocketServer.Models public required string Email { get; set; } public required string Role { get; set; } public required string DefaultVoice { get; set; } + public required string StreamElementsOverlayKey { get; set; } } } \ No newline at end of file diff --git a/Requests/DeleteGroup.cs b/Requests/DeleteGroup.cs index 3d8606e..34fb021 100644 --- a/Requests/DeleteGroup.cs +++ b/Requests/DeleteGroup.cs @@ -14,54 +14,53 @@ namespace HermesSocketServer.Requests _logger = logger; } - public async Task Grant(Channel channel, IDictionary data) + public Task Grant(Channel channel, IDictionary data) { - var groupId = data["id"].ToString()!; + var groupIdString = data["id"].ToString()!; + var groupId = new Guid(groupIdString); - var result = channel.Groups.Remove(groupId); + var result = channel.Groups.Remove(groupIdString); if (result) { - var permissions = channel.GroupPermissions.Get().Values - .Where(p => p.GroupId.ToString() == groupId); - - Task? chattersSave = null; - if (channel.Groups.Chatters.TryGetValue(groupId, out var chatters)) + if (channel.Groups.Chatters.TryGetValue(groupId.ToString(), out var chatters)) { - var filteredChatters = chatters.Get().Values.Where(c => c.GroupId == groupId).ToArray(); - if (filteredChatters.Any()) + var filteredChatters = chatters.Get().Values; + foreach (var chatter in filteredChatters) { - foreach (var chatter in filteredChatters) - { - var res = chatters.Remove(chatter.ChatterId.ToString(), fromCascade: true); - if (!res) - _logger.Warning($"Failed to delete group chatter by id [group chatter id: {chatter.ChatterId}]"); - } - - chattersSave = chatters.Save(); + var res = chatters.Remove(chatter.ChatterId.ToString(), fromCascade: true); + if (!res) + _logger.Warning($"Failed to delete group chatter by id from cascade [group chatter id: {chatter.ChatterId}]"); } } + var permissions = channel.GroupPermissions.Get().Values + .Where(p => p.GroupId == groupId); + foreach (var permission in permissions) { var res = channel.GroupPermissions.Remove(permission.Id, fromCascade: true); if (!res) - _logger.Warning($"Failed to delete group permission by id [group chatter id: {permission.Id}]"); + _logger.Warning($"Failed to delete group permission by id from cascade [group permission id: {permission.Id}]"); } - if (chattersSave != null) - await Task.WhenAll(chattersSave, channel.GroupPermissions.Save()); - else - await channel.GroupPermissions.Save(); - - if (!channel.Groups.Chatters.Remove(groupId)) + var policies = channel.Policies.Get().Values + .Where(c => c.GroupId == groupId).ToArray(); + foreach (var policy in policies) + { + var res = channel.Policies.Remove(policy.Id.ToString(), fromCascade: true); + if (!res) + _logger.Warning($"Failed to delete group policy by id from cascade [group policy id: {policy.Id}]"); + } + + if (!channel.Groups.Chatters.Remove(groupIdString)) _logger.Warning($"Failed to delete group chatters from inner store [group id: {groupId}]"); _logger.Information($"Deleted a group by id [group id: {groupId}]"); - return RequestResult.Successful(null); + return Task.FromResult(RequestResult.Successful(null)); } _logger.Warning($"Group Id does not exist [group id: {groupId}]"); - return RequestResult.Failed("Something went wrong when updating the cache."); + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); } } } \ No newline at end of file diff --git a/Requests/DeleteRedeemableAction.cs b/Requests/DeleteRedeemableAction.cs index f6c2410..fbfa01a 100644 --- a/Requests/DeleteRedeemableAction.cs +++ b/Requests/DeleteRedeemableAction.cs @@ -21,6 +21,15 @@ namespace HermesSocketServer.Requests if (result) { + var redemptions = channel.Redemptions.Get().Values + .Where(r => r.ActionName == name); + foreach (var redemption in redemptions) + { + var res = channel.Redemptions.Remove(redemption.Id, fromCascade: true); + if (!res) + _logger.Warning($"Failed to delete redemption by id from cascade [redemption id: {redemption.Id}]"); + } + _logger.Information($"Deleted a redeemable action by name [name: {name}]"); return Task.FromResult(RequestResult.Successful(null)); } diff --git a/Requests/GetEmotes.cs b/Requests/GetEmotes.cs index 442253c..e078f0b 100644 --- a/Requests/GetEmotes.cs +++ b/Requests/GetEmotes.cs @@ -1,5 +1,3 @@ -using HermesSocketLibrary.db; -using HermesSocketLibrary.Requests.Messages; using HermesSocketServer.Models; using ILogger = Serilog.ILogger; @@ -9,24 +7,16 @@ namespace HermesSocketServer.Requests { public string Name => "get_emotes"; public string[] RequiredKeys => []; - private Database _database; private ILogger _logger; - public GetEmotes(Database database, ILogger logger) + public GetEmotes(ILogger logger) { - _database = database; _logger = logger; } public async Task Grant(Channel channel, IDictionary data) { - IList emotes = new List(); - string sql = $"SELECT id, name FROM \"Emote\""; - await _database.Execute(sql, (IDictionary?) null, (r) => emotes.Add(new EmoteInfo() - { - Id = r.GetString(0), - Name = r.GetString(1) - })); + var emotes = channel.Emotes.Get().Values; _logger.Information($"Fetched all emotes for channel [channel: {channel.Id}]"); return RequestResult.Successful(emotes, notifyClientsOnAccount: false); } diff --git a/Services/ChannelManager.cs b/Services/ChannelManager.cs index 3edef2c..42dfa1b 100644 --- a/Services/ChannelManager.cs +++ b/Services/ChannelManager.cs @@ -46,6 +46,7 @@ namespace HermesSocketServer.Services var chatterTable = _configuration.Database.Tables["Chatter"]; var connectionTable = _configuration.Database.Tables["Connection"]; var connectionStateTable = _configuration.Database.Tables["ConnectionState"]; + var emoteTable = _configuration.Database.Tables["Emote"]; var groupTable = _configuration.Database.Tables["Group"]; var groupPermissionTable = _configuration.Database.Tables["GroupPermission"]; var policyTable = _configuration.Database.Tables["Policy"]; @@ -55,6 +56,7 @@ namespace HermesSocketServer.Services var chatters = new ChatterStore(userId, chatterTable, _database, _logger); var connections = new ConnectionStore(userId, connectionTable, _database, _logger); + var emotes = new EmoteStore(userId, emoteTable, _database, _logger); var groups = new GroupStore(userId, groupTable, _database, _configuration, _logger); var groupPermissions = new GroupPermissionStore(userId, groupPermissionTable, groups, _database, _logger); var policies = new PolicyStore(userId, policyTable, groups, _database, _logger); @@ -69,6 +71,7 @@ namespace HermesSocketServer.Services User = user, Chatters = chatters, Connections = connections, + Emotes = emotes, Groups = groups, GroupPermissions = groupPermissions, Policies = policies, @@ -81,6 +84,7 @@ namespace HermesSocketServer.Services Task.WaitAll([ channel.Actions.Load(), channel.Chatters.Load(), + channel.Emotes.Load(), channel.Connections.Load(), channel.Groups.Load(), channel.Filters.Load(), @@ -136,6 +140,7 @@ namespace HermesSocketServer.Services await Task.WhenAll([ channel.Chatters.Save(), channel.Connections.Save(), + channel.Emotes.Save(), channel.Groups.Save(), channel.GroupPermissions.Save(), channel.Policies.Save(), @@ -154,6 +159,7 @@ namespace HermesSocketServer.Services var genericTablesTask = Task.WhenAll([ channel.Chatters.Save(), channel.Connections.Save(), + channel.Emotes.Save(), channel.Filters.Save(), channel.VoiceStates.Save(), ]).ConfigureAwait(false); diff --git a/Socket/Handlers/EmoteDetailsHandler.cs b/Socket/Handlers/EmoteDetailsHandler.cs index 6963537..16e4917 100644 --- a/Socket/Handlers/EmoteDetailsHandler.cs +++ b/Socket/Handlers/EmoteDetailsHandler.cs @@ -1,6 +1,6 @@ -using HermesSocketLibrary.db; +using HermesSocketLibrary.Requests.Messages; using HermesSocketLibrary.Socket.Data; -using Npgsql; +using HermesSocketServer.Services; using ILogger = Serilog.ILogger; namespace HermesSocketServer.Socket.Handlers @@ -10,7 +10,7 @@ namespace HermesSocketServer.Socket.Handlers private const int EMOTE_BUFFER_SIZE = 5000; public int OperationCode { get; } = 7; - private readonly Database _database; + private readonly ChannelManager _manager; private readonly HashSet _emotes; private readonly string[] _array; private readonly ILogger _logger; @@ -18,9 +18,9 @@ namespace HermesSocketServer.Socket.Handlers private int _index; - public EmoteDetailsHandler(Database database, ILogger logger) + public EmoteDetailsHandler(ChannelManager manager, ILogger logger) { - _database = database; + _manager = manager; _emotes = new HashSet(EMOTE_BUFFER_SIZE); _array = new string[EMOTE_BUFFER_SIZE]; _logger = logger; @@ -72,30 +72,12 @@ namespace HermesSocketServer.Socket.Handlers if (!data.Emotes.Any()) return; - int rows = 0; - string sql = "INSERT INTO \"Emote\" (id, name) VALUES (@idd, @name) ON CONFLICT (id) DO UPDATE SET name = @name;"; - using (var connection = await _database.DataSource.OpenConnectionAsync()) - { - using (var command = new NpgsqlCommand(sql, connection)) - { - foreach (var entry in data.Emotes) - { - command.Parameters.Clear(); - command.Parameters.AddWithValue("idd", entry.Key); - command.Parameters.AddWithValue("name", entry.Value); - - await command.PrepareAsync(); - try - { - rows += await command.ExecuteNonQueryAsync(); - } - catch (Exception e) - { - _logger.Error(e, "Failed to add emote detail: " + entry.Key + " -> " + entry.Value); - } - } - } - } + var channel = _manager.Get(sender.Id); + if (channel == null) + return; + + foreach (var entry in data.Emotes) + channel.Emotes.Set(entry.Key, new EmoteInfo() { Id = entry.Key, Name = entry.Value, UserId = channel.Id }); } } } \ No newline at end of file diff --git a/Socket/Handlers/EmoteUsageHandler.cs b/Socket/Handlers/EmoteUsageHandler.cs index ad1aa51..25c7216 100644 --- a/Socket/Handlers/EmoteUsageHandler.cs +++ b/Socket/Handlers/EmoteUsageHandler.cs @@ -61,6 +61,7 @@ namespace HermesSocketServer.Socket.Handlers _mutex.ReleaseMutex(); } + // TODO: multi-row inserts to increase database performance. int rows = 0; string sql = "INSERT INTO \"EmoteUsageHistory\" (timestamp, \"broadcasterId\", \"emoteId\", \"chatterId\") VALUES (@time, @broadcaster, @emote, @chatter)"; using (var connection = await _database.DataSource.OpenConnectionAsync()) diff --git a/Socket/Handlers/HermesLoginHandler.cs b/Socket/Handlers/HermesLoginHandler.cs index b54c680..00ba4b6 100644 --- a/Socket/Handlers/HermesLoginHandler.cs +++ b/Socket/Handlers/HermesLoginHandler.cs @@ -54,7 +54,7 @@ namespace HermesSocketServer.Socket.Handlers sender.Id = userId; recipients = _sockets.GetSockets(userId).ToList().Where(s => s.SessionId != sender.SessionId); - sender.Slave = data.WebLogin || recipients.Where(r => r != null && !r.WebLogin).Any(); + sender.Slave = data.WebLogin || recipients.Any(r => r != null && !r.WebLogin); sender.ApiKey = data.ApiKey; sender.WebLogin = data.WebLogin; @@ -95,6 +95,7 @@ namespace HermesSocketServer.Socket.Handlers ProviderAccountId = providerId, SessionId = sender.SessionId, UserName = channel.User.Name, + StreamElementsOverlayKey = channel.User.StreamElementsOverlayKey, OwnerId = _configuration.Tts.OwnerId, Admin = sender.Admin, WebLogin = data.WebLogin, diff --git a/Store/EmoteStore.cs b/Store/EmoteStore.cs new file mode 100644 index 0000000..a84ae2a --- /dev/null +++ b/Store/EmoteStore.cs @@ -0,0 +1,62 @@ +using System.Collections.Concurrent; +using HermesSocketLibrary.db; +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Store.Internal; + +namespace HermesSocketServer.Store +{ + public class EmoteStore : AutoSavedStore + { + private readonly string _userId; + private readonly Database _database; + private readonly Serilog.ILogger _logger; + + + public EmoteStore(string userId, DatabaseTable table, Database database, Serilog.ILogger logger) + : base(table, database, logger) + { + _userId = userId; + _database = database; + _logger = logger; + } + + public override async Task Load() + { + var emotes = new List(); + var data = new Dictionary() { { "user", _userId } }; + string sql = $"SELECT id, name FROM \"Emote\" WHERE \"userId\" = @user"; + await _database.Execute(sql, data, (reader) => + { + var id = reader.GetString(0); + var name = reader.GetString(1); + _store.Add(id, new EmoteInfo() + { + Id = id, + UserId = _userId, + Name = name, + }); + emotes.Add(new EmoteInfo() { Id = id, Name = name, UserId = _userId }); + }); + _logger.Information($"Loaded {_store.Count} emotes from database."); + } + + protected override void OnInitialAdd(string key, EmoteInfo value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key)); + ArgumentNullException.ThrowIfNull(value, nameof(value)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.UserId, nameof(value.UserId)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.Name, nameof(value.Name)); + } + + protected override void OnInitialModify(string key, EmoteInfo oldValue, EmoteInfo newValue) + { + ArgumentNullException.ThrowIfNull(newValue, nameof(newValue)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.UserId, nameof(newValue.UserId)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.Name, nameof(newValue.Name)); + } + + protected override void OnPostRemove(string key, EmoteInfo value) + { + } + } +} \ No newline at end of file diff --git a/Store/UserStore.cs b/Store/UserStore.cs index fff9284..e5d849c 100644 --- a/Store/UserStore.cs +++ b/Store/UserStore.cs @@ -19,7 +19,7 @@ namespace HermesSocketServer.Store public override async Task Load() { - string sql = "SELECT id, name, email, role, \"ttsDefaultVoice\" FROM \"User\";"; + string sql = "SELECT id, name, email, role, \"ttsDefaultVoice\", seolkey FROM \"User\";"; await _database.Execute(sql, new Dictionary(), (reader) => { string id = reader.GetString(0); @@ -30,6 +30,7 @@ namespace HermesSocketServer.Store Email = reader.GetString(2), Role = reader.GetString(3), DefaultVoice = reader.GetString(4), + StreamElementsOverlayKey = reader.GetString(5), }); }); _logger.Information($"Loaded {_store.Count} users from database.");