Fixed command permissions. Moved to using Twitch's EventSub via websockets. Cleaned some code up. Added detection for subscription messages (no TTS), message deletion, full or partial chat clear. Removes messages from TTS queue if applicable. Added command aliases for static parameters. Word filters use compiled regex if possible. Fixed TTS voice deletion.

This commit is contained in:
Tom
2024-08-04 23:46:10 +00:00
parent 472bfcee5d
commit 75fcb8e0f8
61 changed files with 2268 additions and 925 deletions

View File

@ -1,17 +1,18 @@
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public interface IChatCommand {
public interface IChatCommand
{
string Name { get; }
void Build(ICommandBuilder builder);
}
public interface IChatPartialCommand {
public interface IChatPartialCommand
{
bool AcceptCustomPermission { get; }
bool CheckDefaultPermissions(ChatMessage message);
Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client);
Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client);
}
}

View File

@ -1,5 +1,6 @@
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Commands
{
@ -8,15 +9,18 @@ namespace TwitchChatTTS.Chat.Commands
public interface ICommandBuilder
{
ICommandSelector Build();
ICommandBuilder AddPermission(string path);
ICommandBuilder AddAlias(string alias, string child);
void Clear();
ICommandBuilder CreateCommandTree(string name, Action<ICommandBuilder> callback);
ICommandBuilder CreateCommand(IChatPartialCommand command);
ICommandBuilder CreateStaticInputParameter(string value, Action<ICommandBuilder> callback, bool optional = false);
ICommandBuilder CreateMentionParameter(string name, bool enabled, bool optional = false);
ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false);
ICommandBuilder CreateStateParameter(string name, bool optional = false);
ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false);
ICommandBuilder CreateVoiceNameParameter(string name, bool enabled, bool optional = false);
}
public sealed class CommandBuilder : ICommandBuilder
@ -37,6 +41,25 @@ namespace TwitchChatTTS.Chat.Commands
}
public ICommandBuilder AddPermission(string path)
{
if (_current == _root)
throw new Exception("Cannot add permissions without a command name.");
_current.AddPermission(path);
return this;
}
public ICommandBuilder AddAlias(string alias, string child) {
if (_current == _root)
throw new Exception("Cannot add aliases without a command name.");
if (_current.Children == null || !_current.Children.Any())
throw new Exception("Cannot add alias if this has no parameter.");
_current.AddAlias(alias, child);
return this;
}
public ICommandSelector Build()
{
return new CommandSelector(_root);
@ -89,6 +112,19 @@ namespace TwitchChatTTS.Chat.Commands
return this;
}
public ICommandBuilder CreateMentionParameter(string name, bool enabled, bool optional = false)
{
if (_root == _current)
throw new Exception("Cannot create a parameter without a command name.");
if (optional && _current.IsRequired() && _current.Command == null)
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
var node = _current.CreateUserInput(new MentionParameter(name, optional));
_logger.Debug($"Creating obs transformation parameter '{name}'");
_current = node;
return this;
}
public ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false)
{
if (_root == _current)
@ -164,9 +200,8 @@ namespace TwitchChatTTS.Chat.Commands
public interface ICommandSelector
{
CommandSelectorResult GetBestMatch(string[] args);
CommandSelectorResult GetBestMatch(string[] args, ChannelChatMessage message);
IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path);
CommandValidationResult Validate(string[] args, string path);
}
public sealed class CommandSelector : ICommandSelector
@ -178,67 +213,36 @@ namespace TwitchChatTTS.Chat.Commands
_root = root;
}
public CommandSelectorResult GetBestMatch(string[] args)
public CommandSelectorResult GetBestMatch(string[] args, ChannelChatMessage message)
{
return GetBestMatch(_root, args, null, string.Empty);
return GetBestMatch(_root, message, args, null, string.Empty, null);
}
private CommandSelectorResult GetBestMatch(CommandNode node, IEnumerable<string> args, IChatPartialCommand? match, string path)
private CommandSelectorResult GetBestMatch(CommandNode node, ChannelChatMessage message, IEnumerable<string> args, IChatPartialCommand? match, string path, string[]? permissions)
{
if (node == null || !args.Any())
return new CommandSelectorResult(match, path);
return new CommandSelectorResult(match, path, permissions);
if (!node.Children.Any())
return new CommandSelectorResult(node.Command ?? match, path);
return new CommandSelectorResult(node.Command ?? match, path, permissions);
var argument = args.First();
var argumentLower = argument.ToLower();
foreach (var child in node.Children)
{
var perms = child.Permissions != null ? (permissions ?? []).Union(child.Permissions).Distinct().ToArray() : permissions;
if (child.Parameter.GetType() == typeof(StaticParameter))
{
if (child.Parameter.Name.ToLower() == argumentLower)
{
return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + child.Parameter.Name.ToLower());
}
return GetBestMatch(child, message, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + child.Parameter.Name.ToLower(), perms);
continue;
}
return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + "*");
if ((!child.Parameter.Optional || child.Parameter.Validate(argument, message)) && child.Command != null)
return GetBestMatch(child, message, args.Skip(1), child.Command, (path.Length == 0 ? string.Empty : path + ".") + "*", perms);
if (!child.Parameter.Optional)
return GetBestMatch(child, message, args.Skip(1), match, (path.Length == 0 ? string.Empty : path + ".") + "*", permissions);
}
return new CommandSelectorResult(match, path);
}
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);
return new CommandSelectorResult(match, path, permissions);
}
public IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path)
@ -276,11 +280,13 @@ namespace TwitchChatTTS.Chat.Commands
{
public IChatPartialCommand? Command { get; set; }
public string Path { get; set; }
public string[]? Permissions { get; set; }
public CommandSelectorResult(IChatPartialCommand? command, string path)
public CommandSelectorResult(IChatPartialCommand? command, string path, string[]? permissions)
{
Command = command;
Path = path;
Permissions = permissions;
}
}
@ -300,6 +306,7 @@ namespace TwitchChatTTS.Chat.Commands
{
public IChatPartialCommand? Command { get; private set; }
public CommandParameter Parameter { get; }
public string[]? Permissions { get; private set; }
public IList<CommandNode> Children { get => _children.AsReadOnly(); }
private IList<CommandNode> _children;
@ -308,9 +315,34 @@ namespace TwitchChatTTS.Chat.Commands
{
Parameter = parameter;
_children = new List<CommandNode>();
Permissions = null;
}
public void AddPermission(string path)
{
if (Permissions == null)
Permissions = [path];
else
Permissions = Permissions.Union([path]).ToArray();
}
public CommandNode AddAlias(string alias, string child) {
var target = _children.FirstOrDefault(c => c.Parameter.Name == child);
if (target == null)
throw new Exception($"Cannot find child parameter [parameter: {child}][alias: {alias}]");
if (target.Parameter.GetType() != typeof(StaticParameter))
throw new Exception("Command aliases can only be used on static parameters.");
if (Children.FirstOrDefault(n => n.Parameter.Name == alias) != null)
throw new Exception("Failed to create a command alias - name is already in use.");
var clone = target.MemberwiseClone() as CommandNode;
var node = new CommandNode(new StaticParameter(alias, alias, target.Parameter.Optional));
node._children = target._children;
_children.Add(node);
return this;
}
public CommandNode CreateCommand(IChatPartialCommand command)
{
if (Command != null)

View File

@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
@ -44,7 +44,7 @@ namespace TwitchChatTTS.Chat.Commands
}
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> groups)
public async Task<ChatCommandResult> Execute(string arg, ChannelChatMessage message, IEnumerable<string> groups)
{
if (string.IsNullOrWhiteSpace(arg))
return ChatCommandResult.Unknown;
@ -62,7 +62,7 @@ namespace TwitchChatTTS.Chat.Commands
string[] args = parts.ToArray();
string com = args.First().ToLower();
CommandSelectorResult selectorResult = _commandSelector.GetBestMatch(args);
CommandSelectorResult selectorResult = _commandSelector.GetBestMatch(args, message);
if (selectorResult.Command == null)
{
_logger.Warning($"Could not match '{arg}' to any command.");
@ -71,31 +71,27 @@ namespace TwitchChatTTS.Chat.Commands
// Check if command can be executed by this chatter.
var command = selectorResult.Command;
long chatterId = long.Parse(message.UserId);
long chatterId = long.Parse(message.ChatterUserId);
if (chatterId != _user.OwnerId)
{
var executable = command.AcceptCustomPermission ? CanExecute(chatterId, groups, com) : null;
if (executable == false)
bool executable = command.AcceptCustomPermission ? CanExecute(chatterId, groups, $"tts.command.{com}", selectorResult.Permissions) : false;
if (!executable)
{
_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.
// Check if the arguments are valid.
var arguments = _commandSelector.GetNonStaticArguments(args, selectorResult.Path);
foreach (var entry in arguments)
{
var parameter = entry.Value;
var argument = entry.Key;
if (!parameter.Validate(argument))
// Optional parameters were validated while fetching this command.
if (!parameter.Optional && !parameter.Validate(argument, message))
{
_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}]");
_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.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
return ChatCommandResult.Syntax;
}
}
@ -107,18 +103,18 @@ namespace TwitchChatTTS.Chat.Commands
}
catch (Exception e)
{
_logger.Error(e, $"Command '{arg}' failed [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
_logger.Error(e, $"Command '{arg}' failed [args: {arg}][command type: {command.GetType().Name}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
return ChatCommandResult.Fail;
}
_logger.Information($"Executed the {com} command [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
_logger.Information($"Executed the {com} command [args: {arg}][command type: {command.GetType().Name}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
return ChatCommandResult.Success;
}
private bool? CanExecute(long chatterId, IEnumerable<string> groups, string path)
private bool CanExecute(long chatterId, IEnumerable<string> groups, string path, string[]? additionalPaths)
{
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]");
return _permissionManager.CheckIfAllowed(groups, path);
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]{(additionalPaths != null ? "[paths: " + string.Join('|', additionalPaths) + "]" : string.Empty)}");
return _permissionManager.CheckIfAllowed(groups, path) != false && (additionalPaths == null || additionalPaths.All(p => _permissionManager.CheckIfAllowed(groups, p) != false));
}
}
}

