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

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

View File

@ -6,22 +6,22 @@ using TwitchChatTTS.Chat.Commands;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchChatTTS.Chat.Emotes;
using Microsoft.Extensions.DependencyInjection;
using CommonSocketLibrary.Common;
using CommonSocketLibrary.Abstract;
using TwitchChatTTS.OBS.Socket;
public class ChatMessageHandler
{
private readonly User _user;
private readonly TTSPlayer _player;
private readonly ChatCommandManager _commands;
private readonly CommandManager _commands;
private readonly IGroupPermissionManager _permissionManager;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly IEmoteDatabase _emotes;
private readonly OBSManager _obsManager;
private readonly OBSSocketClient _obs;
private readonly HermesSocketClient _hermes;
private readonly Configuration _configuration;
@ -36,12 +36,12 @@ public class ChatMessageHandler
public ChatMessageHandler(
User user,
TTSPlayer player,
ChatCommandManager commands,
CommandManager commands,
IGroupPermissionManager permissionManager,
IChatterGroupManager chatterGroupManager,
IEmoteDatabase emotes,
OBSManager obsManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
Configuration configuration,
ILogger logger
)
@ -52,7 +52,7 @@ public class ChatMessageHandler
_permissionManager = permissionManager;
_chatterGroupManager = chatterGroupManager;
_emotes = emotes;
_obsManager = obsManager;
_obs = (obs as OBSSocketClient)!;
_hermes = (hermes as HermesSocketClient)!;
_configuration = configuration;
_logger = logger;
@ -71,7 +71,7 @@ public class ChatMessageHandler
_logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]");
return new MessageResult(MessageStatus.NotReady, -1, -1);
}
if (_configuration.Twitch?.TtsWhenOffline != true && !_obsManager.Streaming)
if (_configuration.Twitch?.TtsWhenOffline != true && !_obs.Streaming)
{
_logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {m.Id}]");
return new MessageResult(MessageStatus.NotReady, -1, -1);
@ -109,7 +109,7 @@ public class ChatMessageHandler
return new MessageResult(MessageStatus.Blocked, -1, -1);
}
if (_obsManager.Streaming && !_chatters.Contains(chatterId))
if (_obs.Streaming && !_chatters.Contains(chatterId))
{
tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username));
_chatters.Add(chatterId);
@ -148,7 +148,7 @@ public class ChatMessageHandler
if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
filteredMsg += w + " ";
}
if (_obsManager.Streaming && newEmotes.Any())
if (_obs.Streaming && newEmotes.Any())
tasks.Add(_hermes.SendEmoteDetails(newEmotes));
msg = filteredMsg;

View File

@ -1,50 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class AddTTSVoiceCommand : ChatCommand
{
private readonly User _user;
private readonly ILogger _logger;
public new bool DefaultPermissionsOverwrite { get => true; }
public AddTTSVoiceCommand(
User user,
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
ILogger logger
) : base("addttsvoice", "Select a TTS voice as the default for that user.")
{
_user = user;
_logger = logger;
AddParameter(unvalidatedParameter);
}
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{
return false;
}
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesAvailable == null)
return;
var voiceName = args.First();
var voiceNameLower = voiceName.ToLower();
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
if (exists) {
_logger.Information("Voice already exists.");
return;
}
await client.CreateTTSVoice(voiceName);
_logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]");
}
}
}

View File

@ -1,34 +1,17 @@
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public abstract class ChatCommand
{
public string Name { get; }
public string Description { get; }
public IList<ChatCommandParameter> Parameters { get => _parameters.AsReadOnly(); }
public bool DefaultPermissionsOverwrite { get; }
private IList<ChatCommandParameter> _parameters;
public ChatCommand(string name, string description)
{
Name = name;
Description = description;
DefaultPermissionsOverwrite = false;
_parameters = new List<ChatCommandParameter>();
public interface IChatCommand {
string Name { get; }
void Build(ICommandBuilder builder);
}
protected void AddParameter(ChatCommandParameter parameter, bool optional = false)
{
if (parameter != null && parameter.Clone() is ChatCommandParameter p) {
_parameters.Add(optional ? p.Permissive() : p);
}
}
public abstract Task<bool> CheckDefaultPermissions(ChatMessage message);
public abstract Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client);
public interface IChatPartialCommand {
bool AcceptCustomPermission { get; }
bool CheckDefaultPermissions(ChatMessage message);
Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client);
}
}

View File

@ -1,149 +0,0 @@
using System.Text.RegularExpressions;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class ChatCommandManager
{
private IDictionary<string, ChatCommand> _commands;
private readonly User _user;
private readonly HermesSocketClient _hermes;
private readonly IGroupPermissionManager _permissionManager;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
private string CommandStartSign { get; } = "!";
public ChatCommandManager(
User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> socketClient,
IGroupPermissionManager permissionManager,
IServiceProvider serviceProvider,
ILogger logger
)
{
_user = user;
_hermes = (socketClient as HermesSocketClient)!;
_permissionManager = permissionManager;
_serviceProvider = serviceProvider;
_logger = logger;
_commands = new Dictionary<string, ChatCommand>();
GenerateCommands();
}
private void Add(ChatCommand command)
{
_commands.Add(command.Name.ToLower(), command);
}
private void GenerateCommands()
{
var basetype = typeof(ChatCommand);
var assembly = GetType().Assembly;
var types = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Chat.") == true);
foreach (var type in types)
{
var key = "command-" + type.Name.Replace("Commands", "Comm#ands")
.Replace("Command", "")
.Replace("Comm#ands", "Commands")
.ToLower();
var command = _serviceProvider.GetKeyedService<ChatCommand>(key);
if (command == null)
{
_logger.Error("Failed to add chat command: " + type.AssemblyQualifiedName);
continue;
}
_logger.Debug($"Added chat command {type.AssemblyQualifiedName}");
Add(command);
}
}
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> groups)
{
if (string.IsNullOrWhiteSpace(arg))
return ChatCommandResult.Unknown;
arg = arg.Trim();
if (!arg.StartsWith(CommandStartSign))
return ChatCommandResult.Unknown;
string[] parts = Regex.Matches(arg, "(?<match>[^\"\\n\\s]+|\"[^\"\\n]*\")")
.Cast<Match>()
.Select(m => m.Groups["match"].Value)
.Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m)
.ToArray();
string com = parts.First().Substring(CommandStartSign.Length).ToLower();
string[] args = parts.Skip(1).ToArray();
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null)
{
// Could be for another bot or just misspelled.
_logger.Debug($"Failed to find command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Missing;
}
// Check if command can be executed by this chatter.
long chatterId = long.Parse(message.UserId);
if (chatterId != _user.OwnerId)
{
var executable = command.DefaultPermissionsOverwrite ? false : CanExecute(chatterId, groups, com);
if (executable == false)
{
_logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]");
return ChatCommandResult.Permission;
}
else if (executable == null && !await command.CheckDefaultPermissions(message))
{
_logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Permission;
}
}
// Check if the syntax is correct.
if (command.Parameters.Count(p => !p.Optional) > args.Length)
{
_logger.Debug($"Command syntax issue when executing command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Syntax;
}
for (int i = 0; i < Math.Min(args.Length, command.Parameters.Count); i++)
{
if (!command.Parameters[i].Validate(args[i]))
{
_logger.Warning($"Commmand '{com}' failed because of the #{i + 1} argument. Invalid value: {args[i]}");
return ChatCommandResult.Syntax;
}
}
try
{
await command.Execute(args, message, _hermes);
}
catch (Exception e)
{
_logger.Error(e, $"Command '{arg}' failed [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Fail;
}
_logger.Information($"Executed the {com} command [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Success;
}
private bool? CanExecute(long chatterId, IEnumerable<string> groups, string path)
{
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]");
return _permissionManager.CheckIfAllowed(groups, path);
}
}
}

View File

