From 6d955f245af40a8ab34ec89c42570acab418cbe4 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 17 Jan 2025 04:32:31 +0000 Subject: [PATCH] Added stores for connections. Added requests for groups, group chatters, group permissions & connections. Using TTS Voice State store. --- Models/Channel.cs | 2 + Requests/CreateConnection.cs | 50 ++++++++++++++++++ Requests/CreateGroup.cs | 51 ++++++++++++++++++ Requests/CreateGroupChatter.cs | 46 ++++++++++++++++ Requests/CreateGroupPermission.cs | 43 +++++++++++++++ Requests/CreateRedemption.cs | 2 +- Requests/DeleteConnection.cs | 32 ++++++++++++ Requests/DeleteGroup.cs | 67 ++++++++++++++++++++++++ Requests/DeleteGroupChatter.cs | 36 +++++++++++++ Requests/DeleteGroupPermission.cs | 32 ++++++++++++ Requests/GetConnections.cs | 29 ++--------- Requests/GetGroupPermissions.cs | 24 +++++++++ Requests/GetGroups.cs | 37 +++++++++++++ Requests/UpdateConnection.cs | 50 ++++++++++++++++++ Requests/UpdateGroup.cs | 42 +++++++++++++++ Requests/UpdateGroupChatter.cs | 46 ++++++++++++++++ Requests/UpdateGroupPermission.cs | 43 +++++++++++++++ Requests/UpdatePolicy.cs | 8 +-- Requests/UpdateRedeemableAction.cs | 2 +- Requests/UpdateRedemption.cs | 9 +--- Requests/UpdateTTSFilter.cs | 3 +- Requests/UpdateTTSUser.cs | 20 +++---- Requests/UpdateTTSVoice.cs | 10 ++-- Requests/UpdateTTSVoiceState.cs | 28 ++++++---- Services/ChannelManager.cs | 18 +++++++ Socket/Handlers/HermesLoginHandler.cs | 1 + Startup.cs | 18 +++++-- Store/ConnectionStore.cs | 75 +++++++++++++++++++++++++++ Store/GroupStore.cs | 2 +- 29 files changed, 759 insertions(+), 67 deletions(-) create mode 100644 Requests/CreateConnection.cs create mode 100644 Requests/CreateGroup.cs create mode 100644 Requests/CreateGroupChatter.cs create mode 100644 Requests/CreateGroupPermission.cs create mode 100644 Requests/DeleteConnection.cs create mode 100644 Requests/DeleteGroup.cs create mode 100644 Requests/DeleteGroupChatter.cs create mode 100644 Requests/DeleteGroupPermission.cs create mode 100644 Requests/GetGroupPermissions.cs create mode 100644 Requests/GetGroups.cs create mode 100644 Requests/UpdateConnection.cs create mode 100644 Requests/UpdateGroup.cs create mode 100644 Requests/UpdateGroupChatter.cs create mode 100644 Requests/UpdateGroupPermission.cs create mode 100644 Store/ConnectionStore.cs diff --git a/Models/Channel.cs b/Models/Channel.cs index 0b88879..6571be7 100644 --- a/Models/Channel.cs +++ b/Models/Channel.cs @@ -7,11 +7,13 @@ namespace HermesSocketServer.Models public required string Id { get; set; } public required User User { get; set; } public required ChatterStore Chatters { get; set; } + public required ConnectionStore Connections { 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; } public required RedemptionStore Redemptions { get; set; } + public required VoiceStateStore VoiceStates { get; set; } } } \ No newline at end of file diff --git a/Requests/CreateConnection.cs b/Requests/CreateConnection.cs new file mode 100644 index 0000000..c390621 --- /dev/null +++ b/Requests/CreateConnection.cs @@ -0,0 +1,50 @@ +using HermesSocketLibrary.Socket.Data; +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class CreateConnection : IRequest + { + public string Name => "create_connection"; + public string[] RequiredKeys => ["name", "type", "clientId", "accessToken", "grantType", "scope", "expiration"]; + private ILogger _logger; + + public CreateConnection(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + string name = data["name"].ToString()!; + string type = data["type"].ToString()!; + string clientId = data["clientId"].ToString()!; + string accessToken = data["accessToken"].ToString()!; + string grantType = data["grantType"].ToString()!; + string scope = data["scope"].ToString()!; + if (!DateTime.TryParse(data["expiration"].ToString()!, out var expiresAt)) + return Task.FromResult(RequestResult.Failed("Expiration needs to be a date time string.")); + + var connection = new Connection() + { + UserId = channel.Id, + Name = name, + Type = type, + ClientId = clientId, + AccessToken = accessToken, + GrantType = grantType, + Scope = scope, + ExpiresAt = expiresAt, + }; + + bool result = channel.Connections.Set(name, connection); + if (result) + { + _logger.Information($"Added connection to channel [name: {name}][type: {type}][scope: {scope}][expiration: {expiresAt}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(connection)); + } + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/CreateGroup.cs b/Requests/CreateGroup.cs new file mode 100644 index 0000000..a12a2ce --- /dev/null +++ b/Requests/CreateGroup.cs @@ -0,0 +1,51 @@ +using HermesSocketLibrary.db; +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Models; +using HermesSocketServer.Store; +using HermesSocketServer.Store.Internal; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class CreateGroup : IRequest + { + public string Name => "create_group"; + public string[] RequiredKeys => ["name", "priority"]; + private readonly DatabaseTable _table; + private readonly Database _database; + private ILogger _logger; + + public CreateGroup([FromKeyedServices("ChatterGroup")] DatabaseTable table, Database database, ILogger logger) + { + _table = table; + _database = database; + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var id = Guid.NewGuid(); + string name = data["name"].ToString()!; + if (!int.TryParse(data["priority"].ToString()!, out var priority)) + return Task.FromResult(RequestResult.Failed("Priority needs to be an integer.")); + + var group = new Group() + { + Id = id.ToString(), + UserId = channel.Id, + Name = name, + Priority = priority, + }; + + bool result = channel.Groups.Set(id.ToString(), group); + if (result) + { + var store = new ChatterGroupStore(channel.Id, group.Id, _table, _database, _logger); + channel.Groups.Chatters.Add(group.Id, store); + _logger.Information($"Added group to channel [group id: {id}][name: {name}][priority: {priority}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(group)); + } + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/CreateGroupChatter.cs b/Requests/CreateGroupChatter.cs new file mode 100644 index 0000000..2b09741 --- /dev/null +++ b/Requests/CreateGroupChatter.cs @@ -0,0 +1,46 @@ +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class CreateGroupChatter : IRequest + { + public string Name => "create_group_chatter"; + public string[] RequiredKeys => ["group", "chatter", "label"]; + private ILogger _logger; + + public CreateGroupChatter(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var id = Guid.NewGuid(); + string groupId = data["group"].ToString()!; + if (!int.TryParse(data["chatter"].ToString()!, out var chatterId)) + return Task.FromResult(RequestResult.Failed("Priority needs to be an integer.")); + string chatterLabel = data["label"].ToString()!; + + if (!channel.Groups.Chatters.TryGetValue(groupId, out var chatters)) + return Task.FromResult(RequestResult.Failed($"The group does not exist.")); + + var groupChatter = new GroupChatter() + { + UserId = channel.Id, + GroupId = groupId, + ChatterId = chatterId, + ChatterLabel = chatterLabel, + }; + + bool result = chatters.Set(chatterId.ToString(), groupChatter); + if (result) + { + _logger.Information($"Added group chatter to channel [group id: {id}][group id: {groupId}][chatter id: {chatterId}][chatter label: {chatterLabel}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(groupChatter)); + } + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/CreateGroupPermission.cs b/Requests/CreateGroupPermission.cs new file mode 100644 index 0000000..5a5dcd2 --- /dev/null +++ b/Requests/CreateGroupPermission.cs @@ -0,0 +1,43 @@ +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class CreateGroupPermission : IRequest + { + public string Name => "create_group_permission"; + public string[] RequiredKeys => ["group", "path", "allow"]; + private ILogger _logger; + + public CreateGroupPermission(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var id = Guid.NewGuid(); + string groupId = data["group"].ToString()!; + string path = data["path"].ToString()!; + bool? allow = bool.TryParse(data["allow"].ToString()!, out bool a) ? a : null; + + var permission = new GroupPermission() + { + Id = id.ToString(), + UserId = channel.Id, + GroupId = groupId, + Path = path, + Allow = allow, + }; + + bool result = channel.GroupPermissions.Set(id.ToString(), permission); + if (result) + { + _logger.Information($"Added group permission to channel [permission id: {id}][group id: {groupId}][path: {path}][allow: {allow}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(permission)); + } + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/CreateRedemption.cs b/Requests/CreateRedemption.cs index 14b86a1..ad6dd8c 100644 --- a/Requests/CreateRedemption.cs +++ b/Requests/CreateRedemption.cs @@ -38,7 +38,7 @@ namespace HermesSocketServer.Requests bool result = channel.Redemptions.Set(id.ToString(), redemption); if (result) { - _logger.Information($"Added redemption to channel [id: {id}][redemption id: {redemptionId}][action: {actionName}][order: {order}][channel: {channel.Id}]"); + _logger.Information($"Added redemption to channel [redemption id: {id}][twitch redemption id: {redemptionId}][action: {actionName}][order: {order}][channel: {channel.Id}]"); return Task.FromResult(RequestResult.Successful(redemption)); } return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); diff --git a/Requests/DeleteConnection.cs b/Requests/DeleteConnection.cs new file mode 100644 index 0000000..3706106 --- /dev/null +++ b/Requests/DeleteConnection.cs @@ -0,0 +1,32 @@ +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class DeleteConnection : IRequest + { + public string Name => "delete_connection"; + public string[] RequiredKeys => ["id"]; + private ILogger _logger; + + public DeleteConnection(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var connectionId = data["id"].ToString()!; + + var result = channel.Connections.Remove(connectionId); + if (result) + { + _logger.Information($"Deleted a connection by id [connection id: {connectionId}]"); + return Task.FromResult(RequestResult.Successful(null)); + } + + _logger.Warning($"Connection Id does not exist [connection id: {connectionId}]"); + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/DeleteGroup.cs b/Requests/DeleteGroup.cs new file mode 100644 index 0000000..9b8194d --- /dev/null +++ b/Requests/DeleteGroup.cs @@ -0,0 +1,67 @@ +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class DeleteGroup : IRequest + { + public string Name => "delete_group"; + public string[] RequiredKeys => ["id"]; + private ILogger _logger; + + public DeleteGroup(ILogger logger) + { + _logger = logger; + } + + public async Task Grant(Channel channel, IDictionary data) + { + var groupId = data["id"].ToString()!; + + var result = channel.Groups.Remove(groupId); + if (result) + { + var permissions = channel.GroupPermissions.Get().Values + .Where(p => p.GroupId == groupId); + + Task? chattersSave = null; + if (channel.Groups.Chatters.TryGetValue(groupId, out var chatters)) + { + var filteredChatters = chatters.Get().Values.Where(c => c.GroupId == groupId).ToArray(); + if (filteredChatters.Any()) + { + foreach (var chatter in filteredChatters) + { + var res = chatters.Remove(chatter.ChatterId.ToString()); + if (!res) + _logger.Warning($"Failed to delete group chatter by id [group chatter id: {chatter.ChatterId}]"); + } + + chattersSave = chatters.Save(); + } + } + + foreach (var permission in permissions) + { + var res = channel.GroupPermissions.Remove(permission.Id); + if (!res) + _logger.Warning($"Failed to delete group permission by id [group chatter id: {permission.Id}]"); + } + + if (chattersSave != null) + await Task.WhenAll(chattersSave, channel.GroupPermissions.Save()); + else + await channel.GroupPermissions.Save(); + + if (!channel.Groups.Chatters.Remove(groupId)) + _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); + } + + _logger.Warning($"Group Id does not exist [group id: {groupId}]"); + return RequestResult.Failed("Something went wrong when updating the cache."); + } + } +} \ No newline at end of file diff --git a/Requests/DeleteGroupChatter.cs b/Requests/DeleteGroupChatter.cs new file mode 100644 index 0000000..32baf72 --- /dev/null +++ b/Requests/DeleteGroupChatter.cs @@ -0,0 +1,36 @@ +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class DeleteGroupChatter : IRequest + { + public string Name => "delete_group_chatter"; + public string[] RequiredKeys => ["id", "group"]; + private ILogger _logger; + + public DeleteGroupChatter(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var chatterId = data["id"].ToString()!; + var groupId = data["group"].ToString()!; + + if (!channel.Groups.Chatters.TryGetValue(groupId, out var chatters)) + return Task.FromResult(RequestResult.Failed($"The group does not exist.")); + + var result = chatters.Remove(chatterId); + if (result) + { + _logger.Information($"Deleted a group chatter by id [group id: {chatterId}]"); + return Task.FromResult(RequestResult.Successful(null)); + } + + _logger.Warning($"Group Chatter Id does not exist [group id: {chatterId}]"); + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/DeleteGroupPermission.cs b/Requests/DeleteGroupPermission.cs new file mode 100644 index 0000000..c0f4e84 --- /dev/null +++ b/Requests/DeleteGroupPermission.cs @@ -0,0 +1,32 @@ +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class DeleteGroupPermission : IRequest + { + public string Name => "delete_group_permission"; + public string[] RequiredKeys => ["id"]; + private ILogger _logger; + + public DeleteGroupPermission(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var id = data["id"].ToString()!; + + var result = channel.GroupPermissions.Remove(id); + if (result) + { + _logger.Information($"Deleted a group permission by id [group permission id: {id}]"); + return Task.FromResult(RequestResult.Successful(null)); + } + + _logger.Warning($"Group Permission Id does not exist [group permission id: {id}]"); + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/GetConnections.cs b/Requests/GetConnections.cs index 3c5aa7e..245d563 100644 --- a/Requests/GetConnections.cs +++ b/Requests/GetConnections.cs @@ -1,5 +1,3 @@ -using HermesSocketLibrary.db; -using HermesSocketLibrary.Socket.Data; using HermesSocketServer.Models; using ILogger = Serilog.ILogger; @@ -9,35 +7,18 @@ namespace HermesSocketServer.Requests { public string Name => "get_connections"; public string[] RequiredKeys => []; - private Database _database; private ILogger _logger; - public GetConnections(Database database, ILogger logger) + public GetConnections(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 connections = new List(); - string sql = "select \"name\", \"type\", \"clientId\", \"accessToken\", \"grantType\", \"scope\", \"expiresAt\", \"default\" from \"Connection\" where \"userId\" = @user"; - await _database.Execute(sql, temp, sql => - connections.Add(new Connection() - { - Name = sql.GetString(0), - Type = sql.GetString(1), - ClientId = sql.GetString(2), - AccessToken = sql.GetString(3), - GrantType = sql.GetString(4), - Scope = sql.GetString(5), - ExpiresAt = sql.GetDateTime(6), - Default = sql.GetBoolean(7) - }) - ); - return RequestResult.Successful(connections, notifyClientsOnAccount: false); + var connections = channel.Connections.Get().Values; + _logger.Information($"Fetched all connections for channel [channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(connections, notifyClientsOnAccount: false)); } } } \ No newline at end of file diff --git a/Requests/GetGroupPermissions.cs b/Requests/GetGroupPermissions.cs new file mode 100644 index 0000000..eac536a --- /dev/null +++ b/Requests/GetGroupPermissions.cs @@ -0,0 +1,24 @@ +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class GetGroupPermissions : IRequest + { + public string Name => "get_group_permissions"; + public string[] RequiredKeys => []; + private readonly ILogger _logger; + + public GetGroupPermissions(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var permissions = channel.GroupPermissions.Get().Values; + _logger.Information($"Fetched all group permissions for channel [channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(permissions, notifyClientsOnAccount: false)); + } + } +} \ No newline at end of file diff --git a/Requests/GetGroups.cs b/Requests/GetGroups.cs new file mode 100644 index 0000000..63676ef --- /dev/null +++ b/Requests/GetGroups.cs @@ -0,0 +1,37 @@ +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class GetGroups : IRequest + { + public string Name => "get_groups"; + public string[] RequiredKeys => []; + private readonly ILogger _logger; + + public GetGroups(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var groups = channel.Groups.Get().Values; + var chatters = channel.Groups.Chatters; + var all = groups.Select(g => new GroupDetails() + { + Group = g, + Chatters = chatters[g.Id].Get().Values, + }); + _logger.Information($"Fetched all groups for channel [channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(all, notifyClientsOnAccount: false)); + } + + private class GroupDetails + { + public required Group Group; + public required IEnumerable Chatters; + } + } +} \ No newline at end of file diff --git a/Requests/UpdateConnection.cs b/Requests/UpdateConnection.cs new file mode 100644 index 0000000..360a904 --- /dev/null +++ b/Requests/UpdateConnection.cs @@ -0,0 +1,50 @@ +using HermesSocketLibrary.Socket.Data; +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class UpdateConnection : IRequest + { + public string Name => "update_connection"; + public string[] RequiredKeys => ["name", "type", "clientId", "accessToken", "grantType", "scope", "expiration"]; + private ILogger _logger; + + public UpdateConnection(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + string name = data["name"].ToString()!; + string type = data["type"].ToString()!; + string clientId = data["clientId"].ToString()!; + string accessToken = data["accessToken"].ToString()!; + string grantType = data["grantType"].ToString()!; + string scope = data["scope"].ToString()!; + if (!DateTime.TryParse(data["expiration"].ToString()!, out var expiresAt)) + return Task.FromResult(RequestResult.Failed("Expiration needs to be a date time string.")); + + var connection = new Connection() + { + UserId = channel.Id, + Name = name, + Type = type, + ClientId = clientId, + AccessToken = accessToken, + GrantType = grantType, + Scope = scope, + ExpiresAt = expiresAt, + }; + + bool result = channel.Connections.Modify(name, connection); + if (result) + { + _logger.Information($"Added connection to channel [name: {name}][type: {type}][scope: {scope}][expiration: {expiresAt}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(connection)); + } + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/UpdateGroup.cs b/Requests/UpdateGroup.cs new file mode 100644 index 0000000..427f074 --- /dev/null +++ b/Requests/UpdateGroup.cs @@ -0,0 +1,42 @@ +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class UpdateGroup : IRequest + { + public string Name => "update_group"; + public string[] RequiredKeys => ["id", "name", "priority"]; + private ILogger _logger; + + public UpdateGroup(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var id = data["id"].ToString()!; + string name = data["name"].ToString()!; + if (!int.TryParse(data["priority"].ToString()!, out var priority)) + return Task.FromResult(RequestResult.Failed("Priority needs to be an integer.")); + + var group = new Group() + { + Id = id, + UserId = channel.Id, + Name = name, + Priority = priority, + }; + + bool result = channel.Groups.Modify(id, group); + if (result) + { + _logger.Information($"Updated group on channel [group id: {id}][name: {name}][priority: {priority}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(group)); + } + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/UpdateGroupChatter.cs b/Requests/UpdateGroupChatter.cs new file mode 100644 index 0000000..892cba3 --- /dev/null +++ b/Requests/UpdateGroupChatter.cs @@ -0,0 +1,46 @@ +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class UpdateGroupChatter : IRequest + { + public string Name => "update_group_chatter"; + public string[] RequiredKeys => ["group", "chatter", "label"]; + private ILogger _logger; + + public UpdateGroupChatter(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var id = Guid.NewGuid(); + string groupId = data["group"].ToString()!; + if (!int.TryParse(data["chatter"].ToString()!, out var chatterId)) + return Task.FromResult(RequestResult.Failed("Priority needs to be an integer.")); + string chatterLabel = data["label"].ToString()!; + + var groupChatter = new GroupChatter() + { + UserId = channel.Id, + GroupId = groupId, + ChatterId = chatterId, + ChatterLabel = chatterLabel, + }; + + if (!channel.Groups.Chatters.TryGetValue(groupId, out var chatters)) + return Task.FromResult(RequestResult.Failed($"The group does not exist.")); + + bool result = chatters.Modify(chatterId.ToString(), groupChatter); + if (result) + { + _logger.Information($"Updated group chatter on channel [group id: {id}][group id: {groupId}][chatter id: {chatterId}][chatter label: {chatterLabel}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(groupChatter)); + } + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/UpdateGroupPermission.cs b/Requests/UpdateGroupPermission.cs new file mode 100644 index 0000000..4c23bfc --- /dev/null +++ b/Requests/UpdateGroupPermission.cs @@ -0,0 +1,43 @@ +using HermesSocketLibrary.Requests.Messages; +using HermesSocketServer.Models; +using ILogger = Serilog.ILogger; + +namespace HermesSocketServer.Requests +{ + public class UpdateGroupPermission : IRequest + { + public string Name => "update_group_permission"; + public string[] RequiredKeys => ["id", "group", "path", "allow"]; + private ILogger _logger; + + public UpdateGroupPermission(ILogger logger) + { + _logger = logger; + } + + public Task Grant(Channel channel, IDictionary data) + { + var id = data["id"].ToString()!; + string groupId = data["group"].ToString()!; + string path = data["path"].ToString()!; + bool? allow = bool.TryParse(data["allow"].ToString()!, out bool a) ? a : null; + + var permission = new GroupPermission() + { + Id = id.ToString(), + UserId = channel.Id, + GroupId = groupId, + Path = path, + Allow = allow, + }; + + bool result = channel.GroupPermissions.Modify(id.ToString(), permission); + if (result) + { + _logger.Information($"Updated group permission on channel [permission id: {id}][group id: {groupId}][path: {path}][allow: {allow}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(permission)); + } + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); + } + } +} \ No newline at end of file diff --git a/Requests/UpdatePolicy.cs b/Requests/UpdatePolicy.cs index 2403ed0..03e5c42 100644 --- a/Requests/UpdatePolicy.cs +++ b/Requests/UpdatePolicy.cs @@ -23,7 +23,7 @@ namespace HermesSocketServer.Requests int count = int.Parse(data["count"].ToString()!); int span = int.Parse(data["span"].ToString()!); - bool result = channel.Policies.Set(id.ToString(), new Policy() + var policy = new Policy() { Id = id, UserId = channel.Id, @@ -31,12 +31,12 @@ namespace HermesSocketServer.Requests Path = path, Usage = count, Span = span, - }); + }; + bool result = channel.Policies.Modify(id.ToString(), policy); if (result) { - var policy = channel.Policies.Get(id.ToString()); - _logger.Information($"Updated policy to channel [policy id: {id}][group id: {groupId}][path: {path}][count: {count}][span: {span}][channel: {channel.Id}]"); + _logger.Information($"Updated policy on channel [policy id: {id}][group id: {groupId}][path: {path}][count: {count}][span: {span}][channel: {channel.Id}]"); return Task.FromResult(RequestResult.Successful(policy)); } return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); diff --git a/Requests/UpdateRedeemableAction.cs b/Requests/UpdateRedeemableAction.cs index 70b560b..5487328 100644 --- a/Requests/UpdateRedeemableAction.cs +++ b/Requests/UpdateRedeemableAction.cs @@ -44,7 +44,7 @@ namespace HermesSocketServer.Requests bool result = channel.Actions.Modify(name, action); if (result) { - _logger.Information($"Added redeemable action to channel [name: {name}][type: {type}][channel: {channel.Id}]"); + _logger.Information($"Updated redeemable action on channel [name: {name}][type: {type}][channel: {channel.Id}]"); return Task.FromResult(RequestResult.Successful(action)); } if (channel.Actions.Get(name) == null) diff --git a/Requests/UpdateRedemption.cs b/Requests/UpdateRedemption.cs index f623dff..7ed200a 100644 --- a/Requests/UpdateRedemption.cs +++ b/Requests/UpdateRedemption.cs @@ -36,16 +36,11 @@ namespace HermesSocketServer.Requests }; bool result = channel.Redemptions.Modify(id, redemption); - - var r = channel.Redemptions.Get(id); if (result) { - _logger.Information($"Updated redemption to channel [id: {id}][redemption id: {redemptionId}][action: {actionName}][order: {order}][channel: {channel.Id}]"); - return Task.FromResult(RequestResult.Successful(r)); + _logger.Information($"Updated redemption on channel [id: {id}][redemption id: {redemptionId}][action: {actionName}][order: {order}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(redemption)); } - - if (r == null || r.UserId != channel.Id) - return Task.FromResult(RequestResult.Failed("Redemption does not exist.")); return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); } } diff --git a/Requests/UpdateTTSFilter.cs b/Requests/UpdateTTSFilter.cs index d1df472..7b79db9 100644 --- a/Requests/UpdateTTSFilter.cs +++ b/Requests/UpdateTTSFilter.cs @@ -36,10 +36,9 @@ namespace HermesSocketServer.Requests }; bool result = channel.Filters.Modify(id, filter); - if (result) { - _logger.Information($"Updated filter to channel [filter id: {id}][search: {search}][replace: {replace}][channel: {channel.Id}]"); + _logger.Information($"Updated filter on channel [filter id: {id}][search: {search}][replace: {replace}][channel: {channel.Id}]"); return Task.FromResult(RequestResult.Successful(filter)); } return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); diff --git a/Requests/UpdateTTSUser.cs b/Requests/UpdateTTSUser.cs index 2a5c6dc..6416fd6 100644 --- a/Requests/UpdateTTSUser.cs +++ b/Requests/UpdateTTSUser.cs @@ -23,25 +23,27 @@ namespace HermesSocketServer.Requests public async Task Grant(Channel channel, IDictionary data) { - if (long.TryParse(data["chatter"].ToString(), out long chatterId)) - data["chatter"] = chatterId; - data["voice"] = data["voice"].ToString()!; - data["user"] = channel.Id; + if (!long.TryParse(data["chatter"].ToString(), out long chatterId)) + return RequestResult.Failed("Chatter should be an integer, representing the Twitch User Id of the chatter."); + + var voiceId = data["voice"].ToString()!; 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.Tts.OwnerId) return RequestResult.Failed("Voice is either non-existent or disabled on this channel."); - var result = channel.Chatters.Set(chatterId.ToString(), new ChatterVoice() + var voice = new ChatterVoice() { UserId = channel.Id, ChatterId = chatterId, - VoiceId = data["voice"].ToString()! - }); + VoiceId = voiceId + }; + + var result = channel.Chatters.Modify(chatterId.ToString(), voice); if (result) { - _logger.Information($"Updated chatter's [chatter: {data["chatter"]}] selected tts voice [voice: {data["voice"]}] in channel [channel: {channel.Id}]"); - return RequestResult.Successful(null); + _logger.Information($"Updated chatter's selected tts voice on channel [chatter id: {chatterId}][voice id: {voiceId}][channel: {channel.Id}]"); + return RequestResult.Successful(voice); } return RequestResult.Failed("Soemthing went wrong when updating the cache."); } diff --git a/Requests/UpdateTTSVoice.cs b/Requests/UpdateTTSVoice.cs index ed8b13b..3576e00 100644 --- a/Requests/UpdateTTSVoice.cs +++ b/Requests/UpdateTTSVoice.cs @@ -23,15 +23,17 @@ namespace HermesSocketServer.Requests string voiceName = data["voice"].ToString()!; string voiceId = data["voiceid"].ToString()!; - var result = _voices.Set(voiceId, new TTSVoice() + var voice = new TTSVoice() { Id = voiceId, Name = voiceName - }); + }; + + var result = _voices.Modify(voiceId, voice); if (result) { - _logger.Information($"Updated voice's [voice id: {voiceId}] name [new name: {voiceName}]"); - return Task.FromResult(RequestResult.Successful(null)); + _logger.Information($"Updated voice's name on channel [voice id: {voiceId}][name: {voiceName}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(voice)); } return Task.FromResult(RequestResult.Failed("Something went wrong when updating the cache.")); } diff --git a/Requests/UpdateTTSVoiceState.cs b/Requests/UpdateTTSVoiceState.cs index d4d9c8d..08c7cc7 100644 --- a/Requests/UpdateTTSVoiceState.cs +++ b/Requests/UpdateTTSVoiceState.cs @@ -1,4 +1,5 @@ using HermesSocketLibrary.db; +using HermesSocketLibrary.Requests.Messages; using HermesSocketServer.Models; using ILogger = Serilog.ILogger; @@ -17,18 +18,25 @@ namespace HermesSocketServer.Requests _logger = logger; } - public async Task Grant(Channel channel, IDictionary data) + public Task Grant(Channel channel, IDictionary data) { - data["voice"] = data["voice"].ToString()!; - data["state"] = data["state"].ToString() == "True"; - data["user"] = channel.Id; + var id = data["voice"].ToString()!; + var state = data["state"].ToString() == "True"; - string sql = "INSERT INTO \"TtsVoiceState\" (\"userId\", \"ttsVoiceId\", state) VALUES (@user, @voice, @state) ON CONFLICT (\"userId\", \"ttsVoiceId\") DO UPDATE SET state = @state"; - var result = await _database.Execute(sql, data); - _logger.Information($"Updated voice's [voice id: {data["voice"]}] state [new state: {data["state"]}][channel: {data["user"]}]"); - if (result > 0) - return RequestResult.Successful(null); - return RequestResult.Failed("Something went wrong when updating the database."); + var voiceState = new TTSVoiceState() + { + Id = id, + UserId = channel.Id, + Enabled = state, + }; + + var result = channel.VoiceStates.Set(id, voiceState); + if (result) + { + _logger.Information($"Updated voice state on channel [voice id: {id}][state: {state}][channel: {channel.Id}]"); + return Task.FromResult(RequestResult.Successful(voiceState)); + } + return Task.FromResult(RequestResult.Failed("Something went wrong when updating the database.")); } } } \ No newline at end of file diff --git a/Services/ChannelManager.cs b/Services/ChannelManager.cs index 9549cc5..42b9a64 100644 --- a/Services/ChannelManager.cs +++ b/Services/ChannelManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using HermesSocketLibrary.db; using HermesSocketServer.Models; using HermesSocketServer.Store; +using HermesSocketServer.Validators; namespace HermesSocketServer.Services { @@ -38,29 +39,35 @@ namespace HermesSocketServer.Services var actionTable = _configuration.Database.Tables["Action"]; var chatterTable = _configuration.Database.Tables["Chatter"]; + var connectionTable = _configuration.Database.Tables["Connection"]; //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 ttsVoiceStateTable = _configuration.Database.Tables["VoiceState"]; var chatters = new ChatterStore(userId, chatterTable, _database, _logger); + var connections = new ConnectionStore(userId, connectionTable, _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); var redemptions = new RedemptionStore(userId, redemptionTable, _database, _logger); + var voiceStates = new VoiceStateStore(userId, new VoiceIdValidator(), ttsVoiceStateTable, _database, _logger); Task.WaitAll([ chatters.Load(), + connections.Load(), groups.Load(), groupPermissions.Load(), policies.Load(), filters.Load(), actions.Load(), redemptions.Load(), + voiceStates.Save(), ]); var channel = new Channel() @@ -68,12 +75,14 @@ namespace HermesSocketServer.Services Id = userId, User = user, Chatters = chatters, + Connections = connections, Groups = groups, GroupPermissions = groupPermissions, Policies = policies, Filters = filters, Actions = actions, Redemptions = redemptions, + VoiceStates = voiceStates, }; _channels.Add(userId, channel); @@ -93,12 +102,17 @@ namespace HermesSocketServer.Services if (!_channels.TryGetValue(userId, out var channel)) return; + _logger.Debug($"Saving channel data to database [channel id: {channel.Id}][channel name: {channel.User.Name}]"); await Task.WhenAll([ channel.Chatters.Save(), + channel.Connections.Save(), + channel.Groups.Save(), + channel.GroupPermissions.Save(), channel.Policies.Save(), channel.Filters.Save(), channel.Actions.Save(), channel.Redemptions.Save(), + channel.VoiceStates.Save(), ]); } @@ -109,10 +123,14 @@ namespace HermesSocketServer.Services _logger.Debug($"Saving channel data to database [channel id: {channel.Id}][channel name: {channel.User.Name}]"); await Task.WhenAll([ channel.Chatters.Save(), + channel.Connections.Save(), + channel.Groups.Save(), + channel.GroupPermissions.Save(), channel.Policies.Save(), channel.Filters.Save(), channel.Actions.Save(), channel.Redemptions.Save(), + channel.VoiceStates.Save(), ]); } } diff --git a/Socket/Handlers/HermesLoginHandler.cs b/Socket/Handlers/HermesLoginHandler.cs index 78f05c4..4144335 100644 --- a/Socket/Handlers/HermesLoginHandler.cs +++ b/Socket/Handlers/HermesLoginHandler.cs @@ -102,6 +102,7 @@ namespace HermesSocketServer.Socket.Handlers await _database.Execute(sql3, userIdDict, sql => ack.Connections.Add(new Connection() { + UserId = channel.Id, Name = sql.GetString(0), Type = sql.GetString(1), ClientId = sql.GetString(2), diff --git a/Startup.cs b/Startup.cs index 0eb66bf..2494450 100644 --- a/Startup.cs +++ b/Startup.cs @@ -94,15 +94,23 @@ s.AddSingleton, VoiceStore>(); s.AddSingleton, UserStore>(); // Request handlers +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); -s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); @@ -117,7 +125,11 @@ s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); +s.AddSingleton(); s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); @@ -149,9 +161,7 @@ var wsOptions = new WebSocketOptions() { KeepAliveInterval = TimeSpan.FromSeconds(30) }; -// wsOptions.AllowedOrigins.Add("wss://tomtospeech.com"); -//wsOptions.AllowedOrigins.Add("ws.tomtospeech.com"); -//wsOptions.AllowedOrigins.Add("hermes-ws.goblincaves.com"); + app.UseWebSockets(wsOptions); var options = app.Services.GetRequiredService(); diff --git a/Store/ConnectionStore.cs b/Store/ConnectionStore.cs new file mode 100644 index 0000000..6d57ae4 --- /dev/null +++ b/Store/ConnectionStore.cs @@ -0,0 +1,75 @@ +using HermesSocketLibrary.db; +using HermesSocketLibrary.Socket.Data; +using HermesSocketServer.Store.Internal; + +namespace HermesSocketServer.Store +{ + public class ConnectionStore : ComplexAutoSavedStore + { + private readonly string _userId; + private readonly Database _database; + private readonly Serilog.ILogger _logger; + + public ConnectionStore(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 name, \"type\", \"clientId\", \"accessToken\", \"grantType\", \"scope\", \"expiresAt\", \"default\" FROM \"Connection\" WHERE \"userId\" = @user"; + await _database.Execute(sql, data, (reader) => + { + var name = reader.GetString(0); + _store.Add(name, new Connection() + { + Name = name, + UserId = _userId, + Type = reader.GetString(1), + ClientId = reader.GetString(2), + AccessToken = reader.GetString(3), + GrantType = reader.GetString(4), + Scope = reader.GetString(5), + ExpiresAt = reader.GetDateTime(6), + Default = reader.GetBoolean(7), + }); + }); + _logger.Information($"Loaded {_store.Count} groups from database."); + } + + protected override void OnInitialAdd(string key, Connection 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)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.Type, nameof(value.Type)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.ClientId, nameof(value.ClientId)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.AccessToken, nameof(value.AccessToken)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.GrantType, nameof(value.GrantType)); + ArgumentException.ThrowIfNullOrWhiteSpace(value.Scope, nameof(value.Scope)); + ArgumentNullException.ThrowIfNull(value.ExpiresAt, nameof(value.ExpiresAt)); + ArgumentNullException.ThrowIfNull(value.Default, nameof(value.Default)); + } + + protected override void OnInitialModify(string key, Connection oldValue, Connection newValue) + { + ArgumentNullException.ThrowIfNull(newValue, nameof(newValue)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.UserId, nameof(newValue.UserId)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.Name, nameof(newValue.Name)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.Type, nameof(newValue.Type)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.ClientId, nameof(newValue.ClientId)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.AccessToken, nameof(newValue.AccessToken)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.GrantType, nameof(newValue.GrantType)); + ArgumentException.ThrowIfNullOrWhiteSpace(newValue.Scope, nameof(newValue.Scope)); + ArgumentNullException.ThrowIfNull(newValue.ExpiresAt, nameof(newValue.ExpiresAt)); + ArgumentNullException.ThrowIfNull(newValue.Default, nameof(newValue.Default)); + ArgumentOutOfRangeException.ThrowIfNotEqual(oldValue.UserId, newValue.UserId, nameof(oldValue.UserId)); + ArgumentOutOfRangeException.ThrowIfNotEqual(oldValue.Name, newValue.Name, nameof(oldValue.Name)); + } + } +} \ No newline at end of file diff --git a/Store/GroupStore.cs b/Store/GroupStore.cs index ff2c552..364046f 100644 --- a/Store/GroupStore.cs +++ b/Store/GroupStore.cs @@ -7,7 +7,7 @@ namespace HermesSocketServer.Store { public class GroupStore : AutoSavedStore { - private static readonly string[] AUTO_GENERATED_GROUP_NAMES = ["everyone", "subscribers", "vip", "moderators", "broadcaster"]; + public static readonly string[] AUTO_GENERATED_GROUP_NAMES = ["everyone", "subscribers", "vip", "moderators", "broadcaster"]; private IDictionary _chatters; private readonly string _userId;