View File

@ -0,0 +1,88 @@
namespace TwitchChatTTS.Chat.Commands.Limits
{
public interface ICommandLimitManager
{
bool HasReachedLimit(long chatterId, string name, string group);
void RemoveUsageLimit(string name, string group);
void SetUsageLimit(int count, TimeSpan span, string name, string group);
bool TryUse(long chatterId, string name, string group);
}
public class CommandLimitManager : ICommandLimitManager
{
// group + name -> chatter id -> usage
private readonly IDictionary<string, IDictionary<long, Usage>> _usages;
// group + name -> limit
private readonly IDictionary<string, Limit> _limits;
public CommandLimitManager()
{
_usages = new Dictionary<string, IDictionary<long, Usage>>();
_limits = new Dictionary<string, Limit>();
}
public bool HasReachedLimit(long chatterId, string name, string group)
{
throw new NotImplementedException();
}
public void RemoveUsageLimit(string name, string group)
{
throw new NotImplementedException();
}
public void SetUsageLimit(int count, TimeSpan span, string name, string group)
{
throw new NotImplementedException();
}
public bool TryUse(long chatterId, string name, string group)
{
var path = $"{group}.{name}";
if (!_limits.TryGetValue(path, out var limit))
return true;
if (!_usages.TryGetValue(path, out var groupUsage))
{
groupUsage = new Dictionary<long, Usage>();
_usages.Add(path, groupUsage);
}
if (!groupUsage.TryGetValue(chatterId, out var usage))
{
usage = new Usage()
{
Usages = new long[limit.Count],
Index = 0
};
groupUsage.Add(chatterId, usage);
}
int first = (usage.Index + 1) % limit.Count;
long timestamp = DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond;
if (timestamp - usage.Usages[first] < limit.Span)
{
return false;
}
usage.Usages[usage.Index] = timestamp;
usage.Index = first;
return true;
}
private class Usage
{
public long[] Usages { get; set; }
public int Index { get; set; }
}
private struct Limit
{
public int Count { get; set; }
public int Span { get; set; }
}
}
}