@ -0,0 +1,356 @@
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
namespace TwitchChatTTS.Chat.Commands
{
public static class TTSCommands
{
public interface ICommandBuilder
{
ICommandSelector Build();
void Clear();
ICommandBuilder CreateCommandTree(string name, Action<ICommandBuilder> callback);
ICommandBuilder CreateCommand(IChatPartialCommand command);
ICommandBuilder CreateStaticInputParameter(string value, Action<ICommandBuilder> callback, bool optional = false);
ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false);
ICommandBuilder CreateStateParameter(string name, bool optional = false);
ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false);
ICommandBuilder CreateVoiceNameParameter(string name, bool enabled, bool optional = false);
}
public sealed class CommandBuilder : ICommandBuilder
{
private CommandNode _root;
private CommandNode _current;
private Stack<CommandNode> _stack;
private readonly User _user;
private readonly ILogger _logger;
public CommandBuilder(User user, ILogger logger)
{
_user = user;
_logger = logger;
_stack = new Stack<CommandNode>();
Clear();
}
public ICommandSelector Build()
{
return new CommandSelector(_root);
}
public void Clear()
{
_root = new CommandNode(new StaticParameter("root", "root"));
ResetToRoot();
}
public ICommandBuilder CreateCommandTree(string name, Action<ICommandBuilder> callback)
{
ResetToRoot();
var node = _current.CreateStaticInput(name);
_logger.Debug($"Creating command name '{name}'");
CreateStack(() =>
{
_current = node;
callback(this);
});
return this;
}
public ICommandBuilder CreateCommand(IChatPartialCommand command)
{
if (_root == _current)
throw new Exception("Cannot create a command without a command name.");
_current.CreateCommand(command);
_logger.Debug($"Set command to '{command.GetType().Name}'");
return this;
}
public ICommandBuilder CreateStaticInputParameter(string value, Action<ICommandBuilder> callback, bool optional = false)
{
if (_root == _current)
throw new Exception("Cannot create a parameter without a command name.");
if (optional && _current.IsRequired() && _current.Command == null)
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
var node = _current.CreateStaticInput(value, optional);
_logger.Debug($"Creating static parameter '{value}'");
CreateStack(() =>
{
_current = node;
callback(this);
});
return this;
}
public ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false)
{
if (_root == _current)
throw new Exception("Cannot create a parameter without a command name.");
if (optional && _current.IsRequired() && _current.Command == null)
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
var node = _current.CreateUserInput(new OBSTransformationParameter(name, optional));
_logger.Debug($"Creating obs transformation parameter '{name}'");
_current = node;
return this;
}
public ICommandBuilder CreateStateParameter(string name, bool optional = false)
{
if (_root == _current)
throw new Exception("Cannot create a parameter without a command name.");
if (optional && _current.IsRequired() && _current.Command == null)
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
var node = _current.CreateUserInput(new StateParameter(name, optional));
_logger.Debug($"Creating unvalidated parameter '{name}'");
_current = node;
return this;
}
public ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false)
{
if (_root == _current)
throw new Exception("Cannot create a parameter without a command name.");
if (optional && _current.IsRequired() && _current.Command == null)
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
var node = _current.CreateUserInput(new UnvalidatedParameter(name, optional));
_logger.Debug($"Creating unvalidated parameter '{name}'");
_current = node;
return this;
}
public ICommandBuilder CreateVoiceNameParameter(string name, bool enabled, bool optional = false)
{
if (_root == _current)
throw new Exception("Cannot create a parameter without a command name.");
if (optional && _current.IsRequired() && _current.Command == null)
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
var node = _current.CreateUserInput(new TTSVoiceNameParameter(name, enabled, _user, optional));
_logger.Debug($"Creating tts voice name parameter '{name}'");
_current = node;
return this;
}
private ICommandBuilder ResetToRoot()
{
_current = _root;
_stack.Clear();
return this;
}
private void CreateStack(Action func)
{
try
{
_stack.Push(_current);
func();
}
finally
{
_current = _stack.Pop();
}
}
}
public interface ICommandSelector
{
CommandSelectorResult GetBestMatch(string[] args);
IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path);
CommandValidationResult Validate(string[] args, string path);
}
public sealed class CommandSelector : ICommandSelector
{
private CommandNode _root;
public CommandSelector(CommandNode root)
{
_root = root;
}
public CommandSelectorResult GetBestMatch(string[] args)
{
return GetBestMatch(_root, args, null, string.Empty);
}
private CommandSelectorResult GetBestMatch(CommandNode node, IEnumerable<string> args, IChatPartialCommand? match, string path)
{
if (node == null || !args.Any())
return new CommandSelectorResult(match, path);
if (!node.Children.Any())
return new CommandSelectorResult(node.Command ?? match, path);
var argument = args.First();
var argumentLower = argument.ToLower();
foreach (var child in node.Children)
{
if (child.Parameter.GetType() == typeof(StaticParameter))
{
if (child.Parameter.Name.ToLower() == argumentLower)
{
return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + child.Parameter.Name.ToLower());
}
continue;
}
return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + "*");
}
return new CommandSelectorResult(match, path);
}
public CommandValidationResult Validate(string[] args, string path)
{
CommandNode? current = _root;
var parts = path.Split('.');
if (args.Length < parts.Length)
throw new Exception($"Command path too long for the number of arguments passed in [path: {path}][parts: {parts.Length}][args count: {args.Length}]");
for (var i = 0; i < parts.Length; i++)
{
var part = parts[i];
if (part == "*")
{
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() != typeof(StaticParameter));
if (current == null)
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
if (!current.Parameter.Validate(args[i]))
{
return new CommandValidationResult(false, args[i]);
}
}
else
{
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() == typeof(StaticParameter) && n.Parameter.Name == part);
if (current == null)
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
}
}
return new CommandValidationResult(true, null);
}
public IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path)
{
Dictionary<string, CommandParameter> arguments = new Dictionary<string, CommandParameter>();
CommandNode? current = _root;
var parts = path.Split('.');
if (args.Length < parts.Length)
throw new Exception($"Command path too long for the number of arguments passed in [path: {path}][parts: {parts.Length}][args count: {args.Length}]");
for (var i = 0; i < parts.Length; i++)
{
var part = parts[i];
if (part == "*")
{
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() != typeof(StaticParameter));
if (current == null)
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
arguments.Add(args[i], current.Parameter);
}
else
{
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() == typeof(StaticParameter) && n.Parameter.Name == part);
if (current == null)
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
}
}
return arguments;
}
}
public class CommandSelectorResult
{
public IChatPartialCommand? Command { get; set; }
public string Path { get; set; }
public CommandSelectorResult(IChatPartialCommand? command, string path)
{
Command = command;
Path = path;
}
}
public class CommandValidationResult
{
public bool Result { get; set; }
public string? ErrorParameterName { get; set; }
public CommandValidationResult(bool result, string? parameterName)
{
Result = result;
ErrorParameterName = parameterName;
}
}
public sealed class CommandNode
{
public IChatPartialCommand? Command { get; private set; }
public CommandParameter Parameter { get; }
public IList<CommandNode> Children { get => _children.AsReadOnly(); }
private IList<CommandNode> _children;
public CommandNode(CommandParameter parameter)
{
Parameter = parameter;
_children = new List<CommandNode>();
}
public CommandNode CreateCommand(IChatPartialCommand command)
{
if (Command != null)
throw new InvalidOperationException("Cannot change the command of an existing one.");
Command = command;
return this;
}
public CommandNode CreateStaticInput(string value, bool optional = false)
{
if (Children.Any(n => n.Parameter.GetType() != typeof(StaticParameter)))
throw new InvalidOperationException("Cannot have mixed static and user inputs in the same position of a subcommand.");
return Create(n => n.Parameter.Name == value, new StaticParameter(value.ToLower(), value, optional));
}
public CommandNode CreateUserInput(CommandParameter parameter)
{
if (Children.Any(n => n.Parameter.GetType() == typeof(StaticParameter)))
throw new InvalidOperationException("Cannot have mixed static and user inputs in the same position of a subcommand.");
return Create(n => true, parameter);
}
private CommandNode Create(Predicate<CommandNode> predicate, CommandParameter parameter)
{
CommandNode? node = Children.FirstOrDefault(n => predicate(n));
if (node == null)
{
node = new CommandNode(parameter);
_children.Add(node);
}
if (node.Parameter.GetType() != parameter.GetType())
throw new Exception("User input argument already exist for this partial command.");
return node;
}
public bool IsRequired()
{
return !Parameter.Optional;
}
}
}
}

View File

@ -0,0 +1,124 @@
using System.Text.RegularExpressions;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public class CommandManager
{
private readonly User _user;
private readonly ICommandSelector _commandSelector;
private readonly HermesSocketClient _hermes;
private readonly IGroupPermissionManager _permissionManager;
private readonly ILogger _logger;
private string CommandStartSign { get; } = "!";
public CommandManager(
IEnumerable<IChatCommand> commands,
ICommandBuilder commandBuilder,
User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> socketClient,
IGroupPermissionManager permissionManager,
ILogger logger
)
{
_user = user;
_hermes = (socketClient as HermesSocketClient)!;
_permissionManager = permissionManager;
_logger = logger;
foreach (var command in commands)
{
_logger.Debug($"Creating command tree for '{command.Name}'.");
command.Build(commandBuilder);
}
_commandSelector = commandBuilder.Build();
}
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> groups)
{
if (string.IsNullOrWhiteSpace(arg))
return ChatCommandResult.Unknown;
arg = arg.Trim();
if (!arg.StartsWith(CommandStartSign))
return ChatCommandResult.Unknown;
string[] parts = Regex.Matches(arg.Substring(CommandStartSign.Length), "(?<match>[^\"\\n\\s]+|\"[^\"\\n]*\")")
.Cast<Match>()
.Select(m => m.Groups["match"].Value)
.Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m)
.ToArray();
string[] args = parts.ToArray();
string com = args.First().ToLower();
CommandSelectorResult selectorResult = _commandSelector.GetBestMatch(args);
if (selectorResult.Command == null)
{
_logger.Warning($"Could not match '{arg}' to any command.");
return ChatCommandResult.Missing;
}
// Check if command can be executed by this chatter.
var command = selectorResult.Command;
long chatterId = long.Parse(message.UserId);
if (chatterId != _user.OwnerId)
{
var executable = command.AcceptCustomPermission ? CanExecute(chatterId, groups, com) : null;
if (executable == false)
{
_logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]");
return ChatCommandResult.Permission;
}
else if (executable == null && !command.CheckDefaultPermissions(message))
{
_logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Permission;
}
}
// Check if the arguments are correct.
var arguments = _commandSelector.GetNonStaticArguments(args, selectorResult.Path);
foreach (var entry in arguments)
{
var parameter = entry.Value;
var argument = entry.Key;
if (!parameter.Validate(argument))
{
_logger.Warning($"Command failed due to an argument being invalid [argument name: {parameter.Name}][argument value: {argument}][arguments: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Syntax;
}
}
var values = arguments.ToDictionary(d => d.Value.Name, d => d.Key);
try
{
await command.Execute(values, message, _hermes);
}
catch (Exception e)
{
_logger.Error(e, $"Command '{arg}' failed [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Fail;
}
_logger.Information($"Executed the {com} command [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Success;
}
private bool? CanExecute(long chatterId, IEnumerable<string> groups, string path)
{
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]");
return _permissionManager.CheckIfAllowed(groups, path);
}
}
}

View File

@ -2,89 +2,161 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public class OBSCommand : ChatCommand
public class OBSCommand : IChatCommand
{
private readonly User _user;
private readonly OBSManager _manager;
private readonly OBSSocketClient _obs;
private readonly ILogger _logger;
public OBSCommand(
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
User user,
OBSManager manager,
ILogger logger
) : base("obs", "Various obs commands.")
{
_user = user;
_manager = manager;
_logger = logger;
public string Name => "obs";
AddParameter(unvalidatedParameter);
AddParameter(unvalidatedParameter, optional: true);
AddParameter(unvalidatedParameter, optional: true);
AddParameter(unvalidatedParameter, optional: true);
public OBSCommand(
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
ILogger logger
)
{
_obs = (obs as OBSSocketClient)!;
_logger = logger;
}
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
public void Build(ICommandBuilder builder)
{
builder.CreateCommandTree(Name, b =>
{
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));
});
});
}
private sealed class OBSGetSceneItemId : IChatPartialCommand
{
private readonly OBSSocketClient _obs;
private readonly ILogger _logger;
public string Name => "obs";
public bool AcceptCustomPermission { get => true; }
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 override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesAvailable == null)
return;
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 } }));
}
}
var action = args[0].ToLower();
switch (action)
private sealed class OBSTransform : IChatPartialCommand
{
case "get_scene_item_id":
if (args.Count < 3)
return;
private readonly OBSSocketClient _obs;
private readonly ILogger _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;
public string Name => "obs";
public bool AcceptCustomPermission { get => true; }
_logger.Debug($"Getting scene item transformation data via chat command [args: {string.Join(" ", args)}]");
await _manager.UpdateTransformation(args[1], args[2], (d) =>
public OBSTransform(
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
ILogger logger
)
{
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]);
_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);
});
break;
case "sleep":
if (args.Count < 2)
return;
}
}
_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;
private sealed class OBSVisibility : IChatPartialCommand
{
private readonly OBSSocketClient _obs;
private readonly ILogger _logger;
_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 string Name => "obs";
public bool AcceptCustomPermission { get => true; }
public OBSVisibility(
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
ILogger logger
)
{
_obs = (obs as OBSSocketClient)!;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
string sceneName = values["sceneName"];
string sourceName = values["sourceName"];
string state = values["state"];
_logger.Debug($"Updating scene item visibility via chat command [scene name: {sceneName}][source name: {sourceName}][state: {state}]");
string stateLower = state.ToLower();
bool stateBool = stateLower == "true" || stateLower == "enable" || stateLower == "enabled" || stateLower == "yes";
await _obs.UpdateSceneItemVisibility(sceneName, sourceName, stateBool);
}
}
}

View File

@ -1,27 +0,0 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public abstract class ChatCommandParameter : ICloneable
{
public string Name { get; }
public string Description { get; }
public bool Optional { get; private set; }
public ChatCommandParameter(string name, string description, bool optional = false)
{
Name = name;
Description = description;
Optional = optional;
}
public abstract bool Validate(string value);
public object Clone() {
return (ChatCommandParameter) MemberwiseClone();
}
public ChatCommandParameter Permissive() {
Optional = true;
return this;
}
}
}

View File

@ -0,0 +1,20 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public abstract class CommandParameter : ICloneable
{
public string Name { get; }
public bool Optional { get; }
public CommandParameter(string name, bool optional)
{
Name = name;
Optional = optional;
}
public abstract bool Validate(string value);
public object Clone() {
return (CommandParameter) MemberwiseClone();
}
}
}

View File

@ -0,0 +1,16 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class OBSTransformationParameter : CommandParameter
{
private string[] _values = ["x", "y", "rotation", "rotate", "r"];
public OBSTransformationParameter(string name, bool optional = false) : base(name, optional)
{
}
public override bool Validate(string value)
{
return _values.Contains(value.ToLower());
}
}
}

View File

@ -1,17 +0,0 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class SimpleListedParameter : ChatCommandParameter
{
private readonly string[] _values;
public SimpleListedParameter(string[] possibleValues, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
{
_values = possibleValues;
}
public override bool Validate(string value)
{
return _values.Contains(value.ToLower());
}
}
}

View File

@ -0,0 +1,16 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class StateParameter : CommandParameter
{
private string[] _values = ["on", "off", "true", "false", "enabled", "disabled", "enable", "disable", "yes", "no"];
public StateParameter(string name, bool optional = false) : base(name, optional)
{
}
public override bool Validate(string value)
{
return _values.Contains(value.ToLower());
}
}
}

View File

@ -0,0 +1,19 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class StaticParameter : CommandParameter
{
private readonly string _value;
public string Value { get => _value; }
public StaticParameter(string name, string value, bool optional = false) : base(name, optional)
{
_value = value.ToLower();
}
public override bool Validate(string value)
{
return _value == value.ToLower();
}
}
}

