diff --git a/Chat/ChatMessageHandler.cs b/Chat/ChatMessageHandler.cs index 8034e44..b64d146 100644 --- a/Chat/ChatMessageHandler.cs +++ b/Chat/ChatMessageHandler.cs @@ -4,22 +4,22 @@ using TwitchChatTTS.OBS.Socket; using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using Microsoft.Extensions.Logging; -using TwitchChatTTS.Twitch; using Microsoft.Extensions.DependencyInjection; using TwitchChatTTS; using TwitchChatTTS.Seven; +using TwitchChatTTS.Chat.Commands; public class ChatMessageHandler { - private ILogger Logger { get; } - private Configuration Configuration { get; } - public EmoteCounter EmoteCounter { get; } - private EmoteDatabase Emotes { get; } - private TTSPlayer Player { get; } - private OBSSocketClient? Client { get; } - private TTSContext Context { get; } + 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 IServiceProvider _serviceProvider { get; } - private Regex? voicesRegex; private Regex sfxRegex; @@ -29,46 +29,53 @@ public class ChatMessageHandler { EmoteCounter emoteCounter, EmoteDatabase emotes, TTSPlayer player, + ChatCommandManager commands, [FromKeyedServices("obs")] SocketClient client, - TTSContext context + IServiceProvider serviceProvider ) { - Logger = logger; - Configuration = configuration; - EmoteCounter = emoteCounter; - Emotes = emotes; - Player = player; - Client = client as OBSSocketClient; - Context = context; + _logger = logger; + _configuration = configuration; + _emoteCounter = emoteCounter; + _emotes = emotes; + _player = player; + _commands = commands; + _obsClient = client as OBSSocketClient; + _serviceProvider = serviceProvider; - voicesRegex = GenerateEnabledVoicesRegex(); sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)"); } - public MessageResult Handle(OnMessageReceivedArgs e) { - if (Configuration.Twitch?.TtsWhenOffline != true && Client?.Live != true) + public async Task Handle(OnMessageReceivedArgs e) { + if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient?.Live == false) return MessageResult.Blocked; - + + var user = _serviceProvider.GetRequiredService(); var m = e.ChatMessage; var msg = e.ChatMessage.Message; - - // Skip TTS messages - if (m.IsVip || m.IsModerator || m.IsBroadcaster) { - if (msg.ToLower().StartsWith("!skip ") || msg.ToLower() == "!skip") - return MessageResult.Skip; - - if (msg.ToLower().StartsWith("!skipall ") || msg.ToLower() == "!skipall") - return MessageResult.SkipAll; + var chatterId = long.Parse(m.UserId); + + var blocked = user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted"; + + 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 (Context.UsernameFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted") { - Logger.LogTrace($"Blocked message by {m.Username}: {msg}"); + if (blocked) { + _logger.LogTrace($"Blocked message by {m.Username}: {msg}"); return MessageResult.Blocked; } // Replace filtered words. - if (Context.WordFilters is not null) { - foreach (var wf in Context.WordFilters) { + if (user.RegexFilters != null) { + foreach (var wf in user.RegexFilters) { if (wf.Search == null || wf.Replace == null) continue; @@ -87,6 +94,7 @@ public class ChatMessageHandler { } // Filter highly repetitive words (like emotes) from the message. + int totalEmoteUsed = 0; var emotesUsed = new HashSet(); var words = msg.Split(" "); var wordCounter = new Dictionary(); @@ -98,24 +106,31 @@ public class ChatMessageHandler { wordCounter.Add(w, 1); } - var emoteId = Emotes?.Get(w); - if (emoteId != null) - emotesUsed.Add("7tv-" + emoteId); + var emoteId = _emotes?.Get(w); + if (emoteId == null) + emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id; + if (emoteId != null) { + emotesUsed.Add(emoteId); + totalEmoteUsed++; + } - if (wordCounter[w] <= 4 && (emoteId == null || emotesUsed.Count <= 4)) + if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5)) filteredMsg += w + " "; } msg = filteredMsg; // Adding twitch emotes to the counter. - foreach (var emote in e.ChatMessage.EmoteSet.Emotes) - emotesUsed.Add("twitch-" + emote.Id); + foreach (var emote in e.ChatMessage.EmoteSet.Emotes) { + _logger.LogTrace("Twitch emote name used: " + emote.Name); + emotesUsed.Add(emote.Id); + } if (long.TryParse(e.ChatMessage.UserId, out long userId)) - EmoteCounter.Add(userId, emotesUsed); + _emoteCounter.Add(userId, emotesUsed); if (emotesUsed.Any()) - Logger.LogDebug("Emote counters for user #" + userId + ": " + string.Join(" | ", emotesUsed.Select(e => e + "=" + EmoteCounter.Get(userId, e)))); + _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) { priority = int.MinValue; @@ -130,19 +145,30 @@ public class ChatMessageHandler { } else if (m.IsHighlighted) { priority = -1; } - priority = (int) Math.Round(Math.Min(priority, -m.SubscribedMonthCount * (m.Badges.Any(b => b.Key == "subscriber") ? 1.2 : 1))); + priority = Math.Min(priority, -m.SubscribedMonthCount * (m.IsSubscriber ? 2 : 1)); - var matches = voicesRegex?.Matches(msg).ToArray() ?? new Match[0]; - int defaultEnd = matches.FirstOrDefault()?.Index ?? msg.Length; - if (defaultEnd > 0) { - HandlePartialMessage(priority, Context.DefaultVoice, msg.Substring(0, defaultEnd).Trim(), e); + // Determine voice selected. + string voiceSelected = user.DefaultTTSVoice; + if (user.VoicesSelected?.ContainsKey(userId) == true) { + var voiceId = user.VoicesSelected[userId]; + 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) { + HandlePartialMessage(priority, voiceSelected, msg.Trim(), e); + return MessageResult.None; + } + + HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.FirstOrDefault().Index).Trim(), e); foreach (Match match in matches) { var message = match.Groups[2].ToString(); - if (string.IsNullOrWhiteSpace(message)) { + if (string.IsNullOrWhiteSpace(message)) continue; - } var voice = match.Groups[1].ToString(); voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower(); @@ -162,8 +188,8 @@ public class ChatMessageHandler { 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() { + _logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}"); + _player.Add(new TTSMessage() { Voice = voice, Message = message, Moderator = m.IsModerator, @@ -189,8 +215,8 @@ public class ChatMessageHandler { } if (!string.IsNullOrWhiteSpace(parts[i * 2])) { - Logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}"); - Player.Add(new TTSMessage() { + _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() { Voice = voice, Message = parts[i * 2], Moderator = m.IsModerator, @@ -202,8 +228,8 @@ public class ChatMessageHandler { }); } - Logger.LogInformation($"Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}"); - Player.Add(new TTSMessage() { + _logger.LogInformation($"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", @@ -217,8 +243,8 @@ public class ChatMessageHandler { } if (!string.IsNullOrWhiteSpace(parts.Last())) { - Logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}"); - Player.Add(new TTSMessage() { + _logger.LogInformation($"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, @@ -230,13 +256,4 @@ public class ChatMessageHandler { }); } } - - private Regex? GenerateEnabledVoicesRegex() { - if (Context.EnabledVoices == null || Context.EnabledVoices.Count() <= 0) { - return null; - } - - var enabledVoicesString = string.Join("|", Context.EnabledVoices.Select(v => v.Label)); - return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase); - } } \ No newline at end of file diff --git a/Chat/Commands/AddTTSVoiceCommand.cs b/Chat/Commands/AddTTSVoiceCommand.cs new file mode 100644 index 0000000..8fd4ccf --- /dev/null +++ b/Chat/Commands/AddTTSVoiceCommand.cs @@ -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 _logger; + + public AddTTSVoiceCommand( + [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, + IServiceProvider serviceProvider, + ILogger logger + ) : base("addttsvoice", "Select a TTS voice as the default for that user.") { + _serviceProvider = serviceProvider; + _logger = logger; + + AddParameter(ttsVoiceParameter); + } + + public override async Task CheckPermissions(ChatMessage message, long broadcasterId) + { + return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566"; + } + + public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + { + var client = _serviceProvider.GetRequiredKeyedService>("hermes"); + if (client == null) + return; + var context = _serviceProvider.GetRequiredService(); + 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() { { "@voice", voiceName } } + }); + _logger.LogInformation($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}."); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/ChatCommand.cs b/Chat/Commands/ChatCommand.cs new file mode 100644 index 0000000..5b8b761 --- /dev/null +++ b/Chat/Commands/ChatCommand.cs @@ -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 Parameters { get => _parameters.AsReadOnly(); } + private IList _parameters; + + public ChatCommand(string name, string description) { + Name = name; + Description = description; + _parameters = new List(); + } + + protected void AddParameter(ChatCommandParameter parameter) { + if (parameter != null) + _parameters.Add(parameter); + } + + public abstract Task CheckPermissions(ChatMessage message, long broadcasterId); + public abstract Task Execute(IList args, ChatMessage message, long broadcasterId); + } +} \ No newline at end of file diff --git a/Chat/Commands/ChatCommandManager.cs b/Chat/Commands/ChatCommandManager.cs new file mode 100644 index 0000000..c6ec5a5 --- /dev/null +++ b/Chat/Commands/ChatCommandManager.cs @@ -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 _commands; + private TwitchBotToken _token; + private IServiceProvider _serviceProvider; + private ILogger _logger; + private string CommandStartSign { get; } = "!"; + + + public ChatCommandManager(TwitchBotToken token, IServiceProvider serviceProvider, ILogger logger) { + _token = token; + _serviceProvider = serviceProvider; + _logger = logger; + + _commands = new Dictionary(); + 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(key); + if (command == null) { + _logger.LogError("Failed to add command: " + type.AssemblyQualifiedName); + continue; + } + + _logger.LogDebug($"Added command {type.AssemblyQualifiedName}."); + Add(command); + } + } + + public async Task 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; + } + } +} \ No newline at end of file diff --git a/Chat/Commands/ChatCommandResult.cs b/Chat/Commands/ChatCommandResult.cs new file mode 100644 index 0000000..f8c5ddf --- /dev/null +++ b/Chat/Commands/ChatCommandResult.cs @@ -0,0 +1,12 @@ +namespace TwitchChatTTS.Chat.Commands +{ + public enum ChatCommandResult + { + Unknown = 0, + Missing = 1, + Success = 2, + Permission = 3, + Syntax = 4, + Fail = 5 + } +} \ No newline at end of file diff --git a/Chat/Commands/Parameters/ChatCommandParameter.cs b/Chat/Commands/Parameters/ChatCommandParameter.cs new file mode 100644 index 0000000..df223c1 --- /dev/null +++ b/Chat/Commands/Parameters/ChatCommandParameter.cs @@ -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); + } +} \ No newline at end of file diff --git a/Chat/Commands/Parameters/TTSVoiceNameParameter.cs b/Chat/Commands/Parameters/TTSVoiceNameParameter.cs new file mode 100644 index 0000000..672c24c --- /dev/null +++ b/Chat/Commands/Parameters/TTSVoiceNameParameter.cs @@ -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(); + if (user.VoicesAvailable == null) + return false; + + value = value.ToLower(); + return user.VoicesAvailable.Any(e => e.Value.ToLower() == value); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/Parameters/UnvalidatedParameter.cs b/Chat/Commands/Parameters/UnvalidatedParameter.cs new file mode 100644 index 0000000..7a4d534 --- /dev/null +++ b/Chat/Commands/Parameters/UnvalidatedParameter.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Chat/Commands/RemoveTTSVoiceCommand.cs b/Chat/Commands/RemoveTTSVoiceCommand.cs new file mode 100644 index 0000000..e0786db --- /dev/null +++ b/Chat/Commands/RemoveTTSVoiceCommand.cs @@ -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 _logger; + + public RemoveTTSVoiceCommand( + [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, + IServiceProvider serviceProvider, + ILogger logger + ) : base("removettsvoice", "Select a TTS voice as the default for that user.") { + _serviceProvider = serviceProvider; + _logger = logger; + + AddParameter(ttsVoiceParameter); + } + + public override async Task CheckPermissions(ChatMessage message, long broadcasterId) + { + return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566"; + } + + public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + { + var client = _serviceProvider.GetRequiredKeyedService>("hermes"); + if (client == null) + return; + var context = _serviceProvider.GetRequiredService(); + 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() { { "@voice", voiceId } } + }); + _logger.LogInformation($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}."); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/SkipAllCommand.cs b/Chat/Commands/SkipAllCommand.cs new file mode 100644 index 0000000..8e4da55 --- /dev/null +++ b/Chat/Commands/SkipAllCommand.cs @@ -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 _logger; + + public SkipAllCommand(IServiceProvider serviceProvider, ILogger logger) + : base("skipall", "Skips all text to speech messages in queue and playing.") { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public override async Task CheckPermissions(ChatMessage message, long broadcasterId) + { + return message.IsModerator || message.IsVip || message.IsBroadcaster; + } + + public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + { + var player = _serviceProvider.GetRequiredService(); + player.RemoveAll(); + + if (player.Playing == null) + return; + + AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing); + player.Playing = null; + + _logger.LogInformation("Skipped all queued and playing tts."); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/SkipCommand.cs b/Chat/Commands/SkipCommand.cs new file mode 100644 index 0000000..8874530 --- /dev/null +++ b/Chat/Commands/SkipCommand.cs @@ -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 _logger; + + public SkipCommand(IServiceProvider serviceProvider, ILogger logger) + : base("skip", "Skips the current text to speech message.") { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public override async Task CheckPermissions(ChatMessage message, long broadcasterId) + { + return message.IsModerator || message.IsVip || message.IsBroadcaster; + } + + public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + { + var player = _serviceProvider.GetRequiredService(); + if (player.Playing == null) + return; + + AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing); + player.Playing = null; + + _logger.LogInformation("Skipped current tts."); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/VoiceCommand.cs b/Chat/Commands/VoiceCommand.cs new file mode 100644 index 0000000..cb6470f --- /dev/null +++ b/Chat/Commands/VoiceCommand.cs @@ -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 _logger; + + public VoiceCommand( + [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, + IServiceProvider serviceProvider, + ILogger logger + ) : base("voice", "Select a TTS voice as the default for that user.") { + _serviceProvider = serviceProvider; + _logger = logger; + + AddParameter(ttsVoiceParameter); + } + + public override async Task CheckPermissions(ChatMessage message, long broadcasterId) + { + return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100 || message.UserId == "126224566"; + } + + public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + { + var client = _serviceProvider.GetRequiredKeyedService>("hermes"); + if (client == null) + return; + var context = _serviceProvider.GetRequiredService(); + 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() { { "@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() { { "@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})."); + } + } + } +} \ No newline at end of file diff --git a/Chat/MessageResult.cs b/Chat/MessageResult.cs index bd392b9..d9ec612 100644 --- a/Chat/MessageResult.cs +++ b/Chat/MessageResult.cs @@ -1,6 +1,6 @@ public enum MessageResult { - Skip = 1, - SkipAll = 2, - Blocked = 3, - None = 0 + None = 0, + NotReady = 1, + Blocked = 2, + Command = 3 } \ No newline at end of file diff --git a/Chat/Speech/AudioPlaybackEngine.cs b/Chat/Speech/AudioPlaybackEngine.cs index 0920178..790c2d6 100644 --- a/Chat/Speech/AudioPlaybackEngine.cs +++ b/Chat/Speech/AudioPlaybackEngine.cs @@ -24,7 +24,7 @@ public class AudioPlaybackEngine : IDisposable private ISampleProvider ConvertToRightChannelCount(ISampleProvider? input) { - if (input is null) + if (input == null) throw new NullReferenceException(nameof(input)); if (input.WaveFormat.Channels == mixer.WaveFormat.Channels) diff --git a/Chat/Speech/TTSPlayer.cs b/Chat/Speech/TTSPlayer.cs index 293c4f3..810d6fa 100644 --- a/Chat/Speech/TTSPlayer.cs +++ b/Chat/Speech/TTSPlayer.cs @@ -6,6 +6,8 @@ public class TTSPlayer { private Mutex _mutex; private Mutex _mutex2; + public ISampleProvider? Playing { get; set; } + public TTSPlayer() { _messages = new PriorityQueue(); _buffer = new PriorityQueue(); diff --git a/Configuration.cs b/Configuration.cs index ebc9b9f..865f22b 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -1,5 +1,3 @@ -using TwitchChatTTS.Seven.Socket.Context; - namespace TwitchChatTTS { public class Configuration @@ -39,10 +37,7 @@ namespace TwitchChatTTS } public class SevenConfiguration { - public string? Protocol; - public string? Url; - - public IEnumerable? InitialSubscriptions; + public string? UserId; } } } \ No newline at end of file diff --git a/Hermes/HermesClient.cs b/Hermes/HermesClient.cs index 72a40bb..f4fb5c4 100644 --- a/Hermes/HermesClient.cs +++ b/Hermes/HermesClient.cs @@ -4,18 +4,10 @@ using TwitchChatTTS.Hermes; using System.Text.Json; public class HermesClient { - private Account? account; private WebClientWrap _web; - private Configuration Configuration { get; } - - public string? Id { get => account?.Id; } - public string? Username { get => account?.Username; } - public HermesClient(Configuration configuration) { - Configuration = configuration; - - if (string.IsNullOrWhiteSpace(Configuration.Hermes?.Token)) { + if (string.IsNullOrWhiteSpace(configuration.Hermes?.Token)) { throw new Exception("Ensure you have written your API key in \".token\" file, in the same folder as this application."); } @@ -23,54 +15,52 @@ public class HermesClient { PropertyNameCaseInsensitive = false, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); - _web.AddHeader("x-api-key", Configuration.Hermes.Token); + _web.AddHeader("x-api-key", configuration.Hermes.Token); } - public async Task FetchHermesAccountDetails() { - account = await _web.GetJson("https://hermes.goblincaves.com/api/account"); + public async Task FetchHermesAccountDetails() { + var account = await _web.GetJson("https://hermes.goblincaves.com/api/account"); + if (account == null || account.Id == null || account.Username == null) + throw new NullReferenceException("Invalid value found while fetching for hermes account data."); + return account; } public async Task FetchTwitchBotToken() { var token = await _web.GetJson("https://hermes.goblincaves.com/api/token/bot"); - if (token == null) { + if (token == null || token.ClientId == null || token.AccessToken == null || token.RefreshToken == null || token.ClientSecret == null) throw new Exception("Failed to fetch Twitch API token from Hermes."); - } return token; } public async Task> FetchTTSUsernameFilters() { var filters = await _web.GetJson>("https://hermes.goblincaves.com/api/settings/tts/filter/users"); - if (filters == null) { + if (filters == null) throw new Exception("Failed to fetch TTS username filters from Hermes."); - } return filters; } - public async Task FetchTTSDefaultVoice() { + public async Task FetchTTSDefaultVoice() { var data = await _web.GetJson("https://hermes.goblincaves.com/api/settings/tts/default"); - if (data == null) { + if (data == null) throw new Exception("Failed to fetch TTS default voice from Hermes."); - } return data.Label; } public async Task> FetchTTSEnabledVoices() { var voices = await _web.GetJson>("https://hermes.goblincaves.com/api/settings/tts"); - if (voices == null) { + if (voices == null) throw new Exception("Failed to fetch TTS enabled voices from Hermes."); - } return voices; } public async Task> FetchTTSWordFilters() { var filters = await _web.GetJson>("https://hermes.goblincaves.com/api/settings/tts/filter/words"); - if (filters == null) { + if (filters == null) throw new Exception("Failed to fetch TTS word filters from Hermes."); - } return filters; } diff --git a/Hermes/Socket/Handlers/HeartbeatHandler.cs b/Hermes/Socket/Handlers/HeartbeatHandler.cs new file mode 100644 index 0000000..bbe335e --- /dev/null +++ b/Hermes/Socket/Handlers/HeartbeatHandler.cs @@ -0,0 +1,35 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using HermesSocketLibrary.Socket.Data; +using Microsoft.Extensions.Logging; + +namespace TwitchChatTTS.Hermes.Socket.Handlers +{ + public class HeartbeatHandler : IWebSocketHandler + { + private ILogger _logger { get; } + public int OperationCode { get; set; } = 0; + + public HeartbeatHandler(ILogger logger) { + _logger = logger; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not HeartbeatMessage obj || obj == null) + return; + + if (sender is not HermesSocketClient client) { + return; + } + + _logger.LogTrace("Received heartbeat."); + + client.LastHeartbeat = DateTime.UtcNow; + + await sender.Send(0, new HeartbeatMessage() { + DateTime = DateTime.UtcNow + }); + } + } +} \ No newline at end of file diff --git a/Hermes/Socket/Handlers/LoginAckHandler.cs b/Hermes/Socket/Handlers/LoginAckHandler.cs new file mode 100644 index 0000000..0d328d8 --- /dev/null +++ b/Hermes/Socket/Handlers/LoginAckHandler.cs @@ -0,0 +1,34 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using HermesSocketLibrary.Socket.Data; +using Microsoft.Extensions.Logging; + +namespace TwitchChatTTS.Hermes.Socket.Handlers +{ + public class LoginAckHandler : IWebSocketHandler + { + private ILogger _logger { get; } + public int OperationCode { get; set; } = 2; + + public LoginAckHandler(ILogger logger) { + _logger = logger; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not LoginAckMessage obj || obj == null) + return; + + if (sender is not HermesSocketClient client) { + return; + } + + if (obj.AnotherClient) { + _logger.LogWarning("Another client has connected to the same account."); + } else { + client.UserId = obj.UserId; + _logger.LogInformation($"Logged in as {client.UserId}."); + } + } + } +} \ No newline at end of file diff --git a/Hermes/Socket/Handlers/RequestAckHandler.cs b/Hermes/Socket/Handlers/RequestAckHandler.cs new file mode 100644 index 0000000..2f904a3 --- /dev/null +++ b/Hermes/Socket/Handlers/RequestAckHandler.cs @@ -0,0 +1,106 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using HermesSocketLibrary.Requests.Messages; +using HermesSocketLibrary.Socket.Data; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TwitchChatTTS.Hermes.Socket.Handlers +{ + public class RequestAckHandler : IWebSocketHandler + { + private readonly IServiceProvider _serviceProvider; + private readonly JsonSerializerOptions _options; + private readonly ILogger _logger; + public int OperationCode { get; set; } = 4; + + public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger) { + _serviceProvider = serviceProvider; + _options = options; + _logger = logger; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not RequestAckMessage obj || obj == null) + return; + if (obj.Request == null) + return; + var context = _serviceProvider.GetRequiredService(); + if (context == null) + return; + + if (obj.Request.Type == "get_tts_voices") { + _logger.LogDebug("Updating all available voices."); + var voices = JsonSerializer.Deserialize>(obj.Data.ToString(), _options); + if (voices == null) + return; + + context.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name); + _logger.LogInformation("Updated all available voices."); + } else if (obj.Request.Type == "create_tts_user") { + _logger.LogDebug("Creating new tts voice."); + if (!long.TryParse(obj.Request.Data["@user"], out long userId)) + return; + string broadcasterId = obj.Request.Data["@broadcaster"].ToString(); + // TODO: validate broadcaster id. + string voice = obj.Request.Data["@voice"].ToString(); + + context.VoicesSelected.Add(userId, voice); + _logger.LogInformation("Created new tts user."); + } else if (obj.Request.Type == "update_tts_user") { + _logger.LogDebug("Updating user's voice"); + if (!long.TryParse(obj.Request.Data["@user"], out long userId)) + return; + string broadcasterId = obj.Request.Data["@broadcaster"].ToString(); + string voice = obj.Request.Data["@voice"].ToString(); + + context.VoicesSelected[userId] = voice; + _logger.LogInformation($"Updated user's voice to {voice}."); + } else if (obj.Request.Type == "create_tts_voice") { + _logger.LogDebug("Creating new tts voice."); + string? voice = obj.Request.Data["@voice"]; + string? voiceId = obj.Data.ToString(); + if (voice == null || voiceId == null) + return; + + context.VoicesAvailable.Add(voiceId, voice); + _logger.LogInformation($"Created new tts voice named {voice} (id: {voiceId})."); + } else if (obj.Request.Type == "delete_tts_voice") { + _logger.LogDebug("Deleting tts voice."); + + var voice = obj.Request.Data["@voice"]; + if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) { + return; + } + + context.VoicesAvailable.Remove(voice); + _logger.LogInformation("Deleted a voice, named " + voiceName + "."); + } else if (obj.Request.Type == "update_tts_voice") { + _logger.LogDebug("Updating tts voice."); + string voiceId = obj.Request.Data["@idd"].ToString(); + string voice = obj.Request.Data["@voice"].ToString(); + + if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) { + return; + } + + context.VoicesAvailable[voiceId] = voice; + _logger.LogInformation("Update tts voice: " + voice); + } else if (obj.Request.Type == "get_tts_users") { + _logger.LogDebug("Attempting to update all chatters' selected voice."); + var users = JsonSerializer.Deserialize>(obj.Data.ToString(), _options); + if (users == null) + return; + + var temp = new ConcurrentDictionary(); + foreach (var entry in users) + temp.TryAdd(entry.Key, entry.Value); + context.VoicesSelected = temp; + _logger.LogInformation($"Fetched {temp.Count()} chatters' selected voice."); + } + } + } +} \ No newline at end of file diff --git a/Hermes/Socket/HermesSocketClient.cs b/Hermes/Socket/HermesSocketClient.cs new file mode 100644 index 0000000..9969657 --- /dev/null +++ b/Hermes/Socket/HermesSocketClient.cs @@ -0,0 +1,24 @@ +using System.Text.Json; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TwitchChatTTS.Hermes.Socket +{ + public class HermesSocketClient : WebSocketClient { + public DateTime LastHeartbeat { get; set; } + public string? UserId { get; set; } + + public HermesSocketClient( + ILogger logger, + [FromKeyedServices("hermes")] HandlerManager handlerManager, + [FromKeyedServices("hermes")] HandlerTypeManager typeManager + ) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }) { + + } + } +} \ No newline at end of file diff --git a/Hermes/Socket/Managers/HermesHandlerManager.cs b/Hermes/Socket/Managers/HermesHandlerManager.cs new file mode 100644 index 0000000..4c4897f --- /dev/null +++ b/Hermes/Socket/Managers/HermesHandlerManager.cs @@ -0,0 +1,36 @@ +using CommonSocketLibrary.Common; +using CommonSocketLibrary.Socket.Manager; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TwitchChatTTS.Hermes.Socket.Managers +{ + public class HermesHandlerManager : WebSocketHandlerManager + { + public HermesHandlerManager(ILogger logger, IServiceProvider provider) : base(logger) { + //Add(provider.GetRequiredService()); + 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(key); + if (handler == null) { + logger.LogError("Failed to find hermes websocket handler: " + type.AssemblyQualifiedName); + continue; + } + + Logger.LogDebug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers."); + Add(handler); + } + } catch (Exception e) { + Logger.LogError(e, "Failed to load hermes websocket handler types."); + } + } + } +} \ No newline at end of file diff --git a/Hermes/Socket/Managers/HermesHandlerTypeManager.cs b/Hermes/Socket/Managers/HermesHandlerTypeManager.cs new file mode 100644 index 0000000..b19374f --- /dev/null +++ b/Hermes/Socket/Managers/HermesHandlerTypeManager.cs @@ -0,0 +1,32 @@ +using System.Reflection; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using CommonSocketLibrary.Socket.Manager; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TwitchChatTTS.Hermes.Socket.Managers +{ + public class HermesHandlerTypeManager : WebSocketHandlerTypeManager + { + public HermesHandlerTypeManager( + ILogger factory, + [FromKeyedServices("hermes")] HandlerManager handlers + ) : base(factory, handlers) + { + } + + protected override Type? FetchMessageType(Type handlerType) + { + if (handlerType == null) + return null; + + var name = handlerType.Namespace + "." + handlerType.Name; + name = name.Replace(".Handlers.", ".Data.") + .Replace("Handler", "Message") + .Replace("TwitchChatTTS.Hermes.", "HermesSocketLibrary."); + + return Assembly.Load("HermesSocketLibrary").GetType(name); + } + } +} \ No newline at end of file diff --git a/Hermes/TTSUsernameFilter.cs b/Hermes/TTSUsernameFilter.cs index bf2030e..2141e4e 100644 --- a/Hermes/TTSUsernameFilter.cs +++ b/Hermes/TTSUsernameFilter.cs @@ -1,5 +1,5 @@ public class TTSUsernameFilter { - public string? Username { get; set; } - public string? Tag { get; set; } - public string? UserId { get; set; } + public string Username { get; set; } + public string Tag { get; set; } + public string UserId { get; set; } } \ No newline at end of file diff --git a/Hermes/TTSVoice.cs b/Hermes/TTSVoice.cs index 286f145..76b98ee 100644 --- a/Hermes/TTSVoice.cs +++ b/Hermes/TTSVoice.cs @@ -1,5 +1,5 @@ public class TTSVoice { - public string? Label { get; set; } + public string Label { get; set; } public int Value { get; set; } public string? Gender { get; set; } public string? Language { get; set; } diff --git a/Hermes/TTSWordFilter.cs b/Hermes/TTSWordFilter.cs index 68faecc..b79d60f 100644 --- a/Hermes/TTSWordFilter.cs +++ b/Hermes/TTSWordFilter.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - namespace TwitchChatTTS.Hermes { public class TTSWordFilter diff --git a/OBS/Socket/Data/EventMessage.cs b/OBS/Socket/Data/EventMessage.cs index 2f34557..8632252 100644 --- a/OBS/Socket/Data/EventMessage.cs +++ b/OBS/Socket/Data/EventMessage.cs @@ -3,8 +3,8 @@ namespace TwitchChatTTS.OBS.Socket.Data [Serializable] public class EventMessage { - public string eventType { get; set; } - public int eventIntent { get; set; } - public Dictionary eventData { get; set; } + public string EventType { get; set; } + public int EventIntent { get; set; } + public Dictionary EventData { get; set; } } } \ No newline at end of file diff --git a/OBS/Socket/Data/HelloMessage.cs b/OBS/Socket/Data/HelloMessage.cs index f576c28..69009be 100644 --- a/OBS/Socket/Data/HelloMessage.cs +++ b/OBS/Socket/Data/HelloMessage.cs @@ -3,13 +3,13 @@ namespace TwitchChatTTS.OBS.Socket.Data [Serializable] public class HelloMessage { - public string obsWebSocketVersion { get; set; } - public int rpcVersion { get; set; } - public AuthenticationMessage authentication { get; set; } + public string ObsWebSocketVersion { get; set; } + public int RpcVersion { get; set; } + public AuthenticationMessage Authentication { get; set; } } public class AuthenticationMessage { - public string challenge { get; set; } - public string salt { get; set; } + public string Challenge { get; set; } + public string Salt { get; set; } } } \ No newline at end of file diff --git a/OBS/Socket/Data/IdentifiedMessage.cs b/OBS/Socket/Data/IdentifiedMessage.cs index eb07559..beebf58 100644 --- a/OBS/Socket/Data/IdentifiedMessage.cs +++ b/OBS/Socket/Data/IdentifiedMessage.cs @@ -3,6 +3,6 @@ namespace TwitchChatTTS.OBS.Socket.Data [Serializable] public class IdentifiedMessage { - public int negotiatedRpcVersion { get; set; } + public int NegotiatedRpcVersion { get; set; } } } \ No newline at end of file diff --git a/OBS/Socket/Data/IdentifyMessage.cs b/OBS/Socket/Data/IdentifyMessage.cs index f3b1594..bf7a1e3 100644 --- a/OBS/Socket/Data/IdentifyMessage.cs +++ b/OBS/Socket/Data/IdentifyMessage.cs @@ -3,14 +3,14 @@ namespace TwitchChatTTS.OBS.Socket.Data [Serializable] public class IdentifyMessage { - public int rpcVersion { get; set; } - public string? authentication { get; set; } - public int eventSubscriptions { get; set; } + public int RpcVersion { get; set; } + public string? Authentication { get; set; } + public int EventSubscriptions { get; set; } public IdentifyMessage(int version, string auth, int subscriptions) { - rpcVersion = version; - authentication = auth; - eventSubscriptions = subscriptions; + RpcVersion = version; + Authentication = auth; + EventSubscriptions = subscriptions; } } } \ No newline at end of file diff --git a/OBS/Socket/Data/RequestMessage.cs b/OBS/Socket/Data/RequestMessage.cs index 5212c12..edaab05 100644 --- a/OBS/Socket/Data/RequestMessage.cs +++ b/OBS/Socket/Data/RequestMessage.cs @@ -3,14 +3,14 @@ namespace TwitchChatTTS.OBS.Socket.Data [Serializable] public class RequestMessage { - public string requestType { get; set; } - public string requestId { get; set; } - public Dictionary requestData { get; set; } + public string RequestType { get; set; } + public string RequestId { get; set; } + public Dictionary RequestData { get; set; } public RequestMessage(string type, string id, Dictionary data) { - requestType = type; - requestId = id; - requestData = data; + RequestType = type; + RequestId = id; + RequestData = data; } } } \ No newline at end of file diff --git a/OBS/Socket/Data/RequestResponseMessage.cs b/OBS/Socket/Data/RequestResponseMessage.cs index 657e1c6..4648277 100644 --- a/OBS/Socket/Data/RequestResponseMessage.cs +++ b/OBS/Socket/Data/RequestResponseMessage.cs @@ -3,9 +3,9 @@ namespace TwitchChatTTS.OBS.Socket.Data [Serializable] public class RequestResponseMessage { - public string requestType { get; set; } - public string requestId { get; set; } - public object requestStatus { get; set; } - public Dictionary responseData { get; set; } + public string RequestType { get; set; } + public string RequestId { get; set; } + public object RequestStatus { get; set; } + public Dictionary ResponseData { get; set; } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/EventMessageHandler.cs b/OBS/Socket/Handlers/EventMessageHandler.cs index a8d08d4..170d62d 100644 --- a/OBS/Socket/Handlers/EventMessageHandler.cs +++ b/OBS/Socket/Handlers/EventMessageHandler.cs @@ -21,15 +21,15 @@ namespace TwitchChatTTS.OBS.Socket.Handlers if (message is not EventMessage obj || obj == null) return; - switch (obj.eventType) { + switch (obj.EventType) { case "StreamStateChanged": case "RecordStateChanged": if (sender is not OBSSocketClient client) return; - string? raw_state = obj.eventData["outputState"].ToString(); + string? raw_state = obj.EventData["outputState"].ToString(); string? state = raw_state?.Substring(21).ToLower(); - client.Live = obj.eventData["outputActive"].ToString() == "True"; + client.Live = obj.EventData["outputActive"].ToString() == "True"; Logger.LogWarning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + "."); if (client.Live == false && state != null && !state.EndsWith("ing")) { @@ -37,7 +37,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers } break; default: - Logger.LogDebug(obj.eventType + " EVENT: " + string.Join(" | ", obj.eventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0])); + Logger.LogDebug(obj.EventType + " EVENT: " + string.Join(" | ", obj.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0])); break; } } diff --git a/OBS/Socket/Handlers/HelloHandler.cs b/OBS/Socket/Handlers/HelloHandler.cs index 81a0645..1432bfb 100644 --- a/OBS/Socket/Handlers/HelloHandler.cs +++ b/OBS/Socket/Handlers/HelloHandler.cs @@ -25,15 +25,14 @@ namespace TwitchChatTTS.OBS.Socket.Handlers return; Logger.LogTrace("OBS websocket password: " + Context.Password); - if (obj.authentication is null || Context.Password is null) // TODO: send re-identify message. + if (obj.Authentication == null || Context.Password == null) // TODO: send re-identify message. return; - var salt = obj.authentication.salt; - var challenge = obj.authentication.challenge; + var salt = obj.Authentication.Salt; + var challenge = obj.Authentication.Challenge; Logger.LogTrace("Salt: " + salt); Logger.LogTrace("Challenge: " + challenge); - string secret = Context.Password + salt; byte[] bytes = Encoding.UTF8.GetBytes(secret); string hash = null; @@ -48,8 +47,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers } Logger.LogTrace("Final hash: " + hash); - //await sender.Send(1, new IdentifyMessage(obj.rpcVersion, hash, 1023 | 262144 | 524288)); - await sender.Send(1, new IdentifyMessage(obj.rpcVersion, hash, 1023 | 262144)); + await sender.Send(1, new IdentifyMessage(obj.RpcVersion, hash, 1023 | 262144)); } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/IdentifiedHandler.cs b/OBS/Socket/Handlers/IdentifiedHandler.cs index a7f6ae1..6fe7a27 100644 --- a/OBS/Socket/Handlers/IdentifiedHandler.cs +++ b/OBS/Socket/Handlers/IdentifiedHandler.cs @@ -20,7 +20,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers return; sender.Connected = true; - Logger.LogInformation("Connected to OBS via rpc version " + obj.negotiatedRpcVersion + "."); + Logger.LogInformation("Connected to OBS via rpc version " + obj.NegotiatedRpcVersion + "."); } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/RequestResponseHandler.cs b/OBS/Socket/Handlers/RequestResponseHandler.cs index bcc1d95..bfb221e 100644 --- a/OBS/Socket/Handlers/RequestResponseHandler.cs +++ b/OBS/Socket/Handlers/RequestResponseHandler.cs @@ -19,13 +19,13 @@ namespace TwitchChatTTS.OBS.Socket.Handlers if (message is not RequestResponseMessage obj || obj == null) return; - switch (obj.requestType) { + switch (obj.RequestType) { case "GetOutputStatus": if (sender is not OBSSocketClient client) return; - if (obj.requestId == "stream") { - client.Live = obj.responseData["outputActive"].ToString() == "True"; + if (obj.RequestId == "stream") { + client.Live = obj.ResponseData["outputActive"].ToString() == "True"; Logger.LogWarning("Updated stream's live status to " + client.Live); } break; diff --git a/OBS/Socket/Manager/OBSHandlerTypeManager.cs b/OBS/Socket/Manager/OBSHandlerTypeManager.cs index 60d1c15..9ac5549 100644 --- a/OBS/Socket/Manager/OBSHandlerTypeManager.cs +++ b/OBS/Socket/Manager/OBSHandlerTypeManager.cs @@ -10,8 +10,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager { public OBSHandlerTypeManager( ILogger factory, - [FromKeyedServices("obs")] HandlerManager handlers + [FromKeyedServices("obs")] HandlerManager handlers ) : base(factory, handlers) { } diff --git a/OBS/Socket/OBSSocketClient.cs b/OBS/Socket/OBSSocketClient.cs index dddf36b..5bc51e6 100644 --- a/OBS/Socket/OBSSocketClient.cs +++ b/OBS/Socket/OBSSocketClient.cs @@ -1,4 +1,3 @@ -using TwitchChatTTS.OBS.Socket.Manager; using CommonSocketLibrary.Common; using CommonSocketLibrary.Abstract; using Microsoft.Extensions.DependencyInjection; diff --git a/Seven/Emotes.cs b/Seven/Emotes.cs index 458ed54..7c0cdce 100644 --- a/Seven/Emotes.cs +++ b/Seven/Emotes.cs @@ -73,7 +73,6 @@ namespace TwitchChatTTS.Seven public IList Emotes { get; set; } public int EmoteCount { get; set; } public int Capacity { get; set; } - } public class Emote { diff --git a/Seven/SevenApiClient.cs b/Seven/SevenApiClient.cs index 4487c20..17c99af 100644 --- a/Seven/SevenApiClient.cs +++ b/Seven/SevenApiClient.cs @@ -1,18 +1,18 @@ using System.Text.Json; using TwitchChatTTS.Helpers; using Microsoft.Extensions.Logging; -using TwitchChatTTS; using TwitchChatTTS.Seven; public class SevenApiClient { + public static readonly string API_URL = "https://7tv.io/v3"; + public static readonly string WEBSOCKET_URL = "wss://events.7tv.io/v3"; + private WebClientWrap Web { get; } - private Configuration Configuration { get; } private ILogger Logger { get; } private long? Id { get; } - public SevenApiClient(Configuration configuration, ILogger logger, TwitchBotToken token) { - Configuration = configuration; + public SevenApiClient(ILogger logger, TwitchBotToken token) { Logger = logger; Id = long.TryParse(token?.BroadcasterId, out long id) ? id : -1; @@ -23,16 +23,16 @@ public class SevenApiClient { } public async Task GetSevenEmotes() { - if (Id is null) + if (Id == null) throw new NullReferenceException(nameof(Id)); try { - var details = await Web.GetJson("https://7tv.io/v3/users/twitch/" + Id); - if (details is null) + var details = await Web.GetJson($"{API_URL}/users/twitch/" + Id); + if (details == null) return null; var emotes = new EmoteDatabase(); - if (details.EmoteSet is not null) + if (details.EmoteSet != null) foreach (var emote in details.EmoteSet.Emotes) emotes.Add(emote.Name, emote.Id); Logger.LogInformation($"Loaded {details.EmoteSet?.Emotes.Count() ?? 0} emotes from 7tv."); diff --git a/Seven/Socket/Context/ReconnectContext.cs b/Seven/Socket/Context/ReconnectContext.cs index f1e6840..19db724 100644 --- a/Seven/Socket/Context/ReconnectContext.cs +++ b/Seven/Socket/Context/ReconnectContext.cs @@ -2,8 +2,6 @@ namespace TwitchChatTTS.Seven.Socket.Context { public class ReconnectContext { - public string? Protocol; - public string Url; public string? SessionId; } } \ No newline at end of file diff --git a/Seven/Socket/Context/SevenHelloContext.cs b/Seven/Socket/Context/SevenHelloContext.cs deleted file mode 100644 index ee0bc9a..0000000 --- a/Seven/Socket/Context/SevenHelloContext.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TwitchChatTTS.Seven.Socket.Context -{ - public class SevenHelloContext - { - public IEnumerable? Subscriptions; - } - - public class SevenSubscriptionConfiguration { - public string? Type; - public IDictionary? Condition; - } -} \ No newline at end of file diff --git a/Seven/Socket/Handlers/DispatchHandler.cs b/Seven/Socket/Handlers/DispatchHandler.cs index 40ec74d..e36cfb1 100644 --- a/Seven/Socket/Handlers/DispatchHandler.cs +++ b/Seven/Socket/Handlers/DispatchHandler.cs @@ -10,12 +10,12 @@ namespace TwitchChatTTS.Seven.Socket.Handlers public class DispatchHandler : IWebSocketHandler { private ILogger Logger { get; } - private IServiceProvider ServiceProvider { get; } + private EmoteDatabase Emotes { get; } public int OperationCode { get; set; } = 0; - public DispatchHandler(ILogger logger, IServiceProvider serviceProvider) { + public DispatchHandler(ILogger logger, EmoteDatabase emotes) { Logger = logger; - ServiceProvider = serviceProvider; + Emotes = emotes; } public async Task Execute(SocketClient sender, Data message) @@ -23,23 +23,31 @@ namespace TwitchChatTTS.Seven.Socket.Handlers if (message is not DispatchMessage obj || obj == null) return; - Do(obj?.Body?.Pulled, cf => cf.OldValue); - Do(obj?.Body?.Pushed, cf => cf.Value); + ApplyChanges(obj?.Body?.Pulled, cf => cf.OldValue, true); + ApplyChanges(obj?.Body?.Pushed, cf => cf.Value, false); } - private void Do(IEnumerable? fields, Func getter) { - if (fields is null) + private void ApplyChanges(IEnumerable? fields, Func getter, bool removing) { + if (fields == null) return; - //ServiceProvider.GetRequiredService() foreach (var val in fields) { - if (getter(val) == null) + var value = getter(val); + if (value == null) continue; - var o = JsonSerializer.Deserialize(val.OldValue.ToString(), new JsonSerializerOptions() { + var o = JsonSerializer.Deserialize(value.ToString(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = false, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); + + if (removing) { + Emotes.Remove(o.Name); + Logger.LogInformation($"Removed 7tv emote: {o.Name} (id: {o.Id})"); + } else { + Emotes.Add(o.Name, o.Id); + Logger.LogInformation($"Added 7tv emote: {o.Name} (id: {o.Id})"); + } } } } diff --git a/Seven/Socket/Handlers/EndOfStreamHandler.cs b/Seven/Socket/Handlers/EndOfStreamHandler.cs index dfe1511..841a35d 100644 --- a/Seven/Socket/Handlers/EndOfStreamHandler.cs +++ b/Seven/Socket/Handlers/EndOfStreamHandler.cs @@ -10,6 +10,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers public class EndOfStreamHandler : IWebSocketHandler { private ILogger Logger { get; } + private Configuration Configuration { get; } private IServiceProvider ServiceProvider { get; } private string[] ErrorCodes { get; } private int[] ReconnectDelay { get; } @@ -17,8 +18,9 @@ namespace TwitchChatTTS.Seven.Socket.Handlers public int OperationCode { get; set; } = 7; - public EndOfStreamHandler(ILogger logger, IServiceProvider serviceProvider) { + public EndOfStreamHandler(ILogger logger, Configuration configuration, IServiceProvider serviceProvider) { Logger = logger; + Configuration = configuration; ServiceProvider = serviceProvider; ErrorCodes = [ @@ -71,17 +73,23 @@ namespace TwitchChatTTS.Seven.Socket.Handlers return; } + if (string.IsNullOrWhiteSpace(Configuration.Seven?.UserId)) + return; + var context = ServiceProvider.GetRequiredService(); await Task.Delay(ReconnectDelay[code]); - Logger.LogInformation($"7tv client reconnecting."); - await sender.ConnectAsync($"{context.Protocol ?? "wss"}://{context.Url}"); - if (context.SessionId is null) { - await sender.Send(33, new object()); + //var base_url = "@" + string.Join(",", Configuration.Seven.SevenId.Select(sub => sub.Type + "<" + string.Join(",", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0]) + ">")); + var base_url = $"@emote_set.*"; + string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}"; + Logger.LogDebug($"7tv websocket reconnecting to {url}."); + + await sender.ConnectAsync(url); + if (context.SessionId != null) { + await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId }); + Logger.LogInformation("Resumed connection to 7tv websocket."); } else { - await sender.Send(34, new ResumeMessage() { - SessionId = context.SessionId - }); + Logger.LogDebug("7tv websocket session id not available."); } } } diff --git a/Seven/Socket/Handlers/SevenHelloHandler.cs b/Seven/Socket/Handlers/SevenHelloHandler.cs index 13b23e7..811c297 100644 --- a/Seven/Socket/Handlers/SevenHelloHandler.cs +++ b/Seven/Socket/Handlers/SevenHelloHandler.cs @@ -1,7 +1,6 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using Microsoft.Extensions.Logging; -using TwitchChatTTS.Seven.Socket.Context; using TwitchChatTTS.Seven.Socket.Data; namespace TwitchChatTTS.Seven.Socket.Handlers @@ -9,12 +8,12 @@ namespace TwitchChatTTS.Seven.Socket.Handlers public class SevenHelloHandler : IWebSocketHandler { private ILogger Logger { get; } - private SevenHelloContext Context { get; } + private Configuration Configuration { get; } public int OperationCode { get; set; } = 1; - public SevenHelloHandler(ILogger logger, SevenHelloContext context) { + public SevenHelloHandler(ILogger logger, Configuration configuration) { Logger = logger; - Context = context; + Configuration = configuration; } public async Task Execute(SocketClient sender, Data message) @@ -27,30 +26,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers seven.Connected = true; seven.ConnectionDetails = obj; - - // if (Context.Subscriptions == null || !Context.Subscriptions.Any()) { - // Logger.LogWarning("No subscriptions have been set for the 7tv websocket client."); - // return; - // } - - //await Task.Delay(TimeSpan.FromMilliseconds(1000)); - //await sender.Send(33, new IdentifyMessage()); - //await Task.Delay(TimeSpan.FromMilliseconds(5000)); - //await sender.SendRaw("{\"op\":35,\"d\":{\"type\":\"emote_set.*\",\"condition\":{\"object_id\":\"64505914b9fc508169ffe7cc\"}}}"); - //await sender.SendRaw(File.ReadAllText("test.txt")); - - // foreach (var sub in Context.Subscriptions) { - // if (string.IsNullOrWhiteSpace(sub.Type)) { - // Logger.LogWarning("Non-existent or empty subscription type found on the 7tv websocket client."); - // continue; - // } - - // Logger.LogDebug($"Subscription Type: {sub.Type} | Condition: {string.Join(", ", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0])}"); - // await sender.Send(35, new SubscribeMessage() { - // Type = sub.Type, - // Condition = sub.Condition - // }); - // } + Logger.LogInformation("Connected to 7tv websockets."); } } } \ No newline at end of file diff --git a/Seven/Socket/Manager/SevenHandlerManager.cs b/Seven/Socket/Managers/SevenHandlerManager.cs similarity index 97% rename from Seven/Socket/Manager/SevenHandlerManager.cs rename to Seven/Socket/Managers/SevenHandlerManager.cs index e7cb97d..8000bb4 100644 --- a/Seven/Socket/Manager/SevenHandlerManager.cs +++ b/Seven/Socket/Managers/SevenHandlerManager.cs @@ -3,7 +3,7 @@ using CommonSocketLibrary.Socket.Manager; using CommonSocketLibrary.Common; using Microsoft.Extensions.DependencyInjection; -namespace TwitchChatTTS.Seven.Socket.Manager +namespace TwitchChatTTS.Seven.Socket.Managers { public class SevenHandlerManager : WebSocketHandlerManager { diff --git a/Seven/Socket/Manager/SevenHandlerTypeManager.cs b/Seven/Socket/Managers/SevenHandlerTypeManager.cs similarity index 92% rename from Seven/Socket/Manager/SevenHandlerTypeManager.cs rename to Seven/Socket/Managers/SevenHandlerTypeManager.cs index c140c10..e99bdc6 100644 --- a/Seven/Socket/Manager/SevenHandlerTypeManager.cs +++ b/Seven/Socket/Managers/SevenHandlerTypeManager.cs @@ -4,7 +4,7 @@ using CommonSocketLibrary.Socket.Manager; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace TwitchChatTTS.Seven.Socket.Manager +namespace TwitchChatTTS.Seven.Socket.Managers { public class SevenHandlerTypeManager : WebSocketHandlerTypeManager { diff --git a/Startup.cs b/Startup.cs index 490a484..8162b8f 100644 --- a/Startup.cs +++ b/Startup.cs @@ -7,38 +7,30 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -using TwitchChatTTS.Twitch; using Microsoft.Extensions.Logging; -using TwitchChatTTS.Seven.Socket.Manager; using TwitchChatTTS.Seven.Socket; using TwitchChatTTS.OBS.Socket.Handlers; using TwitchChatTTS.Seven.Socket.Handlers; using TwitchChatTTS.Seven.Socket.Context; using TwitchChatTTS.Seven; using TwitchChatTTS.OBS.Socket.Context; - -/** -Future handshake/connection procedure: -- GET all tts config data -- Continuous connection to server to receive commands from tom & send logs/errors (med priority, though tough task) - -Ideas: -- Filter messages by badges. -- Speed up TTS based on message queue size? -- Cut TTS off shortly after raid (based on size of raid)? -- Limit duration of TTS -**/ +using TwitchLib.Client.Interfaces; +using TwitchLib.Client; +using TwitchLib.PubSub.Interfaces; +using TwitchLib.PubSub; +using TwitchLib.Communication.Interfaces; +using TwitchChatTTS.Seven.Socket.Managers; +using TwitchChatTTS.Hermes.Socket.Handlers; +using TwitchChatTTS.Hermes.Socket; +using TwitchChatTTS.Hermes.Socket.Managers; +using TwitchChatTTS.Chat.Commands.Parameters; +using TwitchChatTTS.Chat.Commands; +using System.Text.Json; // dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true // SE voices: https://api.streamelements.com/kappa/v2/speech?voice=brian&text=hello -// TODO: -// Fix OBS/7tv websocket connections when not available. -// Make it possible to do things at end of streams. -// Update emote database with twitch emotes. -// Event Subscription for emote usage? - HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); var s = builder.Services; @@ -49,7 +41,7 @@ var deserializer = new DeserializerBuilder() var configContent = File.ReadAllText("tts.config.yml"); var configuration = deserializer.Deserialize(configContent); var redeemKeys = configuration.Twitch?.Redeems?.Keys; -if (redeemKeys is not null) { +if (redeemKeys != null) { foreach (var key in redeemKeys) { if (key != key.ToLower() && configuration.Twitch?.Redeems != null) configuration.Twitch.Redeems.Add(key.ToLower(), configuration.Twitch.Redeems[key]); @@ -58,45 +50,35 @@ if (redeemKeys is not null) { s.AddSingleton(configuration); s.AddLogging(); +s.AddSingleton(new User()); -s.AddSingleton(sp => { - var context = new TTSContext(); - var logger = sp.GetRequiredService>(); - var hermes = sp.GetRequiredService(); - - logger.LogInformation("Fetching TTS username filters..."); - var usernameFiltersList = hermes.FetchTTSUsernameFilters(); - usernameFiltersList.Wait(); - context.UsernameFilters = usernameFiltersList.Result.Where(x => x.Username != null).ToDictionary(x => x.Username ?? "", x => x); - logger.LogInformation($"{context.UsernameFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked."); - logger.LogInformation($"{context.UsernameFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized."); - - var enabledVoices = hermes.FetchTTSEnabledVoices(); - enabledVoices.Wait(); - context.EnabledVoices = enabledVoices.Result; - logger.LogInformation($"{context.EnabledVoices.Count()} TTS voices enabled."); - - var wordFilters = hermes.FetchTTSWordFilters(); - wordFilters.Wait(); - context.WordFilters = wordFilters.Result; - logger.LogInformation($"{context.WordFilters.Count()} TTS word filters."); - - var defaultVoice = hermes.FetchTTSDefaultVoice(); - defaultVoice.Wait(); - context.DefaultVoice = defaultVoice.Result ?? "Brian"; - logger.LogInformation("Default Voice: " + context.DefaultVoice); - - return context; +s.AddSingleton(new JsonSerializerOptions() { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); + +// Command parameters +s.AddKeyedSingleton("parameter-ttsvoicename"); +s.AddKeyedSingleton("parameter-unvalidated"); +s.AddKeyedSingleton("command-skipall"); +s.AddKeyedSingleton("command-skip"); +s.AddKeyedSingleton("command-voice"); +s.AddKeyedSingleton("command-addttsvoice"); +s.AddKeyedSingleton("command-removettsvoice"); +s.AddSingleton(); + s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); -s.AddTransient(sp => { +s.AddSingleton(sp => { var hermes = sp.GetRequiredService(); var task = hermes.FetchTwitchBotToken(); task.Wait(); return task.Result; }); +s.AddTransient(); +s.AddTransient(); +s.AddTransient(); s.AddSingleton(); s.AddSingleton(); @@ -106,14 +88,11 @@ s.AddSingleton(sp => { task.Wait(); return task.Result; }); -var emoteCounter = new EmoteCounter(); -if (!string.IsNullOrWhiteSpace(configuration.Emotes?.CounterFilePath) && File.Exists(configuration.Emotes.CounterFilePath.Trim())) { - var d = new DeserializerBuilder() - .WithNamingConvention(HyphenatedNamingConvention.Instance) - .Build(); - emoteCounter = deserializer.Deserialize(File.ReadAllText(configuration.Emotes.CounterFilePath.Trim())); -} -s.AddSingleton(emoteCounter); +s.AddSingleton(sp => { + if (!string.IsNullOrWhiteSpace(configuration.Emotes?.CounterFilePath) && File.Exists(configuration.Emotes.CounterFilePath.Trim())) + return deserializer.Deserialize(File.ReadAllText(configuration.Emotes.CounterFilePath.Trim())); + return new EmoteCounter(); +}); // OBS websocket s.AddSingleton(sp => @@ -135,34 +114,16 @@ s.AddKeyedSingleton, OBSSocketClient>("obs"); // 7tv websocket s.AddTransient(sp => { var logger = sp.GetRequiredService>(); - var configuration = sp.GetRequiredService(); var client = sp.GetRequiredKeyedService>("7tv") as SevenSocketClient; if (client == null) { - logger.LogError("7tv client is null."); - return new ReconnectContext() { - Protocol = configuration.Seven?.Protocol, - Url = configuration.Seven?.Url, - SessionId = null - }; + logger.LogError("7tv client == null."); + return new ReconnectContext() { SessionId = null }; } if (client.ConnectionDetails == null) { - logger.LogError("Connection details in 7tv client is null."); - return new ReconnectContext() { - Protocol = configuration.Seven?.Protocol, - Url = configuration.Seven?.Url, - SessionId = null - }; + logger.LogError("Connection details in 7tv client == null."); + return new ReconnectContext() { SessionId = null }; } - return new ReconnectContext() { - Protocol = configuration.Seven?.Protocol, - Url = configuration.Seven?.Url, - SessionId = client.ConnectionDetails.SessionId - }; -}); -s.AddSingleton(sp => { - return new SevenHelloContext() { - Subscriptions = configuration.Seven?.InitialSubscriptions - }; + return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId }; }); s.AddKeyedSingleton("7tv-sevenhello"); s.AddKeyedSingleton("7tv-hello"); @@ -175,9 +136,17 @@ s.AddKeyedSingleton, SevenHan s.AddKeyedSingleton, SevenHandlerTypeManager>("7tv"); s.AddKeyedSingleton, SevenSocketClient>("7tv"); +// hermes websocket +s.AddKeyedSingleton("hermes-heartbeat"); +s.AddKeyedSingleton("hermes-loginack"); +s.AddKeyedSingleton("hermes-requestack"); +s.AddKeyedSingleton("hermes-error"); + +s.AddKeyedSingleton, HermesHandlerManager>("hermes"); +s.AddKeyedSingleton, HermesHandlerTypeManager>("hermes"); +s.AddKeyedSingleton, HermesSocketClient>("hermes"); + s.AddHostedService(); using IHost host = builder.Build(); -using IServiceScope scope = host.Services.CreateAsyncScope(); -IServiceProvider provider = scope.ServiceProvider; await host.RunAsync(); \ No newline at end of file diff --git a/TTS.cs b/TTS.cs index 49f4f3c..cc77dc3 100644 --- a/TTS.cs +++ b/TTS.cs @@ -2,11 +2,12 @@ using System.Runtime.InteropServices; using System.Web; using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; +using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using NAudio.Wave; using NAudio.Wave.SampleProviders; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Events; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -15,32 +16,54 @@ namespace TwitchChatTTS { public class TTS : IHostedService { - private ILogger Logger { get; } - private Configuration Configuration { get; } - private TTSPlayer Player { get; } - private IServiceProvider ServiceProvider { get; } - private ISampleProvider? Playing { get; set; } + private readonly ILogger _logger; + private readonly Configuration _configuration; + private readonly TTSPlayer _player; + private readonly IServiceProvider _serviceProvider; public TTS(ILogger logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider) { - Logger = logger; - Configuration = configuration; - Player = player; - ServiceProvider = serviceProvider; + _logger = logger; + _configuration = configuration; + _player = player; + _serviceProvider = serviceProvider; } public async Task StartAsync(CancellationToken cancellationToken) { Console.Title = "TTS - Twitch Chat"; + var user = _serviceProvider.GetRequiredService(); + var hermes = await InitializeHermes(); + + var hermesAccount = await hermes.FetchHermesAccountDetails(); + user.HermesUserId = hermesAccount.Id; + user.TwitchUsername = hermesAccount.Username; + + var twitchBotToken = await hermes.FetchTwitchBotToken(); + user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId); + _logger.LogInformation($"Username: {user.TwitchUsername} (id: {user.TwitchUserId})"); + + user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice(); + _logger.LogInformation("Default Voice: " + user.DefaultTTSVoice); + + var wordFilters = await hermes.FetchTTSWordFilters(); + user.RegexFilters = wordFilters.ToList(); + _logger.LogInformation($"{user.RegexFilters.Count()} TTS word filters."); + + var usernameFilters = await hermes.FetchTTSUsernameFilters(); + user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e); + _logger.LogInformation($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked."); + _logger.LogInformation($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized."); + + var twitchapiclient = await InitializeTwitchApiClient(user.TwitchUsername); + + await InitializeHermesWebsocket(user); await InitializeSevenTv(); await InitializeObs(); try { - var hermes = await InitializeHermes(); - var twitchapiclient = await InitializeTwitchApiClient(hermes); - AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => { - if (e.SampleProvider == Playing) { - Playing = null; + if (e.SampleProvider == _player.Playing) { + _player.Playing = null; } }); @@ -48,11 +71,11 @@ namespace TwitchChatTTS while (true) { try { if (cancellationToken.IsCancellationRequested) { - Logger.LogWarning("TTS Buffer - Cancellation token was canceled."); + _logger.LogWarning("TTS Buffer - Cancellation token was canceled."); return; } - var m = Player.ReceiveBuffer(); + var m = _player.ReceiveBuffer(); if (m == null) { await Task.Delay(200); continue; @@ -63,14 +86,14 @@ namespace TwitchChatTTS var provider = new CachedWavProvider(sound); var data = AudioPlaybackEngine.Instance.ConvertSound(provider); var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate); - Logger.LogDebug("Fetched TTS audio data."); + _logger.LogDebug("Fetched TTS audio data."); m.Audio = resampled; - Player.Ready(m); + _player.Ready(m); } catch (COMException e) { - Logger.LogError(e, "Failed to send request for TTS (HResult: " + e.HResult + ")."); + _logger.LogError(e, "Failed to send request for TTS (HResult: " + e.HResult + ")."); } catch (Exception e) { - Logger.LogError(e, "Failed to send request for TTS."); + _logger.LogError(e, "Failed to send request for TTS."); } } }); @@ -79,40 +102,40 @@ namespace TwitchChatTTS while (true) { try { if (cancellationToken.IsCancellationRequested) { - Logger.LogWarning("TTS Queue - Cancellation token was canceled."); + _logger.LogWarning("TTS Queue - Cancellation token was canceled."); return; } - while (Player.IsEmpty() || Playing != null) { + while (_player.IsEmpty() || _player.Playing != null) { await Task.Delay(200); continue; } - var m = Player.ReceiveReady(); + var m = _player.ReceiveReady(); if (m == null) { continue; } if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File)) { - Logger.LogInformation("Playing message: " + m.File); + _logger.LogInformation("Playing message: " + m.File); AudioPlaybackEngine.Instance.PlaySound(m.File); continue; } - Logger.LogInformation("Playing message: " + m.Message); - Playing = m.Audio; + _logger.LogInformation("Playing message: " + m.Message); + _player.Playing = m.Audio; if (m.Audio != null) AudioPlaybackEngine.Instance.AddMixerInput(m.Audio); } catch (Exception e) { - Logger.LogError(e, "Failed to play a TTS audio message"); + _logger.LogError(e, "Failed to play a TTS audio message"); } } }); StartSavingEmoteCounter(); - Logger.LogInformation("Twitch API client connecting..."); + _logger.LogInformation("Twitch API client connecting..."); await twitchapiclient.Connect(); } catch (Exception e) { - Logger.LogError(e, "Failed to initialize."); + _logger.LogError(e, "Failed to initialize."); } Console.ReadLine(); } @@ -120,75 +143,100 @@ namespace TwitchChatTTS public async Task StopAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) - Logger.LogWarning("Application has stopped due to cancellation token."); + _logger.LogWarning("Application has stopped due to cancellation token."); else - Logger.LogWarning("Application has stopped."); + _logger.LogWarning("Application has stopped."); + } + + private async Task InitializeHermesWebsocket(User user) { + if (_configuration.Hermes?.Token == null) { + _logger.LogDebug("No api token given to hermes. Skipping hermes websockets."); + return; + } + + try { + _logger.LogInformation("Initializing hermes websocket client."); + var hermesClient = _serviceProvider.GetRequiredKeyedService>("hermes") as HermesSocketClient; + var url = "wss://hermes-ws.goblincaves.com"; + _logger.LogDebug($"Attempting to connect to {url}"); + await hermesClient.ConnectAsync(url); + await hermesClient.Send(1, new HermesLoginMessage() { + ApiKey = _configuration.Hermes.Token + }); + + while (hermesClient.UserId == null) + await Task.Delay(TimeSpan.FromMilliseconds(200)); + + await hermesClient.Send(3, new RequestMessage() { + Type = "get_tts_voices", + Data = null + }); + var token = _serviceProvider.GetRequiredService(); + await hermesClient.Send(3, new RequestMessage() { + Type = "get_tts_users", + Data = new Dictionary() { { "@broadcaster", token.BroadcasterId } } + }); + } catch (Exception) { + _logger.LogWarning("Connecting to hermes failed. Skipping hermes websockets."); + } } private async Task InitializeSevenTv() { - Logger.LogInformation("Initializing 7tv client."); - var sevenClient = ServiceProvider.GetRequiredKeyedService>("7tv"); - if (Configuration.Seven is not null && !string.IsNullOrWhiteSpace(Configuration.Seven.Url)) { - var base_url = "@" + string.Join(",", Configuration.Seven.InitialSubscriptions.Select(sub => sub.Type + "<" + string.Join(",", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0]) + ">")); - Logger.LogDebug($"Attempting to connect to {Configuration.Seven.Protocol?.Trim() ?? "wss"}://{Configuration.Seven.Url.Trim()}{base_url}"); - await sevenClient.ConnectAsync($"{Configuration.Seven.Protocol?.Trim() ?? "wss"}://{Configuration.Seven.Url.Trim()}{base_url}"); + if (_configuration.Seven?.UserId == null) { + _logger.LogDebug("No user id given to 7tv. Skipping 7tv websockets."); + return; + } + + try { + _logger.LogInformation("Initializing 7tv websocket client."); + var sevenClient = _serviceProvider.GetRequiredKeyedService>("7tv"); + //var base_url = "@" + string.Join(",", Configuration.Seven.InitialSubscriptions.Select(sub => sub.Type + "<" + string.Join(",", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0]) + ">")); + var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*"; + _logger.LogDebug($"Attempting to connect to {url}"); + await sevenClient.ConnectAsync($"{url}"); + } catch (Exception) { + _logger.LogWarning("Connecting to 7tv failed. Skipping 7tv websockets."); } } private async Task InitializeObs() { - Logger.LogInformation("Initializing obs client."); - var obsClient = ServiceProvider.GetRequiredKeyedService>("obs"); - if (Configuration.Obs is not null && !string.IsNullOrWhiteSpace(Configuration.Obs.Host) && Configuration.Obs.Port.HasValue && Configuration.Obs.Port.Value >= 0) { - Logger.LogDebug($"Attempting to connect to ws://{Configuration.Obs.Host.Trim()}:{Configuration.Obs.Port}"); - await obsClient.ConnectAsync($"ws://{Configuration.Obs.Host.Trim()}:{Configuration.Obs.Port}"); - await Task.Delay(500); + if (_configuration.Obs == null || string.IsNullOrWhiteSpace(_configuration.Obs.Host) || !_configuration.Obs.Port.HasValue || _configuration.Obs.Port.Value < 0) { + _logger.LogDebug("Lacking obs connection info. Skipping obs websockets."); + return; + } + + try { + _logger.LogInformation("Initializing obs websocket client."); + var obsClient = _serviceProvider.GetRequiredKeyedService>("obs"); + var url = $"ws://{_configuration.Obs.Host.Trim()}:{_configuration.Obs.Port}"; + _logger.LogDebug($"Attempting to connect to {url}"); + await obsClient.ConnectAsync(url); + } catch (Exception) { + _logger.LogWarning("Connecting to obs failed. Skipping obs websockets."); } } private async Task InitializeHermes() { // Fetch id and username based on api key given. - Logger.LogInformation("Initializing hermes client."); - var hermes = ServiceProvider.GetRequiredService(); + _logger.LogInformation("Initializing hermes client."); + var hermes = _serviceProvider.GetRequiredService(); await hermes.FetchHermesAccountDetails(); - - if (hermes.Username == null) - throw new Exception("Username fetched from Hermes is invalid."); - - Logger.LogInformation("Username: " + hermes.Username); return hermes; } - private async Task InitializeTwitchApiClient(HermesClient hermes) { - Logger.LogInformation("Initializing twitch client."); - var twitchapiclient = ServiceProvider.GetRequiredService(); + private async Task InitializeTwitchApiClient(string username) { + _logger.LogInformation("Initializing twitch client."); + var twitchapiclient = _serviceProvider.GetRequiredService(); await twitchapiclient.Authorize(); - var channels = Configuration.Twitch?.Channels ?? [hermes.Username]; - Logger.LogInformation("Twitch channels: " + string.Join(", ", channels)); - twitchapiclient.InitializeClient(hermes, channels); + var channels = _configuration.Twitch.Channels ?? [username]; + _logger.LogInformation("Twitch channels: " + string.Join(", ", channels)); + twitchapiclient.InitializeClient(username, channels); twitchapiclient.InitializePublisher(); - var handler = ServiceProvider.GetRequiredService(); + var handler = _serviceProvider.GetRequiredService(); twitchapiclient.AddOnNewMessageReceived(async Task (object? s, OnMessageReceivedArgs e) => { - var result = handler.Handle(e); - - switch (result) { - case MessageResult.Skip: - if (Playing != null) { - AudioPlaybackEngine.Instance.RemoveMixerInput(Playing); - Playing = null; - } - break; - case MessageResult.SkipAll: - Player.RemoveAll(); - if (Playing != null) { - AudioPlaybackEngine.Instance.RemoveMixerInput(Playing); - Playing = null; - } - break; - default: - break; - } + var result = await handler.Handle(e); }); return twitchapiclient; @@ -204,13 +252,13 @@ namespace TwitchChatTTS .WithNamingConvention(HyphenatedNamingConvention.Instance) .Build(); - var chathandler = ServiceProvider.GetRequiredService(); - using (TextWriter writer = File.CreateText(Configuration.Emotes.CounterFilePath.Trim())) + var chathandler = _serviceProvider.GetRequiredService(); + using (TextWriter writer = File.CreateText(_configuration.Emotes.CounterFilePath.Trim())) { - await writer.WriteAsync(serializer.Serialize(chathandler.EmoteCounter)); + await writer.WriteAsync(serializer.Serialize(chathandler._emoteCounter)); } } catch (Exception e) { - Logger.LogError(e, "Failed to save the emote counter."); + _logger.LogError(e, "Failed to save the emote counter."); } } }); diff --git a/Twitch/TTSContext.cs b/Twitch/TTSContext.cs index 0340edf..fc03469 100644 --- a/Twitch/TTSContext.cs +++ b/Twitch/TTSContext.cs @@ -1,12 +1,29 @@ -using TwitchChatTTS.Hermes; +// using System.Text.RegularExpressions; +// using HermesSocketLibrary.Request.Message; +// using TwitchChatTTS.Hermes; -namespace TwitchChatTTS.Twitch -{ - public class TTSContext - { - public string DefaultVoice; - public IEnumerable? EnabledVoices; - public IDictionary? UsernameFilters; - public IEnumerable? WordFilters; - } -} \ No newline at end of file +// namespace TwitchChatTTS.Twitch +// { +// public class TTSContext +// { +// public string DefaultVoice; +// public IEnumerable? EnabledVoices; +// public IDictionary? UsernameFilters; +// public IEnumerable? WordFilters; +// public IList? AvailableVoices { get => _availableVoices; set { _availableVoices = value; EnabledVoicesRegex = GenerateEnabledVoicesRegex(); } } +// public IDictionary? SelectedVoices; +// public Regex? EnabledVoicesRegex; + +// private IList? _availableVoices; + + +// private Regex? GenerateEnabledVoicesRegex() { +// if (AvailableVoices == null || AvailableVoices.Count() <= 0) { +// return null; +// } + +// var enabledVoicesString = string.Join("|", AvailableVoices.Select(v => v.Name)); +// return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase); +// } +// } +// } \ No newline at end of file diff --git a/Twitch/TwitchApiClient.cs b/Twitch/TwitchApiClient.cs index 423e05e..3e2491c 100644 --- a/Twitch/TwitchApiClient.cs +++ b/Twitch/TwitchApiClient.cs @@ -3,137 +3,160 @@ using TwitchChatTTS.Helpers; using Microsoft.Extensions.Logging; using TwitchChatTTS; using TwitchLib.Api.Core.Exceptions; -using TwitchLib.Client; using TwitchLib.Client.Events; using TwitchLib.Client.Models; -using TwitchLib.Communication.Clients; using TwitchLib.Communication.Events; -using TwitchLib.PubSub; using static TwitchChatTTS.Configuration; +using Microsoft.Extensions.DependencyInjection; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using TwitchLib.PubSub.Interfaces; +using TwitchLib.Client.Interfaces; +using TwitchChatTTS.OBS.Socket; public class TwitchApiClient { - private TwitchBotToken Token { get; } - private TwitchClient Client { get; } - private TwitchPubSub Publisher { get; } - private WebClientWrap Web { get; } - private Configuration Configuration { get; } - private ILogger Logger { get; } - private bool Initialized { get; set; } + private readonly Configuration _configuration; + private readonly ILogger _logger; + private readonly TwitchBotToken _token; + private readonly ITwitchClient _client; + private readonly ITwitchPubSub _publisher; + private readonly WebClientWrap Web; + private readonly IServiceProvider _serviceProvider; + private bool Initialized; - public TwitchApiClient(Configuration configuration, ILogger logger, TwitchBotToken token) { - Configuration = configuration; - Logger = logger; - Client = new TwitchClient(new WebSocketClient()); - Publisher = new TwitchPubSub(); + public TwitchApiClient( + Configuration configuration, + ILogger logger, + TwitchBotToken token, + ITwitchClient twitchClient, + ITwitchPubSub twitchPublisher, + IServiceProvider serviceProvider + ) { + _configuration = configuration; + _logger = logger; + _token = token; + _client = twitchClient; + _publisher = twitchPublisher; + _serviceProvider = serviceProvider; Initialized = false; - Token = token; Web = new WebClientWrap(new JsonSerializerOptions() { PropertyNameCaseInsensitive = false, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); - if (!string.IsNullOrWhiteSpace(Configuration.Hermes?.Token)) - Web.AddHeader("x-api-key", Configuration.Hermes?.Token); + if (!string.IsNullOrWhiteSpace(_configuration.Hermes?.Token)) + Web.AddHeader("x-api-key", _configuration.Hermes.Token.Trim()); } public async Task Authorize() { try { var authorize = await Web.GetJson("https://hermes.goblincaves.com/api/account/reauthorize"); - if (authorize != null && Token.BroadcasterId == authorize.BroadcasterId) { - Token.AccessToken = authorize.AccessToken; - Token.RefreshToken = authorize.RefreshToken; - Logger.LogInformation("Updated Twitch API tokens."); + if (authorize != null && _token.BroadcasterId == authorize.BroadcasterId) { + _token.AccessToken = authorize.AccessToken; + _token.RefreshToken = authorize.RefreshToken; + _logger.LogInformation("Updated Twitch API tokens."); } else if (authorize != null) { - Logger.LogError("Twitch API Authorization failed."); + _logger.LogError("Twitch API Authorization failed."); } } catch (HttpResponseException e) { - if (string.IsNullOrWhiteSpace(Configuration.Hermes?.Token)) - Logger.LogError("No Hermes API key found. Enter it into the configuration file."); + if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token)) + _logger.LogError("No Hermes API key found. Enter it into the configuration file."); else - Logger.LogError("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode); + _logger.LogError("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode); } catch (JsonException) { } catch (Exception e) { - Logger.LogError(e, "Failed to authorize to Twitch API."); + _logger.LogError(e, "Failed to authorize to Twitch API."); } } public async Task Connect() { - Client.Connect(); - await Publisher.ConnectAsync(); + _client.Connect(); + await _publisher.ConnectAsync(); } - public void InitializeClient(HermesClient hermes, IEnumerable channels) { - ConnectionCredentials credentials = new ConnectionCredentials(hermes.Username, Token?.AccessToken); - Client.Initialize(credentials, channels.Distinct().ToList()); + public void InitializeClient(string username, IEnumerable channels) { + ConnectionCredentials credentials = new ConnectionCredentials(username, _token?.AccessToken); + _client.Initialize(credentials, channels.Distinct().ToList()); if (Initialized) { - Logger.LogDebug("Twitch API client has already been initialized."); + _logger.LogDebug("Twitch API client has already been initialized."); return; } Initialized = true; - Client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) => { - Logger.LogInformation("Joined channel: " + e.Channel); + _client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) => { + _logger.LogInformation("Joined channel: " + e.Channel); }; - Client.OnConnected += async Task (object? s, OnConnectedArgs e) => { - Logger.LogInformation("-----------------------------------------------------------"); + _client.OnConnected += async Task (object? s, OnConnectedArgs e) => { + _logger.LogInformation("-----------------------------------------------------------"); }; - Client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => { - Logger.LogError(e.Exception, "Incorrect Login on Twitch API client."); + _client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => { + _logger.LogError(e.Exception, "Incorrect Login on Twitch API client."); - Logger.LogInformation("Attempting to re-authorize."); + _logger.LogInformation("Attempting to re-authorize."); await Authorize(); }; - Client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => { - Logger.LogError("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")"); + _client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => { + _logger.LogError("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")"); + + _logger.LogInformation("Attempting to re-authorize."); + await Authorize(); }; - Client.OnError += async Task (object? s, OnErrorEventArgs e) => { - Logger.LogError(e.Exception, "Twitch API client error."); + _client.OnError += async Task (object? s, OnErrorEventArgs e) => { + _logger.LogError(e.Exception, "Twitch API client error."); }; } public void InitializePublisher() { - Publisher.OnPubSubServiceConnected += async (s, e) => { - Publisher.ListenToChannelPoints(Token.BroadcasterId); - Publisher.ListenToFollows(Token.BroadcasterId); + _publisher.OnPubSubServiceConnected += async (s, e) => { + _publisher.ListenToChannelPoints(_token.BroadcasterId); + _publisher.ListenToFollows(_token.BroadcasterId); - await Publisher.SendTopicsAsync(Token.AccessToken); - Logger.LogInformation("Twitch PubSub has been connected."); + await _publisher.SendTopicsAsync(_token.AccessToken); + _logger.LogInformation("Twitch PubSub has been connected."); }; - Publisher.OnFollow += (s, e) => { - Logger.LogInformation("Follow: " + e.DisplayName); + _publisher.OnFollow += (s, e) => { + var client = _serviceProvider.GetRequiredKeyedService>("obs") as OBSSocketClient; + if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false) + return; + + _logger.LogInformation("Follow: " + e.DisplayName); }; - Publisher.OnChannelPointsRewardRedeemed += (s, e) => { - Logger.LogInformation($"Channel Point Reward Redeemed: {e.RewardRedeemed.Redemption.Reward.Title} (id: {e.RewardRedeemed.Redemption.Id})"); + _publisher.OnChannelPointsRewardRedeemed += (s, e) => { + var client = _serviceProvider.GetRequiredKeyedService>("obs") as OBSSocketClient; + if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false) + return; - if (Configuration.Twitch?.Redeems is null) { - Logger.LogDebug("No redeems found in the configuration."); + _logger.LogInformation($"Channel Point Reward Redeemed: {e.RewardRedeemed.Redemption.Reward.Title} (id: {e.RewardRedeemed.Redemption.Id})"); + + if (_configuration.Twitch?.Redeems == null) { + _logger.LogDebug("No redeems found in the configuration."); return; } var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-"); - if (!Configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem)) + if (!_configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem)) return; - if (redeem is null) + if (redeem == null) return; // Write or append to file if needed. var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim(); - if (outputFile is null) { - Logger.LogDebug($"No output file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); + if (outputFile == null) { + _logger.LogDebug($"No output file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); } else { var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n"); - if (outputContent is null) { - Logger.LogWarning($"No output content was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); + if (outputContent == null) { + _logger.LogWarning($"No output content was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); } else { if (redeem.OutputAppend == true) { File.AppendAllText(outputFile, outputContent + "\n"); @@ -145,12 +168,12 @@ public class TwitchApiClient { // Play audio file if needed. var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim(); - if (audioFile is null) { - Logger.LogDebug($"No audio file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); + if (audioFile == null) { + _logger.LogDebug($"No audio file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); return; } if (!File.Exists(audioFile)) { - Logger.LogWarning($"Cannot find audio file @ {audioFile} for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); + _logger.LogWarning($"Cannot find audio file @ {audioFile} for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); return; } @@ -179,6 +202,6 @@ public class TwitchApiClient { } public void AddOnNewMessageReceived(AsyncEventHandler handler) { - Client.OnMessageReceived += handler; + _client.OnMessageReceived += handler; } } \ No newline at end of file diff --git a/TwitchChatTTS.csproj b/TwitchChatTTS.csproj index ef5ad9f..83eda40 100644 --- a/TwitchChatTTS.csproj +++ b/TwitchChatTTS.csproj @@ -35,5 +35,6 @@ + diff --git a/User.cs b/User.cs new file mode 100644 index 0000000..a61839d --- /dev/null +++ b/User.cs @@ -0,0 +1,36 @@ +using System.Text.RegularExpressions; +using TwitchChatTTS.Hermes; + +namespace TwitchChatTTS +{ + public class User + { + // Hermes user id + public string HermesUserId { get; set; } + public long TwitchUserId { get; set; } + public string TwitchUsername { get; set; } + + public string DefaultTTSVoice { get; set; } + // voice id -> voice name + public IDictionary VoicesAvailable { get; set; } + // chatter/twitch id -> voice name + public IDictionary VoicesSelected { get; set; } + public HashSet VoicesEnabled { get; set; } + + public IDictionary ChatterFilters { get; set; } + public IList RegexFilters { get; set; } + + + public User() { + + } + + public Regex? GenerateEnabledVoicesRegex() { + if (VoicesAvailable == null || VoicesAvailable.Count() <= 0) + return null; + + var enabledVoicesString = string.Join("|", VoicesAvailable.Select(v => v.Value)); + return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase); + } + } +} \ No newline at end of file