View File

@ -5,7 +5,7 @@ using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchLib.Client.Models;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
@ -71,12 +71,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
string sceneName = values["sceneName"];
string sourceName = values["sourceName"];
@ -102,12 +97,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
string sceneName = values["sceneName"];
string sourceName = values["sourceName"];
@ -143,12 +133,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
string sceneName = values["sceneName"];
string sourceName = values["sourceName"];

View File

@ -1,6 +1,8 @@
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public abstract class CommandParameter : ICloneable
public abstract class CommandParameter
{
public string Name { get; }
public bool Optional { get; }
@ -11,10 +13,6 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
Optional = optional;
}
public abstract bool Validate(string value);
public object Clone() {
return (CommandParameter) MemberwiseClone();
}
public abstract bool Validate(string value, ChannelChatMessage message);
}
}

View File

@ -0,0 +1,16 @@
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class MentionParameter : CommandParameter
{
public MentionParameter(string name, bool optional = false) : base(name, optional)
{
}
public override bool Validate(string value, ChannelChatMessage message)
{
return value.StartsWith('@') && message.Message.Fragments.Any(f => f.Text == value && f.Mention != null);
}
}
}

View File

@ -1,3 +1,5 @@
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class OBSTransformationParameter : CommandParameter
@ -8,7 +10,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
{
}
public override bool Validate(string value)
public override bool Validate(string value, ChannelChatMessage message)
{
return _values.Contains(value.ToLower());
}

View File

