Added hermes websocket support. Added chat command support. Added selectable voice command via websocket. Added websocket heartbeat management.

This commit is contained in:
Tom
2024-03-15 12:27:35 +00:00
parent b5cc6b5706
commit d4004d6230
53 changed files with 1227 additions and 461 deletions

View File

@ -0,0 +1,54 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class AddTTSVoiceCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<AddTTSVoiceCommand> _logger;
public AddTTSVoiceCommand(
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
IServiceProvider serviceProvider,
ILogger<AddTTSVoiceCommand> logger
) : base("addttsvoice", "Select a TTS voice as the default for that user.") {
_serviceProvider = serviceProvider;
_logger = logger;
AddParameter(ttsVoiceParameter);
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566";
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
if (client == null)
return;
var context = _serviceProvider.GetRequiredService<User>();
if (context == null || context.VoicesAvailable == null)
return;
var voiceName = args.First();
var voiceNameLower = voiceName.ToLower();
var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
if (exists)
return;
await client.Send(3, new RequestMessage() {
Type = "create_tts_voice",
Data = new Dictionary<string, string>() { { "@voice", voiceName } }
});
_logger.LogInformation($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
}
}
}

View File

@ -0,0 +1,27 @@
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public abstract class ChatCommand
{
public string Name { get; }
public string Description { get; }
public IList<ChatCommandParameter> Parameters { get => _parameters.AsReadOnly(); }
private IList<ChatCommandParameter> _parameters;
public ChatCommand(string name, string description) {
Name = name;
Description = description;
_parameters = new List<ChatCommandParameter>();
}
protected void AddParameter(ChatCommandParameter parameter) {
if (parameter != null)
_parameters.Add(parameter);
}
public abstract Task<bool> CheckPermissions(ChatMessage message, long broadcasterId);
public abstract Task Execute(IList<string> args, ChatMessage message, long broadcasterId);
}
}

View File

@ -0,0 +1,100 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class ChatCommandManager
{
private IDictionary<string, ChatCommand> _commands;
private TwitchBotToken _token;
private IServiceProvider _serviceProvider;
private ILogger<ChatCommandManager> _logger;
private string CommandStartSign { get; } = "!";
public ChatCommandManager(TwitchBotToken token, IServiceProvider serviceProvider, ILogger<ChatCommandManager> logger) {
_token = token;
_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.LogError("Failed to add command: " + type.AssemblyQualifiedName);
continue;
}
_logger.LogDebug($"Added command {type.AssemblyQualifiedName}.");
Add(command);
}
}
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message) {
if (_token.BroadcasterId == null)
return ChatCommandResult.Unknown;
if (string.IsNullOrWhiteSpace(arg))
return ChatCommandResult.Unknown;
arg = arg.Trim();
if (!arg.StartsWith(CommandStartSign))
return ChatCommandResult.Unknown;
string[] parts = arg.Split(" ");
string com = parts.First().Substring(CommandStartSign.Length).ToLower();
string[] args = parts.Skip(1).ToArray();
long broadcasterId = long.Parse(_token.BroadcasterId);
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null) {
_logger.LogDebug($"Failed to find command named '{com}'.");
return ChatCommandResult.Missing;
}
if (!await command.CheckPermissions(message, broadcasterId)) {
_logger.LogWarning($"Chatter is missing permission to execute command named '{com}'.");
return ChatCommandResult.Permission;
}
if (command.Parameters.Count(p => !p.Optional) > args.Length) {
_logger.LogWarning($"Command syntax issue when executing command named '{com}' with the following args: {string.Join(" ", args)}");
return ChatCommandResult.Syntax;
}
for (int i = 0; i < Math.Min(args.Length, command.Parameters.Count); i++) {
if (!command.Parameters[i].Validate(args[i])) {
_logger.LogWarning($"Commmand '{com}' failed because of the #{i + 1} argument. Invalid value: {args[i]}");
return ChatCommandResult.Syntax;
}
}
try {
await command.Execute(args, message, broadcasterId);
} catch (Exception e) {
_logger.LogError(e, $"Command '{arg}' failed.");
return ChatCommandResult.Fail;
}
_logger.LogInformation($"Execute the {com} command with the following args: " + string.Join(" ", args));
return ChatCommandResult.Success;
}
}
}

View File

@ -0,0 +1,12 @@
namespace TwitchChatTTS.Chat.Commands
{
public enum ChatCommandResult
{
Unknown = 0,
Missing = 1,
Success = 2,
Permission = 3,
Syntax = 4,
Fail = 5
}
}

View File

