diff --git a/.gitignore b/.gitignore index f629320..ccbd1f4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ appsettings.json tts.config.yml obj/ bin/ +logs/ \ No newline at end of file diff --git a/Chat/ChatMessageHandler.cs b/Chat/ChatMessageHandler.cs index 8f99383..616c528 100644 --- a/Chat/ChatMessageHandler.cs +++ b/Chat/ChatMessageHandler.cs @@ -1,17 +1,16 @@ using System.Text.RegularExpressions; using TwitchLib.Client.Events; -using TwitchChatTTS.OBS.Socket; -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; using Serilog; -using Microsoft.Extensions.DependencyInjection; using TwitchChatTTS; -using TwitchChatTTS.Seven; using TwitchChatTTS.Chat.Commands; using TwitchChatTTS.Hermes.Socket; -using HermesSocketLibrary.Socket.Data; using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups; +using TwitchChatTTS.OBS.Socket.Manager; +using TwitchChatTTS.Chat.Emotes; +using Microsoft.Extensions.DependencyInjection; +using CommonSocketLibrary.Common; +using CommonSocketLibrary.Abstract; public class ChatMessageHandler @@ -21,14 +20,14 @@ public class ChatMessageHandler private readonly ChatCommandManager _commands; private readonly IGroupPermissionManager _permissionManager; private readonly IChatterGroupManager _chatterGroupManager; - private readonly EmoteDatabase _emotes; - private readonly OBSSocketClient? _obsClient; - private readonly HermesSocketClient? _hermesClient; + private readonly IEmoteDatabase _emotes; + private readonly OBSManager _obsManager; + private readonly HermesSocketClient _hermes; private readonly Configuration _configuration; private readonly ILogger _logger; - private Regex sfxRegex; + private Regex _sfxRegex; private HashSet _chatters; public HashSet Chatters { get => _chatters; set => _chatters = value; } @@ -40,9 +39,9 @@ public class ChatMessageHandler ChatCommandManager commands, IGroupPermissionManager permissionManager, IChatterGroupManager chatterGroupManager, - EmoteDatabase emotes, - [FromKeyedServices("obs")] SocketClient obsClient, - [FromKeyedServices("hermes")] SocketClient hermesClient, + IEmoteDatabase emotes, + OBSManager obsManager, + [FromKeyedServices("hermes")] SocketClient hermes, Configuration configuration, ILogger logger ) @@ -53,66 +52,66 @@ public class ChatMessageHandler _permissionManager = permissionManager; _chatterGroupManager = chatterGroupManager; _emotes = emotes; - _obsClient = obsClient as OBSSocketClient; - _hermesClient = hermesClient as HermesSocketClient; + _obsManager = obsManager; + _hermes = (hermes as HermesSocketClient)!; _configuration = configuration; _logger = logger; _chatters = new HashSet(); - sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)"); + _sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)"); } public async Task Handle(OnMessageReceivedArgs e) { - if (_obsClient == null || _hermesClient == null || _obsClient.Connected && _chatters == null) - return new MessageResult(MessageStatus.NotReady, -1, -1); - if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient.Live == false) - return new MessageResult(MessageStatus.NotReady, -1, -1); - var m = e.ChatMessage; + + if (!_hermes.Ready) + { + _logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]"); + return new MessageResult(MessageStatus.NotReady, -1, -1); + } + if (_configuration.Twitch?.TtsWhenOffline != true && !_obsManager.Streaming) + { + _logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {m.Id}]"); + return new MessageResult(MessageStatus.NotReady, -1, -1); + } + + var msg = e.ChatMessage.Message; var chatterId = long.Parse(m.UserId); var tasks = new List(); - var permissionPath = "tts.chat.messages.read"; - if (!string.IsNullOrWhiteSpace(m.CustomRewardId)) - permissionPath = "tts.chat.redemptions.read"; - var checks = new bool[] { true, m.IsSubscriber, m.IsVip, m.IsModerator, m.IsBroadcaster }; var defaultGroups = new string[] { "everyone", "subscribers", "vip", "moderators", "broadcaster" }; var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId); var groups = defaultGroups.Where((e, i) => checks[i]).Union(customGroups); - var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath); - var blocked = permission != true; - if (!blocked || m.IsBroadcaster) + try { - try - { - var commandResult = await _commands.Execute(msg, m, groups); - if (commandResult != ChatCommandResult.Unknown) - return new MessageResult(MessageStatus.Command, -1, -1); - } - catch (Exception ex) - { - _logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]"); - } + var commandResult = await _commands.Execute(msg, m, groups); + if (commandResult != ChatCommandResult.Unknown) + return new MessageResult(MessageStatus.Command, -1, -1); + } + catch (Exception ex) + { + _logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]"); } - if (blocked) + var permissionPath = "tts.chat.messages.read"; + if (!string.IsNullOrWhiteSpace(m.CustomRewardId)) + permissionPath = "tts.chat.redemptions.read"; + + var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath); + if (permission != true) { _logger.Debug($"Blocked message by {m.Username}: {msg}"); return new MessageResult(MessageStatus.Blocked, -1, -1); } - if (_obsClient.Connected && !_chatters.Contains(chatterId)) + if (_obsManager.Streaming && !_chatters.Contains(chatterId)) { - tasks.Add(_hermesClient.Send(6, new ChatterMessage() - { - Id = chatterId, - Name = m.Username - })); + tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username)); _chatters.Add(chatterId); } @@ -149,11 +148,8 @@ public class ChatMessageHandler if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5)) filteredMsg += w + " "; } - if (_obsClient.Connected && newEmotes.Any()) - tasks.Add(_hermesClient.Send(7, new EmoteDetailsMessage() - { - Emotes = newEmotes - })); + if (_obsManager.Streaming && newEmotes.Any()) + tasks.Add(_hermes.SendEmoteDetails(newEmotes)); msg = filteredMsg; // Replace filtered words. @@ -231,7 +227,7 @@ public class ChatMessageHandler return; var m = e.ChatMessage; - var parts = sfxRegex.Split(message); + var parts = _sfxRegex.Split(message); var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value)); if (parts.Length == 1) @@ -251,7 +247,7 @@ public class ChatMessageHandler return; } - var sfxMatches = sfxRegex.Matches(message); + var sfxMatches = _sfxRegex.Matches(message); var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length; for (var i = 0; i < sfxMatches.Count; i++) diff --git a/Seven/ChatterDatabase.cs b/Chat/ChatterDatabase.cs similarity index 97% rename from Seven/ChatterDatabase.cs rename to Chat/ChatterDatabase.cs index 2b02538..1bfab42 100644 --- a/Seven/ChatterDatabase.cs +++ b/Chat/ChatterDatabase.cs @@ -1,4 +1,4 @@ -namespace TwitchChatTTS.Seven +namespace TwitchChatTTS.Chat { public class ChatterDatabase { diff --git a/Chat/Commands/AddTTSVoiceCommand.cs b/Chat/Commands/AddTTSVoiceCommand.cs index e730bb1..56ebb1c 100644 --- a/Chat/Commands/AddTTSVoiceCommand.cs +++ b/Chat/Commands/AddTTSVoiceCommand.cs @@ -1,9 +1,7 @@ -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; -using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchChatTTS.Chat.Commands.Parameters; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands @@ -11,48 +9,41 @@ namespace TwitchChatTTS.Chat.Commands public class AddTTSVoiceCommand : ChatCommand { private readonly User _user; - private readonly SocketClient _hermesClient; private readonly ILogger _logger; public new bool DefaultPermissionsOverwrite { get => true; } public AddTTSVoiceCommand( User user, - [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, - [FromKeyedServices("hermes")] SocketClient hermesClient, + [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter, ILogger logger ) : base("addttsvoice", "Select a TTS voice as the default for that user.") { _user = user; - _hermesClient = hermesClient; _logger = logger; - AddParameter(ttsVoiceParameter); + AddParameter(unvalidatedParameter); } - public override async Task CheckDefaultPermissions(ChatMessage message, long broadcasterId) + public override async Task CheckDefaultPermissions(ChatMessage message) { return false; } - public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) { - if (_hermesClient == null) - return; if (_user == null || _user.VoicesAvailable == null) return; var voiceName = args.First(); var voiceNameLower = voiceName.ToLower(); var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower); - if (exists) + if (exists) { + _logger.Information("Voice already exists."); return; + } - await _hermesClient.Send(3, new RequestMessage() - { - Type = "create_tts_voice", - Data = new Dictionary() { { "voice", voiceName } } - }); + await client.CreateTTSVoice(voiceName); _logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]"); } } diff --git a/Chat/Commands/ChatCommand.cs b/Chat/Commands/ChatCommand.cs index 9427a29..1e0fba5 100644 --- a/Chat/Commands/ChatCommand.cs +++ b/Chat/Commands/ChatCommand.cs @@ -1,4 +1,5 @@ using TwitchChatTTS.Chat.Commands.Parameters; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands @@ -27,7 +28,7 @@ namespace TwitchChatTTS.Chat.Commands } } - public abstract Task CheckDefaultPermissions(ChatMessage message, long broadcasterId); - public abstract Task Execute(IList args, ChatMessage message, long broadcasterId); + public abstract Task CheckDefaultPermissions(ChatMessage message); + public abstract Task Execute(IList args, ChatMessage message, HermesSocketClient client); } } \ No newline at end of file diff --git a/Chat/Commands/ChatCommandManager.cs b/Chat/Commands/ChatCommandManager.cs index 777ab80..00146aa 100644 --- a/Chat/Commands/ChatCommandManager.cs +++ b/Chat/Commands/ChatCommandManager.cs @@ -1,8 +1,10 @@ using System.Text.RegularExpressions; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; using Microsoft.Extensions.DependencyInjection; using Serilog; -using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups.Permissions; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands @@ -10,28 +12,25 @@ namespace TwitchChatTTS.Chat.Commands public class ChatCommandManager { private IDictionary _commands; - private readonly TwitchBotAuth _token; private readonly User _user; + private readonly HermesSocketClient _hermes; private readonly IGroupPermissionManager _permissionManager; - private readonly IChatterGroupManager _chatterGroupManager; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private string CommandStartSign { get; } = "!"; public ChatCommandManager( - TwitchBotAuth token, User user, + [FromKeyedServices("hermes")] SocketClient socketClient, IGroupPermissionManager permissionManager, - IChatterGroupManager chatterGroupManager, IServiceProvider serviceProvider, ILogger logger ) { - _token = token; _user = user; + _hermes = (socketClient as HermesSocketClient)!; _permissionManager = permissionManager; - _chatterGroupManager = chatterGroupManager; _serviceProvider = serviceProvider; _logger = logger; @@ -71,8 +70,6 @@ namespace TwitchChatTTS.Chat.Commands public async Task Execute(string arg, ChatMessage message, IEnumerable groups) { - if (_token.BroadcasterId == null) - return ChatCommandResult.Unknown; if (string.IsNullOrWhiteSpace(arg)) return ChatCommandResult.Unknown; @@ -88,7 +85,6 @@ namespace TwitchChatTTS.Chat.Commands .ToArray(); string com = parts.First().Substring(CommandStartSign.Length).ToLower(); string[] args = parts.Skip(1).ToArray(); - long broadcasterId = long.Parse(_token.BroadcasterId); if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null) { @@ -107,7 +103,7 @@ namespace TwitchChatTTS.Chat.Commands _logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]"); return ChatCommandResult.Permission; } - else if (executable == null && !await command.CheckDefaultPermissions(message, broadcasterId)) + else if (executable == null && !await command.CheckDefaultPermissions(message)) { _logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]"); return ChatCommandResult.Permission; @@ -132,7 +128,7 @@ namespace TwitchChatTTS.Chat.Commands try { - await command.Execute(args, message, broadcasterId); + await command.Execute(args, message, _hermes); } catch (Exception e) { diff --git a/Chat/Commands/OBSCommand.cs b/Chat/Commands/OBSCommand.cs index 4081af2..ad8449b 100644 --- a/Chat/Commands/OBSCommand.cs +++ b/Chat/Commands/OBSCommand.cs @@ -3,6 +3,7 @@ using CommonSocketLibrary.Common; using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchChatTTS.Chat.Commands.Parameters; +using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Manager; using TwitchLib.Client.Models; @@ -19,7 +20,6 @@ namespace TwitchChatTTS.Chat.Commands [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter, User user, OBSManager manager, - [FromKeyedServices("obs")] SocketClient hermesClient, ILogger logger ) : base("obs", "Various obs commands.") { @@ -28,14 +28,17 @@ namespace TwitchChatTTS.Chat.Commands _logger = logger; AddParameter(unvalidatedParameter); + AddParameter(unvalidatedParameter, optional: true); + AddParameter(unvalidatedParameter, optional: true); + AddParameter(unvalidatedParameter, optional: true); } - public override async Task CheckDefaultPermissions(ChatMessage message, long broadcasterId) + public override async Task CheckDefaultPermissions(ChatMessage message) { return message.IsModerator || message.IsBroadcaster; } - public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) { if (_user == null || _user.VoicesAvailable == null) return; diff --git a/Chat/Commands/Parameters/SimpleListedParameter.cs b/Chat/Commands/Parameters/SimpleListedParameter.cs new file mode 100644 index 0000000..838252d --- /dev/null +++ b/Chat/Commands/Parameters/SimpleListedParameter.cs @@ -0,0 +1,17 @@ +namespace TwitchChatTTS.Chat.Commands.Parameters +{ + public class SimpleListedParameter : ChatCommandParameter + { + private readonly string[] _values; + + public SimpleListedParameter(string[] possibleValues, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional) + { + _values = possibleValues; + } + + public override bool Validate(string value) + { + return _values.Contains(value.ToLower()); + } + } +} \ No newline at end of file diff --git a/Chat/Commands/RefreshTTSDataCommand.cs b/Chat/Commands/RefreshTTSDataCommand.cs index 89b562e..333782f 100644 --- a/Chat/Commands/RefreshTTSDataCommand.cs +++ b/Chat/Commands/RefreshTTSDataCommand.cs @@ -1,6 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; using Serilog; +using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups.Permissions; +using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.OBS.Socket.Manager; using TwitchChatTTS.Twitch.Redemptions; using TwitchLib.Client.Models; @@ -34,20 +37,28 @@ namespace TwitchChatTTS.Chat.Commands _obsManager = obsManager; _hermesApi = hermesApi; _logger = logger; + + AddParameter(new SimpleListedParameter([ + "tts_voice_enabled", + "word_filters", + "selected_voices", + "default_voice", + "redemptions", + "obs_cache", + "permissions" + ])); } - public override async Task CheckDefaultPermissions(ChatMessage message, long broadcasterId) + public override async Task CheckDefaultPermissions(ChatMessage message) { return message.IsModerator || message.IsBroadcaster; } - public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) { - var value = args.FirstOrDefault(); - if (value == null) - return; + var value = args.First().ToLower(); - switch (value.ToLower()) + switch (value) { case "tts_voice_enabled": var voicesEnabled = await _hermesApi.FetchTTSEnabledVoices(); @@ -62,12 +73,6 @@ namespace TwitchChatTTS.Chat.Commands _user.RegexFilters = wordFilters.ToList(); _logger.Information($"{_user.RegexFilters.Count()} TTS word filters."); break; - case "username_filters": - 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 "selected_voices": { var voicesSelected = await _hermesApi.FetchTTSChatterSelectedVoices(); @@ -87,15 +92,8 @@ namespace TwitchChatTTS.Chat.Commands break; case "obs_cache": { - try - { - _obsManager.ClearCache(); - await _obsManager.GetGroupList(async groups => await _obsManager.GetGroupSceneItemList(groups)); - } - catch (Exception e) - { - _logger.Error(e, "Failed to load OBS group info via command."); - } + _obsManager.ClearCache(); + await _obsManager.GetGroupList(async groups => await _obsManager.GetGroupSceneItemList(groups)); break; } case "permissions": diff --git a/Chat/Commands/RemoveTTSVoiceCommand.cs b/Chat/Commands/RemoveTTSVoiceCommand.cs index 75d62fb..33a4023 100644 --- a/Chat/Commands/RemoveTTSVoiceCommand.cs +++ b/Chat/Commands/RemoveTTSVoiceCommand.cs @@ -1,9 +1,7 @@ -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; -using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchChatTTS.Chat.Commands.Parameters; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands @@ -11,7 +9,6 @@ namespace TwitchChatTTS.Chat.Commands public class RemoveTTSVoiceCommand : ChatCommand { private readonly User _user; - private readonly SocketClient _hermesClient; private ILogger _logger; public new bool DefaultPermissionsOverwrite { get => true; } @@ -19,39 +16,39 @@ namespace TwitchChatTTS.Chat.Commands public RemoveTTSVoiceCommand( [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, User user, - [FromKeyedServices("hermes")] SocketClient hermesClient, ILogger logger ) : base("removettsvoice", "Select a TTS voice as the default for that user.") { _user = user; - _hermesClient = hermesClient; _logger = logger; AddParameter(ttsVoiceParameter); } - public override async Task CheckDefaultPermissions(ChatMessage message, long broadcasterId) + public override async Task CheckDefaultPermissions(ChatMessage message) { return false; } - public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) { if (_user == null || _user.VoicesAvailable == null) + { + _logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]"); return; + } var voiceName = args.First().ToLower(); var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName); if (!exists) + { + _logger.Debug($"Voice does not exist [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]"); return; + } 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 [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]"); + await client.DeleteTTSVoice(voiceId); + _logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]"); } } } \ No newline at end of file diff --git a/Chat/Commands/SkipAllCommand.cs b/Chat/Commands/SkipAllCommand.cs index dd2270e..bafb314 100644 --- a/Chat/Commands/SkipAllCommand.cs +++ b/Chat/Commands/SkipAllCommand.cs @@ -1,4 +1,5 @@ using Serilog; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands @@ -15,12 +16,12 @@ namespace TwitchChatTTS.Chat.Commands _logger = logger; } - public override async Task CheckDefaultPermissions(ChatMessage message, long broadcasterId) + public override async Task CheckDefaultPermissions(ChatMessage message) { return message.IsModerator || message.IsVip || message.IsBroadcaster; } - public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) { _ttsPlayer.RemoveAll(); diff --git a/Chat/Commands/SkipCommand.cs b/Chat/Commands/SkipCommand.cs index 39d02fc..543f149 100644 --- a/Chat/Commands/SkipCommand.cs +++ b/Chat/Commands/SkipCommand.cs @@ -1,4 +1,5 @@ using Serilog; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands @@ -15,12 +16,12 @@ namespace TwitchChatTTS.Chat.Commands _logger = logger; } - public override async Task CheckDefaultPermissions(ChatMessage message, long broadcasterId) + public override async Task CheckDefaultPermissions(ChatMessage message) { return message.IsModerator || message.IsVip || message.IsBroadcaster; } - public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) { if (_ttsPlayer.Playing == null) return; diff --git a/Chat/Commands/TTSCommand.cs b/Chat/Commands/TTSCommand.cs index d2ed749..b6fd6b5 100644 --- a/Chat/Commands/TTSCommand.cs +++ b/Chat/Commands/TTSCommand.cs @@ -1,9 +1,7 @@ -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; -using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchChatTTS.Chat.Commands.Parameters; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands @@ -11,31 +9,27 @@ namespace TwitchChatTTS.Chat.Commands public class TTSCommand : ChatCommand { private readonly User _user; - private readonly SocketClient _hermesClient; private readonly ILogger _logger; public TTSCommand( [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, - [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter, User user, - [FromKeyedServices("hermes")] SocketClient hermesClient, ILogger logger ) : base("tts", "Various tts commands.") { _user = user; - _hermesClient = hermesClient; _logger = logger; AddParameter(ttsVoiceParameter); - AddParameter(unvalidatedParameter); + AddParameter(new SimpleListedParameter(["enable", "disable"])); } - public override async Task CheckDefaultPermissions(ChatMessage message, long broadcasterId) + public override async Task CheckDefaultPermissions(ChatMessage message) { return message.IsModerator || message.IsBroadcaster; } - public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) { if (_user == null || _user.VoicesAvailable == null) return; @@ -44,25 +38,9 @@ namespace TwitchChatTTS.Chat.Commands var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; var action = args[1].ToLower(); - switch (action) - { - case "enable": - await _hermesClient.Send(3, new RequestMessage() - { - Type = "update_tts_voice_state", - Data = new Dictionary() { { "voice", voiceId }, { "state", true } } - }); - _logger.Information($"Enabled a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]"); - break; - case "disable": - await _hermesClient.Send(3, new RequestMessage() - { - Type = "update_tts_voice_state", - Data = new Dictionary() { { "voice", voiceId }, { "state", false } } - }); - _logger.Information($"Disabled a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]"); - break; - } + bool state = action == "enable"; + await client.UpdateTTSVoiceState(voiceId, state); + _logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {state}][invoker: {message.Username}][id: {message.UserId}]"); } } } \ No newline at end of file diff --git a/Chat/Commands/VersionCommand.cs b/Chat/Commands/VersionCommand.cs index 74b195d..8893c24 100644 --- a/Chat/Commands/VersionCommand.cs +++ b/Chat/Commands/VersionCommand.cs @@ -1,26 +1,32 @@ +using HermesSocketLibrary.Socket.Data; using Serilog; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands { public class VersionCommand : ChatCommand { + private readonly User _user; private ILogger _logger; - public VersionCommand(ILogger logger) + public VersionCommand(User user, ILogger logger) : base("version", "Does nothing.") { + _user = user; _logger = logger; } - public override async Task CheckDefaultPermissions(ChatMessage message, long broadcasterId) + public override async Task CheckDefaultPermissions(ChatMessage message) { return message.IsBroadcaster; } - public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) { _logger.Information($"Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}"); + + await client.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}."); } } } \ No newline at end of file diff --git a/Chat/Commands/VoiceCommand.cs b/Chat/Commands/VoiceCommand.cs index e085878..5160e72 100644 --- a/Chat/Commands/VoiceCommand.cs +++ b/Chat/Commands/VoiceCommand.cs @@ -1,9 +1,7 @@ -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; -using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchChatTTS.Chat.Commands.Parameters; +using TwitchChatTTS.Hermes.Socket; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands @@ -11,29 +9,26 @@ namespace TwitchChatTTS.Chat.Commands public class VoiceCommand : ChatCommand { private readonly User _user; - private readonly SocketClient _hermesClient; private readonly ILogger _logger; public VoiceCommand( [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, User user, - [FromKeyedServices("hermes")] SocketClient hermesClient, ILogger logger ) : base("voice", "Select a TTS voice as the default for that user.") { _user = user; - _hermesClient = hermesClient; _logger = logger; AddParameter(ttsVoiceParameter); } - public override async Task CheckDefaultPermissions(ChatMessage message, long broadcasterId) + public override async Task CheckDefaultPermissions(ChatMessage message) { return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100; } - public override async Task Execute(IList args, ChatMessage message, long broadcasterId) + public override async Task Execute(IList args, ChatMessage message, HermesSocketClient client) { if (_user == null || _user.VoicesSelected == null || _user.VoicesEnabled == null) return; @@ -43,14 +38,21 @@ namespace TwitchChatTTS.Chat.Commands var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName); var enabled = _user.VoicesEnabled.Contains(voice.Value); - if (enabled) + if (!enabled) { - await _hermesClient.Send(3, new RequestMessage() - { - Type = _user.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user", - Data = new Dictionary() { { "chatter", chatterId }, { "voice", voice.Key } } - }); - _logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}]."); + _logger.Information($"Voice is disabled. Cannot switch to that voice [voice: {voice.Value}][username: {message.Username}]"); + return; + } + + if (_user.VoicesSelected.ContainsKey(chatterId)) + { + await client.UpdateTTSUser(chatterId, voice.Key); + _logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]"); + } + else + { + await client.CreateTTSUser(chatterId, voice.Key); + _logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]"); } } } diff --git a/Seven/EmoteDatabase.cs b/Chat/Emotes/EmoteDatabase.cs similarity index 94% rename from Seven/EmoteDatabase.cs rename to Chat/Emotes/EmoteDatabase.cs index 41a55aa..cc9cfa0 100644 --- a/Seven/EmoteDatabase.cs +++ b/Chat/Emotes/EmoteDatabase.cs @@ -1,6 +1,6 @@ -namespace TwitchChatTTS.Seven +namespace TwitchChatTTS.Chat.Emotes { - public class EmoteDatabase + public class EmoteDatabase : IEmoteDatabase { private readonly IDictionary _emotes; public IDictionary Emotes { get => _emotes.AsReadOnly(); } diff --git a/Chat/Emotes/IEmoteDatabase.cs b/Chat/Emotes/IEmoteDatabase.cs new file mode 100644 index 0000000..4f96b7f --- /dev/null +++ b/Chat/Emotes/IEmoteDatabase.cs @@ -0,0 +1,10 @@ +namespace TwitchChatTTS.Chat.Emotes +{ + public interface IEmoteDatabase + { + void Add(string emoteName, string emoteId); + void Clear(); + string? Get(string emoteName); + void Remove(string emoteName); + } +} \ No newline at end of file diff --git a/Configuration.cs b/Configuration.cs index ebc6537..35b1359 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -15,7 +15,7 @@ namespace TwitchChatTTS public class TwitchConfiguration { public IEnumerable? Channels; - public bool? TtsWhenOffline; + public bool TtsWhenOffline; } public class OBSConfiguration { diff --git a/Hermes/HermesApiClient.cs b/Hermes/HermesApiClient.cs index c5093be..11fc898 100644 --- a/Hermes/HermesApiClient.cs +++ b/Hermes/HermesApiClient.cs @@ -3,9 +3,9 @@ using TwitchChatTTS; using System.Text.Json; using HermesSocketLibrary.Requests.Messages; using TwitchChatTTS.Hermes; -using TwitchChatTTS.Twitch.Redemptions; using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups; +using HermesSocketLibrary.Socket.Data; public class HermesApiClient { @@ -50,15 +50,6 @@ public class HermesApiClient return token; } - public async Task> FetchTTSUsernameFilters() - { - var filters = await _web.GetJson>($"https://{BASE_URL}/api/settings/tts/filter/users"); - if (filters == null) - throw new Exception("Failed to fetch TTS username filters from Hermes."); - - return filters; - } - public async Task FetchTTSDefaultVoice() { var data = await _web.GetJson($"https://{BASE_URL}/api/settings/tts/default"); diff --git a/Hermes/Socket/Handlers/HeartbeatHandler.cs b/Hermes/Socket/Handlers/HeartbeatHandler.cs index 8a13d5c..abb07d8 100644 --- a/Hermes/Socket/Handlers/HeartbeatHandler.cs +++ b/Hermes/Socket/Handlers/HeartbeatHandler.cs @@ -19,7 +19,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers { if (data is not HeartbeatMessage message || message == null) return; - if (sender is not HermesSocketClient client) return; @@ -28,11 +27,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers client.LastHeartbeatReceived = DateTime.UtcNow; if (message.Respond) - await sender.Send(0, new HeartbeatMessage() - { - DateTime = DateTime.UtcNow, - Respond = false - }); + await client.SendHeartbeat(); } } } \ No newline at end of file diff --git a/Hermes/Socket/Handlers/LoginAckHandler.cs b/Hermes/Socket/Handlers/LoginAckHandler.cs index 4b78db5..c983b07 100644 --- a/Hermes/Socket/Handlers/LoginAckHandler.cs +++ b/Hermes/Socket/Handlers/LoginAckHandler.cs @@ -1,7 +1,6 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using HermesSocketLibrary.Socket.Data; -using Microsoft.Extensions.DependencyInjection; using Serilog; namespace TwitchChatTTS.Hermes.Socket.Handlers @@ -22,18 +21,23 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers { if (data is not LoginAckMessage message || message == null) return; - if (sender is not HermesSocketClient client) return; - if (message.AnotherClient) + if (message.AnotherClient && client.LoggedIn) { _logger.Warning("Another client has connected to the same account."); return; } + if (client.LoggedIn) + { + _logger.Warning("Attempted to log in again while still logged in."); + return; + } - client.UserId = message.UserId; + _user.HermesUserId = message.UserId; _user.OwnerId = message.OwnerId; + client.LoggedIn = true; _logger.Information($"Logged in as {_user.TwitchUsername} {(message.WebLogin ? "via web" : "via TTS app")}."); await client.Send(3, new RequestMessage() @@ -48,6 +52,12 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers Data = new Dictionary() { { "user", _user.HermesUserId } } }); + await client.Send(3, new RequestMessage() + { + Type = "get_default_tts_voice", + Data = null + }); + await client.Send(3, new RequestMessage() { Type = "get_chatter_ids", @@ -59,6 +69,13 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers Type = "get_emotes", Data = null }); + + await client.GetRedemptions(); + + await Task.Delay(TimeSpan.FromSeconds(3)); + + _logger.Information("TTS is now ready."); + client.Ready = true; } } } \ No newline at end of file diff --git a/Hermes/Socket/Handlers/RequestAckHandler.cs b/Hermes/Socket/Handlers/RequestAckHandler.cs index d255572..89e9d8b 100644 --- a/Hermes/Socket/Handlers/RequestAckHandler.cs +++ b/Hermes/Socket/Handlers/RequestAckHandler.cs @@ -2,17 +2,21 @@ using System.Collections.Concurrent; using System.Text.Json; using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; +using HermesSocketLibrary.Requests.Callbacks; using HermesSocketLibrary.Requests.Messages; using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Serilog; -using TwitchChatTTS.Seven; +using TwitchChatTTS.Chat.Emotes; +using TwitchChatTTS.Twitch.Redemptions; namespace TwitchChatTTS.Hermes.Socket.Handlers { public class RequestAckHandler : IWebSocketHandler { private User _user; + //private readonly RedemptionManager _redemptionManager; + private readonly ICallbackManager _callbackManager; private readonly IServiceProvider _serviceProvider; private readonly JsonSerializerOptions _options; private readonly ILogger _logger; @@ -21,9 +25,19 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers public int OperationCode { get; } = 4; - public RequestAckHandler(User user, IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger) + + public RequestAckHandler( + User user, + //RedemptionManager redemptionManager, + ICallbackManager callbackManager, + IServiceProvider serviceProvider, + JsonSerializerOptions options, + ILogger logger + ) { _user = user; + //_redemptionManager = redemptionManager; + _callbackManager = callbackManager; _serviceProvider = serviceProvider; _options = options; _logger = logger; @@ -34,10 +48,22 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers if (data is not RequestAckMessage message || message == null) return; if (message.Request == null) + { + _logger.Warning("Received a Hermes request message without a proper request."); return; - if (_user == null) - return; + } + HermesRequestData? hermesRequestData = null; + if (!string.IsNullOrEmpty(message.Request.RequestId)) + { + hermesRequestData = _callbackManager.Take(message.Request.RequestId); + if (hermesRequestData == null) + _logger.Warning($"Could not find callback for request [request id: {message.Request.RequestId}][type: {message.Request.Type}]"); + else if (hermesRequestData.Data == null) + hermesRequestData.Data = new Dictionary(); + } + + _logger.Debug($"Received a Hermes request message [type: {message.Request.Type}][data: {string.Join(',', message.Request.Data?.Select(entry => entry.Key + '=' + entry.Value) ?? Array.Empty())}]"); if (message.Request.Type == "get_tts_voices") { _logger.Verbose("Updating all available voices for TTS."); @@ -54,16 +80,16 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers else if (message.Request.Type == "create_tts_user") { _logger.Verbose("Adding new tts voice for user."); - if (!long.TryParse(message.Request.Data["user"].ToString(), out long chatterId)) + if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId)) { _logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]"); return; } string userId = message.Request.Data["user"].ToString(); - string voice = message.Request.Data["voice"].ToString(); + string voiceId = message.Request.Data["voice"].ToString(); - _user.VoicesSelected.Add(chatterId, voice); - _logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]"); + _user.VoicesSelected.Add(chatterId, voiceId); + _logger.Information($"Added new TTS voice [voice: {voiceId}] for user [user id: {userId}]"); } else if (message.Request.Type == "update_tts_user") { @@ -74,10 +100,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers return; } string userId = message.Request.Data["user"].ToString(); - string voice = message.Request.Data["voice"].ToString(); + string voiceId = message.Request.Data["voice"].ToString(); - _user.VoicesSelected[chatterId] = voice; - _logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]"); + _user.VoicesSelected[chatterId] = voiceId; + _logger.Information($"Updated TTS voice [voice: {voiceId}] for user [user id: {userId}]"); } else if (message.Request.Type == "create_tts_voice") { @@ -99,7 +125,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers { _logger.Verbose("Deleting tts voice."); var voice = message.Request.Data["voice"].ToString(); - if (!_user.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) + if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null) return; lock (_voicesAvailableLock) @@ -116,7 +142,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers string voiceId = message.Request.Data["idd"].ToString(); string voice = message.Request.Data["voice"].ToString(); - if (!_user.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) + if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null) return; _user.VoicesAvailable[voiceId] = voice; @@ -153,8 +179,9 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers if (emotes == null) return; - var emoteDb = _serviceProvider.GetRequiredService(); + var emoteDb = _serviceProvider.GetRequiredService(); var count = 0; + var duplicateNames = 0; foreach (var emote in emotes) { if (emoteDb.Get(emote.Name) == null) @@ -162,8 +189,12 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers emoteDb.Add(emote.Name, emote.Id); count++; } + else + duplicateNames++; } _logger.Information($"Fetched {count} emotes from various sources."); + if (duplicateNames > 0) + _logger.Warning($"Found {duplicateNames} emotes with duplicate names."); } else if (message.Request.Type == "update_tts_voice_state") { @@ -171,7 +202,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers string voiceId = message.Request.Data["voice"].ToString(); bool state = message.Request.Data["state"].ToString().ToLower() == "true"; - if (!_user.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) + if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null) { _logger.Warning($"Failed to find voice by id [id: {voiceId}]"); return; @@ -183,6 +214,73 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers _user.VoicesEnabled.Remove(voiceId); _logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]"); } + else if (message.Request.Type == "get_redemptions") + { + _logger.Verbose("Fetching all the redemptions."); + IEnumerable? redemptions = JsonSerializer.Deserialize>(message.Data!.ToString()!, _options); + if (redemptions != null) + { + _logger.Information($"Redemptions [count: {redemptions.Count()}] loaded."); + if (hermesRequestData != null) + hermesRequestData.Data!.Add("redemptions", redemptions); + } + else + _logger.Information(message.Data.GetType().ToString()); + } + else if (message.Request.Type == "get_redeemable_actions") + { + _logger.Verbose("Fetching all the redeemable actions."); + IEnumerable? actions = JsonSerializer.Deserialize>(message.Data!.ToString()!, _options); + if (actions == null) + { + _logger.Warning("Failed to read the redeemable actions for redemptions."); + return; + } + if (hermesRequestData?.Data == null || !(hermesRequestData.Data["redemptions"] is IEnumerable redemptions)) + { + _logger.Warning("Failed to read the redemptions while updating redemption actions."); + return; + } + + _logger.Information($"Redeemable actions [count: {actions.Count()}] loaded."); + var redemptionManager = _serviceProvider.GetRequiredService(); + redemptionManager.Initialize(redemptions, actions.ToDictionary(a => a.Name, a => a)); + } + else if (message.Request.Type == "get_default_tts_voice") + { + string? defaultVoice = message.Data?.ToString(); + if (defaultVoice != null) + { + _user.DefaultTTSVoice = defaultVoice; + _logger.Information($"Default TTS voice was changed to '{defaultVoice}'."); + } + } + else if (message.Request.Type == "update_default_tts_voice") + { + if (message.Request.Data?.TryGetValue("voice", out object? voice) == true && voice is string v) + { + _user.DefaultTTSVoice = v; + _logger.Information($"Default TTS voice was changed to '{v}'."); + } + else + _logger.Warning("Failed to update default TTS voice via request."); + } + else + { + _logger.Warning($"Found unknown request type when acknowledging [type: {message.Request.Type}]"); + } + + if (hermesRequestData != null) + { + _logger.Debug($"Callback was found for request [request id: {message.Request.RequestId}][type: {message.Request.Type}]"); + hermesRequestData.Callback?.Invoke(hermesRequestData.Data); + } } } + + public class HermesRequestData + { + public Action?>? Callback { get; set; } + public IDictionary? Data { get; set; } + } } \ No newline at end of file diff --git a/Hermes/Socket/HermesSocketClient.cs b/Hermes/Socket/HermesSocketClient.cs index 59e3171..7d2da26 100644 --- a/Hermes/Socket/HermesSocketClient.cs +++ b/Hermes/Socket/HermesSocketClient.cs @@ -1,26 +1,41 @@ +using System.Net.WebSockets; using System.Text.Json; using System.Timers; using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; +using HermesSocketLibrary.Requests.Callbacks; +using HermesSocketLibrary.Requests.Messages; using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Serilog; +using TwitchChatTTS.Hermes.Socket.Handlers; namespace TwitchChatTTS.Hermes.Socket { public class HermesSocketClient : WebSocketClient { - private Configuration _configuration; + public const string BASE_URL = "ws.tomtospeech.com"; + + private readonly User _user; + private readonly Configuration _configuration; + private readonly ICallbackManager _callbackManager; + private string URL; + public DateTime LastHeartbeatReceived { get; set; } public DateTime LastHeartbeatSent { get; set; } public string? UserId { get; set; } private System.Timers.Timer _heartbeatTimer; private System.Timers.Timer _reconnectTimer; - public const string BASE_URL = "ws.tomtospeech.com"; + public bool Connected { get; set; } + public bool LoggedIn { get; set; } + public bool Ready { get; set; } + public HermesSocketClient( + User user, Configuration configuration, + ICallbackManager callbackManager, [FromKeyedServices("hermes")] HandlerManager handlerManager, [FromKeyedServices("hermes")] HandlerTypeManager typeManager, ILogger logger @@ -30,7 +45,9 @@ namespace TwitchChatTTS.Hermes.Socket PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }) { + _user = user; _configuration = configuration; + _callbackManager = callbackManager; _heartbeatTimer = new System.Timers.Timer(TimeSpan.FromSeconds(15)); _heartbeatTimer.Elapsed += async (sender, e) => await HandleHeartbeat(e); @@ -39,11 +56,208 @@ namespace TwitchChatTTS.Hermes.Socket _reconnectTimer.Elapsed += async (sender, e) => await Reconnect(e); LastHeartbeatReceived = LastHeartbeatSent = DateTime.UtcNow; + URL = $"wss://{BASE_URL}"; } - protected override async Task OnConnection() + + public async Task Connect() { - _heartbeatTimer.Enabled = true; + if (Connected) + return; + + _logger.Debug($"Attempting to connect to {URL}"); + await ConnectAsync(URL); + } + + private async Task Disconnect() + { + if (!Connected) + return; + + await DisconnectAsync(); + } + + public async Task CreateTTSVoice(string voiceName) + { + await Send(3, new RequestMessage() + { + Type = "create_tts_voice", + Data = new Dictionary() { { "voice", voiceName } } + }); + } + + public async Task CreateTTSUser(long chatterId, string voiceId) + { + await Send(3, new RequestMessage() + { + Type = "create_tts_user", + Data = new Dictionary() { { "chatter", chatterId }, { "voice", voiceId } } + }); + } + + public async Task DeleteTTSVoice(string voiceId) + { + await Send(3, new RequestMessage() + { + Type = "delete_tts_voice", + Data = new Dictionary() { { "voice", voiceId } } + }); + } + + public async Task GetRedemptions() + { + var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData() + { + Callback = async (d) => await GetRedeemableActions(d["redemptions"] as IEnumerable), + Data = new Dictionary() + }); + + await Send(3, new RequestMessage() + { + RequestId = requestId, + Type = "get_redemptions", + Data = null + }); + } + + public async Task GetRedeemableActions(IEnumerable redemptions) + { + var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData() + { + Data = new Dictionary() { { "redemptions", redemptions } } + }); + + await Send(3, new RequestMessage() + { + RequestId = requestId, + Type = "get_redeemable_actions", + Data = null + }); + } + + public void Initialize() + { + _logger.Information("Initializing Hermes websocket client."); + + OnConnected += async (sender, e) => + { + Connected = true; + _logger.Information("Hermes websocket client connected."); + + _reconnectTimer.Enabled = false; + _heartbeatTimer.Enabled = true; + LastHeartbeatReceived = DateTime.UtcNow; + + await Send(1, new HermesLoginMessage() + { + ApiKey = _configuration.Hermes!.Token!, + MajorVersion = TTS.MAJOR_VERSION, + MinorVersion = TTS.MINOR_VERSION, + }); + }; + + OnDisconnected += (sender, e) => + { + Connected = false; + LoggedIn = false; + Ready = false; + _logger.Warning("Hermes websocket client disconnected."); + + _heartbeatTimer.Enabled = false; + _reconnectTimer.Enabled = true; + }; + } + + public async Task SendLoggingMessage(HermesLoggingLevel level, string message) + { + await Send(5, new LoggingMessage(message, level)); + } + + public async Task SendLoggingMessage(Exception exception, HermesLoggingLevel level, string message) + { + await Send(5, new LoggingMessage(exception, message, level)); + } + + public async Task SendEmoteUsage(string messageId, long chatterId, ICollection emotes) + { + if (!LoggedIn) + { + _logger.Debug("Not logged in. Cannot sent EmoteUsage message."); + return; + } + + await Send(8, new EmoteUsageMessage() + { + MessageId = messageId, + DateTime = DateTime.UtcNow, + BroadcasterId = _user.TwitchUserId, + ChatterId = chatterId, + Emotes = emotes + }); + } + + public async Task SendChatterDetails(long chatterId, string username) + { + if (!LoggedIn) + { + _logger.Debug("Not logged in. Cannot send Chatter message."); + return; + } + + await Send(6, new ChatterMessage() + { + Id = chatterId, + Name = username + }); + } + + public async Task SendEmoteDetails(IDictionary emotes) + { + if (!LoggedIn) + { + _logger.Debug("Not logged in. Cannot send EmoteDetails message."); + return; + } + + await Send(7, new EmoteDetailsMessage() + { + Emotes = emotes + }); + } + + public async Task SendHeartbeat(bool respond = false, DateTime? date = null) + { + await Send(0, new HeartbeatMessage() { DateTime = date ?? DateTime.UtcNow, Respond = respond }); + } + + public async Task UpdateTTSUser(long chatterId, string voiceId) + { + if (!LoggedIn) + { + _logger.Debug("Not logged in. Cannot send UpdateTTSUser message."); + return; + } + + await Send(3, new RequestMessage() + { + Type = "update_tts_user", + Data = new Dictionary() { { "chatter", chatterId }, { "voice", voiceId } } + }); + } + + public async Task UpdateTTSVoiceState(string voiceId, bool state) + { + if (!LoggedIn) + { + _logger.Debug("Not logged in. Cannot send UpdateTTSVoiceState message."); + return; + } + + await Send(3, new RequestMessage() + { + Type = "update_tts_voice_state", + Data = new Dictionary() { { "voice", voiceId }, { "state", state } } + }); } private async Task HandleHeartbeat(ElapsedEventArgs e) @@ -58,20 +272,22 @@ namespace TwitchChatTTS.Hermes.Socket LastHeartbeatSent = DateTime.UtcNow; try { - await Send(0, new HeartbeatMessage() { DateTime = LastHeartbeatSent }); + await SendHeartbeat(date: LastHeartbeatSent); } - catch (Exception) + catch (Exception ex) { + _logger.Error(ex, "Failed to send a heartbeat back to the Hermes websocket server."); } } else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120)) { try { - await DisconnectAsync(); + await Disconnect(); } - catch (Exception) + catch (Exception ex) { + _logger.Error(ex, "Failed to disconnect from Hermes websocket server."); } UserId = null; _heartbeatTimer.Enabled = false; @@ -84,32 +300,41 @@ namespace TwitchChatTTS.Hermes.Socket private async Task Reconnect(ElapsedEventArgs e) { - try + if (Connected) { - await ConnectAsync($"wss://{HermesSocketClient.BASE_URL}"); - Connected = true; - } - catch (Exception) - { - } - finally - { - if (Connected) + try { - _logger.Information("Reconnected."); - _reconnectTimer.Enabled = false; - _heartbeatTimer.Enabled = true; - LastHeartbeatReceived = DateTime.UtcNow; - - if (_configuration.Hermes?.Token != null) - await Send(1, new HermesLoginMessage() - { - ApiKey = _configuration.Hermes.Token, - MajorVersion = TTS.MAJOR_VERSION, - MinorVersion = TTS.MINOR_VERSION, - }); + await Disconnect(); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to disconnect from Hermes websocket server."); } } + + try + { + await Connect(); + } + catch (WebSocketException wse) when (wse.Message.Contains("502")) + { + _logger.Error("Hermes websocket server cannot be found."); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to reconnect to Hermes websocket server."); + } + } + + public new async Task Send(int opcode, T message) + { + if (!Connected) + { + _logger.Warning("Hermes websocket client is not connected. Not sending a message."); + return; + } + + await base.Send(opcode, message); } } } \ No newline at end of file diff --git a/OBS/Socket/Context/HelloContext.cs b/OBS/Socket/Context/HelloContext.cs deleted file mode 100644 index 2a22961..0000000 --- a/OBS/Socket/Context/HelloContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace TwitchChatTTS.OBS.Socket.Context -{ - public class HelloContext - { - public string? Host { get; set; } - public short? Port { get; set; } - public string? Password { get; set; } - } -} \ No newline at end of file diff --git a/OBS/Socket/Data/RequestMessage.cs b/OBS/Socket/Data/RequestMessage.cs index d1bb8c4..e9bbe08 100644 --- a/OBS/Socket/Data/RequestMessage.cs +++ b/OBS/Socket/Data/RequestMessage.cs @@ -12,5 +12,9 @@ namespace TwitchChatTTS.OBS.Socket.Data RequestId = id; RequestData = data; } + + public RequestMessage(string type, Dictionary data) : this(type, string.Empty, data) { } + + public RequestMessage(string type) : this(type, string.Empty, new()) { } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/EventMessageHandler.cs b/OBS/Socket/Handlers/EventMessageHandler.cs index c78dc5a..a7a7fe0 100644 --- a/OBS/Socket/Handlers/EventMessageHandler.cs +++ b/OBS/Socket/Handlers/EventMessageHandler.cs @@ -2,16 +2,19 @@ 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 EventMessageHandler : IWebSocketHandler { + private readonly OBSManager _manager; private readonly ILogger _logger; public int OperationCode { get; } = 5; - public EventMessageHandler(ILogger logger) + public EventMessageHandler(OBSManager manager, ILogger logger) { + _manager = manager; _logger = logger; } @@ -23,28 +26,23 @@ namespace TwitchChatTTS.OBS.Socket.Handlers switch (message.EventType) { case "StreamStateChanged": - case "RecordStateChanged": if (sender is not OBSSocketClient client) return; string? raw_state = message.EventData["outputState"].ToString(); string? state = raw_state?.Substring(21).ToLower(); - client.Live = message.EventData["outputActive"].ToString() == "True"; + _manager.Streaming = message.EventData["outputActive"].ToString().ToLower() == "true"; _logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + "."); - if (client.Live == false && state != null && !state.EndsWith("ing")) + if (_manager.Streaming == false && state != null && !state.EndsWith("ing")) { - OnStreamEnd(); + // Stream ended } break; default: - _logger.Debug(message.EventType + " EVENT: " + string.Join(" | ", message.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()) ?? Array.Empty())); break; } } - - private void OnStreamEnd() - { - } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/HelloHandler.cs b/OBS/Socket/Handlers/HelloHandler.cs index 29ddd24..28d5ad6 100644 --- a/OBS/Socket/Handlers/HelloHandler.cs +++ b/OBS/Socket/Handlers/HelloHandler.cs @@ -4,19 +4,18 @@ using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using Serilog; using TwitchChatTTS.OBS.Socket.Data; -using TwitchChatTTS.OBS.Socket.Context; namespace TwitchChatTTS.OBS.Socket.Handlers { public class HelloHandler : IWebSocketHandler { - private readonly HelloContext _context; + private readonly Configuration _configuration; private readonly ILogger _logger; public int OperationCode { get; } = 0; - public HelloHandler(HelloContext context, ILogger logger) + public HelloHandler(Configuration configuration, ILogger logger) { - _context = context; + _configuration = configuration; _logger = logger; } @@ -24,9 +23,10 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { if (data is not HelloMessage message || message == null) return; - - _logger.Verbose("OBS websocket password: " + _context.Password); - if (message.Authentication == null || string.IsNullOrWhiteSpace(_context.Password)) + + string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim(); + _logger.Verbose("OBS websocket password: " + password); + if (message.Authentication == null || string.IsNullOrWhiteSpace(password)) { await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144)); return; @@ -37,7 +37,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers _logger.Verbose("Salt: " + salt); _logger.Verbose("Challenge: " + challenge); - string secret = _context.Password + salt; + string secret = password + salt; byte[] bytes = Encoding.UTF8.GetBytes(secret); string hash = null; using (var sha = SHA256.Create()) diff --git a/OBS/Socket/Handlers/IdentifiedHandler.cs b/OBS/Socket/Handlers/IdentifiedHandler.cs index 08d1b76..71268e2 100644 --- a/OBS/Socket/Handlers/IdentifiedHandler.cs +++ b/OBS/Socket/Handlers/IdentifiedHandler.cs @@ -23,7 +23,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers if (data is not IdentifiedMessage message || message == null) return; - sender.Connected = true; + _manager.Connected = true; _logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + "."); try @@ -34,6 +34,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers { _logger.Error(e, "Failed to load OBS group info upon OBS identification."); } + + await _manager.UpdateStreamingState(); } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/RequestResponseHandler.cs b/OBS/Socket/Handlers/RequestResponseHandler.cs index 5ff467a..e8beb1b 100644 --- a/OBS/Socket/Handlers/RequestResponseHandler.cs +++ b/OBS/Socket/Handlers/RequestResponseHandler.cs @@ -42,10 +42,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers switch (request.RequestType) { case "GetOutputStatus": - if (sender is not OBSSocketClient client) - return; - - _logger.Debug($"Fetched stream's live status [live: {client.Live}][obs request id: {message.RequestId}]"); + _logger.Debug($"Fetched stream's live status [live: {_manager.Streaming}][obs request id: {message.RequestId}]"); break; case "GetSceneItemId": { @@ -227,6 +224,24 @@ namespace TwitchChatTTS.OBS.Socket.Handlers _logger.Debug($"Received response from OBS for sleeping [sleep: {sleepMillis}][obs request id: {message.RequestId}]"); break; } + case "GetStreamStatus": + { + if (message.ResponseData == null) + { + _logger.Warning($"OBS Response is null [obs request id: {message.RequestId}]"); + return; + } + if (!message.ResponseData.TryGetValue("outputActive", out object? outputActive) || outputActive == null) + { + _logger.Warning($"Failed to fetch the scene item visibility [obs request id: {message.RequestId}]"); + return; + } + + _manager.Streaming = outputActive?.ToString()!.ToLower() == "true"; + requestData.ResponseValues = message.ResponseData; + _logger.Information($"OBS is currently {(_manager.Streaming ? "" : "not ")}streaming."); + 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()) ?? [])}]"); break; diff --git a/OBS/Socket/Manager/OBSManager.cs b/OBS/Socket/Manager/OBSManager.cs index 5565b37..a76c6fd 100644 --- a/OBS/Socket/Manager/OBSManager.cs +++ b/OBS/Socket/Manager/OBSManager.cs @@ -12,11 +12,19 @@ namespace TwitchChatTTS.OBS.Socket.Manager { private readonly IDictionary _requests; private readonly IDictionary _sourceIds; + private string? URL; + + private readonly Configuration _configuration; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - public OBSManager(IServiceProvider serviceProvider, ILogger logger) + public bool Connected { get; set; } + public bool Streaming { get; set; } + + + public OBSManager(Configuration configuration, IServiceProvider serviceProvider, ILogger logger) { + _configuration = configuration; _serviceProvider = serviceProvider; _logger = logger; @@ -24,6 +32,27 @@ namespace TwitchChatTTS.OBS.Socket.Manager _sourceIds = new Dictionary(); } + public void Initialize() + { + _logger.Information($"Initializing OBS websocket client."); + var client = _serviceProvider.GetRequiredKeyedService>("obs"); + + client.OnConnected += (sender, e) => + { + Connected = true; + _logger.Information("OBS websocket client connected."); + }; + + client.OnDisconnected += (sender, e) => + { + Connected = false; + _logger.Information("OBS websocket client disconnected."); + }; + + if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null) + URL = $"ws://{_configuration.Obs.Host?.Trim()}:{_configuration.Obs.Port}"; + } + public void AddSourceId(string sourceName, long sourceId) { @@ -39,8 +68,35 @@ namespace TwitchChatTTS.OBS.Socket.Manager _sourceIds.Clear(); } + public async Task Connect() + { + if (string.IsNullOrWhiteSpace(URL)) + { + _logger.Warning("Lacking connection info for OBS websockets. Not connecting to OBS."); + return; + } + + var client = _serviceProvider.GetRequiredKeyedService>("obs"); + _logger.Debug($"OBS websocket client attempting to connect to {URL}"); + + try + { + await client.ConnectAsync(URL); + } + catch (Exception) + { + _logger.Warning("Connecting to obs failed. Skipping obs websockets."); + } + } + public async Task Send(IEnumerable messages) { + if (!Connected) + { + _logger.Warning("OBS websocket client is not connected. Not sending a message."); + return; + } + string uid = GenerateUniqueIdentifier(); var list = messages.ToList(); _logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}]."); @@ -60,6 +116,12 @@ namespace TwitchChatTTS.OBS.Socket.Manager public async Task Send(RequestMessage message, Action>? callback = null) { + if (!Connected) + { + _logger.Warning("OBS websocket client is not connected. Not sending a message."); + return; + } + string uid = GenerateUniqueIdentifier(); _logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]"); @@ -85,21 +147,26 @@ namespace TwitchChatTTS.OBS.Socket.Manager return null; } + public async Task UpdateStreamingState() + { + await Send(new RequestMessage("GetStreamStatus")); + } + public async Task UpdateTransformation(string sceneName, string sceneItemName, Action action) { if (action == null) return; - await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => + await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => { - var m2 = new RequestMessage("GetSceneItemTransform", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); + var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); await Send(m2, async (d) => { if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null) return; _logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]"); - var transform = JsonSerializer.Deserialize(transformData.ToString(), new JsonSerializerOptions() + var transform = JsonSerializer.Deserialize(transformData.ToString()!, new JsonSerializerOptions() { PropertyNameCaseInsensitive = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase @@ -126,20 +193,6 @@ namespace TwitchChatTTS.OBS.Socket.Manager transform.PositionY = transform.PositionY + h / 2; } - // 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; - // } - action?.Invoke(transform); var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } }); @@ -151,7 +204,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName) { - await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => + await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => { var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); await Send(m1, async (d) => @@ -167,7 +220,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible) { - await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => + await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => { var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } }); await Send(m); @@ -176,7 +229,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index) { - await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => + await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => { var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } }); await Send(m); @@ -220,7 +273,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager _logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]"); } - private async Task GetSceneItemById(string sceneName, string sceneItemName, Action action) + private async Task GetSceneItemByName(string sceneName, string sceneItemName, Action action) { if (_sourceIds.TryGetValue(sceneItemName, out long sourceId)) { @@ -245,18 +298,6 @@ namespace TwitchChatTTS.OBS.Socket.Manager { return Guid.NewGuid().ToString("N"); } - - private void LogExceptions(Action action, string description) - { - try - { - action.Invoke(); - } - catch (Exception e) - { - _logger.Error(e, description); - } - } } public class RequestData diff --git a/OBS/Socket/OBSSocketClient.cs b/OBS/Socket/OBSSocketClient.cs index d5e0648..a963d45 100644 --- a/OBS/Socket/OBSSocketClient.cs +++ b/OBS/Socket/OBSSocketClient.cs @@ -8,17 +8,6 @@ namespace TwitchChatTTS.OBS.Socket { public class OBSSocketClient : WebSocketClient { - private bool _live; - public bool? Live - { - get => Connected ? _live : null; - set - { - if (value.HasValue) - _live = value.Value; - } - } - public OBSSocketClient( ILogger logger, [FromKeyedServices("obs")] HandlerManager handlerManager, @@ -29,7 +18,6 @@ namespace TwitchChatTTS.OBS.Socket PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) { - _live = false; } } } \ No newline at end of file diff --git a/Seven/SevenApiClient.cs b/Seven/SevenApiClient.cs index e91d6f1..88745c3 100644 --- a/Seven/SevenApiClient.cs +++ b/Seven/SevenApiClient.cs @@ -2,6 +2,7 @@ using System.Text.Json; using TwitchChatTTS.Helpers; using Serilog; using TwitchChatTTS.Seven; +using TwitchChatTTS.Chat.Emotes; public class SevenApiClient { diff --git a/Seven/SevenManager.cs b/Seven/SevenManager.cs new file mode 100644 index 0000000..c328f3e --- /dev/null +++ b/Seven/SevenManager.cs @@ -0,0 +1,57 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; +using Serilog; + +namespace TwitchChatTTS.Seven.Socket +{ + public class SevenManager + { + private readonly User _user; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private string URL; + + public bool Connected { get; set; } + public bool Streaming { get; set; } + + + public SevenManager(User user, IServiceProvider serviceProvider, ILogger logger) + { + _user = user; + _serviceProvider = serviceProvider; + _logger = logger; + } + + public void Initialize() { + _logger.Information("Initializing 7tv websocket client."); + var client = _serviceProvider.GetRequiredKeyedService>("7tv"); + + client.OnConnected += (sender, e) => { + Connected = true; + _logger.Information("7tv websocket client connected."); + }; + + client.OnDisconnected += (sender, e) => { + Connected = false; + _logger.Information("7tv websocket client disconnected."); + }; + + if (!string.IsNullOrEmpty(_user.SevenEmoteSetId)) + URL = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*"; + } + + public async Task Connect() + { + if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId)) + { + _logger.Warning("Cannot find 7tv data for your channel. Not connecting to 7tv websockets."); + return; + } + + var client = _serviceProvider.GetRequiredKeyedService>("7tv"); + _logger.Debug($"7tv client attempting to connect to {URL}"); + await client.ConnectAsync($"{URL}"); + } + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/IdentifyMessage.cs b/Seven/Socket/Data/IdentifyMessage.cs deleted file mode 100644 index bd6ccfb..0000000 --- a/Seven/Socket/Data/IdentifyMessage.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace TwitchChatTTS.Seven.Socket.Data -{ - public class IdentifyMessage - { - - } -} \ No newline at end of file diff --git a/Seven/Socket/Handlers/DispatchHandler.cs b/Seven/Socket/Handlers/DispatchHandler.cs index 1622a67..e1de2ac 100644 --- a/Seven/Socket/Handlers/DispatchHandler.cs +++ b/Seven/Socket/Handlers/DispatchHandler.cs @@ -2,6 +2,7 @@ using System.Text.Json; using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; using Serilog; +using TwitchChatTTS.Chat.Emotes; using TwitchChatTTS.Seven.Socket.Data; namespace TwitchChatTTS.Seven.Socket.Handlers @@ -9,14 +10,14 @@ namespace TwitchChatTTS.Seven.Socket.Handlers public class DispatchHandler : IWebSocketHandler { private readonly ILogger _logger; - private readonly EmoteDatabase _emotes; + private readonly IEmoteDatabase _emotes; private readonly object _lock = new object(); public int OperationCode { get; } = 0; - public DispatchHandler(ILogger logger, EmoteDatabase emotes) + public DispatchHandler(IEmoteDatabase emotes, ILogger logger) { - _logger = logger; _emotes = emotes; + _logger = logger; } public async Task Execute(SocketClient sender, Data data) @@ -53,12 +54,20 @@ namespace TwitchChatTTS.Seven.Socket.Handlers { if (removing) { - RemoveEmoteById(o.Id); + if (_emotes.Get(o.Name) != o.Id) { + _logger.Warning("Mismatched emote found while removing a 7tv emote."); + continue; + } + _emotes.Remove(o.Name); _logger.Information($"Removed 7tv emote [name: {o.Name}][id: {o.Id}]"); } else if (updater != null) { - RemoveEmoteById(o.Id); + if (_emotes.Get(o.Name) != o.Id) { + _logger.Warning("Mismatched emote found while updating a 7tv emote."); + continue; + } + _emotes.Remove(o.Name); var update = updater(val); var u = JsonSerializer.Deserialize(update.ToString(), new JsonSerializerOptions() @@ -85,20 +94,5 @@ namespace TwitchChatTTS.Seven.Socket.Handlers } } } - - private void RemoveEmoteById(string id) - { - string? key = null; - foreach (var e in _emotes.Emotes) - { - if (e.Value == id) - { - key = e.Key; - break; - } - } - if (key != null) - _emotes.Remove(key); - } } } \ No newline at end of file diff --git a/Seven/Socket/Handlers/EndOfStreamHandler.cs b/Seven/Socket/Handlers/EndOfStreamHandler.cs index 421fcdd..d7da8fc 100644 --- a/Seven/Socket/Handlers/EndOfStreamHandler.cs +++ b/Seven/Socket/Handlers/EndOfStreamHandler.cs @@ -41,9 +41,9 @@ namespace TwitchChatTTS.Seven.Socket.Handlers ]; _reconnectDelay = [ 1000, - 0, - 0, - 0, + -1, + -1, + -1, 0, 3000, 1000, @@ -77,19 +77,17 @@ namespace TwitchChatTTS.Seven.Socket.Handlers if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId)) { - _logger.Warning("Connected to 7tv websocket previously, but no emote set id was set."); + _logger.Warning("Could not find the 7tv emote set id. Not reconnecting."); return; } var context = _serviceProvider.GetRequiredService(); if (_reconnectDelay[code] > 0) await Task.Delay(_reconnectDelay[code]); + + var manager = _serviceProvider.GetRequiredService(); + await manager.Connect(); - var base_url = $"@emote_set.*"; - string url = $"{SevenApiClient.WEBSOCKET_URL}{base_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 }); diff --git a/Seven/Socket/Handlers/SevenHelloHandler.cs b/Seven/Socket/Handlers/SevenHelloHandler.cs index 027feb6..d4eff78 100644 --- a/Seven/Socket/Handlers/SevenHelloHandler.cs +++ b/Seven/Socket/Handlers/SevenHelloHandler.cs @@ -23,9 +23,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers if (sender is not SevenSocketClient seven || seven == null) return; - seven.Connected = true; seven.ConnectionDetails = message; - _logger.Information("Connected to 7tv websockets."); + _logger.Debug("Received hello handshake ack."); } } } \ No newline at end of file diff --git a/Seven/UserDetails.cs b/Seven/UserDetails.cs index 40c104e..4983649 100644 --- a/Seven/UserDetails.cs +++ b/Seven/UserDetails.cs @@ -1,3 +1,5 @@ +using TwitchChatTTS.Chat.Emotes; + namespace TwitchChatTTS.Seven { public class UserDetails diff --git a/Startup.cs b/Startup.cs index a8b1744..e8ec733 100644 --- a/Startup.cs +++ b/Startup.cs @@ -11,8 +11,6 @@ using TwitchChatTTS.Seven.Socket; using TwitchChatTTS.OBS.Socket.Handlers; using TwitchChatTTS.Seven.Socket.Handlers; using TwitchChatTTS.Seven.Socket.Context; -using TwitchChatTTS.Seven; -using TwitchChatTTS.OBS.Socket.Context; using TwitchLib.Client.Interfaces; using TwitchLib.Client; using TwitchLib.PubSub.Interfaces; @@ -31,6 +29,8 @@ using Serilog.Sinks.SystemConsole.Themes; using TwitchChatTTS.Twitch.Redemptions; using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups; +using TwitchChatTTS.Chat.Emotes; +using HermesSocketLibrary.Requests.Callbacks; // dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true @@ -61,6 +61,7 @@ var logger = new LoggerConfiguration() s.AddSerilog(logger); s.AddSingleton(new User()); +s.AddSingleton, CallbackManager>(); s.AddSingleton(new JsonSerializerOptions() { @@ -71,6 +72,7 @@ s.AddSingleton(new JsonSerializerOptions() // Command parameters s.AddKeyedSingleton("parameter-ttsvoicename"); s.AddKeyedSingleton("parameter-unvalidated"); +s.AddKeyedSingleton("parameter-simplelisted"); s.AddKeyedSingleton("command-skipall"); s.AddKeyedSingleton("command-skip"); s.AddKeyedSingleton("command-voice"); @@ -88,24 +90,16 @@ s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); -s.AddSingleton(new TwitchBotAuth()); +s.AddSingleton(); s.AddTransient(); s.AddTransient(); s.AddTransient(); s.AddSingleton(); s.AddSingleton(); -s.AddSingleton(new EmoteDatabase()); +s.AddSingleton(); // OBS websocket -s.AddSingleton(sp => - new HelloContext() - { - Host = string.IsNullOrWhiteSpace(configuration.Obs?.Host) ? null : configuration.Obs.Host.Trim(), - Port = configuration.Obs?.Port, - Password = string.IsNullOrWhiteSpace(configuration.Obs?.Password) ? null : configuration.Obs.Password.Trim() - } -); s.AddSingleton(); s.AddKeyedSingleton("obs-hello"); s.AddKeyedSingleton("obs-identified"); @@ -123,15 +117,9 @@ s.AddTransient(sp => var logger = sp.GetRequiredService(); var client = sp.GetRequiredKeyedService>("7tv") as SevenSocketClient; if (client == null) - { - logger.Error("7tv client == null."); return new ReconnectContext() { SessionId = null }; - } if (client.ConnectionDetails == null) - { - logger.Error("Connection details in 7tv client == null."); return new ReconnectContext() { SessionId = null }; - } return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId }; }); s.AddKeyedSingleton("7tv-sevenhello"); @@ -141,6 +129,7 @@ s.AddKeyedSingleton("7tv-reconnect"); s.AddKeyedSingleton("7tv-error"); s.AddKeyedSingleton("7tv-endofstream"); +s.AddSingleton(); s.AddKeyedSingleton, SevenHandlerManager>("7tv"); s.AddKeyedSingleton, SevenHandlerTypeManager>("7tv"); s.AddKeyedSingleton, SevenSocketClient>("7tv"); diff --git a/TTS.cs b/TTS.cs index 2ea4a61..ce1e75f 100644 --- a/TTS.cs +++ b/TTS.cs @@ -1,30 +1,34 @@ using System.Runtime.InteropServices; using System.Web; -using CommonSocketLibrary.Abstract; -using CommonSocketLibrary.Common; -using HermesSocketLibrary.Socket.Data; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using NAudio.Wave.SampleProviders; -using TwitchChatTTS.Seven; using TwitchLib.Client.Events; using TwitchChatTTS.Twitch.Redemptions; using org.mariuszgromada.math.mxparser; using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups; +using TwitchChatTTS.OBS.Socket.Manager; +using TwitchChatTTS.Seven.Socket; +using TwitchChatTTS.Chat.Emotes; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; namespace TwitchChatTTS { public class TTS : IHostedService { public const int MAJOR_VERSION = 3; - public const int MINOR_VERSION = 8; + public const int MINOR_VERSION = 9; private readonly User _user; private readonly HermesApiClient _hermesApiClient; private readonly SevenApiClient _sevenApiClient; + private readonly OBSManager _obsManager; + private readonly SevenManager _sevenManager; + private readonly HermesSocketClient _hermes; private readonly RedemptionManager _redemptionManager; private readonly IChatterGroupManager _chatterGroupManager; private readonly IGroupPermissionManager _permissionManager; @@ -37,6 +41,9 @@ namespace TwitchChatTTS User user, HermesApiClient hermesApiClient, SevenApiClient sevenApiClient, + OBSManager obsManager, + SevenManager sevenManager, + [FromKeyedServices("hermes")] SocketClient hermes, RedemptionManager redemptionManager, IChatterGroupManager chatterGroupManager, IGroupPermissionManager permissionManager, @@ -49,6 +56,9 @@ namespace TwitchChatTTS _user = user; _hermesApiClient = hermesApiClient; _sevenApiClient = sevenApiClient; + _obsManager = obsManager; + _sevenManager = sevenManager; + _hermes = (hermes as HermesSocketClient)!; _redemptionManager = redemptionManager; _chatterGroupManager = chatterGroupManager; _permissionManager = permissionManager; @@ -63,7 +73,7 @@ namespace TwitchChatTTS Console.Title = "TTS - Twitch Chat"; License.iConfirmCommercialUse("abcdef"); - if (string.IsNullOrWhiteSpace(_configuration.Hermes.Token)) + if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token)) { _logger.Error("Hermes API token not set in the configuration file."); return; @@ -83,13 +93,14 @@ namespace TwitchChatTTS await Task.Delay(15 * 1000); } + await InitializeHermesWebsocket(); try { - await FetchUserData(_user, _hermesApiClient, _sevenApiClient); + await FetchUserData(_user, _hermesApiClient); } catch (Exception ex) { - _logger.Error(ex, "Failed to initialize properly."); + _logger.Error(ex, "Failed to initialize properly. Restart app please."); await Task.Delay(30 * 1000); } @@ -101,13 +112,21 @@ namespace TwitchChatTTS } var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString()); - _user.SevenEmoteSetId = emoteSet.Id; + if (emoteSet != null) + _user.SevenEmoteSetId = emoteSet.Id; await InitializeEmotes(_sevenApiClient, emoteSet); - await InitializeHermesWebsocket(); await InitializeSevenTv(); await InitializeObs(); + // _logger.Information("Sending a request to server..."); + // await _hermesManager.Send(3, new RequestMessage() { + // Type = "get_redeemable_actions", + // Data = new Dictionary() + // }); + // _logger.Warning("OS VERSION: " + Environment.OSVersion + " | " + Environment.OSVersion.Platform); + // return; + AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => { if (e.SampleProvider == _player.Playing) @@ -212,12 +231,9 @@ namespace TwitchChatTTS _logger.Warning("Application has stopped."); } - private async Task FetchUserData(User user, HermesApiClient hermes, SevenApiClient seven) + private async Task FetchUserData(User user, HermesApiClient hermes) { var hermesAccount = await hermes.FetchHermesAccountDetails(); - if (hermesAccount == null) - throw new Exception("Cannot connect to Hermes. Ensure your token is valid."); - user.HermesUserId = hermesAccount.Id; user.HermesUsername = hermesAccount.Username; user.TwitchUsername = hermesAccount.Username; @@ -226,25 +242,20 @@ namespace TwitchChatTTS user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId); _logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]"); - user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice(); - _logger.Information("TTS Default Voice: " + user.DefaultTTSVoice); + // user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice(); + // _logger.Information("TTS Default Voice: " + user.DefaultTTSVoice); - var wordFilters = await hermes.FetchTTSWordFilters(); - user.RegexFilters = wordFilters.ToList(); - _logger.Information($"{user.RegexFilters.Count()} TTS word 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 wordFilters = await hermes.FetchTTSWordFilters(); + // user.RegexFilters = wordFilters.ToList(); + // _logger.Information($"{user.RegexFilters.Count()} TTS word filters."); var voicesSelected = await hermes.FetchTTSChatterSelectedVoices(); user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice); - _logger.Information($"{user.VoicesSelected.Count} TTS voices have been selected for specific chatters."); + _logger.Information($"{user.VoicesSelected.Count} chatters have selected a specific TTS voice, among {user.VoicesSelected.Values.Distinct().Count()} distinct TTS voices."); var voicesEnabled = await hermes.FetchTTSEnabledVoices(); if (voicesEnabled == null || !voicesEnabled.Any()) - user.VoicesEnabled = new HashSet(["Brian"]); + user.VoicesEnabled = new HashSet([user.DefaultTTSVoice]); else user.VoicesEnabled = new HashSet(voicesEnabled.Select(v => v)); _logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled."); @@ -253,13 +264,10 @@ namespace TwitchChatTTS 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(); - _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a)); - _logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); - - _chatterGroupManager.Clear(); - _permissionManager.Clear(); + // var redemptionActions = await hermes.FetchRedeemableActions(); + // var redemptions = await hermes.FetchRedemptions(); + // _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a)); + // _logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); var groups = await hermes.FetchGroups(); var groupsById = groups.ToDictionary(g => g.Id, g => g); @@ -296,22 +304,12 @@ namespace TwitchChatTTS { try { - _logger.Information("Initializing hermes websocket client."); - var hermesClient = _serviceProvider.GetRequiredKeyedService>("hermes"); - var url = $"wss://{HermesSocketClient.BASE_URL}"; - _logger.Debug($"Attempting to connect to {url}"); - await hermesClient.ConnectAsync(url); - hermesClient.Connected = true; - await hermesClient.Send(1, new HermesLoginMessage() - { - ApiKey = _configuration.Hermes!.Token!, - MajorVersion = TTS.MAJOR_VERSION, - MinorVersion = TTS.MINOR_VERSION, - }); + _hermes.Initialize(); + await _hermes.Connect(); } - catch (Exception) + catch (Exception e) { - _logger.Warning("Connecting to hermes failed. Skipping hermes websockets."); + _logger.Error(e, "Connecting to hermes failed. Skipping hermes websockets."); } } @@ -319,37 +317,21 @@ namespace TwitchChatTTS { try { - _logger.Information("Initializing 7tv websocket client."); - var sevenClient = _serviceProvider.GetRequiredKeyedService>("7tv"); - if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId)) - { - _logger.Warning("Could not fetch 7tv emotes."); - return; - } - var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*"; - _logger.Debug($"Attempting to connect to {url}"); - await sevenClient.ConnectAsync($"{url}"); + _sevenManager.Initialize(); + await _sevenManager.Connect(); } - catch (Exception) + catch (Exception e) { - _logger.Warning("Connecting to 7tv failed. Skipping 7tv websockets."); + _logger.Error(e, "Connecting to 7tv failed. Skipping 7tv websockets."); } } private async Task InitializeObs() { - if (_configuration.Obs == null || string.IsNullOrWhiteSpace(_configuration.Obs.Host) || !_configuration.Obs.Port.HasValue || _configuration.Obs.Port.Value < 0) - { - _logger.Warning("Lacking OBS connection info. Skipping OBS websockets."); - return; - } - try { - var obsClient = _serviceProvider.GetRequiredKeyedService>("obs"); - var url = $"ws://{_configuration.Obs.Host.Trim()}:{_configuration.Obs.Port}"; - _logger.Debug($"Initializing OBS websocket client. Attempting to connect to {url}"); - await obsClient.ConnectAsync(url); + _obsManager.Initialize(); + await _obsManager.Connect(); } catch (Exception) { @@ -367,7 +349,7 @@ namespace TwitchChatTTS return null; } - var channels = _configuration.Twitch.Channels ?? [username]; + var channels = _configuration.Twitch?.Channels ?? [username]; _logger.Information("Twitch channels: " + string.Join(", ", channels)); twitchapiclient.InitializeClient(username, channels); twitchapiclient.InitializePublisher(); @@ -381,15 +363,7 @@ namespace TwitchChatTTS if (result.Status != MessageStatus.None || result.Emotes == null || !result.Emotes.Any()) return; - var ws = _serviceProvider.GetRequiredKeyedService>("hermes"); - await ws.Send(8, new EmoteUsageMessage() - { - MessageId = e.ChatMessage.Id, - DateTime = DateTime.UtcNow, - BroadcasterId = result.BroadcasterId, - ChatterId = result.ChatterId, - Emotes = result.Emotes - }); + await _hermes.SendEmoteUsage(e.ChatMessage.Id, result.ChatterId, result.Emotes); } catch (Exception ex) { @@ -400,9 +374,9 @@ namespace TwitchChatTTS return twitchapiclient; } - private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet channelEmotes) + private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes) { - var emotes = _serviceProvider.GetRequiredService(); + var emotes = _serviceProvider.GetRequiredService(); var globalEmotes = await sevenapi.FetchGlobalSevenEmotes(); if (channelEmotes != null && channelEmotes.Emotes.Any()) diff --git a/Twitch/Redemptions/Action.cs b/Twitch/Redemptions/Action.cs deleted file mode 100644 index 59f3f77..0000000 --- a/Twitch/Redemptions/Action.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 7fb6740..0000000 --- a/Twitch/Redemptions/Redemption.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 index 2991618..9100811 100644 --- a/Twitch/Redemptions/RedemptionManager.cs +++ b/Twitch/Redemptions/RedemptionManager.cs @@ -1,9 +1,11 @@ using System.Reflection; using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Common; +using HermesSocketLibrary.Requests.Messages; using Microsoft.Extensions.DependencyInjection; using org.mariuszgromada.math.mxparser; using Serilog; +using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Manager; @@ -14,7 +16,7 @@ namespace TwitchChatTTS.Twitch.Redemptions private readonly IDictionary> _store; private readonly User _user; private readonly OBSManager _obsManager; - private readonly SocketClient _hermesClient; + private readonly HermesSocketClient _hermes; private readonly ILogger _logger; private readonly Random _random; private bool _isReady; @@ -23,13 +25,13 @@ namespace TwitchChatTTS.Twitch.Redemptions public RedemptionManager( User user, OBSManager obsManager, - [FromKeyedServices("hermes")] SocketClient hermesClient, + [FromKeyedServices("hermes")] SocketClient hermes, ILogger logger) { _store = new Dictionary>(); _user = user; _obsManager = obsManager; - _hermesClient = hermesClient; + _hermes = (hermes as HermesSocketClient)!; _logger = logger; _random = new Random(); _isReady = false; @@ -46,6 +48,14 @@ namespace TwitchChatTTS.Twitch.Redemptions public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId) { + _logger.Debug($"Executing an action for a redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); + + if (action.Data == null) + { + _logger.Warning($"No data was provided for an action, caused by redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); + return; + } + try { switch (action.Type) @@ -53,12 +63,12 @@ namespace TwitchChatTTS.Twitch.Redemptions case "WRITE_TO_FILE": Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"])); await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName)); - _logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}]"); + _logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]"); 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"], senderDisplayName)); - _logger.Debug($"Appended text to file [file: {action.Data["file_path"]}]"); + _logger.Debug($"Appended text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]"); break; case "OBS_TRANSFORM": var type = typeof(OBSTransformationData); @@ -74,29 +84,30 @@ namespace TwitchChatTTS.Twitch.Redemptions 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}]"); + _logger.Warning($"Failed to find property for OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}][chatter: {senderDisplayName}][chatter id: {senderId}]"); 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}]"); + _logger.Warning($"Found a null value from OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}][chatter: {senderDisplayName}][chatter id: {senderId}]"); + continue; } 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}]"); + _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}][chatter: {senderDisplayName}][chatter id: {senderId}]"); 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($"OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}][old value: {currentValue}][new value: {newValue}][expression: {expressionString}][chatter: {senderDisplayName}][chatter id: {senderId}]"); } - _logger.Debug($"Finished applying the OBS transformation property changes [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}]"); + _logger.Debug($"Finished applying the OBS transformation property changes [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][chatter: {senderDisplayName}][chatter id: {senderId}]"); }); break; case "TOGGLE_OBS_VISIBILITY": @@ -113,63 +124,78 @@ namespace TwitchChatTTS.Twitch.Redemptions await Task.Delay(int.Parse(action.Data["sleep"])); break; case "SPECIFIC_TTS_VOICE": - var voiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id].ToLower() == action.Data["tts_voice"].ToLower()); - if (voiceId == null) + case "RANDOM_TTS_VOICE": + string voiceId = string.Empty; + bool specific = action.Type == "SPECIFIC_TTS_VOICE"; + + var voicesEnabled = _user.VoicesEnabled.ToList(); + if (specific) + voiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id].ToLower() == action.Data["tts_voice"].ToLower()); + else { - _logger.Warning($"Voice specified is not valid [voice: {action.Data["tts_voice"]}]"); + if (!voicesEnabled.Any()) + { + _logger.Warning($"There are no TTS voices enabled [voice pool size: {voicesEnabled.Count}][chatter: {senderDisplayName}][chatter id: {senderId}]"); + return; + } + if (voicesEnabled.Count <= 1) + { + _logger.Warning($"There are not enough TTS voices enabled to randomize [voice pool size: {voicesEnabled.Count}][chatter: {senderDisplayName}][chatter id: {senderId}]"); + return; + } + + string? selectedId = null; + if (!_user.VoicesSelected.ContainsKey(senderId)) + selectedId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id] == _user.DefaultTTSVoice); + else + selectedId = _user.VoicesSelected[senderId]; + + do + { + var randomVoice = voicesEnabled[_random.Next(voicesEnabled.Count)]; + voiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id] == randomVoice); + } while (voiceId == selectedId); + } + if (string.IsNullOrEmpty(voiceId)) + { + _logger.Warning($"Voice is not valid [voice: {action.Data["tts_voice"]}][voice pool size: {voicesEnabled.Count}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]"); return; } var voiceName = _user.VoicesAvailable[voiceId]; if (!_user.VoicesEnabled.Contains(voiceName)) { - _logger.Warning($"Voice specified is not enabled [voice: {action.Data["tts_voice"]}][voice id: {voiceId}]"); + _logger.Warning($"Voice is not enabled [voice: {action.Data["tts_voice"]}][voice pool size: {voicesEnabled.Count}][voice id: {voiceId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]"); return; } - await _hermesClient.Send(3, new HermesSocketLibrary.Socket.Data.RequestMessage() + + if (_user.VoicesSelected.ContainsKey(senderId)) { - Type = _user.VoicesSelected.ContainsKey(senderId) ? "update_tts_user" : "create_tts_user", - Data = new Dictionary() { { "chatter", senderId }, { "voice", voiceId } } - }); - _logger.Debug($"Changed the TTS voice of a chatter [voice: {action.Data["tts_voice"]}][display name: {senderDisplayName}][chatter id: {senderId}]"); - break; - case "RANDOM_TTS_VOICE": - var voicesEnabled = _user.VoicesEnabled.ToList(); - if (!voicesEnabled.Any()) - { - _logger.Warning($"There are no TTS voices enabled [voice pool size: {voicesEnabled.Count}]"); - return; + await _hermes.UpdateTTSUser(senderId, voiceId); + _logger.Debug($"Sent request to create chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]"); } - if (voicesEnabled.Count <= 1) + else { - _logger.Warning($"There are not enough TTS voices enabled to randomize [voice pool size: {voicesEnabled.Count}]"); - return; + await _hermes.CreateTTSUser(senderId, voiceId); + _logger.Debug($"Sent request to update chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]"); } - var randomVoice = voicesEnabled[_random.Next(voicesEnabled.Count)]; - var randomVoiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id] == randomVoice); - await _hermesClient.Send(3, new HermesSocketLibrary.Socket.Data.RequestMessage() - { - Type = _user.VoicesSelected.ContainsKey(senderId) ? "update_tts_user" : "create_tts_user", - Data = new Dictionary() { { "chatter", senderId }, { "voice", randomVoiceId } } - }); - _logger.Debug($"Randomly changed the TTS voice of a chatter [voice: {randomVoice}][display name: {senderDisplayName}][chatter id: {senderId}]"); 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"]}]"); + _logger.Warning($"Cannot find audio file for Twitch channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]"); return; } AudioPlaybackEngine.Instance.PlaySound(action.Data["file_path"]); - _logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}]"); + _logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]"); break; default: - _logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}]"); + _logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); break; } } catch (Exception ex) { - _logger.Error(ex, "Failed to execute a redemption action."); + _logger.Error(ex, $"Failed to execute a redemption action [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); } } @@ -187,9 +213,15 @@ namespace TwitchChatTTS.Twitch.Redemptions { _store.Clear(); - var ordered = redemptions.OrderBy(r => r.Order); + var ordered = redemptions.Where(r => r != null).OrderBy(r => r.Order); foreach (var redemption in ordered) { + if (redemption.ActionName == null) + { + _logger.Warning("Null value found for the action name of a redemption."); + continue; + } + try { if (actions.TryGetValue(redemption.ActionName, out var action) && action != null) diff --git a/Twitch/TwitchApiClient.cs b/Twitch/TwitchApiClient.cs index 5e54383..78a0dbb 100644 --- a/Twitch/TwitchApiClient.cs +++ b/Twitch/TwitchApiClient.cs @@ -6,47 +6,43 @@ using TwitchLib.Api.Core.Exceptions; using TwitchLib.Client.Events; using TwitchLib.Client.Models; using TwitchLib.Communication.Events; -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 TwitchBotAuth _token; private readonly ITwitchClient _client; private readonly ITwitchPubSub _publisher; - private readonly WebClientWrap _web; - private readonly IServiceProvider _serviceProvider; + private readonly User _user; + private readonly Configuration _configuration; + private readonly TwitchBotAuth _token; private readonly ILogger _logger; + private readonly WebClientWrap _web; private bool _initialized; private string _broadcasterId; public TwitchApiClient( - RedemptionManager redemptionManager, - HermesApiClient hermesApiClient, - Configuration configuration, - TwitchBotAuth token, ITwitchClient twitchClient, ITwitchPubSub twitchPublisher, - IServiceProvider serviceProvider, + RedemptionManager redemptionManager, + HermesApiClient hermesApiClient, + User user, + Configuration configuration, + TwitchBotAuth token, ILogger logger ) { _redemptionManager = redemptionManager; _hermesApiClient = hermesApiClient; - _configuration = configuration; - _token = token; _client = twitchClient; _publisher = twitchPublisher; - _serviceProvider = serviceProvider; + _user = user; + _configuration = configuration; + _token = token; _logger = logger; _initialized = false; _broadcasterId = string.Empty; @@ -88,7 +84,7 @@ public class TwitchApiClient } catch (HttpResponseException e) { - if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token)) + if (string.IsNullOrWhiteSpace(_configuration.Hermes!.Token)) _logger.Error("No Hermes API key found. Enter it into the configuration file."); else _logger.Error("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode); @@ -112,7 +108,7 @@ public class TwitchApiClient public void InitializeClient(string username, IEnumerable channels) { - ConnectionCredentials credentials = new ConnectionCredentials(username, _token?.AccessToken); + ConnectionCredentials credentials = new ConnectionCredentials(username, _token!.AccessToken); _client.Initialize(credentials, channels.Distinct().ToList()); if (_initialized) @@ -130,7 +126,7 @@ public class TwitchApiClient _client.OnConnected += async Task (object? s, OnConnectedArgs e) => { - _logger.Information("-----------------------------------------------------------"); + _logger.Information("Twitch API client connected."); }; _client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => @@ -139,9 +135,10 @@ 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.SetConnectionCredentials(new ConnectionCredentials(_user.TwitchUsername, _token!.AccessToken)); + + await Task.Delay(TimeSpan.FromSeconds(3)); + await _client.ReconnectAsync(); }; _client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => @@ -156,6 +153,8 @@ public class TwitchApiClient { _logger.Error(e.Exception, "Twitch API client error."); }; + + _client.OnDisconnected += async Task (s, e) => _logger.Warning("Twitch API client disconnected."); } public void InitializePublisher() @@ -171,38 +170,37 @@ public class TwitchApiClient _publisher.OnFollow += (s, e) => { - var client = _serviceProvider.GetRequiredKeyedService>("obs") as OBSSocketClient; - if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false) - return; - _logger.Information($"New Follower [name: {e.DisplayName}][username: {e.Username}]"); }; _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}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]"); - var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id); - if (!actions.Any()) + try { - _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; - } - _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}]"); + var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id); + if (!actions.Any()) + { + _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; + } + _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 - { - await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName, long.Parse(e.RewardRedeemed.Redemption.User.Id)); - } - catch (Exception ex) - { - _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}]"); - } + foreach (var action in actions) + try + { + await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName, long.Parse(e.RewardRedeemed.Redemption.User.Id)); + } + catch (Exception ex) + { + _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}]"); + } + } + catch (Exception ex) + { + _logger.Error(ex, $"Failed to fetch the redeemable actions for a redemption [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]"); + } }; _publisher.OnPubSubServiceClosed += async (s, e) => diff --git a/TwitchChatTTS.csproj b/TwitchChatTTS.csproj index b09d161..7c11483 100644 --- a/TwitchChatTTS.csproj +++ b/TwitchChatTTS.csproj @@ -25,7 +25,7 @@ - + @@ -35,8 +35,8 @@ - - + +