@ -1,3 +1,5 @@
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class StateParameter : CommandParameter
@ -8,7 +10,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
{
}
public override bool Validate(string value)
public override bool Validate(string value, ChannelChatMessage message)
{
return _values.Contains(value.ToLower());
}

View File

@ -1,3 +1,5 @@
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class StaticParameter : CommandParameter
@ -11,7 +13,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
_value = value.ToLower();
}
public override bool Validate(string value)
public override bool Validate(string value, ChannelChatMessage message)
{
return _value == value.ToLower();
}

View File

@ -1,3 +1,5 @@
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class TTSVoiceNameParameter : CommandParameter
@ -11,7 +13,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
_user = user;
}
public override bool Validate(string value)
public override bool Validate(string value, ChannelChatMessage message)
{
if (_user.VoicesAvailable == null)
return false;

View File

@ -1,3 +1,5 @@
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class UnvalidatedParameter : CommandParameter
@ -6,7 +8,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
{
}
public override bool Validate(string value)
public override bool Validate(string value, ChannelChatMessage message)
{
return true;
}

View File

@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket;
using TwitchLib.Client.Models;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
@ -44,12 +44,7 @@ namespace TwitchChatTTS.Chat.Commands
{
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)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
await client.FetchEnabledTTSVoices();
}
@ -59,12 +54,7 @@ namespace TwitchChatTTS.Chat.Commands
{
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)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
await client.FetchTTSWordFilters();
}
@ -74,12 +64,7 @@ namespace TwitchChatTTS.Chat.Commands
{
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)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
await client.FetchTTSChatterVoices();
}
@ -89,12 +74,7 @@ namespace TwitchChatTTS.Chat.Commands
{
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)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
await client.FetchDefaultTTSVoice();
}
@ -104,12 +84,7 @@ namespace TwitchChatTTS.Chat.Commands
{
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)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
await client.FetchRedemptions();
}
@ -127,12 +102,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
_obsManager.ClearCache();
_logger.Information("Cleared the cache used for OBS.");
@ -144,20 +114,10 @@ namespace TwitchChatTTS.Chat.Commands
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)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
await client.FetchPermissions();
}
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
}
}

View File

@ -1,6 +1,6 @@
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
@ -8,11 +8,13 @@ namespace TwitchChatTTS.Chat.Commands
public class SkipCommand : IChatCommand
{
private readonly TTSPlayer _player;
private readonly AudioPlaybackEngine _playback;
private readonly ILogger _logger;
public SkipCommand(TTSPlayer ttsPlayer, ILogger logger)
public SkipCommand(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
{
_player = ttsPlayer;
_player = player;
_playback = playback;
_logger = logger;
}
@ -24,40 +26,38 @@ namespace TwitchChatTTS.Chat.Commands
{
b.CreateStaticInputParameter("all", b =>
{
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _logger));
}).CreateCommand(new TTSPlayerSkipCommand(_player, _logger));
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _playback, _logger));
}).CreateCommand(new TTSPlayerSkipCommand(_player, _playback, _logger));
});
builder.CreateCommandTree("skipall", b => {
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _logger));
builder.CreateCommandTree("skipall", b =>
{
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _playback, _logger));
});
}
private sealed class TTSPlayerSkipCommand : IChatPartialCommand
{
private readonly TTSPlayer _ttsPlayer;
private readonly TTSPlayer _player;
private readonly AudioPlaybackEngine _playback;
private readonly ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public TTSPlayerSkipCommand(TTSPlayer ttsPlayer, ILogger logger)
public TTSPlayerSkipCommand(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
{
_ttsPlayer = ttsPlayer;
_player = player;
_playback = playback;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
return message.IsModerator || message.IsVip || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
if (_ttsPlayer.Playing == null)
if (_player.Playing == null)
return;
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
_ttsPlayer.Playing = null;
_playback.RemoveMixerInput(_player.Playing.Audio!);
_player.Playing = null;
_logger.Information("Skipped current tts.");
}
@ -65,31 +65,28 @@ namespace TwitchChatTTS.Chat.Commands
private sealed class TTSPlayerSkipAllCommand : IChatPartialCommand
{
private readonly TTSPlayer _ttsPlayer;
private readonly TTSPlayer _player;
private readonly AudioPlaybackEngine _playback;
private readonly ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public TTSPlayerSkipAllCommand(TTSPlayer ttsPlayer, ILogger logger)
public TTSPlayerSkipAllCommand(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
{
_ttsPlayer = ttsPlayer;
_player = player;
_playback = playback;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
return message.IsModerator || message.IsVip || message.IsBroadcaster;
}
_player.RemoveAll();
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
_ttsPlayer.RemoveAll();
if (_ttsPlayer.Playing == null)
if (_player.Playing == null)
return;
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
_ttsPlayer.Playing = null;
_playback.RemoveMixerInput(_player.Playing.Audio!);
_player.Playing = null;
_logger.Information("Skipped all queued and playing tts.");
}

View File

@ -1,6 +1,6 @@
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
@ -28,41 +28,30 @@ namespace TwitchChatTTS.Chat.Commands
b.CreateVoiceNameParameter("voiceName", false)
.CreateCommand(new AddTTSVoiceCommand(_user, _logger));
})
.CreateStaticInputParameter("del", b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
})
.AddAlias("insert", "add")
.CreateStaticInputParameter("delete", b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
})
.CreateStaticInputParameter("remove", b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
})
.AddAlias("del", "delete")
.AddAlias("remove", "delete")
.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));
})
.AddAlias("on", "enable")
.AddAlias("enabled", "enable")
.AddAlias("true", "enable")
.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));
});
.AddAlias("off", "disable")
.AddAlias("disabled", "disable")
.AddAlias("false", "disable");
});
}
@ -80,12 +69,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return false;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesAvailable == null)
return;
@ -95,12 +79,12 @@ namespace TwitchChatTTS.Chat.Commands
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
if (exists)
{
_logger.Warning($"Voice already exists [voice: {voiceName}][id: {message.UserId}]");
_logger.Warning($"Voice already exists [voice: {voiceName}][id: {message.ChatterUserId}]");
return;
}
await client.CreateTTSVoice(voiceName);
_logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]");
_logger.Information($"Added a new TTS voice [voice: {voiceName}][creator: {message.ChatterUserLogin}][creator id: {message.ChatterUserId}]");
}
}
@ -117,16 +101,11 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return false;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesAvailable == null)
{
_logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]");
_logger.Warning($"Voices available are not loaded [chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
return;
}
@ -135,13 +114,18 @@ namespace TwitchChatTTS.Chat.Commands
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}]");
_logger.Warning($"Voice does not exist [voice: {voiceName}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
return;
}
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceNameLower).Key;
if (voiceId == null) {
_logger.Warning($"Could not find the identifier for the tts voice [voice name: {voiceName}]");
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}]");
_logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
}
}
@ -160,12 +144,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesAvailable == null)
return;
@ -175,7 +154,7 @@ namespace TwitchChatTTS.Chat.Commands
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}]");
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {_state}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]");
}
}
}

