diff --git a/Models/Channel.cs b/Models/Channel.cs index 1313959..0b88879 100644 --- a/Models/Channel.cs +++ b/Models/Channel.cs @@ -7,6 +7,8 @@ namespace HermesSocketServer.Models public required string Id { get; set; } public required User User { get; set; } public required ChatterStore Chatters { get; set; } + public required GroupStore Groups { get; set; } + public required GroupPermissionStore GroupPermissions { get; set; } public required PolicyStore Policies { get; set; } public required TTSFilterStore Filters { get; set; } public required ActionStore Actions { get; set; } diff --git a/Requests/GetPermissions.cs b/Requests/GetPermissions.cs index 0c1afe4..9d868ff 100644 --- a/Requests/GetPermissions.cs +++ b/Requests/GetPermissions.cs @@ -1,4 +1,3 @@ -using HermesSocketLibrary.db; using HermesSocketLibrary.Requests.Messages; using HermesSocketServer.Models; using ILogger = Serilog.ILogger; @@ -9,54 +8,27 @@ namespace HermesSocketServer.Requests { public string Name => "get_permissions"; public string[] RequiredKeys => []; - private readonly Database _database; private readonly ILogger _logger; - public GetPermissions(Database database, ILogger logger) + public GetPermissions(ILogger logger) { - _database = database; _logger = logger; } - public async Task Grant(Channel channel, IDictionary data) + public Task Grant(Channel channel, IDictionary data) { - var temp = new Dictionary() { { "user", channel.Id } }; - - var groups = new List(); - string sql = $"SELECT id, name, priority FROM \"Group\" WHERE \"userId\" = @user"; - await _database.Execute(sql, temp, (r) => groups.Add(new Group() - { - Id = r.GetGuid(0).ToString("D"), - Name = r.GetString(1), - Priority = r.GetInt32(2) - })); - - var groupChatters = new List(); - sql = $"SELECT \"groupId\", \"chatterId\", \"chatterId\" FROM \"ChatterGroup\" WHERE \"userId\" = @user"; - await _database.Execute(sql, temp, (r) => groupChatters.Add(new GroupChatter() - { - GroupId = r.GetGuid(0).ToString("D"), - ChatterId = r.GetInt32(1) - })); - - var groupPermissions = new List(); - sql = $"SELECT id, \"groupId\", \"path\", \"allow\" FROM \"GroupPermission\" WHERE \"userId\" = @user"; - await _database.Execute(sql, temp, (r) => groupPermissions.Add(new GroupPermission() - { - Id = r.GetGuid(0).ToString("D"), - GroupId = r.GetGuid(1).ToString("D"), - Path = r.GetString(2), - Allow = r.GetBoolean(3) - })); + var groups = channel.Groups.Get().Values; + var groupChatters = channel.Groups.Chatters.Values.SelectMany(g => g.Get().Values); + var groupPermissions = channel.GroupPermissions.Get().Values; _logger.Information($"Fetched all permissions for channel [channel: {channel.Id}]"); var info = new GroupInfo() { Groups = groups, GroupChatters = groupChatters, - GroupPermissions = groupPermissions + GroupPermissions = groupPermissions, }; - return RequestResult.Successful(info, notifyClientsOnAccount: false); + return Task.FromResult(RequestResult.Successful(info, notifyClientsOnAccount: false)); } } } \ No newline at end of file diff --git a/Services/ChannelManager.cs b/Services/ChannelManager.cs index 1f7f676..9549cc5 100644 --- a/Services/ChannelManager.cs +++ b/Services/ChannelManager.cs @@ -38,11 +38,16 @@ namespace HermesSocketServer.Services var actionTable = _configuration.Database.Tables["Action"]; var chatterTable = _configuration.Database.Tables["Chatter"]; + //var chatterGroupTable = _configuration.Database.Tables["ChatterGroup"]; + var groupTable = _configuration.Database.Tables["Group"]; + var groupPermissionTable = _configuration.Database.Tables["GroupPermission"]; var policyTable = _configuration.Database.Tables["Policy"]; var redemptionTable = _configuration.Database.Tables["Redemption"]; var ttsFilterTable = _configuration.Database.Tables["TtsFilter"]; var chatters = new ChatterStore(userId, chatterTable, _database, _logger); + var groups = new GroupStore(userId, groupTable, _database, _configuration, _logger); + var groupPermissions = new GroupPermissionStore(userId, groupPermissionTable, _database, _logger); var policies = new PolicyStore(userId, policyTable, _database, _logger); var filters = new TTSFilterStore(userId, ttsFilterTable, _database, _logger); var actions = new ActionStore(userId, actionTable, _database, _logger); @@ -50,6 +55,8 @@ namespace HermesSocketServer.Services Task.WaitAll([ chatters.Load(), + groups.Load(), + groupPermissions.Load(), policies.Load(), filters.Load(), actions.Load(), @@ -61,6 +68,8 @@ namespace HermesSocketServer.Services Id = userId, User = user, Chatters = chatters, + Groups = groups, + GroupPermissions = groupPermissions, Policies = policies, Filters = filters, Actions = actions, diff --git a/Store/ActionStore.cs b/Store/ActionStore.cs index 27352fe..fa82b68 100644 --- a/Store/ActionStore.cs +++ b/Store/ActionStore.cs @@ -27,7 +27,7 @@ namespace HermesSocketServer.Store await _database.Execute(sql, data, (reader) => { var name = reader.GetString(0); - _store.Add(name.ToString(), new RedeemableAction() + _store.Add(name, new RedeemableAction() { UserId = _userId, Name = name, @@ -35,7 +35,7 @@ namespace HermesSocketServer.Store Data = JsonSerializer.Deserialize>(reader.GetString(2))! }); }); - _logger.Information($"Loaded {_store.Count} TTS chatter voices from database."); + _logger.Information($"Loaded {_store.Count} redeemable actions from database."); } protected override void OnInitialAdd(string key, RedeemableAction value) diff --git a/Store/ChatterGroupStore.cs b/Store/ChatterGroupStore.cs new file mode 100644 index 0000000..0d9ea74 --- /dev/null +++ b/Store/ChatterGroupStore.cs @@ -0,0 +1,66 @@ +using HermesSocketLibrary.db; +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Store.Internal; + +namespace HermesSocketServer.Store +{ + public class ChatterGroupStore : ComplexAutoSavedStore + { + private readonly string _userId; + private readonly string _groupId; + private readonly Database _database; + private readonly Serilog.ILogger _logger; + + public string GroupId { get => _groupId; } + + + public ChatterGroupStore(string userId, string groupId, DatabaseTable table, Database database, Serilog.ILogger logger) + : base(table, database, logger) + { + _userId = userId; + _groupId = groupId; + _database = database; + _logger = logger; + } + + public override async Task Load() + { + var data = new Dictionary() { { "user", _userId }, { "group", new Guid(_groupId) } }; + string sql = $"SELECT \"chatterId\", \"chatterLabel\" FROM \"ChatterGroup\" WHERE \"userId\" = @user AND \"groupId\" = @group"; + await _database.Execute(sql, data, (reader) => + { + var chatterId = reader.GetInt32(0).ToString(); + lock (_lock) + { + _store.Add(chatterId, new GroupChatter() + { + UserId = _userId, + GroupId = _groupId, + ChatterId = reader.GetInt32(0), + ChatterLabel = reader.GetString(1), + }); + } + }); + _logger.Information($"Loaded {_store.Count} group chatters from database [group id: {_groupId}]"); + } + + protected override void OnInitialAdd(string key, GroupChatter value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key)); + ArgumentNullException.ThrowIfNull(value, nameof(value)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.UserId, nameof(value.UserId)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.GroupId, nameof(value.GroupId)); + ArgumentNullException.ThrowIfNull(value.ChatterId, nameof(value.ChatterId)); + } + + protected override void OnInitialModify(string key, GroupChatter oldValue, GroupChatter newValue) + { + ArgumentNullException.ThrowIfNull(newValue, nameof(newValue)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.GroupId, nameof(newValue.GroupId)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.GroupId, nameof(newValue.GroupId)); + ArgumentNullException.ThrowIfNull(newValue.ChatterId, nameof(newValue.ChatterId)); + ArgumentOutOfRangeException.ThrowIfNotEqual(oldValue.UserId, newValue.UserId, nameof(oldValue.UserId)); + ArgumentOutOfRangeException.ThrowIfNotEqual(oldValue.GroupId, newValue.GroupId, nameof(oldValue.GroupId)); + } + } +} \ No newline at end of file diff --git a/Store/GroupPermissionStore.cs b/Store/GroupPermissionStore.cs new file mode 100644 index 0000000..44db00b --- /dev/null +++ b/Store/GroupPermissionStore.cs @@ -0,0 +1,64 @@ +using HermesSocketLibrary.db; +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Store.Internal; + +namespace HermesSocketServer.Store +{ + public class GroupPermissionStore : AutoSavedStore + { + private readonly string _userId; + private readonly Database _database; + private readonly Serilog.ILogger _logger; + + + public GroupPermissionStore(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 data = new Dictionary() { { "user", _userId } }; + string sql = $"SELECT id, \"groupId\", path, allow FROM \"GroupPermission\" WHERE \"userId\" = @user"; + await _database.Execute(sql, data, async (reader) => + { + var id = reader.GetGuid(0).ToString(); + _store.Add(id, new GroupPermission() + { + Id = id, + UserId = _userId, + GroupId = reader.GetGuid(1).ToString(), + Path = reader.GetString(2), + Allow = await reader.IsDBNullAsync(3) ? null : reader.GetBoolean(3), + }); + }); + _logger.Information($"Loaded {_store.Count} group permissions from database."); + } + + protected override void OnInitialAdd(string key, GroupPermission value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key)); + ArgumentNullException.ThrowIfNull(value, nameof(value)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.UserId, nameof(value.UserId)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.GroupId, nameof(value.GroupId)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.Path, nameof(value.Path)); + } + + protected override void OnInitialModify(string key, GroupPermission oldValue, GroupPermission newValue) + { + ArgumentNullException.ThrowIfNull(newValue, nameof(newValue)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.UserId, nameof(newValue.UserId)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.GroupId, nameof(newValue.GroupId)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.Path, nameof(newValue.Path)); + ArgumentOutOfRangeException.ThrowIfNotEqual(oldValue.UserId, newValue.UserId, nameof(oldValue.UserId)); + ArgumentOutOfRangeException.ThrowIfNotEqual(oldValue.GroupId, newValue.GroupId, nameof(oldValue.GroupId)); + } + + protected override void OnPostRemove(string key, GroupPermission value) + { + } + } +} \ No newline at end of file diff --git a/Store/GroupStore.cs b/Store/GroupStore.cs new file mode 100644 index 0000000..ff2c552 --- /dev/null +++ b/Store/GroupStore.cs @@ -0,0 +1,97 @@ +using System.Collections.Concurrent; +using HermesSocketLibrary.db; +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Store.Internal; + +namespace HermesSocketServer.Store +{ + public class GroupStore : AutoSavedStore + { + private static readonly string[] AUTO_GENERATED_GROUP_NAMES = ["everyone", "subscribers", "vip", "moderators", "broadcaster"]; + + private IDictionary _chatters; + private readonly string _userId; + private readonly Database _database; + private readonly ServerConfiguration _configuration; + private readonly Serilog.ILogger _logger; + + public IDictionary Chatters { get => _chatters; } + + + public GroupStore(string userId, DatabaseTable table, Database database, ServerConfiguration configuration, Serilog.ILogger logger) + : base(table, database, logger) + { + _userId = userId; + _database = database; + _logger = logger; + _configuration = configuration; + _chatters = new ConcurrentDictionary(); + } + + public override async Task Load() + { + var groups = new List<(string, string)>(); + var data = new Dictionary() { { "user", _userId } }; + string sql = $"SELECT id, name, priority FROM \"Group\" WHERE \"userId\" = @user"; + await _database.Execute(sql, data, (reader) => + { + var id = reader.GetGuid(0).ToString(); + var name = reader.GetString(1); + _store.Add(id, new Group() + { + Id = id, + UserId = _userId, + Name = name, + Priority = reader.GetInt32(2), + }); + groups.Add((id, name)); + }); + _logger.Information($"Loaded {_store.Count} groups from database."); + + // Load Chatter Groups + var chatterGroupTable = _configuration.Database.Tables["ChatterGroup"]; + var groupChatters = groups.Select(group => new ChatterGroupStore(_userId, group.Item1, chatterGroupTable, _database, _logger)).ToArray(); + + List tasks = new List(); + for (var i = 0; i < groups.Count; i++) + { + var store = groupChatters[i]; + if (AUTO_GENERATED_GROUP_NAMES.All(n => n != groups[i].Item2)) + tasks.Add(store.Load()); + _chatters.Add(store.GroupId, store); + } + await Task.WhenAll(tasks); + } + + public override async Task Save() + { + List tasks = _chatters.Values.Select(c => c.Save()).ToList(); + tasks.Add(base.Save()); + await Task.WhenAll(base.Save()); + } + + protected override void OnInitialAdd(string key, Group 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)); + ArgumentNullException.ThrowIfNull(value.Priority, nameof(value.Priority)); + } + + protected override void OnInitialModify(string key, Group oldValue, Group newValue) + { + ArgumentNullException.ThrowIfNull(newValue, nameof(newValue)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.UserId, nameof(newValue.UserId)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.Name, nameof(newValue.Name)); + ArgumentNullException.ThrowIfNull(newValue.Priority, nameof(newValue.Priority)); + ArgumentOutOfRangeException.ThrowIfNotEqual(oldValue.UserId, newValue.UserId, nameof(oldValue.UserId)); + if (AUTO_GENERATED_GROUP_NAMES.Any(s => s == oldValue.Name)) + ArgumentOutOfRangeException.ThrowIfNotEqual(oldValue.Name, newValue.Name, nameof(oldValue.Name)); + } + + protected override void OnPostRemove(string key, Group value) + { + } + } +} \ No newline at end of file diff --git a/Store/Internal/GroupSaveSqlGenerator.cs b/Store/Internal/GroupSaveSqlGenerator.cs index ffe7fca..2676b36 100644 --- a/Store/Internal/GroupSaveSqlGenerator.cs +++ b/Store/Internal/GroupSaveSqlGenerator.cs @@ -18,7 +18,8 @@ namespace HermesSocketServer.Store.Internal public GroupSaveSqlGenerator(IDictionary columnsToProperties, IDictionary columnTypes, Serilog.ILogger logger) { - _columnPropertyRelations = columnsToProperties.ToDictionary(p => p.Key, p => typeof(T).GetProperty(p.Value)); + var type = typeof(T); + _columnPropertyRelations = columnsToProperties.ToDictionary(p => p.Key, p => type.GetProperty(p.Value)); _columnTypes = columnTypes; _logger = logger; diff --git a/Store/RedemptionStore.cs b/Store/RedemptionStore.cs index 515e7d6..22e64c5 100644 --- a/Store/RedemptionStore.cs +++ b/Store/RedemptionStore.cs @@ -36,7 +36,7 @@ namespace HermesSocketServer.Store ActionName = reader.GetString(4), }); }); - _logger.Information($"Loaded {_store.Count} TTS chatter voices from database."); + _logger.Information($"Loaded {_store.Count} redemptions from database."); } protected override void OnInitialAdd(string key, Redemption value) diff --git a/Store/TTSFilterStore.cs b/Store/TTSFilterStore.cs index 8295563..50920bd 100644 --- a/Store/TTSFilterStore.cs +++ b/Store/TTSFilterStore.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using HermesSocketLibrary.db; using HermesSocketLibrary.Requests.Messages; using HermesSocketServer.Store.Internal; @@ -36,7 +35,7 @@ namespace HermesSocketServer.Store Flag = reader.GetInt32(3), }); }); - _logger.Information($"Loaded {_store.Count} TTS chatter voices from database."); + _logger.Information($"Loaded {_store.Count} TTS filters from database."); } protected override void OnInitialAdd(string key, TTSWordFilter value)