View File

@ -1,11 +1,13 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class TTSVoiceNameParameter : ChatCommandParameter
public class TTSVoiceNameParameter : CommandParameter
{
private bool _enabled;
private readonly User _user;
public TTSVoiceNameParameter(User user, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
public TTSVoiceNameParameter(string name, bool enabled, User user, bool optional = false) : base(name, optional)
{
_enabled = enabled;
_user = user;
}
@ -15,6 +17,9 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
return false;
value = value.ToLower();
if (_enabled)
return _user.VoicesEnabled.Any(v => v.ToLower() == value);
return _user.VoicesAvailable.Any(e => e.Value.ToLower() == value);
}
}

View File

@ -1,8 +1,8 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class UnvalidatedParameter : ChatCommandParameter
public class UnvalidatedParameter : CommandParameter
{
public UnvalidatedParameter(bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
public UnvalidatedParameter(string name, bool optional = false) : base(name, optional)
{
}

View File

@ -0,0 +1,163 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket;
using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public class RefreshCommand : IChatCommand
{
private readonly OBSSocketClient _obs;
private readonly ILogger _logger;
public string Name => "refresh";
public RefreshCommand(
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
ILogger logger
)
{
_obs = (obs as OBSSocketClient)!;
_logger = logger;
}
public void Build(ICommandBuilder builder)
{
builder.CreateCommandTree(Name, b =>
{
b.CreateStaticInputParameter("tts_voice_enabled", b => b.CreateCommand(new RefreshTTSVoicesEnabled()))
.CreateStaticInputParameter("word_filters", b => b.CreateCommand(new RefreshTTSWordFilters()))
.CreateStaticInputParameter("selected_voices", b => b.CreateCommand(new RefreshTTSChatterVoices()))
.CreateStaticInputParameter("default_voice", b => b.CreateCommand(new RefreshTTSDefaultVoice()))
.CreateStaticInputParameter("redemptions", b => b.CreateCommand(new RefreshRedemptions()))
.CreateStaticInputParameter("obs_cache", b => b.CreateCommand(new RefreshObs(_obs, _logger)))
.CreateStaticInputParameter("permissions", b => b.CreateCommand(new RefreshPermissions()));
});
}
private sealed class RefreshTTSVoicesEnabled : IChatPartialCommand
{
public bool AcceptCustomPermission { get => true; }
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
await client.FetchEnabledTTSVoices();
}
}
private sealed class RefreshTTSWordFilters : IChatPartialCommand
{
public bool AcceptCustomPermission { get => true; }
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
await client.FetchTTSWordFilters();
}
}
private sealed class RefreshTTSChatterVoices : IChatPartialCommand
{
public bool AcceptCustomPermission { get => true; }
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
await client.FetchTTSChatterVoices();
}
}
private sealed class RefreshTTSDefaultVoice : IChatPartialCommand
{
public bool AcceptCustomPermission { get => true; }
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
await client.FetchDefaultTTSVoice();
}
}
private sealed class RefreshRedemptions : IChatPartialCommand
{
public bool AcceptCustomPermission { get => true; }
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
await client.FetchRedemptions();
}
}
private sealed class RefreshObs : IChatPartialCommand
{
private readonly OBSSocketClient _obsManager;
private readonly ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public RefreshObs(OBSSocketClient obsManager, ILogger logger) {
_obsManager = obsManager;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
_obsManager.ClearCache();
_logger.Information("Cleared the cache used for OBS.");
}
}
private sealed class RefreshPermissions : IChatPartialCommand
{
public bool AcceptCustomPermission { get => true; }
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
await client.FetchPermissions();
}
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
}
}

View File

@ -1,141 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class RefreshTTSDataCommand : ChatCommand
{
private readonly User _user;
private readonly RedemptionManager _redemptionManager;
private readonly IGroupPermissionManager _permissionManager;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly OBSManager _obsManager;
private readonly HermesApiClient _hermesApi;
private readonly ILogger _logger;
public RefreshTTSDataCommand(
User user,
RedemptionManager redemptionManager,
IGroupPermissionManager permissionManager,
IChatterGroupManager chatterGroupManager,
OBSManager obsManager,
HermesApiClient hermesApi,
ILogger logger
) : base("refresh", "Refreshes certain TTS related data on the client.")
{
_user = user;
_redemptionManager = redemptionManager;
_permissionManager = permissionManager;
_chatterGroupManager = chatterGroupManager;
_obsManager = obsManager;
_hermesApi = hermesApi;
_logger = logger;
AddParameter(new SimpleListedParameter([
"tts_voice_enabled",
"word_filters",
"selected_voices",
"default_voice",
"redemptions",
"obs_cache",
"permissions"
]));
}
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
{
var value = args.First().ToLower();
switch (value)
{
case "tts_voice_enabled":
var voicesEnabled = await _hermesApi.FetchTTSEnabledVoices();
if (voicesEnabled == null || !voicesEnabled.Any())
_user.VoicesEnabled = new HashSet<string>(["Brian"]);
else
_user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
_logger.Information($"{_user.VoicesEnabled.Count} TTS voices have been enabled.");
break;
case "word_filters":
var wordFilters = await _hermesApi.FetchTTSWordFilters();
_user.RegexFilters = wordFilters.ToList();
_logger.Information($"{_user.RegexFilters.Count()} TTS word filters.");
break;
case "selected_voices":
{
var voicesSelected = await _hermesApi.FetchTTSChatterSelectedVoices();
_user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice);
_logger.Information($"{_user.VoicesSelected.Count} TTS voices have been selected for specific chatters.");
break;
}
case "default_voice":
_user.DefaultTTSVoice = await _hermesApi.FetchTTSDefaultVoice();
_logger.Information("TTS Default Voice: " + _user.DefaultTTSVoice);
break;
case "redemptions":
var redemptionActions = await _hermesApi.FetchRedeemableActions();
var redemptions = await _hermesApi.FetchRedemptions();
_redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a));
_logger.Information($"Redemption Manager has been refreshed with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
break;
case "obs_cache":
{
_obsManager.ClearCache();
await _obsManager.GetGroupList(async groups => await _obsManager.GetGroupSceneItemList(groups));
break;
}
case "permissions":
{
_chatterGroupManager.Clear();
_permissionManager.Clear();
var groups = await _hermesApi.FetchGroups();
var groupsById = groups.ToDictionary(g => g.Id, g => g);
foreach (var group in groups)
_chatterGroupManager.Add(group);
_logger.Information($"{groups.Count()} groups have been loaded.");
var groupChatters = await _hermesApi.FetchGroupChatters();
_logger.Debug($"{groupChatters.Count()} group users have been fetched.");
var permissions = await _hermesApi.FetchGroupPermissions();
foreach (var permission in permissions)
{
_logger.Debug($"Adding group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
if (groupsById.TryGetValue(permission.GroupId, out var group))
{
_logger.Warning($"Failed to find group by id [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
continue;
}
var path = $"{group.Name}.{permission.Path}";
_permissionManager.Set(path, permission.Allow);
_logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
}
_logger.Information($"{permissions.Count()} group permissions have been loaded.");
foreach (var chatter in groupChatters)
if (groupsById.TryGetValue(chatter.GroupId, out var group))
_chatterGroupManager.Add(chatter.ChatterId, group.Name);
_logger.Information($"Users in each group have been loaded.");
break;
}
default:
_logger.Warning($"Unknown refresh value given [value: {value}]");
break;
}
}
}
}

View File

@ -1,54 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class RemoveTTSVoiceCommand : ChatCommand
{
private readonly User _user;
private ILogger _logger;
public new bool DefaultPermissionsOverwrite { get => true; }
public RemoveTTSVoiceCommand(
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
User user,
ILogger logger
) : base("removettsvoice", "Select a TTS voice as the default for that user.")
{
_user = user;
_logger = logger;
AddParameter(ttsVoiceParameter);
}
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{
return false;
}
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesAvailable == null)
{
_logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]");
return;
}
var voiceName = args.First().ToLower();
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName);
if (!exists)
{
_logger.Debug($"Voice does not exist [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
return;
}
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
await client.DeleteTTSVoice(voiceId);
_logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
}
}
}

View File

@ -1,37 +0,0 @@
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class SkipAllCommand : ChatCommand
{
private readonly TTSPlayer _ttsPlayer;
private readonly ILogger _logger;
public SkipAllCommand(TTSPlayer ttsPlayer, ILogger logger)
: base("skipall", "Skips all text to speech messages in queue and playing.")
{
_ttsPlayer = ttsPlayer;
_logger = logger;
}
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsVip || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
{
_ttsPlayer.RemoveAll();
if (_ttsPlayer.Playing == null)
return;
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
_ttsPlayer.Playing = null;
_logger.Information("Skipped all queued and playing tts.");
}
}
}

View File

@ -1,27 +1,57 @@
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 _player;
private readonly ILogger _logger;
public SkipCommand(TTSPlayer ttsPlayer, ILogger logger)
{
_player = ttsPlayer;
_logger = logger;
}
public string Name => "skip";
public void Build(ICommandBuilder builder)
{
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));
});
}
private sealed class TTSPlayerSkipCommand : IChatPartialCommand
{
private readonly TTSPlayer _ttsPlayer;
private readonly ILogger _logger;
public SkipCommand(TTSPlayer ttsPlayer, ILogger logger)
: base("skip", "Skips the current text to speech message.")
public bool AcceptCustomPermission { get => true; }
public TTSPlayerSkipCommand(TTSPlayer ttsPlayer, ILogger logger)
{
_ttsPlayer = ttsPlayer;
_logger = logger;
}
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsVip || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
if (_ttsPlayer.Playing == null)
return;
@ -32,4 +62,37 @@ namespace TwitchChatTTS.Chat.Commands
_logger.Information("Skipped current tts.");
}
}
private sealed class TTSPlayerSkipAllCommand : IChatPartialCommand
{
private readonly TTSPlayer _ttsPlayer;
private readonly ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public TTSPlayerSkipAllCommand(TTSPlayer ttsPlayer, ILogger logger)
{
_ttsPlayer = ttsPlayer;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsVip || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
_ttsPlayer.RemoveAll();
if (_ttsPlayer.Playing == null)
return;
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
_ttsPlayer.Playing = null;
_logger.Information("Skipped all queued and playing tts.");
}
}
}
}

View File

@ -1,46 +1,182 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public class TTSCommand : ChatCommand
public class TTSCommand : IChatCommand
{
private readonly User _user;
private readonly ILogger _logger;
public TTSCommand(
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
User user,
ILogger logger
) : base("tts", "Various tts commands.")
public TTSCommand(User user, ILogger logger)
{
_user = user;
_logger = logger;
AddParameter(ttsVoiceParameter);
AddParameter(new SimpleListedParameter(["enable", "disable"]));
}
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
public string Name => "tts";
public void Build(ICommandBuilder builder)
{
return message.IsModerator || message.IsBroadcaster;
builder.CreateCommandTree(Name, b =>
{
b.CreateStaticInputParameter("add", b =>
{
b.CreateVoiceNameParameter("voiceName", false)
.CreateCommand(new AddTTSVoiceCommand(_user, _logger));
})
.CreateStaticInputParameter("del", b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
})
.CreateStaticInputParameter("delete", b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
})
.CreateStaticInputParameter("remove", b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
})
.CreateStaticInputParameter("enable", b =>
{
b.CreateVoiceNameParameter("voiceName", false)
.CreateCommand(new SetTTSVoiceStateCommand(true, _user, _logger));
})
.CreateStaticInputParameter("on", b =>
{
b.CreateVoiceNameParameter("voiceName", false)
.CreateCommand(new SetTTSVoiceStateCommand(true, _user, _logger));
})
.CreateStaticInputParameter("disable", b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new SetTTSVoiceStateCommand(false, _user, _logger));
})
.CreateStaticInputParameter("off", b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new SetTTSVoiceStateCommand(false, _user, _logger));
});
});
}
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
private sealed class AddTTSVoiceCommand : IChatPartialCommand
{
private readonly User _user;
private readonly ILogger _logger;
public bool AcceptCustomPermission { get => false; }
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 = args[0].ToLower();
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
var action = args[1].ToLower();
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;
}
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}]");
await client.CreateTTSVoice(voiceName);
_logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]");
}
}
private sealed class DeleteTTSVoiceCommand : IChatPartialCommand
{
private readonly User _user;
private ILogger _logger;
public bool AcceptCustomPermission { get => false; }
public DeleteTTSVoiceCommand(User user, ILogger logger)
{
_user = user;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return false;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesAvailable == null)
{
_logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]");
return;
}
var voiceName = values["voiceName"];
var voiceNameLower = voiceName.ToLower();
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
if (!exists)
{
_logger.Debug($"Voice does not exist [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
return;
}
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
await client.DeleteTTSVoice(voiceId);
_logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
}
}
private sealed class SetTTSVoiceStateCommand : IChatPartialCommand
{
private bool _state;
private readonly User _user;
private ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public SetTTSVoiceStateCommand(bool state, User user, ILogger logger)
{
_state = state;
_user = user;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster;
}
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesAvailable == null)
return;
var voiceName = values["voiceName"];
var voiceNameLower = voiceName.ToLower();
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceNameLower).Key;
await client.UpdateTTSVoiceState(voiceId, _state);
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {_state}][invoker: {message.Username}][id: {message.UserId}]");
}
}
}
}