View File

@ -1,7 +1,7 @@
using HermesSocketLibrary.Socket.Data;
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
@ -37,12 +37,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
_logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");

View File

@ -1,6 +1,6 @@
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
@ -8,6 +8,8 @@ namespace TwitchChatTTS.Chat.Commands
public class VoiceCommand : IChatCommand
{
private readonly User _user;
// TODO: get permissions
// TODO: validated parameter for username by including '@' and regex for username
private readonly ILogger _logger;
public VoiceCommand(User user, ILogger logger)
@ -23,7 +25,10 @@ namespace TwitchChatTTS.Chat.Commands
builder.CreateCommandTree(Name, b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new TTSVoiceSelector(_user, _logger));
.CreateCommand(new TTSVoiceSelector(_user, _logger))
.CreateUnvalidatedParameter("chatter", optional: true)
.AddPermission("tts.command.voice.admin")
.CreateCommand(new TTSVoiceSelectorAdmin(_user, _logger));
});
}
@ -40,18 +45,12 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesSelected == null)
return;
long chatterId = long.Parse(message.UserId);
long chatterId = long.Parse(message.ChatterUserId);
var voiceName = values["voiceName"];
var voiceNameLower = voiceName.ToLower();
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceNameLower);
@ -59,12 +58,56 @@ namespace TwitchChatTTS.Chat.Commands
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]");
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.ChatterUserLogin}][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]");
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.ChatterUserLogin}][reason: command]");
}
}
}
private sealed class TTSVoiceSelectorAdmin : IChatPartialCommand
{
private readonly User _user;
private readonly ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public TTSVoiceSelectorAdmin(User user, ILogger logger)
{
_user = user;
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesSelected == null)
return;
var chatterLogin = values["chatter"].Substring(1);
var mention = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Mention.UserLogin == chatterLogin)?.Mention;
if (mention == null)
{
_logger.Warning("Failed to find the chatter to apply voice command to.");
return;
}
long chatterId = long.Parse(mention.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: {mention.UserLogin}][reason: command]");
}
else
{
await client.CreateTTSUser(chatterId, voice.Key);
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {mention.UserLogin}][reason: command]");
}
}
}