Using Serilog. Added partial OBS batch request support. Added update checking. Added more commands. Added enabled/disabled TTS voices. And more.

This commit is contained in:
Tom
2024-06-17 00:19:31 +00:00
parent d4004d6230
commit 706cd06930
67 changed files with 1933 additions and 925 deletions

View File

@ -2,7 +2,7 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
@ -11,13 +11,14 @@ namespace TwitchChatTTS.Chat.Commands
public class AddTTSVoiceCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<AddTTSVoiceCommand> _logger;
private ILogger _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.") {
ILogger logger
) : base("addttsvoice", "Select a TTS voice as the default for that user.")
{
_serviceProvider = serviceProvider;
_logger = logger;
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Chat.Commands
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566";
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
@ -43,12 +44,13 @@ namespace TwitchChatTTS.Chat.Commands
var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
if (exists)
return;
await client.Send(3, new RequestMessage() {
await client.Send(3, new RequestMessage()
{
Type = "create_tts_voice",
Data = new Dictionary<string, string>() { { "@voice", voiceName } }
Data = new Dictionary<string, object>() { { "voice", voiceName } }
});
_logger.LogInformation($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
}
}
}

View File

@ -10,15 +10,18 @@ namespace TwitchChatTTS.Chat.Commands
public IList<ChatCommandParameter> Parameters { get => _parameters.AsReadOnly(); }
private IList<ChatCommandParameter> _parameters;
public ChatCommand(string name, string description) {
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);
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> CheckPermissions(ChatMessage message, long broadcasterId);

View File

@ -1,5 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
@ -7,13 +7,14 @@ namespace TwitchChatTTS.Chat.Commands
public class ChatCommandManager
{
private IDictionary<string, ChatCommand> _commands;
private TwitchBotToken _token;
private TwitchBotAuth _token;
private IServiceProvider _serviceProvider;
private ILogger<ChatCommandManager> _logger;
private ILogger _logger;
private string CommandStartSign { get; } = "!";
public ChatCommandManager(TwitchBotToken token, IServiceProvider serviceProvider, ILogger<ChatCommandManager> logger) {
public ChatCommandManager(TwitchBotAuth token, IServiceProvider serviceProvider, ILogger logger)
{
_token = token;
_serviceProvider = serviceProvider;
_logger = logger;
@ -22,33 +23,38 @@ namespace TwitchChatTTS.Chat.Commands
GenerateCommands();
}
private void Add(ChatCommand command) {
private void Add(ChatCommand command)
{
_commands.Add(command.Name.ToLower(), command);
}
private void GenerateCommands() {
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) {
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);
if (command == null)
{
_logger.Error("Failed to add command: " + type.AssemblyQualifiedName);
continue;
}
_logger.LogDebug($"Added command {type.AssemblyQualifiedName}.");
_logger.Debug($"Added command {type.AssemblyQualifiedName}.");
Add(command);
}
}
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message) {
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message)
{
if (_token.BroadcasterId == null)
return ChatCommandResult.Unknown;
if (string.IsNullOrWhiteSpace(arg))
@ -64,36 +70,44 @@ namespace TwitchChatTTS.Chat.Commands
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}'.");
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null)
{
_logger.Debug($"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}'.");
if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != "126224566" && !message.IsStaff)
{
_logger.Warning($"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)}");
if (command.Parameters.Count(p => !p.Optional) > args.Length)
{
_logger.Warning($"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]}");
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 {
try
{
await command.Execute(args, message, broadcasterId);
} catch (Exception e) {
_logger.LogError(e, $"Command '{arg}' failed.");
}
catch (Exception e)
{
_logger.Error(e, $"Command '{arg}' failed.");
return ChatCommandResult.Fail;
}
_logger.LogInformation($"Execute the {com} command with the following args: " + string.Join(" ", args));
_logger.Information($"Executed the {com} command with the following args: " + string.Join(" ", args));
return ChatCommandResult.Success;
}
}

View File

@ -0,0 +1,81 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class OBSCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger _logger;
public OBSCommand(
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
IServiceProvider serviceProvider,
ILogger logger
) : base("obs", "Various obs commands.")
{
_serviceProvider = serviceProvider;
_logger = logger;
AddParameter(unvalidatedParameter);
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
if (client == null)
return;
var context = _serviceProvider.GetRequiredService<User>();
if (context == null || context.VoicesAvailable == null)
return;
var voiceName = args[0].ToLower();
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
var action = args[1].ToLower();
switch (action) {
case "sleep":
await client.Send(8, new RequestMessage()
{
Type = "Sleep",
Data = new Dictionary<string, object>() { { "requestId", "siduhsidasd" }, { "sleepMillis", 10000 } }
});
break;
case "get_scene_item_id":
await client.Send(6, new RequestMessage()
{
Type = "GetSceneItemId",
Data = new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" }, { "rotation", 90 } }
});
break;
case "transform":
await client.Send(6, new RequestMessage()
{
Type = "Transform",
Data = new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sceneItemId", 90 }, { "rotation", 90 } }
});
break;
case "remove":
await client.Send(3, new RequestMessage()
{
Type = "delete_tts_voice",
Data = new Dictionary<string, object>() { { "voice", voiceId } }
});
break;
}
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
}
}
}

View File

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

View File