View File

@ -2,31 +2,52 @@ using HermesSocketLibrary.Socket.Data;
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public class VersionCommand : ChatCommand
public class VersionCommand : IChatCommand
{
private readonly User _user;
private ILogger _logger;
public string Name => "version";
public VersionCommand(User user, ILogger logger)
: base("version", "Does nothing.")
{
_user = user;
_logger = logger;
}
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
public void Build(ICommandBuilder builder)
{
builder.CreateCommandTree(Name, b => b.CreateCommand(new AppVersionCommand(_user, _logger)));
}
private sealed class AppVersionCommand : IChatPartialCommand
{
private readonly User _user;
private ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public AppVersionCommand(User user, ILogger logger)
{
_user = user;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
_logger.Information($"Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
_logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
await client.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.");
}
}
}
}

View File

@ -1,48 +1,60 @@
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)
{
builder.CreateCommandTree(Name, b =>
{
b.CreateVoiceNameParameter("voiceName", true)
.CreateCommand(new TTSVoiceSelector(_user, _logger));
});
}
private sealed class TTSVoiceSelector : IChatPartialCommand
{
private readonly User _user;
private readonly ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public TTSVoiceSelector(User user, ILogger logger)
{
_user = user;
_logger = logger;
}
public bool CheckDefaultPermissions(ChatMessage message)
{
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100;
}
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
{
if (_user == null || _user.VoicesSelected == null || _user.VoicesEnabled == null)
if (_user == null || _user.VoicesSelected == null)
return;
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);
if (!enabled)
{
_logger.Information($"Voice is disabled. Cannot switch to that voice [voice: {voice.Value}][username: {message.Username}]");
return;
}
var voiceName = values["voiceName"];
var voiceNameLower = voiceName.ToLower();
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceNameLower);
if (_user.VoicesSelected.ContainsKey(chatterId))
{
@ -57,3 +69,4 @@ namespace TwitchChatTTS.Chat.Commands
}
}
}
}

View File

@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
namespace TwitchChatTTS.Chat.Groups
@ -59,7 +60,10 @@ namespace TwitchChatTTS.Chat.Groups
}
public int GetPriorityFor(IEnumerable<string> groupNames) {
return groupNames.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null).Max(g => g.Priority);
var values = groupNames.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null);
if (values.Any())
return values.Max(g => g.Priority);
return 0;
}
public bool Remove(long chatterId, string groupId) {

View File

@ -1,9 +0,0 @@
namespace TwitchChatTTS.Chat.Groups
{
public class Group
{
public string Id { get; set; }
public string Name { get; set; }
public int Priority { get; set; }
}
}

View File

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

View File

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

View File

@ -1,10 +0,0 @@
namespace TwitchChatTTS.Chat.Groups.Permissions
{
public class GroupPermission
{
public string Id { get; set; }
public string GroupId { get; set; }
public string Path { get; set; }
public bool? Allow { get; set; }
}
}

View File

@ -37,8 +37,7 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
public void Clear()
{
if (_root.Children != null)
_root.Children.Clear();
_root.Clear();
}
public bool Remove(string path)
@ -127,6 +126,11 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
_children.Add(child);
}
internal void Clear() {
if (_children != null)
_children.Clear();
}
public void Remove(string name)
{
if (_children == null || !_children.Any())

View File

@ -23,7 +23,7 @@ namespace TwitchChatTTS.Helpers
_client.DefaultRequestHeaders.Add(key, value);
}
public async Task<T?> GetJson<T>(string uri, JsonSerializerOptions options = null)
public async Task<T?> GetJson<T>(string uri, JsonSerializerOptions? options = null)
{
var response = await _client.GetAsync(uri);
return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), options ?? _options);

View File

@ -40,39 +40,15 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
client.LoggedIn = true;
_logger.Information($"Logged in as {_user.TwitchUsername} {(message.WebLogin ? "via web" : "via TTS app")}.");
await client.Send(3, new RequestMessage()
{
Type = "get_tts_voices",
Data = null
});
await client.Send(3, new RequestMessage()
{
Type = "get_tts_users",
Data = new Dictionary<string, object>() { { "user", _user.HermesUserId } }
});
await client.Send(3, new RequestMessage()
{
Type = "get_default_tts_voice",
Data = null
});
await client.Send(3, new RequestMessage()
{
Type = "get_chatter_ids",
Data = null
});
await client.Send(3, new RequestMessage()
{
Type = "get_emotes",
Data = null
});
await client.GetRedemptions();
await Task.Delay(TimeSpan.FromSeconds(3));
await client.FetchTTSVoices();
await client.FetchEnabledTTSVoices();
await client.FetchTTSWordFilters();
await client.FetchTTSChatterVoices();
await client.FetchDefaultTTSVoice();
await client.FetchChatterIdentifiers();
await client.FetchEmotes();
await client.FetchRedemptions();
await client.FetchPermissions();
_logger.Information("TTS is now ready.");
client.Ready = true;

View File

