Changed command dictionary to a command tree. Fixed various requests. OBS reconnection added if identified previously.

This commit is contained in:
Tom
2024-07-19 16:56:41 +00:00
parent e6b3819356
commit 472bfcee5d
56 changed files with 1943 additions and 1553 deletions

View File

@ -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<WebSocketMessage> hermes,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> 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;

View File

@ -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<bool> CheckDefaultPermissions(ChatMessage message)
{
return false;
}
public override async Task Execute(IList<string> 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}]");
}
}
}

View File

@ -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<ChatCommandParameter> Parameters { get => _parameters.AsReadOnly(); }
public bool DefaultPermissionsOverwrite { get; }
public interface IChatCommand {
string Name { get; }
void Build(ICommandBuilder builder);
}
private IList<ChatCommandParameter> _parameters;
public ChatCommand(string name, string description)
{
Name = name;
Description = description;
DefaultPermissionsOverwrite = false;
_parameters = new List<ChatCommandParameter>();
}
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<bool> CheckDefaultPermissions(ChatMessage message);
public abstract Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client);
public interface IChatPartialCommand {
bool AcceptCustomPermission { get; }
bool CheckDefaultPermissions(ChatMessage message);
Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client);
}
}

View File

@ -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<string, ChatCommand> _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<WebSocketMessage> socketClient,
IGroupPermissionManager permissionManager,
IServiceProvider serviceProvider,
ILogger logger
)
{
_user = user;
_hermes = (socketClient as HermesSocketClient)!;
_permissionManager = permissionManager;
_serviceProvider = serviceProvider;
_logger = logger;
_commands = new Dictionary<string, ChatCommand>();
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<ChatCommand>(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<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> groups)
{
if (string.IsNullOrWhiteSpace(arg))
return ChatCommandResult.Unknown;
arg = arg.Trim();
if (!arg.StartsWith(CommandStartSign))
return ChatCommandResult.Unknown;
string[] parts = Regex.Matches(arg, "(?<match>[^\"\\n\\s]+|\"[^\"\\n]*\")")
.Cast<Match>()
.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<string> groups, string path)
{
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]");
return _permissionManager.CheckIfAllowed(groups, path);
}
}
}

View File

@ -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<ICommandBuilder> callback);
ICommandBuilder CreateCommand(IChatPartialCommand command);
ICommandBuilder CreateStaticInputParameter(string value, Action<ICommandBuilder> 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<CommandNode> _stack;
private readonly User _user;
private readonly ILogger _logger;
public CommandBuilder(User user, ILogger logger)
{
_user = user;
_logger = logger;
_stack = new Stack<CommandNode>();
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<ICommandBuilder> 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<ICommandBuilder> 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<string, CommandParameter> 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<string> 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<string, CommandParameter> GetNonStaticArguments(string[] args, string path)
{
Dictionary<string, CommandParameter> arguments = new Dictionary<string, CommandParameter>();
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<CommandNode> Children { get => _children.AsReadOnly(); }
private IList<CommandNode> _children;
public CommandNode(CommandParameter parameter)
{
Parameter = parameter;
_children = new List<CommandNode>();
}
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<CommandNode> 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;
}
}
}
}

View File

@ -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<IChatCommand> commands,
ICommandBuilder commandBuilder,
User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> 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<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> 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), "(?<match>[^\"\\n\\s]+|\"[^\"\\n]*\")")
.Cast<Match>()
.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<string> groups, string path)
{
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]");
return _permissionManager.CheckIfAllowed(groups, path);
}
}
}

View File

@ -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<WebSocketMessage> 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<bool> CheckDefaultPermissions(ChatMessage message)
public void Build(ICommandBuilder builder)
{
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> 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<string, object>() { { "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<string, object>() { { "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<WebSocketMessage> obs,
ILogger logger
)
{
_obs = (obs as OBSSocketClient)!;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> 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<string, object>() { { "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<WebSocketMessage> obs,
ILogger logger
)
{
_obs = (obs as OBSSocketClient)!;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> 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<WebSocketMessage> obs,
ILogger logger
)
{
_obs = (obs as OBSSocketClient)!;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> 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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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)
{
}

View File

@ -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<WebSocketMessage> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> values, ChatMessage message, HermesSocketClient client)
{
await client.FetchPermissions();
}
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
}
}

View File

@ -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<bool> CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> 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<string>(["Brian"]);
else
_user.VoicesEnabled = new HashSet<string>(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;
}
}
}
}

View File

@ -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<bool> CheckDefaultPermissions(ChatMessage message)
{
return false;
}
public override async Task Execute(IList<string> 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}]");
}
}
}

View File

@ -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<bool> CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsVip || message.IsBroadcaster;
}
public override async Task Execute(IList<string> 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.");
}
}
}

View File

@ -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<bool> 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<string> 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<string, string> 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<string, string> 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.");
}
}
}
}

View File

@ -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<bool> 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<string> 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<string, string> 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<string, string> 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<string, string> 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}]");
}
}
}
}

View File

@ -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<bool> 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<string> 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<string, string> 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}.");
}
}
}
}

View File

@ -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<bool> 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<string> 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<string, string> 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]");
}
}
}
}

View File

@ -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<string> 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) {

View File

@ -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; }
}
}

View File

@ -1,8 +0,0 @@
namespace TwitchChatTTS.Chat.Groups
{
public class GroupChatter
{
public string GroupId { get; set; }
public long ChatterId { get; set;}
}
}

View File

@ -1,3 +1,5 @@
using HermesSocketLibrary.Requests.Messages;
namespace TwitchChatTTS.Chat.Groups
{
public interface IChatterGroupManager

View File

@ -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; }
}
}

View File

@ -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())