From 706eecf2d2cc63deb7d5f5befe6c41812ad50210 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 24 Jun 2024 22:11:36 +0000 Subject: [PATCH] Revised the redeem system, activated via channel point redeems. Added OBS transformation to redeems. Logs changed & writes to logs folder as well. Removed most use of IServiceProvider. --- Chat/ChatMessageHandler.cs | 59 ++--- Chat/Commands/AddTTSVoiceCommand.cs | 24 +- Chat/Commands/ChatCommandManager.cs | 17 +- Chat/Commands/OBSCommand.cs | 67 +++--- .../Parameters/TTSVoiceNameParameter.cs | 12 +- Chat/Commands/RefreshTTSDataCommand.cs | 40 ++-- Chat/Commands/RemoveTTSVoiceCommand.cs | 25 +-- Chat/Commands/SkipAllCommand.cs | 18 +- Chat/Commands/SkipCommand.cs | 16 +- Chat/Commands/TTSCommand.cs | 39 ++-- Chat/Commands/VoiceCommand.cs | 25 +-- Chat/Speech/AudioPlaybackEngine.cs | 6 +- Chat/Speech/TTSPlayer.cs | 8 +- Configuration.cs | 8 - Helpers/WebClientWrap.cs | 8 +- Hermes/HermesClient.cs | 25 ++- Hermes/Socket/Handlers/HeartbeatHandler.cs | 10 +- Hermes/Socket/Handlers/LoginAckHandler.cs | 24 +- Hermes/Socket/Handlers/RequestAckHandler.cs | 62 +++--- Hermes/Socket/HermesSocketClient.cs | 2 +- .../Socket/Managers/HermesHandlerManager.cs | 5 +- OBS/Socket/Data/OBSAlignment.cs | 8 + OBS/Socket/Data/TransformationData.cs | 24 ++ OBS/Socket/Handlers/EventMessageHandler.cs | 20 +- OBS/Socket/Handlers/HelloHandler.cs | 24 +- OBS/Socket/Handlers/IdentifiedHandler.cs | 20 +- .../Handlers/RequestBatchResponseHandler.cs | 72 +++--- OBS/Socket/Handlers/RequestResponseHandler.cs | 98 ++++++-- OBS/Socket/Manager/OBSBatchRequestManager.cs | 60 ----- OBS/Socket/Manager/OBSHandlerManager.cs | 2 +- OBS/Socket/Manager/OBSManager.cs | 210 ++++++++++++++++++ Seven/SevenApiClient.cs | 23 +- Seven/Socket/Data/ErrorMessage.cs | 2 +- Seven/Socket/Handlers/DispatchHandler.cs | 41 ++-- Seven/Socket/Handlers/EndOfStreamHandler.cs | 65 +++--- Seven/Socket/Handlers/ErrorHandler.cs | 10 +- Seven/Socket/Handlers/ReconnectHandler.cs | 12 +- Seven/Socket/Handlers/SevenHelloHandler.cs | 18 +- Seven/Socket/Managers/SevenHandlerManager.cs | 4 +- Startup.cs | 30 ++- TTS.cs | 44 +++- Twitch/Redemptions/Action.cs | 9 + Twitch/Redemptions/Redemption.cs | 11 + Twitch/Redemptions/RedemptionManager.cs | 148 ++++++++++++ Twitch/TwitchApiClient.cs | 86 ++++--- 45 files changed, 964 insertions(+), 577 deletions(-) create mode 100644 OBS/Socket/Data/OBSAlignment.cs create mode 100644 OBS/Socket/Data/TransformationData.cs delete mode 100644 OBS/Socket/Manager/OBSBatchRequestManager.cs create mode 100644 OBS/Socket/Manager/OBSManager.cs create mode 100644 Twitch/Redemptions/Action.cs create mode 100644 Twitch/Redemptions/Redemption.cs create mode 100644 Twitch/Redemptions/RedemptionManager.cs diff --git a/Chat/ChatMessageHandler.cs b/Chat/ChatMessageHandler.cs index 6d5a210..919a311 100644 --- a/Chat/ChatMessageHandler.cs +++ b/Chat/ChatMessageHandler.cs @@ -14,14 +14,14 @@ using HermesSocketLibrary.Socket.Data; public class ChatMessageHandler { - private ILogger _logger { get; } - private Configuration _configuration { get; } - private EmoteDatabase _emotes { get; } - private TTSPlayer _player { get; } - private ChatCommandManager _commands { get; } - private OBSSocketClient? _obsClient { get; } - private HermesSocketClient? _hermesClient { get; } - private IServiceProvider _serviceProvider { get; } + private readonly User _user; + private readonly Configuration _configuration; + private readonly EmoteDatabase _emotes; + private readonly TTSPlayer _player; + private readonly ChatCommandManager _commands; + private readonly OBSSocketClient? _obsClient; + private readonly HermesSocketClient? _hermesClient; + private readonly ILogger _logger; private Regex sfxRegex; private HashSet _chatters; @@ -30,23 +30,23 @@ public class ChatMessageHandler public ChatMessageHandler( + User user, TTSPlayer player, ChatCommandManager commands, EmoteDatabase emotes, [FromKeyedServices("obs")] SocketClient obsClient, [FromKeyedServices("hermes")] SocketClient hermesClient, Configuration configuration, - IServiceProvider serviceProvider, ILogger logger ) { + _user = user; _player = player; _commands = commands; _emotes = emotes; _obsClient = obsClient as OBSSocketClient; _hermesClient = hermesClient as HermesSocketClient; _configuration = configuration; - _serviceProvider = serviceProvider; _logger = logger; _chatters = null; @@ -61,13 +61,12 @@ public class ChatMessageHandler if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient.Live == false) return new MessageResult(MessageStatus.NotReady, -1, -1); - var user = _serviceProvider.GetRequiredService(); var m = e.ChatMessage; var msg = e.ChatMessage.Message; var chatterId = long.Parse(m.UserId); var tasks = new List(); - var blocked = user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted"; + var blocked = _user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted"; if (!blocked || m.IsBroadcaster) { try @@ -78,7 +77,7 @@ public class ChatMessageHandler } catch (Exception ex) { - _logger.Error(ex, "Failed at executing command."); + _logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][cid: {m.UserId}][mid: {m.Id}]"); } } @@ -108,13 +107,9 @@ public class ChatMessageHandler foreach (var w in words) { if (wordCounter.ContainsKey(w)) - { wordCounter[w]++; - } else - { wordCounter.Add(w, 1); - } var emoteId = _emotes.Get(w); if (emoteId == null) @@ -143,9 +138,9 @@ public class ChatMessageHandler msg = filteredMsg; // Replace filtered words. - if (user.RegexFilters != null) + if (_user.RegexFilters != null) { - foreach (var wf in user.RegexFilters) + foreach (var wf in _user.RegexFilters) { if (wf.Search == null || wf.Replace == null) continue; @@ -171,48 +166,36 @@ public class ChatMessageHandler // Determine the priority of this message int priority = 0; if (m.IsStaff) - { priority = int.MinValue; - } else if (filter?.Tag == "priority") - { priority = int.MinValue + 1; - } else if (m.IsModerator) - { priority = -100; - } else if (m.IsVip) - { priority = -10; - } else if (m.IsPartner) - { priority = -5; - } else if (m.IsHighlighted) - { priority = -1; - } priority = Math.Min(priority, -m.SubscribedMonthCount * (m.IsSubscriber ? 2 : 1)); // Determine voice selected. - string voiceSelected = user.DefaultTTSVoice; - if (long.TryParse(e.ChatMessage.UserId, out long userId) && user.VoicesSelected?.ContainsKey(userId) == true) + string voiceSelected = _user.DefaultTTSVoice; + if (long.TryParse(e.ChatMessage.UserId, out long userId) && _user.VoicesSelected?.ContainsKey(userId) == true) { - var voiceId = user.VoicesSelected[userId]; - if (user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null) + var voiceId = _user.VoicesSelected[userId]; + if (_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null) { voiceSelected = voiceName; } } // Determine additional voices used - var matches = user.WordFilterRegex?.Matches(msg).ToArray(); + var matches = _user.WordFilterRegex?.Matches(msg).ToArray(); if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0) { HandlePartialMessage(priority, voiceSelected, msg.Trim(), e); - return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed); + return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed); } HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e); @@ -230,7 +213,7 @@ public class ChatMessageHandler if (tasks.Any()) await Task.WhenAll(tasks); - return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed); + return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed); } private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e) diff --git a/Chat/Commands/AddTTSVoiceCommand.cs b/Chat/Commands/AddTTSVoiceCommand.cs index 2694b3c..6fef20b 100644 --- a/Chat/Commands/AddTTSVoiceCommand.cs +++ b/Chat/Commands/AddTTSVoiceCommand.cs @@ -10,16 +10,19 @@ namespace TwitchChatTTS.Chat.Commands { public class AddTTSVoiceCommand : ChatCommand { - private IServiceProvider _serviceProvider; - private ILogger _logger; + private readonly User _user; + private readonly SocketClient _hermesClient; + private readonly ILogger _logger; public AddTTSVoiceCommand( + User user, [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, - IServiceProvider serviceProvider, + [FromKeyedServices("hermes")] SocketClient hermesClient, ILogger logger ) : base("addttsvoice", "Select a TTS voice as the default for that user.") { - _serviceProvider = serviceProvider; + _user = user; + _hermesClient = hermesClient; _logger = logger; AddParameter(ttsVoiceParameter); @@ -32,25 +35,24 @@ namespace TwitchChatTTS.Chat.Commands public override async Task Execute(IList args, ChatMessage message, long broadcasterId) { - var client = _serviceProvider.GetRequiredKeyedService>("hermes"); - if (client == null) + //var HermesClient = _serviceProvider.GetRequiredKeyedService>("hermes"); + if (_hermesClient == null) return; - var context = _serviceProvider.GetRequiredService(); - if (context == null || context.VoicesAvailable == null) + if (_user == null || _user.VoicesAvailable == null) return; var voiceName = args.First(); var voiceNameLower = voiceName.ToLower(); - var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower); + var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower); if (exists) return; - await client.Send(3, new RequestMessage() + await _hermesClient.Send(3, new RequestMessage() { Type = "create_tts_voice", Data = new Dictionary() { { "voice", voiceName } } }); - _logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}."); + _logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]"); } } } \ No newline at end of file diff --git a/Chat/Commands/ChatCommandManager.cs b/Chat/Commands/ChatCommandManager.cs index b5ead0a..feccd82 100644 --- a/Chat/Commands/ChatCommandManager.cs +++ b/Chat/Commands/ChatCommandManager.cs @@ -7,9 +7,9 @@ namespace TwitchChatTTS.Chat.Commands public class ChatCommandManager { private IDictionary _commands; - private TwitchBotAuth _token; - private IServiceProvider _serviceProvider; - private ILogger _logger; + private readonly TwitchBotAuth _token; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; private string CommandStartSign { get; } = "!"; @@ -44,11 +44,11 @@ namespace TwitchChatTTS.Chat.Commands var command = _serviceProvider.GetKeyedService(key); if (command == null) { - _logger.Error("Failed to add command: " + type.AssemblyQualifiedName); + _logger.Error("Failed to add chat command: " + type.AssemblyQualifiedName); continue; } - _logger.Debug($"Added command {type.AssemblyQualifiedName}."); + _logger.Debug($"Added chat command {type.AssemblyQualifiedName}"); Add(command); } } @@ -72,19 +72,20 @@ namespace TwitchChatTTS.Chat.Commands if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null) { - _logger.Debug($"Failed to find command named '{com}'."); + // Could be for another bot or just misspelled. + _logger.Debug($"Failed to find command named '{com}' [args: {arg}][chatter: {message.Username}][cid: {message.UserId}]"); return ChatCommandResult.Missing; } if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != "126224566" && !message.IsStaff) { - _logger.Warning($"Chatter is missing permission to execute command named '{com}'."); + _logger.Warning($"Chatter is missing permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][cid: {message.UserId}]"); return ChatCommandResult.Permission; } if (command.Parameters.Count(p => !p.Optional) > args.Length) { - _logger.Warning($"Command syntax issue when executing command named '{com}' with the following args: {string.Join(" ", args)}"); + _logger.Warning($"Command syntax issue when executing command named '{com}' [args: {arg}][chatter: {message.Username}][cid: {message.UserId}]"); return ChatCommandResult.Syntax; } diff --git a/Chat/Commands/OBSCommand.cs b/Chat/Commands/OBSCommand.cs index 457eebf..65ba80e 100644 --- a/Chat/Commands/OBSCommand.cs +++ b/Chat/Commands/OBSCommand.cs @@ -1,25 +1,30 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; -using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchChatTTS.Chat.Commands.Parameters; +using TwitchChatTTS.OBS.Socket.Data; +using TwitchChatTTS.OBS.Socket.Manager; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands { public class OBSCommand : ChatCommand { - private IServiceProvider _serviceProvider; - private ILogger _logger; + private readonly User _user; + private readonly OBSManager _manager; + private readonly ILogger _logger; public OBSCommand( [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter, - IServiceProvider serviceProvider, + User user, + OBSManager manager, + [FromKeyedServices("obs")] SocketClient hermesClient, ILogger logger ) : base("obs", "Various obs commands.") { - _serviceProvider = serviceProvider; + _user = user; + _manager = manager; _logger = logger; AddParameter(unvalidatedParameter); @@ -32,50 +37,34 @@ namespace TwitchChatTTS.Chat.Commands public override async Task Execute(IList args, ChatMessage message, long broadcasterId) { - var client = _serviceProvider.GetRequiredKeyedService>("obs"); - if (client == null) - return; - var context = _serviceProvider.GetRequiredService(); - if (context == null || context.VoicesAvailable == null) + if (_user == null || _user.VoicesAvailable == null) return; var voiceName = args[0].ToLower(); - var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; + var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; var action = args[1].ToLower(); - switch (action) { + switch (action) + { case "sleep": - await client.Send(8, new RequestMessage() - { - Type = "Sleep", - Data = new Dictionary() { { "requestId", "siduhsidasd" }, { "sleepMillis", 10000 } } - }); - break; + await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary() { { "sleepMillis", 10000 } })); + break; case "get_scene_item_id": - await client.Send(6, new RequestMessage() - { - Type = "GetSceneItemId", - Data = new Dictionary() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" }, { "rotation", 90 } } - }); - break; + await _manager.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" }, { "rotation", 90 } })); + break; case "transform": - await client.Send(6, new RequestMessage() + await _manager.UpdateTransformation(args[1], args[2], (d) => { - Type = "Transform", - Data = new Dictionary() { { "sceneName", "Generic" }, { "sceneItemId", 90 }, { "rotation", 90 } } - }); - break; - case "remove": - await client.Send(3, new RequestMessage() - { - Type = "delete_tts_voice", - Data = new Dictionary() { { "voice", voiceId } } - }); - break; - } - - _logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}."); + }); + await _manager.Send(new RequestMessage("Transform", string.Empty, new Dictionary() { { "sceneName", "Generic" }, { "sceneItemId", 90 }, { "rotation", 90 } })); + break; + case "remove": + await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary() { { "sleepMillis", 10000 } })); + break; + default: + break; + } } } } \ No newline at end of file diff --git a/Chat/Commands/Parameters/TTSVoiceNameParameter.cs b/Chat/Commands/Parameters/TTSVoiceNameParameter.cs index 672c24c..42a7189 100644 --- a/Chat/Commands/Parameters/TTSVoiceNameParameter.cs +++ b/Chat/Commands/Parameters/TTSVoiceNameParameter.cs @@ -1,23 +1,21 @@ -using Microsoft.Extensions.DependencyInjection; namespace TwitchChatTTS.Chat.Commands.Parameters { public class TTSVoiceNameParameter : ChatCommandParameter { - private IServiceProvider _serviceProvider; + private readonly User _user; - public TTSVoiceNameParameter(IServiceProvider serviceProvider, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional) + public TTSVoiceNameParameter(User user, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional) { - _serviceProvider = serviceProvider; + _user = user; } public override bool Validate(string value) { - var user = _serviceProvider.GetRequiredService(); - if (user.VoicesAvailable == null) + if (_user.VoicesAvailable == null) return false; value = value.ToLower(); - return user.VoicesAvailable.Any(e => e.Value.ToLower() == value); + return _user.VoicesAvailable.Any(e => e.Value.ToLower() == value); } } } \ No newline at end of file diff --git a/Chat/Commands/RefreshTTSDataCommand.cs b/Chat/Commands/RefreshTTSDataCommand.cs index 5f16137..0acfe0a 100644 --- a/Chat/Commands/RefreshTTSDataCommand.cs +++ b/Chat/Commands/RefreshTTSDataCommand.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchLib.Client.Models; @@ -6,13 +5,15 @@ namespace TwitchChatTTS.Chat.Commands { public class RefreshTTSDataCommand : ChatCommand { - private IServiceProvider _serviceProvider; - private ILogger _logger; + private readonly User _user; + private readonly HermesApiClient _hermesApi; + private readonly ILogger _logger; - public RefreshTTSDataCommand(IServiceProvider serviceProvider, ILogger logger) + public RefreshTTSDataCommand(User user, HermesApiClient hermesApi, ILogger logger) : base("refresh", "Refreshes certain TTS related data on the client.") { - _serviceProvider = serviceProvider; + _user = user; + _hermesApi = hermesApi; _logger = logger; } @@ -23,37 +24,34 @@ namespace TwitchChatTTS.Chat.Commands public override async Task Execute(IList args, ChatMessage message, long broadcasterId) { - var user = _serviceProvider.GetRequiredService(); var service = args.FirstOrDefault(); if (service == null) return; - var hermes = _serviceProvider.GetRequiredService(); - switch (service) { case "tts_voice_enabled": - var voicesEnabled = await hermes.FetchTTSEnabledVoices(); + var voicesEnabled = await _hermesApi.FetchTTSEnabledVoices(); if (voicesEnabled == null || !voicesEnabled.Any()) - user.VoicesEnabled = new HashSet(new string[] { "Brian" }); + _user.VoicesEnabled = new HashSet(["Brian"]); else - user.VoicesEnabled = new HashSet(voicesEnabled.Select(v => v)); - _logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled."); + _user.VoicesEnabled = new HashSet(voicesEnabled.Select(v => v)); + _logger.Information($"{_user.VoicesEnabled.Count} TTS voices have been enabled."); break; case "word_filters": - var wordFilters = await hermes.FetchTTSWordFilters(); - user.RegexFilters = wordFilters.ToList(); - _logger.Information($"{user.RegexFilters.Count()} TTS word filters."); + var wordFilters = await _hermesApi.FetchTTSWordFilters(); + _user.RegexFilters = wordFilters.ToList(); + _logger.Information($"{_user.RegexFilters.Count()} TTS word filters."); break; case "username_filters": - var usernameFilters = await hermes.FetchTTSUsernameFilters(); - user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e); - _logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked."); - _logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized."); + var usernameFilters = await _hermesApi.FetchTTSUsernameFilters(); + _user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e); + _logger.Information($"{_user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked."); + _logger.Information($"{_user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized."); break; case "default_voice": - user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice(); - _logger.Information("Default Voice: " + user.DefaultTTSVoice); + _user.DefaultTTSVoice = await _hermesApi.FetchTTSDefaultVoice(); + _logger.Information("Default Voice: " + _user.DefaultTTSVoice); break; } } diff --git a/Chat/Commands/RemoveTTSVoiceCommand.cs b/Chat/Commands/RemoveTTSVoiceCommand.cs index f14662a..73aa0f7 100644 --- a/Chat/Commands/RemoveTTSVoiceCommand.cs +++ b/Chat/Commands/RemoveTTSVoiceCommand.cs @@ -10,16 +10,19 @@ namespace TwitchChatTTS.Chat.Commands { public class RemoveTTSVoiceCommand : ChatCommand { - private IServiceProvider _serviceProvider; + private readonly User _user; + private readonly SocketClient _hermesClient; private ILogger _logger; public RemoveTTSVoiceCommand( [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, - IServiceProvider serviceProvider, + User user, + [FromKeyedServices("hermes")] SocketClient hermesClient, ILogger logger ) : base("removettsvoice", "Select a TTS voice as the default for that user.") { - _serviceProvider = serviceProvider; + _user = user; + _hermesClient = hermesClient; _logger = logger; AddParameter(ttsVoiceParameter); @@ -27,30 +30,26 @@ namespace TwitchChatTTS.Chat.Commands public override async Task CheckPermissions(ChatMessage message, long broadcasterId) { - return message.IsModerator || message.IsBroadcaster; + return message.IsBroadcaster; } 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) + if (_user == null || _user.VoicesAvailable == null) return; var voiceName = args.First().ToLower(); - var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName); + var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName); if (!exists) return; - var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; - await client.Send(3, new RequestMessage() + var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; + await _hermesClient.Send(3, new RequestMessage() { Type = "delete_tts_voice", Data = new Dictionary() { { "voice", voiceId } } }); - _logger.Information($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}."); + _logger.Information($"Deleted a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]"); } } } \ No newline at end of file diff --git a/Chat/Commands/SkipAllCommand.cs b/Chat/Commands/SkipAllCommand.cs index c7b4c13..bad77d3 100644 --- a/Chat/Commands/SkipAllCommand.cs +++ b/Chat/Commands/SkipAllCommand.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchLib.Client.Models; @@ -6,13 +5,13 @@ namespace TwitchChatTTS.Chat.Commands { public class SkipAllCommand : ChatCommand { - private IServiceProvider _serviceProvider; - private ILogger _logger; + private readonly TTSPlayer _ttsPlayer; + private readonly ILogger _logger; - public SkipAllCommand(IServiceProvider serviceProvider, ILogger logger) + public SkipAllCommand(TTSPlayer ttsPlayer, ILogger logger) : base("skipall", "Skips all text to speech messages in queue and playing.") { - _serviceProvider = serviceProvider; + _ttsPlayer = ttsPlayer; _logger = logger; } @@ -23,14 +22,13 @@ namespace TwitchChatTTS.Chat.Commands public override async Task Execute(IList args, ChatMessage message, long broadcasterId) { - var player = _serviceProvider.GetRequiredService(); - player.RemoveAll(); + _ttsPlayer.RemoveAll(); - if (player.Playing == null) + if (_ttsPlayer.Playing == null) return; - AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing); - player.Playing = null; + AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing); + _ttsPlayer.Playing = null; _logger.Information("Skipped all queued and playing tts."); } diff --git a/Chat/Commands/SkipCommand.cs b/Chat/Commands/SkipCommand.cs index ebbb52e..5206ef8 100644 --- a/Chat/Commands/SkipCommand.cs +++ b/Chat/Commands/SkipCommand.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchLib.Client.Models; @@ -6,13 +5,13 @@ namespace TwitchChatTTS.Chat.Commands { public class SkipCommand : ChatCommand { - private IServiceProvider _serviceProvider; - private ILogger _logger; + private readonly TTSPlayer _ttsPlayer; + private readonly ILogger _logger; - public SkipCommand(IServiceProvider serviceProvider, ILogger logger) + public SkipCommand(TTSPlayer ttsPlayer, ILogger logger) : base("skip", "Skips the current text to speech message.") { - _serviceProvider = serviceProvider; + _ttsPlayer = ttsPlayer; _logger = logger; } @@ -23,12 +22,11 @@ namespace TwitchChatTTS.Chat.Commands public override async Task Execute(IList args, ChatMessage message, long broadcasterId) { - var player = _serviceProvider.GetRequiredService(); - if (player.Playing == null) + if (_ttsPlayer.Playing == null) return; - AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing); - player.Playing = null; + AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing); + _ttsPlayer.Playing = null; _logger.Information("Skipped current tts."); } diff --git a/Chat/Commands/TTSCommand.cs b/Chat/Commands/TTSCommand.cs index 435723b..b95e7eb 100644 --- a/Chat/Commands/TTSCommand.cs +++ b/Chat/Commands/TTSCommand.cs @@ -10,17 +10,20 @@ namespace TwitchChatTTS.Chat.Commands { public class TTSCommand : ChatCommand { - private IServiceProvider _serviceProvider; - private ILogger _logger; + private readonly User _user; + private readonly SocketClient _hermesClient; + private readonly ILogger _logger; public TTSCommand( [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter, - IServiceProvider serviceProvider, + User user, + [FromKeyedServices("hermes")] SocketClient hermesClient, ILogger logger ) : base("tts", "Various tts commands.") { - _serviceProvider = serviceProvider; + _user = user; + _hermesClient = hermesClient; _logger = logger; AddParameter(ttsVoiceParameter); @@ -29,48 +32,44 @@ namespace TwitchChatTTS.Chat.Commands public override async Task CheckPermissions(ChatMessage message, long broadcasterId) { - return message.IsModerator || message.IsBroadcaster; + return message.IsBroadcaster; } 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) + if (_user == null || _user.VoicesAvailable == null) return; var voiceName = args[0].ToLower(); - var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; + var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; var action = args[1].ToLower(); - switch (action) { + switch (action) + { case "enable": - await client.Send(3, new RequestMessage() + await _hermesClient.Send(3, new RequestMessage() { Type = "update_tts_voice_state", Data = new Dictionary() { { "voice", voiceId }, { "state", true } } }); - break; + break; case "disable": - await client.Send(3, new RequestMessage() + await _hermesClient.Send(3, new RequestMessage() { Type = "update_tts_voice_state", Data = new Dictionary() { { "voice", voiceId }, { "state", false } } }); - break; + break; case "remove": - await client.Send(3, new RequestMessage() + await _hermesClient.Send(3, new RequestMessage() { Type = "delete_tts_voice", Data = new Dictionary() { { "voice", voiceId } } }); - break; + break; } - - _logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}."); + _logger.Information($"Added a new TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]"); } } } \ No newline at end of file diff --git a/Chat/Commands/VoiceCommand.cs b/Chat/Commands/VoiceCommand.cs index 8e23217..d88074f 100644 --- a/Chat/Commands/VoiceCommand.cs +++ b/Chat/Commands/VoiceCommand.cs @@ -10,16 +10,19 @@ namespace TwitchChatTTS.Chat.Commands { public class VoiceCommand : ChatCommand { - private IServiceProvider _serviceProvider; - private ILogger _logger; + private readonly User _user; + private readonly SocketClient _hermesClient; + private readonly ILogger _logger; public VoiceCommand( [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, - IServiceProvider serviceProvider, + User user, + [FromKeyedServices("hermes")] SocketClient hermesClient, ILogger logger ) : base("voice", "Select a TTS voice as the default for that user.") { - _serviceProvider = serviceProvider; + _user = user; + _hermesClient = hermesClient; _logger = logger; AddParameter(ttsVoiceParameter); @@ -32,23 +35,19 @@ namespace TwitchChatTTS.Chat.Commands 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) + if (_user == null || _user.VoicesSelected == null || _user.VoicesAvailable == null) return; long chatterId = long.Parse(message.UserId); var voiceName = args.First().ToLower(); - var voice = context.VoicesAvailable.First(v => v.Value.ToLower() == voiceName); + var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName); - await client.Send(3, new RequestMessage() + await _hermesClient.Send(3, new RequestMessage() { - Type = context.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user", + Type = _user.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user", Data = new Dictionary() { { "chatter", chatterId }, { "voice", voice.Key } } }); - _logger.Information($"Updated {message.Username}'s [id: {chatterId}] tts voice to {voice.Value} (id: {voice.Key})."); + _logger.Information($"Updated chat TTS voice [voice: {voice.Value}][username: {message.Username}]."); } } } \ No newline at end of file diff --git a/Chat/Speech/AudioPlaybackEngine.cs b/Chat/Speech/AudioPlaybackEngine.cs index a9dadba..e2cc137 100644 --- a/Chat/Speech/AudioPlaybackEngine.cs +++ b/Chat/Speech/AudioPlaybackEngine.cs @@ -28,13 +28,11 @@ public class AudioPlaybackEngine : IDisposable throw new NullReferenceException(nameof(input)); if (input.WaveFormat.Channels == mixer.WaveFormat.Channels) - { return input; - } if (input.WaveFormat.Channels == 1 && mixer.WaveFormat.Channels == 2) - { return new MonoToStereoSampleProvider(input); - } + if (input.WaveFormat.Channels == 2 && mixer.WaveFormat.Channels == 1) + return new StereoToMonoSampleProvider(input); throw new NotImplementedException("Not yet implemented this channel count conversion"); } diff --git a/Chat/Speech/TTSPlayer.cs b/Chat/Speech/TTSPlayer.cs index d8f2a6e..124b792 100644 --- a/Chat/Speech/TTSPlayer.cs +++ b/Chat/Speech/TTSPlayer.cs @@ -2,10 +2,10 @@ using NAudio.Wave; public class TTSPlayer { - private PriorityQueue _messages; // ready to play - private PriorityQueue _buffer; - private Mutex _mutex; - private Mutex _mutex2; + private readonly PriorityQueue _messages; // ready to play + private readonly PriorityQueue _buffer; + private readonly Mutex _mutex; + private readonly Mutex _mutex2; public ISampleProvider? Playing { get; set; } diff --git a/Configuration.cs b/Configuration.cs index a516ba0..ebc6537 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -15,17 +15,9 @@ namespace TwitchChatTTS public class TwitchConfiguration { public IEnumerable? Channels; - public IDictionary? Redeems; public bool? TtsWhenOffline; } - public class RedeemConfiguration { - public string? AudioFilePath; - public string? OutputFilePath; - public string? OutputContent; - public bool? OutputAppend; - } - public class OBSConfiguration { public string? Host; public short? Port; diff --git a/Helpers/WebClientWrap.cs b/Helpers/WebClientWrap.cs index df0f609..5df0294 100644 --- a/Helpers/WebClientWrap.cs +++ b/Helpers/WebClientWrap.cs @@ -5,8 +5,8 @@ namespace TwitchChatTTS.Helpers { public class WebClientWrap { - private HttpClient _client; - private JsonSerializerOptions _options; + private readonly HttpClient _client; + private readonly JsonSerializerOptions _options; public WebClientWrap(JsonSerializerOptions options) @@ -23,10 +23,10 @@ namespace TwitchChatTTS.Helpers _client.DefaultRequestHeaders.Add(key, value); } - public async Task GetJson(string uri) + public async Task GetJson(string uri, JsonSerializerOptions options = null) { var response = await _client.GetAsync(uri); - return JsonSerializer.Deserialize(await response.Content.ReadAsStreamAsync(), _options); + return JsonSerializer.Deserialize(await response.Content.ReadAsStreamAsync(), options ?? _options); } public async Task Get(string uri) diff --git a/Hermes/HermesClient.cs b/Hermes/HermesClient.cs index 4ef9487..eec233a 100644 --- a/Hermes/HermesClient.cs +++ b/Hermes/HermesClient.cs @@ -3,10 +3,11 @@ using TwitchChatTTS; using System.Text.Json; using HermesSocketLibrary.Requests.Messages; using TwitchChatTTS.Hermes; +using TwitchChatTTS.Twitch.Redemptions; public class HermesApiClient { - private WebClientWrap _web; + private readonly WebClientWrap _web; public HermesApiClient(Configuration configuration) { @@ -90,4 +91,26 @@ public class HermesApiClient return filters; } + + public async Task> FetchRedemptions() + { + var redemptions = await _web.GetJson>("https://hermes.goblincaves.com/api/settings/redemptions", new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + if (redemptions == null) + throw new Exception("Failed to redemptions from Hermes."); + + return redemptions; + } + + public async Task> FetchRedeemableActions() + { + var actions = await _web.GetJson>("https://hermes.goblincaves.com/api/settings/redemptions/actions"); + if (actions == null) + throw new Exception("Failed to fetch redeemable actions from Hermes."); + + return actions; + } } \ No newline at end of file diff --git a/Hermes/Socket/Handlers/HeartbeatHandler.cs b/Hermes/Socket/Handlers/HeartbeatHandler.cs index a31146c..5f7e664 100644 --- a/Hermes/Socket/Handlers/HeartbeatHandler.cs +++ b/Hermes/Socket/Handlers/HeartbeatHandler.cs @@ -7,17 +7,17 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers { public class HeartbeatHandler : IWebSocketHandler { - private ILogger _logger { get; } - public int OperationCode { get; set; } = 0; + private readonly ILogger _logger; + public int OperationCode { get; } = 0; public HeartbeatHandler(ILogger logger) { _logger = logger; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not HeartbeatMessage obj || obj == null) + if (data is not HeartbeatMessage message || message == null) return; if (sender is not HermesSocketClient client) @@ -29,7 +29,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers client.LastHeartbeatReceived = DateTime.UtcNow; - if (obj.Respond) + if (message.Respond) await sender.Send(0, new HeartbeatMessage() { DateTime = DateTime.UtcNow, diff --git a/Hermes/Socket/Handlers/LoginAckHandler.cs b/Hermes/Socket/Handlers/LoginAckHandler.cs index 3c70bc2..d6509f1 100644 --- a/Hermes/Socket/Handlers/LoginAckHandler.cs +++ b/Hermes/Socket/Handlers/LoginAckHandler.cs @@ -8,33 +8,32 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers { public class LoginAckHandler : IWebSocketHandler { - private IServiceProvider _serviceProvider; - private ILogger _logger; - public int OperationCode { get; set; } = 2; + private readonly User _user; + private readonly ILogger _logger; + public int OperationCode { get; } = 2; - public LoginAckHandler(IServiceProvider serviceProvider, ILogger logger) + public LoginAckHandler(User user, ILogger logger) { - _serviceProvider = serviceProvider; + _user = user; _logger = logger; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not LoginAckMessage obj || obj == null) + if (data is not LoginAckMessage message || message == null) return; if (sender is not HermesSocketClient client) return; - if (obj.AnotherClient) + if (message.AnotherClient) { _logger.Warning("Another client has connected to the same account."); } else { - var user = _serviceProvider.GetRequiredService(); - client.UserId = obj.UserId; - _logger.Information($"Logged in as {user.TwitchUsername} (id: {client.UserId})."); + client.UserId = message.UserId; + _logger.Information($"Logged in as {_user.TwitchUsername}."); } await client.Send(3, new RequestMessage() @@ -43,11 +42,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers Data = null }); - var token = _serviceProvider.GetRequiredService(); await client.Send(3, new RequestMessage() { Type = "get_tts_users", - Data = new Dictionary() { { "user", token.HermesUserId } } + Data = new Dictionary() { { "user", _user.HermesUserId } } }); await client.Send(3, new RequestMessage() diff --git a/Hermes/Socket/Handlers/RequestAckHandler.cs b/Hermes/Socket/Handlers/RequestAckHandler.cs index 1cdc995..bf08c43 100644 --- a/Hermes/Socket/Handlers/RequestAckHandler.cs +++ b/Hermes/Socket/Handlers/RequestAckHandler.cs @@ -18,7 +18,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers private readonly object _voicesAvailableLock = new object(); - public int OperationCode { get; set; } = 4; + public int OperationCode { get; } = 4; public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger) { @@ -27,20 +27,20 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers _logger = logger; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not RequestAckMessage obj || obj == null) + if (data is not RequestAckMessage message || message == null) return; - if (obj.Request == null) + if (message.Request == null) return; var context = _serviceProvider.GetRequiredService(); if (context == null) return; - if (obj.Request.Type == "get_tts_voices") + if (message.Request.Type == "get_tts_voices") { _logger.Verbose("Updating all available voices for TTS."); - var voices = JsonSerializer.Deserialize>(obj.Data.ToString(), _options); + var voices = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (voices == null) return; @@ -50,33 +50,33 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } _logger.Information("Updated all available voices for TTS."); } - else if (obj.Request.Type == "create_tts_user") + else if (message.Request.Type == "create_tts_user") { _logger.Verbose("Adding new tts voice for user."); - if (!long.TryParse(obj.Request.Data["user"].ToString(), out long chatterId)) + if (!long.TryParse(message.Request.Data["user"].ToString(), out long chatterId)) return; - string userId = obj.Request.Data["user"].ToString(); - string voice = obj.Request.Data["voice"].ToString(); + string userId = message.Request.Data["user"].ToString(); + string voice = message.Request.Data["voice"].ToString(); context.VoicesSelected.Add(chatterId, voice); _logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]"); } - else if (obj.Request.Type == "update_tts_user") + else if (message.Request.Type == "update_tts_user") { _logger.Verbose("Updating user's voice"); - if (!long.TryParse(obj.Request.Data["chatter"].ToString(), out long chatterId)) + if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId)) return; - string userId = obj.Request.Data["user"].ToString(); - string voice = obj.Request.Data["voice"].ToString(); + string userId = message.Request.Data["user"].ToString(); + string voice = message.Request.Data["voice"].ToString(); context.VoicesSelected[chatterId] = voice; _logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]"); } - else if (obj.Request.Type == "create_tts_voice") + else if (message.Request.Type == "create_tts_voice") { _logger.Verbose("Creating new tts voice."); - string? voice = obj.Request.Data["voice"].ToString(); - string? voiceId = obj.Data.ToString(); + string? voice = message.Request.Data["voice"].ToString(); + string? voiceId = message.Data.ToString(); if (voice == null || voiceId == null) return; @@ -88,10 +88,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } _logger.Information($"Created new tts voice [voice: {voice}][id: {voiceId}]."); } - else if (obj.Request.Type == "delete_tts_voice") + else if (message.Request.Type == "delete_tts_voice") { _logger.Verbose("Deleting tts voice."); - var voice = obj.Request.Data["voice"].ToString(); + var voice = message.Request.Data["voice"].ToString(); if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) return; @@ -103,11 +103,11 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } _logger.Information($"Deleted a voice [voice: {voiceName}]"); } - else if (obj.Request.Type == "update_tts_voice") + else if (message.Request.Type == "update_tts_voice") { _logger.Verbose("Updating TTS voice."); - string voiceId = obj.Request.Data["idd"].ToString(); - string voice = obj.Request.Data["voice"].ToString(); + string voiceId = message.Request.Data["idd"].ToString(); + string voice = message.Request.Data["voice"].ToString(); if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) return; @@ -115,10 +115,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers context.VoicesAvailable[voiceId] = voice; _logger.Information($"Updated TTS voice [voice: {voice}][id: {voiceId}]"); } - else if (obj.Request.Type == "get_tts_users") + else if (message.Request.Type == "get_tts_users") { _logger.Verbose("Updating all chatters' selected voice."); - var users = JsonSerializer.Deserialize>(obj.Data.ToString(), _options); + var users = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (users == null) return; @@ -128,10 +128,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers context.VoicesSelected = temp; _logger.Information($"Updated {temp.Count()} chatters' selected voice."); } - else if (obj.Request.Type == "get_chatter_ids") + else if (message.Request.Type == "get_chatter_ids") { _logger.Verbose("Fetching all chatters' id."); - var chatters = JsonSerializer.Deserialize>(obj.Data.ToString(), _options); + var chatters = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (chatters == null) return; @@ -139,10 +139,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers client.Chatters = [.. chatters]; _logger.Information($"Fetched {chatters.Count()} chatters' id."); } - else if (obj.Request.Type == "get_emotes") + else if (message.Request.Type == "get_emotes") { _logger.Verbose("Updating emotes."); - var emotes = JsonSerializer.Deserialize>(obj.Data.ToString(), _options); + var emotes = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (emotes == null) return; @@ -158,11 +158,11 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers } _logger.Information($"Fetched {count} emotes from various sources."); } - else if (obj.Request.Type == "update_tts_voice_state") + else if (message.Request.Type == "update_tts_voice_state") { _logger.Verbose("Updating TTS voice states."); - string voiceId = obj.Request.Data["voice"].ToString(); - bool state = obj.Request.Data["state"].ToString() == "true"; + string voiceId = message.Request.Data["voice"].ToString(); + bool state = message.Request.Data["state"].ToString() == "true"; if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) { diff --git a/Hermes/Socket/HermesSocketClient.cs b/Hermes/Socket/HermesSocketClient.cs index 4ccf4f1..17e242b 100644 --- a/Hermes/Socket/HermesSocketClient.cs +++ b/Hermes/Socket/HermesSocketClient.cs @@ -74,7 +74,7 @@ namespace TwitchChatTTS.Hermes.Socket UserId = null; _heartbeatTimer.Enabled = false; - _logger.Information("Logged off due to disconnection. Attempting to reconnect..."); + _logger.Warning("Logged off due to disconnection. Attempting to reconnect..."); _reconnectTimer.Enabled = true; } } diff --git a/Hermes/Socket/Managers/HermesHandlerManager.cs b/Hermes/Socket/Managers/HermesHandlerManager.cs index 8f5dc8c..a602c31 100644 --- a/Hermes/Socket/Managers/HermesHandlerManager.cs +++ b/Hermes/Socket/Managers/HermesHandlerManager.cs @@ -9,7 +9,6 @@ namespace TwitchChatTTS.Hermes.Socket.Managers { public HermesHandlerManager(ILogger logger, IServiceProvider provider) : base(logger) { - //Add(provider.GetRequiredService()); try { var basetype = typeof(IWebSocketHandler); @@ -29,13 +28,13 @@ namespace TwitchChatTTS.Hermes.Socket.Managers continue; } - Logger.Debug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers."); + _logger.Debug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers."); Add(handler); } } catch (Exception e) { - Logger.Error(e, "Failed to load hermes websocket handler types."); + _logger.Error(e, "Failed to load hermes websocket handler types."); } } } diff --git a/OBS/Socket/Data/OBSAlignment.cs b/OBS/Socket/Data/OBSAlignment.cs new file mode 100644 index 0000000..57703a9 --- /dev/null +++ b/OBS/Socket/Data/OBSAlignment.cs @@ -0,0 +1,8 @@ +namespace TwitchChatTTS.OBS.Socket.Data +{ + public enum OBSAlignment + { + Center = 0, + TopLeft = 5 + } +} \ No newline at end of file diff --git a/OBS/Socket/Data/TransformationData.cs b/OBS/Socket/Data/TransformationData.cs new file mode 100644 index 0000000..559af9f --- /dev/null +++ b/OBS/Socket/Data/TransformationData.cs @@ -0,0 +1,24 @@ +namespace TwitchChatTTS.OBS.Socket.Data +{ + public class OBSTransformationData + { + public int Alignment { get; set; } + public int BoundsAlignment { get; set; } + public double BoundsHeight { get; set; } + public string BoundsType { get; set; } + public double BoundsWidth { get; set; } + public int CropBottom { get; set; } + public int CropLeft { get; set; } + public int CropRight { get; set; } + public int CropTop { get; set; } + public double Height { get; set; } + public double PositionX { get; set; } + public double PositionY { get; set; } + public double Rotation { get; set; } + public double ScaleX { get; set; } + public double ScaleY { get; set; } + public double SourceHeight { get; set; } + public double SourceWidth { get; set; } + public double Width { get; set; } + } +} \ No newline at end of file diff --git a/OBS/Socket/Handlers/EventMessageHandler.cs b/OBS/Socket/Handlers/EventMessageHandler.cs index a6c7d04..c78dc5a 100644 --- a/OBS/Socket/Handlers/EventMessageHandler.cs +++ b/OBS/Socket/Handlers/EventMessageHandler.cs @@ -7,31 +7,29 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { public class EventMessageHandler : IWebSocketHandler { - private ILogger _logger { get; } - private IServiceProvider _serviceProvider { get; } - public int OperationCode { get; set; } = 5; + private readonly ILogger _logger; + public int OperationCode { get; } = 5; - public EventMessageHandler(ILogger logger, IServiceProvider serviceProvider) + public EventMessageHandler(ILogger logger) { _logger = logger; - _serviceProvider = serviceProvider; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not EventMessage obj || obj == null) + if (data is not EventMessage message || message == null) return; - switch (obj.EventType) + switch (message.EventType) { case "StreamStateChanged": case "RecordStateChanged": if (sender is not OBSSocketClient client) return; - string? raw_state = obj.EventData["outputState"].ToString(); + string? raw_state = message.EventData["outputState"].ToString(); string? state = raw_state?.Substring(21).ToLower(); - client.Live = obj.EventData["outputActive"].ToString() == "True"; + client.Live = message.EventData["outputActive"].ToString() == "True"; _logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + "."); if (client.Live == false && state != null && !state.EndsWith("ing")) @@ -40,7 +38,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers } break; default: - _logger.Debug(obj.EventType + " EVENT: " + string.Join(" | ", obj.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0])); + _logger.Debug(message.EventType + " EVENT: " + string.Join(" | ", message.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 12a26d8..29ddd24 100644 --- a/OBS/Socket/Handlers/HelloHandler.cs +++ b/OBS/Socket/Handlers/HelloHandler.cs @@ -10,30 +10,30 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { public class HelloHandler : IWebSocketHandler { - private ILogger _logger { get; } - public int OperationCode { get; set; } = 0; - private HelloContext _context { get; } + private readonly HelloContext _context; + private readonly ILogger _logger; + public int OperationCode { get; } = 0; - public HelloHandler(ILogger logger, HelloContext context) + public HelloHandler(HelloContext context, ILogger logger) { - _logger = logger; _context = context; + _logger = logger; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not HelloMessage obj || obj == null) + if (data is not HelloMessage message || message == null) return; _logger.Verbose("OBS websocket password: " + _context.Password); - if (obj.Authentication == null || string.IsNullOrWhiteSpace(_context.Password)) + if (message.Authentication == null || string.IsNullOrWhiteSpace(_context.Password)) { - await sender.Send(1, new IdentifyMessage(obj.RpcVersion, string.Empty, 1023 | 262144)); + await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144)); return; } - var salt = obj.Authentication.Salt; - var challenge = obj.Authentication.Challenge; + var salt = message.Authentication.Salt; + var challenge = message.Authentication.Challenge; _logger.Verbose("Salt: " + salt); _logger.Verbose("Challenge: " + challenge); @@ -52,7 +52,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers } _logger.Verbose("Final hash: " + hash); - await sender.Send(1, new IdentifyMessage(obj.RpcVersion, hash, 1023 | 262144)); + await sender.Send(1, new IdentifyMessage(message.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 9995a43..6a2dbde 100644 --- a/OBS/Socket/Handlers/IdentifiedHandler.cs +++ b/OBS/Socket/Handlers/IdentifiedHandler.cs @@ -7,21 +7,29 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { public class IdentifiedHandler : IWebSocketHandler { - private ILogger Logger { get; } - public int OperationCode { get; set; } = 2; + private readonly ILogger _logger; + public int OperationCode { get; } = 2; public IdentifiedHandler(ILogger logger) { - Logger = logger; + _logger = logger; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not IdentifiedMessage obj || obj == null) + if (data is not IdentifiedMessage message || message == null) return; sender.Connected = true; - Logger.Information("Connected to OBS via rpc version " + obj.NegotiatedRpcVersion + "."); + _logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + "."); + + await Task.Delay(TimeSpan.FromSeconds(5)); + + /*var messages = new RequestMessage[] { + //new RequestMessage("Sleep", string.Empty, new Dictionary() { { "sleepMillis", 5000 } }), + new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" } }), + }; + await _manager.Send(messages);*/ } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/RequestBatchResponseHandler.cs b/OBS/Socket/Handlers/RequestBatchResponseHandler.cs index 84fe243..deaf57f 100644 --- a/OBS/Socket/Handlers/RequestBatchResponseHandler.cs +++ b/OBS/Socket/Handlers/RequestBatchResponseHandler.cs @@ -11,18 +11,17 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { public class RequestBatchResponseHandler : IWebSocketHandler { - private OBSRequestBatchManager _manager { get; } - private IServiceProvider _serviceProvider { get; } - private ILogger _logger { get; } - private JsonSerializerOptions _options; - public int OperationCode { get; set; } = 9; + private readonly IWebSocketHandler _requestResponseHandler; + private readonly ILogger _logger; + public int OperationCode { get; } = 9; - public RequestBatchResponseHandler(OBSRequestBatchManager manager, JsonSerializerOptions options, IServiceProvider serviceProvider, ILogger logger) + public RequestBatchResponseHandler( + [FromKeyedServices("obs-requestresponse")] IWebSocketHandler requestResponseHandler, + ILogger logger + ) { - _manager = manager; - _serviceProvider = serviceProvider; + _requestResponseHandler = requestResponseHandler; _logger = logger; - _options = options; } public async Task Execute(SocketClient sender, Data data) @@ -32,54 +31,37 @@ namespace TwitchChatTTS.OBS.Socket.Handlers using (LogContext.PushProperty("obsrid", message.RequestId)) { - + var results = message.Results.ToList(); _logger.Debug($"Received request batch response of {results.Count} messages."); - var requestData = _manager.Take(message.RequestId); - if (requestData == null || !results.Any()) - { - _logger.Verbose($"Received request batch response of {results.Count} messages."); - return; - } - - IList tasks = new List(); - int count = Math.Min(results.Count, requestData.RequestTypes.Count); + int count = results.Count; for (int i = 0; i < count; i++) { - Type type = requestData.RequestTypes[i]; - - using (LogContext.PushProperty("type", type.Name)) + if (results[i] == null) + continue; + + try { - try + _logger.Debug($"Request response from OBS request batch #{i + 1}/{count}: {results[i]}"); + var response = JsonSerializer.Deserialize(results[i].ToString(), new JsonSerializerOptions() { - var handler = GetResponseHandlerForRequestType(type); - _logger.Verbose($"Request handled by {handler.GetType().Name}."); - tasks.Add(handler.Execute(sender, results[i])); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to process an item in a request batch message."); - } + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + if (response == null) + continue; + + await _requestResponseHandler.Execute(sender, response); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to process an item in a request batch message."); } } - _logger.Verbose($"Waiting for processing to complete."); - await Task.WhenAll(tasks); - _logger.Debug($"Finished processing all request in this batch."); } } - - private IWebSocketHandler? GetResponseHandlerForRequestType(Type type) - { - if (type == typeof(RequestMessage)) - return _serviceProvider.GetRequiredKeyedService("obs-requestresponse"); - else if (type == typeof(RequestBatchMessage)) - return _serviceProvider.GetRequiredKeyedService("obs-requestbatcresponse"); - else if (type == typeof(IdentifyMessage)) - return _serviceProvider.GetRequiredKeyedService("obs-identified"); - return null; - } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/RequestResponseHandler.cs b/OBS/Socket/Handlers/RequestResponseHandler.cs index a7851da..34a4d71 100644 --- a/OBS/Socket/Handlers/RequestResponseHandler.cs +++ b/OBS/Socket/Handlers/RequestResponseHandler.cs @@ -2,36 +2,100 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using Serilog; using TwitchChatTTS.OBS.Socket.Data; +using TwitchChatTTS.OBS.Socket.Manager; namespace TwitchChatTTS.OBS.Socket.Handlers { public class RequestResponseHandler : IWebSocketHandler { - private ILogger Logger { get; } - public int OperationCode { get; set; } = 7; + private readonly OBSManager _manager; + private readonly ILogger _logger; + public int OperationCode { get; } = 7; - public RequestResponseHandler(ILogger logger) + public RequestResponseHandler(OBSManager manager, ILogger logger) { - Logger = logger; + _manager = manager; + _logger = logger; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not RequestResponseMessage obj || obj == null) + if (data is not RequestResponseMessage message || message == null) return; - switch (obj.RequestType) - { - case "GetOutputStatus": - if (sender is not OBSSocketClient client) - return; + _logger.Debug($"Received an OBS request response [response id: {message.RequestId}]"); - if (obj.RequestId == "stream") - { - client.Live = obj.ResponseData["outputActive"].ToString() == "True"; - Logger.Warning("Updated stream's live status to " + client.Live); - } - break; + var requestData = _manager.Take(message.RequestId); + if (requestData == null) + { + _logger.Warning($"OBS Request Response not being processed: request not stored [response id: {message.RequestId}]"); + return; + } + + var request = requestData.Message; + if (request == null) + return; + + try + { + switch (request.RequestType) + { + case "GetOutputStatus": + if (sender is not OBSSocketClient client) + return; + + if (message.RequestId == "stream") + { + client.Live = message.ResponseData["outputActive"].ToString() == "True"; + _logger.Warning($"Updated stream's live status to {client.Live} [response id: {message.RequestId}]"); + } + break; + case "GetSceneItemId": + if (!request.RequestData.TryGetValue("sceneName", out object sceneName)) + { + _logger.Warning($"Failed to find the scene name that was requested [response id: {message.RequestId}]"); + return; + } + if (!request.RequestData.TryGetValue("sourceName", out object sourceName)) + { + _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][response id: {message.RequestId}]"); + return; + } + if (!message.ResponseData.TryGetValue("sceneItemId", out object sceneItemId)) { + _logger.Warning($"Failed to fetch the scene item id [scene: {sceneName}][scene item: {sourceName}][response id: {message.RequestId}]"); + return; + } + + _logger.Information($"Added scene item id [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][response id: {message.RequestId}]."); + _manager.AddSourceId(sceneName.ToString(), sourceName.ToString(), long.Parse(sceneItemId.ToString())); + + requestData.ResponseValues = new Dictionary + { + { "sceneItemId", sceneItemId } + }; + break; + case "GetSceneItemTransform": + if (!message.ResponseData.TryGetValue("sceneItemTransform", out object? transformData)) + { + _logger.Warning($"Failed to find the OBS scene item [response id: {message.RequestId}]"); + return; + } + + _logger.Verbose("Fetching OBS transformation data: " + transformData?.ToString()); + requestData.ResponseValues = new Dictionary + { + { "sceneItemTransform", transformData } + }; + break; + default: + _logger.Warning($"OBS Request Response not being processed [type: {request.RequestType}][{string.Join(Environment.NewLine, message.ResponseData?.Select(kvp => kvp.Key + " = " + kvp.Value?.ToString()) ?? new string[0])}]"); + break; + } + } + finally + { + if (requestData.Callback != null) + requestData.Callback(requestData.ResponseValues); } } } diff --git a/OBS/Socket/Manager/OBSBatchRequestManager.cs b/OBS/Socket/Manager/OBSBatchRequestManager.cs deleted file mode 100644 index ee477f6..0000000 --- a/OBS/Socket/Manager/OBSBatchRequestManager.cs +++ /dev/null @@ -1,60 +0,0 @@ -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using TwitchChatTTS.OBS.Socket.Data; - -namespace TwitchChatTTS.OBS.Socket.Manager -{ - public class OBSRequestBatchManager - { - private IDictionary _requests; - private IServiceProvider _serviceProvider; - private ILogger _logger; - - public OBSRequestBatchManager(IServiceProvider serviceProvider, ILogger logger) - { - _serviceProvider = serviceProvider; - _logger = logger; - } - - - public async Task Send(long broadcasterId, IEnumerable messages) { - string uid = GenerateUniqueIdentifier(); - var data = new OBSRequestBatchData(broadcasterId, uid, new List()); - _logger.Debug($"Sending request batch of {messages.Count()} messages."); - - foreach (WebSocketMessage message in messages) - data.RequestTypes.Add(message.GetType()); - - var client = _serviceProvider.GetRequiredKeyedService>("obs"); - await client.Send(8, new RequestBatchMessage(uid, messages)); - } - - public OBSRequestBatchData? Take(string id) { - if (_requests.TryGetValue(id, out var request)) { - _requests.Remove(id); - return request; - } - return null; - } - - private string GenerateUniqueIdentifier() - { - return Guid.NewGuid().ToString("X"); - } - } - - public class OBSRequestBatchData - { - public long BroadcasterId { get; } - public string RequestId { get; } - public IList RequestTypes { get; } - - public OBSRequestBatchData(long bid, string rid, IList types) { - BroadcasterId = bid; - RequestId = rid; - RequestTypes = types; - } - } -} \ No newline at end of file diff --git a/OBS/Socket/Manager/OBSHandlerManager.cs b/OBS/Socket/Manager/OBSHandlerManager.cs index 6b1f8ee..d14d30a 100644 --- a/OBS/Socket/Manager/OBSHandlerManager.cs +++ b/OBS/Socket/Manager/OBSHandlerManager.cs @@ -26,7 +26,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager continue; } - Logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}."); + _logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}."); Add(handler); } } diff --git a/OBS/Socket/Manager/OBSManager.cs b/OBS/Socket/Manager/OBSManager.cs new file mode 100644 index 0000000..a035785 --- /dev/null +++ b/OBS/Socket/Manager/OBSManager.cs @@ -0,0 +1,210 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using TwitchChatTTS.OBS.Socket.Data; + +namespace TwitchChatTTS.OBS.Socket.Manager +{ + public class OBSManager + { + private IDictionary _requests; + private IDictionary> _sourceIds; + private IServiceProvider _serviceProvider; + private ILogger _logger; + + public OBSManager(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + + _requests = new ConcurrentDictionary(); + _sourceIds = new Dictionary>(); + } + + + public void AddSourceId(string sceneName, string sourceName, long sourceId) + { + if (!_sourceIds.TryGetValue(sceneName, out var scene)) + { + scene = new Dictionary(); + _sourceIds.Add(sceneName, scene); + } + + if (scene.ContainsKey(sourceName)) + scene[sourceName] = sourceId; + else + scene.Add(sourceName, sourceId); + + } + + public async Task Send(IEnumerable messages) + { + string uid = GenerateUniqueIdentifier(); + _logger.Debug($"Sending OBS request batch of {messages.Count()} messages [obsid: {uid}]."); + + // Keep track of requests to know what we requested. + foreach (var message in messages) + { + message.RequestId = GenerateUniqueIdentifier(); + var data = new RequestData(message, uid); + _requests.Add(message.RequestId, data); + } + _logger.Debug($"Generated uid for all OBS request messages in batch [obsid: {uid}]: {string.Join(", ", messages.Select(m => m.RequestType + "=" + m.RequestId))}"); + + var client = _serviceProvider.GetRequiredKeyedService>("obs"); + await client.Send(8, new RequestBatchMessage(uid, messages)); + } + + public async Task Send(RequestMessage message, Action>? callback = null) + { + string uid = GenerateUniqueIdentifier(); + _logger.Debug($"Sending an OBS request [obsid: {uid}]"); + + // Keep track of requests to know what we requested. + message.RequestId = GenerateUniqueIdentifier(); + var data = new RequestData(message, uid) + { + Callback = callback + }; + _requests.Add(message.RequestId, data); + + var client = _serviceProvider.GetRequiredKeyedService>("obs"); + await client.Send(6, message); + } + + public RequestData? Take(string id) + { + if (id != null && _requests.TryGetValue(id, out var request)) + { + _requests.Remove(id); + return request; + } + return null; + } + + public async Task UpdateTransformation(string sceneName, string sceneItemName, Action action) + { + var m1 = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sourceName", sceneItemName } }); + await Send(m1, async (d) => + { + if (!d.TryGetValue("sceneItemId", out object value) || !long.TryParse(value.ToString(), out long sceneItemId)) + return; + + _logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][obsid: {m1.RequestId}]: {sceneItemId}"); + var m2 = new RequestMessage("GetSceneItemTransform", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); + await Send(m2, async (d) => + { + if (d == null) + return; + if (!d.TryGetValue("sceneItemTransform", out object transformData)) + return; + + _logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obsid: {m2.RequestId}]: {transformData}"); + var transform = JsonSerializer.Deserialize(transformData.ToString(), new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + if (transform == null) + { + _logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obsid: {m2.RequestId}]."); + return; + } + + //double fr = (transform.Rotation + rotation) % 360; + double w = transform.Width; + double h = transform.Height; + + // double ox = w * Math.Cos(r) - h * Math.Sin(r); + // double oy = w * Math.Sin(r) + h * Math.Cos(r); + //var oo = (fr > 45 && fr < 225 ? 0 : 1); + // var ww = fr >= 135 && fr < 225 ? h : w; + // var hh = fr >= 315 || fr < 45 ? h : w; + //double dx = h * Math.Sin(r); + //double dy = w * Math.Cos(fr > 90 && fr < 270 ? Math.PI - r : r); // * (fr >= 135 && fr < 225 || fr >= 315 || fr <= 45 ? -1 : 1); + + int a = transform.Alignment; + bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE"; + + if (hasBounds) + { + // Take care of bounds, for most cases. + // 'Crop to Bounding Box' might be unsupported. + w = transform.BoundsWidth; + h = transform.BoundsHeight; + a = transform.BoundsAlignment; + } + else if (transform.CropBottom + transform.CropLeft + transform.CropRight + transform.CropTop > 0) + { + w -= transform.CropLeft + transform.CropRight; + h -= transform.CropTop + transform.CropBottom; + } + + if (a != (int)OBSAlignment.Center) + { + if (hasBounds) + transform.BoundsAlignment = a = (int)OBSAlignment.Center; + else + transform.Alignment = a = (int)OBSAlignment.Center; + transform.PositionX = transform.PositionX + w / 2; + transform.PositionY = transform.PositionY + h / 2; + } + + action?.Invoke(transform); + + // double ax = w * Math.Cos(ir) - h * Math.Sin(ir); + // double ay = w * Math.Sin(ir) + h * Math.Cos(ir); + // _logger.Information($"ax: {ax} ay: {ay}"); + + // double bx = w * Math.Cos(r) - h * Math.Sin(r); + // double by = w * Math.Sin(r) + h * Math.Cos(r); + // _logger.Information($"bx: {bx} by: {by}"); + + // double ddx = bx - ax; + // double ddy = by - ay; + // _logger.Information($"dx: {ddx} dy: {ddy}"); + + // double arctan = Math.Atan(ddy / ddx); + // _logger.Information("Angle: " + arctan); + + // var xs = new int[] { 0, 0, 1, 1 }; + // var ys = new int[] { 0, 1, 1, 0 }; + // int i = ((int)Math.Floor(fr / 90) + 8) % 4; + // double dx = xs[i] * w * Math.Cos(rad) - ys[i] * h * Math.Sin(rad); + // double dy = xs[i] * w * Math.Sin(rad) + ys[i] * h * Math.Cos(rad); + + + //transform.Rotation = fr; + //_logger.Information($"w: {w} h: {h} fr: {fr} r: {r} rot: {rotation}"); + //_logger.Information($"dx: {dx} ox: {ox} oox: {oox}"); + //_logger.Information($"dy: {dy} oy: {oy} ooy: {ooy}"); + + var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } }); + await Send(m3); + }); + }); + } + + private string GenerateUniqueIdentifier() + { + return Guid.NewGuid().ToString("N"); + } + } + + public class RequestData + { + public RequestMessage Message { get; } + public string ParentId { get; } + public Dictionary ResponseValues { get; set; } + public Action>? Callback { get; set; } + + public RequestData(RequestMessage message, string parentId) + { + Message = message; + ParentId = parentId; + } + } +} \ No newline at end of file diff --git a/Seven/SevenApiClient.cs b/Seven/SevenApiClient.cs index e68070f..8d2d921 100644 --- a/Seven/SevenApiClient.cs +++ b/Seven/SevenApiClient.cs @@ -9,33 +9,34 @@ 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 ILogger Logger { get; } + private readonly WebClientWrap _web; + private readonly ILogger _logger; public SevenApiClient(ILogger logger) { - Logger = logger; - Web = new WebClientWrap(new JsonSerializerOptions() + _logger = logger; + _web = new WebClientWrap(new JsonSerializerOptions() { PropertyNameCaseInsensitive = false, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); } - public async Task FetchChannelEmoteSet(string twitchId) { + public async Task FetchChannelEmoteSet(string twitchId) + { try { - var details = await Web.GetJson($"{API_URL}/users/twitch/" + twitchId); + var details = await _web.GetJson($"{API_URL}/users/twitch/" + twitchId); return details?.EmoteSet; } catch (JsonException e) { - Logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON."); + _logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON."); } catch (Exception e) { - Logger.Error(e, "Failed to fetch emotes from 7tv."); + _logger.Error(e, "Failed to fetch emotes from 7tv."); } return null; } @@ -44,16 +45,16 @@ public class SevenApiClient { try { - var emoteSet = await Web.GetJson($"{API_URL}/emote-sets/6353512c802a0e34bac96dd2"); + var emoteSet = await _web.GetJson($"{API_URL}/emote-sets/6353512c802a0e34bac96dd2"); return emoteSet?.Emotes; } catch (JsonException e) { - Logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON."); + _logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON."); } catch (Exception e) { - Logger.Error(e, "Failed to fetch emotes from 7tv."); + _logger.Error(e, "Failed to fetch emotes from 7tv."); } return null; } diff --git a/Seven/Socket/Data/ErrorMessage.cs b/Seven/Socket/Data/ErrorMessage.cs index 8f5f122..aa397f5 100644 --- a/Seven/Socket/Data/ErrorMessage.cs +++ b/Seven/Socket/Data/ErrorMessage.cs @@ -2,6 +2,6 @@ namespace TwitchChatTTS.Seven.Socket.Data { public class ErrorMessage { - + } } \ No newline at end of file diff --git a/Seven/Socket/Handlers/DispatchHandler.cs b/Seven/Socket/Handlers/DispatchHandler.cs index 3ad91fb..1622a67 100644 --- a/Seven/Socket/Handlers/DispatchHandler.cs +++ b/Seven/Socket/Handlers/DispatchHandler.cs @@ -1,7 +1,6 @@ using System.Text.Json; using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; -using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchChatTTS.Seven.Socket.Data; @@ -9,26 +8,26 @@ namespace TwitchChatTTS.Seven.Socket.Handlers { public class DispatchHandler : IWebSocketHandler { - private ILogger Logger { get; } - private EmoteDatabase Emotes { get; } - private object _lock = new object(); - public int OperationCode { get; set; } = 0; + private readonly ILogger _logger; + private readonly EmoteDatabase _emotes; + private readonly object _lock = new object(); + public int OperationCode { get; } = 0; public DispatchHandler(ILogger logger, EmoteDatabase emotes) { - Logger = logger; - Emotes = emotes; + _logger = logger; + _emotes = emotes; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not DispatchMessage obj || obj == null) + if (data is not DispatchMessage message || message == null) return; - ApplyChanges(obj?.Body?.Pulled, cf => cf.OldValue, true); - ApplyChanges(obj?.Body?.Pushed, cf => cf.Value, false); - ApplyChanges(obj?.Body?.Removed, cf => cf.OldValue, true); - ApplyChanges(obj?.Body?.Updated, cf => cf.OldValue, false, cf => cf.Value); + ApplyChanges(message?.Body?.Pulled, cf => cf.OldValue, true); + ApplyChanges(message?.Body?.Pushed, cf => cf.Value, false); + ApplyChanges(message?.Body?.Removed, cf => cf.OldValue, true); + ApplyChanges(message?.Body?.Updated, cf => cf.OldValue, false, cf => cf.Value); } private void ApplyChanges(IEnumerable? fields, Func getter, bool removing, Func? updater = null) @@ -55,7 +54,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers if (removing) { RemoveEmoteById(o.Id); - Logger.Information($"Removed 7tv emote: {o.Name} (id: {o.Id})"); + _logger.Information($"Removed 7tv emote [name: {o.Name}][id: {o.Id}]"); } else if (updater != null) { @@ -70,18 +69,18 @@ namespace TwitchChatTTS.Seven.Socket.Handlers if (u != null) { - Emotes.Add(u.Name, u.Id); - Logger.Information($"Updated 7tv emote: from '{o.Name}' to '{u.Name}' (id: {u.Id})"); + _emotes.Add(u.Name, u.Id); + _logger.Information($"Updated 7tv emote [old name: {o.Name}][new name: {u.Name}][id: {u.Id}]"); } else { - Logger.Warning("Failed to update 7tv emote."); + _logger.Warning("Failed to update 7tv emote."); } } else { - Emotes.Add(o.Name, o.Id); - Logger.Information($"Added 7tv emote: {o.Name} (id: {o.Id})"); + _emotes.Add(o.Name, o.Id); + _logger.Information($"Added 7tv emote [name: {o.Name}][id: {o.Id}]"); } } } @@ -90,7 +89,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers private void RemoveEmoteById(string id) { string? key = null; - foreach (var e in Emotes.Emotes) + foreach (var e in _emotes.Emotes) { if (e.Value == id) { @@ -99,7 +98,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers } } if (key != null) - Emotes.Remove(key); + _emotes.Remove(key); } } } \ No newline at end of file diff --git a/Seven/Socket/Handlers/EndOfStreamHandler.cs b/Seven/Socket/Handlers/EndOfStreamHandler.cs index 4ba614b..b18f5a7 100644 --- a/Seven/Socket/Handlers/EndOfStreamHandler.cs +++ b/Seven/Socket/Handlers/EndOfStreamHandler.cs @@ -9,22 +9,22 @@ namespace TwitchChatTTS.Seven.Socket.Handlers { public class EndOfStreamHandler : IWebSocketHandler { - private ILogger Logger { get; } - private User User { get; } - private IServiceProvider ServiceProvider { get; } - private string[] ErrorCodes { get; } - private int[] ReconnectDelay { get; } + private readonly ILogger _logger; + private readonly User _user; + private readonly IServiceProvider _serviceProvider; + private readonly string[] _errorCodes; + private readonly int[] _reconnectDelay; - public int OperationCode { get; set; } = 7; + public int OperationCode { get; } = 7; public EndOfStreamHandler(ILogger logger, User user, IServiceProvider serviceProvider) { - Logger = logger; - User = user; - ServiceProvider = serviceProvider; + _logger = logger; + _user = user; + _serviceProvider = serviceProvider; - ErrorCodes = [ + _errorCodes = [ "Server Error", "Unknown Operation", "Invalid Payload", @@ -39,61 +39,62 @@ namespace TwitchChatTTS.Seven.Socket.Handlers "Insufficient Privilege", "Inactivity?" ]; - ReconnectDelay = [ + _reconnectDelay = [ 1000, - -1, - -1, - -1, - -1, + 0, + 0, + 0, + 0, 3000, 1000, 300000, 1000, - -1, - -1, + 0, + 0, 1000, 1000 ]; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not EndOfStreamMessage obj || obj == null) + if (data is not EndOfStreamMessage message || message == null) return; - var code = obj.Code - 4000; - if (code >= 0 && code < ErrorCodes.Length) - Logger.Warning($"Received end of stream message (reason: {ErrorCodes[code]}, code: {obj.Code}, message: {obj.Message})."); + var code = message.Code - 4000; + if (code >= 0 && code < _errorCodes.Length) + _logger.Warning($"Received end of stream message (reason: {_errorCodes[code]}, code: {message.Code}, message: {message.Message})."); else - Logger.Warning($"Received end of stream message (code: {obj.Code}, message: {obj.Message})."); + _logger.Warning($"Received end of stream message (code: {message.Code}, message: {message.Message})."); await sender.DisconnectAsync(); - if (code >= 0 && code < ReconnectDelay.Length && ReconnectDelay[code] < 0) + if (code >= 0 && code < _reconnectDelay.Length && _reconnectDelay[code] < 0) { - Logger.Error($"7tv client will remain disconnected due to a bad client implementation."); + _logger.Error($"7tv client will remain disconnected due to a bad client implementation."); return; } - if (string.IsNullOrWhiteSpace(User.SevenEmoteSetId)) + if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId)) return; - var context = ServiceProvider.GetRequiredService(); - await Task.Delay(ReconnectDelay[code]); + var context = _serviceProvider.GetRequiredService(); + if (_reconnectDelay[code] > 0) + await Task.Delay(_reconnectDelay[code]); - var base_url = $"@emote_set.*"; + var base_url = $"@emote_set.*"; string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}"; - Logger.Debug($"7tv websocket reconnecting to {url}."); + _logger.Debug($"7tv websocket reconnecting to {url}."); await sender.ConnectAsync(url); if (context.SessionId != null) { await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId }); - Logger.Information("Resumed connection to 7tv websocket."); + _logger.Information("Resumed connection to 7tv websocket."); } else { - Logger.Information("Resumed connection to 7tv websocket on a different session."); + _logger.Information("Resumed connection to 7tv websocket on a different session."); } } } diff --git a/Seven/Socket/Handlers/ErrorHandler.cs b/Seven/Socket/Handlers/ErrorHandler.cs index 47f18d9..67db0bf 100644 --- a/Seven/Socket/Handlers/ErrorHandler.cs +++ b/Seven/Socket/Handlers/ErrorHandler.cs @@ -7,17 +7,17 @@ namespace TwitchChatTTS.Seven.Socket.Handlers { public class ErrorHandler : IWebSocketHandler { - private ILogger Logger { get; } - public int OperationCode { get; set; } = 6; + private readonly ILogger _logger; + public int OperationCode { get; } = 6; public ErrorHandler(ILogger logger) { - Logger = logger; + _logger = logger; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not ErrorMessage obj || obj == null) + if (data is not ErrorMessage message || message == null) return; } } diff --git a/Seven/Socket/Handlers/ReconnectHandler.cs b/Seven/Socket/Handlers/ReconnectHandler.cs index 8255204..5cb52d2 100644 --- a/Seven/Socket/Handlers/ReconnectHandler.cs +++ b/Seven/Socket/Handlers/ReconnectHandler.cs @@ -7,20 +7,20 @@ namespace TwitchChatTTS.Seven.Socket.Handlers { public class ReconnectHandler : IWebSocketHandler { - private ILogger Logger { get; } - public int OperationCode { get; set; } = 4; + private readonly ILogger _logger; + public int OperationCode { get; } = 4; public ReconnectHandler(ILogger logger) { - Logger = logger; + _logger = logger; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not ReconnectMessage obj || obj == null) + if (data is not ReconnectMessage message || message == null) return; - Logger.Information($"7tv server wants us to reconnect (reason: {obj.Reason})."); + _logger.Information($"7tv server wants this client to reconnect (reason: {message.Reason})."); } } } \ No newline at end of file diff --git a/Seven/Socket/Handlers/SevenHelloHandler.cs b/Seven/Socket/Handlers/SevenHelloHandler.cs index 87447b3..027feb6 100644 --- a/Seven/Socket/Handlers/SevenHelloHandler.cs +++ b/Seven/Socket/Handlers/SevenHelloHandler.cs @@ -7,27 +7,25 @@ namespace TwitchChatTTS.Seven.Socket.Handlers { public class SevenHelloHandler : IWebSocketHandler { - private ILogger Logger { get; } - private Configuration Configuration { get; } - public int OperationCode { get; set; } = 1; + private readonly ILogger _logger; + public int OperationCode { get; } = 1; - public SevenHelloHandler(ILogger logger, Configuration configuration) + public SevenHelloHandler(ILogger logger) { - Logger = logger; - Configuration = configuration; + _logger = logger; } - public async Task Execute(SocketClient sender, Data message) + public async Task Execute(SocketClient sender, Data data) { - if (message is not SevenHelloMessage obj || obj == null) + if (data is not SevenHelloMessage message || message == null) return; if (sender is not SevenSocketClient seven || seven == null) return; seven.Connected = true; - seven.ConnectionDetails = obj; - Logger.Information("Connected to 7tv websockets."); + seven.ConnectionDetails = message; + _logger.Information("Connected to 7tv websockets."); } } } \ No newline at end of file diff --git a/Seven/Socket/Managers/SevenHandlerManager.cs b/Seven/Socket/Managers/SevenHandlerManager.cs index 2601e63..ecc5a1f 100644 --- a/Seven/Socket/Managers/SevenHandlerManager.cs +++ b/Seven/Socket/Managers/SevenHandlerManager.cs @@ -28,13 +28,13 @@ namespace TwitchChatTTS.Seven.Socket.Managers continue; } - Logger.Debug($"Linked type {type.AssemblyQualifiedName} to 7tv websocket handler {handler.GetType().AssemblyQualifiedName}."); + _logger.Debug($"Linked type {type.AssemblyQualifiedName} to 7tv websocket handler {handler.GetType().AssemblyQualifiedName}."); Add(handler); } } catch (Exception e) { - Logger.Error(e, "Failed to load 7tv websocket handler types."); + _logger.Error(e, "Failed to load 7tv websocket handler types."); } } } diff --git a/Startup.cs b/Startup.cs index fed9c4d..96260cc 100644 --- a/Startup.cs +++ b/Startup.cs @@ -27,6 +27,8 @@ using TwitchChatTTS.Chat.Commands; using System.Text.Json; using Serilog; using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; +using TwitchChatTTS.Twitch.Redemptions; // dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true @@ -36,30 +38,23 @@ HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); var s = builder.Services; var deserializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() .WithNamingConvention(HyphenatedNamingConvention.Instance) .Build(); var configContent = File.ReadAllText("tts.config.yml"); var configuration = deserializer.Deserialize(configContent); -var redeemKeys = configuration.Twitch?.Redeems?.Keys; -if (redeemKeys != null && redeemKeys.Any()) -{ - foreach (var key in redeemKeys) - { - if (key != key.ToLower()) - configuration.Twitch.Redeems.Add(key.ToLower(), configuration.Twitch.Redeems[key]); - } -} s.AddSingleton(configuration); var logger = new LoggerConfiguration() - #if DEBUG - .MinimumLevel.Debug() - #else - .MinimumLevel.Information() - #endif - .WriteTo.File("logs/log.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7) - .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information) + .MinimumLevel.Verbose() + //.MinimumLevel.Override("TwitchLib.Communication.Clients.WebSocketClient", LogEventLevel.Warning) + //.MinimumLevel.Override("TwitchLib.PubSub.TwitchPubSub", LogEventLevel.Warning) + .MinimumLevel.Override("TwitchLib", LogEventLevel.Warning) + .MinimumLevel.Override("mariuszgromada", LogEventLevel.Error) + .Enrich.FromLogContext() + .WriteTo.File("logs/log-.log", restrictedToMinimumLevel: LogEventLevel.Debug, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7) + .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information, theme: SystemConsoleTheme.Colored) .CreateLogger(); s.AddSerilog(logger); @@ -87,6 +82,7 @@ s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); +s.AddSingleton(); s.AddSingleton(); s.AddSingleton(new TwitchBotAuth()); s.AddTransient(); @@ -106,7 +102,7 @@ s.AddSingleton(sp => Password = string.IsNullOrWhiteSpace(configuration.Obs?.Password) ? null : configuration.Obs.Password.Trim() } ); -s.AddSingleton(); +s.AddSingleton(); s.AddKeyedSingleton("obs-hello"); s.AddKeyedSingleton("obs-identified"); s.AddKeyedSingleton("obs-requestresponse"); diff --git a/TTS.cs b/TTS.cs index 8bfce1a..d6b185e 100644 --- a/TTS.cs +++ b/TTS.cs @@ -7,9 +7,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using NAudio.Wave.SampleProviders; -using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Seven; using TwitchLib.Client.Events; +using TwitchChatTTS.Twitch.Redemptions; +using org.mariuszgromada.math.mxparser; namespace TwitchChatTTS { @@ -18,17 +19,28 @@ namespace TwitchChatTTS public const int MAJOR_VERSION = 3; public const int MINOR_VERSION = 3; - private readonly ILogger _logger; + private readonly RedemptionManager _redemptionManager; private readonly Configuration _configuration; private readonly TTSPlayer _player; private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; - public TTS(ILogger logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider) + public TTS( + User user, + HermesApiClient hermesApiClient, + SevenApiClient sevenApiClient, + RedemptionManager redemptionManager, + Configuration configuration, + TTSPlayer player, + IServiceProvider serviceProvider, + ILogger logger + ) { - _logger = logger; + _redemptionManager = redemptionManager; _configuration = configuration; _player = player; _serviceProvider = serviceProvider; + _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) @@ -69,6 +81,8 @@ namespace TwitchChatTTS var emoteSet = await seven.FetchChannelEmoteSet(user.TwitchUserId.ToString()); user.SevenEmoteSetId = emoteSet?.Id; + License.iConfirmCommercialUse("abcdef"); + await InitializeEmotes(seven, emoteSet); await InitializeHermesWebsocket(); await InitializeSevenTv(emoteSet.Id); @@ -106,14 +120,14 @@ namespace TwitchChatTTS var provider = new CachedWavProvider(sound); var data = AudioPlaybackEngine.Instance.ConvertSound(provider); var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate); - _logger.Debug("Fetched TTS audio data."); + _logger.Verbose("Fetched TTS audio data."); m.Audio = resampled; _player.Ready(m); } catch (COMException e) { - _logger.Error(e, "Failed to send request for TTS (HResult: " + e.HResult + ")."); + _logger.Error(e, "Failed to send request for TTS [HResult: " + e.HResult + "]"); } catch (Exception e) { @@ -187,7 +201,7 @@ namespace TwitchChatTTS var twitchBotToken = await hermes.FetchTwitchBotToken(); user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId); - _logger.Information($"Username: {user.TwitchUsername} (id: {user.TwitchUserId})"); + _logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]"); user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice(); _logger.Information("Default Voice: " + user.DefaultTTSVoice); @@ -207,13 +221,23 @@ namespace TwitchChatTTS var voicesEnabled = await hermes.FetchTTSEnabledVoices(); if (voicesEnabled == null || !voicesEnabled.Any()) - user.VoicesEnabled = new HashSet(new string[] { "Brian" }); + user.VoicesEnabled = new HashSet(["Brian"]); else user.VoicesEnabled = new HashSet(voicesEnabled.Select(v => v)); _logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled."); var defaultedChatters = voicesSelected.Where(item => item.Voice == null || !user.VoicesEnabled.Contains(item.Voice)); - _logger.Information($"{defaultedChatters.Count()} chatters will have their TTS voice set to default due to having selected a disabled TTS voice."); + if (defaultedChatters.Any()) + _logger.Information($"{defaultedChatters.Count()} chatter(s) will have their TTS voice set to default due to having selected a disabled TTS voice."); + + var redemptionActions = await hermes.FetchRedeemableActions(); + var redemptions = await hermes.FetchRedemptions(); + foreach (var action in redemptionActions) + _redemptionManager.AddAction(action); + foreach (var redemption in redemptions) + _redemptionManager.AddTwitchRedemption(redemption); + _redemptionManager.Ready(); + _logger.Information($"Redemption Manager is ready with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); } private async Task InitializeHermesWebsocket() @@ -221,7 +245,7 @@ namespace TwitchChatTTS try { _logger.Information("Initializing hermes websocket client."); - var hermesClient = _serviceProvider.GetRequiredKeyedService>("hermes") as HermesSocketClient; + var hermesClient = _serviceProvider.GetRequiredKeyedService>("hermes"); var url = "wss://hermes-ws.goblincaves.com"; _logger.Debug($"Attempting to connect to {url}"); await hermesClient.ConnectAsync(url); diff --git a/Twitch/Redemptions/Action.cs b/Twitch/Redemptions/Action.cs new file mode 100644 index 0000000..59f3f77 --- /dev/null +++ b/Twitch/Redemptions/Action.cs @@ -0,0 +1,9 @@ +namespace TwitchChatTTS.Twitch.Redemptions +{ + public class RedeemableAction + { + public string Name { get; set; } + public string Type { get; set; } + public IDictionary Data { get; set; } + } +} \ No newline at end of file diff --git a/Twitch/Redemptions/Redemption.cs b/Twitch/Redemptions/Redemption.cs new file mode 100644 index 0000000..7fb6740 --- /dev/null +++ b/Twitch/Redemptions/Redemption.cs @@ -0,0 +1,11 @@ +namespace TwitchChatTTS.Twitch.Redemptions +{ + public class Redemption + { + public string Id { get; set; } + public string RedemptionId { get; set; } + public string ActionName { get; set; } + public int Order { get; set; } + public bool State { get; set; } + } +} \ No newline at end of file diff --git a/Twitch/Redemptions/RedemptionManager.cs b/Twitch/Redemptions/RedemptionManager.cs new file mode 100644 index 0000000..71c24e6 --- /dev/null +++ b/Twitch/Redemptions/RedemptionManager.cs @@ -0,0 +1,148 @@ +using System.Reflection; +using org.mariuszgromada.math.mxparser; +using Serilog; +using TwitchChatTTS.OBS.Socket.Data; +using TwitchChatTTS.OBS.Socket.Manager; + +namespace TwitchChatTTS.Twitch.Redemptions +{ + public class RedemptionManager + { + private readonly IList _redemptions; + private readonly IDictionary _actions; + private readonly IDictionary> _store; + private readonly OBSManager _obsManager; + private readonly ILogger _logger; + private bool _isReady; + + + public RedemptionManager(OBSManager obsManager, ILogger logger) + { + _redemptions = new List(); + _actions = new Dictionary(); + _store = new Dictionary>(); + _obsManager = obsManager; + _logger = logger; + _isReady = false; + } + + public void AddTwitchRedemption(Redemption redemption) + { + _redemptions.Add(redemption); + } + + public void AddAction(RedeemableAction action) + { + _actions.Add(action.Name, action); + } + + private void Add(string twitchRedemptionId, RedeemableAction action) + { + if (!_store.TryGetValue(twitchRedemptionId, out var actions)) + _store.Add(twitchRedemptionId, actions = new List()); + actions.Add(action); + _store[twitchRedemptionId] = actions.OrderBy(a => a).ToList(); + } + + public async Task Execute(RedeemableAction action, string sender) + { + try + { + switch (action.Type) + { + case "WRITE_TO_FILE": + Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"])); + await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], sender)); + _logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}]"); + break; + case "APPEND_TO_FILE": + Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"])); + await File.AppendAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], sender)); + _logger.Debug($"Appended text to file [file: {action.Data["file_path"]}]"); + break; + case "OBS_TRANSFORM": + var type = typeof(OBSTransformationData); + await _obsManager.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) => + { + string[] properties = ["rotation", "position_x", "position_y"]; + foreach (var property in properties) + { + if (!action.Data.TryGetValue(property, out var expressionString) || expressionString == null) + continue; + + var propertyName = string.Join("", property.Split('_').Select(p => char.ToUpper(p[0]) + p.Substring(1))); + PropertyInfo? prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); + if (prop == null) + { + _logger.Warning($"Failed to find property for OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}]"); + continue; + } + + var currentValue = prop.GetValue(d); + if (currentValue == null) + { + _logger.Warning($"Found a null value from OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}]"); + } + + Expression expression = new Expression(expressionString); + expression.addConstants(new Constant("x", (double?)currentValue ?? 0.0d)); + if (!expression.checkSyntax()) + { + _logger.Warning($"Could not parse math expression for OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][expression: {expressionString}][property: {propertyName}]"); + continue; + } + + var newValue = expression.calculate(); + prop.SetValue(d, newValue); + _logger.Debug($"OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}][old value: {currentValue}][new value: {newValue}][expression: {expressionString}]"); + } + _logger.Debug($"Finished applying the OBS transformation property changes [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}]"); + }); + break; + case "AUDIO_FILE": + if (!File.Exists(action.Data["file_path"])) { + _logger.Warning($"Cannot find audio file for Twitch channel point redeem [file: {action.Data["file_path"]}]"); + return; + } + AudioPlaybackEngine.Instance.PlaySound(action.Data["file_path"]); + _logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}]"); + break; + default: + _logger.Warning($"Unknown redeemable action has occured [type: {action.Type}]"); + break; + } + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to execute a redemption action."); + } + } + + public IList Get(string twitchRedemptionId) + { + if (!_isReady) + throw new InvalidOperationException("Not ready"); + + if (_store.TryGetValue(twitchRedemptionId, out var actions)) + return actions; + return new List(0); + } + + public void Ready() + { + var ordered = _redemptions.OrderBy(r => r.Order); + _store.Clear(); + + foreach (var redemption in ordered) + if (_actions.TryGetValue(redemption.ActionName, out var action) && action != null) + Add(redemption.RedemptionId, action); + + _isReady = true; + _logger.Debug("Redemption Manager is ready."); + } + + private string ReplaceContentText(string content, string username) { + return content.Replace("%USER%", username); + } + } +} \ No newline at end of file diff --git a/Twitch/TwitchApiClient.cs b/Twitch/TwitchApiClient.cs index 3f7984a..3d1dacc 100644 --- a/Twitch/TwitchApiClient.cs +++ b/Twitch/TwitchApiClient.cs @@ -6,28 +6,32 @@ using TwitchLib.Api.Core.Exceptions; using TwitchLib.Client.Events; using TwitchLib.Client.Models; using TwitchLib.Communication.Events; -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; +using TwitchChatTTS.Twitch.Redemptions; public class TwitchApiClient { + private readonly RedemptionManager _redemptionManager; + private readonly HermesApiClient _hermesApiClient; private readonly Configuration _configuration; - private readonly ILogger _logger; - private TwitchBotAuth _token; + private readonly TwitchBotAuth _token; private readonly ITwitchClient _client; private readonly ITwitchPubSub _publisher; private readonly WebClientWrap _web; private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; private bool _initialized; private string _broadcasterId; public TwitchApiClient( + RedemptionManager redemptionManager, + HermesApiClient hermesApiClient, Configuration configuration, TwitchBotAuth token, ITwitchClient twitchClient, @@ -36,6 +40,8 @@ public class TwitchApiClient ILogger logger ) { + _redemptionManager = redemptionManager; + _hermesApiClient = hermesApiClient; _configuration = configuration; _token = token; _client = twitchClient; @@ -43,6 +49,7 @@ public class TwitchApiClient _serviceProvider = serviceProvider; _logger = logger; _initialized = false; + _broadcasterId = string.Empty; _web = new WebClientWrap(new JsonSerializerOptions() { @@ -126,6 +133,9 @@ public class TwitchApiClient _logger.Information("Attempting to re-authorize."); await Authorize(_broadcasterId); + await _client.DisconnectAsync(); + await Task.Delay(TimeSpan.FromSeconds(1)); + await _client.ConnectAsync(); }; _client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => @@ -159,67 +169,49 @@ public class TwitchApiClient if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false) return; - _logger.Information("Follow: " + e.DisplayName); + _logger.Information($"New Follower [name: {e.DisplayName}][username: {e.Username}]"); }; - _publisher.OnChannelPointsRewardRedeemed += (s, e) => + _publisher.OnChannelPointsRewardRedeemed += async (s, e) => { var client = _serviceProvider.GetRequiredKeyedService>("obs") as OBSSocketClient; if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false) return; - _logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]"); + _logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]"); - if (_configuration.Twitch?.Redeems == null) - return; - - var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-"); - if (!_configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem)) - return; - - if (redeem == null) - return; - - // Write or append to file if needed. - var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim(); - if (outputFile == null) + var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id); + if (!actions.Any()) { - _logger.Debug($"No output file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]"); + _logger.Debug($"No redemable actions for this redeem was found [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]"); + return; } - else - { - var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n"); - if (outputContent == null) + _logger.Debug($"Found {actions.Count} actions for this Twitch channel point redemption [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]"); + + foreach (var action in actions) + try { - _logger.Warning($"No output content was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]"); + await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName); } - else + catch (Exception ex) { - if (redeem.OutputAppend == true) - { - File.AppendAllText(outputFile, outputContent + "\n"); - } - else - { - File.WriteAllText(outputFile, outputContent); - } + _logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]"); } - } + }; - // Play audio file if needed. - var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim(); - if (audioFile == null) + _publisher.OnPubSubServiceClosed += async (s, e) => + { + _logger.Warning("Twitch PubSub ran into a service close. Attempting to connect again."); + //await Task.Delay(Math.Min(3000 + (1 << psConnectionFailures), 120000)); + var authorized = await Authorize(_broadcasterId); + + var twitchBotData = await _hermesApiClient.FetchTwitchBotToken(); + if (twitchBotData == null) { - _logger.Debug($"No audio file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]"); - } - else if (!File.Exists(audioFile)) - { - _logger.Warning($"Cannot find audio file [location: {audioFile}] for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]"); - } - else - { - AudioPlaybackEngine.Instance.PlaySound(audioFile); + Console.WriteLine("The API is down. Contact the owner."); + return; } + await _publisher.ConnectAsync(); }; }