@ -8,6 +8,8 @@ using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Emotes;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Handlers
@ -28,7 +30,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
public RequestAckHandler(
User user,
//RedemptionManager redemptionManager,
ICallbackManager<HermesRequestData> callbackManager,
IServiceProvider serviceProvider,
JsonSerializerOptions options,
@ -36,7 +37,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
)
{
_user = user;
//_redemptionManager = redemptionManager;
_callbackManager = callbackManager;
_serviceProvider = serviceProvider;
_options = options;
@ -66,7 +66,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_logger.Debug($"Received a Hermes request message [type: {message.Request.Type}][data: {string.Join(',', message.Request.Data?.Select(entry => entry.Key + '=' + entry.Value) ?? Array.Empty<string>())}]");
if (message.Request.Type == "get_tts_voices")
{
_logger.Verbose("Updating all available voices for TTS.");
var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(message.Data.ToString(), _options);
if (voices == null)
return;
@ -79,7 +78,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "create_tts_user")
{
_logger.Verbose("Adding new tts voice for user.");
if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
{
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]");
@ -93,7 +91,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "update_tts_user")
{
_logger.Verbose("Updating user's voice");
if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
{
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]");
@ -107,7 +104,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "create_tts_voice")
{
_logger.Verbose("Creating new tts voice.");
string? voice = message.Request.Data["voice"].ToString();
string? voiceId = message.Data.ToString();
if (voice == null || voiceId == null)
@ -123,7 +119,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "delete_tts_voice")
{
_logger.Verbose("Deleting tts voice.");
var voice = message.Request.Data["voice"].ToString();
if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null)
return;
@ -138,7 +133,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "update_tts_voice")
{
_logger.Verbose("Updating TTS voice.");
string voiceId = message.Request.Data["idd"].ToString();
string voice = message.Request.Data["voice"].ToString();
@ -150,7 +144,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "get_tts_users")
{
_logger.Verbose("Updating all chatters' selected voice.");
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(message.Data.ToString(), _options);
if (users == null)
return;
@ -163,7 +156,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "get_chatter_ids")
{
_logger.Verbose("Fetching all chatters' id.");
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(message.Data.ToString(), _options);
if (chatters == null)
return;
@ -174,7 +166,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "get_emotes")
{
_logger.Verbose("Updating emotes.");
var emotes = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(message.Data.ToString(), _options);
if (emotes == null)
return;
@ -196,9 +187,78 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (duplicateNames > 0)
_logger.Warning($"Found {duplicateNames} emotes with duplicate names.");
}
else if (message.Request.Type == "get_enabled_tts_voices")
{
var enabledTTSVoices = JsonSerializer.Deserialize<IEnumerable<string>>(message.Data.ToString(), _options);
if (enabledTTSVoices == null)
{
_logger.Error("Failed to load enabled tts voices.");
return;
}
if (_user.VoicesEnabled == null)
_user.VoicesEnabled = enabledTTSVoices.ToHashSet();
else
_user.VoicesEnabled.Clear();
foreach (var voice in enabledTTSVoices)
_user.VoicesEnabled.Add(voice);
_logger.Information($"TTS voices [count: {_user.VoicesEnabled.Count}] have been enabled.");
}
else if (message.Request.Type == "get_permissions")
{
var groupInfo = JsonSerializer.Deserialize<GroupInfo>(message.Data.ToString(), _options);
if (groupInfo == null)
{
_logger.Error("Failed to load groups & permissions.");
return;
}
var chatterGroupManager = _serviceProvider.GetRequiredService<IChatterGroupManager>();
var permissionManager = _serviceProvider.GetRequiredService<IGroupPermissionManager>();
permissionManager.Clear();
chatterGroupManager.Clear();
var groupsById = groupInfo.Groups.ToDictionary(g => g.Id, g => g);
foreach (var group in groupInfo.Groups)
chatterGroupManager.Add(group);
foreach (var permission in groupInfo.GroupPermissions)
{
_logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
if (!groupsById.TryGetValue(permission.GroupId, out var group))
{
_logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
continue;
}
var path = $"{group.Name}.{permission.Path}";
permissionManager.Set(path, permission.Allow);
_logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
}
_logger.Information($"Groups [count: {groupInfo.Groups.Count()}] & Permissions [count: {groupInfo.GroupPermissions.Count()}] have been loaded.");
foreach (var chatter in groupInfo.GroupChatters)
if (groupsById.TryGetValue(chatter.GroupId, out var group))
chatterGroupManager.Add(chatter.ChatterId, group.Name);
_logger.Information($"Users in each group [count: {groupInfo.GroupChatters.Count()}] have been loaded.");
}
else if (message.Request.Type == "get_tts_word_filters")
{
var wordFilters = JsonSerializer.Deserialize<IEnumerable<TTSWordFilter>>(message.Data.ToString(), _options);
if (wordFilters == null)
{
_logger.Error("Failed to load word filters.");
return;
}
_user.RegexFilters = wordFilters.ToList();
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count}] have been refreshed.");
}
else if (message.Request.Type == "update_tts_voice_state")
{
_logger.Verbose("Updating TTS voice states.");
string voiceId = message.Request.Data["voice"].ToString();
bool state = message.Request.Data["state"].ToString().ToLower() == "true";
@ -216,7 +276,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "get_redemptions")
{
_logger.Verbose("Fetching all the redemptions.");
IEnumerable<Redemption>? redemptions = JsonSerializer.Deserialize<IEnumerable<Redemption>>(message.Data!.ToString()!, _options);
if (redemptions != null)
{
@ -229,7 +288,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
}
else if (message.Request.Type == "get_redeemable_actions")
{
_logger.Verbose("Fetching all the redeemable actions.");
IEnumerable<RedeemableAction>? actions = JsonSerializer.Deserialize<IEnumerable<RedeemableAction>>(message.Data!.ToString()!, _options);
if (actions == null)
{

View File

@ -36,14 +36,14 @@ namespace TwitchChatTTS.Hermes.Socket
User user,
Configuration configuration,
ICallbackManager<HermesRequestData> callbackManager,
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
[FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager,
[FromKeyedServices("hermes")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("hermes")] MessageTypeManager<IWebSocketHandler> typeManager,
ILogger logger
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
) : base(handlers, typeManager, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
})
}, logger)
{
_user = user;
_configuration = configuration;
@ -74,7 +74,7 @@ namespace TwitchChatTTS.Hermes.Socket
if (!Connected)
return;
await DisconnectAsync();
await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed"));
}
public async Task CreateTTSVoice(string voiceName)
@ -104,11 +104,67 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task GetRedemptions()
public async Task FetchChatterIdentifiers() {
await Send(3, new RequestMessage()
{
Type = "get_chatter_ids",
Data = null
});
}
public async Task FetchDefaultTTSVoice() {
await Send(3, new RequestMessage()
{
Type = "get_default_tts_voice",
Data = null
});
}
public async Task FetchEmotes() {
await Send(3, new RequestMessage()
{
Type = "get_emotes",
Data = null
});
}
public async Task FetchEnabledTTSVoices() {
await Send(3, new RequestMessage()
{
Type = "get_enabled_tts_voices",
Data = null
});
}
public async Task FetchTTSVoices() {
await Send(3, new RequestMessage()
{
Type = "get_tts_voices",
Data = null
});
}
public async Task FetchTTSChatterVoices() {
await Send(3, new RequestMessage()
{
Type = "get_tts_users",
Data = null
});
}
public async Task FetchTTSWordFilters() {
await Send(3, new RequestMessage()
{
Type = "get_tts_word_filters",
Data = null
});
}
public async Task FetchRedemptions()
{
var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData()
{
Callback = async (d) => await GetRedeemableActions(d["redemptions"] as IEnumerable<Redemption>),
Callback = async (d) => await FetchRedeemableActions(d["redemptions"] as IEnumerable<Redemption>),
Data = new Dictionary<string, object>()
});
@ -120,7 +176,7 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task GetRedeemableActions(IEnumerable<Redemption> redemptions)
private async Task FetchRedeemableActions(IEnumerable<Redemption> redemptions)
{
var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData()
{
@ -135,6 +191,15 @@ namespace TwitchChatTTS.Hermes.Socket
});
}
public async Task FetchPermissions()
{
await Send(3, new RequestMessage()
{
Type = "get_permissions",
Data = null
});
}
public void Initialize()
{
_logger.Information("Initializing Hermes websocket client.");

View File

@ -1,41 +0,0 @@
using CommonSocketLibrary.Common;
using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Managers
{
public class HermesHandlerManager : WebSocketHandlerManager
{
public HermesHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
{
try
{
var basetype = typeof(IWebSocketHandler);
var assembly = GetType().Assembly;
var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Hermes.") == true);
foreach (var type in types)
{
var key = "hermes-" + type.Name.Replace("Handlers", "Hand#lers")
.Replace("Handler", "")
.Replace("Hand#lers", "Handlers")
.ToLower();
var handler = provider.GetKeyedService<IWebSocketHandler>(key);
if (handler == null)
{
logger.Error("Failed to find hermes websocket handler: " + type.AssemblyQualifiedName);
continue;
}
_logger.Debug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers.");
Add(handler);
}
}
catch (Exception e)
{
_logger.Error(e, "Failed to load hermes websocket handler types.");
}
}
}
}

View File

@ -7,12 +7,12 @@ using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Managers
{
public class HermesHandlerTypeManager : WebSocketHandlerTypeManager
public class HermesMessageTypeManager : WebSocketMessageTypeManager
{
public HermesHandlerTypeManager(
ILogger factory,
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
) : base(factory, handlers)
public HermesMessageTypeManager(
[FromKeyedServices("hermes")] IEnumerable<IWebSocketHandler> handlers,
ILogger logger
) : base(handlers, logger)
{
}

View File

@ -1,15 +1,19 @@
using System.Text.Json.Serialization;
namespace TwitchChatTTS.OBS.Socket.Data
{
public class IdentifyMessage
{
public int RpcVersion { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Authentication { get; set; }
public int EventSubscriptions { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? EventSubscriptions { get; set; }
public IdentifyMessage(int version, string auth, int subscriptions)
public IdentifyMessage(int rpcVersion, string? authentication, int? subscriptions)
{
RpcVersion = version;
Authentication = auth;
RpcVersion = rpcVersion;
Authentication = authentication;
EventSubscriptions = subscriptions;
}
}

View File

@ -2,19 +2,18 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class EventMessageHandler : IWebSocketHandler
{
private readonly OBSManager _manager;
private readonly ILogger _logger;
public int OperationCode { get; } = 5;
public EventMessageHandler(OBSManager manager, ILogger logger)
public EventMessageHandler(
ILogger logger
)
{
_manager = manager;
_logger = logger;
}
@ -22,6 +21,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
if (data is not EventMessage message || message == null)
return;
if (sender is not OBSSocketClient obs)
return;
switch (message.EventType)
{
@ -31,10 +32,10 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
string? raw_state = message.EventData["outputState"].ToString();
string? state = raw_state?.Substring(21).ToLower();
_manager.Streaming = message.EventData["outputActive"].ToString().ToLower() == "true";
obs.Streaming = message.EventData["outputActive"].ToString()!.ToLower() == "true";
_logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
if (_manager.Streaming == false && state != null && !state.EndsWith("ing"))
if (obs.Streaming == false && state != null && !state.EndsWith("ing"))
{
// Stream ended
}

View File

@ -26,9 +26,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim();
_logger.Verbose("OBS websocket password: " + password);
if (message.Authentication == null || string.IsNullOrWhiteSpace(password))
if (message.Authentication == null || string.IsNullOrEmpty(password))
{
await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144));
await sender.Send(1, new IdentifyMessage(message.RpcVersion, null, 1023 | 262144));
return;
}
@ -39,7 +39,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
string secret = password + salt;
byte[] bytes = Encoding.UTF8.GetBytes(secret);
string hash = null;
string? hash = null;
using (var sha = SHA256.Create())
{
bytes = sha.ComputeHash(bytes);

View File

@ -2,19 +2,16 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class IdentifiedHandler : IWebSocketHandler
{
private readonly OBSManager _manager;
private readonly ILogger _logger;
public int OperationCode { get; } = 2;
public IdentifiedHandler(OBSManager manager, ILogger logger)
public IdentifiedHandler(ILogger logger)
{
_manager = manager;
_logger = logger;
}
@ -22,20 +19,22 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
if (data is not IdentifiedMessage message || message == null)
return;
if (sender is not OBSSocketClient obs)
return;
_manager.Connected = true;
obs.Identified = true;
_logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + ".");
try
{
await _manager.GetGroupList(async groups => await _manager.GetGroupSceneItemList(groups));
await obs.GetGroupList(async groups => await obs.GetGroupSceneItemList(groups));
}
catch (Exception e)
{
_logger.Error(e, "Failed to load OBS group info upon OBS identification.");
}
await _manager.UpdateStreamingState();
await obs.UpdateStreamingState();
}
}
}

View File

@ -5,22 +5,16 @@ using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Context;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class RequestBatchResponseHandler : IWebSocketHandler
{
private readonly IWebSocketHandler _requestResponseHandler;
private readonly ILogger _logger;
public int OperationCode { get; } = 9;
public RequestBatchResponseHandler(
[FromKeyedServices("obs-requestresponse")] IWebSocketHandler requestResponseHandler,
ILogger logger
)
public RequestBatchResponseHandler(ILogger logger)
{
_requestResponseHandler = requestResponseHandler;
_logger = logger;
}
@ -28,9 +22,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
if (data is not RequestBatchResponseMessage message || message == null)
return;
using (LogContext.PushProperty("obsrid", message.RequestId))
{
if (sender is not OBSSocketClient obs)
return;
var results = message.Results.ToList();
_logger.Debug($"Received request batch response of {results.Count} messages.");
@ -52,7 +45,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (response == null)
continue;
await _requestResponseHandler.Execute(sender, response);
await obs.ExecuteRequest(response);
}
catch (Exception ex)
{
@ -64,4 +57,3 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
}
}
}
}

View File

@ -3,19 +3,18 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class RequestResponseHandler : IWebSocketHandler
{
private readonly OBSManager _manager;
private readonly ILogger _logger;
public int OperationCode { get; } = 7;
public RequestResponseHandler(OBSManager manager, ILogger logger)
public RequestResponseHandler(
ILogger logger
)
{
_manager = manager;
_logger = logger;
}
@ -23,10 +22,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
if (data is not RequestResponseMessage message || message == null)
return;
if (sender is not OBSSocketClient obs)
return;
_logger.Debug($"Received an OBS request response [obs request id: {message.RequestId}]");
var requestData = _manager.Take(message.RequestId);
var requestData = obs.Take(message.RequestId);
if (requestData == null)
{
_logger.Warning($"OBS Request Response not being processed: request not stored [obs request id: {message.RequestId}]");
@ -42,7 +43,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
switch (request.RequestType)
{
case "GetOutputStatus":
_logger.Debug($"Fetched stream's live status [live: {_manager.Streaming}][obs request id: {message.RequestId}]");
_logger.Debug($"Fetched stream's live status [live: {obs.Streaming}][obs request id: {message.RequestId}]");
break;
case "GetSceneItemId":
{
@ -206,7 +207,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
}
foreach (var sceneItem in sceneItems)
_manager.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId);
obs.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId);
requestData.ResponseValues = new Dictionary<string, object>()
{
@ -237,9 +238,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
return;
}
_manager.Streaming = outputActive?.ToString()!.ToLower() == "true";
obs.Streaming = outputActive?.ToString()!.ToLower() == "true";
requestData.ResponseValues = message.ResponseData;
_logger.Information($"OBS is currently {(_manager.Streaming ? "" : "not ")}streaming.");
_logger.Information($"OBS is currently {(obs.Streaming ? "" : "not ")}streaming.");
break;
}
default:

View File

@ -1,34 +0,0 @@
using Serilog;
using Microsoft.Extensions.DependencyInjection;
using CommonSocketLibrary.Socket.Manager;
using CommonSocketLibrary.Common;
namespace TwitchChatTTS.OBS.Socket.Manager
{
public class OBSHandlerManager : WebSocketHandlerManager
{
public OBSHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
{
var basetype = typeof(IWebSocketHandler);
var assembly = GetType().Assembly;
var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".OBS.") == true);
foreach (var type in types)
{
var key = "obs-" + type.Name.Replace("Handlers", "Hand#lers")
.Replace("Handler", "")
.Replace("Hand#lers", "Handlers")
.ToLower();
var handler = provider.GetKeyedService<IWebSocketHandler>(key);
if (handler == null)
{
logger.Error("Failed to find obs websocket handler: " + type.AssemblyQualifiedName);
continue;
}
_logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}.");
Add(handler);
}
}
}
}

View File

