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.Hermes.Socket;
using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchChatTTS.Chat.Emotes; using TwitchChatTTS.Chat.Emotes;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using TwitchChatTTS.OBS.Socket;
public class ChatMessageHandler public class ChatMessageHandler
{ {
private readonly User _user; private readonly User _user;
private readonly TTSPlayer _player; private readonly TTSPlayer _player;
private readonly ChatCommandManager _commands; private readonly CommandManager _commands;
private readonly IGroupPermissionManager _permissionManager; private readonly IGroupPermissionManager _permissionManager;
private readonly IChatterGroupManager _chatterGroupManager; private readonly IChatterGroupManager _chatterGroupManager;
private readonly IEmoteDatabase _emotes; private readonly IEmoteDatabase _emotes;
private readonly OBSManager _obsManager; private readonly OBSSocketClient _obs;
private readonly HermesSocketClient _hermes; private readonly HermesSocketClient _hermes;
private readonly Configuration _configuration; private readonly Configuration _configuration;
@ -36,12 +36,12 @@ public class ChatMessageHandler
public ChatMessageHandler( public ChatMessageHandler(
User user, User user,
TTSPlayer player, TTSPlayer player,
ChatCommandManager commands, CommandManager commands,
IGroupPermissionManager permissionManager, IGroupPermissionManager permissionManager,
IChatterGroupManager chatterGroupManager, IChatterGroupManager chatterGroupManager,
IEmoteDatabase emotes, IEmoteDatabase emotes,
OBSManager obsManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes, [FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
Configuration configuration, Configuration configuration,
ILogger logger ILogger logger
) )
@ -52,7 +52,7 @@ public class ChatMessageHandler
_permissionManager = permissionManager; _permissionManager = permissionManager;
_chatterGroupManager = chatterGroupManager; _chatterGroupManager = chatterGroupManager;
_emotes = emotes; _emotes = emotes;
_obsManager = obsManager; _obs = (obs as OBSSocketClient)!;
_hermes = (hermes as HermesSocketClient)!; _hermes = (hermes as HermesSocketClient)!;
_configuration = configuration; _configuration = configuration;
_logger = logger; _logger = logger;
@ -71,7 +71,7 @@ public class ChatMessageHandler
_logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]"); _logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]");
return new MessageResult(MessageStatus.NotReady, -1, -1); 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}]"); _logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {m.Id}]");
return new MessageResult(MessageStatus.NotReady, -1, -1); return new MessageResult(MessageStatus.NotReady, -1, -1);
@ -109,7 +109,7 @@ public class ChatMessageHandler
return new MessageResult(MessageStatus.Blocked, -1, -1); 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)); tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username));
_chatters.Add(chatterId); _chatters.Add(chatterId);
@ -148,7 +148,7 @@ public class ChatMessageHandler
if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5)) if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
filteredMsg += w + " "; filteredMsg += w + " ";
} }
if (_obsManager.Streaming && newEmotes.Any()) if (_obs.Streaming && newEmotes.Any())
tasks.Add(_hermes.SendEmoteDetails(newEmotes)); tasks.Add(_hermes.SendEmoteDetails(newEmotes));
msg = filteredMsg; 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 TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
{ {
public abstract class ChatCommand public interface IChatCommand {
{ string Name { get; }
public string Name { get; } void Build(ICommandBuilder builder);
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>();
} }
protected void AddParameter(ChatCommandParameter parameter, bool optional = false) public interface IChatPartialCommand {
{ bool AcceptCustomPermission { get; }
if (parameter != null && parameter.Clone() is ChatCommandParameter p) { bool CheckDefaultPermissions(ChatMessage message);
_parameters.Add(optional ? p.Permissive() : p); Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client);
}
}
public abstract Task<bool> CheckDefaultPermissions(ChatMessage message);
public abstract Task Execute(IList<string> args, 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 CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
{ {
public class OBSCommand : ChatCommand public class OBSCommand : IChatCommand
{ {
private readonly User _user; private readonly OBSSocketClient _obs;
private readonly OBSManager _manager;
private readonly ILogger _logger; private readonly ILogger _logger;
public OBSCommand( public string Name => "obs";
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
User user,
OBSManager manager,
ILogger logger
) : base("obs", "Various obs commands.")
{
_user = user;
_manager = manager;
_logger = logger;
AddParameter(unvalidatedParameter); public OBSCommand(
AddParameter(unvalidatedParameter, optional: true); [FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
AddParameter(unvalidatedParameter, optional: true); ILogger logger
AddParameter(unvalidatedParameter, optional: true); )
{
_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; 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) string sceneName = values["sceneName"];
return; 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(); private sealed class OBSTransform : IChatPartialCommand
switch (action)
{ {
case "get_scene_item_id": private readonly OBSSocketClient _obs;
if (args.Count < 3) private readonly ILogger _logger;
return;
_logger.Debug($"Getting scene item id via chat command [args: {string.Join(" ", args)}]"); public string Name => "obs";
await _manager.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", args[1] }, { "sourceName", args[2] } })); public bool AcceptCustomPermission { get => true; }
break;
case "transform":
if (args.Count < 5)
return;
_logger.Debug($"Getting scene item transformation data via chat command [args: {string.Join(" ", args)}]"); public OBSTransform(
await _manager.UpdateTransformation(args[1], args[2], (d) => [FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
ILogger logger
)
{ {
if (args[3].ToLower() == "rotation") _obs = (obs as OBSSocketClient)!;
d.Rotation = int.Parse(args[4]); _logger = logger;
else if (args[3].ToLower() == "x") }
d.Rotation = int.Parse(args[4]);
else if (args[3].ToLower() == "y") public bool CheckDefaultPermissions(ChatMessage message)
d.PositionY = int.Parse(args[4]); {
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)}]"); private sealed class OBSVisibility : IChatPartialCommand
await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", int.Parse(args[1]) } })); {
break; private readonly OBSSocketClient _obs;
case "visibility": private readonly ILogger _logger;
if (args.Count < 4)
return;
_logger.Debug($"Updating scene item visibility via chat command [args: {string.Join(" ", args)}]"); public string Name => "obs";
await _manager.UpdateSceneItemVisibility(args[1], args[2], args[3].ToLower() == "true"); public bool AcceptCustomPermission { get => true; }
break;
default: public OBSVisibility(
break; [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 namespace TwitchChatTTS.Chat.Commands.Parameters
{ {
public class TTSVoiceNameParameter : ChatCommandParameter public class TTSVoiceNameParameter : CommandParameter
{ {
private bool _enabled;
private readonly User _user; 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; _user = user;
} }
@ -15,6 +17,9 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
return false; return false;
value = value.ToLower(); value = value.ToLower();
if (_enabled)
return _user.VoicesEnabled.Any(v => v.ToLower() == value);
return _user.VoicesAvailable.Any(e => e.Value.ToLower() == value); return _user.VoicesAvailable.Any(e => e.Value.ToLower() == value);
} }
} }

View File

@ -1,8 +1,8 @@
namespace TwitchChatTTS.Chat.Commands.Parameters 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 Serilog;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands 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 TTSPlayer _ttsPlayer;
private readonly ILogger _logger; private readonly ILogger _logger;
public SkipCommand(TTSPlayer ttsPlayer, ILogger logger) public bool AcceptCustomPermission { get => true; }
: base("skip", "Skips the current text to speech message.")
public TTSPlayerSkipCommand(TTSPlayer ttsPlayer, ILogger logger)
{ {
_ttsPlayer = ttsPlayer; _ttsPlayer = ttsPlayer;
_logger = logger; _logger = logger;
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message) public bool CheckDefaultPermissions(ChatMessage message)
{ {
return message.IsModerator || message.IsVip || message.IsBroadcaster; 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) if (_ttsPlayer.Playing == null)
return; return;
@ -32,4 +62,37 @@ namespace TwitchChatTTS.Chat.Commands
_logger.Information("Skipped current tts."); _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 Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
{ {
public class TTSCommand : ChatCommand public class TTSCommand : IChatCommand
{ {
private readonly User _user; private readonly User _user;
private readonly ILogger _logger; private readonly ILogger _logger;
public TTSCommand(
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, public TTSCommand(User user, ILogger logger)
User user,
ILogger logger
) : base("tts", "Various tts commands.")
{ {
_user = user; _user = user;
_logger = logger; _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) if (_user == null || _user.VoicesAvailable == null)
return; return;
var voiceName = args[0].ToLower(); var voiceName = values["voiceName"];
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; var voiceNameLower = voiceName.ToLower();
var action = args[1].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.CreateTTSVoice(voiceName);
await client.UpdateTTSVoiceState(voiceId, state); _logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]");
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {state}][invoker: {message.Username}][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 Serilog;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
{ {
public class VersionCommand : ChatCommand public class VersionCommand : IChatCommand
{ {
private readonly User _user; private readonly User _user;
private ILogger _logger; private ILogger _logger;
public string Name => "version";
public VersionCommand(User user, ILogger logger) public VersionCommand(User user, ILogger logger)
: base("version", "Does nothing.")
{ {
_user = user; _user = user;
_logger = logger; _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; 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}."); 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 Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
{ {
public class VoiceCommand : ChatCommand public class VoiceCommand : IChatCommand
{ {
private readonly User _user; private readonly User _user;
private readonly ILogger _logger; private readonly ILogger _logger;
public VoiceCommand( public VoiceCommand(User user, ILogger logger)
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
User user,
ILogger logger
) : base("voice", "Select a TTS voice as the default for that user.")
{ {
_user = user; _user = user;
_logger = logger; _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; 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; return;
long chatterId = long.Parse(message.UserId); long chatterId = long.Parse(message.UserId);
var voiceName = args.First().ToLower(); var voiceName = values["voiceName"];
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName); var voiceNameLower = voiceName.ToLower();
var enabled = _user.VoicesEnabled.Contains(voice.Value); var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceNameLower);
if (!enabled)
{
_logger.Information($"Voice is disabled. Cannot switch to that voice [voice: {voice.Value}][username: {message.Username}]");
return;
}
if (_user.VoicesSelected.ContainsKey(chatterId)) if (_user.VoicesSelected.ContainsKey(chatterId))
{ {
@ -57,3 +69,4 @@ namespace TwitchChatTTS.Chat.Commands
} }
} }
} }
}

View File

@ -1,5 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using HermesSocketLibrary.Requests.Messages;
using Serilog; using Serilog;
namespace TwitchChatTTS.Chat.Groups namespace TwitchChatTTS.Chat.Groups
@ -59,7 +60,10 @@ namespace TwitchChatTTS.Chat.Groups
} }
public int GetPriorityFor(IEnumerable<string> groupNames) { 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) { 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 namespace TwitchChatTTS.Chat.Groups
{ {
public interface IChatterGroupManager 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() public void Clear()
{ {
if (_root.Children != null) _root.Clear();
_root.Children.Clear();
} }
public bool Remove(string path) public bool Remove(string path)
@ -127,6 +126,11 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
_children.Add(child); _children.Add(child);
} }
internal void Clear() {
if (_children != null)
_children.Clear();
}
public void Remove(string name) public void Remove(string name)
{ {
if (_children == null || !_children.Any()) if (_children == null || !_children.Any())

View File

@ -23,7 +23,7 @@ namespace TwitchChatTTS.Helpers
_client.DefaultRequestHeaders.Add(key, value); _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); var response = await _client.GetAsync(uri);
return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), options ?? _options); return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), options ?? _options);

View File

@ -40,39 +40,15 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
client.LoggedIn = true; client.LoggedIn = true;
_logger.Information($"Logged in as {_user.TwitchUsername} {(message.WebLogin ? "via web" : "via TTS app")}."); _logger.Information($"Logged in as {_user.TwitchUsername} {(message.WebLogin ? "via web" : "via TTS app")}.");
await client.Send(3, new RequestMessage() await client.FetchTTSVoices();
{ await client.FetchEnabledTTSVoices();
Type = "get_tts_voices", await client.FetchTTSWordFilters();
Data = null await client.FetchTTSChatterVoices();
}); await client.FetchDefaultTTSVoice();
await client.FetchChatterIdentifiers();
await client.Send(3, new RequestMessage() await client.FetchEmotes();
{ await client.FetchRedemptions();
Type = "get_tts_users", await client.FetchPermissions();
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));
_logger.Information("TTS is now ready."); _logger.Information("TTS is now ready.");
client.Ready = true; client.Ready = true;

View File

@ -8,6 +8,8 @@ using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Emotes; using TwitchChatTTS.Chat.Emotes;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Twitch.Redemptions; using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Handlers namespace TwitchChatTTS.Hermes.Socket.Handlers
@ -28,7 +30,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
public RequestAckHandler( public RequestAckHandler(
User user, User user,
//RedemptionManager redemptionManager,
ICallbackManager<HermesRequestData> callbackManager, ICallbackManager<HermesRequestData> callbackManager,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
JsonSerializerOptions options, JsonSerializerOptions options,
@ -36,7 +37,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
) )
{ {
_user = user; _user = user;
//_redemptionManager = redemptionManager;
_callbackManager = callbackManager; _callbackManager = callbackManager;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_options = options; _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>())}]"); _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") 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); var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(message.Data.ToString(), _options);
if (voices == null) if (voices == null)
return; return;
@ -79,7 +78,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
} }
else if (message.Request.Type == "create_tts_user") 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)) if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
{ {
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]"); _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") 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)) if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
{ {
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]"); _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") else if (message.Request.Type == "create_tts_voice")
{ {
_logger.Verbose("Creating new tts voice.");
string? voice = message.Request.Data["voice"].ToString(); string? voice = message.Request.Data["voice"].ToString();
string? voiceId = message.Data.ToString(); string? voiceId = message.Data.ToString();
if (voice == null || voiceId == null) if (voice == null || voiceId == null)
@ -123,7 +119,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
} }
else if (message.Request.Type == "delete_tts_voice") else if (message.Request.Type == "delete_tts_voice")
{ {
_logger.Verbose("Deleting tts voice.");
var voice = message.Request.Data["voice"].ToString(); var voice = message.Request.Data["voice"].ToString();
if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null) if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null)
return; return;
@ -138,7 +133,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
} }
else if (message.Request.Type == "update_tts_voice") else if (message.Request.Type == "update_tts_voice")
{ {
_logger.Verbose("Updating TTS voice.");
string voiceId = message.Request.Data["idd"].ToString(); string voiceId = message.Request.Data["idd"].ToString();
string voice = message.Request.Data["voice"].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") 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); var users = JsonSerializer.Deserialize<IDictionary<long, string>>(message.Data.ToString(), _options);
if (users == null) if (users == null)
return; return;
@ -163,7 +156,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
} }
else if (message.Request.Type == "get_chatter_ids") else if (message.Request.Type == "get_chatter_ids")
{ {
_logger.Verbose("Fetching all chatters' id.");
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(message.Data.ToString(), _options); var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(message.Data.ToString(), _options);
if (chatters == null) if (chatters == null)
return; return;
@ -174,7 +166,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
} }
else if (message.Request.Type == "get_emotes") else if (message.Request.Type == "get_emotes")
{ {
_logger.Verbose("Updating emotes.");
var emotes = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(message.Data.ToString(), _options); var emotes = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(message.Data.ToString(), _options);
if (emotes == null) if (emotes == null)
return; return;
@ -196,9 +187,78 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (duplicateNames > 0) if (duplicateNames > 0)
_logger.Warning($"Found {duplicateNames} emotes with duplicate names."); _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") else if (message.Request.Type == "update_tts_voice_state")
{ {
_logger.Verbose("Updating TTS voice states.");
string voiceId = message.Request.Data["voice"].ToString(); string voiceId = message.Request.Data["voice"].ToString();
bool state = message.Request.Data["state"].ToString().ToLower() == "true"; 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") else if (message.Request.Type == "get_redemptions")
{ {
_logger.Verbose("Fetching all the redemptions.");
IEnumerable<Redemption>? redemptions = JsonSerializer.Deserialize<IEnumerable<Redemption>>(message.Data!.ToString()!, _options); IEnumerable<Redemption>? redemptions = JsonSerializer.Deserialize<IEnumerable<Redemption>>(message.Data!.ToString()!, _options);
if (redemptions != null) if (redemptions != null)
{ {
@ -229,7 +288,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
} }
else if (message.Request.Type == "get_redeemable_actions") 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); IEnumerable<RedeemableAction>? actions = JsonSerializer.Deserialize<IEnumerable<RedeemableAction>>(message.Data!.ToString()!, _options);
if (actions == null) if (actions == null)
{ {

View File

@ -36,14 +36,14 @@ namespace TwitchChatTTS.Hermes.Socket
User user, User user,
Configuration configuration, Configuration configuration,
ICallbackManager<HermesRequestData> callbackManager, ICallbackManager<HermesRequestData> callbackManager,
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager, [FromKeyedServices("hermes")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager, [FromKeyedServices("hermes")] MessageTypeManager<IWebSocketHandler> typeManager,
ILogger logger ILogger logger
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() ) : base(handlers, typeManager, new JsonSerializerOptions()
{ {
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}) }, logger)
{ {
_user = user; _user = user;
_configuration = configuration; _configuration = configuration;
@ -74,7 +74,7 @@ namespace TwitchChatTTS.Hermes.Socket
if (!Connected) if (!Connected)
return; return;
await DisconnectAsync(); await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed"));
} }
public async Task CreateTTSVoice(string voiceName) 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() 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>() 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() 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() public void Initialize()
{ {
_logger.Information("Initializing Hermes websocket client."); _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 namespace TwitchChatTTS.Hermes.Socket.Managers
{ {
public class HermesHandlerTypeManager : WebSocketHandlerTypeManager public class HermesMessageTypeManager : WebSocketMessageTypeManager
{ {
public HermesHandlerTypeManager( public HermesMessageTypeManager(
ILogger factory, [FromKeyedServices("hermes")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers ILogger logger
) : base(factory, handlers) ) : base(handlers, logger)
{ {
} }

View File

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

View File

@ -2,19 +2,18 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Serilog; using Serilog;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
public class EventMessageHandler : IWebSocketHandler public class EventMessageHandler : IWebSocketHandler
{ {
private readonly OBSManager _manager;
private readonly ILogger _logger; private readonly ILogger _logger;
public int OperationCode { get; } = 5; public int OperationCode { get; } = 5;
public EventMessageHandler(OBSManager manager, ILogger logger) public EventMessageHandler(
ILogger logger
)
{ {
_manager = manager;
_logger = logger; _logger = logger;
} }
@ -22,6 +21,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
if (data is not EventMessage message || message == null) if (data is not EventMessage message || message == null)
return; return;
if (sender is not OBSSocketClient obs)
return;
switch (message.EventType) switch (message.EventType)
{ {
@ -31,10 +32,10 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
string? raw_state = message.EventData["outputState"].ToString(); string? raw_state = message.EventData["outputState"].ToString();
string? state = raw_state?.Substring(21).ToLower(); 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 + "."); _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 // 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(); string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim();
_logger.Verbose("OBS websocket password: " + password); _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; return;
} }
@ -39,7 +39,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
string secret = password + salt; string secret = password + salt;
byte[] bytes = Encoding.UTF8.GetBytes(secret); byte[] bytes = Encoding.UTF8.GetBytes(secret);
string hash = null; string? hash = null;
using (var sha = SHA256.Create()) using (var sha = SHA256.Create())
{ {
bytes = sha.ComputeHash(bytes); bytes = sha.ComputeHash(bytes);

View File

@ -2,19 +2,16 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Serilog; using Serilog;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
public class IdentifiedHandler : IWebSocketHandler public class IdentifiedHandler : IWebSocketHandler
{ {
private readonly OBSManager _manager;
private readonly ILogger _logger; private readonly ILogger _logger;
public int OperationCode { get; } = 2; public int OperationCode { get; } = 2;
public IdentifiedHandler(OBSManager manager, ILogger logger) public IdentifiedHandler(ILogger logger)
{ {
_manager = manager;
_logger = logger; _logger = logger;
} }
@ -22,20 +19,22 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
if (data is not IdentifiedMessage message || message == null) if (data is not IdentifiedMessage message || message == null)
return; return;
if (sender is not OBSSocketClient obs)
return;
_manager.Connected = true; obs.Identified = true;
_logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + "."); _logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + ".");
try try
{ {
await _manager.GetGroupList(async groups => await _manager.GetGroupSceneItemList(groups)); await obs.GetGroupList(async groups => await obs.GetGroupSceneItemList(groups));
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Error(e, "Failed to load OBS group info upon OBS identification."); _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;
using Serilog.Context; using Serilog.Context;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
public class RequestBatchResponseHandler : IWebSocketHandler public class RequestBatchResponseHandler : IWebSocketHandler
{ {
private readonly IWebSocketHandler _requestResponseHandler;
private readonly ILogger _logger; private readonly ILogger _logger;
public int OperationCode { get; } = 9; public int OperationCode { get; } = 9;
public RequestBatchResponseHandler( public RequestBatchResponseHandler(ILogger logger)
[FromKeyedServices("obs-requestresponse")] IWebSocketHandler requestResponseHandler,
ILogger logger
)
{ {
_requestResponseHandler = requestResponseHandler;
_logger = logger; _logger = logger;
} }
@ -28,9 +22,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
if (data is not RequestBatchResponseMessage message || message == null) if (data is not RequestBatchResponseMessage message || message == null)
return; return;
if (sender is not OBSSocketClient obs)
using (LogContext.PushProperty("obsrid", message.RequestId)) return;
{
var results = message.Results.ToList(); var results = message.Results.ToList();
_logger.Debug($"Received request batch response of {results.Count} messages."); _logger.Debug($"Received request batch response of {results.Count} messages.");
@ -52,7 +45,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (response == null) if (response == null)
continue; continue;
await _requestResponseHandler.Execute(sender, response); await obs.ExecuteRequest(response);
} }
catch (Exception ex) 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 CommonSocketLibrary.Common;
using Serilog; using Serilog;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
public class RequestResponseHandler : IWebSocketHandler public class RequestResponseHandler : IWebSocketHandler
{ {
private readonly OBSManager _manager;
private readonly ILogger _logger; private readonly ILogger _logger;
public int OperationCode { get; } = 7; public int OperationCode { get; } = 7;
public RequestResponseHandler(OBSManager manager, ILogger logger) public RequestResponseHandler(
ILogger logger
)
{ {
_manager = manager;
_logger = logger; _logger = logger;
} }
@ -23,10 +22,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
if (data is not RequestResponseMessage message || message == null) if (data is not RequestResponseMessage message || message == null)
return; return;
if (sender is not OBSSocketClient obs)
return;
_logger.Debug($"Received an OBS request response [obs request id: {message.RequestId}]"); _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) if (requestData == null)
{ {
_logger.Warning($"OBS Request Response not being processed: request not stored [obs request id: {message.RequestId}]"); _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) switch (request.RequestType)
{ {
case "GetOutputStatus": 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; break;
case "GetSceneItemId": case "GetSceneItemId":
{ {
@ -206,7 +207,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
} }
foreach (var sceneItem in sceneItems) foreach (var sceneItem in sceneItems)
_manager.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId); obs.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId);
requestData.ResponseValues = new Dictionary<string, object>() requestData.ResponseValues = new Dictionary<string, object>()
{ {
@ -237,9 +238,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
return; return;
} }
_manager.Streaming = outputActive?.ToString()!.ToLower() == "true"; obs.Streaming = outputActive?.ToString()!.ToLower() == "true";
requestData.ResponseValues = message.ResponseData; requestData.ResponseValues = message.ResponseData;
_logger.Information($"OBS is currently {(_manager.Streaming ? "" : "not ")}streaming."); _logger.Information($"OBS is currently {(obs.Streaming ? "" : "not ")}streaming.");
break; break;
} }
default: 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.Common;
using CommonSocketLibrary.Socket.Manager; using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -6,12 +5,12 @@ using Serilog;
namespace TwitchChatTTS.OBS.Socket.Manager namespace TwitchChatTTS.OBS.Socket.Manager
{ {
public class OBSHandlerTypeManager : WebSocketHandlerTypeManager public class OBSMessageTypeManager : WebSocketMessageTypeManager
{ {
public OBSHandlerTypeManager( public OBSMessageTypeManager(
ILogger factory, [FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers ILogger logger
) : base(factory, handlers) ) : 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 Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using System.Text.Json; using System.Text.Json;
using System.Collections.Concurrent;
using TwitchChatTTS.OBS.Socket.Data;
using System.Timers;
using System.Net.WebSockets;
namespace TwitchChatTTS.OBS.Socket namespace TwitchChatTTS.OBS.Socket
{ {
public class OBSSocketClient : WebSocketClient 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( public OBSSocketClient(
ILogger logger, Configuration configuration,
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager, [FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("obs")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager [FromKeyedServices("obs")] MessageTypeManager<IWebSocketHandler> typeManager,
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() ILogger logger
) : base(handlers, typeManager, new JsonSerializerOptions()
{ {
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase 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.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Seven.Socket.Context;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;
namespace TwitchChatTTS.Seven.Socket.Handlers namespace TwitchChatTTS.Seven.Socket.Handlers
{ {
public class EndOfStreamHandler : IWebSocketHandler 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 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) public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{ {
if (data is not EndOfStreamMessage message || message == null) if (data is not EndOfStreamMessage message || message == null)
return; return;
var code = message.Code - 4000; var code = message.Code - 4000;
if (code >= 0 && code < _errorCodes.Length) await sender.DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), code.ToString()));
_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.");
}
} }
} }
} }

View File

@ -19,7 +19,6 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
{ {
if (data is not SevenHelloMessage message || message == null) if (data is not SevenHelloMessage message || message == null)
return; return;
if (sender is not SevenSocketClient seven || seven == null) if (sender is not SevenSocketClient seven || seven == null)
return; 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.Common;
using CommonSocketLibrary.Socket.Manager; using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -6,13 +5,12 @@ using Serilog;
namespace TwitchChatTTS.Seven.Socket.Managers namespace TwitchChatTTS.Seven.Socket.Managers
{ {
public class SevenHandlerTypeManager : WebSocketHandlerTypeManager public class SevenMessageTypeManager : WebSocketMessageTypeManager
{ {
public SevenHandlerTypeManager( public SevenMessageTypeManager(
ILogger factory, [FromKeyedServices("7tv")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient, ILogger logger
IWebSocketHandler> handlers ) : base(handlers, logger)
) : base(factory, handlers)
{ {
} }
} }

View File

@ -9,19 +9,133 @@ namespace TwitchChatTTS.Seven.Socket
{ {
public class SevenSocketClient : WebSocketClient 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 SevenHelloMessage? ConnectionDetails { get; set; }
public SevenSocketClient( public SevenSocketClient(
ILogger logger, User user,
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager, [FromKeyedServices("7tv")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("7tv")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager [FromKeyedServices("7tv")] MessageTypeManager<IWebSocketHandler> typeManager,
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() ILogger logger
) : base(handlers, typeManager, new JsonSerializerOptions()
{ {
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}) }, logger)
{ {
_user = user;
ConnectionDetails = null; 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.Seven.Socket;
using TwitchChatTTS.OBS.Socket.Handlers; using TwitchChatTTS.OBS.Socket.Handlers;
using TwitchChatTTS.Seven.Socket.Handlers; using TwitchChatTTS.Seven.Socket.Handlers;
using TwitchChatTTS.Seven.Socket.Context;
using TwitchLib.Client.Interfaces; using TwitchLib.Client.Interfaces;
using TwitchLib.Client; using TwitchLib.Client;
using TwitchLib.PubSub.Interfaces; using TwitchLib.PubSub.Interfaces;
@ -31,6 +30,7 @@ using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Emotes; using TwitchChatTTS.Chat.Emotes;
using HermesSocketLibrary.Requests.Callbacks; using HermesSocketLibrary.Requests.Callbacks;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
// dotnet publish -r win-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 // Command parameters
s.AddKeyedSingleton<ChatCommandParameter, TTSVoiceNameParameter>("parameter-ttsvoicename"); s.AddSingleton<IChatCommand, SkipCommand>();
s.AddKeyedSingleton<ChatCommandParameter, UnvalidatedParameter>("parameter-unvalidated"); s.AddSingleton<IChatCommand, VoiceCommand>();
s.AddKeyedSingleton<ChatCommandParameter, SimpleListedParameter>("parameter-simplelisted"); s.AddSingleton<IChatCommand, RefreshCommand>();
s.AddKeyedSingleton<ChatCommand, SkipAllCommand>("command-skipall"); s.AddSingleton<IChatCommand, OBSCommand>();
s.AddKeyedSingleton<ChatCommand, SkipCommand>("command-skip"); s.AddSingleton<IChatCommand, TTSCommand>();
s.AddKeyedSingleton<ChatCommand, VoiceCommand>("command-voice"); s.AddSingleton<IChatCommand, VersionCommand>();
s.AddKeyedSingleton<ChatCommand, AddTTSVoiceCommand>("command-addttsvoice"); s.AddSingleton<ICommandBuilder, CommandBuilder>();
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<IChatterGroupManager, ChatterGroupManager>(); s.AddSingleton<IChatterGroupManager, ChatterGroupManager>();
s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>(); s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>();
s.AddSingleton<ChatCommandManager>(); s.AddSingleton<CommandManager>();
s.AddSingleton<TTSPlayer>(); s.AddSingleton<TTSPlayer>();
s.AddSingleton<ChatMessageHandler>(); s.AddSingleton<ChatMessageHandler>();
@ -100,48 +95,32 @@ s.AddSingleton<SevenApiClient>();
s.AddSingleton<IEmoteDatabase, EmoteDatabase>(); s.AddSingleton<IEmoteDatabase, EmoteDatabase>();
// OBS websocket // OBS websocket
s.AddSingleton<OBSManager>(); s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello"); s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified"); s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs-requestresponse"); s.AddKeyedSingleton<IWebSocketHandler, RequestBatchResponseHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, RequestBatchResponseHandler>("obs-requestbatchresponse"); s.AddKeyedSingleton<IWebSocketHandler, EventMessageHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, EventMessageHandler>("obs-eventmessage");
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, OBSHandlerManager>("obs"); s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, OBSMessageTypeManager>("obs");
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, OBSHandlerTypeManager>("obs");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, OBSSocketClient>("obs"); s.AddKeyedSingleton<SocketClient<WebSocketMessage>, OBSSocketClient>("obs");
// 7tv websocket // 7tv websocket
s.AddTransient(sp => s.AddKeyedSingleton<IWebSocketHandler, SevenHelloHandler>("7tv");
{ s.AddKeyedSingleton<IWebSocketHandler, DispatchHandler>("7tv");
var logger = sp.GetRequiredService<ILogger>(); s.AddKeyedSingleton<IWebSocketHandler, ReconnectHandler>("7tv");
var client = sp.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv") as SevenSocketClient; s.AddKeyedSingleton<IWebSocketHandler, ErrorHandler>("7tv");
if (client == null) s.AddKeyedSingleton<IWebSocketHandler, EndOfStreamHandler>("7tv");
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.AddSingleton<SevenManager>(); s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, SevenMessageTypeManager>("7tv");
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, SevenHandlerManager>("7tv");
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, SevenHandlerTypeManager>("7tv");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv"); s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
// hermes websocket // hermes websocket
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes-heartbeat"); s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes-loginack"); s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes-requestack"); s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes-error"); //s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, HermesHandlerManager>("hermes"); s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes");
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, HermesHandlerTypeManager>("hermes");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes"); s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");
s.AddHostedService<TTS>(); s.AddHostedService<TTS>();

109
TTS.cs
View File

@ -5,33 +5,30 @@ using Microsoft.Extensions.Hosting;
using Serilog; using Serilog;
using NAudio.Wave.SampleProviders; using NAudio.Wave.SampleProviders;
using TwitchLib.Client.Events; using TwitchLib.Client.Events;
using TwitchChatTTS.Twitch.Redemptions;
using org.mariuszgromada.math.mxparser; using org.mariuszgromada.math.mxparser;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchChatTTS.Seven.Socket; using TwitchChatTTS.Seven.Socket;
using TwitchChatTTS.Chat.Emotes; using TwitchChatTTS.Chat.Emotes;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using TwitchChatTTS.OBS.Socket;
namespace TwitchChatTTS namespace TwitchChatTTS
{ {
public class TTS : IHostedService public class TTS : IHostedService
{ {
public const int MAJOR_VERSION = 3; public const int MAJOR_VERSION = 3;
public const int MINOR_VERSION = 9; public const int MINOR_VERSION = 10;
private readonly User _user; private readonly User _user;
private readonly HermesApiClient _hermesApiClient; private readonly HermesApiClient _hermesApiClient;
private readonly SevenApiClient _sevenApiClient; private readonly SevenApiClient _sevenApiClient;
private readonly OBSManager _obsManager; private readonly OBSSocketClient _obs;
private readonly SevenManager _sevenManager; private readonly SevenSocketClient _seven;
private readonly HermesSocketClient _hermes; private readonly HermesSocketClient _hermes;
private readonly RedemptionManager _redemptionManager; private readonly IEmoteDatabase _emotes;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly IGroupPermissionManager _permissionManager;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly TTSPlayer _player; private readonly TTSPlayer _player;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
@ -41,12 +38,10 @@ namespace TwitchChatTTS
User user, User user,
HermesApiClient hermesApiClient, HermesApiClient hermesApiClient,
SevenApiClient sevenApiClient, SevenApiClient sevenApiClient,
OBSManager obsManager,
SevenManager sevenManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes, [FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
RedemptionManager redemptionManager, [FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
IChatterGroupManager chatterGroupManager, [FromKeyedServices("7tv")] SocketClient<WebSocketMessage> seven,
IGroupPermissionManager permissionManager, IEmoteDatabase emotes,
Configuration configuration, Configuration configuration,
TTSPlayer player, TTSPlayer player,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
@ -56,12 +51,10 @@ namespace TwitchChatTTS
_user = user; _user = user;
_hermesApiClient = hermesApiClient; _hermesApiClient = hermesApiClient;
_sevenApiClient = sevenApiClient; _sevenApiClient = sevenApiClient;
_obsManager = obsManager;
_sevenManager = sevenManager;
_hermes = (hermes as HermesSocketClient)!; _hermes = (hermes as HermesSocketClient)!;
_redemptionManager = redemptionManager; _obs = (obs as OBSSocketClient)!;
_chatterGroupManager = chatterGroupManager; _seven = (seven as SevenSocketClient)!;
_permissionManager = permissionManager; _emotes = emotes;
_configuration = configuration; _configuration = configuration;
_player = player; _player = player;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
@ -119,14 +112,6 @@ namespace TwitchChatTTS
await InitializeSevenTv(); await InitializeSevenTv();
await InitializeObs(); 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) => AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
{ {
if (e.SampleProvider == _player.Playing) if (e.SampleProvider == _player.Playing)
@ -239,65 +224,8 @@ namespace TwitchChatTTS
user.TwitchUsername = hermesAccount.Username; user.TwitchUsername = hermesAccount.Username;
var twitchBotToken = await hermes.FetchTwitchBotToken(); 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}]"); _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() private async Task InitializeHermesWebsocket()
@ -317,8 +245,8 @@ namespace TwitchChatTTS
{ {
try try
{ {
_sevenManager.Initialize(); _seven.Initialize();
await _sevenManager.Connect(); await _seven.Connect();
} }
catch (Exception e) catch (Exception e)
{ {
@ -330,8 +258,8 @@ namespace TwitchChatTTS
{ {
try try
{ {
_obsManager.Initialize(); _obs.Initialize();
await _obsManager.Connect(); await _obs.Connect();
} }
catch (Exception) catch (Exception)
{ {
@ -376,20 +304,19 @@ namespace TwitchChatTTS
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes) private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes)
{ {
var emotes = _serviceProvider.GetRequiredService<IEmoteDatabase>();
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes(); var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();
if (channelEmotes != null && channelEmotes.Emotes.Any()) if (channelEmotes != null && channelEmotes.Emotes.Any())
{ {
_logger.Information($"Loaded {channelEmotes.Emotes.Count()} 7tv channel emotes."); _logger.Information($"Loaded {channelEmotes.Emotes.Count()} 7tv channel emotes.");
foreach (var entry in channelEmotes.Emotes) foreach (var entry in channelEmotes.Emotes)
emotes.Add(entry.Name, entry.Id); _emotes.Add(entry.Name, entry.Id);
} }
if (globalEmotes != null && globalEmotes.Any()) if (globalEmotes != null && globalEmotes.Any())
{ {
_logger.Information($"Loaded {globalEmotes.Count()} 7tv global emotes."); _logger.Information($"Loaded {globalEmotes.Count()} 7tv global emotes.");
foreach (var entry in globalEmotes) 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 org.mariuszgromada.math.mxparser;
using Serilog; using Serilog;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.Twitch.Redemptions namespace TwitchChatTTS.Twitch.Redemptions
{ {
@ -15,7 +15,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
{ {
private readonly IDictionary<string, IList<RedeemableAction>> _store; private readonly IDictionary<string, IList<RedeemableAction>> _store;
private readonly User _user; private readonly User _user;
private readonly OBSManager _obsManager; private readonly OBSSocketClient _obs;
private readonly HermesSocketClient _hermes; private readonly HermesSocketClient _hermes;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly Random _random; private readonly Random _random;
@ -24,13 +24,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
public RedemptionManager( public RedemptionManager(
User user, User user,
OBSManager obsManager, [FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes, [FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
ILogger logger) ILogger logger)
{ {
_store = new Dictionary<string, IList<RedeemableAction>>(); _store = new Dictionary<string, IList<RedeemableAction>>();
_user = user; _user = user;
_obsManager = obsManager; _obs = (obs as OBSSocketClient)!;
_hermes = (hermes as HermesSocketClient)!; _hermes = (hermes as HermesSocketClient)!;
_logger = logger; _logger = logger;
_random = new Random(); _random = new Random();
@ -72,7 +72,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
break; break;
case "OBS_TRANSFORM": case "OBS_TRANSFORM":
var type = typeof(OBSTransformationData); 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"]; string[] properties = ["rotation", "position_x", "position_y"];
foreach (var property in properties) foreach (var property in properties)
@ -111,13 +111,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
}); });
break; break;
case "TOGGLE_OBS_VISIBILITY": 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; break;
case "SPECIFIC_OBS_VISIBILITY": 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; break;
case "SPECIFIC_OBS_INDEX": 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; break;
case "SLEEP": case "SLEEP":
_logger.Debug("Sleeping on thread due to redemption for OBS."); _logger.Debug("Sleeping on thread due to redemption for OBS.");

View File

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