Changed command dictionary to a command tree. Fixed various requests. OBS reconnection added if identified previously.
This commit is contained in:
@ -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;
|
||||
|
||||
|
@ -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}]");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
356
Chat/Commands/CommandBuilder.cs
Normal file
356
Chat/Commands/CommandBuilder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
124
Chat/Commands/CommandManager.cs
Normal file
124
Chat/Commands/CommandManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
20
Chat/Commands/Parameters/CommandParameter.cs
Normal file
20
Chat/Commands/Parameters/CommandParameter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
16
Chat/Commands/Parameters/OBSTransformationParameter.cs
Normal file
16
Chat/Commands/Parameters/OBSTransformationParameter.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
16
Chat/Commands/Parameters/StateParameter.cs
Normal file
16
Chat/Commands/Parameters/StateParameter.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
19
Chat/Commands/Parameters/StaticParameter.cs
Normal file
19
Chat/Commands/Parameters/StaticParameter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
163
Chat/Commands/RefreshCommand.cs
Normal file
163
Chat/Commands/RefreshCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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}]");
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
namespace TwitchChatTTS.Chat.Groups
|
||||
{
|
||||
public class GroupChatter
|
||||
{
|
||||
public string GroupId { get; set; }
|
||||
public long ChatterId { get; set;}
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Groups
|
||||
{
|
||||
public interface IChatterGroupManager
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
Reference in New Issue
Block a user