@ -0,0 +1,17 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public abstract class ChatCommandParameter
{
public string Name { get; }
public string Description { get; }
public bool Optional { get; }
public ChatCommandParameter(string name, string description, bool optional = false) {
Name = name;
Description = description;
Optional = optional;
}
public abstract bool Validate(string value);
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.Extensions.DependencyInjection;
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class TTSVoiceNameParameter : ChatCommandParameter
{
private IServiceProvider _serviceProvider;
public TTSVoiceNameParameter(IServiceProvider serviceProvider, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
{
_serviceProvider = serviceProvider;
}
public override bool Validate(string value)
{
var user = _serviceProvider.GetRequiredService<User>();
if (user.VoicesAvailable == null)
return false;
value = value.ToLower();
return user.VoicesAvailable.Any(e => e.Value.ToLower() == value);
}
}
}

View File

@ -0,0 +1,14 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class UnvalidatedParameter : ChatCommandParameter
{
public UnvalidatedParameter(bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
{
}
public override bool Validate(string value)
{
return true;
}
}
}

View File

@ -0,0 +1,54 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class RemoveTTSVoiceCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<RemoveTTSVoiceCommand> _logger;
public RemoveTTSVoiceCommand(
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
IServiceProvider serviceProvider,
ILogger<RemoveTTSVoiceCommand> logger
) : base("removettsvoice", "Select a TTS voice as the default for that user.") {
_serviceProvider = serviceProvider;
_logger = logger;
AddParameter(ttsVoiceParameter);
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566";
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
if (client == null)
return;
var context = _serviceProvider.GetRequiredService<User>();
if (context == null || context.VoicesAvailable == null)
return;
var voiceName = args.First().ToLower();
var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName);
if (!exists)
return;
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
await client.Send(3, new RequestMessage() {
Type = "delete_tts_voice",
Data = new Dictionary<string, string>() { { "@voice", voiceId } }
});
_logger.LogInformation($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
}
}
}

View File

@ -0,0 +1,37 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class SkipAllCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<SkipAllCommand> _logger;
public SkipAllCommand(IServiceProvider serviceProvider, ILogger<SkipAllCommand> logger)
: base("skipall", "Skips all text to speech messages in queue and playing.") {
_serviceProvider = serviceProvider;
_logger = logger;
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsVip || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var player = _serviceProvider.GetRequiredService<TTSPlayer>();
player.RemoveAll();
if (player.Playing == null)
return;
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
player.Playing = null;
_logger.LogInformation("Skipped all queued and playing tts.");
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class SkipCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<SkipCommand> _logger;
public SkipCommand(IServiceProvider serviceProvider, ILogger<SkipCommand> logger)
: base("skip", "Skips the current text to speech message.") {
_serviceProvider = serviceProvider;
_logger = logger;
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsVip || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var player = _serviceProvider.GetRequiredService<TTSPlayer>();
if (player.Playing == null)
return;
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
player.Playing = null;
_logger.LogInformation("Skipped current tts.");
}
}
}

View File

@ -0,0 +1,60 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class VoiceCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<VoiceCommand> _logger;
public VoiceCommand(
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
IServiceProvider serviceProvider,
ILogger<VoiceCommand> logger
) : base("voice", "Select a TTS voice as the default for that user.") {
_serviceProvider = serviceProvider;
_logger = logger;
AddParameter(ttsVoiceParameter);
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100 || message.UserId == "126224566";
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
if (client == null)
return;
var context = _serviceProvider.GetRequiredService<User>();
if (context == null || context.VoicesSelected == null || context.VoicesAvailable == null)
return;
long chatterId = long.Parse(message.UserId);
var voiceName = args.First().ToLower();
var voice = context.VoicesAvailable.First(v => v.Value.ToLower() == voiceName);
if (context.VoicesSelected.ContainsKey(chatterId)) {
await client.Send(3, new RequestMessage() {
Type = "update_tts_user",
Data = new Dictionary<string, string>() { { "@user", message.UserId }, { "@broadcaster", broadcasterId.ToString() }, { "@voice", voice.Key } }
});
_logger.LogInformation($"Updated {message.Username}'s (id: {message.UserId}) tts voice to {voice.Value} (id: {voice.Key}).");
} else {
await client.Send(3, new RequestMessage() {
Type = "create_tts_user",
Data = new Dictionary<string, string>() { { "@user", message.UserId }, { "@broadcaster", broadcasterId.ToString() }, { "@voice", voice.Key } }
});
_logger.LogInformation($"Added {message.Username}'s (id: {message.UserId}) tts voice as {voice.Value} (id: {voice.Key}).");
}
}
}
}