@ -1,4 +1,3 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection;
@ -6,12 +5,12 @@ using Serilog;
namespace TwitchChatTTS.OBS.Socket.Manager
{
public class OBSHandlerTypeManager : WebSocketHandlerTypeManager
public class OBSMessageTypeManager : WebSocketMessageTypeManager
{
public OBSHandlerTypeManager(
ILogger factory,
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
) : base(factory, handlers)
public OBSMessageTypeManager(
[FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
ILogger logger
) : base(handlers, logger)
{
}
}

View File

@ -1,316 +0,0 @@
using System.Collections.Concurrent;
using System.Text.Json;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
namespace TwitchChatTTS.OBS.Socket.Manager
{
public class OBSManager
{
private readonly IDictionary<string, RequestData> _requests;
private readonly IDictionary<string, long> _sourceIds;
private string? URL;
private readonly Configuration _configuration;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
public bool Connected { get; set; }
public bool Streaming { get; set; }
public OBSManager(Configuration configuration, IServiceProvider serviceProvider, ILogger logger)
{
_configuration = configuration;
_serviceProvider = serviceProvider;
_logger = logger;
_requests = new ConcurrentDictionary<string, RequestData>();
_sourceIds = new Dictionary<string, long>();
}
public void Initialize()
{
_logger.Information($"Initializing OBS websocket client.");
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
client.OnConnected += (sender, e) =>
{
Connected = true;
_logger.Information("OBS websocket client connected.");
};
client.OnDisconnected += (sender, e) =>
{
Connected = false;
_logger.Information("OBS websocket client disconnected.");
};
if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null)
URL = $"ws://{_configuration.Obs.Host?.Trim()}:{_configuration.Obs.Port}";
}
public void AddSourceId(string sourceName, long sourceId)
{
if (!_sourceIds.TryGetValue(sourceName, out _))
_sourceIds.Add(sourceName, sourceId);
else
_sourceIds[sourceName] = sourceId;
_logger.Debug($"Added OBS scene item to cache [scene item: {sourceName}][scene item id: {sourceId}]");
}
public void ClearCache()
{
_sourceIds.Clear();
}
public async Task Connect()
{
if (string.IsNullOrWhiteSpace(URL))
{
_logger.Warning("Lacking connection info for OBS websockets. Not connecting to OBS.");
return;
}
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
_logger.Debug($"OBS websocket client attempting to connect to {URL}");
try
{
await client.ConnectAsync(URL);
}
catch (Exception)
{
_logger.Warning("Connecting to obs failed. Skipping obs websockets.");
}
}
public async Task Send(IEnumerable<RequestMessage> messages)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
var list = messages.ToList();
_logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}].");
// Keep track of requests to know what we requested.
foreach (var message in list)
{
message.RequestId = GenerateUniqueIdentifier();
var data = new RequestData(message, uid);
_requests.Add(message.RequestId, data);
}
_logger.Debug($"Generated uid for all OBS request messages in batch [obs request batch id: {uid}][obs request ids: {string.Join(", ", list.Select(m => m.RequestType + "=" + m.RequestId))}]");
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
await client.Send(8, new RequestBatchMessage(uid, list));
}
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
_logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]");
// Keep track of requests to know what we requested.
message.RequestId = GenerateUniqueIdentifier();
var data = new RequestData(message, uid)
{
Callback = callback
};
_requests.Add(message.RequestId, data);
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
await client.Send(6, message);
}
public RequestData? Take(string id)
{
if (id != null && _requests.TryGetValue(id, out var request))
{
_requests.Remove(id);
return request;
}
return null;
}
public async Task UpdateStreamingState()
{
await Send(new RequestMessage("GetStreamStatus"));
}
public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
{
if (action == null)
return;
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m2, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
return;
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]");
var transform = JsonSerializer.Deserialize<OBSTransformationData>(transformData.ToString()!, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
if (transform == null)
{
_logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m2.RequestId}].");
return;
}
double w = transform.Width;
double h = transform.Height;
int a = transform.Alignment;
bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE";
if (a != (int)OBSAlignment.Center)
{
if (hasBounds)
transform.BoundsAlignment = a = (int)OBSAlignment.Center;
else
transform.Alignment = a = (int)OBSAlignment.Center;
transform.PositionX = transform.PositionX + w / 2;
transform.PositionY = transform.PositionY + h / 2;
}
action?.Invoke(transform);
var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } });
await Send(m3);
_logger.Debug($"New transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m3.RequestId}]");
});
});
}
public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m1, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemEnabled", out object? visible) || visible == null)
return;
var m2 = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", visible.ToString().ToLower() == "true" ? false : true } });
await Send(m2);
});
});
}
public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } });
await Send(m);
});
}
public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } });
await Send(m);
});
}
public async Task GetGroupList(Action<IEnumerable<string>>? action)
{
var m = new RequestMessage("GetGroupList", string.Empty, new Dictionary<string, object>());
await Send(m, (d) =>
{
if (d == null || !d.TryGetValue("groups", out object? value) || value == null)
return;
var list = (IEnumerable<string>)value;
_logger.Debug("Fetched the list of groups in OBS.");
if (list != null)
action?.Invoke(list);
});
}
public async Task GetGroupSceneItemList(string groupName, Action<IEnumerable<OBSSceneItem>>? action)
{
var m = new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", groupName } });
await Send(m, (d) =>
{
if (d == null || !d.TryGetValue("sceneItems", out object? value) || value == null)
return;
var list = (IEnumerable<OBSSceneItem>)value;
_logger.Debug($"Fetched the list of OBS scene items in a group [group: {groupName}]");
if (list != null)
action?.Invoke(list);
});
}
public async Task GetGroupSceneItemList(IEnumerable<string> groupNames)
{
var messages = groupNames.Select(group => new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", group } }));
await Send(messages);
_logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]");
}
private async Task GetSceneItemByName(string sceneName, string sceneItemName, Action<long> action)
{
if (_sourceIds.TryGetValue(sceneItemName, out long sourceId))
{
_logger.Debug($"Fetched scene item id from cache [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sourceId}]");
action.Invoke(sourceId);
return;
}
var m = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
await Send(m, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId))
return;
_logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sceneItemId}][obs request id: {m.RequestId}]");
AddSourceId(sceneItemName, sceneItemId);
action.Invoke(sceneItemId);
});
}
private string GenerateUniqueIdentifier()
{
return Guid.NewGuid().ToString("N");
}
}
public class RequestData
{
public RequestMessage Message { get; }
public string ParentId { get; }
public Dictionary<string, object> ResponseValues { get; set; }
public Action<Dictionary<string, object>>? Callback { get; set; }
public RequestData(RequestMessage message, string parentId)
{
Message = message;
ParentId = parentId;
}
}
}

View File

@ -3,21 +3,365 @@ using CommonSocketLibrary.Abstract;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using System.Text.Json;
using System.Collections.Concurrent;
using TwitchChatTTS.OBS.Socket.Data;
using System.Timers;
using System.Net.WebSockets;
namespace TwitchChatTTS.OBS.Socket
{
public class OBSSocketClient : WebSocketClient
{
private readonly IDictionary<string, RequestData> _requests;
private readonly IDictionary<string, long> _sourceIds;
private string? URL;
private readonly Configuration _configuration;
private System.Timers.Timer _reconnectTimer;
public bool Connected { get; set; }
public bool Identified { get; set; }
public bool Streaming { get; set; }
public OBSSocketClient(
ILogger logger,
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
[FromKeyedServices("obs")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
Configuration configuration,
[FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("obs")] MessageTypeManager<IWebSocketHandler> typeManager,
ILogger logger
) : base(handlers, typeManager, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
})
}, logger)
{
_configuration = configuration;
_reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(30));
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect(e);
_reconnectTimer.Enabled = false;
_requests = new ConcurrentDictionary<string, RequestData>();
_sourceIds = new Dictionary<string, long>();
}
public void Initialize()
{
_logger.Information($"Initializing OBS websocket client.");
OnConnected += (sender, e) =>
{
Connected = true;
_reconnectTimer.Enabled = false;
_logger.Information("OBS websocket client connected.");
};
OnDisconnected += (sender, e) =>
{
_reconnectTimer.Enabled = Identified;
_logger.Information($"OBS websocket client disconnected [status: {e.Status}][reason: {e.Reason}] " + (Identified ? "Will be attempting to reconnect every 30 seconds." : "Will not be attempting to reconnect."));
Connected = false;
Identified = false;
Streaming = false;
};
if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null)
URL = $"ws://{_configuration.Obs.Host?.Trim()}:{_configuration.Obs.Port}";
}
public void AddSourceId(string sourceName, long sourceId)
{
if (!_sourceIds.TryGetValue(sourceName, out _))
_sourceIds.Add(sourceName, sourceId);
else
_sourceIds[sourceName] = sourceId;
_logger.Debug($"Added OBS scene item to cache [scene item: {sourceName}][scene item id: {sourceId}]");
}
public void ClearCache()
{
_sourceIds.Clear();
}
public async Task Connect()
{
if (string.IsNullOrWhiteSpace(URL))
{
_logger.Warning("Lacking connection info for OBS websockets. Not connecting to OBS.");
return;
}
_logger.Debug($"OBS websocket client attempting to connect to {URL}");
try
{
await ConnectAsync(URL);
}
catch (Exception)
{
_logger.Warning("Connecting to obs failed. Skipping obs websockets.");
}
}
public async Task ExecuteRequest(RequestResponseMessage message) {
if (!_handlers.TryGetValue(7, out var handler) || handler == null)
{
_logger.Error("Failed to find the request response handler for OBS.");
return;
}
await handler.Execute(this, message);
}
private async Task Reconnect(ElapsedEventArgs e)
{
if (Connected)
{
try
{
await DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), ""));
}
catch (Exception)
{
_logger.Error("Failed to disconnect from OBS websocket server.");
}
}
try
{
await Connect();
}
catch (WebSocketException wse) when (wse.Message.Contains("502"))
{
_logger.Error("OBS websocket server cannot be found. Be sure the server is on by looking at OBS > Tools > Websocket Server Settings.");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to reconnect to OBS websocket server.");
}
}
public async Task Send(IEnumerable<RequestMessage> messages)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
var list = messages.ToList();
_logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}].");
// Keep track of requests to know what we requested.
foreach (var message in list)
{
message.RequestId = GenerateUniqueIdentifier();
var data = new RequestData(message, uid);
_requests.Add(message.RequestId, data);
}
_logger.Debug($"Generated uid for all OBS request messages in batch [obs request batch id: {uid}][obs request ids: {string.Join(", ", list.Select(m => m.RequestType + "=" + m.RequestId))}]");
await Send(8, new RequestBatchMessage(uid, list));
}
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
_logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]");
// Keep track of requests to know what we requested.
message.RequestId = uid;
var data = new RequestData(message, uid)
{
Callback = callback
};
_requests.Add(message.RequestId, data);
await Send(6, message);
}
public RequestData? Take(string id)
{
if (id != null && _requests.TryGetValue(id, out var request))
{
_requests.Remove(id);
return request;
}
return null;
}
public async Task UpdateStreamingState()
{
await Send(new RequestMessage("GetStreamStatus"));
}
public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
{
if (action == null)
return;
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m2, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
return;
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]");
var transform = JsonSerializer.Deserialize<OBSTransformationData>(transformData.ToString()!, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
if (transform == null)
{
_logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m2.RequestId}].");
return;
}
double w = transform.Width;
double h = transform.Height;
int a = transform.Alignment;
bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE";
if (a != (int)OBSAlignment.Center)
{
if (hasBounds)
transform.BoundsAlignment = a = (int)OBSAlignment.Center;
else
transform.Alignment = a = (int)OBSAlignment.Center;
transform.PositionX = transform.PositionX + w / 2;
transform.PositionY = transform.PositionY + h / 2;
}
action?.Invoke(transform);
var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } });
await Send(m3);
_logger.Debug($"New transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m3.RequestId}]");
});
});
}
public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m1, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemEnabled", out object? visible) || visible == null)
return;
var m2 = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", visible.ToString().ToLower() == "true" ? false : true } });
await Send(m2);
});
});
}
public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } });
await Send(m);
});
}
public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } });
await Send(m);
});
}
public async Task GetGroupList(Action<IEnumerable<string>>? action)
{
var m = new RequestMessage("GetGroupList", string.Empty, new Dictionary<string, object>());
await Send(m, (d) =>
{
if (d == null || !d.TryGetValue("groups", out object? value) || value == null)
return;
var list = (IEnumerable<string>)value;
_logger.Debug("Fetched the list of groups in OBS.");
if (list != null)
action?.Invoke(list);
});
}
public async Task GetGroupSceneItemList(string groupName, Action<IEnumerable<OBSSceneItem>>? action)
{
var m = new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", groupName } });
await Send(m, (d) =>
{
if (d == null || !d.TryGetValue("sceneItems", out object? value) || value == null)
return;
var list = (IEnumerable<OBSSceneItem>)value;
_logger.Debug($"Fetched the list of OBS scene items in a group [group: {groupName}]");
if (list != null)
action?.Invoke(list);
});
}
public async Task GetGroupSceneItemList(IEnumerable<string> groupNames)
{
var messages = groupNames.Select(group => new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", group } }));
await Send(messages);
_logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]");
}
private async Task GetSceneItemByName(string sceneName, string sceneItemName, Action<long> action)
{
if (_sourceIds.TryGetValue(sceneItemName, out long sourceId))
{
_logger.Debug($"Fetched scene item id from cache [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sourceId}]");
action.Invoke(sourceId);
return;
}
var m = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
await Send(m, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId))
return;
_logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sceneItemId}][obs request id: {m.RequestId}]");
AddSourceId(sceneItemName, sceneItemId);
action.Invoke(sceneItemId);
});
}
private string GenerateUniqueIdentifier()
{
return Guid.NewGuid().ToString("N");
}
}
public class RequestData
{
public RequestMessage Message { get; }
public string ParentId { get; }
public Dictionary<string, object>? ResponseValues { get; set; }
public Action<Dictionary<string, object>>? Callback { get; set; }
public RequestData(RequestMessage message, string parentId)
{
Message = message;
ParentId = parentId;
}
}
}

