Changed command dictionary to a command tree. Fixed various requests. OBS reconnection added if identified previously.
This commit is contained in:
parent
e6b3819356
commit
472bfcee5d
@ -6,22 +6,22 @@ using TwitchChatTTS.Chat.Commands;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
||||
using TwitchChatTTS.Chat.Groups;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
using TwitchChatTTS.Chat.Emotes;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CommonSocketLibrary.Common;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
|
||||
|
||||
public class ChatMessageHandler
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly ChatCommandManager _commands;
|
||||
private readonly CommandManager _commands;
|
||||
private readonly IGroupPermissionManager _permissionManager;
|
||||
private readonly IChatterGroupManager _chatterGroupManager;
|
||||
private readonly IEmoteDatabase _emotes;
|
||||
private readonly OBSManager _obsManager;
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly HermesSocketClient _hermes;
|
||||
private readonly Configuration _configuration;
|
||||
|
||||
@ -36,12 +36,12 @@ public class ChatMessageHandler
|
||||
public ChatMessageHandler(
|
||||
User user,
|
||||
TTSPlayer player,
|
||||
ChatCommandManager commands,
|
||||
CommandManager commands,
|
||||
IGroupPermissionManager permissionManager,
|
||||
IChatterGroupManager chatterGroupManager,
|
||||
IEmoteDatabase emotes,
|
||||
OBSManager obsManager,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
Configuration configuration,
|
||||
ILogger logger
|
||||
)
|
||||
@ -52,7 +52,7 @@ public class ChatMessageHandler
|
||||
_permissionManager = permissionManager;
|
||||
_chatterGroupManager = chatterGroupManager;
|
||||
_emotes = emotes;
|
||||
_obsManager = obsManager;
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_hermes = (hermes as HermesSocketClient)!;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
@ -71,7 +71,7 @@ public class ChatMessageHandler
|
||||
_logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]");
|
||||
return new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||
}
|
||||
if (_configuration.Twitch?.TtsWhenOffline != true && !_obsManager.Streaming)
|
||||
if (_configuration.Twitch?.TtsWhenOffline != true && !_obs.Streaming)
|
||||
{
|
||||
_logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {m.Id}]");
|
||||
return new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||
@ -109,7 +109,7 @@ public class ChatMessageHandler
|
||||
return new MessageResult(MessageStatus.Blocked, -1, -1);
|
||||
}
|
||||
|
||||
if (_obsManager.Streaming && !_chatters.Contains(chatterId))
|
||||
if (_obs.Streaming && !_chatters.Contains(chatterId))
|
||||
{
|
||||
tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username));
|
||||
_chatters.Add(chatterId);
|
||||
@ -148,7 +148,7 @@ public class ChatMessageHandler
|
||||
if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
|
||||
filteredMsg += w + " ";
|
||||
}
|
||||
if (_obsManager.Streaming && newEmotes.Any())
|
||||
if (_obs.Streaming && newEmotes.Any())
|
||||
tasks.Add(_hermes.SendEmoteDetails(newEmotes));
|
||||
msg = filteredMsg;
|
||||
|
||||
|
@ -1,50 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class AddTTSVoiceCommand : ChatCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public new bool DefaultPermissionsOverwrite { get => true; }
|
||||
|
||||
public AddTTSVoiceCommand(
|
||||
User user,
|
||||
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
|
||||
ILogger logger
|
||||
) : base("addttsvoice", "Select a TTS voice as the default for that user.")
|
||||
{
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
|
||||
AddParameter(unvalidatedParameter);
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
if (_user == null || _user.VoicesAvailable == null)
|
||||
return;
|
||||
|
||||
var voiceName = args.First();
|
||||
var voiceNameLower = voiceName.ToLower();
|
||||
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
||||
if (exists) {
|
||||
_logger.Information("Voice already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
await client.CreateTTSVoice(voiceName);
|
||||
_logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +1,17 @@
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public abstract class ChatCommand
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
public IList<ChatCommandParameter> Parameters { get => _parameters.AsReadOnly(); }
|
||||
public bool DefaultPermissionsOverwrite { get; }
|
||||
public interface IChatCommand {
|
||||
string Name { get; }
|
||||
void Build(ICommandBuilder builder);
|
||||
}
|
||||
|
||||
private IList<ChatCommandParameter> _parameters;
|
||||
|
||||
public ChatCommand(string name, string description)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
DefaultPermissionsOverwrite = false;
|
||||
_parameters = new List<ChatCommandParameter>();
|
||||
}
|
||||
|
||||
protected void AddParameter(ChatCommandParameter parameter, bool optional = false)
|
||||
{
|
||||
if (parameter != null && parameter.Clone() is ChatCommandParameter p) {
|
||||
_parameters.Add(optional ? p.Permissive() : p);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Task<bool> CheckDefaultPermissions(ChatMessage message);
|
||||
public abstract Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client);
|
||||
public interface IChatPartialCommand {
|
||||
bool AcceptCustomPermission { get; }
|
||||
bool CheckDefaultPermissions(ChatMessage message);
|
||||
Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client);
|
||||
}
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class ChatCommandManager
|
||||
{
|
||||
private IDictionary<string, ChatCommand> _commands;
|
||||
private readonly User _user;
|
||||
private readonly HermesSocketClient _hermes;
|
||||
private readonly IGroupPermissionManager _permissionManager;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger _logger;
|
||||
private string CommandStartSign { get; } = "!";
|
||||
|
||||
|
||||
public ChatCommandManager(
|
||||
User user,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> socketClient,
|
||||
IGroupPermissionManager permissionManager,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_user = user;
|
||||
_hermes = (socketClient as HermesSocketClient)!;
|
||||
_permissionManager = permissionManager;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
_commands = new Dictionary<string, ChatCommand>();
|
||||
GenerateCommands();
|
||||
}
|
||||
|
||||
private void Add(ChatCommand command)
|
||||
{
|
||||
_commands.Add(command.Name.ToLower(), command);
|
||||
}
|
||||
|
||||
private void GenerateCommands()
|
||||
{
|
||||
var basetype = typeof(ChatCommand);
|
||||
var assembly = GetType().Assembly;
|
||||
var types = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Chat.") == true);
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
var key = "command-" + type.Name.Replace("Commands", "Comm#ands")
|
||||
.Replace("Command", "")
|
||||
.Replace("Comm#ands", "Commands")
|
||||
.ToLower();
|
||||
|
||||
var command = _serviceProvider.GetKeyedService<ChatCommand>(key);
|
||||
if (command == null)
|
||||
{
|
||||
_logger.Error("Failed to add chat command: " + type.AssemblyQualifiedName);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.Debug($"Added chat command {type.AssemblyQualifiedName}");
|
||||
Add(command);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> groups)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(arg))
|
||||
return ChatCommandResult.Unknown;
|
||||
|
||||
arg = arg.Trim();
|
||||
|
||||
if (!arg.StartsWith(CommandStartSign))
|
||||
return ChatCommandResult.Unknown;
|
||||
|
||||
string[] parts = Regex.Matches(arg, "(?<match>[^\"\\n\\s]+|\"[^\"\\n]*\")")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups["match"].Value)
|
||||
.Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m)
|
||||
.ToArray();
|
||||
string com = parts.First().Substring(CommandStartSign.Length).ToLower();
|
||||
string[] args = parts.Skip(1).ToArray();
|
||||
|
||||
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null)
|
||||
{
|
||||
// Could be for another bot or just misspelled.
|
||||
_logger.Debug($"Failed to find command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return ChatCommandResult.Missing;
|
||||
}
|
||||
|
||||
// Check if command can be executed by this chatter.
|
||||
long chatterId = long.Parse(message.UserId);
|
||||
if (chatterId != _user.OwnerId)
|
||||
{
|
||||
var executable = command.DefaultPermissionsOverwrite ? false : CanExecute(chatterId, groups, com);
|
||||
if (executable == false)
|
||||
{
|
||||
_logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]");
|
||||
return ChatCommandResult.Permission;
|
||||
}
|
||||
else if (executable == null && !await command.CheckDefaultPermissions(message))
|
||||
{
|
||||
_logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return ChatCommandResult.Permission;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the syntax is correct.
|
||||
if (command.Parameters.Count(p => !p.Optional) > args.Length)
|
||||
{
|
||||
_logger.Debug($"Command syntax issue when executing command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return ChatCommandResult.Syntax;
|
||||
}
|
||||
|
||||
for (int i = 0; i < Math.Min(args.Length, command.Parameters.Count); i++)
|
||||
{
|
||||
if (!command.Parameters[i].Validate(args[i]))
|
||||
{
|
||||
_logger.Warning($"Commmand '{com}' failed because of the #{i + 1} argument. Invalid value: {args[i]}");
|
||||
return ChatCommandResult.Syntax;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await command.Execute(args, message, _hermes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, $"Command '{arg}' failed [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return ChatCommandResult.Fail;
|
||||
}
|
||||
|
||||
_logger.Information($"Executed the {com} command [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return ChatCommandResult.Success;
|
||||
}
|
||||
|
||||
private bool? CanExecute(long chatterId, IEnumerable<string> groups, string path)
|
||||
{
|
||||
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]");
|
||||
return _permissionManager.CheckIfAllowed(groups, path);
|
||||
}
|
||||
}
|
||||
}
|
356
Chat/Commands/CommandBuilder.cs
Normal file
356
Chat/Commands/CommandBuilder.cs
Normal file
@ -0,0 +1,356 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public static class TTSCommands
|
||||
{
|
||||
public interface ICommandBuilder
|
||||
{
|
||||
ICommandSelector Build();
|
||||
void Clear();
|
||||
ICommandBuilder CreateCommandTree(string name, Action<ICommandBuilder> callback);
|
||||
ICommandBuilder CreateCommand(IChatPartialCommand command);
|
||||
ICommandBuilder CreateStaticInputParameter(string value, Action<ICommandBuilder> callback, bool optional = false);
|
||||
ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false);
|
||||
ICommandBuilder CreateStateParameter(string name, bool optional = false);
|
||||
ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false);
|
||||
ICommandBuilder CreateVoiceNameParameter(string name, bool enabled, bool optional = false);
|
||||
|
||||
}
|
||||
|
||||
public sealed class CommandBuilder : ICommandBuilder
|
||||
{
|
||||
private CommandNode _root;
|
||||
private CommandNode _current;
|
||||
private Stack<CommandNode> _stack;
|
||||
private readonly User _user;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public CommandBuilder(User user, ILogger logger)
|
||||
{
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
|
||||
_stack = new Stack<CommandNode>();
|
||||
Clear();
|
||||
}
|
||||
|
||||
|
||||
public ICommandSelector Build()
|
||||
{
|
||||
return new CommandSelector(_root);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_root = new CommandNode(new StaticParameter("root", "root"));
|
||||
ResetToRoot();
|
||||
}
|
||||
|
||||
public ICommandBuilder CreateCommandTree(string name, Action<ICommandBuilder> callback)
|
||||
{
|
||||
ResetToRoot();
|
||||
|
||||
var node = _current.CreateStaticInput(name);
|
||||
_logger.Debug($"Creating command name '{name}'");
|
||||
CreateStack(() =>
|
||||
{
|
||||
_current = node;
|
||||
callback(this);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICommandBuilder CreateCommand(IChatPartialCommand command)
|
||||
{
|
||||
if (_root == _current)
|
||||
throw new Exception("Cannot create a command without a command name.");
|
||||
|
||||
_current.CreateCommand(command);
|
||||
_logger.Debug($"Set command to '{command.GetType().Name}'");
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICommandBuilder CreateStaticInputParameter(string value, Action<ICommandBuilder> callback, bool optional = false)
|
||||
{
|
||||
if (_root == _current)
|
||||
throw new Exception("Cannot create a parameter without a command name.");
|
||||
if (optional && _current.IsRequired() && _current.Command == null)
|
||||
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
|
||||
|
||||
var node = _current.CreateStaticInput(value, optional);
|
||||
_logger.Debug($"Creating static parameter '{value}'");
|
||||
CreateStack(() =>
|
||||
{
|
||||
_current = node;
|
||||
callback(this);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false)
|
||||
{
|
||||
if (_root == _current)
|
||||
throw new Exception("Cannot create a parameter without a command name.");
|
||||
if (optional && _current.IsRequired() && _current.Command == null)
|
||||
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
|
||||
|
||||
var node = _current.CreateUserInput(new OBSTransformationParameter(name, optional));
|
||||
_logger.Debug($"Creating obs transformation parameter '{name}'");
|
||||
_current = node;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICommandBuilder CreateStateParameter(string name, bool optional = false)
|
||||
{
|
||||
if (_root == _current)
|
||||
throw new Exception("Cannot create a parameter without a command name.");
|
||||
if (optional && _current.IsRequired() && _current.Command == null)
|
||||
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
|
||||
|
||||
var node = _current.CreateUserInput(new StateParameter(name, optional));
|
||||
_logger.Debug($"Creating unvalidated parameter '{name}'");
|
||||
_current = node;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false)
|
||||
{
|
||||
if (_root == _current)
|
||||
throw new Exception("Cannot create a parameter without a command name.");
|
||||
if (optional && _current.IsRequired() && _current.Command == null)
|
||||
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
|
||||
|
||||
var node = _current.CreateUserInput(new UnvalidatedParameter(name, optional));
|
||||
_logger.Debug($"Creating unvalidated parameter '{name}'");
|
||||
_current = node;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICommandBuilder CreateVoiceNameParameter(string name, bool enabled, bool optional = false)
|
||||
{
|
||||
if (_root == _current)
|
||||
throw new Exception("Cannot create a parameter without a command name.");
|
||||
if (optional && _current.IsRequired() && _current.Command == null)
|
||||
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
|
||||
|
||||
var node = _current.CreateUserInput(new TTSVoiceNameParameter(name, enabled, _user, optional));
|
||||
_logger.Debug($"Creating tts voice name parameter '{name}'");
|
||||
_current = node;
|
||||
return this;
|
||||
}
|
||||
|
||||
private ICommandBuilder ResetToRoot()
|
||||
{
|
||||
_current = _root;
|
||||
_stack.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
private void CreateStack(Action func)
|
||||
{
|
||||
try
|
||||
{
|
||||
_stack.Push(_current);
|
||||
func();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_current = _stack.Pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICommandSelector
|
||||
{
|
||||
CommandSelectorResult GetBestMatch(string[] args);
|
||||
IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path);
|
||||
CommandValidationResult Validate(string[] args, string path);
|
||||
}
|
||||
|
||||
public sealed class CommandSelector : ICommandSelector
|
||||
{
|
||||
private CommandNode _root;
|
||||
|
||||
public CommandSelector(CommandNode root)
|
||||
{
|
||||
_root = root;
|
||||
}
|
||||
|
||||
public CommandSelectorResult GetBestMatch(string[] args)
|
||||
{
|
||||
return GetBestMatch(_root, args, null, string.Empty);
|
||||
}
|
||||
|
||||
private CommandSelectorResult GetBestMatch(CommandNode node, IEnumerable<string> args, IChatPartialCommand? match, string path)
|
||||
{
|
||||
if (node == null || !args.Any())
|
||||
return new CommandSelectorResult(match, path);
|
||||
if (!node.Children.Any())
|
||||
return new CommandSelectorResult(node.Command ?? match, path);
|
||||
|
||||
var argument = args.First();
|
||||
var argumentLower = argument.ToLower();
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
if (child.Parameter.GetType() == typeof(StaticParameter))
|
||||
{
|
||||
if (child.Parameter.Name.ToLower() == argumentLower)
|
||||
{
|
||||
return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + child.Parameter.Name.ToLower());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + "*");
|
||||
}
|
||||
|
||||
return new CommandSelectorResult(match, path);
|
||||
}
|
||||
|
||||
public CommandValidationResult Validate(string[] args, string path)
|
||||
{
|
||||
CommandNode? current = _root;
|
||||
var parts = path.Split('.');
|
||||
if (args.Length < parts.Length)
|
||||
throw new Exception($"Command path too long for the number of arguments passed in [path: {path}][parts: {parts.Length}][args count: {args.Length}]");
|
||||
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
if (part == "*")
|
||||
{
|
||||
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() != typeof(StaticParameter));
|
||||
if (current == null)
|
||||
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
|
||||
|
||||
if (!current.Parameter.Validate(args[i]))
|
||||
{
|
||||
return new CommandValidationResult(false, args[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() == typeof(StaticParameter) && n.Parameter.Name == part);
|
||||
if (current == null)
|
||||
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
|
||||
}
|
||||
}
|
||||
|
||||
return new CommandValidationResult(true, null);
|
||||
}
|
||||
|
||||
public IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path)
|
||||
{
|
||||
Dictionary<string, CommandParameter> arguments = new Dictionary<string, CommandParameter>();
|
||||
CommandNode? current = _root;
|
||||
var parts = path.Split('.');
|
||||
if (args.Length < parts.Length)
|
||||
throw new Exception($"Command path too long for the number of arguments passed in [path: {path}][parts: {parts.Length}][args count: {args.Length}]");
|
||||
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
if (part == "*")
|
||||
{
|
||||
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() != typeof(StaticParameter));
|
||||
if (current == null)
|
||||
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
|
||||
|
||||
arguments.Add(args[i], current.Parameter);
|
||||
}
|
||||
else
|
||||
{
|
||||
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() == typeof(StaticParameter) && n.Parameter.Name == part);
|
||||
if (current == null)
|
||||
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
|
||||
}
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
|
||||
public class CommandSelectorResult
|
||||
{
|
||||
public IChatPartialCommand? Command { get; set; }
|
||||
public string Path { get; set; }
|
||||
|
||||
public CommandSelectorResult(IChatPartialCommand? command, string path)
|
||||
{
|
||||
Command = command;
|
||||
Path = path;
|
||||
}
|
||||
}
|
||||
|
||||
public class CommandValidationResult
|
||||
{
|
||||
public bool Result { get; set; }
|
||||
public string? ErrorParameterName { get; set; }
|
||||
|
||||
public CommandValidationResult(bool result, string? parameterName)
|
||||
{
|
||||
Result = result;
|
||||
ErrorParameterName = parameterName;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CommandNode
|
||||
{
|
||||
public IChatPartialCommand? Command { get; private set; }
|
||||
public CommandParameter Parameter { get; }
|
||||
public IList<CommandNode> Children { get => _children.AsReadOnly(); }
|
||||
|
||||
private IList<CommandNode> _children;
|
||||
|
||||
public CommandNode(CommandParameter parameter)
|
||||
{
|
||||
Parameter = parameter;
|
||||
_children = new List<CommandNode>();
|
||||
}
|
||||
|
||||
|
||||
public CommandNode CreateCommand(IChatPartialCommand command)
|
||||
{
|
||||
if (Command != null)
|
||||
throw new InvalidOperationException("Cannot change the command of an existing one.");
|
||||
|
||||
Command = command;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandNode CreateStaticInput(string value, bool optional = false)
|
||||
{
|
||||
if (Children.Any(n => n.Parameter.GetType() != typeof(StaticParameter)))
|
||||
throw new InvalidOperationException("Cannot have mixed static and user inputs in the same position of a subcommand.");
|
||||
return Create(n => n.Parameter.Name == value, new StaticParameter(value.ToLower(), value, optional));
|
||||
}
|
||||
|
||||
public CommandNode CreateUserInput(CommandParameter parameter)
|
||||
{
|
||||
if (Children.Any(n => n.Parameter.GetType() == typeof(StaticParameter)))
|
||||
throw new InvalidOperationException("Cannot have mixed static and user inputs in the same position of a subcommand.");
|
||||
return Create(n => true, parameter);
|
||||
}
|
||||
|
||||
private CommandNode Create(Predicate<CommandNode> predicate, CommandParameter parameter)
|
||||
{
|
||||
CommandNode? node = Children.FirstOrDefault(n => predicate(n));
|
||||
if (node == null)
|
||||
{
|
||||
node = new CommandNode(parameter);
|
||||
_children.Add(node);
|
||||
}
|
||||
if (node.Parameter.GetType() != parameter.GetType())
|
||||
throw new Exception("User input argument already exist for this partial command.");
|
||||
return node;
|
||||
}
|
||||
|
||||
public bool IsRequired()
|
||||
{
|
||||
return !Parameter.Optional;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
124
Chat/Commands/CommandManager.cs
Normal file
124
Chat/Commands/CommandManager.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class CommandManager
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly ICommandSelector _commandSelector;
|
||||
private readonly HermesSocketClient _hermes;
|
||||
private readonly IGroupPermissionManager _permissionManager;
|
||||
private readonly ILogger _logger;
|
||||
private string CommandStartSign { get; } = "!";
|
||||
|
||||
|
||||
public CommandManager(
|
||||
IEnumerable<IChatCommand> commands,
|
||||
ICommandBuilder commandBuilder,
|
||||
User user,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> socketClient,
|
||||
IGroupPermissionManager permissionManager,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_user = user;
|
||||
_hermes = (socketClient as HermesSocketClient)!;
|
||||
_permissionManager = permissionManager;
|
||||
_logger = logger;
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
_logger.Debug($"Creating command tree for '{command.Name}'.");
|
||||
command.Build(commandBuilder);
|
||||
}
|
||||
|
||||
_commandSelector = commandBuilder.Build();
|
||||
}
|
||||
|
||||
|
||||
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> groups)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(arg))
|
||||
return ChatCommandResult.Unknown;
|
||||
|
||||
arg = arg.Trim();
|
||||
|
||||
if (!arg.StartsWith(CommandStartSign))
|
||||
return ChatCommandResult.Unknown;
|
||||
|
||||
string[] parts = Regex.Matches(arg.Substring(CommandStartSign.Length), "(?<match>[^\"\\n\\s]+|\"[^\"\\n]*\")")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups["match"].Value)
|
||||
.Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m)
|
||||
.ToArray();
|
||||
string[] args = parts.ToArray();
|
||||
string com = args.First().ToLower();
|
||||
|
||||
CommandSelectorResult selectorResult = _commandSelector.GetBestMatch(args);
|
||||
if (selectorResult.Command == null)
|
||||
{
|
||||
_logger.Warning($"Could not match '{arg}' to any command.");
|
||||
return ChatCommandResult.Missing;
|
||||
}
|
||||
|
||||
// Check if command can be executed by this chatter.
|
||||
var command = selectorResult.Command;
|
||||
long chatterId = long.Parse(message.UserId);
|
||||
if (chatterId != _user.OwnerId)
|
||||
{
|
||||
var executable = command.AcceptCustomPermission ? CanExecute(chatterId, groups, com) : null;
|
||||
if (executable == false)
|
||||
{
|
||||
_logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]");
|
||||
return ChatCommandResult.Permission;
|
||||
}
|
||||
else if (executable == null && !command.CheckDefaultPermissions(message))
|
||||
{
|
||||
_logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return ChatCommandResult.Permission;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the arguments are correct.
|
||||
var arguments = _commandSelector.GetNonStaticArguments(args, selectorResult.Path);
|
||||
foreach (var entry in arguments)
|
||||
{
|
||||
var parameter = entry.Value;
|
||||
var argument = entry.Key;
|
||||
if (!parameter.Validate(argument))
|
||||
{
|
||||
_logger.Warning($"Command failed due to an argument being invalid [argument name: {parameter.Name}][argument value: {argument}][arguments: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return ChatCommandResult.Syntax;
|
||||
}
|
||||
}
|
||||
|
||||
var values = arguments.ToDictionary(d => d.Value.Name, d => d.Key);
|
||||
try
|
||||
{
|
||||
await command.Execute(values, message, _hermes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, $"Command '{arg}' failed [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return ChatCommandResult.Fail;
|
||||
}
|
||||
|
||||
_logger.Information($"Executed the {com} command [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return ChatCommandResult.Success;
|
||||
}
|
||||
|
||||
private bool? CanExecute(long chatterId, IEnumerable<string> groups, string path)
|
||||
{
|
||||
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]");
|
||||
return _permissionManager.CheckIfAllowed(groups, path);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,89 +2,161 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
using TwitchLib.Client.Models;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class OBSCommand : ChatCommand
|
||||
public class OBSCommand : IChatCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly OBSManager _manager;
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public string Name => "obs";
|
||||
|
||||
public OBSCommand(
|
||||
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
|
||||
User user,
|
||||
OBSManager manager,
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
ILogger logger
|
||||
) : base("obs", "Various obs commands.")
|
||||
)
|
||||
{
|
||||
_user = user;
|
||||
_manager = manager;
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_logger = logger;
|
||||
|
||||
AddParameter(unvalidatedParameter);
|
||||
AddParameter(unvalidatedParameter, optional: true);
|
||||
AddParameter(unvalidatedParameter, optional: true);
|
||||
AddParameter(unvalidatedParameter, optional: true);
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
|
||||
|
||||
public void Build(ICommandBuilder builder)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
if (_user == null || _user.VoicesAvailable == null)
|
||||
return;
|
||||
|
||||
var action = args[0].ToLower();
|
||||
|
||||
switch (action)
|
||||
builder.CreateCommandTree(Name, b =>
|
||||
{
|
||||
case "get_scene_item_id":
|
||||
if (args.Count < 3)
|
||||
return;
|
||||
b.CreateStaticInputParameter("get_scene_item_id", b =>
|
||||
{
|
||||
b.CreateUnvalidatedParameter("sceneName")
|
||||
.CreateCommand(new OBSGetSceneItemId(_obs, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("transform", b =>
|
||||
{
|
||||
b.CreateUnvalidatedParameter("sceneName")
|
||||
.CreateUnvalidatedParameter("sourceName")
|
||||
.CreateObsTransformationParameter("propertyName")
|
||||
.CreateUnvalidatedParameter("value")
|
||||
.CreateCommand(new OBSTransform(_obs, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("visibility", b =>
|
||||
{
|
||||
b.CreateUnvalidatedParameter("sceneName")
|
||||
.CreateUnvalidatedParameter("sourceName")
|
||||
.CreateStateParameter("state")
|
||||
.CreateCommand(new OBSVisibility(_obs, _logger));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_logger.Debug($"Getting scene item id via chat command [args: {string.Join(" ", args)}]");
|
||||
await _manager.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", args[1] }, { "sourceName", args[2] } }));
|
||||
break;
|
||||
case "transform":
|
||||
if (args.Count < 5)
|
||||
return;
|
||||
private sealed class OBSGetSceneItemId : IChatPartialCommand
|
||||
{
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
_logger.Debug($"Getting scene item transformation data via chat command [args: {string.Join(" ", args)}]");
|
||||
await _manager.UpdateTransformation(args[1], args[2], (d) =>
|
||||
{
|
||||
if (args[3].ToLower() == "rotation")
|
||||
d.Rotation = int.Parse(args[4]);
|
||||
else if (args[3].ToLower() == "x")
|
||||
d.Rotation = int.Parse(args[4]);
|
||||
else if (args[3].ToLower() == "y")
|
||||
d.PositionY = int.Parse(args[4]);
|
||||
});
|
||||
break;
|
||||
case "sleep":
|
||||
if (args.Count < 2)
|
||||
return;
|
||||
public string Name => "obs";
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
_logger.Debug($"Sending OBS to sleep via chat command [args: {string.Join(" ", args)}]");
|
||||
await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", int.Parse(args[1]) } }));
|
||||
break;
|
||||
case "visibility":
|
||||
if (args.Count < 4)
|
||||
return;
|
||||
|
||||
_logger.Debug($"Updating scene item visibility via chat command [args: {string.Join(" ", args)}]");
|
||||
await _manager.UpdateSceneItemVisibility(args[1], args[2], args[3].ToLower() == "true");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
public OBSGetSceneItemId(
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
string sceneName = values["sceneName"];
|
||||
string sourceName = values["sourceName"];
|
||||
_logger.Debug($"Getting scene item id via chat command [scene name: {sceneName}][source name: {sourceName}]");
|
||||
await _obs.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sourceName } }));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class OBSTransform : IChatPartialCommand
|
||||
{
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public string Name => "obs";
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public OBSTransform(
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
string sceneName = values["sceneName"];
|
||||
string sourceName = values["sourceName"];
|
||||
string propertyName = values["propertyName"];
|
||||
string value = values["value"];
|
||||
_logger.Debug($"Getting scene item transformation data via chat command [scene name: {sceneName}][source name: {sourceName}][property: {propertyName}][value: {value}]");
|
||||
await _obs.UpdateTransformation(sceneName, sourceName, (d) =>
|
||||
{
|
||||
if (propertyName.ToLower() == "rotation" || propertyName.ToLower() == "rotate")
|
||||
d.Rotation = int.Parse(value);
|
||||
else if (propertyName.ToLower() == "x")
|
||||
d.PositionX = int.Parse(value);
|
||||
else if (propertyName.ToLower() == "y")
|
||||
d.PositionY = int.Parse(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class OBSVisibility : IChatPartialCommand
|
||||
{
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public string Name => "obs";
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public OBSVisibility(
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
string sceneName = values["sceneName"];
|
||||
string sourceName = values["sourceName"];
|
||||
string state = values["state"];
|
||||
_logger.Debug($"Updating scene item visibility via chat command [scene name: {sceneName}][source name: {sourceName}][state: {state}]");
|
||||
string stateLower = state.ToLower();
|
||||
bool stateBool = stateLower == "true" || stateLower == "enable" || stateLower == "enabled" || stateLower == "yes";
|
||||
await _obs.UpdateSceneItemVisibility(sceneName, sourceName, stateBool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
public abstract class ChatCommandParameter : ICloneable
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
public bool Optional { get; private set; }
|
||||
|
||||
public ChatCommandParameter(string name, string description, bool optional = false)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
Optional = optional;
|
||||
}
|
||||
|
||||
public abstract bool Validate(string value);
|
||||
|
||||
public object Clone() {
|
||||
return (ChatCommandParameter) MemberwiseClone();
|
||||
}
|
||||
|
||||
public ChatCommandParameter Permissive() {
|
||||
Optional = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
20
Chat/Commands/Parameters/CommandParameter.cs
Normal file
20
Chat/Commands/Parameters/CommandParameter.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
public abstract class CommandParameter : ICloneable
|
||||
{
|
||||
public string Name { get; }
|
||||
public bool Optional { get; }
|
||||
|
||||
public CommandParameter(string name, bool optional)
|
||||
{
|
||||
Name = name;
|
||||
Optional = optional;
|
||||
}
|
||||
|
||||
public abstract bool Validate(string value);
|
||||
|
||||
public object Clone() {
|
||||
return (CommandParameter) MemberwiseClone();
|
||||
}
|
||||
}
|
||||
}
|
16
Chat/Commands/Parameters/OBSTransformationParameter.cs
Normal file
16
Chat/Commands/Parameters/OBSTransformationParameter.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
public class OBSTransformationParameter : CommandParameter
|
||||
{
|
||||
private string[] _values = ["x", "y", "rotation", "rotate", "r"];
|
||||
|
||||
public OBSTransformationParameter(string name, bool optional = false) : base(name, optional)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Validate(string value)
|
||||
{
|
||||
return _values.Contains(value.ToLower());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
public class SimpleListedParameter : ChatCommandParameter
|
||||
{
|
||||
private readonly string[] _values;
|
||||
|
||||
public SimpleListedParameter(string[] possibleValues, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
|
||||
{
|
||||
_values = possibleValues;
|
||||
}
|
||||
|
||||
public override bool Validate(string value)
|
||||
{
|
||||
return _values.Contains(value.ToLower());
|
||||
}
|
||||
}
|
||||
}
|
16
Chat/Commands/Parameters/StateParameter.cs
Normal file
16
Chat/Commands/Parameters/StateParameter.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
public class StateParameter : CommandParameter
|
||||
{
|
||||
private string[] _values = ["on", "off", "true", "false", "enabled", "disabled", "enable", "disable", "yes", "no"];
|
||||
|
||||
public StateParameter(string name, bool optional = false) : base(name, optional)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Validate(string value)
|
||||
{
|
||||
return _values.Contains(value.ToLower());
|
||||
}
|
||||
}
|
||||
}
|
19
Chat/Commands/Parameters/StaticParameter.cs
Normal file
19
Chat/Commands/Parameters/StaticParameter.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
public class StaticParameter : CommandParameter
|
||||
{
|
||||
private readonly string _value;
|
||||
|
||||
public string Value { get => _value; }
|
||||
|
||||
public StaticParameter(string name, string value, bool optional = false) : base(name, optional)
|
||||
{
|
||||
_value = value.ToLower();
|
||||
}
|
||||
|
||||
public override bool Validate(string value)
|
||||
{
|
||||
return _value == value.ToLower();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
public class TTSVoiceNameParameter : ChatCommandParameter
|
||||
public class TTSVoiceNameParameter : CommandParameter
|
||||
{
|
||||
private bool _enabled;
|
||||
private readonly User _user;
|
||||
|
||||
public TTSVoiceNameParameter(User user, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
|
||||
public TTSVoiceNameParameter(string name, bool enabled, User user, bool optional = false) : base(name, optional)
|
||||
{
|
||||
_enabled = enabled;
|
||||
_user = user;
|
||||
}
|
||||
|
||||
@ -13,8 +15,11 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
if (_user.VoicesAvailable == null)
|
||||
return false;
|
||||
|
||||
|
||||
value = value.ToLower();
|
||||
if (_enabled)
|
||||
return _user.VoicesEnabled.Any(v => v.ToLower() == value);
|
||||
|
||||
return _user.VoicesAvailable.Any(e => e.Value.ToLower() == value);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
public class UnvalidatedParameter : ChatCommandParameter
|
||||
public class UnvalidatedParameter : CommandParameter
|
||||
{
|
||||
public UnvalidatedParameter(bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
|
||||
public UnvalidatedParameter(string name, bool optional = false) : base(name, optional)
|
||||
{
|
||||
}
|
||||
|
||||
|
163
Chat/Commands/RefreshCommand.cs
Normal file
163
Chat/Commands/RefreshCommand.cs
Normal file
@ -0,0 +1,163 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class RefreshCommand : IChatCommand
|
||||
{
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public string Name => "refresh";
|
||||
|
||||
public RefreshCommand(
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public void Build(ICommandBuilder builder)
|
||||
{
|
||||
builder.CreateCommandTree(Name, b =>
|
||||
{
|
||||
b.CreateStaticInputParameter("tts_voice_enabled", b => b.CreateCommand(new RefreshTTSVoicesEnabled()))
|
||||
.CreateStaticInputParameter("word_filters", b => b.CreateCommand(new RefreshTTSWordFilters()))
|
||||
.CreateStaticInputParameter("selected_voices", b => b.CreateCommand(new RefreshTTSChatterVoices()))
|
||||
.CreateStaticInputParameter("default_voice", b => b.CreateCommand(new RefreshTTSDefaultVoice()))
|
||||
.CreateStaticInputParameter("redemptions", b => b.CreateCommand(new RefreshRedemptions()))
|
||||
.CreateStaticInputParameter("obs_cache", b => b.CreateCommand(new RefreshObs(_obs, _logger)))
|
||||
.CreateStaticInputParameter("permissions", b => b.CreateCommand(new RefreshPermissions()));
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class RefreshTTSVoicesEnabled : IChatPartialCommand
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
await client.FetchEnabledTTSVoices();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RefreshTTSWordFilters : IChatPartialCommand
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
await client.FetchTTSWordFilters();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RefreshTTSChatterVoices : IChatPartialCommand
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
await client.FetchTTSChatterVoices();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RefreshTTSDefaultVoice : IChatPartialCommand
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
await client.FetchDefaultTTSVoice();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RefreshRedemptions : IChatPartialCommand
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
await client.FetchRedemptions();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RefreshObs : IChatPartialCommand
|
||||
{
|
||||
private readonly OBSSocketClient _obsManager;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public RefreshObs(OBSSocketClient obsManager, ILogger logger) {
|
||||
_obsManager = obsManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
_obsManager.ClearCache();
|
||||
_logger.Information("Cleared the cache used for OBS.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RefreshPermissions : IChatPartialCommand
|
||||
{
|
||||
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
await client.FetchPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchChatTTS.Chat.Groups;
|
||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
using TwitchChatTTS.Twitch.Redemptions;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class RefreshTTSDataCommand : ChatCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly RedemptionManager _redemptionManager;
|
||||
private readonly IGroupPermissionManager _permissionManager;
|
||||
private readonly IChatterGroupManager _chatterGroupManager;
|
||||
private readonly OBSManager _obsManager;
|
||||
private readonly HermesApiClient _hermesApi;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public RefreshTTSDataCommand(
|
||||
User user,
|
||||
RedemptionManager redemptionManager,
|
||||
IGroupPermissionManager permissionManager,
|
||||
IChatterGroupManager chatterGroupManager,
|
||||
OBSManager obsManager,
|
||||
HermesApiClient hermesApi,
|
||||
ILogger logger
|
||||
) : base("refresh", "Refreshes certain TTS related data on the client.")
|
||||
{
|
||||
_user = user;
|
||||
_redemptionManager = redemptionManager;
|
||||
_permissionManager = permissionManager;
|
||||
_chatterGroupManager = chatterGroupManager;
|
||||
_obsManager = obsManager;
|
||||
_hermesApi = hermesApi;
|
||||
_logger = logger;
|
||||
|
||||
AddParameter(new SimpleListedParameter([
|
||||
"tts_voice_enabled",
|
||||
"word_filters",
|
||||
"selected_voices",
|
||||
"default_voice",
|
||||
"redemptions",
|
||||
"obs_cache",
|
||||
"permissions"
|
||||
]));
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
var value = args.First().ToLower();
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case "tts_voice_enabled":
|
||||
var voicesEnabled = await _hermesApi.FetchTTSEnabledVoices();
|
||||
if (voicesEnabled == null || !voicesEnabled.Any())
|
||||
_user.VoicesEnabled = new HashSet<string>(["Brian"]);
|
||||
else
|
||||
_user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
|
||||
_logger.Information($"{_user.VoicesEnabled.Count} TTS voices have been enabled.");
|
||||
break;
|
||||
case "word_filters":
|
||||
var wordFilters = await _hermesApi.FetchTTSWordFilters();
|
||||
_user.RegexFilters = wordFilters.ToList();
|
||||
_logger.Information($"{_user.RegexFilters.Count()} TTS word filters.");
|
||||
break;
|
||||
case "selected_voices":
|
||||
{
|
||||
var voicesSelected = await _hermesApi.FetchTTSChatterSelectedVoices();
|
||||
_user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice);
|
||||
_logger.Information($"{_user.VoicesSelected.Count} TTS voices have been selected for specific chatters.");
|
||||
break;
|
||||
}
|
||||
case "default_voice":
|
||||
_user.DefaultTTSVoice = await _hermesApi.FetchTTSDefaultVoice();
|
||||
_logger.Information("TTS Default Voice: " + _user.DefaultTTSVoice);
|
||||
break;
|
||||
case "redemptions":
|
||||
var redemptionActions = await _hermesApi.FetchRedeemableActions();
|
||||
var redemptions = await _hermesApi.FetchRedemptions();
|
||||
_redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a));
|
||||
_logger.Information($"Redemption Manager has been refreshed with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
|
||||
break;
|
||||
case "obs_cache":
|
||||
{
|
||||
_obsManager.ClearCache();
|
||||
await _obsManager.GetGroupList(async groups => await _obsManager.GetGroupSceneItemList(groups));
|
||||
break;
|
||||
}
|
||||
case "permissions":
|
||||
{
|
||||
_chatterGroupManager.Clear();
|
||||
_permissionManager.Clear();
|
||||
|
||||
var groups = await _hermesApi.FetchGroups();
|
||||
var groupsById = groups.ToDictionary(g => g.Id, g => g);
|
||||
foreach (var group in groups)
|
||||
_chatterGroupManager.Add(group);
|
||||
_logger.Information($"{groups.Count()} groups have been loaded.");
|
||||
|
||||
var groupChatters = await _hermesApi.FetchGroupChatters();
|
||||
_logger.Debug($"{groupChatters.Count()} group users have been fetched.");
|
||||
|
||||
var permissions = await _hermesApi.FetchGroupPermissions();
|
||||
foreach (var permission in permissions)
|
||||
{
|
||||
_logger.Debug($"Adding group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
|
||||
if (groupsById.TryGetValue(permission.GroupId, out var group))
|
||||
{
|
||||
_logger.Warning($"Failed to find group by id [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = $"{group.Name}.{permission.Path}";
|
||||
_permissionManager.Set(path, permission.Allow);
|
||||
_logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
|
||||
}
|
||||
_logger.Information($"{permissions.Count()} group permissions have been loaded.");
|
||||
|
||||
foreach (var chatter in groupChatters)
|
||||
if (groupsById.TryGetValue(chatter.GroupId, out var group))
|
||||
_chatterGroupManager.Add(chatter.ChatterId, group.Name);
|
||||
_logger.Information($"Users in each group have been loaded.");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
_logger.Warning($"Unknown refresh value given [value: {value}]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class RemoveTTSVoiceCommand : ChatCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
private ILogger _logger;
|
||||
|
||||
public new bool DefaultPermissionsOverwrite { get => true; }
|
||||
|
||||
public RemoveTTSVoiceCommand(
|
||||
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
|
||||
User user,
|
||||
ILogger logger
|
||||
) : base("removettsvoice", "Select a TTS voice as the default for that user.")
|
||||
{
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
|
||||
AddParameter(ttsVoiceParameter);
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
if (_user == null || _user.VoicesAvailable == null)
|
||||
{
|
||||
_logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
var voiceName = args.First().ToLower();
|
||||
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName);
|
||||
if (!exists)
|
||||
{
|
||||
_logger.Debug($"Voice does not exist [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||
await client.DeleteTTSVoice(voiceId);
|
||||
_logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class SkipAllCommand : ChatCommand
|
||||
{
|
||||
private readonly TTSPlayer _ttsPlayer;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SkipAllCommand(TTSPlayer ttsPlayer, ILogger logger)
|
||||
: base("skipall", "Skips all text to speech messages in queue and playing.")
|
||||
{
|
||||
_ttsPlayer = ttsPlayer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsVip || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
_ttsPlayer.RemoveAll();
|
||||
|
||||
if (_ttsPlayer.Playing == null)
|
||||
return;
|
||||
|
||||
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
|
||||
_ttsPlayer.Playing = null;
|
||||
|
||||
_logger.Information("Skipped all queued and playing tts.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +1,98 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class SkipCommand : ChatCommand
|
||||
public class SkipCommand : IChatCommand
|
||||
{
|
||||
private readonly TTSPlayer _ttsPlayer;
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SkipCommand(TTSPlayer ttsPlayer, ILogger logger)
|
||||
: base("skip", "Skips the current text to speech message.")
|
||||
{
|
||||
_ttsPlayer = ttsPlayer;
|
||||
_player = ttsPlayer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
|
||||
public string Name => "skip";
|
||||
|
||||
public void Build(ICommandBuilder builder)
|
||||
{
|
||||
return message.IsModerator || message.IsVip || message.IsBroadcaster;
|
||||
builder.CreateCommandTree(Name, b =>
|
||||
{
|
||||
b.CreateStaticInputParameter("all", b =>
|
||||
{
|
||||
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _logger));
|
||||
}).CreateCommand(new TTSPlayerSkipCommand(_player, _logger));
|
||||
});
|
||||
builder.CreateCommandTree("skipall", b => {
|
||||
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _logger));
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
|
||||
|
||||
private sealed class TTSPlayerSkipCommand : IChatPartialCommand
|
||||
{
|
||||
if (_ttsPlayer.Playing == null)
|
||||
return;
|
||||
private readonly TTSPlayer _ttsPlayer;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
|
||||
_ttsPlayer.Playing = null;
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
_logger.Information("Skipped current tts.");
|
||||
public TTSPlayerSkipCommand(TTSPlayer ttsPlayer, ILogger logger)
|
||||
{
|
||||
_ttsPlayer = ttsPlayer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsVip || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
if (_ttsPlayer.Playing == null)
|
||||
return;
|
||||
|
||||
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
|
||||
_ttsPlayer.Playing = null;
|
||||
|
||||
_logger.Information("Skipped current tts.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TTSPlayerSkipAllCommand : IChatPartialCommand
|
||||
{
|
||||
private readonly TTSPlayer _ttsPlayer;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public TTSPlayerSkipAllCommand(TTSPlayer ttsPlayer, ILogger logger)
|
||||
{
|
||||
_ttsPlayer = ttsPlayer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsVip || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
_ttsPlayer.RemoveAll();
|
||||
|
||||
if (_ttsPlayer.Playing == null)
|
||||
return;
|
||||
|
||||
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
|
||||
_ttsPlayer.Playing = null;
|
||||
|
||||
_logger.Information("Skipped all queued and playing tts.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +1,182 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class TTSCommand : ChatCommand
|
||||
public class TTSCommand : IChatCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public TTSCommand(
|
||||
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
|
||||
User user,
|
||||
ILogger logger
|
||||
) : base("tts", "Various tts commands.")
|
||||
|
||||
public TTSCommand(User user, ILogger logger)
|
||||
{
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
|
||||
AddParameter(ttsVoiceParameter);
|
||||
AddParameter(new SimpleListedParameter(["enable", "disable"]));
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
|
||||
public string Name => "tts";
|
||||
|
||||
public void Build(ICommandBuilder builder)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
builder.CreateCommandTree(Name, b =>
|
||||
{
|
||||
b.CreateStaticInputParameter("add", b =>
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", false)
|
||||
.CreateCommand(new AddTTSVoiceCommand(_user, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("del", b =>
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", true)
|
||||
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("delete", b =>
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", true)
|
||||
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("remove", b =>
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", true)
|
||||
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("enable", b =>
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", false)
|
||||
.CreateCommand(new SetTTSVoiceStateCommand(true, _user, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("on", b =>
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", false)
|
||||
.CreateCommand(new SetTTSVoiceStateCommand(true, _user, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("disable", b =>
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", true)
|
||||
.CreateCommand(new SetTTSVoiceStateCommand(false, _user, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("off", b =>
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", true)
|
||||
.CreateCommand(new SetTTSVoiceStateCommand(false, _user, _logger));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
|
||||
private sealed class AddTTSVoiceCommand : IChatPartialCommand
|
||||
{
|
||||
if (_user == null || _user.VoicesAvailable == null)
|
||||
return;
|
||||
private readonly User _user;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
var voiceName = args[0].ToLower();
|
||||
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||
var action = args[1].ToLower();
|
||||
public bool AcceptCustomPermission { get => false; }
|
||||
|
||||
bool state = action == "enable";
|
||||
await client.UpdateTTSVoiceState(voiceId, state);
|
||||
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {state}][invoker: {message.Username}][id: {message.UserId}]");
|
||||
|
||||
public AddTTSVoiceCommand(User user, ILogger logger)
|
||||
{
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
if (_user == null || _user.VoicesAvailable == null)
|
||||
return;
|
||||
|
||||
var voiceName = values["voiceName"];
|
||||
var voiceNameLower = voiceName.ToLower();
|
||||
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
||||
if (exists)
|
||||
{
|
||||
_logger.Warning($"Voice already exists [voice: {voiceName}][id: {message.UserId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
await client.CreateTTSVoice(voiceName);
|
||||
_logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DeleteTTSVoiceCommand : IChatPartialCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
private ILogger _logger;
|
||||
|
||||
public bool AcceptCustomPermission { get => false; }
|
||||
|
||||
public DeleteTTSVoiceCommand(User user, ILogger logger)
|
||||
{
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
if (_user == null || _user.VoicesAvailable == null)
|
||||
{
|
||||
_logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
var voiceName = values["voiceName"];
|
||||
var voiceNameLower = voiceName.ToLower();
|
||||
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
||||
if (!exists)
|
||||
{
|
||||
_logger.Debug($"Voice does not exist [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||
await client.DeleteTTSVoice(voiceId);
|
||||
_logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SetTTSVoiceStateCommand : IChatPartialCommand
|
||||
{
|
||||
private bool _state;
|
||||
private readonly User _user;
|
||||
private ILogger _logger;
|
||||
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public SetTTSVoiceStateCommand(bool state, User user, ILogger logger)
|
||||
{
|
||||
_state = state;
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
if (_user == null || _user.VoicesAvailable == null)
|
||||
return;
|
||||
|
||||
var voiceName = values["voiceName"];
|
||||
var voiceNameLower = voiceName.ToLower();
|
||||
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceNameLower).Key;
|
||||
|
||||
await client.UpdateTTSVoiceState(voiceId, _state);
|
||||
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {_state}][invoker: {message.Username}][id: {message.UserId}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,31 +2,52 @@ using HermesSocketLibrary.Socket.Data;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class VersionCommand : ChatCommand
|
||||
public class VersionCommand : IChatCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
private ILogger _logger;
|
||||
|
||||
public string Name => "version";
|
||||
|
||||
public VersionCommand(User user, ILogger logger)
|
||||
: base("version", "Does nothing.")
|
||||
{
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
|
||||
public void Build(ICommandBuilder builder)
|
||||
{
|
||||
return message.IsBroadcaster;
|
||||
builder.CreateCommandTree(Name, b => b.CreateCommand(new AppVersionCommand(_user, _logger)));
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
|
||||
private sealed class AppVersionCommand : IChatPartialCommand
|
||||
{
|
||||
_logger.Information($"Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
|
||||
private readonly User _user;
|
||||
private ILogger _logger;
|
||||
|
||||
await client.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.");
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public AppVersionCommand(User user, ILogger logger)
|
||||
{
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
return message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
_logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
|
||||
|
||||
await client.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +1,71 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchLib.Client.Models;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class VoiceCommand : ChatCommand
|
||||
public class VoiceCommand : IChatCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public VoiceCommand(
|
||||
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
|
||||
User user,
|
||||
ILogger logger
|
||||
) : base("voice", "Select a TTS voice as the default for that user.")
|
||||
public VoiceCommand(User user, ILogger logger)
|
||||
{
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
|
||||
AddParameter(ttsVoiceParameter);
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
|
||||
public string Name => "voice";
|
||||
|
||||
public void Build(ICommandBuilder builder)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100;
|
||||
builder.CreateCommandTree(Name, b =>
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", true)
|
||||
.CreateCommand(new TTSVoiceSelector(_user, _logger));
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
|
||||
private sealed class TTSVoiceSelector : IChatPartialCommand
|
||||
{
|
||||
if (_user == null || _user.VoicesSelected == null || _user.VoicesEnabled == null)
|
||||
return;
|
||||
private readonly User _user;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
long chatterId = long.Parse(message.UserId);
|
||||
var voiceName = args.First().ToLower();
|
||||
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName);
|
||||
var enabled = _user.VoicesEnabled.Contains(voice.Value);
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
if (!enabled)
|
||||
public TTSVoiceSelector(User user, ILogger logger)
|
||||
{
|
||||
_logger.Information($"Voice is disabled. Cannot switch to that voice [voice: {voice.Value}][username: {message.Username}]");
|
||||
return;
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
if (_user.VoicesSelected.ContainsKey(chatterId))
|
||||
|
||||
public bool CheckDefaultPermissions(ChatMessage message)
|
||||
{
|
||||
await client.UpdateTTSUser(chatterId, voice.Key);
|
||||
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]");
|
||||
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100;
|
||||
}
|
||||
else
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
await client.CreateTTSUser(chatterId, voice.Key);
|
||||
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]");
|
||||
if (_user == null || _user.VoicesSelected == null)
|
||||
return;
|
||||
|
||||
long chatterId = long.Parse(message.UserId);
|
||||
var voiceName = values["voiceName"];
|
||||
var voiceNameLower = voiceName.ToLower();
|
||||
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceNameLower);
|
||||
|
||||
if (_user.VoicesSelected.ContainsKey(chatterId))
|
||||
{
|
||||
await client.UpdateTTSUser(chatterId, voice.Key);
|
||||
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]");
|
||||
}
|
||||
else
|
||||
{
|
||||
await client.CreateTTSUser(chatterId, voice.Key);
|
||||
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Groups
|
||||
@ -59,7 +60,10 @@ namespace TwitchChatTTS.Chat.Groups
|
||||
}
|
||||
|
||||
public int GetPriorityFor(IEnumerable<string> groupNames) {
|
||||
return groupNames.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null).Max(g => g.Priority);
|
||||
var values = groupNames.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null);
|
||||
if (values.Any())
|
||||
return values.Max(g => g.Priority);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool Remove(long chatterId, string groupId) {
|
||||
|
@ -1,9 +0,0 @@
|
||||
namespace TwitchChatTTS.Chat.Groups
|
||||
{
|
||||
public class Group
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
namespace TwitchChatTTS.Chat.Groups
|
||||
{
|
||||
public class GroupChatter
|
||||
{
|
||||
public string GroupId { get; set; }
|
||||
public long ChatterId { get; set;}
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Groups
|
||||
{
|
||||
public interface IChatterGroupManager
|
||||
|
@ -1,10 +0,0 @@
|
||||
namespace TwitchChatTTS.Chat.Groups.Permissions
|
||||
{
|
||||
public class GroupPermission
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string GroupId { get; set; }
|
||||
public string Path { get; set; }
|
||||
public bool? Allow { get; set; }
|
||||
}
|
||||
}
|
@ -37,8 +37,7 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
if (_root.Children != null)
|
||||
_root.Children.Clear();
|
||||
_root.Clear();
|
||||
}
|
||||
|
||||
public bool Remove(string path)
|
||||
@ -127,6 +126,11 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
||||
_children.Add(child);
|
||||
}
|
||||
|
||||
internal void Clear() {
|
||||
if (_children != null)
|
||||
_children.Clear();
|
||||
}
|
||||
|
||||
public void Remove(string name)
|
||||
{
|
||||
if (_children == null || !_children.Any())
|
||||
|
@ -23,7 +23,7 @@ namespace TwitchChatTTS.Helpers
|
||||
_client.DefaultRequestHeaders.Add(key, value);
|
||||
}
|
||||
|
||||
public async Task<T?> GetJson<T>(string uri, JsonSerializerOptions options = null)
|
||||
public async Task<T?> GetJson<T>(string uri, JsonSerializerOptions? options = null)
|
||||
{
|
||||
var response = await _client.GetAsync(uri);
|
||||
return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), options ?? _options);
|
||||
|
@ -40,39 +40,15 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
client.LoggedIn = true;
|
||||
_logger.Information($"Logged in as {_user.TwitchUsername} {(message.WebLogin ? "via web" : "via TTS app")}.");
|
||||
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_voices",
|
||||
Data = null
|
||||
});
|
||||
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_users",
|
||||
Data = new Dictionary<string, object>() { { "user", _user.HermesUserId } }
|
||||
});
|
||||
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_default_tts_voice",
|
||||
Data = null
|
||||
});
|
||||
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_chatter_ids",
|
||||
Data = null
|
||||
});
|
||||
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_emotes",
|
||||
Data = null
|
||||
});
|
||||
|
||||
await client.GetRedemptions();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(3));
|
||||
await client.FetchTTSVoices();
|
||||
await client.FetchEnabledTTSVoices();
|
||||
await client.FetchTTSWordFilters();
|
||||
await client.FetchTTSChatterVoices();
|
||||
await client.FetchDefaultTTSVoice();
|
||||
await client.FetchChatterIdentifiers();
|
||||
await client.FetchEmotes();
|
||||
await client.FetchRedemptions();
|
||||
await client.FetchPermissions();
|
||||
|
||||
_logger.Information("TTS is now ready.");
|
||||
client.Ready = true;
|
||||
|
@ -8,6 +8,8 @@ using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Emotes;
|
||||
using TwitchChatTTS.Chat.Groups;
|
||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
||||
using TwitchChatTTS.Twitch.Redemptions;
|
||||
|
||||
namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
@ -28,7 +30,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
|
||||
public RequestAckHandler(
|
||||
User user,
|
||||
//RedemptionManager redemptionManager,
|
||||
ICallbackManager<HermesRequestData> callbackManager,
|
||||
IServiceProvider serviceProvider,
|
||||
JsonSerializerOptions options,
|
||||
@ -36,7 +37,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
)
|
||||
{
|
||||
_user = user;
|
||||
//_redemptionManager = redemptionManager;
|
||||
_callbackManager = callbackManager;
|
||||
_serviceProvider = serviceProvider;
|
||||
_options = options;
|
||||
@ -66,7 +66,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
_logger.Debug($"Received a Hermes request message [type: {message.Request.Type}][data: {string.Join(',', message.Request.Data?.Select(entry => entry.Key + '=' + entry.Value) ?? Array.Empty<string>())}]");
|
||||
if (message.Request.Type == "get_tts_voices")
|
||||
{
|
||||
_logger.Verbose("Updating all available voices for TTS.");
|
||||
var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(message.Data.ToString(), _options);
|
||||
if (voices == null)
|
||||
return;
|
||||
@ -79,7 +78,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "create_tts_user")
|
||||
{
|
||||
_logger.Verbose("Adding new tts voice for user.");
|
||||
if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
|
||||
{
|
||||
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]");
|
||||
@ -93,7 +91,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "update_tts_user")
|
||||
{
|
||||
_logger.Verbose("Updating user's voice");
|
||||
if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
|
||||
{
|
||||
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]");
|
||||
@ -107,7 +104,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "create_tts_voice")
|
||||
{
|
||||
_logger.Verbose("Creating new tts voice.");
|
||||
string? voice = message.Request.Data["voice"].ToString();
|
||||
string? voiceId = message.Data.ToString();
|
||||
if (voice == null || voiceId == null)
|
||||
@ -123,7 +119,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "delete_tts_voice")
|
||||
{
|
||||
_logger.Verbose("Deleting tts voice.");
|
||||
var voice = message.Request.Data["voice"].ToString();
|
||||
if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null)
|
||||
return;
|
||||
@ -138,7 +133,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "update_tts_voice")
|
||||
{
|
||||
_logger.Verbose("Updating TTS voice.");
|
||||
string voiceId = message.Request.Data["idd"].ToString();
|
||||
string voice = message.Request.Data["voice"].ToString();
|
||||
|
||||
@ -150,7 +144,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "get_tts_users")
|
||||
{
|
||||
_logger.Verbose("Updating all chatters' selected voice.");
|
||||
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(message.Data.ToString(), _options);
|
||||
if (users == null)
|
||||
return;
|
||||
@ -163,7 +156,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "get_chatter_ids")
|
||||
{
|
||||
_logger.Verbose("Fetching all chatters' id.");
|
||||
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(message.Data.ToString(), _options);
|
||||
if (chatters == null)
|
||||
return;
|
||||
@ -174,7 +166,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "get_emotes")
|
||||
{
|
||||
_logger.Verbose("Updating emotes.");
|
||||
var emotes = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(message.Data.ToString(), _options);
|
||||
if (emotes == null)
|
||||
return;
|
||||
@ -196,9 +187,78 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
if (duplicateNames > 0)
|
||||
_logger.Warning($"Found {duplicateNames} emotes with duplicate names.");
|
||||
}
|
||||
else if (message.Request.Type == "get_enabled_tts_voices")
|
||||
{
|
||||
var enabledTTSVoices = JsonSerializer.Deserialize<IEnumerable<string>>(message.Data.ToString(), _options);
|
||||
if (enabledTTSVoices == null)
|
||||
{
|
||||
_logger.Error("Failed to load enabled tts voices.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_user.VoicesEnabled == null)
|
||||
_user.VoicesEnabled = enabledTTSVoices.ToHashSet();
|
||||
else
|
||||
_user.VoicesEnabled.Clear();
|
||||
foreach (var voice in enabledTTSVoices)
|
||||
_user.VoicesEnabled.Add(voice);
|
||||
_logger.Information($"TTS voices [count: {_user.VoicesEnabled.Count}] have been enabled.");
|
||||
}
|
||||
else if (message.Request.Type == "get_permissions")
|
||||
{
|
||||
var groupInfo = JsonSerializer.Deserialize<GroupInfo>(message.Data.ToString(), _options);
|
||||
if (groupInfo == null)
|
||||
{
|
||||
_logger.Error("Failed to load groups & permissions.");
|
||||
return;
|
||||
}
|
||||
|
||||
var chatterGroupManager = _serviceProvider.GetRequiredService<IChatterGroupManager>();
|
||||
var permissionManager = _serviceProvider.GetRequiredService<IGroupPermissionManager>();
|
||||
|
||||
permissionManager.Clear();
|
||||
chatterGroupManager.Clear();
|
||||
|
||||
var groupsById = groupInfo.Groups.ToDictionary(g => g.Id, g => g);
|
||||
foreach (var group in groupInfo.Groups)
|
||||
chatterGroupManager.Add(group);
|
||||
|
||||
foreach (var permission in groupInfo.GroupPermissions)
|
||||
{
|
||||
_logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
|
||||
if (!groupsById.TryGetValue(permission.GroupId, out var group))
|
||||
{
|
||||
_logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var path = $"{group.Name}.{permission.Path}";
|
||||
permissionManager.Set(path, permission.Allow);
|
||||
_logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
|
||||
}
|
||||
|
||||
_logger.Information($"Groups [count: {groupInfo.Groups.Count()}] & Permissions [count: {groupInfo.GroupPermissions.Count()}] have been loaded.");
|
||||
|
||||
foreach (var chatter in groupInfo.GroupChatters)
|
||||
if (groupsById.TryGetValue(chatter.GroupId, out var group))
|
||||
chatterGroupManager.Add(chatter.ChatterId, group.Name);
|
||||
_logger.Information($"Users in each group [count: {groupInfo.GroupChatters.Count()}] have been loaded.");
|
||||
}
|
||||
else if (message.Request.Type == "get_tts_word_filters")
|
||||
{
|
||||
var wordFilters = JsonSerializer.Deserialize<IEnumerable<TTSWordFilter>>(message.Data.ToString(), _options);
|
||||
if (wordFilters == null)
|
||||
{
|
||||
_logger.Error("Failed to load word filters.");
|
||||
return;
|
||||
}
|
||||
|
||||
_user.RegexFilters = wordFilters.ToList();
|
||||
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count}] have been refreshed.");
|
||||
}
|
||||
else if (message.Request.Type == "update_tts_voice_state")
|
||||
{
|
||||
_logger.Verbose("Updating TTS voice states.");
|
||||
string voiceId = message.Request.Data["voice"].ToString();
|
||||
bool state = message.Request.Data["state"].ToString().ToLower() == "true";
|
||||
|
||||
@ -216,7 +276,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "get_redemptions")
|
||||
{
|
||||
_logger.Verbose("Fetching all the redemptions.");
|
||||
IEnumerable<Redemption>? redemptions = JsonSerializer.Deserialize<IEnumerable<Redemption>>(message.Data!.ToString()!, _options);
|
||||
if (redemptions != null)
|
||||
{
|
||||
@ -229,7 +288,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
}
|
||||
else if (message.Request.Type == "get_redeemable_actions")
|
||||
{
|
||||
_logger.Verbose("Fetching all the redeemable actions.");
|
||||
IEnumerable<RedeemableAction>? actions = JsonSerializer.Deserialize<IEnumerable<RedeemableAction>>(message.Data!.ToString()!, _options);
|
||||
if (actions == null)
|
||||
{
|
||||
|
@ -36,14 +36,14 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
User user,
|
||||
Configuration configuration,
|
||||
ICallbackManager<HermesRequestData> callbackManager,
|
||||
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
|
||||
[FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager,
|
||||
[FromKeyedServices("hermes")] IEnumerable<IWebSocketHandler> handlers,
|
||||
[FromKeyedServices("hermes")] MessageTypeManager<IWebSocketHandler> typeManager,
|
||||
ILogger logger
|
||||
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
|
||||
) : base(handlers, typeManager, new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
})
|
||||
}, logger)
|
||||
{
|
||||
_user = user;
|
||||
_configuration = configuration;
|
||||
@ -74,7 +74,7 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
if (!Connected)
|
||||
return;
|
||||
|
||||
await DisconnectAsync();
|
||||
await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed"));
|
||||
}
|
||||
|
||||
public async Task CreateTTSVoice(string voiceName)
|
||||
@ -104,11 +104,67 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task GetRedemptions()
|
||||
public async Task FetchChatterIdentifiers() {
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_chatter_ids",
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchDefaultTTSVoice() {
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_default_tts_voice",
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchEmotes() {
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_emotes",
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchEnabledTTSVoices() {
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_enabled_tts_voices",
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchTTSVoices() {
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_voices",
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchTTSChatterVoices() {
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_users",
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchTTSWordFilters() {
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_word_filters",
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchRedemptions()
|
||||
{
|
||||
var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData()
|
||||
{
|
||||
Callback = async (d) => await GetRedeemableActions(d["redemptions"] as IEnumerable<Redemption>),
|
||||
Callback = async (d) => await FetchRedeemableActions(d["redemptions"] as IEnumerable<Redemption>),
|
||||
Data = new Dictionary<string, object>()
|
||||
});
|
||||
|
||||
@ -120,7 +176,7 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task GetRedeemableActions(IEnumerable<Redemption> redemptions)
|
||||
private async Task FetchRedeemableActions(IEnumerable<Redemption> redemptions)
|
||||
{
|
||||
var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData()
|
||||
{
|
||||
@ -135,6 +191,15 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchPermissions()
|
||||
{
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_permissions",
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_logger.Information("Initializing Hermes websocket client.");
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,12 +7,12 @@ using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.Hermes.Socket.Managers
|
||||
{
|
||||
public class HermesHandlerTypeManager : WebSocketHandlerTypeManager
|
||||
public class HermesMessageTypeManager : WebSocketMessageTypeManager
|
||||
{
|
||||
public HermesHandlerTypeManager(
|
||||
ILogger factory,
|
||||
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
|
||||
) : base(factory, handlers)
|
||||
public HermesMessageTypeManager(
|
||||
[FromKeyedServices("hermes")] IEnumerable<IWebSocketHandler> handlers,
|
||||
ILogger logger
|
||||
) : base(handlers, logger)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,19 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Data
|
||||
{
|
||||
public class IdentifyMessage
|
||||
{
|
||||
public int RpcVersion { get; set; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Authentication { get; set; }
|
||||
public int EventSubscriptions { get; set; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? EventSubscriptions { get; set; }
|
||||
|
||||
public IdentifyMessage(int version, string auth, int subscriptions)
|
||||
public IdentifyMessage(int rpcVersion, string? authentication, int? subscriptions)
|
||||
{
|
||||
RpcVersion = version;
|
||||
Authentication = auth;
|
||||
RpcVersion = rpcVersion;
|
||||
Authentication = authentication;
|
||||
EventSubscriptions = subscriptions;
|
||||
}
|
||||
}
|
||||
|
@ -2,19 +2,18 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
public class EventMessageHandler : IWebSocketHandler
|
||||
{
|
||||
private readonly OBSManager _manager;
|
||||
private readonly ILogger _logger;
|
||||
public int OperationCode { get; } = 5;
|
||||
|
||||
public EventMessageHandler(OBSManager manager, ILogger logger)
|
||||
public EventMessageHandler(
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_manager = manager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -22,6 +21,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
if (data is not EventMessage message || message == null)
|
||||
return;
|
||||
if (sender is not OBSSocketClient obs)
|
||||
return;
|
||||
|
||||
switch (message.EventType)
|
||||
{
|
||||
@ -31,10 +32,10 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
|
||||
string? raw_state = message.EventData["outputState"].ToString();
|
||||
string? state = raw_state?.Substring(21).ToLower();
|
||||
_manager.Streaming = message.EventData["outputActive"].ToString().ToLower() == "true";
|
||||
obs.Streaming = message.EventData["outputActive"].ToString()!.ToLower() == "true";
|
||||
_logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
|
||||
|
||||
if (_manager.Streaming == false && state != null && !state.EndsWith("ing"))
|
||||
if (obs.Streaming == false && state != null && !state.EndsWith("ing"))
|
||||
{
|
||||
// Stream ended
|
||||
}
|
||||
|
@ -26,9 +26,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
|
||||
string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim();
|
||||
_logger.Verbose("OBS websocket password: " + password);
|
||||
if (message.Authentication == null || string.IsNullOrWhiteSpace(password))
|
||||
if (message.Authentication == null || string.IsNullOrEmpty(password))
|
||||
{
|
||||
await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144));
|
||||
await sender.Send(1, new IdentifyMessage(message.RpcVersion, null, 1023 | 262144));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
|
||||
string secret = password + salt;
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(secret);
|
||||
string hash = null;
|
||||
string? hash = null;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
bytes = sha.ComputeHash(bytes);
|
||||
|
@ -2,19 +2,16 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
public class IdentifiedHandler : IWebSocketHandler
|
||||
{
|
||||
private readonly OBSManager _manager;
|
||||
private readonly ILogger _logger;
|
||||
public int OperationCode { get; } = 2;
|
||||
|
||||
public IdentifiedHandler(OBSManager manager, ILogger logger)
|
||||
public IdentifiedHandler(ILogger logger)
|
||||
{
|
||||
_manager = manager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -22,20 +19,22 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
if (data is not IdentifiedMessage message || message == null)
|
||||
return;
|
||||
if (sender is not OBSSocketClient obs)
|
||||
return;
|
||||
|
||||
_manager.Connected = true;
|
||||
obs.Identified = true;
|
||||
_logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + ".");
|
||||
|
||||
try
|
||||
{
|
||||
await _manager.GetGroupList(async groups => await _manager.GetGroupSceneItemList(groups));
|
||||
await obs.GetGroupList(async groups => await obs.GetGroupSceneItemList(groups));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to load OBS group info upon OBS identification.");
|
||||
}
|
||||
|
||||
await _manager.UpdateStreamingState();
|
||||
await obs.UpdateStreamingState();
|
||||
}
|
||||
}
|
||||
}
|
@ -5,22 +5,16 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using Serilog.Context;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
public class RequestBatchResponseHandler : IWebSocketHandler
|
||||
{
|
||||
private readonly IWebSocketHandler _requestResponseHandler;
|
||||
private readonly ILogger _logger;
|
||||
public int OperationCode { get; } = 9;
|
||||
|
||||
public RequestBatchResponseHandler(
|
||||
[FromKeyedServices("obs-requestresponse")] IWebSocketHandler requestResponseHandler,
|
||||
ILogger logger
|
||||
)
|
||||
public RequestBatchResponseHandler(ILogger logger)
|
||||
{
|
||||
_requestResponseHandler = requestResponseHandler;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -28,40 +22,38 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
if (data is not RequestBatchResponseMessage message || message == null)
|
||||
return;
|
||||
if (sender is not OBSSocketClient obs)
|
||||
return;
|
||||
|
||||
using (LogContext.PushProperty("obsrid", message.RequestId))
|
||||
var results = message.Results.ToList();
|
||||
_logger.Debug($"Received request batch response of {results.Count} messages.");
|
||||
|
||||
int count = results.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (results[i] == null)
|
||||
continue;
|
||||
|
||||
var results = message.Results.ToList();
|
||||
_logger.Debug($"Received request batch response of {results.Count} messages.");
|
||||
|
||||
int count = results.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
try
|
||||
{
|
||||
if (results[i] == null)
|
||||
_logger.Debug($"Request response from OBS request batch #{i + 1}/{count}: {results[i]}");
|
||||
var response = JsonSerializer.Deserialize<RequestResponseMessage>(results[i].ToString()!, new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
if (response == null)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Debug($"Request response from OBS request batch #{i + 1}/{count}: {results[i]}");
|
||||
var response = JsonSerializer.Deserialize<RequestResponseMessage>(results[i].ToString()!, new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
if (response == null)
|
||||
continue;
|
||||
|
||||
await _requestResponseHandler.Execute(sender, response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to process an item in a request batch message.");
|
||||
}
|
||||
await obs.ExecuteRequest(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to process an item in a request batch message.");
|
||||
}
|
||||
|
||||
_logger.Debug($"Finished processing all request in this batch.");
|
||||
}
|
||||
|
||||
_logger.Debug($"Finished processing all request in this batch.");
|
||||
}
|
||||
}
|
||||
}
|
@ -3,19 +3,18 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
public class RequestResponseHandler : IWebSocketHandler
|
||||
{
|
||||
private readonly OBSManager _manager;
|
||||
private readonly ILogger _logger;
|
||||
public int OperationCode { get; } = 7;
|
||||
|
||||
public RequestResponseHandler(OBSManager manager, ILogger logger)
|
||||
public RequestResponseHandler(
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_manager = manager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -23,10 +22,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
if (data is not RequestResponseMessage message || message == null)
|
||||
return;
|
||||
if (sender is not OBSSocketClient obs)
|
||||
return;
|
||||
|
||||
_logger.Debug($"Received an OBS request response [obs request id: {message.RequestId}]");
|
||||
|
||||
var requestData = _manager.Take(message.RequestId);
|
||||
var requestData = obs.Take(message.RequestId);
|
||||
if (requestData == null)
|
||||
{
|
||||
_logger.Warning($"OBS Request Response not being processed: request not stored [obs request id: {message.RequestId}]");
|
||||
@ -42,7 +43,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
switch (request.RequestType)
|
||||
{
|
||||
case "GetOutputStatus":
|
||||
_logger.Debug($"Fetched stream's live status [live: {_manager.Streaming}][obs request id: {message.RequestId}]");
|
||||
_logger.Debug($"Fetched stream's live status [live: {obs.Streaming}][obs request id: {message.RequestId}]");
|
||||
break;
|
||||
case "GetSceneItemId":
|
||||
{
|
||||
@ -206,7 +207,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
}
|
||||
|
||||
foreach (var sceneItem in sceneItems)
|
||||
_manager.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId);
|
||||
obs.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId);
|
||||
|
||||
requestData.ResponseValues = new Dictionary<string, object>()
|
||||
{
|
||||
@ -237,9 +238,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
return;
|
||||
}
|
||||
|
||||
_manager.Streaming = outputActive?.ToString()!.ToLower() == "true";
|
||||
obs.Streaming = outputActive?.ToString()!.ToLower() == "true";
|
||||
requestData.ResponseValues = message.ResponseData;
|
||||
_logger.Information($"OBS is currently {(_manager.Streaming ? "" : "not ")}streaming.");
|
||||
_logger.Information($"OBS is currently {(obs.Streaming ? "" : "not ")}streaming.");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using CommonSocketLibrary.Socket.Manager;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -6,12 +5,12 @@ using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Manager
|
||||
{
|
||||
public class OBSHandlerTypeManager : WebSocketHandlerTypeManager
|
||||
public class OBSMessageTypeManager : WebSocketMessageTypeManager
|
||||
{
|
||||
public OBSHandlerTypeManager(
|
||||
ILogger factory,
|
||||
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
|
||||
) : base(factory, handlers)
|
||||
public OBSMessageTypeManager(
|
||||
[FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
|
||||
ILogger logger
|
||||
) : base(handlers, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,21 +3,365 @@ using CommonSocketLibrary.Abstract;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using System.Text.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using System.Timers;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket
|
||||
{
|
||||
public class OBSSocketClient : WebSocketClient
|
||||
{
|
||||
private readonly IDictionary<string, RequestData> _requests;
|
||||
private readonly IDictionary<string, long> _sourceIds;
|
||||
private string? URL;
|
||||
|
||||
private readonly Configuration _configuration;
|
||||
private System.Timers.Timer _reconnectTimer;
|
||||
|
||||
public bool Connected { get; set; }
|
||||
public bool Identified { get; set; }
|
||||
public bool Streaming { get; set; }
|
||||
|
||||
|
||||
public OBSSocketClient(
|
||||
ILogger logger,
|
||||
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
|
||||
[FromKeyedServices("obs")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
|
||||
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
|
||||
Configuration configuration,
|
||||
[FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
|
||||
[FromKeyedServices("obs")] MessageTypeManager<IWebSocketHandler> typeManager,
|
||||
ILogger logger
|
||||
) : base(handlers, typeManager, new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
})
|
||||
}, logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
|
||||
_reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(30));
|
||||
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect(e);
|
||||
_reconnectTimer.Enabled = false;
|
||||
|
||||
_requests = new ConcurrentDictionary<string, RequestData>();
|
||||
_sourceIds = new Dictionary<string, long>();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_logger.Information($"Initializing OBS websocket client.");
|
||||
OnConnected += (sender, e) =>
|
||||
{
|
||||
Connected = true;
|
||||
_reconnectTimer.Enabled = false;
|
||||
_logger.Information("OBS websocket client connected.");
|
||||
};
|
||||
|
||||
OnDisconnected += (sender, e) =>
|
||||
{
|
||||
_reconnectTimer.Enabled = Identified;
|
||||
_logger.Information($"OBS websocket client disconnected [status: {e.Status}][reason: {e.Reason}] " + (Identified ? "Will be attempting to reconnect every 30 seconds." : "Will not be attempting to reconnect."));
|
||||
|
||||
Connected = false;
|
||||
Identified = false;
|
||||
Streaming = false;
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null)
|
||||
URL = $"ws://{_configuration.Obs.Host?.Trim()}:{_configuration.Obs.Port}";
|
||||
}
|
||||
|
||||
|
||||
public void AddSourceId(string sourceName, long sourceId)
|
||||
{
|
||||
if (!_sourceIds.TryGetValue(sourceName, out _))
|
||||
_sourceIds.Add(sourceName, sourceId);
|
||||
else
|
||||
_sourceIds[sourceName] = sourceId;
|
||||
_logger.Debug($"Added OBS scene item to cache [scene item: {sourceName}][scene item id: {sourceId}]");
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
_sourceIds.Clear();
|
||||
}
|
||||
|
||||
public async Task Connect()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(URL))
|
||||
{
|
||||
_logger.Warning("Lacking connection info for OBS websockets. Not connecting to OBS.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug($"OBS websocket client attempting to connect to {URL}");
|
||||
|
||||
try
|
||||
{
|
||||
await ConnectAsync(URL);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Warning("Connecting to obs failed. Skipping obs websockets.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExecuteRequest(RequestResponseMessage message) {
|
||||
if (!_handlers.TryGetValue(7, out var handler) || handler == null)
|
||||
{
|
||||
_logger.Error("Failed to find the request response handler for OBS.");
|
||||
return;
|
||||
}
|
||||
|
||||
await handler.Execute(this, message);
|
||||
}
|
||||
|
||||
private async Task Reconnect(ElapsedEventArgs e)
|
||||
{
|
||||
if (Connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), ""));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Error("Failed to disconnect from OBS websocket server.");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Connect();
|
||||
}
|
||||
catch (WebSocketException wse) when (wse.Message.Contains("502"))
|
||||
{
|
||||
_logger.Error("OBS websocket server cannot be found. Be sure the server is on by looking at OBS > Tools > Websocket Server Settings.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to reconnect to OBS websocket server.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Send(IEnumerable<RequestMessage> messages)
|
||||
{
|
||||
if (!Connected)
|
||||
{
|
||||
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
|
||||
return;
|
||||
}
|
||||
|
||||
string uid = GenerateUniqueIdentifier();
|
||||
var list = messages.ToList();
|
||||
_logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}].");
|
||||
|
||||
// Keep track of requests to know what we requested.
|
||||
foreach (var message in list)
|
||||
{
|
||||
message.RequestId = GenerateUniqueIdentifier();
|
||||
var data = new RequestData(message, uid);
|
||||
_requests.Add(message.RequestId, data);
|
||||
}
|
||||
|
||||
_logger.Debug($"Generated uid for all OBS request messages in batch [obs request batch id: {uid}][obs request ids: {string.Join(", ", list.Select(m => m.RequestType + "=" + m.RequestId))}]");
|
||||
await Send(8, new RequestBatchMessage(uid, list));
|
||||
}
|
||||
|
||||
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null)
|
||||
{
|
||||
if (!Connected)
|
||||
{
|
||||
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
|
||||
return;
|
||||
}
|
||||
|
||||
string uid = GenerateUniqueIdentifier();
|
||||
_logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]");
|
||||
|
||||
// Keep track of requests to know what we requested.
|
||||
message.RequestId = uid;
|
||||
var data = new RequestData(message, uid)
|
||||
{
|
||||
Callback = callback
|
||||
};
|
||||
_requests.Add(message.RequestId, data);
|
||||
|
||||
await Send(6, message);
|
||||
}
|
||||
|
||||
public RequestData? Take(string id)
|
||||
{
|
||||
if (id != null && _requests.TryGetValue(id, out var request))
|
||||
{
|
||||
_requests.Remove(id);
|
||||
return request;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task UpdateStreamingState()
|
||||
{
|
||||
await Send(new RequestMessage("GetStreamStatus"));
|
||||
}
|
||||
|
||||
public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
|
||||
{
|
||||
var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
|
||||
await Send(m2, async (d) =>
|
||||
{
|
||||
if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
|
||||
return;
|
||||
|
||||
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]");
|
||||
var transform = JsonSerializer.Deserialize<OBSTransformationData>(transformData.ToString()!, new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
if (transform == null)
|
||||
{
|
||||
_logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m2.RequestId}].");
|
||||
return;
|
||||
}
|
||||
|
||||
double w = transform.Width;
|
||||
double h = transform.Height;
|
||||
int a = transform.Alignment;
|
||||
bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE";
|
||||
|
||||
if (a != (int)OBSAlignment.Center)
|
||||
{
|
||||
if (hasBounds)
|
||||
transform.BoundsAlignment = a = (int)OBSAlignment.Center;
|
||||
else
|
||||
transform.Alignment = a = (int)OBSAlignment.Center;
|
||||
|
||||
transform.PositionX = transform.PositionX + w / 2;
|
||||
transform.PositionY = transform.PositionY + h / 2;
|
||||
}
|
||||
|
||||
action?.Invoke(transform);
|
||||
|
||||
var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } });
|
||||
await Send(m3);
|
||||
_logger.Debug($"New transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m3.RequestId}]");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName)
|
||||
{
|
||||
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
|
||||
{
|
||||
var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
|
||||
await Send(m1, async (d) =>
|
||||
{
|
||||
if (d == null || !d.TryGetValue("sceneItemEnabled", out object? visible) || visible == null)
|
||||
return;
|
||||
|
||||
var m2 = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", visible.ToString().ToLower() == "true" ? false : true } });
|
||||
await Send(m2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible)
|
||||
{
|
||||
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
|
||||
{
|
||||
var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } });
|
||||
await Send(m);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index)
|
||||
{
|
||||
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
|
||||
{
|
||||
var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } });
|
||||
await Send(m);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task GetGroupList(Action<IEnumerable<string>>? action)
|
||||
{
|
||||
var m = new RequestMessage("GetGroupList", string.Empty, new Dictionary<string, object>());
|
||||
await Send(m, (d) =>
|
||||
{
|
||||
if (d == null || !d.TryGetValue("groups", out object? value) || value == null)
|
||||
return;
|
||||
|
||||
var list = (IEnumerable<string>)value;
|
||||
_logger.Debug("Fetched the list of groups in OBS.");
|
||||
if (list != null)
|
||||
action?.Invoke(list);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task GetGroupSceneItemList(string groupName, Action<IEnumerable<OBSSceneItem>>? action)
|
||||
{
|
||||
var m = new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", groupName } });
|
||||
await Send(m, (d) =>
|
||||
{
|
||||
if (d == null || !d.TryGetValue("sceneItems", out object? value) || value == null)
|
||||
return;
|
||||
|
||||
var list = (IEnumerable<OBSSceneItem>)value;
|
||||
_logger.Debug($"Fetched the list of OBS scene items in a group [group: {groupName}]");
|
||||
if (list != null)
|
||||
action?.Invoke(list);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task GetGroupSceneItemList(IEnumerable<string> groupNames)
|
||||
{
|
||||
var messages = groupNames.Select(group => new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", group } }));
|
||||
await Send(messages);
|
||||
_logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]");
|
||||
}
|
||||
|
||||
private async Task GetSceneItemByName(string sceneName, string sceneItemName, Action<long> action)
|
||||
{
|
||||
if (_sourceIds.TryGetValue(sceneItemName, out long sourceId))
|
||||
{
|
||||
_logger.Debug($"Fetched scene item id from cache [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sourceId}]");
|
||||
action.Invoke(sourceId);
|
||||
return;
|
||||
}
|
||||
|
||||
var m = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
|
||||
await Send(m, async (d) =>
|
||||
{
|
||||
if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId))
|
||||
return;
|
||||
|
||||
_logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sceneItemId}][obs request id: {m.RequestId}]");
|
||||
AddSourceId(sceneItemName, sceneItemId);
|
||||
action.Invoke(sceneItemId);
|
||||
});
|
||||
}
|
||||
|
||||
private string GenerateUniqueIdentifier()
|
||||
{
|
||||
return Guid.NewGuid().ToString("N");
|
||||
}
|
||||
}
|
||||
|
||||
public class RequestData
|
||||
{
|
||||
public RequestMessage Message { get; }
|
||||
public string ParentId { get; }
|
||||
public Dictionary<string, object>? ResponseValues { get; set; }
|
||||
public Action<Dictionary<string, object>>? Callback { get; set; }
|
||||
|
||||
public RequestData(RequestMessage message, string parentId)
|
||||
{
|
||||
Message = message;
|
||||
ParentId = parentId;
|
||||
}
|
||||
}
|
||||
}
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace TwitchChatTTS.Seven.Socket.Context
|
||||
{
|
||||
public class ReconnectContext
|
||||
{
|
||||
public string? SessionId;
|
||||
}
|
||||
}
|
@ -1,102 +1,21 @@
|
||||
using System.Net.WebSockets;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven.Socket.Context;
|
||||
using TwitchChatTTS.Seven.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
{
|
||||
public class EndOfStreamHandler : IWebSocketHandler
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly User _user;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly string[] _errorCodes;
|
||||
private readonly int[] _reconnectDelay;
|
||||
|
||||
public int OperationCode { get; } = 7;
|
||||
|
||||
|
||||
public EndOfStreamHandler(User user, IServiceProvider serviceProvider, ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_user = user;
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
_errorCodes = [
|
||||
"Server Error",
|
||||
"Unknown Operation",
|
||||
"Invalid Payload",
|
||||
"Auth Failure",
|
||||
"Already Identified",
|
||||
"Rate Limited",
|
||||
"Restart",
|
||||
"Maintenance",
|
||||
"Timeout",
|
||||
"Already Subscribed",
|
||||
"Not Subscribed",
|
||||
"Insufficient Privilege",
|
||||
"Inactivity?"
|
||||
];
|
||||
_reconnectDelay = [
|
||||
1000,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
0,
|
||||
3000,
|
||||
1000,
|
||||
300000,
|
||||
1000,
|
||||
0,
|
||||
0,
|
||||
1000,
|
||||
1000
|
||||
];
|
||||
}
|
||||
|
||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||
{
|
||||
if (data is not EndOfStreamMessage message || message == null)
|
||||
return;
|
||||
|
||||
var code = message.Code - 4000;
|
||||
if (code >= 0 && code < _errorCodes.Length)
|
||||
_logger.Warning($"Received end of stream message (reason: {_errorCodes[code]}, code: {message.Code}, message: {message.Message}).");
|
||||
else
|
||||
_logger.Warning($"Received end of stream message (code: {message.Code}, message: {message.Message}).");
|
||||
|
||||
await sender.DisconnectAsync();
|
||||
|
||||
if (code >= 0 && code < _reconnectDelay.Length && _reconnectDelay[code] < 0)
|
||||
{
|
||||
_logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
|
||||
{
|
||||
_logger.Warning("Could not find the 7tv emote set id. Not reconnecting.");
|
||||
return;
|
||||
}
|
||||
|
||||
var context = _serviceProvider.GetRequiredService<ReconnectContext>();
|
||||
if (_reconnectDelay[code] > 0)
|
||||
await Task.Delay(_reconnectDelay[code]);
|
||||
|
||||
var manager = _serviceProvider.GetRequiredService<SevenManager>();
|
||||
await manager.Connect();
|
||||
|
||||
if (context.SessionId != null)
|
||||
{
|
||||
await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId });
|
||||
_logger.Debug("Resumed connection to 7tv websocket.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Resumed connection to 7tv websocket on a different session.");
|
||||
}
|
||||
await sender.DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), code.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,6 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
{
|
||||
if (data is not SevenHelloMessage message || message == null)
|
||||
return;
|
||||
|
||||
if (sender is not SevenSocketClient seven || seven == null)
|
||||
return;
|
||||
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using CommonSocketLibrary.Socket.Manager;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -6,13 +5,12 @@ using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.Seven.Socket.Managers
|
||||
{
|
||||
public class SevenHandlerTypeManager : WebSocketHandlerTypeManager
|
||||
public class SevenMessageTypeManager : WebSocketMessageTypeManager
|
||||
{
|
||||
public SevenHandlerTypeManager(
|
||||
ILogger factory,
|
||||
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient,
|
||||
IWebSocketHandler> handlers
|
||||
) : base(factory, handlers)
|
||||
public SevenMessageTypeManager(
|
||||
[FromKeyedServices("7tv")] IEnumerable<IWebSocketHandler> handlers,
|
||||
ILogger logger
|
||||
) : base(handlers, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -9,19 +9,133 @@ namespace TwitchChatTTS.Seven.Socket
|
||||
{
|
||||
public class SevenSocketClient : WebSocketClient
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly string[] _errorCodes;
|
||||
private readonly int[] _reconnectDelay;
|
||||
private string? URL;
|
||||
|
||||
public bool Connected { get; set; }
|
||||
|
||||
public SevenHelloMessage? ConnectionDetails { get; set; }
|
||||
|
||||
public SevenSocketClient(
|
||||
ILogger logger,
|
||||
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
|
||||
[FromKeyedServices("7tv")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
|
||||
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
|
||||
User user,
|
||||
[FromKeyedServices("7tv")] IEnumerable<IWebSocketHandler> handlers,
|
||||
[FromKeyedServices("7tv")] MessageTypeManager<IWebSocketHandler> typeManager,
|
||||
ILogger logger
|
||||
) : base(handlers, typeManager, new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
})
|
||||
}, logger)
|
||||
{
|
||||
_user = user;
|
||||
ConnectionDetails = null;
|
||||
|
||||
_errorCodes = [
|
||||
"Server Error",
|
||||
"Unknown Operation",
|
||||
"Invalid Payload",
|
||||
"Auth Failure",
|
||||
"Already Identified",
|
||||
"Rate Limited",
|
||||
"Restart",
|
||||
"Maintenance",
|
||||
"Timeout",
|
||||
"Already Subscribed",
|
||||
"Not Subscribed",
|
||||
"Insufficient Privilege",
|
||||
"Inactivity?"
|
||||
];
|
||||
_reconnectDelay = [
|
||||
1000,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
0,
|
||||
3000,
|
||||
1000,
|
||||
300000,
|
||||
1000,
|
||||
0,
|
||||
0,
|
||||
1000,
|
||||
1000
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_logger.Information("Initializing 7tv websocket client.");
|
||||
OnConnected += (sender, e) =>
|
||||
{
|
||||
Connected = true;
|
||||
_logger.Information("7tv websocket client connected.");
|
||||
};
|
||||
|
||||
OnDisconnected += (sender, e) => OnDisconnection(sender, e);
|
||||
|
||||
if (!string.IsNullOrEmpty(_user.SevenEmoteSetId))
|
||||
URL = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={_user.SevenEmoteSetId}>";
|
||||
}
|
||||
|
||||
public async Task Connect()
|
||||
{
|
||||
if (string.IsNullOrEmpty(URL))
|
||||
{
|
||||
_logger.Warning("Cannot find 7tv url. Not connecting to 7tv websockets.");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
|
||||
{
|
||||
_logger.Warning("Cannot find 7tv data for your channel. Not connecting to 7tv websockets.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug($"7tv client attempting to connect to {URL}");
|
||||
await ConnectAsync($"{URL}");
|
||||
}
|
||||
|
||||
private async void OnDisconnection(object? sender, SocketDisconnectionEventArgs e)
|
||||
{
|
||||
Connected = false;
|
||||
|
||||
if (int.TryParse(e.Reason, out int code))
|
||||
{
|
||||
if (code >= 0 && code < _errorCodes.Length)
|
||||
_logger.Warning($"Received end of stream message for 7tv websocket [reason: {_errorCodes[code]}][code: {code}]");
|
||||
else
|
||||
_logger.Warning($"Received end of stream message for 7tv websocket [code: {code}]");
|
||||
|
||||
if (code >= 0 && code < _reconnectDelay.Length && _reconnectDelay[code] < 0)
|
||||
{
|
||||
_logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reconnectDelay[code] > 0)
|
||||
await Task.Delay(_reconnectDelay[code]);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
|
||||
{
|
||||
_logger.Warning("Could not find the 7tv emote set id. Not reconnecting.");
|
||||
return;
|
||||
}
|
||||
|
||||
await Connect();
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(500));
|
||||
|
||||
if (Connected && ConnectionDetails?.SessionId != null)
|
||||
{
|
||||
await Send(34, new ResumeMessage() { SessionId = ConnectionDetails.SessionId });
|
||||
_logger.Debug("Resumed connection to 7tv websocket.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Resumed connection to 7tv websocket on a different session.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
73
Startup.cs
73
Startup.cs
@ -10,7 +10,6 @@ using YamlDotNet.Serialization.NamingConventions;
|
||||
using TwitchChatTTS.Seven.Socket;
|
||||
using TwitchChatTTS.OBS.Socket.Handlers;
|
||||
using TwitchChatTTS.Seven.Socket.Handlers;
|
||||
using TwitchChatTTS.Seven.Socket.Context;
|
||||
using TwitchLib.Client.Interfaces;
|
||||
using TwitchLib.Client;
|
||||
using TwitchLib.PubSub.Interfaces;
|
||||
@ -31,6 +30,7 @@ using TwitchChatTTS.Chat.Groups.Permissions;
|
||||
using TwitchChatTTS.Chat.Groups;
|
||||
using TwitchChatTTS.Chat.Emotes;
|
||||
using HermesSocketLibrary.Requests.Callbacks;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
|
||||
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
|
||||
@ -70,21 +70,16 @@ s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
|
||||
});
|
||||
|
||||
// Command parameters
|
||||
s.AddKeyedSingleton<ChatCommandParameter, TTSVoiceNameParameter>("parameter-ttsvoicename");
|
||||
s.AddKeyedSingleton<ChatCommandParameter, UnvalidatedParameter>("parameter-unvalidated");
|
||||
s.AddKeyedSingleton<ChatCommandParameter, SimpleListedParameter>("parameter-simplelisted");
|
||||
s.AddKeyedSingleton<ChatCommand, SkipAllCommand>("command-skipall");
|
||||
s.AddKeyedSingleton<ChatCommand, SkipCommand>("command-skip");
|
||||
s.AddKeyedSingleton<ChatCommand, VoiceCommand>("command-voice");
|
||||
s.AddKeyedSingleton<ChatCommand, AddTTSVoiceCommand>("command-addttsvoice");
|
||||
s.AddKeyedSingleton<ChatCommand, RemoveTTSVoiceCommand>("command-removettsvoice");
|
||||
s.AddKeyedSingleton<ChatCommand, RefreshTTSDataCommand>("command-refreshttsdata");
|
||||
s.AddKeyedSingleton<ChatCommand, OBSCommand>("command-obs");
|
||||
s.AddKeyedSingleton<ChatCommand, TTSCommand>("command-tts");
|
||||
s.AddKeyedSingleton<ChatCommand, VersionCommand>("command-version");
|
||||
s.AddSingleton<IChatCommand, SkipCommand>();
|
||||
s.AddSingleton<IChatCommand, VoiceCommand>();
|
||||
s.AddSingleton<IChatCommand, RefreshCommand>();
|
||||
s.AddSingleton<IChatCommand, OBSCommand>();
|
||||
s.AddSingleton<IChatCommand, TTSCommand>();
|
||||
s.AddSingleton<IChatCommand, VersionCommand>();
|
||||
s.AddSingleton<ICommandBuilder, CommandBuilder>();
|
||||
s.AddSingleton<IChatterGroupManager, ChatterGroupManager>();
|
||||
s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>();
|
||||
s.AddSingleton<ChatCommandManager>();
|
||||
s.AddSingleton<CommandManager>();
|
||||
|
||||
s.AddSingleton<TTSPlayer>();
|
||||
s.AddSingleton<ChatMessageHandler>();
|
||||
@ -100,48 +95,32 @@ s.AddSingleton<SevenApiClient>();
|
||||
s.AddSingleton<IEmoteDatabase, EmoteDatabase>();
|
||||
|
||||
// OBS websocket
|
||||
s.AddSingleton<OBSManager>();
|
||||
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs-requestresponse");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, RequestBatchResponseHandler>("obs-requestbatchresponse");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, EventMessageHandler>("obs-eventmessage");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, RequestBatchResponseHandler>("obs");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, EventMessageHandler>("obs");
|
||||
|
||||
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, OBSHandlerManager>("obs");
|
||||
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, OBSHandlerTypeManager>("obs");
|
||||
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, OBSMessageTypeManager>("obs");
|
||||
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, OBSSocketClient>("obs");
|
||||
|
||||
// 7tv websocket
|
||||
s.AddTransient(sp =>
|
||||
{
|
||||
var logger = sp.GetRequiredService<ILogger>();
|
||||
var client = sp.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv") as SevenSocketClient;
|
||||
if (client == null)
|
||||
return new ReconnectContext() { SessionId = null };
|
||||
if (client.ConnectionDetails == null)
|
||||
return new ReconnectContext() { SessionId = null };
|
||||
return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId };
|
||||
});
|
||||
s.AddKeyedSingleton<IWebSocketHandler, SevenHelloHandler>("7tv-sevenhello");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("7tv-hello");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, DispatchHandler>("7tv-dispatch");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, ReconnectHandler>("7tv-reconnect");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, ErrorHandler>("7tv-error");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, EndOfStreamHandler>("7tv-endofstream");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, SevenHelloHandler>("7tv");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, DispatchHandler>("7tv");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, ReconnectHandler>("7tv");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, ErrorHandler>("7tv");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, EndOfStreamHandler>("7tv");
|
||||
|
||||
s.AddSingleton<SevenManager>();
|
||||
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, SevenHandlerManager>("7tv");
|
||||
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, SevenHandlerTypeManager>("7tv");
|
||||
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, SevenMessageTypeManager>("7tv");
|
||||
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
|
||||
|
||||
// hermes websocket
|
||||
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes-heartbeat");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes-loginack");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes-requestack");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes-error");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes");
|
||||
//s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
|
||||
|
||||
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, HermesHandlerManager>("hermes");
|
||||
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, HermesHandlerTypeManager>("hermes");
|
||||
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes");
|
||||
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");
|
||||
|
||||
s.AddHostedService<TTS>();
|
||||
|
109
TTS.cs
109
TTS.cs
@ -5,33 +5,30 @@ using Microsoft.Extensions.Hosting;
|
||||
using Serilog;
|
||||
using NAudio.Wave.SampleProviders;
|
||||
using TwitchLib.Client.Events;
|
||||
using TwitchChatTTS.Twitch.Redemptions;
|
||||
using org.mariuszgromada.math.mxparser;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
||||
using TwitchChatTTS.Chat.Groups;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
using TwitchChatTTS.Seven.Socket;
|
||||
using TwitchChatTTS.Chat.Emotes;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
|
||||
namespace TwitchChatTTS
|
||||
{
|
||||
public class TTS : IHostedService
|
||||
{
|
||||
public const int MAJOR_VERSION = 3;
|
||||
public const int MINOR_VERSION = 9;
|
||||
public const int MINOR_VERSION = 10;
|
||||
|
||||
private readonly User _user;
|
||||
private readonly HermesApiClient _hermesApiClient;
|
||||
private readonly SevenApiClient _sevenApiClient;
|
||||
private readonly OBSManager _obsManager;
|
||||
private readonly SevenManager _sevenManager;
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly SevenSocketClient _seven;
|
||||
private readonly HermesSocketClient _hermes;
|
||||
private readonly RedemptionManager _redemptionManager;
|
||||
private readonly IChatterGroupManager _chatterGroupManager;
|
||||
private readonly IGroupPermissionManager _permissionManager;
|
||||
private readonly IEmoteDatabase _emotes;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
@ -41,12 +38,10 @@ namespace TwitchChatTTS
|
||||
User user,
|
||||
HermesApiClient hermesApiClient,
|
||||
SevenApiClient sevenApiClient,
|
||||
OBSManager obsManager,
|
||||
SevenManager sevenManager,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||
RedemptionManager redemptionManager,
|
||||
IChatterGroupManager chatterGroupManager,
|
||||
IGroupPermissionManager permissionManager,
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
[FromKeyedServices("7tv")] SocketClient<WebSocketMessage> seven,
|
||||
IEmoteDatabase emotes,
|
||||
Configuration configuration,
|
||||
TTSPlayer player,
|
||||
IServiceProvider serviceProvider,
|
||||
@ -56,12 +51,10 @@ namespace TwitchChatTTS
|
||||
_user = user;
|
||||
_hermesApiClient = hermesApiClient;
|
||||
_sevenApiClient = sevenApiClient;
|
||||
_obsManager = obsManager;
|
||||
_sevenManager = sevenManager;
|
||||
_hermes = (hermes as HermesSocketClient)!;
|
||||
_redemptionManager = redemptionManager;
|
||||
_chatterGroupManager = chatterGroupManager;
|
||||
_permissionManager = permissionManager;
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_seven = (seven as SevenSocketClient)!;
|
||||
_emotes = emotes;
|
||||
_configuration = configuration;
|
||||
_player = player;
|
||||
_serviceProvider = serviceProvider;
|
||||
@ -119,14 +112,6 @@ namespace TwitchChatTTS
|
||||
await InitializeSevenTv();
|
||||
await InitializeObs();
|
||||
|
||||
// _logger.Information("Sending a request to server...");
|
||||
// await _hermesManager.Send(3, new RequestMessage() {
|
||||
// Type = "get_redeemable_actions",
|
||||
// Data = new Dictionary<string, object>()
|
||||
// });
|
||||
// _logger.Warning("OS VERSION: " + Environment.OSVersion + " | " + Environment.OSVersion.Platform);
|
||||
// return;
|
||||
|
||||
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
|
||||
{
|
||||
if (e.SampleProvider == _player.Playing)
|
||||
@ -239,65 +224,8 @@ namespace TwitchChatTTS
|
||||
user.TwitchUsername = hermesAccount.Username;
|
||||
|
||||
var twitchBotToken = await hermes.FetchTwitchBotToken();
|
||||
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId);
|
||||
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId!);
|
||||
_logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]");
|
||||
|
||||
// user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
|
||||
// _logger.Information("TTS Default Voice: " + user.DefaultTTSVoice);
|
||||
|
||||
// var wordFilters = await hermes.FetchTTSWordFilters();
|
||||
// user.RegexFilters = wordFilters.ToList();
|
||||
// _logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
|
||||
|
||||
var voicesSelected = await hermes.FetchTTSChatterSelectedVoices();
|
||||
user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice);
|
||||
_logger.Information($"{user.VoicesSelected.Count} chatters have selected a specific TTS voice, among {user.VoicesSelected.Values.Distinct().Count()} distinct TTS voices.");
|
||||
|
||||
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
|
||||
if (voicesEnabled == null || !voicesEnabled.Any())
|
||||
user.VoicesEnabled = new HashSet<string>([user.DefaultTTSVoice]);
|
||||
else
|
||||
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
|
||||
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
|
||||
|
||||
var defaultedChatters = voicesSelected.Where(item => item.Voice == null || !user.VoicesEnabled.Contains(item.Voice));
|
||||
if (defaultedChatters.Any())
|
||||
_logger.Information($"{defaultedChatters.Count()} chatter(s) will have their TTS voice set to default due to having selected a disabled TTS voice.");
|
||||
|
||||
// var redemptionActions = await hermes.FetchRedeemableActions();
|
||||
// var redemptions = await hermes.FetchRedemptions();
|
||||
// _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a));
|
||||
// _logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
|
||||
|
||||
var groups = await hermes.FetchGroups();
|
||||
var groupsById = groups.ToDictionary(g => g.Id, g => g);
|
||||
foreach (var group in groups)
|
||||
_chatterGroupManager.Add(group);
|
||||
_logger.Information($"{groups.Count()} groups have been loaded.");
|
||||
|
||||
var groupChatters = await hermes.FetchGroupChatters();
|
||||
_logger.Debug($"{groupChatters.Count()} group users have been fetched.");
|
||||
|
||||
var permissions = await hermes.FetchGroupPermissions();
|
||||
foreach (var permission in permissions)
|
||||
{
|
||||
_logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
|
||||
if (!groupsById.TryGetValue(permission.GroupId, out var group))
|
||||
{
|
||||
_logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = $"{group.Name}.{permission.Path}";
|
||||
_permissionManager.Set(path, permission.Allow);
|
||||
_logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
|
||||
}
|
||||
_logger.Information($"{permissions.Count()} group permissions have been loaded.");
|
||||
|
||||
foreach (var chatter in groupChatters)
|
||||
if (groupsById.TryGetValue(chatter.GroupId, out var group))
|
||||
_chatterGroupManager.Add(chatter.ChatterId, group.Name);
|
||||
_logger.Information($"Users in each group have been loaded.");
|
||||
}
|
||||
|
||||
private async Task InitializeHermesWebsocket()
|
||||
@ -317,8 +245,8 @@ namespace TwitchChatTTS
|
||||
{
|
||||
try
|
||||
{
|
||||
_sevenManager.Initialize();
|
||||
await _sevenManager.Connect();
|
||||
_seven.Initialize();
|
||||
await _seven.Connect();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -330,8 +258,8 @@ namespace TwitchChatTTS
|
||||
{
|
||||
try
|
||||
{
|
||||
_obsManager.Initialize();
|
||||
await _obsManager.Connect();
|
||||
_obs.Initialize();
|
||||
await _obs.Connect();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -376,20 +304,19 @@ namespace TwitchChatTTS
|
||||
|
||||
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes)
|
||||
{
|
||||
var emotes = _serviceProvider.GetRequiredService<IEmoteDatabase>();
|
||||
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();
|
||||
|
||||
if (channelEmotes != null && channelEmotes.Emotes.Any())
|
||||
{
|
||||
_logger.Information($"Loaded {channelEmotes.Emotes.Count()} 7tv channel emotes.");
|
||||
foreach (var entry in channelEmotes.Emotes)
|
||||
emotes.Add(entry.Name, entry.Id);
|
||||
_emotes.Add(entry.Name, entry.Id);
|
||||
}
|
||||
if (globalEmotes != null && globalEmotes.Any())
|
||||
{
|
||||
_logger.Information($"Loaded {globalEmotes.Count()} 7tv global emotes.");
|
||||
foreach (var entry in globalEmotes)
|
||||
emotes.Add(entry.Name, entry.Id);
|
||||
_emotes.Add(entry.Name, entry.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using org.mariuszgromada.math.mxparser;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Redemptions
|
||||
{
|
||||
@ -15,7 +15,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
||||
{
|
||||
private readonly IDictionary<string, IList<RedeemableAction>> _store;
|
||||
private readonly User _user;
|
||||
private readonly OBSManager _obsManager;
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly HermesSocketClient _hermes;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Random _random;
|
||||
@ -24,13 +24,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
||||
|
||||
public RedemptionManager(
|
||||
User user,
|
||||
OBSManager obsManager,
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||
ILogger logger)
|
||||
{
|
||||
_store = new Dictionary<string, IList<RedeemableAction>>();
|
||||
_user = user;
|
||||
_obsManager = obsManager;
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_hermes = (hermes as HermesSocketClient)!;
|
||||
_logger = logger;
|
||||
_random = new Random();
|
||||
@ -72,7 +72,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
||||
break;
|
||||
case "OBS_TRANSFORM":
|
||||
var type = typeof(OBSTransformationData);
|
||||
await _obsManager.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) =>
|
||||
await _obs.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) =>
|
||||
{
|
||||
string[] properties = ["rotation", "position_x", "position_y"];
|
||||
foreach (var property in properties)
|
||||
@ -111,13 +111,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
||||
});
|
||||
break;
|
||||
case "TOGGLE_OBS_VISIBILITY":
|
||||
await _obsManager.ToggleSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"]);
|
||||
await _obs.ToggleSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"]);
|
||||
break;
|
||||
case "SPECIFIC_OBS_VISIBILITY":
|
||||
await _obsManager.UpdateSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"], action.Data["obs_visible"].ToLower() == "true");
|
||||
await _obs.UpdateSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"], action.Data["obs_visible"].ToLower() == "true");
|
||||
break;
|
||||
case "SPECIFIC_OBS_INDEX":
|
||||
await _obsManager.UpdateSceneItemIndex(action.Data["scene_name"], action.Data["scene_item_name"], int.Parse(action.Data["obs_index"]));
|
||||
await _obs.UpdateSceneItemIndex(action.Data["scene_name"], action.Data["scene_item_name"], int.Parse(action.Data["obs_index"]));
|
||||
break;
|
||||
case "SLEEP":
|
||||
_logger.Debug("Sleeping on thread due to redemption for OBS.");
|
||||
|
Loading…
Reference in New Issue
Block a user