From 472bfcee5d9d5cf4568218a96f0374892c35d999 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 19 Jul 2024 16:56:41 +0000 Subject: [PATCH] Changed command dictionary to a command tree. Fixed various requests. OBS reconnection added if identified previously. --- Chat/ChatMessageHandler.cs | 18 +- Chat/Commands/AddTTSVoiceCommand.cs | 50 --- Chat/Commands/ChatCommand.cs | 35 +- Chat/Commands/ChatCommandManager.cs | 149 -------- Chat/Commands/CommandBuilder.cs | 356 ++++++++++++++++++ Chat/Commands/CommandManager.cs | 124 ++++++ Chat/Commands/OBSCommand.cs | 198 ++++++---- .../Parameters/ChatCommandParameter.cs | 27 -- Chat/Commands/Parameters/CommandParameter.cs | 20 + .../Parameters/OBSTransformationParameter.cs | 16 + .../Parameters/SimpleListedParameter.cs | 17 - Chat/Commands/Parameters/StateParameter.cs | 16 + Chat/Commands/Parameters/StaticParameter.cs | 19 + .../Parameters/TTSVoiceNameParameter.cs | 11 +- .../Parameters/UnvalidatedParameter.cs | 4 +- Chat/Commands/RefreshCommand.cs | 163 ++++++++ Chat/Commands/RefreshTTSDataCommand.cs | 141 ------- Chat/Commands/RemoveTTSVoiceCommand.cs | 54 --- Chat/Commands/SkipAllCommand.cs | 37 -- Chat/Commands/SkipCommand.cs | 87 ++++- Chat/Commands/TTSCommand.cs | 180 +++++++-- Chat/Commands/VersionCommand.cs | 35 +- Chat/Commands/VoiceCommand.cs | 69 ++-- Chat/Groups/ChatterGroupManager.cs | 6 +- Chat/Groups/Group.cs | 9 - Chat/Groups/GroupChatter.cs | 8 - Chat/Groups/IChatterGroupManager.cs | 2 + Chat/Groups/Permissions/GroupPermission.cs | 10 - .../Permissions/GroupPermissionManager.cs | 8 +- Helpers/WebClientWrap.cs | 2 +- Hermes/Socket/Handlers/LoginAckHandler.cs | 42 +-- Hermes/Socket/Handlers/RequestAckHandler.cs | 86 ++++- Hermes/Socket/HermesSocketClient.cs | 81 +++- .../Socket/Managers/HermesHandlerManager.cs | 41 -- .../Managers/HermesHandlerTypeManager.cs | 10 +- OBS/Socket/Data/IdentifyMessage.cs | 12 +- OBS/Socket/Handlers/EventMessageHandler.cs | 13 +- OBS/Socket/Handlers/HelloHandler.cs | 6 +- OBS/Socket/Handlers/IdentifiedHandler.cs | 13 +- .../Handlers/RequestBatchResponseHandler.cs | 58 ++- OBS/Socket/Handlers/RequestResponseHandler.cs | 19 +- OBS/Socket/Manager/OBSHandlerManager.cs | 34 -- OBS/Socket/Manager/OBSHandlerTypeManager.cs | 11 +- OBS/Socket/Manager/OBSManager.cs | 316 ---------------- OBS/Socket/OBSSocketClient.cs | 354 ++++++++++++++++- Seven/SevenManager.cs | 57 --- Seven/Socket/Context/ReconnectContext.cs | 7 - Seven/Socket/Handlers/EndOfStreamHandler.cs | 85 +---- Seven/Socket/Handlers/SevenHelloHandler.cs | 1 - Seven/Socket/Managers/SevenHandlerManager.cs | 41 -- .../Managers/SevenHandlerTypeManager.cs | 12 +- Seven/Socket/SevenSocketClient.cs | 124 +++++- Startup.cs | 73 ++-- TTS.cs | 109 +----- Twitch/Redemptions/RedemptionManager.cs | 16 +- User.cs | 4 - 56 files changed, 1943 insertions(+), 1553 deletions(-) delete mode 100644 Chat/Commands/AddTTSVoiceCommand.cs delete mode 100644 Chat/Commands/ChatCommandManager.cs create mode 100644 Chat/Commands/CommandBuilder.cs create mode 100644 Chat/Commands/CommandManager.cs delete mode 100644 Chat/Commands/Parameters/ChatCommandParameter.cs create mode 100644 Chat/Commands/Parameters/CommandParameter.cs create mode 100644 Chat/Commands/Parameters/OBSTransformationParameter.cs delete mode 100644 Chat/Commands/Parameters/SimpleListedParameter.cs create mode 100644 Chat/Commands/Parameters/StateParameter.cs create mode 100644 Chat/Commands/Parameters/StaticParameter.cs create mode 100644 Chat/Commands/RefreshCommand.cs delete mode 100644 Chat/Commands/RefreshTTSDataCommand.cs delete mode 100644 Chat/Commands/RemoveTTSVoiceCommand.cs delete mode 100644 Chat/Commands/SkipAllCommand.cs delete mode 100644 Chat/Groups/Group.cs delete mode 100644 Chat/Groups/GroupChatter.cs delete mode 100644 Chat/Groups/Permissions/GroupPermission.cs delete mode 100644 Hermes/Socket/Managers/HermesHandlerManager.cs delete mode 100644 OBS/Socket/Manager/OBSHandlerManager.cs delete mode 100644 OBS/Socket/Manager/OBSManager.cs delete mode 100644 Seven/SevenManager.cs delete mode 100644 Seven/Socket/Context/ReconnectContext.cs delete mode 100644 Seven/Socket/Managers/SevenHandlerManager.cs diff --git a/Chat/ChatMessageHandler.cs b/Chat/ChatMessageHandler.cs index 616c528..43a9417 100644 --- a/Chat/ChatMessageHandler.cs +++ b/Chat/ChatMessageHandler.cs @@ -6,22 +6,22 @@ using TwitchChatTTS.Chat.Commands; using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups; -using TwitchChatTTS.OBS.Socket.Manager; using TwitchChatTTS.Chat.Emotes; using Microsoft.Extensions.DependencyInjection; using CommonSocketLibrary.Common; using CommonSocketLibrary.Abstract; +using TwitchChatTTS.OBS.Socket; public class ChatMessageHandler { private readonly User _user; private readonly TTSPlayer _player; - private readonly ChatCommandManager _commands; + private readonly CommandManager _commands; private readonly IGroupPermissionManager _permissionManager; private readonly IChatterGroupManager _chatterGroupManager; private readonly IEmoteDatabase _emotes; - private readonly OBSManager _obsManager; + private readonly OBSSocketClient _obs; private readonly HermesSocketClient _hermes; private readonly Configuration _configuration; @@ -36,12 +36,12 @@ public class ChatMessageHandler public ChatMessageHandler( User user, TTSPlayer player, - ChatCommandManager commands, + CommandManager commands, IGroupPermissionManager permissionManager, IChatterGroupManager chatterGroupManager, IEmoteDatabase emotes, - OBSManager obsManager, [FromKeyedServices("hermes")] SocketClient hermes, + [FromKeyedServices("obs")] SocketClient obs, Configuration configuration, ILogger logger ) @@ -52,7 +52,7 @@ public class ChatMessageHandler _permissionManager = permissionManager; _chatterGroupManager = chatterGroupManager; _emotes = emotes; - _obsManager = obsManager; + _obs = (obs as OBSSocketClient)!; _hermes = (hermes as HermesSocketClient)!; _configuration = configuration; _logger = logger; @@ -71,7 +71,7 @@ public class ChatMessageHandler _logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]"); return new MessageResult(MessageStatus.NotReady, -1, -1); } - if (_configuration.Twitch?.TtsWhenOffline != true && !_obsManager.Streaming) + if (_configuration.Twitch?.TtsWhenOffline != true && !_obs.Streaming) { _logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {m.Id}]"); return new MessageResult(MessageStatus.NotReady, -1, -1); @@ -109,7 +109,7 @@ public class ChatMessageHandler return new MessageResult(MessageStatus.Blocked, -1, -1); } - if (_obsManager.Streaming && !_chatters.Contains(chatterId)) + if (_obs.Streaming && !_chatters.Contains(chatterId)) { tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username)); _chatters.Add(chatterId); @@ -148,7 +148,7 @@ public class ChatMessageHandler if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5)) filteredMsg += w + " "; } - if (_obsManager.Streaming && newEmotes.Any()) + if (_obs.Streaming && newEmotes.Any()) tasks.Add(_hermes.SendEmoteDetails(newEmotes)); msg = filteredMsg; diff --git a/Chat/Commands/AddTTSVoiceCommand.cs b/Chat/Commands/AddTTSVoiceCommand.cs deleted file mode 100644 index 56ebb1c..0000000 --- a/Chat/Commands/AddTTSVoiceCommand.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using TwitchChatTTS.Chat.Commands.Parameters; -using TwitchChatTTS.Hermes.Socket; -using TwitchLib.Client.Models; - -namespace TwitchChatTTS.Chat.Commands -{ - public class AddTTSVoiceCommand : ChatCommand - { - private readonly User _user; - private readonly ILogger _logger; - - public new bool DefaultPermissionsOverwrite { get => true; } - - public AddTTSVoiceCommand( - User user, - [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter, - ILogger logger - ) : base("addttsvoice", "Select a TTS voice as the default for that user.") - { - _user = user; - _logger = logger; - - AddParameter(unvalidatedParameter); - } - - public override async Task CheckDefaultPermissions(ChatMessage message) - { - return false; - } - - public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) - { - if (_user == null || _user.VoicesAvailable == null) - return; - - var voiceName = args.First(); - var voiceNameLower = voiceName.ToLower(); - var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower); - if (exists) { - _logger.Information("Voice already exists."); - return; - } - - await client.CreateTTSVoice(voiceName); - _logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]"); - } - } -} \ No newline at end of file diff --git a/Chat/Commands/ChatCommand.cs b/Chat/Commands/ChatCommand.cs index 1e0fba5..cf6046d 100644 --- a/Chat/Commands/ChatCommand.cs +++ b/Chat/Commands/ChatCommand.cs @@ -1,34 +1,17 @@ -using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; +using static TwitchChatTTS.Chat.Commands.TTSCommands; namespace TwitchChatTTS.Chat.Commands { - public abstract class ChatCommand - { - public string Name { get; } - public string Description { get; } - public IList Parameters { get => _parameters.AsReadOnly(); } - public bool DefaultPermissionsOverwrite { get; } + public interface IChatCommand { + string Name { get; } + void Build(ICommandBuilder builder); + } - private IList _parameters; - - public ChatCommand(string name, string description) - { - Name = name; - Description = description; - DefaultPermissionsOverwrite = false; - _parameters = new List(); - } - - protected void AddParameter(ChatCommandParameter parameter, bool optional = false) - { - if (parameter != null && parameter.Clone() is ChatCommandParameter p) { - _parameters.Add(optional ? p.Permissive() : p); - } - } - - public abstract Task CheckDefaultPermissions(ChatMessage message); - public abstract Task Execute(IList args, ChatMessage message, HermesSocketClient client); + public interface IChatPartialCommand { + bool AcceptCustomPermission { get; } + bool CheckDefaultPermissions(ChatMessage message); + Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client); } } \ No newline at end of file diff --git a/Chat/Commands/ChatCommandManager.cs b/Chat/Commands/ChatCommandManager.cs deleted file mode 100644 index 00146aa..0000000 --- a/Chat/Commands/ChatCommandManager.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.Text.RegularExpressions; -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using TwitchChatTTS.Chat.Groups.Permissions; -using TwitchChatTTS.Hermes.Socket; -using TwitchLib.Client.Models; - -namespace TwitchChatTTS.Chat.Commands -{ - public class ChatCommandManager - { - private IDictionary _commands; - private readonly User _user; - private readonly HermesSocketClient _hermes; - private readonly IGroupPermissionManager _permissionManager; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private string CommandStartSign { get; } = "!"; - - - public ChatCommandManager( - User user, - [FromKeyedServices("hermes")] SocketClient socketClient, - IGroupPermissionManager permissionManager, - IServiceProvider serviceProvider, - ILogger logger - ) - { - _user = user; - _hermes = (socketClient as HermesSocketClient)!; - _permissionManager = permissionManager; - _serviceProvider = serviceProvider; - _logger = logger; - - _commands = new Dictionary(); - GenerateCommands(); - } - - private void Add(ChatCommand command) - { - _commands.Add(command.Name.ToLower(), command); - } - - private void GenerateCommands() - { - var basetype = typeof(ChatCommand); - var assembly = GetType().Assembly; - var types = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Chat.") == true); - - foreach (var type in types) - { - var key = "command-" + type.Name.Replace("Commands", "Comm#ands") - .Replace("Command", "") - .Replace("Comm#ands", "Commands") - .ToLower(); - - var command = _serviceProvider.GetKeyedService(key); - if (command == null) - { - _logger.Error("Failed to add chat command: " + type.AssemblyQualifiedName); - continue; - } - - _logger.Debug($"Added chat command {type.AssemblyQualifiedName}"); - Add(command); - } - } - - public async Task Execute(string arg, ChatMessage message, IEnumerable groups) - { - if (string.IsNullOrWhiteSpace(arg)) - return ChatCommandResult.Unknown; - - arg = arg.Trim(); - - if (!arg.StartsWith(CommandStartSign)) - return ChatCommandResult.Unknown; - - string[] parts = Regex.Matches(arg, "(?[^\"\\n\\s]+|\"[^\"\\n]*\")") - .Cast() - .Select(m => m.Groups["match"].Value) - .Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m) - .ToArray(); - string com = parts.First().Substring(CommandStartSign.Length).ToLower(); - string[] args = parts.Skip(1).ToArray(); - - if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null) - { - // Could be for another bot or just misspelled. - _logger.Debug($"Failed to find command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]"); - return ChatCommandResult.Missing; - } - - // Check if command can be executed by this chatter. - long chatterId = long.Parse(message.UserId); - if (chatterId != _user.OwnerId) - { - var executable = command.DefaultPermissionsOverwrite ? false : CanExecute(chatterId, groups, com); - if (executable == false) - { - _logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]"); - return ChatCommandResult.Permission; - } - else if (executable == null && !await command.CheckDefaultPermissions(message)) - { - _logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]"); - return ChatCommandResult.Permission; - } - } - - // Check if the syntax is correct. - if (command.Parameters.Count(p => !p.Optional) > args.Length) - { - _logger.Debug($"Command syntax issue when executing command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]"); - return ChatCommandResult.Syntax; - } - - for (int i = 0; i < Math.Min(args.Length, command.Parameters.Count); i++) - { - if (!command.Parameters[i].Validate(args[i])) - { - _logger.Warning($"Commmand '{com}' failed because of the #{i + 1} argument. Invalid value: {args[i]}"); - return ChatCommandResult.Syntax; - } - } - - try - { - await command.Execute(args, message, _hermes); - } - catch (Exception e) - { - _logger.Error(e, $"Command '{arg}' failed [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]"); - return ChatCommandResult.Fail; - } - - _logger.Information($"Executed the {com} command [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]"); - return ChatCommandResult.Success; - } - - private bool? CanExecute(long chatterId, IEnumerable groups, string path) - { - _logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]"); - return _permissionManager.CheckIfAllowed(groups, path); - } - } -} \ No newline at end of file diff --git a/Chat/Commands/CommandBuilder.cs b/Chat/Commands/CommandBuilder.cs new file mode 100644 index 0000000..d776bd4 --- /dev/null +++ b/Chat/Commands/CommandBuilder.cs @@ -0,0 +1,356 @@ +using Serilog; +using TwitchChatTTS.Chat.Commands.Parameters; + +namespace TwitchChatTTS.Chat.Commands +{ + public static class TTSCommands + { + public interface ICommandBuilder + { + ICommandSelector Build(); + void Clear(); + ICommandBuilder CreateCommandTree(string name, Action callback); + ICommandBuilder CreateCommand(IChatPartialCommand command); + ICommandBuilder CreateStaticInputParameter(string value, Action callback, bool optional = false); + ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false); + ICommandBuilder CreateStateParameter(string name, bool optional = false); + ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false); + ICommandBuilder CreateVoiceNameParameter(string name, bool enabled, bool optional = false); + + } + + public sealed class CommandBuilder : ICommandBuilder + { + private CommandNode _root; + private CommandNode _current; + private Stack _stack; + private readonly User _user; + private readonly ILogger _logger; + + public CommandBuilder(User user, ILogger logger) + { + _user = user; + _logger = logger; + + _stack = new Stack(); + Clear(); + } + + + public ICommandSelector Build() + { + return new CommandSelector(_root); + } + + public void Clear() + { + _root = new CommandNode(new StaticParameter("root", "root")); + ResetToRoot(); + } + + public ICommandBuilder CreateCommandTree(string name, Action callback) + { + ResetToRoot(); + + var node = _current.CreateStaticInput(name); + _logger.Debug($"Creating command name '{name}'"); + CreateStack(() => + { + _current = node; + callback(this); + }); + return this; + } + + public ICommandBuilder CreateCommand(IChatPartialCommand command) + { + if (_root == _current) + throw new Exception("Cannot create a command without a command name."); + + _current.CreateCommand(command); + _logger.Debug($"Set command to '{command.GetType().Name}'"); + return this; + } + + public ICommandBuilder CreateStaticInputParameter(string value, Action callback, bool optional = false) + { + if (_root == _current) + throw new Exception("Cannot create a parameter without a command name."); + if (optional && _current.IsRequired() && _current.Command == null) + throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); + + var node = _current.CreateStaticInput(value, optional); + _logger.Debug($"Creating static parameter '{value}'"); + CreateStack(() => + { + _current = node; + callback(this); + }); + return this; + } + + public ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false) + { + if (_root == _current) + throw new Exception("Cannot create a parameter without a command name."); + if (optional && _current.IsRequired() && _current.Command == null) + throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); + + var node = _current.CreateUserInput(new OBSTransformationParameter(name, optional)); + _logger.Debug($"Creating obs transformation parameter '{name}'"); + _current = node; + return this; + } + + public ICommandBuilder CreateStateParameter(string name, bool optional = false) + { + if (_root == _current) + throw new Exception("Cannot create a parameter without a command name."); + if (optional && _current.IsRequired() && _current.Command == null) + throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); + + var node = _current.CreateUserInput(new StateParameter(name, optional)); + _logger.Debug($"Creating unvalidated parameter '{name}'"); + _current = node; + return this; + } + + public ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false) + { + if (_root == _current) + throw new Exception("Cannot create a parameter without a command name."); + if (optional && _current.IsRequired() && _current.Command == null) + throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); + + var node = _current.CreateUserInput(new UnvalidatedParameter(name, optional)); + _logger.Debug($"Creating unvalidated parameter '{name}'"); + _current = node; + return this; + } + + public ICommandBuilder CreateVoiceNameParameter(string name, bool enabled, bool optional = false) + { + if (_root == _current) + throw new Exception("Cannot create a parameter without a command name."); + if (optional && _current.IsRequired() && _current.Command == null) + throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter."); + + var node = _current.CreateUserInput(new TTSVoiceNameParameter(name, enabled, _user, optional)); + _logger.Debug($"Creating tts voice name parameter '{name}'"); + _current = node; + return this; + } + + private ICommandBuilder ResetToRoot() + { + _current = _root; + _stack.Clear(); + return this; + } + + private void CreateStack(Action func) + { + try + { + _stack.Push(_current); + func(); + } + finally + { + _current = _stack.Pop(); + } + } + } + + public interface ICommandSelector + { + CommandSelectorResult GetBestMatch(string[] args); + IDictionary GetNonStaticArguments(string[] args, string path); + CommandValidationResult Validate(string[] args, string path); + } + + public sealed class CommandSelector : ICommandSelector + { + private CommandNode _root; + + public CommandSelector(CommandNode root) + { + _root = root; + } + + public CommandSelectorResult GetBestMatch(string[] args) + { + return GetBestMatch(_root, args, null, string.Empty); + } + + private CommandSelectorResult GetBestMatch(CommandNode node, IEnumerable args, IChatPartialCommand? match, string path) + { + if (node == null || !args.Any()) + return new CommandSelectorResult(match, path); + if (!node.Children.Any()) + return new CommandSelectorResult(node.Command ?? match, path); + + var argument = args.First(); + var argumentLower = argument.ToLower(); + foreach (var child in node.Children) + { + if (child.Parameter.GetType() == typeof(StaticParameter)) + { + if (child.Parameter.Name.ToLower() == argumentLower) + { + return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + child.Parameter.Name.ToLower()); + } + continue; + } + + return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + "*"); + } + + return new CommandSelectorResult(match, path); + } + + public CommandValidationResult Validate(string[] args, string path) + { + CommandNode? current = _root; + var parts = path.Split('.'); + if (args.Length < parts.Length) + throw new Exception($"Command path too long for the number of arguments passed in [path: {path}][parts: {parts.Length}][args count: {args.Length}]"); + + for (var i = 0; i < parts.Length; i++) + { + var part = parts[i]; + if (part == "*") + { + current = current.Children.FirstOrDefault(n => n.Parameter.GetType() != typeof(StaticParameter)); + if (current == null) + throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]"); + + if (!current.Parameter.Validate(args[i])) + { + return new CommandValidationResult(false, args[i]); + } + } + else + { + current = current.Children.FirstOrDefault(n => n.Parameter.GetType() == typeof(StaticParameter) && n.Parameter.Name == part); + if (current == null) + throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]"); + } + } + + return new CommandValidationResult(true, null); + } + + public IDictionary GetNonStaticArguments(string[] args, string path) + { + Dictionary arguments = new Dictionary(); + CommandNode? current = _root; + var parts = path.Split('.'); + if (args.Length < parts.Length) + throw new Exception($"Command path too long for the number of arguments passed in [path: {path}][parts: {parts.Length}][args count: {args.Length}]"); + + for (var i = 0; i < parts.Length; i++) + { + var part = parts[i]; + if (part == "*") + { + current = current.Children.FirstOrDefault(n => n.Parameter.GetType() != typeof(StaticParameter)); + if (current == null) + throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]"); + + arguments.Add(args[i], current.Parameter); + } + else + { + current = current.Children.FirstOrDefault(n => n.Parameter.GetType() == typeof(StaticParameter) && n.Parameter.Name == part); + if (current == null) + throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]"); + } + } + + return arguments; + } + } + + public class CommandSelectorResult + { + public IChatPartialCommand? Command { get; set; } + public string Path { get; set; } + + public CommandSelectorResult(IChatPartialCommand? command, string path) + { + Command = command; + Path = path; + } + } + + public class CommandValidationResult + { + public bool Result { get; set; } + public string? ErrorParameterName { get; set; } + + public CommandValidationResult(bool result, string? parameterName) + { + Result = result; + ErrorParameterName = parameterName; + } + } + + public sealed class CommandNode + { + public IChatPartialCommand? Command { get; private set; } + public CommandParameter Parameter { get; } + public IList Children { get => _children.AsReadOnly(); } + + private IList _children; + + public CommandNode(CommandParameter parameter) + { + Parameter = parameter; + _children = new List(); + } + + + public CommandNode CreateCommand(IChatPartialCommand command) + { + if (Command != null) + throw new InvalidOperationException("Cannot change the command of an existing one."); + + Command = command; + return this; + } + + public CommandNode CreateStaticInput(string value, bool optional = false) + { + if (Children.Any(n => n.Parameter.GetType() != typeof(StaticParameter))) + throw new InvalidOperationException("Cannot have mixed static and user inputs in the same position of a subcommand."); + return Create(n => n.Parameter.Name == value, new StaticParameter(value.ToLower(), value, optional)); + } + + public CommandNode CreateUserInput(CommandParameter parameter) + { + if (Children.Any(n => n.Parameter.GetType() == typeof(StaticParameter))) + throw new InvalidOperationException("Cannot have mixed static and user inputs in the same position of a subcommand."); + return Create(n => true, parameter); + } + + private CommandNode Create(Predicate predicate, CommandParameter parameter) + { + CommandNode? node = Children.FirstOrDefault(n => predicate(n)); + if (node == null) + { + node = new CommandNode(parameter); + _children.Add(node); + } + if (node.Parameter.GetType() != parameter.GetType()) + throw new Exception("User input argument already exist for this partial command."); + return node; + } + + public bool IsRequired() + { + return !Parameter.Optional; + } + } + } +} \ No newline at end of file diff --git a/Chat/Commands/CommandManager.cs b/Chat/Commands/CommandManager.cs new file mode 100644 index 0000000..5ba4cc8 --- /dev/null +++ b/Chat/Commands/CommandManager.cs @@ -0,0 +1,124 @@ +using System.Text.RegularExpressions; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using TwitchChatTTS.Chat.Groups.Permissions; +using TwitchChatTTS.Hermes.Socket; +using TwitchLib.Client.Models; +using static TwitchChatTTS.Chat.Commands.TTSCommands; + +namespace TwitchChatTTS.Chat.Commands +{ + public class CommandManager + { + private readonly User _user; + private readonly ICommandSelector _commandSelector; + private readonly HermesSocketClient _hermes; + private readonly IGroupPermissionManager _permissionManager; + private readonly ILogger _logger; + private string CommandStartSign { get; } = "!"; + + + public CommandManager( + IEnumerable commands, + ICommandBuilder commandBuilder, + User user, + [FromKeyedServices("hermes")] SocketClient socketClient, + IGroupPermissionManager permissionManager, + ILogger logger + ) + { + _user = user; + _hermes = (socketClient as HermesSocketClient)!; + _permissionManager = permissionManager; + _logger = logger; + + foreach (var command in commands) + { + _logger.Debug($"Creating command tree for '{command.Name}'."); + command.Build(commandBuilder); + } + + _commandSelector = commandBuilder.Build(); + } + + + public async Task Execute(string arg, ChatMessage message, IEnumerable groups) + { + if (string.IsNullOrWhiteSpace(arg)) + return ChatCommandResult.Unknown; + + arg = arg.Trim(); + + if (!arg.StartsWith(CommandStartSign)) + return ChatCommandResult.Unknown; + + string[] parts = Regex.Matches(arg.Substring(CommandStartSign.Length), "(?[^\"\\n\\s]+|\"[^\"\\n]*\")") + .Cast() + .Select(m => m.Groups["match"].Value) + .Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m) + .ToArray(); + string[] args = parts.ToArray(); + string com = args.First().ToLower(); + + CommandSelectorResult selectorResult = _commandSelector.GetBestMatch(args); + if (selectorResult.Command == null) + { + _logger.Warning($"Could not match '{arg}' to any command."); + return ChatCommandResult.Missing; + } + + // Check if command can be executed by this chatter. + var command = selectorResult.Command; + long chatterId = long.Parse(message.UserId); + if (chatterId != _user.OwnerId) + { + var executable = command.AcceptCustomPermission ? CanExecute(chatterId, groups, com) : null; + if (executable == false) + { + _logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]"); + return ChatCommandResult.Permission; + } + else if (executable == null && !command.CheckDefaultPermissions(message)) + { + _logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]"); + return ChatCommandResult.Permission; + } + } + + // Check if the arguments are correct. + var arguments = _commandSelector.GetNonStaticArguments(args, selectorResult.Path); + foreach (var entry in arguments) + { + var parameter = entry.Value; + var argument = entry.Key; + if (!parameter.Validate(argument)) + { + _logger.Warning($"Command failed due to an argument being invalid [argument name: {parameter.Name}][argument value: {argument}][arguments: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]"); + return ChatCommandResult.Syntax; + } + } + + var values = arguments.ToDictionary(d => d.Value.Name, d => d.Key); + try + { + await command.Execute(values, message, _hermes); + } + catch (Exception e) + { + _logger.Error(e, $"Command '{arg}' failed [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]"); + return ChatCommandResult.Fail; + } + + _logger.Information($"Executed the {com} command [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]"); + return ChatCommandResult.Success; + } + + private bool? CanExecute(long chatterId, IEnumerable groups, string path) + { + _logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]"); + return _permissionManager.CheckIfAllowed(groups, path); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/OBSCommand.cs b/Chat/Commands/OBSCommand.cs index ad8449b..ebb0685 100644 --- a/Chat/Commands/OBSCommand.cs +++ b/Chat/Commands/OBSCommand.cs @@ -2,89 +2,161 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using Microsoft.Extensions.DependencyInjection; using Serilog; -using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Hermes.Socket; +using TwitchChatTTS.OBS.Socket; using TwitchChatTTS.OBS.Socket.Data; -using TwitchChatTTS.OBS.Socket.Manager; using TwitchLib.Client.Models; +using static TwitchChatTTS.Chat.Commands.TTSCommands; namespace TwitchChatTTS.Chat.Commands { - public class OBSCommand : ChatCommand + public class OBSCommand : IChatCommand { - private readonly User _user; - private readonly OBSManager _manager; + private readonly OBSSocketClient _obs; private readonly ILogger _logger; + public string Name => "obs"; + public OBSCommand( - [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter, - User user, - OBSManager manager, + [FromKeyedServices("obs")] SocketClient obs, ILogger logger - ) : base("obs", "Various obs commands.") + ) { - _user = user; - _manager = manager; + _obs = (obs as OBSSocketClient)!; _logger = logger; - - AddParameter(unvalidatedParameter); - AddParameter(unvalidatedParameter, optional: true); - AddParameter(unvalidatedParameter, optional: true); - AddParameter(unvalidatedParameter, optional: true); } - public override async Task CheckDefaultPermissions(ChatMessage message) + + public void Build(ICommandBuilder builder) { - return message.IsModerator || message.IsBroadcaster; - } - - public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) - { - if (_user == null || _user.VoicesAvailable == null) - return; - - var action = args[0].ToLower(); - - switch (action) + builder.CreateCommandTree(Name, b => { - case "get_scene_item_id": - if (args.Count < 3) - return; + b.CreateStaticInputParameter("get_scene_item_id", b => + { + b.CreateUnvalidatedParameter("sceneName") + .CreateCommand(new OBSGetSceneItemId(_obs, _logger)); + }) + .CreateStaticInputParameter("transform", b => + { + b.CreateUnvalidatedParameter("sceneName") + .CreateUnvalidatedParameter("sourceName") + .CreateObsTransformationParameter("propertyName") + .CreateUnvalidatedParameter("value") + .CreateCommand(new OBSTransform(_obs, _logger)); + }) + .CreateStaticInputParameter("visibility", b => + { + b.CreateUnvalidatedParameter("sceneName") + .CreateUnvalidatedParameter("sourceName") + .CreateStateParameter("state") + .CreateCommand(new OBSVisibility(_obs, _logger)); + }); + }); + } - _logger.Debug($"Getting scene item id via chat command [args: {string.Join(" ", args)}]"); - await _manager.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", args[1] }, { "sourceName", args[2] } })); - break; - case "transform": - if (args.Count < 5) - return; + private sealed class OBSGetSceneItemId : IChatPartialCommand + { + private readonly OBSSocketClient _obs; + private readonly ILogger _logger; - _logger.Debug($"Getting scene item transformation data via chat command [args: {string.Join(" ", args)}]"); - await _manager.UpdateTransformation(args[1], args[2], (d) => - { - if (args[3].ToLower() == "rotation") - d.Rotation = int.Parse(args[4]); - else if (args[3].ToLower() == "x") - d.Rotation = int.Parse(args[4]); - else if (args[3].ToLower() == "y") - d.PositionY = int.Parse(args[4]); - }); - break; - case "sleep": - if (args.Count < 2) - return; + public string Name => "obs"; + public bool AcceptCustomPermission { get => true; } - _logger.Debug($"Sending OBS to sleep via chat command [args: {string.Join(" ", args)}]"); - await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary() { { "sleepMillis", int.Parse(args[1]) } })); - break; - case "visibility": - if (args.Count < 4) - return; - - _logger.Debug($"Updating scene item visibility via chat command [args: {string.Join(" ", args)}]"); - await _manager.UpdateSceneItemVisibility(args[1], args[2], args[3].ToLower() == "true"); - break; - default: - break; + public OBSGetSceneItemId( + [FromKeyedServices("obs")] SocketClient obs, + ILogger logger + ) + { + _obs = (obs as OBSSocketClient)!; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + string sceneName = values["sceneName"]; + string sourceName = values["sourceName"]; + _logger.Debug($"Getting scene item id via chat command [scene name: {sceneName}][source name: {sourceName}]"); + await _obs.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sourceName", sourceName } })); + } + } + + private sealed class OBSTransform : IChatPartialCommand + { + private readonly OBSSocketClient _obs; + private readonly ILogger _logger; + + public string Name => "obs"; + public bool AcceptCustomPermission { get => true; } + + public OBSTransform( + [FromKeyedServices("obs")] SocketClient obs, + ILogger logger + ) + { + _obs = (obs as OBSSocketClient)!; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + string sceneName = values["sceneName"]; + string sourceName = values["sourceName"]; + string propertyName = values["propertyName"]; + string value = values["value"]; + _logger.Debug($"Getting scene item transformation data via chat command [scene name: {sceneName}][source name: {sourceName}][property: {propertyName}][value: {value}]"); + await _obs.UpdateTransformation(sceneName, sourceName, (d) => + { + if (propertyName.ToLower() == "rotation" || propertyName.ToLower() == "rotate") + d.Rotation = int.Parse(value); + else if (propertyName.ToLower() == "x") + d.PositionX = int.Parse(value); + else if (propertyName.ToLower() == "y") + d.PositionY = int.Parse(value); + }); + } + } + + private sealed class OBSVisibility : IChatPartialCommand + { + private readonly OBSSocketClient _obs; + private readonly ILogger _logger; + + public string Name => "obs"; + public bool AcceptCustomPermission { get => true; } + + public OBSVisibility( + [FromKeyedServices("obs")] SocketClient obs, + ILogger logger + ) + { + _obs = (obs as OBSSocketClient)!; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + string sceneName = values["sceneName"]; + string sourceName = values["sourceName"]; + string state = values["state"]; + _logger.Debug($"Updating scene item visibility via chat command [scene name: {sceneName}][source name: {sourceName}][state: {state}]"); + string stateLower = state.ToLower(); + bool stateBool = stateLower == "true" || stateLower == "enable" || stateLower == "enabled" || stateLower == "yes"; + await _obs.UpdateSceneItemVisibility(sceneName, sourceName, stateBool); } } } diff --git a/Chat/Commands/Parameters/ChatCommandParameter.cs b/Chat/Commands/Parameters/ChatCommandParameter.cs deleted file mode 100644 index 9f19bbb..0000000 --- a/Chat/Commands/Parameters/ChatCommandParameter.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace TwitchChatTTS.Chat.Commands.Parameters -{ - public abstract class ChatCommandParameter : ICloneable - { - public string Name { get; } - public string Description { get; } - public bool Optional { get; private set; } - - public ChatCommandParameter(string name, string description, bool optional = false) - { - Name = name; - Description = description; - Optional = optional; - } - - public abstract bool Validate(string value); - - public object Clone() { - return (ChatCommandParameter) MemberwiseClone(); - } - - public ChatCommandParameter Permissive() { - Optional = true; - return this; - } - } -} \ No newline at end of file diff --git a/Chat/Commands/Parameters/CommandParameter.cs b/Chat/Commands/Parameters/CommandParameter.cs new file mode 100644 index 0000000..edd4383 --- /dev/null +++ b/Chat/Commands/Parameters/CommandParameter.cs @@ -0,0 +1,20 @@ +namespace TwitchChatTTS.Chat.Commands.Parameters +{ + public abstract class CommandParameter : ICloneable + { + public string Name { get; } + public bool Optional { get; } + + public CommandParameter(string name, bool optional) + { + Name = name; + Optional = optional; + } + + public abstract bool Validate(string value); + + public object Clone() { + return (CommandParameter) MemberwiseClone(); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/Parameters/OBSTransformationParameter.cs b/Chat/Commands/Parameters/OBSTransformationParameter.cs new file mode 100644 index 0000000..92d1f0c --- /dev/null +++ b/Chat/Commands/Parameters/OBSTransformationParameter.cs @@ -0,0 +1,16 @@ +namespace TwitchChatTTS.Chat.Commands.Parameters +{ + public class OBSTransformationParameter : CommandParameter + { + private string[] _values = ["x", "y", "rotation", "rotate", "r"]; + + public OBSTransformationParameter(string name, bool optional = false) : base(name, optional) + { + } + + public override bool Validate(string value) + { + return _values.Contains(value.ToLower()); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/Parameters/SimpleListedParameter.cs b/Chat/Commands/Parameters/SimpleListedParameter.cs deleted file mode 100644 index 838252d..0000000 --- a/Chat/Commands/Parameters/SimpleListedParameter.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace TwitchChatTTS.Chat.Commands.Parameters -{ - public class SimpleListedParameter : ChatCommandParameter - { - private readonly string[] _values; - - public SimpleListedParameter(string[] possibleValues, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional) - { - _values = possibleValues; - } - - public override bool Validate(string value) - { - return _values.Contains(value.ToLower()); - } - } -} \ No newline at end of file diff --git a/Chat/Commands/Parameters/StateParameter.cs b/Chat/Commands/Parameters/StateParameter.cs new file mode 100644 index 0000000..89dfd2d --- /dev/null +++ b/Chat/Commands/Parameters/StateParameter.cs @@ -0,0 +1,16 @@ +namespace TwitchChatTTS.Chat.Commands.Parameters +{ + public class StateParameter : CommandParameter + { + private string[] _values = ["on", "off", "true", "false", "enabled", "disabled", "enable", "disable", "yes", "no"]; + + public StateParameter(string name, bool optional = false) : base(name, optional) + { + } + + public override bool Validate(string value) + { + return _values.Contains(value.ToLower()); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/Parameters/StaticParameter.cs b/Chat/Commands/Parameters/StaticParameter.cs new file mode 100644 index 0000000..1c82468 --- /dev/null +++ b/Chat/Commands/Parameters/StaticParameter.cs @@ -0,0 +1,19 @@ +namespace TwitchChatTTS.Chat.Commands.Parameters +{ + public class StaticParameter : CommandParameter + { + private readonly string _value; + + public string Value { get => _value; } + + public StaticParameter(string name, string value, bool optional = false) : base(name, optional) + { + _value = value.ToLower(); + } + + public override bool Validate(string value) + { + return _value == value.ToLower(); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/Parameters/TTSVoiceNameParameter.cs b/Chat/Commands/Parameters/TTSVoiceNameParameter.cs index 42a7189..9d28bd5 100644 --- a/Chat/Commands/Parameters/TTSVoiceNameParameter.cs +++ b/Chat/Commands/Parameters/TTSVoiceNameParameter.cs @@ -1,11 +1,13 @@ namespace TwitchChatTTS.Chat.Commands.Parameters { - public class TTSVoiceNameParameter : ChatCommandParameter + public class TTSVoiceNameParameter : CommandParameter { + private bool _enabled; private readonly User _user; - public TTSVoiceNameParameter(User user, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional) + public TTSVoiceNameParameter(string name, bool enabled, User user, bool optional = false) : base(name, optional) { + _enabled = enabled; _user = user; } @@ -13,8 +15,11 @@ namespace TwitchChatTTS.Chat.Commands.Parameters { if (_user.VoicesAvailable == null) return false; - + value = value.ToLower(); + if (_enabled) + return _user.VoicesEnabled.Any(v => v.ToLower() == value); + return _user.VoicesAvailable.Any(e => e.Value.ToLower() == value); } } diff --git a/Chat/Commands/Parameters/UnvalidatedParameter.cs b/Chat/Commands/Parameters/UnvalidatedParameter.cs index 7a4d534..4a4acd5 100644 --- a/Chat/Commands/Parameters/UnvalidatedParameter.cs +++ b/Chat/Commands/Parameters/UnvalidatedParameter.cs @@ -1,8 +1,8 @@ namespace TwitchChatTTS.Chat.Commands.Parameters { - public class UnvalidatedParameter : ChatCommandParameter + public class UnvalidatedParameter : CommandParameter { - public UnvalidatedParameter(bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional) + public UnvalidatedParameter(string name, bool optional = false) : base(name, optional) { } diff --git a/Chat/Commands/RefreshCommand.cs b/Chat/Commands/RefreshCommand.cs new file mode 100644 index 0000000..fb99528 --- /dev/null +++ b/Chat/Commands/RefreshCommand.cs @@ -0,0 +1,163 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using TwitchChatTTS.Hermes.Socket; +using TwitchChatTTS.OBS.Socket; +using TwitchLib.Client.Models; +using static TwitchChatTTS.Chat.Commands.TTSCommands; + +namespace TwitchChatTTS.Chat.Commands +{ + public class RefreshCommand : IChatCommand + { + private readonly OBSSocketClient _obs; + private readonly ILogger _logger; + + public string Name => "refresh"; + + public RefreshCommand( + [FromKeyedServices("obs")] SocketClient obs, + ILogger logger + ) + { + _obs = (obs as OBSSocketClient)!; + _logger = logger; + } + + + public void Build(ICommandBuilder builder) + { + builder.CreateCommandTree(Name, b => + { + b.CreateStaticInputParameter("tts_voice_enabled", b => b.CreateCommand(new RefreshTTSVoicesEnabled())) + .CreateStaticInputParameter("word_filters", b => b.CreateCommand(new RefreshTTSWordFilters())) + .CreateStaticInputParameter("selected_voices", b => b.CreateCommand(new RefreshTTSChatterVoices())) + .CreateStaticInputParameter("default_voice", b => b.CreateCommand(new RefreshTTSDefaultVoice())) + .CreateStaticInputParameter("redemptions", b => b.CreateCommand(new RefreshRedemptions())) + .CreateStaticInputParameter("obs_cache", b => b.CreateCommand(new RefreshObs(_obs, _logger))) + .CreateStaticInputParameter("permissions", b => b.CreateCommand(new RefreshPermissions())); + }); + } + + private sealed class RefreshTTSVoicesEnabled : IChatPartialCommand + { + public bool AcceptCustomPermission { get => true; } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + await client.FetchEnabledTTSVoices(); + } + } + + private sealed class RefreshTTSWordFilters : IChatPartialCommand + { + public bool AcceptCustomPermission { get => true; } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + await client.FetchTTSWordFilters(); + } + } + + private sealed class RefreshTTSChatterVoices : IChatPartialCommand + { + public bool AcceptCustomPermission { get => true; } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + await client.FetchTTSChatterVoices(); + } + } + + private sealed class RefreshTTSDefaultVoice : IChatPartialCommand + { + public bool AcceptCustomPermission { get => true; } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + await client.FetchDefaultTTSVoice(); + } + } + + private sealed class RefreshRedemptions : IChatPartialCommand + { + public bool AcceptCustomPermission { get => true; } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + await client.FetchRedemptions(); + } + } + + private sealed class RefreshObs : IChatPartialCommand + { + private readonly OBSSocketClient _obsManager; + private readonly ILogger _logger; + + public bool AcceptCustomPermission { get => true; } + + public RefreshObs(OBSSocketClient obsManager, ILogger logger) { + _obsManager = obsManager; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + _obsManager.ClearCache(); + _logger.Information("Cleared the cache used for OBS."); + } + } + + private sealed class RefreshPermissions : IChatPartialCommand + { + + public bool AcceptCustomPermission { get => true; } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + await client.FetchPermissions(); + } + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + } +} \ No newline at end of file diff --git a/Chat/Commands/RefreshTTSDataCommand.cs b/Chat/Commands/RefreshTTSDataCommand.cs deleted file mode 100644 index 333782f..0000000 --- a/Chat/Commands/RefreshTTSDataCommand.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using TwitchChatTTS.Chat.Commands.Parameters; -using TwitchChatTTS.Chat.Groups; -using TwitchChatTTS.Chat.Groups.Permissions; -using TwitchChatTTS.Hermes.Socket; -using TwitchChatTTS.OBS.Socket.Manager; -using TwitchChatTTS.Twitch.Redemptions; -using TwitchLib.Client.Models; - -namespace TwitchChatTTS.Chat.Commands -{ - public class RefreshTTSDataCommand : ChatCommand - { - private readonly User _user; - private readonly RedemptionManager _redemptionManager; - private readonly IGroupPermissionManager _permissionManager; - private readonly IChatterGroupManager _chatterGroupManager; - private readonly OBSManager _obsManager; - private readonly HermesApiClient _hermesApi; - private readonly ILogger _logger; - - public RefreshTTSDataCommand( - User user, - RedemptionManager redemptionManager, - IGroupPermissionManager permissionManager, - IChatterGroupManager chatterGroupManager, - OBSManager obsManager, - HermesApiClient hermesApi, - ILogger logger - ) : base("refresh", "Refreshes certain TTS related data on the client.") - { - _user = user; - _redemptionManager = redemptionManager; - _permissionManager = permissionManager; - _chatterGroupManager = chatterGroupManager; - _obsManager = obsManager; - _hermesApi = hermesApi; - _logger = logger; - - AddParameter(new SimpleListedParameter([ - "tts_voice_enabled", - "word_filters", - "selected_voices", - "default_voice", - "redemptions", - "obs_cache", - "permissions" - ])); - } - - public override async Task CheckDefaultPermissions(ChatMessage message) - { - return message.IsModerator || message.IsBroadcaster; - } - - public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) - { - var value = args.First().ToLower(); - - switch (value) - { - case "tts_voice_enabled": - var voicesEnabled = await _hermesApi.FetchTTSEnabledVoices(); - if (voicesEnabled == null || !voicesEnabled.Any()) - _user.VoicesEnabled = new HashSet(["Brian"]); - else - _user.VoicesEnabled = new HashSet(voicesEnabled.Select(v => v)); - _logger.Information($"{_user.VoicesEnabled.Count} TTS voices have been enabled."); - break; - case "word_filters": - var wordFilters = await _hermesApi.FetchTTSWordFilters(); - _user.RegexFilters = wordFilters.ToList(); - _logger.Information($"{_user.RegexFilters.Count()} TTS word filters."); - break; - case "selected_voices": - { - var voicesSelected = await _hermesApi.FetchTTSChatterSelectedVoices(); - _user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice); - _logger.Information($"{_user.VoicesSelected.Count} TTS voices have been selected for specific chatters."); - break; - } - case "default_voice": - _user.DefaultTTSVoice = await _hermesApi.FetchTTSDefaultVoice(); - _logger.Information("TTS Default Voice: " + _user.DefaultTTSVoice); - break; - case "redemptions": - var redemptionActions = await _hermesApi.FetchRedeemableActions(); - var redemptions = await _hermesApi.FetchRedemptions(); - _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a)); - _logger.Information($"Redemption Manager has been refreshed with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); - break; - case "obs_cache": - { - _obsManager.ClearCache(); - await _obsManager.GetGroupList(async groups => await _obsManager.GetGroupSceneItemList(groups)); - break; - } - case "permissions": - { - _chatterGroupManager.Clear(); - _permissionManager.Clear(); - - var groups = await _hermesApi.FetchGroups(); - var groupsById = groups.ToDictionary(g => g.Id, g => g); - foreach (var group in groups) - _chatterGroupManager.Add(group); - _logger.Information($"{groups.Count()} groups have been loaded."); - - var groupChatters = await _hermesApi.FetchGroupChatters(); - _logger.Debug($"{groupChatters.Count()} group users have been fetched."); - - var permissions = await _hermesApi.FetchGroupPermissions(); - foreach (var permission in permissions) - { - _logger.Debug($"Adding group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]"); - if (groupsById.TryGetValue(permission.GroupId, out var group)) - { - _logger.Warning($"Failed to find group by id [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); - continue; - } - - var path = $"{group.Name}.{permission.Path}"; - _permissionManager.Set(path, permission.Allow); - _logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); - } - _logger.Information($"{permissions.Count()} group permissions have been loaded."); - - foreach (var chatter in groupChatters) - if (groupsById.TryGetValue(chatter.GroupId, out var group)) - _chatterGroupManager.Add(chatter.ChatterId, group.Name); - _logger.Information($"Users in each group have been loaded."); - break; - } - default: - _logger.Warning($"Unknown refresh value given [value: {value}]"); - break; - } - } - } -} \ No newline at end of file diff --git a/Chat/Commands/RemoveTTSVoiceCommand.cs b/Chat/Commands/RemoveTTSVoiceCommand.cs deleted file mode 100644 index 33a4023..0000000 --- a/Chat/Commands/RemoveTTSVoiceCommand.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using TwitchChatTTS.Chat.Commands.Parameters; -using TwitchChatTTS.Hermes.Socket; -using TwitchLib.Client.Models; - -namespace TwitchChatTTS.Chat.Commands -{ - public class RemoveTTSVoiceCommand : ChatCommand - { - private readonly User _user; - private ILogger _logger; - - public new bool DefaultPermissionsOverwrite { get => true; } - - public RemoveTTSVoiceCommand( - [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, - User user, - ILogger logger - ) : base("removettsvoice", "Select a TTS voice as the default for that user.") - { - _user = user; - _logger = logger; - - AddParameter(ttsVoiceParameter); - } - - public override async Task CheckDefaultPermissions(ChatMessage message) - { - return false; - } - - public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) - { - if (_user == null || _user.VoicesAvailable == null) - { - _logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]"); - return; - } - - var voiceName = args.First().ToLower(); - var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName); - if (!exists) - { - _logger.Debug($"Voice does not exist [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]"); - return; - } - - var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; - await client.DeleteTTSVoice(voiceId); - _logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]"); - } - } -} \ No newline at end of file diff --git a/Chat/Commands/SkipAllCommand.cs b/Chat/Commands/SkipAllCommand.cs deleted file mode 100644 index bafb314..0000000 --- a/Chat/Commands/SkipAllCommand.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Serilog; -using TwitchChatTTS.Hermes.Socket; -using TwitchLib.Client.Models; - -namespace TwitchChatTTS.Chat.Commands -{ - public class SkipAllCommand : ChatCommand - { - private readonly TTSPlayer _ttsPlayer; - private readonly ILogger _logger; - - public SkipAllCommand(TTSPlayer ttsPlayer, ILogger logger) - : base("skipall", "Skips all text to speech messages in queue and playing.") - { - _ttsPlayer = ttsPlayer; - _logger = logger; - } - - public override async Task CheckDefaultPermissions(ChatMessage message) - { - return message.IsModerator || message.IsVip || message.IsBroadcaster; - } - - public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) - { - _ttsPlayer.RemoveAll(); - - if (_ttsPlayer.Playing == null) - return; - - AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing); - _ttsPlayer.Playing = null; - - _logger.Information("Skipped all queued and playing tts."); - } - } -} \ No newline at end of file diff --git a/Chat/Commands/SkipCommand.cs b/Chat/Commands/SkipCommand.cs index 543f149..8fff41b 100644 --- a/Chat/Commands/SkipCommand.cs +++ b/Chat/Commands/SkipCommand.cs @@ -1,35 +1,98 @@ using Serilog; using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; +using static TwitchChatTTS.Chat.Commands.TTSCommands; namespace TwitchChatTTS.Chat.Commands { - public class SkipCommand : ChatCommand + public class SkipCommand : IChatCommand { - private readonly TTSPlayer _ttsPlayer; + private readonly TTSPlayer _player; private readonly ILogger _logger; public SkipCommand(TTSPlayer ttsPlayer, ILogger logger) - : base("skip", "Skips the current text to speech message.") { - _ttsPlayer = ttsPlayer; + _player = ttsPlayer; _logger = logger; } - public override async Task CheckDefaultPermissions(ChatMessage message) + public string Name => "skip"; + + public void Build(ICommandBuilder builder) { - return message.IsModerator || message.IsVip || message.IsBroadcaster; + builder.CreateCommandTree(Name, b => + { + b.CreateStaticInputParameter("all", b => + { + b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _logger)); + }).CreateCommand(new TTSPlayerSkipCommand(_player, _logger)); + }); + builder.CreateCommandTree("skipall", b => { + b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _logger)); + }); } - public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) + + private sealed class TTSPlayerSkipCommand : IChatPartialCommand { - if (_ttsPlayer.Playing == null) - return; + private readonly TTSPlayer _ttsPlayer; + private readonly ILogger _logger; - AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing); - _ttsPlayer.Playing = null; + public bool AcceptCustomPermission { get => true; } - _logger.Information("Skipped current tts."); + public TTSPlayerSkipCommand(TTSPlayer ttsPlayer, ILogger logger) + { + _ttsPlayer = ttsPlayer; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsVip || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + if (_ttsPlayer.Playing == null) + return; + + AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing); + _ttsPlayer.Playing = null; + + _logger.Information("Skipped current tts."); + } + } + + private sealed class TTSPlayerSkipAllCommand : IChatPartialCommand + { + private readonly TTSPlayer _ttsPlayer; + private readonly ILogger _logger; + + public bool AcceptCustomPermission { get => true; } + + public TTSPlayerSkipAllCommand(TTSPlayer ttsPlayer, ILogger logger) + { + _ttsPlayer = ttsPlayer; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsVip || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + _ttsPlayer.RemoveAll(); + + if (_ttsPlayer.Playing == null) + return; + + AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing); + _ttsPlayer.Playing = null; + + _logger.Information("Skipped all queued and playing tts."); + } } } } \ No newline at end of file diff --git a/Chat/Commands/TTSCommand.cs b/Chat/Commands/TTSCommand.cs index b6fd6b5..b9214cd 100644 --- a/Chat/Commands/TTSCommand.cs +++ b/Chat/Commands/TTSCommand.cs @@ -1,46 +1,182 @@ -using Microsoft.Extensions.DependencyInjection; using Serilog; -using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; +using static TwitchChatTTS.Chat.Commands.TTSCommands; namespace TwitchChatTTS.Chat.Commands { - public class TTSCommand : ChatCommand + public class TTSCommand : IChatCommand { private readonly User _user; private readonly ILogger _logger; - public TTSCommand( - [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, - User user, - ILogger logger - ) : base("tts", "Various tts commands.") + + public TTSCommand(User user, ILogger logger) { _user = user; _logger = logger; - - AddParameter(ttsVoiceParameter); - AddParameter(new SimpleListedParameter(["enable", "disable"])); } - public override async Task CheckDefaultPermissions(ChatMessage message) + public string Name => "tts"; + + public void Build(ICommandBuilder builder) { - return message.IsModerator || message.IsBroadcaster; + builder.CreateCommandTree(Name, b => + { + b.CreateStaticInputParameter("add", b => + { + b.CreateVoiceNameParameter("voiceName", false) + .CreateCommand(new AddTTSVoiceCommand(_user, _logger)); + }) + .CreateStaticInputParameter("del", b => + { + b.CreateVoiceNameParameter("voiceName", true) + .CreateCommand(new DeleteTTSVoiceCommand(_user, _logger)); + }) + .CreateStaticInputParameter("delete", b => + { + b.CreateVoiceNameParameter("voiceName", true) + .CreateCommand(new DeleteTTSVoiceCommand(_user, _logger)); + }) + .CreateStaticInputParameter("remove", b => + { + b.CreateVoiceNameParameter("voiceName", true) + .CreateCommand(new DeleteTTSVoiceCommand(_user, _logger)); + }) + .CreateStaticInputParameter("enable", b => + { + b.CreateVoiceNameParameter("voiceName", false) + .CreateCommand(new SetTTSVoiceStateCommand(true, _user, _logger)); + }) + .CreateStaticInputParameter("on", b => + { + b.CreateVoiceNameParameter("voiceName", false) + .CreateCommand(new SetTTSVoiceStateCommand(true, _user, _logger)); + }) + .CreateStaticInputParameter("disable", b => + { + b.CreateVoiceNameParameter("voiceName", true) + .CreateCommand(new SetTTSVoiceStateCommand(false, _user, _logger)); + }) + .CreateStaticInputParameter("off", b => + { + b.CreateVoiceNameParameter("voiceName", true) + .CreateCommand(new SetTTSVoiceStateCommand(false, _user, _logger)); + }); + }); } - public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) + private sealed class AddTTSVoiceCommand : IChatPartialCommand { - if (_user == null || _user.VoicesAvailable == null) - return; + private readonly User _user; + private readonly ILogger _logger; - var voiceName = args[0].ToLower(); - var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; - var action = args[1].ToLower(); + public bool AcceptCustomPermission { get => false; } - bool state = action == "enable"; - await client.UpdateTTSVoiceState(voiceId, state); - _logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {state}][invoker: {message.Username}][id: {message.UserId}]"); + + public AddTTSVoiceCommand(User user, ILogger logger) + { + _user = user; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return false; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + if (_user == null || _user.VoicesAvailable == null) + return; + + var voiceName = values["voiceName"]; + var voiceNameLower = voiceName.ToLower(); + var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower); + if (exists) + { + _logger.Warning($"Voice already exists [voice: {voiceName}][id: {message.UserId}]"); + return; + } + + await client.CreateTTSVoice(voiceName); + _logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]"); + } + } + + private sealed class DeleteTTSVoiceCommand : IChatPartialCommand + { + private readonly User _user; + private ILogger _logger; + + public bool AcceptCustomPermission { get => false; } + + public DeleteTTSVoiceCommand(User user, ILogger logger) + { + _user = user; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return false; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + if (_user == null || _user.VoicesAvailable == null) + { + _logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]"); + return; + } + + var voiceName = values["voiceName"]; + var voiceNameLower = voiceName.ToLower(); + var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower); + if (!exists) + { + _logger.Debug($"Voice does not exist [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]"); + return; + } + + var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; + await client.DeleteTTSVoice(voiceId); + _logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]"); + } + } + + private sealed class SetTTSVoiceStateCommand : IChatPartialCommand + { + private bool _state; + private readonly User _user; + private ILogger _logger; + + public bool AcceptCustomPermission { get => true; } + + public SetTTSVoiceStateCommand(bool state, User user, ILogger logger) + { + _state = state; + _user = user; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsModerator || message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + if (_user == null || _user.VoicesAvailable == null) + return; + + var voiceName = values["voiceName"]; + var voiceNameLower = voiceName.ToLower(); + var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceNameLower).Key; + + await client.UpdateTTSVoiceState(voiceId, _state); + _logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {_state}][invoker: {message.Username}][id: {message.UserId}]"); + } } } } \ No newline at end of file diff --git a/Chat/Commands/VersionCommand.cs b/Chat/Commands/VersionCommand.cs index 8893c24..f95f5aa 100644 --- a/Chat/Commands/VersionCommand.cs +++ b/Chat/Commands/VersionCommand.cs @@ -2,31 +2,52 @@ using HermesSocketLibrary.Socket.Data; using Serilog; using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; +using static TwitchChatTTS.Chat.Commands.TTSCommands; namespace TwitchChatTTS.Chat.Commands { - public class VersionCommand : ChatCommand + public class VersionCommand : IChatCommand { private readonly User _user; private ILogger _logger; + public string Name => "version"; + public VersionCommand(User user, ILogger logger) - : base("version", "Does nothing.") { _user = user; _logger = logger; } - public override async Task CheckDefaultPermissions(ChatMessage message) + public void Build(ICommandBuilder builder) { - return message.IsBroadcaster; + builder.CreateCommandTree(Name, b => b.CreateCommand(new AppVersionCommand(_user, _logger))); } - public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) + private sealed class AppVersionCommand : IChatPartialCommand { - _logger.Information($"Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}"); + private readonly User _user; + private ILogger _logger; - await client.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}."); + public bool AcceptCustomPermission { get => true; } + + public AppVersionCommand(User user, ILogger logger) + { + _user = user; + _logger = logger; + } + + public bool CheckDefaultPermissions(ChatMessage message) + { + return message.IsBroadcaster; + } + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) + { + _logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}"); + + await client.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}."); + } } } } \ No newline at end of file diff --git a/Chat/Commands/VoiceCommand.cs b/Chat/Commands/VoiceCommand.cs index 5160e72..e40b12e 100644 --- a/Chat/Commands/VoiceCommand.cs +++ b/Chat/Commands/VoiceCommand.cs @@ -1,58 +1,71 @@ -using Microsoft.Extensions.DependencyInjection; using Serilog; -using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; +using static TwitchChatTTS.Chat.Commands.TTSCommands; namespace TwitchChatTTS.Chat.Commands { - public class VoiceCommand : ChatCommand + public class VoiceCommand : IChatCommand { private readonly User _user; private readonly ILogger _logger; - public VoiceCommand( - [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, - User user, - ILogger logger - ) : base("voice", "Select a TTS voice as the default for that user.") + public VoiceCommand(User user, ILogger logger) { _user = user; _logger = logger; - - AddParameter(ttsVoiceParameter); } - public override async Task CheckDefaultPermissions(ChatMessage message) + public string Name => "voice"; + + public void Build(ICommandBuilder builder) { - return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100; + builder.CreateCommandTree(Name, b => + { + b.CreateVoiceNameParameter("voiceName", true) + .CreateCommand(new TTSVoiceSelector(_user, _logger)); + }); } - public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) + private sealed class TTSVoiceSelector : IChatPartialCommand { - if (_user == null || _user.VoicesSelected == null || _user.VoicesEnabled == null) - return; + private readonly User _user; + private readonly ILogger _logger; - long chatterId = long.Parse(message.UserId); - var voiceName = args.First().ToLower(); - var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName); - var enabled = _user.VoicesEnabled.Contains(voice.Value); + public bool AcceptCustomPermission { get => true; } - if (!enabled) + public TTSVoiceSelector(User user, ILogger logger) { - _logger.Information($"Voice is disabled. Cannot switch to that voice [voice: {voice.Value}][username: {message.Username}]"); - return; + _user = user; + _logger = logger; } - if (_user.VoicesSelected.ContainsKey(chatterId)) + + public bool CheckDefaultPermissions(ChatMessage message) { - await client.UpdateTTSUser(chatterId, voice.Key); - _logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]"); + return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100; } - else + + public async Task Execute(IDictionary values, ChatMessage message, HermesSocketClient client) { - await client.CreateTTSUser(chatterId, voice.Key); - _logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]"); + if (_user == null || _user.VoicesSelected == null) + return; + + long chatterId = long.Parse(message.UserId); + var voiceName = values["voiceName"]; + var voiceNameLower = voiceName.ToLower(); + var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceNameLower); + + if (_user.VoicesSelected.ContainsKey(chatterId)) + { + await client.UpdateTTSUser(chatterId, voice.Key); + _logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]"); + } + else + { + await client.CreateTTSUser(chatterId, voice.Key); + _logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]"); + } } } } diff --git a/Chat/Groups/ChatterGroupManager.cs b/Chat/Groups/ChatterGroupManager.cs index 240ecc8..6826211 100644 --- a/Chat/Groups/ChatterGroupManager.cs +++ b/Chat/Groups/ChatterGroupManager.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; +using HermesSocketLibrary.Requests.Messages; using Serilog; namespace TwitchChatTTS.Chat.Groups @@ -59,7 +60,10 @@ namespace TwitchChatTTS.Chat.Groups } public int GetPriorityFor(IEnumerable groupNames) { - return groupNames.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null).Max(g => g.Priority); + var values = groupNames.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null); + if (values.Any()) + return values.Max(g => g.Priority); + return 0; } public bool Remove(long chatterId, string groupId) { diff --git a/Chat/Groups/Group.cs b/Chat/Groups/Group.cs deleted file mode 100644 index cc3d356..0000000 --- a/Chat/Groups/Group.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace TwitchChatTTS.Chat.Groups -{ - public class Group - { - public string Id { get; set; } - public string Name { get; set; } - public int Priority { get; set; } - } -} \ No newline at end of file diff --git a/Chat/Groups/GroupChatter.cs b/Chat/Groups/GroupChatter.cs deleted file mode 100644 index 446291f..0000000 --- a/Chat/Groups/GroupChatter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TwitchChatTTS.Chat.Groups -{ - public class GroupChatter - { - public string GroupId { get; set; } - public long ChatterId { get; set;} - } -} \ No newline at end of file diff --git a/Chat/Groups/IChatterGroupManager.cs b/Chat/Groups/IChatterGroupManager.cs index 860620a..3388b81 100644 --- a/Chat/Groups/IChatterGroupManager.cs +++ b/Chat/Groups/IChatterGroupManager.cs @@ -1,3 +1,5 @@ +using HermesSocketLibrary.Requests.Messages; + namespace TwitchChatTTS.Chat.Groups { public interface IChatterGroupManager diff --git a/Chat/Groups/Permissions/GroupPermission.cs b/Chat/Groups/Permissions/GroupPermission.cs deleted file mode 100644 index 0356c78..0000000 --- a/Chat/Groups/Permissions/GroupPermission.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace TwitchChatTTS.Chat.Groups.Permissions -{ - public class GroupPermission - { - public string Id { get; set; } - public string GroupId { get; set; } - public string Path { get; set; } - public bool? Allow { get; set; } - } -} \ No newline at end of file diff --git a/Chat/Groups/Permissions/GroupPermissionManager.cs b/Chat/Groups/Permissions/GroupPermissionManager.cs index 9207128..e70ff06 100644 --- a/Chat/Groups/Permissions/GroupPermissionManager.cs +++ b/Chat/Groups/Permissions/GroupPermissionManager.cs @@ -37,8 +37,7 @@ namespace TwitchChatTTS.Chat.Groups.Permissions public void Clear() { - if (_root.Children != null) - _root.Children.Clear(); + _root.Clear(); } public bool Remove(string path) @@ -127,6 +126,11 @@ namespace TwitchChatTTS.Chat.Groups.Permissions _children.Add(child); } + internal void Clear() { + if (_children != null) + _children.Clear(); + } + public void Remove(string name) { if (_children == null || !_children.Any()) diff --git a/Helpers/WebClientWrap.cs b/Helpers/WebClientWrap.cs index 5df0294..9951943 100644 --- a/Helpers/WebClientWrap.cs +++ b/Helpers/WebClientWrap.cs @@ -23,7 +23,7 @@ namespace TwitchChatTTS.Helpers _client.DefaultRequestHeaders.Add(key, value); } - public async Task GetJson(string uri, JsonSerializerOptions options = null) + public async Task GetJson(string uri, JsonSerializerOptions? options = null) { var response = await _client.GetAsync(uri); return JsonSerializer.Deserialize(await response.Content.ReadAsStreamAsync(), options ?? _options); diff --git a/Hermes/Socket/Handlers/LoginAckHandler.cs b/Hermes/Socket/Handlers/LoginAckHandler.cs index c983b07..69601bc 100644 --- a/Hermes/Socket/Handlers/LoginAckHandler.cs +++ b/Hermes/Socket/Handlers/LoginAckHandler.cs @@ -40,39 +40,15 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers client.LoggedIn = true; _logger.Information($"Logged in as {_user.TwitchUsername} {(message.WebLogin ? "via web" : "via TTS app")}."); - await client.Send(3, new RequestMessage() - { - Type = "get_tts_voices", - Data = null - }); - - await client.Send(3, new RequestMessage() - { - Type = "get_tts_users", - Data = new Dictionary() { { "user", _user.HermesUserId } } - }); - - await client.Send(3, new RequestMessage() - { - Type = "get_default_tts_voice", - Data = null - }); - - await client.Send(3, new RequestMessage() - { - Type = "get_chatter_ids", - Data = null - }); - - await client.Send(3, new RequestMessage() - { - Type = "get_emotes", - Data = null - }); - - await client.GetRedemptions(); - - await Task.Delay(TimeSpan.FromSeconds(3)); + await client.FetchTTSVoices(); + await client.FetchEnabledTTSVoices(); + await client.FetchTTSWordFilters(); + await client.FetchTTSChatterVoices(); + await client.FetchDefaultTTSVoice(); + await client.FetchChatterIdentifiers(); + await client.FetchEmotes(); + await client.FetchRedemptions(); + await client.FetchPermissions(); _logger.Information("TTS is now ready."); client.Ready = true; diff --git a/Hermes/Socket/Handlers/RequestAckHandler.cs b/Hermes/Socket/Handlers/RequestAckHandler.cs index 89e9d8b..35c84ee 100644 --- a/Hermes/Socket/Handlers/RequestAckHandler.cs +++ b/Hermes/Socket/Handlers/RequestAckHandler.cs @@ -8,6 +8,8 @@ using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchChatTTS.Chat.Emotes; +using TwitchChatTTS.Chat.Groups; +using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Twitch.Redemptions; namespace TwitchChatTTS.Hermes.Socket.Handlers @@ -28,7 +30,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers public RequestAckHandler( User user, - //RedemptionManager redemptionManager, ICallbackManager callbackManager, IServiceProvider serviceProvider, JsonSerializerOptions options, @@ -36,7 +37,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers ) { _user = user; - //_redemptionManager = redemptionManager; _callbackManager = callbackManager; _serviceProvider = serviceProvider; _options = options; @@ -66,7 +66,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers _logger.Debug($"Received a Hermes request message [type: {message.Request.Type}][data: {string.Join(',', message.Request.Data?.Select(entry => entry.Key + '=' + entry.Value) ?? Array.Empty())}]"); if (message.Request.Type == "get_tts_voices") { - _logger.Verbose("Updating all available voices for TTS."); var voices = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (voices == null) return; @@ -79,7 +78,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "create_tts_user") { - _logger.Verbose("Adding new tts voice for user."); if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId)) { _logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]"); @@ -93,7 +91,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "update_tts_user") { - _logger.Verbose("Updating user's voice"); if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId)) { _logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]"); @@ -107,7 +104,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "create_tts_voice") { - _logger.Verbose("Creating new tts voice."); string? voice = message.Request.Data["voice"].ToString(); string? voiceId = message.Data.ToString(); if (voice == null || voiceId == null) @@ -123,7 +119,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "delete_tts_voice") { - _logger.Verbose("Deleting tts voice."); var voice = message.Request.Data["voice"].ToString(); if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null) return; @@ -138,7 +133,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "update_tts_voice") { - _logger.Verbose("Updating TTS voice."); string voiceId = message.Request.Data["idd"].ToString(); string voice = message.Request.Data["voice"].ToString(); @@ -150,7 +144,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "get_tts_users") { - _logger.Verbose("Updating all chatters' selected voice."); var users = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (users == null) return; @@ -163,7 +156,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "get_chatter_ids") { - _logger.Verbose("Fetching all chatters' id."); var chatters = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (chatters == null) return; @@ -174,7 +166,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "get_emotes") { - _logger.Verbose("Updating emotes."); var emotes = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (emotes == null) return; @@ -196,9 +187,78 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers if (duplicateNames > 0) _logger.Warning($"Found {duplicateNames} emotes with duplicate names."); } + else if (message.Request.Type == "get_enabled_tts_voices") + { + var enabledTTSVoices = JsonSerializer.Deserialize>(message.Data.ToString(), _options); + if (enabledTTSVoices == null) + { + _logger.Error("Failed to load enabled tts voices."); + return; + } + + if (_user.VoicesEnabled == null) + _user.VoicesEnabled = enabledTTSVoices.ToHashSet(); + else + _user.VoicesEnabled.Clear(); + foreach (var voice in enabledTTSVoices) + _user.VoicesEnabled.Add(voice); + _logger.Information($"TTS voices [count: {_user.VoicesEnabled.Count}] have been enabled."); + } + else if (message.Request.Type == "get_permissions") + { + var groupInfo = JsonSerializer.Deserialize(message.Data.ToString(), _options); + if (groupInfo == null) + { + _logger.Error("Failed to load groups & permissions."); + return; + } + + var chatterGroupManager = _serviceProvider.GetRequiredService(); + var permissionManager = _serviceProvider.GetRequiredService(); + + permissionManager.Clear(); + chatterGroupManager.Clear(); + + var groupsById = groupInfo.Groups.ToDictionary(g => g.Id, g => g); + foreach (var group in groupInfo.Groups) + chatterGroupManager.Add(group); + + foreach (var permission in groupInfo.GroupPermissions) + { + _logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]"); + if (!groupsById.TryGetValue(permission.GroupId, out var group)) + { + _logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); + continue; + } + + + var path = $"{group.Name}.{permission.Path}"; + permissionManager.Set(path, permission.Allow); + _logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); + } + + _logger.Information($"Groups [count: {groupInfo.Groups.Count()}] & Permissions [count: {groupInfo.GroupPermissions.Count()}] have been loaded."); + + foreach (var chatter in groupInfo.GroupChatters) + if (groupsById.TryGetValue(chatter.GroupId, out var group)) + chatterGroupManager.Add(chatter.ChatterId, group.Name); + _logger.Information($"Users in each group [count: {groupInfo.GroupChatters.Count()}] have been loaded."); + } + else if (message.Request.Type == "get_tts_word_filters") + { + var wordFilters = JsonSerializer.Deserialize>(message.Data.ToString(), _options); + if (wordFilters == null) + { + _logger.Error("Failed to load word filters."); + return; + } + + _user.RegexFilters = wordFilters.ToList(); + _logger.Information($"TTS word filters [count: {_user.RegexFilters.Count}] have been refreshed."); + } else if (message.Request.Type == "update_tts_voice_state") { - _logger.Verbose("Updating TTS voice states."); string voiceId = message.Request.Data["voice"].ToString(); bool state = message.Request.Data["state"].ToString().ToLower() == "true"; @@ -216,7 +276,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "get_redemptions") { - _logger.Verbose("Fetching all the redemptions."); IEnumerable? redemptions = JsonSerializer.Deserialize>(message.Data!.ToString()!, _options); if (redemptions != null) { @@ -229,7 +288,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } else if (message.Request.Type == "get_redeemable_actions") { - _logger.Verbose("Fetching all the redeemable actions."); IEnumerable? actions = JsonSerializer.Deserialize>(message.Data!.ToString()!, _options); if (actions == null) { diff --git a/Hermes/Socket/HermesSocketClient.cs b/Hermes/Socket/HermesSocketClient.cs index 7d2da26..199b8dc 100644 --- a/Hermes/Socket/HermesSocketClient.cs +++ b/Hermes/Socket/HermesSocketClient.cs @@ -36,14 +36,14 @@ namespace TwitchChatTTS.Hermes.Socket User user, Configuration configuration, ICallbackManager callbackManager, - [FromKeyedServices("hermes")] HandlerManager handlerManager, - [FromKeyedServices("hermes")] HandlerTypeManager typeManager, + [FromKeyedServices("hermes")] IEnumerable handlers, + [FromKeyedServices("hermes")] MessageTypeManager typeManager, ILogger logger - ) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() + ) : base(handlers, typeManager, new JsonSerializerOptions() { PropertyNameCaseInsensitive = false, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower - }) + }, logger) { _user = user; _configuration = configuration; @@ -74,7 +74,7 @@ namespace TwitchChatTTS.Hermes.Socket if (!Connected) return; - await DisconnectAsync(); + await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed")); } public async Task CreateTTSVoice(string voiceName) @@ -104,11 +104,67 @@ namespace TwitchChatTTS.Hermes.Socket }); } - public async Task GetRedemptions() + public async Task FetchChatterIdentifiers() { + await Send(3, new RequestMessage() + { + Type = "get_chatter_ids", + Data = null + }); + } + + public async Task FetchDefaultTTSVoice() { + await Send(3, new RequestMessage() + { + Type = "get_default_tts_voice", + Data = null + }); + } + + public async Task FetchEmotes() { + await Send(3, new RequestMessage() + { + Type = "get_emotes", + Data = null + }); + } + + public async Task FetchEnabledTTSVoices() { + await Send(3, new RequestMessage() + { + Type = "get_enabled_tts_voices", + Data = null + }); + } + + public async Task FetchTTSVoices() { + await Send(3, new RequestMessage() + { + Type = "get_tts_voices", + Data = null + }); + } + + public async Task FetchTTSChatterVoices() { + await Send(3, new RequestMessage() + { + Type = "get_tts_users", + Data = null + }); + } + + public async Task FetchTTSWordFilters() { + await Send(3, new RequestMessage() + { + Type = "get_tts_word_filters", + Data = null + }); + } + + public async Task FetchRedemptions() { var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData() { - Callback = async (d) => await GetRedeemableActions(d["redemptions"] as IEnumerable), + Callback = async (d) => await FetchRedeemableActions(d["redemptions"] as IEnumerable), Data = new Dictionary() }); @@ -120,7 +176,7 @@ namespace TwitchChatTTS.Hermes.Socket }); } - public async Task GetRedeemableActions(IEnumerable redemptions) + private async Task FetchRedeemableActions(IEnumerable redemptions) { var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData() { @@ -135,6 +191,15 @@ namespace TwitchChatTTS.Hermes.Socket }); } + public async Task FetchPermissions() + { + await Send(3, new RequestMessage() + { + Type = "get_permissions", + Data = null + }); + } + public void Initialize() { _logger.Information("Initializing Hermes websocket client."); diff --git a/Hermes/Socket/Managers/HermesHandlerManager.cs b/Hermes/Socket/Managers/HermesHandlerManager.cs deleted file mode 100644 index a602c31..0000000 --- a/Hermes/Socket/Managers/HermesHandlerManager.cs +++ /dev/null @@ -1,41 +0,0 @@ -using CommonSocketLibrary.Common; -using CommonSocketLibrary.Socket.Manager; -using Microsoft.Extensions.DependencyInjection; -using Serilog; - -namespace TwitchChatTTS.Hermes.Socket.Managers -{ - public class HermesHandlerManager : WebSocketHandlerManager - { - public HermesHandlerManager(ILogger logger, IServiceProvider provider) : base(logger) - { - try - { - var basetype = typeof(IWebSocketHandler); - var assembly = GetType().Assembly; - var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Hermes.") == true); - - foreach (var type in types) - { - var key = "hermes-" + type.Name.Replace("Handlers", "Hand#lers") - .Replace("Handler", "") - .Replace("Hand#lers", "Handlers") - .ToLower(); - var handler = provider.GetKeyedService(key); - if (handler == null) - { - logger.Error("Failed to find hermes websocket handler: " + type.AssemblyQualifiedName); - continue; - } - - _logger.Debug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers."); - Add(handler); - } - } - catch (Exception e) - { - _logger.Error(e, "Failed to load hermes websocket handler types."); - } - } - } -} \ No newline at end of file diff --git a/Hermes/Socket/Managers/HermesHandlerTypeManager.cs b/Hermes/Socket/Managers/HermesHandlerTypeManager.cs index f57174b..2a5f2c1 100644 --- a/Hermes/Socket/Managers/HermesHandlerTypeManager.cs +++ b/Hermes/Socket/Managers/HermesHandlerTypeManager.cs @@ -7,12 +7,12 @@ using Serilog; namespace TwitchChatTTS.Hermes.Socket.Managers { - public class HermesHandlerTypeManager : WebSocketHandlerTypeManager + public class HermesMessageTypeManager : WebSocketMessageTypeManager { - public HermesHandlerTypeManager( - ILogger factory, - [FromKeyedServices("hermes")] HandlerManager handlers - ) : base(factory, handlers) + public HermesMessageTypeManager( + [FromKeyedServices("hermes")] IEnumerable handlers, + ILogger logger + ) : base(handlers, logger) { } diff --git a/OBS/Socket/Data/IdentifyMessage.cs b/OBS/Socket/Data/IdentifyMessage.cs index d1c6c34..27469b8 100644 --- a/OBS/Socket/Data/IdentifyMessage.cs +++ b/OBS/Socket/Data/IdentifyMessage.cs @@ -1,15 +1,19 @@ +using System.Text.Json.Serialization; + namespace TwitchChatTTS.OBS.Socket.Data { public class IdentifyMessage { public int RpcVersion { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Authentication { get; set; } - public int EventSubscriptions { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? EventSubscriptions { get; set; } - public IdentifyMessage(int version, string auth, int subscriptions) + public IdentifyMessage(int rpcVersion, string? authentication, int? subscriptions) { - RpcVersion = version; - Authentication = auth; + RpcVersion = rpcVersion; + Authentication = authentication; EventSubscriptions = subscriptions; } } diff --git a/OBS/Socket/Handlers/EventMessageHandler.cs b/OBS/Socket/Handlers/EventMessageHandler.cs index a7a7fe0..2f7c30c 100644 --- a/OBS/Socket/Handlers/EventMessageHandler.cs +++ b/OBS/Socket/Handlers/EventMessageHandler.cs @@ -2,19 +2,18 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using Serilog; using TwitchChatTTS.OBS.Socket.Data; -using TwitchChatTTS.OBS.Socket.Manager; namespace TwitchChatTTS.OBS.Socket.Handlers { public class EventMessageHandler : IWebSocketHandler { - private readonly OBSManager _manager; private readonly ILogger _logger; public int OperationCode { get; } = 5; - public EventMessageHandler(OBSManager manager, ILogger logger) + public EventMessageHandler( + ILogger logger + ) { - _manager = manager; _logger = logger; } @@ -22,6 +21,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { if (data is not EventMessage message || message == null) return; + if (sender is not OBSSocketClient obs) + return; switch (message.EventType) { @@ -31,10 +32,10 @@ namespace TwitchChatTTS.OBS.Socket.Handlers string? raw_state = message.EventData["outputState"].ToString(); string? state = raw_state?.Substring(21).ToLower(); - _manager.Streaming = message.EventData["outputActive"].ToString().ToLower() == "true"; + obs.Streaming = message.EventData["outputActive"].ToString()!.ToLower() == "true"; _logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + "."); - if (_manager.Streaming == false && state != null && !state.EndsWith("ing")) + if (obs.Streaming == false && state != null && !state.EndsWith("ing")) { // Stream ended } diff --git a/OBS/Socket/Handlers/HelloHandler.cs b/OBS/Socket/Handlers/HelloHandler.cs index 28d5ad6..09e5ec5 100644 --- a/OBS/Socket/Handlers/HelloHandler.cs +++ b/OBS/Socket/Handlers/HelloHandler.cs @@ -26,9 +26,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim(); _logger.Verbose("OBS websocket password: " + password); - if (message.Authentication == null || string.IsNullOrWhiteSpace(password)) + if (message.Authentication == null || string.IsNullOrEmpty(password)) { - await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144)); + await sender.Send(1, new IdentifyMessage(message.RpcVersion, null, 1023 | 262144)); return; } @@ -39,7 +39,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers string secret = password + salt; byte[] bytes = Encoding.UTF8.GetBytes(secret); - string hash = null; + string? hash = null; using (var sha = SHA256.Create()) { bytes = sha.ComputeHash(bytes); diff --git a/OBS/Socket/Handlers/IdentifiedHandler.cs b/OBS/Socket/Handlers/IdentifiedHandler.cs index 71268e2..2f78f87 100644 --- a/OBS/Socket/Handlers/IdentifiedHandler.cs +++ b/OBS/Socket/Handlers/IdentifiedHandler.cs @@ -2,19 +2,16 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using Serilog; using TwitchChatTTS.OBS.Socket.Data; -using TwitchChatTTS.OBS.Socket.Manager; namespace TwitchChatTTS.OBS.Socket.Handlers { public class IdentifiedHandler : IWebSocketHandler { - private readonly OBSManager _manager; private readonly ILogger _logger; public int OperationCode { get; } = 2; - public IdentifiedHandler(OBSManager manager, ILogger logger) + public IdentifiedHandler(ILogger logger) { - _manager = manager; _logger = logger; } @@ -22,20 +19,22 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { if (data is not IdentifiedMessage message || message == null) return; + if (sender is not OBSSocketClient obs) + return; - _manager.Connected = true; + obs.Identified = true; _logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + "."); try { - await _manager.GetGroupList(async groups => await _manager.GetGroupSceneItemList(groups)); + await obs.GetGroupList(async groups => await obs.GetGroupSceneItemList(groups)); } catch (Exception e) { _logger.Error(e, "Failed to load OBS group info upon OBS identification."); } - await _manager.UpdateStreamingState(); + await obs.UpdateStreamingState(); } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/RequestBatchResponseHandler.cs b/OBS/Socket/Handlers/RequestBatchResponseHandler.cs index 10e4c99..ef91dab 100644 --- a/OBS/Socket/Handlers/RequestBatchResponseHandler.cs +++ b/OBS/Socket/Handlers/RequestBatchResponseHandler.cs @@ -5,22 +5,16 @@ using Microsoft.Extensions.DependencyInjection; using Serilog; using Serilog.Context; using TwitchChatTTS.OBS.Socket.Data; -using TwitchChatTTS.OBS.Socket.Manager; namespace TwitchChatTTS.OBS.Socket.Handlers { public class RequestBatchResponseHandler : IWebSocketHandler { - private readonly IWebSocketHandler _requestResponseHandler; private readonly ILogger _logger; public int OperationCode { get; } = 9; - public RequestBatchResponseHandler( - [FromKeyedServices("obs-requestresponse")] IWebSocketHandler requestResponseHandler, - ILogger logger - ) + public RequestBatchResponseHandler(ILogger logger) { - _requestResponseHandler = requestResponseHandler; _logger = logger; } @@ -28,40 +22,38 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { if (data is not RequestBatchResponseMessage message || message == null) return; + if (sender is not OBSSocketClient obs) + return; - using (LogContext.PushProperty("obsrid", message.RequestId)) + var results = message.Results.ToList(); + _logger.Debug($"Received request batch response of {results.Count} messages."); + + int count = results.Count; + for (int i = 0; i < count; i++) { + if (results[i] == null) + continue; - var results = message.Results.ToList(); - _logger.Debug($"Received request batch response of {results.Count} messages."); - - int count = results.Count; - for (int i = 0; i < count; i++) + try { - if (results[i] == null) + _logger.Debug($"Request response from OBS request batch #{i + 1}/{count}: {results[i]}"); + var response = JsonSerializer.Deserialize(results[i].ToString()!, new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + if (response == null) continue; - try - { - _logger.Debug($"Request response from OBS request batch #{i + 1}/{count}: {results[i]}"); - var response = JsonSerializer.Deserialize(results[i].ToString()!, new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = false, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); - if (response == null) - continue; - - await _requestResponseHandler.Execute(sender, response); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to process an item in a request batch message."); - } + await obs.ExecuteRequest(response); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to process an item in a request batch message."); } - - _logger.Debug($"Finished processing all request in this batch."); } + + _logger.Debug($"Finished processing all request in this batch."); } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/RequestResponseHandler.cs b/OBS/Socket/Handlers/RequestResponseHandler.cs index e8beb1b..d5c6223 100644 --- a/OBS/Socket/Handlers/RequestResponseHandler.cs +++ b/OBS/Socket/Handlers/RequestResponseHandler.cs @@ -3,19 +3,18 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using Serilog; using TwitchChatTTS.OBS.Socket.Data; -using TwitchChatTTS.OBS.Socket.Manager; namespace TwitchChatTTS.OBS.Socket.Handlers { public class RequestResponseHandler : IWebSocketHandler { - private readonly OBSManager _manager; private readonly ILogger _logger; public int OperationCode { get; } = 7; - public RequestResponseHandler(OBSManager manager, ILogger logger) + public RequestResponseHandler( + ILogger logger + ) { - _manager = manager; _logger = logger; } @@ -23,10 +22,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { if (data is not RequestResponseMessage message || message == null) return; + if (sender is not OBSSocketClient obs) + return; _logger.Debug($"Received an OBS request response [obs request id: {message.RequestId}]"); - var requestData = _manager.Take(message.RequestId); + var requestData = obs.Take(message.RequestId); if (requestData == null) { _logger.Warning($"OBS Request Response not being processed: request not stored [obs request id: {message.RequestId}]"); @@ -42,7 +43,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers switch (request.RequestType) { case "GetOutputStatus": - _logger.Debug($"Fetched stream's live status [live: {_manager.Streaming}][obs request id: {message.RequestId}]"); + _logger.Debug($"Fetched stream's live status [live: {obs.Streaming}][obs request id: {message.RequestId}]"); break; case "GetSceneItemId": { @@ -206,7 +207,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers } foreach (var sceneItem in sceneItems) - _manager.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId); + obs.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId); requestData.ResponseValues = new Dictionary() { @@ -237,9 +238,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers return; } - _manager.Streaming = outputActive?.ToString()!.ToLower() == "true"; + obs.Streaming = outputActive?.ToString()!.ToLower() == "true"; requestData.ResponseValues = message.ResponseData; - _logger.Information($"OBS is currently {(_manager.Streaming ? "" : "not ")}streaming."); + _logger.Information($"OBS is currently {(obs.Streaming ? "" : "not ")}streaming."); break; } default: diff --git a/OBS/Socket/Manager/OBSHandlerManager.cs b/OBS/Socket/Manager/OBSHandlerManager.cs deleted file mode 100644 index d14d30a..0000000 --- a/OBS/Socket/Manager/OBSHandlerManager.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Serilog; -using Microsoft.Extensions.DependencyInjection; -using CommonSocketLibrary.Socket.Manager; -using CommonSocketLibrary.Common; - -namespace TwitchChatTTS.OBS.Socket.Manager -{ - public class OBSHandlerManager : WebSocketHandlerManager - { - public OBSHandlerManager(ILogger logger, IServiceProvider provider) : base(logger) - { - var basetype = typeof(IWebSocketHandler); - var assembly = GetType().Assembly; - var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".OBS.") == true); - - foreach (var type in types) - { - var key = "obs-" + type.Name.Replace("Handlers", "Hand#lers") - .Replace("Handler", "") - .Replace("Hand#lers", "Handlers") - .ToLower(); - var handler = provider.GetKeyedService(key); - if (handler == null) - { - logger.Error("Failed to find obs websocket handler: " + type.AssemblyQualifiedName); - continue; - } - - _logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}."); - Add(handler); - } - } - } -} \ No newline at end of file diff --git a/OBS/Socket/Manager/OBSHandlerTypeManager.cs b/OBS/Socket/Manager/OBSHandlerTypeManager.cs index a484a28..19dbba6 100644 --- a/OBS/Socket/Manager/OBSHandlerTypeManager.cs +++ b/OBS/Socket/Manager/OBSHandlerTypeManager.cs @@ -1,4 +1,3 @@ -using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using CommonSocketLibrary.Socket.Manager; using Microsoft.Extensions.DependencyInjection; @@ -6,12 +5,12 @@ using Serilog; namespace TwitchChatTTS.OBS.Socket.Manager { - public class OBSHandlerTypeManager : WebSocketHandlerTypeManager + public class OBSMessageTypeManager : WebSocketMessageTypeManager { - public OBSHandlerTypeManager( - ILogger factory, - [FromKeyedServices("obs")] HandlerManager handlers - ) : base(factory, handlers) + public OBSMessageTypeManager( + [FromKeyedServices("obs")] IEnumerable handlers, + ILogger logger + ) : base(handlers, logger) { } } diff --git a/OBS/Socket/Manager/OBSManager.cs b/OBS/Socket/Manager/OBSManager.cs deleted file mode 100644 index a76c6fd..0000000 --- a/OBS/Socket/Manager/OBSManager.cs +++ /dev/null @@ -1,316 +0,0 @@ -using System.Collections.Concurrent; -using System.Text.Json; -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using TwitchChatTTS.OBS.Socket.Data; - -namespace TwitchChatTTS.OBS.Socket.Manager -{ - public class OBSManager - { - private readonly IDictionary _requests; - private readonly IDictionary _sourceIds; - private string? URL; - - private readonly Configuration _configuration; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public bool Connected { get; set; } - public bool Streaming { get; set; } - - - public OBSManager(Configuration configuration, IServiceProvider serviceProvider, ILogger logger) - { - _configuration = configuration; - _serviceProvider = serviceProvider; - _logger = logger; - - _requests = new ConcurrentDictionary(); - _sourceIds = new Dictionary(); - } - - public void Initialize() - { - _logger.Information($"Initializing OBS websocket client."); - var client = _serviceProvider.GetRequiredKeyedService>("obs"); - - client.OnConnected += (sender, e) => - { - Connected = true; - _logger.Information("OBS websocket client connected."); - }; - - client.OnDisconnected += (sender, e) => - { - Connected = false; - _logger.Information("OBS websocket client disconnected."); - }; - - if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null) - URL = $"ws://{_configuration.Obs.Host?.Trim()}:{_configuration.Obs.Port}"; - } - - - public void AddSourceId(string sourceName, long sourceId) - { - if (!_sourceIds.TryGetValue(sourceName, out _)) - _sourceIds.Add(sourceName, sourceId); - else - _sourceIds[sourceName] = sourceId; - _logger.Debug($"Added OBS scene item to cache [scene item: {sourceName}][scene item id: {sourceId}]"); - } - - public void ClearCache() - { - _sourceIds.Clear(); - } - - public async Task Connect() - { - if (string.IsNullOrWhiteSpace(URL)) - { - _logger.Warning("Lacking connection info for OBS websockets. Not connecting to OBS."); - return; - } - - var client = _serviceProvider.GetRequiredKeyedService>("obs"); - _logger.Debug($"OBS websocket client attempting to connect to {URL}"); - - try - { - await client.ConnectAsync(URL); - } - catch (Exception) - { - _logger.Warning("Connecting to obs failed. Skipping obs websockets."); - } - } - - public async Task Send(IEnumerable messages) - { - if (!Connected) - { - _logger.Warning("OBS websocket client is not connected. Not sending a message."); - return; - } - - string uid = GenerateUniqueIdentifier(); - var list = messages.ToList(); - _logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}]."); - - // Keep track of requests to know what we requested. - foreach (var message in list) - { - message.RequestId = GenerateUniqueIdentifier(); - var data = new RequestData(message, uid); - _requests.Add(message.RequestId, data); - } - _logger.Debug($"Generated uid for all OBS request messages in batch [obs request batch id: {uid}][obs request ids: {string.Join(", ", list.Select(m => m.RequestType + "=" + m.RequestId))}]"); - - var client = _serviceProvider.GetRequiredKeyedService>("obs"); - await client.Send(8, new RequestBatchMessage(uid, list)); - } - - public async Task Send(RequestMessage message, Action>? callback = null) - { - if (!Connected) - { - _logger.Warning("OBS websocket client is not connected. Not sending a message."); - return; - } - - string uid = GenerateUniqueIdentifier(); - _logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]"); - - // Keep track of requests to know what we requested. - message.RequestId = GenerateUniqueIdentifier(); - var data = new RequestData(message, uid) - { - Callback = callback - }; - _requests.Add(message.RequestId, data); - - var client = _serviceProvider.GetRequiredKeyedService>("obs"); - await client.Send(6, message); - } - - public RequestData? Take(string id) - { - if (id != null && _requests.TryGetValue(id, out var request)) - { - _requests.Remove(id); - return request; - } - return null; - } - - public async Task UpdateStreamingState() - { - await Send(new RequestMessage("GetStreamStatus")); - } - - public async Task UpdateTransformation(string sceneName, string sceneItemName, Action action) - { - if (action == null) - return; - - await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => - { - var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); - await Send(m2, async (d) => - { - if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null) - return; - - _logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]"); - var transform = JsonSerializer.Deserialize(transformData.ToString()!, new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = false, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); - if (transform == null) - { - _logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m2.RequestId}]."); - return; - } - - double w = transform.Width; - double h = transform.Height; - int a = transform.Alignment; - bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE"; - - if (a != (int)OBSAlignment.Center) - { - if (hasBounds) - transform.BoundsAlignment = a = (int)OBSAlignment.Center; - else - transform.Alignment = a = (int)OBSAlignment.Center; - - transform.PositionX = transform.PositionX + w / 2; - transform.PositionY = transform.PositionY + h / 2; - } - - action?.Invoke(transform); - - var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } }); - await Send(m3); - _logger.Debug($"New transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m3.RequestId}]"); - }); - }); - } - - public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName) - { - await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => - { - var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); - await Send(m1, async (d) => - { - if (d == null || !d.TryGetValue("sceneItemEnabled", out object? visible) || visible == null) - return; - - var m2 = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", visible.ToString().ToLower() == "true" ? false : true } }); - await Send(m2); - }); - }); - } - - public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible) - { - await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => - { - var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } }); - await Send(m); - }); - } - - public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index) - { - await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => - { - var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } }); - await Send(m); - }); - } - - public async Task GetGroupList(Action>? action) - { - var m = new RequestMessage("GetGroupList", string.Empty, new Dictionary()); - await Send(m, (d) => - { - if (d == null || !d.TryGetValue("groups", out object? value) || value == null) - return; - - var list = (IEnumerable)value; - _logger.Debug("Fetched the list of groups in OBS."); - if (list != null) - action?.Invoke(list); - }); - } - - public async Task GetGroupSceneItemList(string groupName, Action>? action) - { - var m = new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary() { { "sceneName", groupName } }); - await Send(m, (d) => - { - if (d == null || !d.TryGetValue("sceneItems", out object? value) || value == null) - return; - - var list = (IEnumerable)value; - _logger.Debug($"Fetched the list of OBS scene items in a group [group: {groupName}]"); - if (list != null) - action?.Invoke(list); - }); - } - - public async Task GetGroupSceneItemList(IEnumerable groupNames) - { - var messages = groupNames.Select(group => new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary() { { "sceneName", group } })); - await Send(messages); - _logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]"); - } - - private async Task GetSceneItemByName(string sceneName, string sceneItemName, Action action) - { - if (_sourceIds.TryGetValue(sceneItemName, out long sourceId)) - { - _logger.Debug($"Fetched scene item id from cache [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sourceId}]"); - action.Invoke(sourceId); - return; - } - - var m = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sourceName", sceneItemName } }); - await Send(m, async (d) => - { - if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId)) - return; - - _logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sceneItemId}][obs request id: {m.RequestId}]"); - AddSourceId(sceneItemName, sceneItemId); - action.Invoke(sceneItemId); - }); - } - - private string GenerateUniqueIdentifier() - { - return Guid.NewGuid().ToString("N"); - } - } - - public class RequestData - { - public RequestMessage Message { get; } - public string ParentId { get; } - public Dictionary ResponseValues { get; set; } - public Action>? Callback { get; set; } - - public RequestData(RequestMessage message, string parentId) - { - Message = message; - ParentId = parentId; - } - } -} \ No newline at end of file diff --git a/OBS/Socket/OBSSocketClient.cs b/OBS/Socket/OBSSocketClient.cs index a963d45..56e4fe2 100644 --- a/OBS/Socket/OBSSocketClient.cs +++ b/OBS/Socket/OBSSocketClient.cs @@ -3,21 +3,365 @@ using CommonSocketLibrary.Abstract; using Microsoft.Extensions.DependencyInjection; using Serilog; using System.Text.Json; +using System.Collections.Concurrent; +using TwitchChatTTS.OBS.Socket.Data; +using System.Timers; +using System.Net.WebSockets; namespace TwitchChatTTS.OBS.Socket { public class OBSSocketClient : WebSocketClient { + private readonly IDictionary _requests; + private readonly IDictionary _sourceIds; + private string? URL; + + private readonly Configuration _configuration; + private System.Timers.Timer _reconnectTimer; + + public bool Connected { get; set; } + public bool Identified { get; set; } + public bool Streaming { get; set; } + + public OBSSocketClient( - ILogger logger, - [FromKeyedServices("obs")] HandlerManager handlerManager, - [FromKeyedServices("obs")] HandlerTypeManager typeManager - ) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() + Configuration configuration, + [FromKeyedServices("obs")] IEnumerable handlers, + [FromKeyedServices("obs")] MessageTypeManager typeManager, + ILogger logger + ) : base(handlers, typeManager, new JsonSerializerOptions() { PropertyNameCaseInsensitive = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }) + }, logger) { + _configuration = configuration; + + _reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(30)); + _reconnectTimer.Elapsed += async (sender, e) => await Reconnect(e); + _reconnectTimer.Enabled = false; + + _requests = new ConcurrentDictionary(); + _sourceIds = new Dictionary(); + } + + public void Initialize() + { + _logger.Information($"Initializing OBS websocket client."); + OnConnected += (sender, e) => + { + Connected = true; + _reconnectTimer.Enabled = false; + _logger.Information("OBS websocket client connected."); + }; + + OnDisconnected += (sender, e) => + { + _reconnectTimer.Enabled = Identified; + _logger.Information($"OBS websocket client disconnected [status: {e.Status}][reason: {e.Reason}] " + (Identified ? "Will be attempting to reconnect every 30 seconds." : "Will not be attempting to reconnect.")); + + Connected = false; + Identified = false; + Streaming = false; + }; + + if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null) + URL = $"ws://{_configuration.Obs.Host?.Trim()}:{_configuration.Obs.Port}"; + } + + + public void AddSourceId(string sourceName, long sourceId) + { + if (!_sourceIds.TryGetValue(sourceName, out _)) + _sourceIds.Add(sourceName, sourceId); + else + _sourceIds[sourceName] = sourceId; + _logger.Debug($"Added OBS scene item to cache [scene item: {sourceName}][scene item id: {sourceId}]"); + } + + public void ClearCache() + { + _sourceIds.Clear(); + } + + public async Task Connect() + { + if (string.IsNullOrWhiteSpace(URL)) + { + _logger.Warning("Lacking connection info for OBS websockets. Not connecting to OBS."); + return; + } + + _logger.Debug($"OBS websocket client attempting to connect to {URL}"); + + try + { + await ConnectAsync(URL); + } + catch (Exception) + { + _logger.Warning("Connecting to obs failed. Skipping obs websockets."); + } + } + + public async Task ExecuteRequest(RequestResponseMessage message) { + if (!_handlers.TryGetValue(7, out var handler) || handler == null) + { + _logger.Error("Failed to find the request response handler for OBS."); + return; + } + + await handler.Execute(this, message); + } + + private async Task Reconnect(ElapsedEventArgs e) + { + if (Connected) + { + try + { + await DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), "")); + } + catch (Exception) + { + _logger.Error("Failed to disconnect from OBS websocket server."); + } + } + + try + { + await Connect(); + } + catch (WebSocketException wse) when (wse.Message.Contains("502")) + { + _logger.Error("OBS websocket server cannot be found. Be sure the server is on by looking at OBS > Tools > Websocket Server Settings."); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to reconnect to OBS websocket server."); + } + } + + public async Task Send(IEnumerable messages) + { + if (!Connected) + { + _logger.Warning("OBS websocket client is not connected. Not sending a message."); + return; + } + + string uid = GenerateUniqueIdentifier(); + var list = messages.ToList(); + _logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}]."); + + // Keep track of requests to know what we requested. + foreach (var message in list) + { + message.RequestId = GenerateUniqueIdentifier(); + var data = new RequestData(message, uid); + _requests.Add(message.RequestId, data); + } + + _logger.Debug($"Generated uid for all OBS request messages in batch [obs request batch id: {uid}][obs request ids: {string.Join(", ", list.Select(m => m.RequestType + "=" + m.RequestId))}]"); + await Send(8, new RequestBatchMessage(uid, list)); + } + + public async Task Send(RequestMessage message, Action>? callback = null) + { + if (!Connected) + { + _logger.Warning("OBS websocket client is not connected. Not sending a message."); + return; + } + + string uid = GenerateUniqueIdentifier(); + _logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]"); + + // Keep track of requests to know what we requested. + message.RequestId = uid; + var data = new RequestData(message, uid) + { + Callback = callback + }; + _requests.Add(message.RequestId, data); + + await Send(6, message); + } + + public RequestData? Take(string id) + { + if (id != null && _requests.TryGetValue(id, out var request)) + { + _requests.Remove(id); + return request; + } + return null; + } + + public async Task UpdateStreamingState() + { + await Send(new RequestMessage("GetStreamStatus")); + } + + public async Task UpdateTransformation(string sceneName, string sceneItemName, Action action) + { + if (action == null) + return; + + await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => + { + var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); + await Send(m2, async (d) => + { + if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null) + return; + + _logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]"); + var transform = JsonSerializer.Deserialize(transformData.ToString()!, new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + if (transform == null) + { + _logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m2.RequestId}]."); + return; + } + + double w = transform.Width; + double h = transform.Height; + int a = transform.Alignment; + bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE"; + + if (a != (int)OBSAlignment.Center) + { + if (hasBounds) + transform.BoundsAlignment = a = (int)OBSAlignment.Center; + else + transform.Alignment = a = (int)OBSAlignment.Center; + + transform.PositionX = transform.PositionX + w / 2; + transform.PositionY = transform.PositionY + h / 2; + } + + action?.Invoke(transform); + + var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } }); + await Send(m3); + _logger.Debug($"New transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m3.RequestId}]"); + }); + }); + } + + public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName) + { + await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => + { + var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); + await Send(m1, async (d) => + { + if (d == null || !d.TryGetValue("sceneItemEnabled", out object? visible) || visible == null) + return; + + var m2 = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", visible.ToString().ToLower() == "true" ? false : true } }); + await Send(m2); + }); + }); + } + + public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible) + { + await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => + { + var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } }); + await Send(m); + }); + } + + public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index) + { + await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => + { + var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } }); + await Send(m); + }); + } + + public async Task GetGroupList(Action>? action) + { + var m = new RequestMessage("GetGroupList", string.Empty, new Dictionary()); + await Send(m, (d) => + { + if (d == null || !d.TryGetValue("groups", out object? value) || value == null) + return; + + var list = (IEnumerable)value; + _logger.Debug("Fetched the list of groups in OBS."); + if (list != null) + action?.Invoke(list); + }); + } + + public async Task GetGroupSceneItemList(string groupName, Action>? action) + { + var m = new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary() { { "sceneName", groupName } }); + await Send(m, (d) => + { + if (d == null || !d.TryGetValue("sceneItems", out object? value) || value == null) + return; + + var list = (IEnumerable)value; + _logger.Debug($"Fetched the list of OBS scene items in a group [group: {groupName}]"); + if (list != null) + action?.Invoke(list); + }); + } + + public async Task GetGroupSceneItemList(IEnumerable groupNames) + { + var messages = groupNames.Select(group => new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary() { { "sceneName", group } })); + await Send(messages); + _logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]"); + } + + private async Task GetSceneItemByName(string sceneName, string sceneItemName, Action action) + { + if (_sourceIds.TryGetValue(sceneItemName, out long sourceId)) + { + _logger.Debug($"Fetched scene item id from cache [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sourceId}]"); + action.Invoke(sourceId); + return; + } + + var m = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sourceName", sceneItemName } }); + await Send(m, async (d) => + { + if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId)) + return; + + _logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sceneItemId}][obs request id: {m.RequestId}]"); + AddSourceId(sceneItemName, sceneItemId); + action.Invoke(sceneItemId); + }); + } + + private string GenerateUniqueIdentifier() + { + return Guid.NewGuid().ToString("N"); + } + } + + public class RequestData + { + public RequestMessage Message { get; } + public string ParentId { get; } + public Dictionary? ResponseValues { get; set; } + public Action>? Callback { get; set; } + + public RequestData(RequestMessage message, string parentId) + { + Message = message; + ParentId = parentId; } } } \ No newline at end of file diff --git a/Seven/SevenManager.cs b/Seven/SevenManager.cs deleted file mode 100644 index c328f3e..0000000 --- a/Seven/SevenManager.cs +++ /dev/null @@ -1,57 +0,0 @@ -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; -using Microsoft.Extensions.DependencyInjection; -using Serilog; - -namespace TwitchChatTTS.Seven.Socket -{ - public class SevenManager - { - private readonly User _user; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private string URL; - - public bool Connected { get; set; } - public bool Streaming { get; set; } - - - public SevenManager(User user, IServiceProvider serviceProvider, ILogger logger) - { - _user = user; - _serviceProvider = serviceProvider; - _logger = logger; - } - - public void Initialize() { - _logger.Information("Initializing 7tv websocket client."); - var client = _serviceProvider.GetRequiredKeyedService>("7tv"); - - client.OnConnected += (sender, e) => { - Connected = true; - _logger.Information("7tv websocket client connected."); - }; - - client.OnDisconnected += (sender, e) => { - Connected = false; - _logger.Information("7tv websocket client disconnected."); - }; - - if (!string.IsNullOrEmpty(_user.SevenEmoteSetId)) - URL = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*"; - } - - public async Task Connect() - { - if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId)) - { - _logger.Warning("Cannot find 7tv data for your channel. Not connecting to 7tv websockets."); - return; - } - - var client = _serviceProvider.GetRequiredKeyedService>("7tv"); - _logger.Debug($"7tv client attempting to connect to {URL}"); - await client.ConnectAsync($"{URL}"); - } - } -} \ No newline at end of file diff --git a/Seven/Socket/Context/ReconnectContext.cs b/Seven/Socket/Context/ReconnectContext.cs deleted file mode 100644 index 19db724..0000000 --- a/Seven/Socket/Context/ReconnectContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace TwitchChatTTS.Seven.Socket.Context -{ - public class ReconnectContext - { - public string? SessionId; - } -} \ No newline at end of file diff --git a/Seven/Socket/Handlers/EndOfStreamHandler.cs b/Seven/Socket/Handlers/EndOfStreamHandler.cs index d7da8fc..a700ba7 100644 --- a/Seven/Socket/Handlers/EndOfStreamHandler.cs +++ b/Seven/Socket/Handlers/EndOfStreamHandler.cs @@ -1,102 +1,21 @@ +using System.Net.WebSockets; using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using TwitchChatTTS.Seven.Socket.Context; using TwitchChatTTS.Seven.Socket.Data; namespace TwitchChatTTS.Seven.Socket.Handlers { public class EndOfStreamHandler : IWebSocketHandler { - private readonly ILogger _logger; - private readonly User _user; - private readonly IServiceProvider _serviceProvider; - private readonly string[] _errorCodes; - private readonly int[] _reconnectDelay; - public int OperationCode { get; } = 7; - - public EndOfStreamHandler(User user, IServiceProvider serviceProvider, ILogger logger) - { - _logger = logger; - _user = user; - _serviceProvider = serviceProvider; - - _errorCodes = [ - "Server Error", - "Unknown Operation", - "Invalid Payload", - "Auth Failure", - "Already Identified", - "Rate Limited", - "Restart", - "Maintenance", - "Timeout", - "Already Subscribed", - "Not Subscribed", - "Insufficient Privilege", - "Inactivity?" - ]; - _reconnectDelay = [ - 1000, - -1, - -1, - -1, - 0, - 3000, - 1000, - 300000, - 1000, - 0, - 0, - 1000, - 1000 - ]; - } - public async Task Execute(SocketClient sender, Data data) { if (data is not EndOfStreamMessage message || message == null) return; var code = message.Code - 4000; - if (code >= 0 && code < _errorCodes.Length) - _logger.Warning($"Received end of stream message (reason: {_errorCodes[code]}, code: {message.Code}, message: {message.Message})."); - else - _logger.Warning($"Received end of stream message (code: {message.Code}, message: {message.Message})."); - - await sender.DisconnectAsync(); - - if (code >= 0 && code < _reconnectDelay.Length && _reconnectDelay[code] < 0) - { - _logger.Error($"7tv client will remain disconnected due to a bad client implementation."); - return; - } - - if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId)) - { - _logger.Warning("Could not find the 7tv emote set id. Not reconnecting."); - return; - } - - var context = _serviceProvider.GetRequiredService(); - if (_reconnectDelay[code] > 0) - await Task.Delay(_reconnectDelay[code]); - - var manager = _serviceProvider.GetRequiredService(); - await manager.Connect(); - - if (context.SessionId != null) - { - await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId }); - _logger.Debug("Resumed connection to 7tv websocket."); - } - else - { - _logger.Debug("Resumed connection to 7tv websocket on a different session."); - } + await sender.DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), code.ToString())); } } } \ No newline at end of file diff --git a/Seven/Socket/Handlers/SevenHelloHandler.cs b/Seven/Socket/Handlers/SevenHelloHandler.cs index d4eff78..92e8058 100644 --- a/Seven/Socket/Handlers/SevenHelloHandler.cs +++ b/Seven/Socket/Handlers/SevenHelloHandler.cs @@ -19,7 +19,6 @@ namespace TwitchChatTTS.Seven.Socket.Handlers { if (data is not SevenHelloMessage message || message == null) return; - if (sender is not SevenSocketClient seven || seven == null) return; diff --git a/Seven/Socket/Managers/SevenHandlerManager.cs b/Seven/Socket/Managers/SevenHandlerManager.cs deleted file mode 100644 index ecc5a1f..0000000 --- a/Seven/Socket/Managers/SevenHandlerManager.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Serilog; -using CommonSocketLibrary.Socket.Manager; -using CommonSocketLibrary.Common; -using Microsoft.Extensions.DependencyInjection; - -namespace TwitchChatTTS.Seven.Socket.Managers -{ - public class SevenHandlerManager : WebSocketHandlerManager - { - public SevenHandlerManager(ILogger logger, IServiceProvider provider) : base(logger) - { - try - { - var basetype = typeof(IWebSocketHandler); - var assembly = GetType().Assembly; - var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Seven.") == true); - - foreach (var type in types) - { - var key = "7tv-" + type.Name.Replace("Handlers", "Hand#lers") - .Replace("Handler", "") - .Replace("Hand#lers", "Handlers") - .ToLower(); - var handler = provider.GetKeyedService(key); - if (handler == null) - { - logger.Error("Failed to find 7tv websocket handler: " + type.AssemblyQualifiedName); - continue; - } - - _logger.Debug($"Linked type {type.AssemblyQualifiedName} to 7tv websocket handler {handler.GetType().AssemblyQualifiedName}."); - Add(handler); - } - } - catch (Exception e) - { - _logger.Error(e, "Failed to load 7tv websocket handler types."); - } - } - } -} \ No newline at end of file diff --git a/Seven/Socket/Managers/SevenHandlerTypeManager.cs b/Seven/Socket/Managers/SevenHandlerTypeManager.cs index b2430ce..86afd8a 100644 --- a/Seven/Socket/Managers/SevenHandlerTypeManager.cs +++ b/Seven/Socket/Managers/SevenHandlerTypeManager.cs @@ -1,4 +1,3 @@ -using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using CommonSocketLibrary.Socket.Manager; using Microsoft.Extensions.DependencyInjection; @@ -6,13 +5,12 @@ using Serilog; namespace TwitchChatTTS.Seven.Socket.Managers { - public class SevenHandlerTypeManager : WebSocketHandlerTypeManager + public class SevenMessageTypeManager : WebSocketMessageTypeManager { - public SevenHandlerTypeManager( - ILogger factory, - [FromKeyedServices("7tv")] HandlerManager handlers - ) : base(factory, handlers) + public SevenMessageTypeManager( + [FromKeyedServices("7tv")] IEnumerable handlers, + ILogger logger + ) : base(handlers, logger) { } } diff --git a/Seven/Socket/SevenSocketClient.cs b/Seven/Socket/SevenSocketClient.cs index dc54435..dd825d2 100644 --- a/Seven/Socket/SevenSocketClient.cs +++ b/Seven/Socket/SevenSocketClient.cs @@ -9,19 +9,133 @@ namespace TwitchChatTTS.Seven.Socket { public class SevenSocketClient : WebSocketClient { + private readonly User _user; + private readonly string[] _errorCodes; + private readonly int[] _reconnectDelay; + private string? URL; + + public bool Connected { get; set; } + public SevenHelloMessage? ConnectionDetails { get; set; } public SevenSocketClient( - ILogger logger, - [FromKeyedServices("7tv")] HandlerManager handlerManager, - [FromKeyedServices("7tv")] HandlerTypeManager typeManager - ) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() + User user, + [FromKeyedServices("7tv")] IEnumerable handlers, + [FromKeyedServices("7tv")] MessageTypeManager typeManager, + ILogger logger + ) : base(handlers, typeManager, new JsonSerializerOptions() { PropertyNameCaseInsensitive = false, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower - }) + }, logger) { + _user = user; ConnectionDetails = null; + + _errorCodes = [ + "Server Error", + "Unknown Operation", + "Invalid Payload", + "Auth Failure", + "Already Identified", + "Rate Limited", + "Restart", + "Maintenance", + "Timeout", + "Already Subscribed", + "Not Subscribed", + "Insufficient Privilege", + "Inactivity?" + ]; + _reconnectDelay = [ + 1000, + -1, + -1, + -1, + 0, + 3000, + 1000, + 300000, + 1000, + 0, + 0, + 1000, + 1000 + ]; + } + + + public void Initialize() + { + _logger.Information("Initializing 7tv websocket client."); + OnConnected += (sender, e) => + { + Connected = true; + _logger.Information("7tv websocket client connected."); + }; + + OnDisconnected += (sender, e) => OnDisconnection(sender, e); + + if (!string.IsNullOrEmpty(_user.SevenEmoteSetId)) + URL = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*"; + } + + public async Task Connect() + { + if (string.IsNullOrEmpty(URL)) + { + _logger.Warning("Cannot find 7tv url. Not connecting to 7tv websockets."); + return; + } + if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId)) + { + _logger.Warning("Cannot find 7tv data for your channel. Not connecting to 7tv websockets."); + return; + } + + _logger.Debug($"7tv client attempting to connect to {URL}"); + await ConnectAsync($"{URL}"); + } + + private async void OnDisconnection(object? sender, SocketDisconnectionEventArgs e) + { + Connected = false; + + if (int.TryParse(e.Reason, out int code)) + { + if (code >= 0 && code < _errorCodes.Length) + _logger.Warning($"Received end of stream message for 7tv websocket [reason: {_errorCodes[code]}][code: {code}]"); + else + _logger.Warning($"Received end of stream message for 7tv websocket [code: {code}]"); + + if (code >= 0 && code < _reconnectDelay.Length && _reconnectDelay[code] < 0) + { + _logger.Error($"7tv client will remain disconnected due to a bad client implementation."); + return; + } + + if (_reconnectDelay[code] > 0) + await Task.Delay(_reconnectDelay[code]); + } + + if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId)) + { + _logger.Warning("Could not find the 7tv emote set id. Not reconnecting."); + return; + } + + await Connect(); + await Task.Delay(TimeSpan.FromMilliseconds(500)); + + if (Connected && ConnectionDetails?.SessionId != null) + { + await Send(34, new ResumeMessage() { SessionId = ConnectionDetails.SessionId }); + _logger.Debug("Resumed connection to 7tv websocket."); + } + else + { + _logger.Debug("Resumed connection to 7tv websocket on a different session."); + } } } } \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index e8ec733..e46956a 100644 --- a/Startup.cs +++ b/Startup.cs @@ -10,7 +10,6 @@ using YamlDotNet.Serialization.NamingConventions; using TwitchChatTTS.Seven.Socket; using TwitchChatTTS.OBS.Socket.Handlers; using TwitchChatTTS.Seven.Socket.Handlers; -using TwitchChatTTS.Seven.Socket.Context; using TwitchLib.Client.Interfaces; using TwitchLib.Client; using TwitchLib.PubSub.Interfaces; @@ -31,6 +30,7 @@ using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Emotes; using HermesSocketLibrary.Requests.Callbacks; +using static TwitchChatTTS.Chat.Commands.TTSCommands; // dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true @@ -70,21 +70,16 @@ s.AddSingleton(new JsonSerializerOptions() }); // Command parameters -s.AddKeyedSingleton("parameter-ttsvoicename"); -s.AddKeyedSingleton("parameter-unvalidated"); -s.AddKeyedSingleton("parameter-simplelisted"); -s.AddKeyedSingleton("command-skipall"); -s.AddKeyedSingleton("command-skip"); -s.AddKeyedSingleton("command-voice"); -s.AddKeyedSingleton("command-addttsvoice"); -s.AddKeyedSingleton("command-removettsvoice"); -s.AddKeyedSingleton("command-refreshttsdata"); -s.AddKeyedSingleton("command-obs"); -s.AddKeyedSingleton("command-tts"); -s.AddKeyedSingleton("command-version"); +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(); @@ -100,48 +95,32 @@ s.AddSingleton(); s.AddSingleton(); // OBS websocket -s.AddSingleton(); -s.AddKeyedSingleton("obs-hello"); -s.AddKeyedSingleton("obs-identified"); -s.AddKeyedSingleton("obs-requestresponse"); -s.AddKeyedSingleton("obs-requestbatchresponse"); -s.AddKeyedSingleton("obs-eventmessage"); +s.AddKeyedSingleton("obs"); +s.AddKeyedSingleton("obs"); +s.AddKeyedSingleton("obs"); +s.AddKeyedSingleton("obs"); +s.AddKeyedSingleton("obs"); -s.AddKeyedSingleton, OBSHandlerManager>("obs"); -s.AddKeyedSingleton, OBSHandlerTypeManager>("obs"); +s.AddKeyedSingleton, OBSMessageTypeManager>("obs"); s.AddKeyedSingleton, OBSSocketClient>("obs"); // 7tv websocket -s.AddTransient(sp => -{ - var logger = sp.GetRequiredService(); - var client = sp.GetRequiredKeyedService>("7tv") as SevenSocketClient; - if (client == null) - return new ReconnectContext() { SessionId = null }; - if (client.ConnectionDetails == null) - return new ReconnectContext() { SessionId = null }; - return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId }; -}); -s.AddKeyedSingleton("7tv-sevenhello"); -s.AddKeyedSingleton("7tv-hello"); -s.AddKeyedSingleton("7tv-dispatch"); -s.AddKeyedSingleton("7tv-reconnect"); -s.AddKeyedSingleton("7tv-error"); -s.AddKeyedSingleton("7tv-endofstream"); +s.AddKeyedSingleton("7tv"); +s.AddKeyedSingleton("7tv"); +s.AddKeyedSingleton("7tv"); +s.AddKeyedSingleton("7tv"); +s.AddKeyedSingleton("7tv"); -s.AddSingleton(); -s.AddKeyedSingleton, SevenHandlerManager>("7tv"); -s.AddKeyedSingleton, SevenHandlerTypeManager>("7tv"); +s.AddKeyedSingleton, SevenMessageTypeManager>("7tv"); s.AddKeyedSingleton, SevenSocketClient>("7tv"); // hermes websocket -s.AddKeyedSingleton("hermes-heartbeat"); -s.AddKeyedSingleton("hermes-loginack"); -s.AddKeyedSingleton("hermes-requestack"); -s.AddKeyedSingleton("hermes-error"); +s.AddKeyedSingleton("hermes"); +s.AddKeyedSingleton("hermes"); +s.AddKeyedSingleton("hermes"); +//s.AddKeyedSingleton("hermes"); -s.AddKeyedSingleton, HermesHandlerManager>("hermes"); -s.AddKeyedSingleton, HermesHandlerTypeManager>("hermes"); +s.AddKeyedSingleton, HermesMessageTypeManager>("hermes"); s.AddKeyedSingleton, HermesSocketClient>("hermes"); s.AddHostedService(); diff --git a/TTS.cs b/TTS.cs index ce1e75f..e856038 100644 --- a/TTS.cs +++ b/TTS.cs @@ -5,33 +5,30 @@ using Microsoft.Extensions.Hosting; using Serilog; using NAudio.Wave.SampleProviders; using TwitchLib.Client.Events; -using TwitchChatTTS.Twitch.Redemptions; using org.mariuszgromada.math.mxparser; using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups; -using TwitchChatTTS.OBS.Socket.Manager; using TwitchChatTTS.Seven.Socket; using TwitchChatTTS.Chat.Emotes; using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; +using TwitchChatTTS.OBS.Socket; namespace TwitchChatTTS { public class TTS : IHostedService { public const int MAJOR_VERSION = 3; - public const int MINOR_VERSION = 9; + public const int MINOR_VERSION = 10; private readonly User _user; private readonly HermesApiClient _hermesApiClient; private readonly SevenApiClient _sevenApiClient; - private readonly OBSManager _obsManager; - private readonly SevenManager _sevenManager; + private readonly OBSSocketClient _obs; + private readonly SevenSocketClient _seven; private readonly HermesSocketClient _hermes; - private readonly RedemptionManager _redemptionManager; - private readonly IChatterGroupManager _chatterGroupManager; - private readonly IGroupPermissionManager _permissionManager; + private readonly IEmoteDatabase _emotes; private readonly Configuration _configuration; private readonly TTSPlayer _player; private readonly IServiceProvider _serviceProvider; @@ -41,12 +38,10 @@ namespace TwitchChatTTS User user, HermesApiClient hermesApiClient, SevenApiClient sevenApiClient, - OBSManager obsManager, - SevenManager sevenManager, [FromKeyedServices("hermes")] SocketClient hermes, - RedemptionManager redemptionManager, - IChatterGroupManager chatterGroupManager, - IGroupPermissionManager permissionManager, + [FromKeyedServices("obs")] SocketClient obs, + [FromKeyedServices("7tv")] SocketClient seven, + IEmoteDatabase emotes, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider, @@ -56,12 +51,10 @@ namespace TwitchChatTTS _user = user; _hermesApiClient = hermesApiClient; _sevenApiClient = sevenApiClient; - _obsManager = obsManager; - _sevenManager = sevenManager; _hermes = (hermes as HermesSocketClient)!; - _redemptionManager = redemptionManager; - _chatterGroupManager = chatterGroupManager; - _permissionManager = permissionManager; + _obs = (obs as OBSSocketClient)!; + _seven = (seven as SevenSocketClient)!; + _emotes = emotes; _configuration = configuration; _player = player; _serviceProvider = serviceProvider; @@ -119,14 +112,6 @@ namespace TwitchChatTTS await InitializeSevenTv(); await InitializeObs(); - // _logger.Information("Sending a request to server..."); - // await _hermesManager.Send(3, new RequestMessage() { - // Type = "get_redeemable_actions", - // Data = new Dictionary() - // }); - // _logger.Warning("OS VERSION: " + Environment.OSVersion + " | " + Environment.OSVersion.Platform); - // return; - AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => { if (e.SampleProvider == _player.Playing) @@ -239,65 +224,8 @@ namespace TwitchChatTTS user.TwitchUsername = hermesAccount.Username; var twitchBotToken = await hermes.FetchTwitchBotToken(); - user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId); + user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId!); _logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]"); - - // user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice(); - // _logger.Information("TTS Default Voice: " + user.DefaultTTSVoice); - - // var wordFilters = await hermes.FetchTTSWordFilters(); - // user.RegexFilters = wordFilters.ToList(); - // _logger.Information($"{user.RegexFilters.Count()} TTS word filters."); - - var voicesSelected = await hermes.FetchTTSChatterSelectedVoices(); - user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice); - _logger.Information($"{user.VoicesSelected.Count} chatters have selected a specific TTS voice, among {user.VoicesSelected.Values.Distinct().Count()} distinct TTS voices."); - - var voicesEnabled = await hermes.FetchTTSEnabledVoices(); - if (voicesEnabled == null || !voicesEnabled.Any()) - user.VoicesEnabled = new HashSet([user.DefaultTTSVoice]); - else - user.VoicesEnabled = new HashSet(voicesEnabled.Select(v => v)); - _logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled."); - - var defaultedChatters = voicesSelected.Where(item => item.Voice == null || !user.VoicesEnabled.Contains(item.Voice)); - if (defaultedChatters.Any()) - _logger.Information($"{defaultedChatters.Count()} chatter(s) will have their TTS voice set to default due to having selected a disabled TTS voice."); - - // var redemptionActions = await hermes.FetchRedeemableActions(); - // var redemptions = await hermes.FetchRedemptions(); - // _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a)); - // _logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); - - var groups = await hermes.FetchGroups(); - var groupsById = groups.ToDictionary(g => g.Id, g => g); - foreach (var group in groups) - _chatterGroupManager.Add(group); - _logger.Information($"{groups.Count()} groups have been loaded."); - - var groupChatters = await hermes.FetchGroupChatters(); - _logger.Debug($"{groupChatters.Count()} group users have been fetched."); - - var permissions = await hermes.FetchGroupPermissions(); - foreach (var permission in permissions) - { - _logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]"); - if (!groupsById.TryGetValue(permission.GroupId, out var group)) - { - _logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); - continue; - } - - var path = $"{group.Name}.{permission.Path}"; - _permissionManager.Set(path, permission.Allow); - _logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); - } - _logger.Information($"{permissions.Count()} group permissions have been loaded."); - - foreach (var chatter in groupChatters) - if (groupsById.TryGetValue(chatter.GroupId, out var group)) - _chatterGroupManager.Add(chatter.ChatterId, group.Name); - _logger.Information($"Users in each group have been loaded."); } private async Task InitializeHermesWebsocket() @@ -317,8 +245,8 @@ namespace TwitchChatTTS { try { - _sevenManager.Initialize(); - await _sevenManager.Connect(); + _seven.Initialize(); + await _seven.Connect(); } catch (Exception e) { @@ -330,8 +258,8 @@ namespace TwitchChatTTS { try { - _obsManager.Initialize(); - await _obsManager.Connect(); + _obs.Initialize(); + await _obs.Connect(); } catch (Exception) { @@ -376,20 +304,19 @@ namespace TwitchChatTTS private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes) { - var emotes = _serviceProvider.GetRequiredService(); var globalEmotes = await sevenapi.FetchGlobalSevenEmotes(); if (channelEmotes != null && channelEmotes.Emotes.Any()) { _logger.Information($"Loaded {channelEmotes.Emotes.Count()} 7tv channel emotes."); foreach (var entry in channelEmotes.Emotes) - emotes.Add(entry.Name, entry.Id); + _emotes.Add(entry.Name, entry.Id); } if (globalEmotes != null && globalEmotes.Any()) { _logger.Information($"Loaded {globalEmotes.Count()} 7tv global emotes."); foreach (var entry in globalEmotes) - emotes.Add(entry.Name, entry.Id); + _emotes.Add(entry.Name, entry.Id); } } } diff --git a/Twitch/Redemptions/RedemptionManager.cs b/Twitch/Redemptions/RedemptionManager.cs index 9100811..f1f2158 100644 --- a/Twitch/Redemptions/RedemptionManager.cs +++ b/Twitch/Redemptions/RedemptionManager.cs @@ -6,8 +6,8 @@ using Microsoft.Extensions.DependencyInjection; using org.mariuszgromada.math.mxparser; using Serilog; using TwitchChatTTS.Hermes.Socket; +using TwitchChatTTS.OBS.Socket; using TwitchChatTTS.OBS.Socket.Data; -using TwitchChatTTS.OBS.Socket.Manager; namespace TwitchChatTTS.Twitch.Redemptions { @@ -15,7 +15,7 @@ namespace TwitchChatTTS.Twitch.Redemptions { private readonly IDictionary> _store; private readonly User _user; - private readonly OBSManager _obsManager; + private readonly OBSSocketClient _obs; private readonly HermesSocketClient _hermes; private readonly ILogger _logger; private readonly Random _random; @@ -24,13 +24,13 @@ namespace TwitchChatTTS.Twitch.Redemptions public RedemptionManager( User user, - OBSManager obsManager, + [FromKeyedServices("obs")] SocketClient obs, [FromKeyedServices("hermes")] SocketClient hermes, ILogger logger) { _store = new Dictionary>(); _user = user; - _obsManager = obsManager; + _obs = (obs as OBSSocketClient)!; _hermes = (hermes as HermesSocketClient)!; _logger = logger; _random = new Random(); @@ -72,7 +72,7 @@ namespace TwitchChatTTS.Twitch.Redemptions break; case "OBS_TRANSFORM": var type = typeof(OBSTransformationData); - await _obsManager.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) => + await _obs.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) => { string[] properties = ["rotation", "position_x", "position_y"]; foreach (var property in properties) @@ -111,13 +111,13 @@ namespace TwitchChatTTS.Twitch.Redemptions }); break; case "TOGGLE_OBS_VISIBILITY": - await _obsManager.ToggleSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"]); + await _obs.ToggleSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"]); break; case "SPECIFIC_OBS_VISIBILITY": - await _obsManager.UpdateSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"], action.Data["obs_visible"].ToLower() == "true"); + await _obs.UpdateSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"], action.Data["obs_visible"].ToLower() == "true"); break; case "SPECIFIC_OBS_INDEX": - await _obsManager.UpdateSceneItemIndex(action.Data["scene_name"], action.Data["scene_item_name"], int.Parse(action.Data["obs_index"])); + await _obs.UpdateSceneItemIndex(action.Data["scene_name"], action.Data["scene_item_name"], int.Parse(action.Data["obs_index"])); break; case "SLEEP": _logger.Debug("Sleeping on thread due to redemption for OBS."); diff --git a/User.cs b/User.cs index 96ffd9d..b1ff599 100644 --- a/User.cs +++ b/User.cs @@ -31,10 +31,6 @@ namespace TwitchChatTTS private HashSet _voicesEnabled; - public User() - { - } - private Regex? GenerateEnabledVoicesRegex() { if (VoicesAvailable == null || VoicesAvailable.Count() <= 0)