View File

@ -1,57 +0,0 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace TwitchChatTTS.Seven.Socket
{
public class SevenManager
{
private readonly User _user;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
private string URL;
public bool Connected { get; set; }
public bool Streaming { get; set; }
public SevenManager(User user, IServiceProvider serviceProvider, ILogger logger)
{
_user = user;
_serviceProvider = serviceProvider;
_logger = logger;
}
public void Initialize() {
_logger.Information("Initializing 7tv websocket client.");
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv");
client.OnConnected += (sender, e) => {
Connected = true;
_logger.Information("7tv websocket client connected.");
};
client.OnDisconnected += (sender, e) => {
Connected = false;
_logger.Information("7tv websocket client disconnected.");
};
if (!string.IsNullOrEmpty(_user.SevenEmoteSetId))
URL = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={_user.SevenEmoteSetId}>";
}
public async Task Connect()
{
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
{
_logger.Warning("Cannot find 7tv data for your channel. Not connecting to 7tv websockets.");
return;
}
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv");
_logger.Debug($"7tv client attempting to connect to {URL}");
await client.ConnectAsync($"{URL}");
}
}
}

View File

@ -1,7 +0,0 @@
namespace TwitchChatTTS.Seven.Socket.Context
{
public class ReconnectContext
{
public string? SessionId;
}
}

View File

@ -1,102 +1,21 @@
using System.Net.WebSockets;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Seven.Socket.Context;
using TwitchChatTTS.Seven.Socket.Data;
namespace TwitchChatTTS.Seven.Socket.Handlers
{
public class EndOfStreamHandler : IWebSocketHandler
{
private readonly ILogger _logger;
private readonly User _user;
private readonly IServiceProvider _serviceProvider;
private readonly string[] _errorCodes;
private readonly int[] _reconnectDelay;
public int OperationCode { get; } = 7;
public EndOfStreamHandler(User user, IServiceProvider serviceProvider, ILogger logger)
{
_logger = logger;
_user = user;
_serviceProvider = serviceProvider;
_errorCodes = [
"Server Error",
"Unknown Operation",
"Invalid Payload",
"Auth Failure",
"Already Identified",
"Rate Limited",
"Restart",
"Maintenance",
"Timeout",
"Already Subscribed",
"Not Subscribed",
"Insufficient Privilege",
"Inactivity?"
];
_reconnectDelay = [
1000,
-1,
-1,
-1,
0,
3000,
1000,
300000,
1000,
0,
0,
1000,
1000
];
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{
if (data is not EndOfStreamMessage message || message == null)
return;
var code = message.Code - 4000;
if (code >= 0 && code < _errorCodes.Length)
_logger.Warning($"Received end of stream message (reason: {_errorCodes[code]}, code: {message.Code}, message: {message.Message}).");
else
_logger.Warning($"Received end of stream message (code: {message.Code}, message: {message.Message}).");
await sender.DisconnectAsync();
if (code >= 0 && code < _reconnectDelay.Length && _reconnectDelay[code] < 0)
{
_logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
return;
}
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
{
_logger.Warning("Could not find the 7tv emote set id. Not reconnecting.");
return;
}
var context = _serviceProvider.GetRequiredService<ReconnectContext>();
if (_reconnectDelay[code] > 0)
await Task.Delay(_reconnectDelay[code]);
var manager = _serviceProvider.GetRequiredService<SevenManager>();
await manager.Connect();
if (context.SessionId != null)
{
await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId });
_logger.Debug("Resumed connection to 7tv websocket.");
}
else
{
_logger.Debug("Resumed connection to 7tv websocket on a different session.");
}
await sender.DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), code.ToString()));
}
}
}

View File

@ -19,7 +19,6 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
{
if (data is not SevenHelloMessage message || message == null)
return;
if (sender is not SevenSocketClient seven || seven == null)
return;

View File

@ -1,41 +0,0 @@
using Serilog;
using CommonSocketLibrary.Socket.Manager;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
namespace TwitchChatTTS.Seven.Socket.Managers
{
public class SevenHandlerManager : WebSocketHandlerManager
{
public SevenHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
{
try
{
var basetype = typeof(IWebSocketHandler);
var assembly = GetType().Assembly;
var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Seven.") == true);
foreach (var type in types)
{
var key = "7tv-" + type.Name.Replace("Handlers", "Hand#lers")
.Replace("Handler", "")
.Replace("Hand#lers", "Handlers")
.ToLower();
var handler = provider.GetKeyedService<IWebSocketHandler>(key);
if (handler == null)
{
logger.Error("Failed to find 7tv websocket handler: " + type.AssemblyQualifiedName);
continue;
}
_logger.Debug($"Linked type {type.AssemblyQualifiedName} to 7tv websocket handler {handler.GetType().AssemblyQualifiedName}.");
Add(handler);
}
}
catch (Exception e)
{
_logger.Error(e, "Failed to load 7tv websocket handler types.");
}
}
}
}

View File

@ -1,4 +1,3 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection;
@ -6,13 +5,12 @@ using Serilog;
namespace TwitchChatTTS.Seven.Socket.Managers
{
public class SevenHandlerTypeManager : WebSocketHandlerTypeManager
public class SevenMessageTypeManager : WebSocketMessageTypeManager
{
public SevenHandlerTypeManager(
ILogger factory,
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient,
IWebSocketHandler> handlers
) : base(factory, handlers)
public SevenMessageTypeManager(
[FromKeyedServices("7tv")] IEnumerable<IWebSocketHandler> handlers,
ILogger logger
) : base(handlers, logger)
{
}
}

View File

@ -9,19 +9,133 @@ namespace TwitchChatTTS.Seven.Socket
{
public class SevenSocketClient : WebSocketClient
{
private readonly User _user;
private readonly string[] _errorCodes;
private readonly int[] _reconnectDelay;
private string? URL;
public bool Connected { get; set; }
public SevenHelloMessage? ConnectionDetails { get; set; }
public SevenSocketClient(
ILogger logger,
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
[FromKeyedServices("7tv")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
User user,
[FromKeyedServices("7tv")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("7tv")] MessageTypeManager<IWebSocketHandler> typeManager,
ILogger logger
) : base(handlers, typeManager, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
})
}, logger)
{
_user = user;
ConnectionDetails = null;
_errorCodes = [
"Server Error",
"Unknown Operation",
"Invalid Payload",
"Auth Failure",
"Already Identified",
"Rate Limited",
"Restart",
"Maintenance",
"Timeout",
"Already Subscribed",
"Not Subscribed",
"Insufficient Privilege",
"Inactivity?"
];
_reconnectDelay = [
1000,
-1,
-1,
-1,
0,
3000,
1000,
300000,
1000,
0,
0,
1000,
1000
];
}
public void Initialize()
{
_logger.Information("Initializing 7tv websocket client.");
OnConnected += (sender, e) =>
{
Connected = true;
_logger.Information("7tv websocket client connected.");
};
OnDisconnected += (sender, e) => OnDisconnection(sender, e);
if (!string.IsNullOrEmpty(_user.SevenEmoteSetId))
URL = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={_user.SevenEmoteSetId}>";
}
public async Task Connect()
{
if (string.IsNullOrEmpty(URL))
{
_logger.Warning("Cannot find 7tv url. Not connecting to 7tv websockets.");
return;
}
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
{
_logger.Warning("Cannot find 7tv data for your channel. Not connecting to 7tv websockets.");
return;
}
_logger.Debug($"7tv client attempting to connect to {URL}");
await ConnectAsync($"{URL}");
}
private async void OnDisconnection(object? sender, SocketDisconnectionEventArgs e)
{
Connected = false;
if (int.TryParse(e.Reason, out int code))
{
if (code >= 0 && code < _errorCodes.Length)
_logger.Warning($"Received end of stream message for 7tv websocket [reason: {_errorCodes[code]}][code: {code}]");
else
_logger.Warning($"Received end of stream message for 7tv websocket [code: {code}]");
if (code >= 0 && code < _reconnectDelay.Length && _reconnectDelay[code] < 0)
{
_logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
return;
}
if (_reconnectDelay[code] > 0)
await Task.Delay(_reconnectDelay[code]);
}
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
{
_logger.Warning("Could not find the 7tv emote set id. Not reconnecting.");
return;
}
await Connect();
await Task.Delay(TimeSpan.FromMilliseconds(500));
if (Connected && ConnectionDetails?.SessionId != null)
{
await Send(34, new ResumeMessage() { SessionId = ConnectionDetails.SessionId });
_logger.Debug("Resumed connection to 7tv websocket.");
}
else
{
_logger.Debug("Resumed connection to 7tv websocket on a different session.");
}
}
}
}

View File

