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

@ -3,94 +3,99 @@ using TwitchLib.Client.Events;
using TwitchChatTTS.OBS.Socket;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.Logging;
using Serilog;
using Microsoft.Extensions.DependencyInjection;
using TwitchChatTTS;
using TwitchChatTTS.Seven;
using TwitchChatTTS.Chat.Commands;
using TwitchChatTTS.Hermes.Socket;
using HermesSocketLibrary.Socket.Data;
public class ChatMessageHandler {
private ILogger<ChatMessageHandler> _logger { get; }
public class ChatMessageHandler
{
private ILogger _logger { get; }
private Configuration _configuration { get; }
public EmoteCounter _emoteCounter { get; }
private EmoteDatabase _emotes { get; }
private TTSPlayer _player { get; }
private ChatCommandManager _commands { get; }
private OBSSocketClient? _obsClient { get; }
private HermesSocketClient? _hermesClient { get; }
private IServiceProvider _serviceProvider { get; }
private Regex sfxRegex;
private HashSet<long> _chatters;
public HashSet<long> Chatters { get => _chatters; set => _chatters = value; }
public ChatMessageHandler(
ILogger<ChatMessageHandler> logger,
Configuration configuration,
EmoteCounter emoteCounter,
EmoteDatabase emotes,
TTSPlayer player,
ChatCommandManager commands,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> client,
IServiceProvider serviceProvider
) {
_logger = logger;
_configuration = configuration;
_emoteCounter = emoteCounter;
_emotes = emotes;
EmoteDatabase emotes,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obsClient,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
Configuration configuration,
IServiceProvider serviceProvider,
ILogger logger
)
{
_player = player;
_commands = commands;
_obsClient = client as OBSSocketClient;
_emotes = emotes;
_obsClient = obsClient as OBSSocketClient;
_hermesClient = hermesClient as HermesSocketClient;
_configuration = configuration;
_serviceProvider = serviceProvider;
_logger = logger;
_chatters = null;
sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
}
public async Task<MessageResult> Handle(OnMessageReceivedArgs e) {
if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient?.Live == false)
return MessageResult.Blocked;
public async Task<MessageResult> Handle(OnMessageReceivedArgs e)
{
if (_obsClient == null || _hermesClient == null || _obsClient.Connected && _chatters == null)
return new MessageResult(MessageStatus.NotReady, -1, -1);
if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient.Live == false)
return new MessageResult(MessageStatus.NotReady, -1, -1);
var user = _serviceProvider.GetRequiredService<User>();
var m = e.ChatMessage;
var msg = e.ChatMessage.Message;
var chatterId = long.Parse(m.UserId);
var tasks = new List<Task>();
var blocked = user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted";
if (!blocked || m.IsBroadcaster) {
try {
if (!blocked || m.IsBroadcaster)
{
try
{
var commandResult = await _commands.Execute(msg, m);
if (commandResult != ChatCommandResult.Unknown) {
return MessageResult.Command;
}
} catch (Exception ex) {
_logger.LogError(ex, "Failed at executing command.");
if (commandResult != ChatCommandResult.Unknown)
return new MessageResult(MessageStatus.Command, -1, -1);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed at executing command.");
}
}
if (blocked) {
_logger.LogTrace($"Blocked message by {m.Username}: {msg}");
return MessageResult.Blocked;
if (blocked)
{
_logger.Debug($"Blocked message by {m.Username}: {msg}");
return new MessageResult(MessageStatus.Blocked, -1, -1);
}
// Replace filtered words.
if (user.RegexFilters != null) {
foreach (var wf in user.RegexFilters) {
if (wf.Search == null || wf.Replace == null)
continue;
if (wf.IsRegex) {
try {
var regex = new Regex(wf.Search);
msg = regex.Replace(msg, wf.Replace);
continue;
} catch (Exception) {
wf.IsRegex = false;
}
}
msg = msg.Replace(wf.Search, wf.Replace);
}
if (_obsClient.Connected && !_chatters.Contains(chatterId))
{
tasks.Add(_hermesClient.Send(6, new ChatterMessage()
{
Id = chatterId,
Name = m.Username
}));
_chatters.Add(chatterId);
}
// Filter highly repetitive words (like emotes) from the message.
@ -99,17 +104,30 @@ public class ChatMessageHandler {
var words = msg.Split(" ");
var wordCounter = new Dictionary<string, int>();
string filteredMsg = string.Empty;
foreach (var w in words) {
if (wordCounter.ContainsKey(w)) {
var newEmotes = new Dictionary<string, string>();
foreach (var w in words)
{
if (wordCounter.ContainsKey(w))
{
wordCounter[w]++;
} else {
}
else
{
wordCounter.Add(w, 1);
}
var emoteId = _emotes?.Get(w);
var emoteId = _emotes.Get(w);
if (emoteId == null)
{
emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id;
if (emoteId != null) {
if (emoteId != null)
{
newEmotes.Add(emoteId, w);
_emotes.Add(w, emoteId);
}
}
if (emoteId != null)
{
emotesUsed.Add(emoteId);
totalEmoteUsed++;
}
@ -117,55 +135,89 @@ public class ChatMessageHandler {
if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
filteredMsg += w + " ";
}
if (_obsClient.Connected && newEmotes.Any())
tasks.Add(_hermesClient.Send(7, new EmoteDetailsMessage()
{
Emotes = newEmotes
}));
msg = filteredMsg;
// Adding twitch emotes to the counter.
foreach (var emote in e.ChatMessage.EmoteSet.Emotes) {
_logger.LogTrace("Twitch emote name used: " + emote.Name);
emotesUsed.Add(emote.Id);
// Replace filtered words.
if (user.RegexFilters != null)
{
foreach (var wf in user.RegexFilters)
{
if (wf.Search == null || wf.Replace == null)
continue;
if (wf.IsRegex)
{
try
{
var regex = new Regex(wf.Search);
msg = regex.Replace(msg, wf.Replace);
continue;
}
catch (Exception)
{
wf.IsRegex = false;
}
}
msg = msg.Replace(wf.Search, wf.Replace);
}
}
if (long.TryParse(e.ChatMessage.UserId, out long userId))
_emoteCounter.Add(userId, emotesUsed);
if (emotesUsed.Any())
_logger.LogDebug("Emote counters for user #" + userId + ": " + string.Join(" | ", emotesUsed.Select(e => e + "=" + _emoteCounter.Get(userId, e))));
// Determine the priority of this message
int priority = 0;
if (m.IsStaff) {
if (m.IsStaff)
{
priority = int.MinValue;
} else if (filter?.Tag == "priority") {
}
else if (filter?.Tag == "priority")
{
priority = int.MinValue + 1;
} else if (m.IsModerator) {
}
else if (m.IsModerator)
{
priority = -100;
} else if (m.IsVip) {
}
else if (m.IsVip)
{
priority = -10;
} else if (m.IsPartner) {
}
else if (m.IsPartner)
{
priority = -5;
} else if (m.IsHighlighted) {
}
else if (m.IsHighlighted)
{
priority = -1;
}
priority = Math.Min(priority, -m.SubscribedMonthCount * (m.IsSubscriber ? 2 : 1));
// Determine voice selected.
string voiceSelected = user.DefaultTTSVoice;
if (user.VoicesSelected?.ContainsKey(userId) == true) {
if (long.TryParse(e.ChatMessage.UserId, out long userId) && user.VoicesSelected?.ContainsKey(userId) == true)
{
var voiceId = user.VoicesSelected[userId];
if (user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null) {
if (user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
{
voiceSelected = voiceName;
}
}
// Determine additional voices used
var voicesRegex = user.GenerateEnabledVoicesRegex();
var matches = voicesRegex?.Matches(msg).ToArray();
if (matches == null || matches.FirstOrDefault() == null || matches.FirstOrDefault().Index == 0) {
var matches = user.WordFilterRegex?.Matches(msg).ToArray();
if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
{
HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
return MessageResult.None;
return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed);
}
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.FirstOrDefault().Index).Trim(), e);
foreach (Match match in matches) {
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e);
foreach (Match match in matches)
{
var message = match.Groups[2].ToString();
if (string.IsNullOrWhiteSpace(message))
continue;
@ -175,21 +227,28 @@ public class ChatMessageHandler {
HandlePartialMessage(priority, voice, message.Trim(), e);
}
return MessageResult.None;
if (tasks.Any())
await Task.WhenAll(tasks);
return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed);
}
private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e) {
if (string.IsNullOrWhiteSpace(message)) {
private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e)
{
if (string.IsNullOrWhiteSpace(message))
{
return;
}
var m = e.ChatMessage;
var parts = sfxRegex.Split(message);
var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value));
if (parts.Length == 1) {
_logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage() {
if (parts.Length == 1)
{
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage()
{
Voice = voice,
Message = message,
Moderator = m.IsModerator,
@ -205,18 +264,22 @@ public class ChatMessageHandler {
var sfxMatches = sfxRegex.Matches(message);
var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length;
for (var i = 0; i < sfxMatches.Count; i++) {
for (var i = 0; i < sfxMatches.Count; i++)
{
var sfxMatch = sfxMatches[i];
var sfxName = sfxMatch.Groups[1]?.ToString()?.ToLower();
if (!File.Exists("sfx/" + sfxName + ".mp3")) {
if (!File.Exists("sfx/" + sfxName + ".mp3"))
{
parts[i * 2 + 2] = parts[i * 2] + " (" + parts[i * 2 + 1] + ")" + parts[i * 2 + 2];
continue;
}
if (!string.IsNullOrWhiteSpace(parts[i * 2])) {
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage() {
if (!string.IsNullOrWhiteSpace(parts[i * 2]))
{
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage()
{
Voice = voice,
Message = parts[i * 2],
Moderator = m.IsModerator,
@ -228,8 +291,9 @@ public class ChatMessageHandler {
});
}
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage() {
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage()
{
Voice = voice,
Message = sfxName,
File = $"sfx/{sfxName}.mp3",
@ -242,9 +306,11 @@ public class ChatMessageHandler {
});
}
if (!string.IsNullOrWhiteSpace(parts.Last())) {
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage() {
if (!string.IsNullOrWhiteSpace(parts.Last()))
{
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage()
{
Voice = voice,
Message = parts.Last(),
Moderator = m.IsModerator,

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}).");
}
}
}

View File

@ -1,4 +1,22 @@
public enum MessageResult {
public class MessageResult
{
public MessageStatus Status;
public long BroadcasterId;
public long ChatterId;
public HashSet<string> Emotes;
public MessageResult(MessageStatus status, long broadcasterId, long chatterId, HashSet<string>? emotes = null)
{
Status = status;
BroadcasterId = broadcasterId;
ChatterId = chatterId;
Emotes = emotes ?? new HashSet<string>();
}
}
public enum MessageStatus
{
None = 0,
NotReady = 1,
Blocked = 2,

View File

@ -14,7 +14,7 @@ public class AudioPlaybackEngine : IDisposable
{
SampleRate = sampleRate;
outputDevice = new WaveOutEvent();
mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channelCount));
mixer.ReadFully = true;
@ -49,25 +49,41 @@ public class AudioPlaybackEngine : IDisposable
AddMixerInput(new CachedWavProvider(sound));
}
public ISampleProvider ConvertSound(IWaveProvider provider) {
public ISampleProvider ConvertSound(IWaveProvider provider)
{
ISampleProvider? converted = null;
if (provider.WaveFormat.Encoding == WaveFormatEncoding.Pcm) {
if (provider.WaveFormat.BitsPerSample == 8) {
if (provider.WaveFormat.Encoding == WaveFormatEncoding.Pcm)
{
if (provider.WaveFormat.BitsPerSample == 8)
{
converted = new Pcm8BitToSampleProvider(provider);
} else if (provider.WaveFormat.BitsPerSample == 16) {
}
else if (provider.WaveFormat.BitsPerSample == 16)
{
converted = new Pcm16BitToSampleProvider(provider);
} else if (provider.WaveFormat.BitsPerSample == 24) {
}
else if (provider.WaveFormat.BitsPerSample == 24)
{
converted = new Pcm24BitToSampleProvider(provider);
} else if (provider.WaveFormat.BitsPerSample == 32) {
}
else if (provider.WaveFormat.BitsPerSample == 32)
{
converted = new Pcm32BitToSampleProvider(provider);
}
} else if (provider.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) {
if (provider.WaveFormat.BitsPerSample == 64) {
}
else if (provider.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat)
{
if (provider.WaveFormat.BitsPerSample == 64)
{
converted = new WaveToSampleProvider64(provider);
} else {
}
else
{
converted = new WaveToSampleProvider(provider);
}
} else {
}
else
{
throw new ArgumentException("Unsupported source encoding while adding to mixer.");
}
return ConvertToRightChannelCount(converted);
@ -83,15 +99,18 @@ public class AudioPlaybackEngine : IDisposable
mixer.AddMixerInput(input);
}
public void RemoveMixerInput(ISampleProvider sound) {
public void RemoveMixerInput(ISampleProvider sound)
{
mixer.RemoveMixerInput(sound);
}
public void AddOnMixerInputEnded(EventHandler<SampleProviderEventArgs> e) {
public void AddOnMixerInputEnded(EventHandler<SampleProviderEventArgs> e)
{
mixer.MixerInputEnded += e;
}
public void Dispose() {
public void Dispose()
{
outputDevice.Dispose();
}
}

View File

@ -7,12 +7,14 @@ public class NetworkWavSound
public NetworkWavSound(string uri)
{
using (var mfr = new MediaFoundationReader(uri)) {
using (var mfr = new MediaFoundationReader(uri))
{
WaveFormat = mfr.WaveFormat;
byte[] buffer = new byte[4096];
int read = 0;
using (var ms = new MemoryStream()) {
using (var ms = new MemoryStream())
{
while ((read = mfr.Read(buffer, 0, buffer.Length)) > 0)
ms.Write(buffer, 0, read);
AudioData = ms.ToArray();

View File

@ -1,6 +1,7 @@
using NAudio.Wave;
public class TTSPlayer {
public class TTSPlayer
{
private PriorityQueue<TTSMessage, int> _messages; // ready to play
private PriorityQueue<TTSMessage, int> _buffer;
private Mutex _mutex;
@ -8,77 +9,105 @@ public class TTSPlayer {
public ISampleProvider? Playing { get; set; }
public TTSPlayer() {
public TTSPlayer()
{
_messages = new PriorityQueue<TTSMessage, int>();
_buffer = new PriorityQueue<TTSMessage, int>();
_mutex = new Mutex();
_mutex2 = new Mutex();
}
public void Add(TTSMessage message) {
try {
public void Add(TTSMessage message)
{
try
{
_mutex2.WaitOne();
_buffer.Enqueue(message, message.Priority);
} finally {
}
finally
{
_mutex2.ReleaseMutex();
}
}
public TTSMessage? ReceiveReady() {
try {
public TTSMessage? ReceiveReady()
{
try
{
_mutex.WaitOne();
if (_messages.TryDequeue(out TTSMessage? message, out int _)) {
if (_messages.TryDequeue(out TTSMessage? message, out int _))
{
return message;
}
return null;
} finally {
}
finally
{
_mutex.ReleaseMutex();
}
}
public TTSMessage? ReceiveBuffer() {
try {
public TTSMessage? ReceiveBuffer()
{
try
{
_mutex2.WaitOne();
if (_buffer.TryDequeue(out TTSMessage? message, out int _)) {
if (_buffer.TryDequeue(out TTSMessage? message, out int _))
{
return message;
}
return null;
} finally {
}
finally
{
_mutex2.ReleaseMutex();
}
}
public void Ready(TTSMessage message) {
try {
public void Ready(TTSMessage message)
{
try
{
_mutex.WaitOne();
_messages.Enqueue(message, message.Priority);
} finally {
}
finally
{
_mutex.ReleaseMutex();
}
}
public void RemoveAll() {
try {
public void RemoveAll()
{
try
{
_mutex2.WaitOne();
_buffer.Clear();
} finally {
}
finally
{
_mutex2.ReleaseMutex();
}
try {
try
{
_mutex.WaitOne();
_messages.Clear();
} finally {
}
finally
{
_mutex.ReleaseMutex();
}
}
public bool IsEmpty() {
public bool IsEmpty()
{
return _messages.Count == 0;
}
}
public class TTSMessage {
public class TTSMessage
{
public string? Voice { get; set; }
public string? Channel { get; set; }
public string? Username { get; set; }