@ -0,0 +1,61 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class RefreshTTSDataCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger _logger;
public RefreshTTSDataCommand(IServiceProvider serviceProvider, ILogger logger)
: base("refresh", "Refreshes certain TTS related data on the client.")
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var user = _serviceProvider.GetRequiredService<User>();
var service = args.FirstOrDefault();
if (service == null)
return;
var hermes = _serviceProvider.GetRequiredService<HermesApiClient>();
switch (service)
{
case "tts_voice_enabled":
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
if (voicesEnabled == null || !voicesEnabled.Any())
user.VoicesEnabled = new HashSet<string>(new 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 hermes.FetchTTSWordFilters();
user.RegexFilters = wordFilters.ToList();
_logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
break;
case "username_filters":
var usernameFilters = await hermes.FetchTTSUsernameFilters();
user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e);
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked.");
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
break;
case "default_voice":
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
_logger.Information("Default Voice: " + user.DefaultTTSVoice);
break;
}
}
}
}

View File

@ -2,7 +2,7 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
@ -11,13 +11,14 @@ namespace TwitchChatTTS.Chat.Commands
public class RemoveTTSVoiceCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<RemoveTTSVoiceCommand> _logger;
private ILogger _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.") {
ILogger logger
) : base("removettsvoice", "Select a TTS voice as the default for that user.")
{
_serviceProvider = serviceProvider;
_logger = logger;
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Chat.Commands
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566";
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
@ -42,13 +43,14 @@ namespace TwitchChatTTS.Chat.Commands
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() {
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 } }
Data = new Dictionary<string, object>() { { "voice", voiceId } }
});
_logger.LogInformation($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
_logger.Information($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
}
}
}

View File

@ -1,5 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
@ -7,10 +7,11 @@ namespace TwitchChatTTS.Chat.Commands
public class SkipAllCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<SkipAllCommand> _logger;
private ILogger _logger;
public SkipAllCommand(IServiceProvider serviceProvider, ILogger<SkipAllCommand> logger)
: base("skipall", "Skips all text to speech messages in queue and playing.") {
public SkipAllCommand(IServiceProvider serviceProvider, ILogger logger)
: base("skipall", "Skips all text to speech messages in queue and playing.")
{
_serviceProvider = serviceProvider;
_logger = logger;
}
@ -27,11 +28,11 @@ namespace TwitchChatTTS.Chat.Commands
if (player.Playing == null)
return;
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
player.Playing = null;
_logger.LogInformation("Skipped all queued and playing tts.");
_logger.Information("Skipped all queued and playing tts.");
}
}
}

View File

@ -1,5 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
@ -7,10 +7,11 @@ namespace TwitchChatTTS.Chat.Commands
public class SkipCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<SkipCommand> _logger;
private ILogger _logger;
public SkipCommand(IServiceProvider serviceProvider, ILogger<SkipCommand> logger)
: base("skip", "Skips the current text to speech message.") {
public SkipCommand(IServiceProvider serviceProvider, ILogger logger)
: base("skip", "Skips the current text to speech message.")
{
_serviceProvider = serviceProvider;
_logger = logger;
}
@ -25,11 +26,11 @@ namespace TwitchChatTTS.Chat.Commands
var player = _serviceProvider.GetRequiredService<TTSPlayer>();
if (player.Playing == null)
return;
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
player.Playing = null;
_logger.LogInformation("Skipped current tts.");
_logger.Information("Skipped current tts.");
}
}
}

View File

@ -0,0 +1,76 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class TTSCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger _logger;
public TTSCommand(
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
IServiceProvider serviceProvider,
ILogger logger
) : base("tts", "Various tts commands.")
{
_serviceProvider = serviceProvider;
_logger = logger;
AddParameter(ttsVoiceParameter);
AddParameter(unvalidatedParameter);
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster;
}
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[0].ToLower();
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
var action = args[1].ToLower();
switch (action) {
case "enable":
await client.Send(3, new RequestMessage()
{
Type = "update_tts_voice_state",
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", true } }
});
break;
case "disable":
await client.Send(3, new RequestMessage()
{
Type = "update_tts_voice_state",
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", false } }
});
break;
case "remove":
await client.Send(3, new RequestMessage()
{
Type = "delete_tts_voice",
Data = new Dictionary<string, object>() { { "voice", voiceId } }
});
break;
}
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
}
}
}

View File

@ -0,0 +1,26 @@
using Serilog;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class VersionCommand : ChatCommand
{
private ILogger _logger;
public VersionCommand(ILogger logger)
: base("version", "Does nothing.")
{
_logger = logger;
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
_logger.Information($"Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
}
}
}

View File

@ -2,7 +2,7 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
@ -11,13 +11,14 @@ namespace TwitchChatTTS.Chat.Commands
public class VoiceCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger<VoiceCommand> _logger;
private ILogger _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.") {
ILogger logger
) : base("voice", "Select a TTS voice as the default for that user.")
{
_serviceProvider = serviceProvider;
_logger = logger;
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Chat.Commands
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100 || message.UserId == "126224566";
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
@ -42,19 +43,12 @@ namespace TwitchChatTTS.Chat.Commands
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}).");
}
await client.Send(3, new RequestMessage()
{
Type = context.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user",
Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voice.Key } }
});
_logger.Information($"Updated {message.Username}'s [id: {chatterId}] tts voice to {voice.Value} (id: {voice.Key}).");
}
}
}