using Serilog; using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Twitch.Socket.Messages; namespace TwitchChatTTS.Chat.Commands { public static class TTSCommands { public interface ICommandBuilder { ICommandSelector Build(); ICommandBuilder AddPermission(string path); ICommandBuilder AddAlias(string alias, string child); void Clear(); ICommandBuilder CreateCommandTree(string name, Action callback); ICommandBuilder CreateCommand(IChatPartialCommand command); ICommandBuilder CreateStaticInputParameter(string value, Action callback, bool optional = false); ICommandBuilder CreateMentionParameter(string name, bool enabled, 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 ICommandBuilder AddPermission(string path) { if (_current == _root) throw new Exception("Cannot add permissions without a command name."); _current.AddPermission(path); return this; } public ICommandBuilder AddAlias(string alias, string child) { if (_current == _root) throw new Exception("Cannot add aliases without a command name."); if (_current.Children == null || !_current.Children.Any()) throw new Exception("Cannot add alias if this has no parameter."); _current.AddAlias(alias, child); return this; } 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 CreateMentionParameter(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 MentionParameter(name, optional)); _logger.Debug($"Creating obs transformation parameter '{name}'"); _current = node; 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, ChannelChatMessage message); IDictionary GetNonStaticArguments(string[] args, string path); } public sealed class CommandSelector : ICommandSelector { private CommandNode _root; public CommandSelector(CommandNode root) { _root = root; } public CommandSelectorResult GetBestMatch(string[] args, ChannelChatMessage message) { return GetBestMatch(_root, message, args, null, string.Empty, null); } private CommandSelectorResult GetBestMatch(CommandNode node, ChannelChatMessage message, IEnumerable args, IChatPartialCommand? match, string path, string[]? permissions) { if (node == null || !args.Any()) return new CommandSelectorResult(match, path, permissions); if (!node.Children.Any()) return new CommandSelectorResult(node.Command ?? match, path, permissions); var argument = args.First(); var argumentLower = argument.ToLower(); foreach (var child in node.Children) { var perms = child.Permissions != null ? (permissions ?? []).Union(child.Permissions).Distinct().ToArray() : permissions; if (child.Parameter.GetType() == typeof(StaticParameter)) { if (child.Parameter.Name.ToLower() == argumentLower) return GetBestMatch(child, message, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + child.Parameter.Name.ToLower(), perms); continue; } if ((!child.Parameter.Optional || child.Parameter.Validate(argument, message)) && child.Command != null) return GetBestMatch(child, message, args.Skip(1), child.Command, (path.Length == 0 ? string.Empty : path + ".") + "*", perms); if (!child.Parameter.Optional) return GetBestMatch(child, message, args.Skip(1), match, (path.Length == 0 ? string.Empty : path + ".") + "*", permissions); } return new CommandSelectorResult(match, path, permissions); } 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 string[]? Permissions { get; set; } public CommandSelectorResult(IChatPartialCommand? command, string path, string[]? permissions) { Command = command; Path = path; Permissions = permissions; } } 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 string[]? Permissions { get; private set; } public IList Children { get => _children.AsReadOnly(); } private IList _children; public CommandNode(CommandParameter parameter) { Parameter = parameter; _children = new List(); Permissions = null; } public void AddPermission(string path) { if (Permissions == null) Permissions = [path]; else Permissions = Permissions.Union([path]).ToArray(); } public CommandNode AddAlias(string alias, string child) { var target = _children.FirstOrDefault(c => c.Parameter.Name == child); if (target == null) throw new Exception($"Cannot find child parameter [parameter: {child}][alias: {alias}]"); if (target.Parameter.GetType() != typeof(StaticParameter)) throw new Exception("Command aliases can only be used on static parameters."); if (Children.FirstOrDefault(n => n.Parameter.Name == alias) != null) throw new Exception("Failed to create a command alias - name is already in use."); var clone = target.MemberwiseClone() as CommandNode; var node = new CommandNode(new StaticParameter(alias, alias, target.Parameter.Optional)); node._children = target._children; _children.Add(node); return this; } 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; } } } }