@ -10,7 +10,6 @@ using YamlDotNet.Serialization.NamingConventions;
using TwitchChatTTS.Seven.Socket;
using TwitchChatTTS.OBS.Socket.Handlers;
using TwitchChatTTS.Seven.Socket.Handlers;
using TwitchChatTTS.Seven.Socket.Context;
using TwitchLib.Client.Interfaces;
using TwitchLib.Client;
using TwitchLib.PubSub.Interfaces;
@ -31,6 +30,7 @@ using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Emotes;
using HermesSocketLibrary.Requests.Callbacks;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
@ -70,21 +70,16 @@ s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
});
// Command parameters
s.AddKeyedSingleton<ChatCommandParameter, TTSVoiceNameParameter>("parameter-ttsvoicename");
s.AddKeyedSingleton<ChatCommandParameter, UnvalidatedParameter>("parameter-unvalidated");
s.AddKeyedSingleton<ChatCommandParameter, SimpleListedParameter>("parameter-simplelisted");
s.AddKeyedSingleton<ChatCommand, SkipAllCommand>("command-skipall");
s.AddKeyedSingleton<ChatCommand, SkipCommand>("command-skip");
s.AddKeyedSingleton<ChatCommand, VoiceCommand>("command-voice");
s.AddKeyedSingleton<ChatCommand, AddTTSVoiceCommand>("command-addttsvoice");
s.AddKeyedSingleton<ChatCommand, RemoveTTSVoiceCommand>("command-removettsvoice");
s.AddKeyedSingleton<ChatCommand, RefreshTTSDataCommand>("command-refreshttsdata");
s.AddKeyedSingleton<ChatCommand, OBSCommand>("command-obs");
s.AddKeyedSingleton<ChatCommand, TTSCommand>("command-tts");
s.AddKeyedSingleton<ChatCommand, VersionCommand>("command-version");
s.AddSingleton<IChatCommand, SkipCommand>();
s.AddSingleton<IChatCommand, VoiceCommand>();
s.AddSingleton<IChatCommand, RefreshCommand>();
s.AddSingleton<IChatCommand, OBSCommand>();
s.AddSingleton<IChatCommand, TTSCommand>();
s.AddSingleton<IChatCommand, VersionCommand>();
s.AddSingleton<ICommandBuilder, CommandBuilder>();
s.AddSingleton<IChatterGroupManager, ChatterGroupManager>();
s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>();
s.AddSingleton<ChatCommandManager>();
s.AddSingleton<CommandManager>();
s.AddSingleton<TTSPlayer>();
s.AddSingleton<ChatMessageHandler>();
@ -100,48 +95,32 @@ s.AddSingleton<SevenApiClient>();
s.AddSingleton<IEmoteDatabase, EmoteDatabase>();
// OBS websocket
s.AddSingleton<OBSManager>();
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello");
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified");
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs-requestresponse");
s.AddKeyedSingleton<IWebSocketHandler, RequestBatchResponseHandler>("obs-requestbatchresponse");
s.AddKeyedSingleton<IWebSocketHandler, EventMessageHandler>("obs-eventmessage");
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, RequestBatchResponseHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, EventMessageHandler>("obs");
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, OBSHandlerManager>("obs");
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, OBSHandlerTypeManager>("obs");
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, OBSMessageTypeManager>("obs");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, OBSSocketClient>("obs");
// 7tv websocket
s.AddTransient(sp =>
{
var logger = sp.GetRequiredService<ILogger>();
var client = sp.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv") as SevenSocketClient;
if (client == null)
return new ReconnectContext() { SessionId = null };
if (client.ConnectionDetails == null)
return new ReconnectContext() { SessionId = null };
return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId };
});
s.AddKeyedSingleton<IWebSocketHandler, SevenHelloHandler>("7tv-sevenhello");
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("7tv-hello");
s.AddKeyedSingleton<IWebSocketHandler, DispatchHandler>("7tv-dispatch");
s.AddKeyedSingleton<IWebSocketHandler, ReconnectHandler>("7tv-reconnect");
s.AddKeyedSingleton<IWebSocketHandler, ErrorHandler>("7tv-error");
s.AddKeyedSingleton<IWebSocketHandler, EndOfStreamHandler>("7tv-endofstream");
s.AddKeyedSingleton<IWebSocketHandler, SevenHelloHandler>("7tv");
s.AddKeyedSingleton<IWebSocketHandler, DispatchHandler>("7tv");
s.AddKeyedSingleton<IWebSocketHandler, ReconnectHandler>("7tv");
s.AddKeyedSingleton<IWebSocketHandler, ErrorHandler>("7tv");
s.AddKeyedSingleton<IWebSocketHandler, EndOfStreamHandler>("7tv");
s.AddSingleton<SevenManager>();
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, SevenHandlerManager>("7tv");
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, SevenHandlerTypeManager>("7tv");
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, SevenMessageTypeManager>("7tv");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
// hermes websocket
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes-heartbeat");
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes-loginack");
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes-requestack");
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes-error");
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes");
//s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, HermesHandlerManager>("hermes");
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, HermesHandlerTypeManager>("hermes");
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");
s.AddHostedService<TTS>();

109
TTS.cs
View File

@ -5,33 +5,30 @@ using Microsoft.Extensions.Hosting;
using Serilog;
using NAudio.Wave.SampleProviders;
using TwitchLib.Client.Events;
using TwitchChatTTS.Twitch.Redemptions;
using org.mariuszgromada.math.mxparser;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchChatTTS.Seven.Socket;
using TwitchChatTTS.Chat.Emotes;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using TwitchChatTTS.OBS.Socket;
namespace TwitchChatTTS
{
public class TTS : IHostedService
{
public const int MAJOR_VERSION = 3;
public const int MINOR_VERSION = 9;
public const int MINOR_VERSION = 10;
private readonly User _user;
private readonly HermesApiClient _hermesApiClient;
private readonly SevenApiClient _sevenApiClient;
private readonly OBSManager _obsManager;
private readonly SevenManager _sevenManager;
private readonly OBSSocketClient _obs;
private readonly SevenSocketClient _seven;
private readonly HermesSocketClient _hermes;
private readonly RedemptionManager _redemptionManager;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly IGroupPermissionManager _permissionManager;
private readonly IEmoteDatabase _emotes;
private readonly Configuration _configuration;
private readonly TTSPlayer _player;
private readonly IServiceProvider _serviceProvider;
@ -41,12 +38,10 @@ namespace TwitchChatTTS
User user,
HermesApiClient hermesApiClient,
SevenApiClient sevenApiClient,
OBSManager obsManager,
SevenManager sevenManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
RedemptionManager redemptionManager,
IChatterGroupManager chatterGroupManager,
IGroupPermissionManager permissionManager,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
[FromKeyedServices("7tv")] SocketClient<WebSocketMessage> seven,
IEmoteDatabase emotes,
Configuration configuration,
TTSPlayer player,
IServiceProvider serviceProvider,
@ -56,12 +51,10 @@ namespace TwitchChatTTS
_user = user;
_hermesApiClient = hermesApiClient;
_sevenApiClient = sevenApiClient;
_obsManager = obsManager;
_sevenManager = sevenManager;
_hermes = (hermes as HermesSocketClient)!;
_redemptionManager = redemptionManager;
_chatterGroupManager = chatterGroupManager;
_permissionManager = permissionManager;
_obs = (obs as OBSSocketClient)!;
_seven = (seven as SevenSocketClient)!;
_emotes = emotes;
_configuration = configuration;
_player = player;
_serviceProvider = serviceProvider;
@ -119,14 +112,6 @@ namespace TwitchChatTTS
await InitializeSevenTv();
await InitializeObs();
// _logger.Information("Sending a request to server...");
// await _hermesManager.Send(3, new RequestMessage() {
// Type = "get_redeemable_actions",
// Data = new Dictionary<string, object>()
// });
// _logger.Warning("OS VERSION: " + Environment.OSVersion + " | " + Environment.OSVersion.Platform);
// return;
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
{
if (e.SampleProvider == _player.Playing)
@ -239,65 +224,8 @@ namespace TwitchChatTTS
user.TwitchUsername = hermesAccount.Username;
var twitchBotToken = await hermes.FetchTwitchBotToken();
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId);
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId!);
_logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]");
// user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
// _logger.Information("TTS Default Voice: " + user.DefaultTTSVoice);
// var wordFilters = await hermes.FetchTTSWordFilters();
// user.RegexFilters = wordFilters.ToList();
// _logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
var voicesSelected = await hermes.FetchTTSChatterSelectedVoices();
user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice);
_logger.Information($"{user.VoicesSelected.Count} chatters have selected a specific TTS voice, among {user.VoicesSelected.Values.Distinct().Count()} distinct TTS voices.");
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
if (voicesEnabled == null || !voicesEnabled.Any())
user.VoicesEnabled = new HashSet<string>([user.DefaultTTSVoice]);
else
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
var defaultedChatters = voicesSelected.Where(item => item.Voice == null || !user.VoicesEnabled.Contains(item.Voice));
if (defaultedChatters.Any())
_logger.Information($"{defaultedChatters.Count()} chatter(s) will have their TTS voice set to default due to having selected a disabled TTS voice.");
// var redemptionActions = await hermes.FetchRedeemableActions();
// var redemptions = await hermes.FetchRedemptions();
// _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a));
// _logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
var groups = await hermes.FetchGroups();
var groupsById = groups.ToDictionary(g => g.Id, g => g);
foreach (var group in groups)
_chatterGroupManager.Add(group);
_logger.Information($"{groups.Count()} groups have been loaded.");
var groupChatters = await hermes.FetchGroupChatters();
_logger.Debug($"{groupChatters.Count()} group users have been fetched.");
var permissions = await hermes.FetchGroupPermissions();
foreach (var permission in permissions)
{
_logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
if (!groupsById.TryGetValue(permission.GroupId, out var group))
{
_logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
continue;
}
var path = $"{group.Name}.{permission.Path}";
_permissionManager.Set(path, permission.Allow);
_logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
}
_logger.Information($"{permissions.Count()} group permissions have been loaded.");
foreach (var chatter in groupChatters)
if (groupsById.TryGetValue(chatter.GroupId, out var group))
_chatterGroupManager.Add(chatter.ChatterId, group.Name);
_logger.Information($"Users in each group have been loaded.");
}
private async Task InitializeHermesWebsocket()
@ -317,8 +245,8 @@ namespace TwitchChatTTS
{
try
{
_sevenManager.Initialize();
await _sevenManager.Connect();
_seven.Initialize();
await _seven.Connect();
}
catch (Exception e)
{
@ -330,8 +258,8 @@ namespace TwitchChatTTS
{
try
{
_obsManager.Initialize();
await _obsManager.Connect();
_obs.Initialize();
await _obs.Connect();
}
catch (Exception)
{
@ -376,20 +304,19 @@ namespace TwitchChatTTS
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes)
{
var emotes = _serviceProvider.GetRequiredService<IEmoteDatabase>();
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();
if (channelEmotes != null && channelEmotes.Emotes.Any())
{
_logger.Information($"Loaded {channelEmotes.Emotes.Count()} 7tv channel emotes.");
foreach (var entry in channelEmotes.Emotes)
emotes.Add(entry.Name, entry.Id);
_emotes.Add(entry.Name, entry.Id);
}
if (globalEmotes != null && globalEmotes.Any())
{
_logger.Information($"Loaded {globalEmotes.Count()} 7tv global emotes.");
foreach (var entry in globalEmotes)
emotes.Add(entry.Name, entry.Id);
_emotes.Add(entry.Name, entry.Id);
}
}
}

View File

@ -6,8 +6,8 @@ using Microsoft.Extensions.DependencyInjection;
using org.mariuszgromada.math.mxparser;
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.Twitch.Redemptions
{
@ -15,7 +15,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
{
private readonly IDictionary<string, IList<RedeemableAction>> _store;
private readonly User _user;
private readonly OBSManager _obsManager;
private readonly OBSSocketClient _obs;
private readonly HermesSocketClient _hermes;
private readonly ILogger _logger;
private readonly Random _random;
@ -24,13 +24,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
public RedemptionManager(
User user,
OBSManager obsManager,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
ILogger logger)
{
_store = new Dictionary<string, IList<RedeemableAction>>();
_user = user;
_obsManager = obsManager;
_obs = (obs as OBSSocketClient)!;
_hermes = (hermes as HermesSocketClient)!;
_logger = logger;
_random = new Random();
@ -72,7 +72,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
break;
case "OBS_TRANSFORM":
var type = typeof(OBSTransformationData);
await _obsManager.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) =>
await _obs.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) =>
{
string[] properties = ["rotation", "position_x", "position_y"];
foreach (var property in properties)
@ -111,13 +111,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
});
break;
case "TOGGLE_OBS_VISIBILITY":
await _obsManager.ToggleSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"]);
await _obs.ToggleSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"]);
break;
case "SPECIFIC_OBS_VISIBILITY":
await _obsManager.UpdateSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"], action.Data["obs_visible"].ToLower() == "true");
await _obs.UpdateSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"], action.Data["obs_visible"].ToLower() == "true");
break;
case "SPECIFIC_OBS_INDEX":
await _obsManager.UpdateSceneItemIndex(action.Data["scene_name"], action.Data["scene_item_name"], int.Parse(action.Data["obs_index"]));
await _obs.UpdateSceneItemIndex(action.Data["scene_name"], action.Data["scene_item_name"], int.Parse(action.Data["obs_index"]));
break;
case "SLEEP":
_logger.Debug("Sleeping on thread due to redemption for OBS.");

View File

@ -31,10 +31,6 @@ namespace TwitchChatTTS
private HashSet<string> _voicesEnabled;
public User()
{
}
private Regex? GenerateEnabledVoicesRegex()
{
if (VoicesAvailable == null || VoicesAvailable.Count() <= 0)