diff --git a/Chat/ChatMessageHandler.cs b/Chat/ChatMessageHandler.cs index 919a311..14e1d08 100644 --- a/Chat/ChatMessageHandler.cs +++ b/Chat/ChatMessageHandler.cs @@ -186,7 +186,10 @@ public class ChatMessageHandler var voiceId = _user.VoicesSelected[userId]; if (_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null) { - voiceSelected = voiceName; + if (_user.VoicesEnabled.Contains(voiceName) || chatterId == _user.OwnerId || m.IsStaff) + { + voiceSelected = voiceName; + } } } @@ -219,9 +222,7 @@ public class ChatMessageHandler private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e) { if (string.IsNullOrWhiteSpace(message)) - { return; - } var m = e.ChatMessage; var parts = sfxRegex.Split(message); diff --git a/Chat/Commands/AddTTSVoiceCommand.cs b/Chat/Commands/AddTTSVoiceCommand.cs index 6fef20b..f1640bb 100644 --- a/Chat/Commands/AddTTSVoiceCommand.cs +++ b/Chat/Commands/AddTTSVoiceCommand.cs @@ -30,7 +30,7 @@ namespace TwitchChatTTS.Chat.Commands public override async Task CheckPermissions(ChatMessage message, long broadcasterId) { - return message.IsModerator || message.IsBroadcaster; + return false; } public override async Task Execute(IList args, ChatMessage message, long broadcasterId) diff --git a/Chat/Commands/ChatCommandManager.cs b/Chat/Commands/ChatCommandManager.cs index feccd82..2a021b7 100644 --- a/Chat/Commands/ChatCommandManager.cs +++ b/Chat/Commands/ChatCommandManager.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; using Serilog; using TwitchLib.Client.Models; @@ -8,14 +9,16 @@ namespace TwitchChatTTS.Chat.Commands { private IDictionary _commands; private readonly TwitchBotAuth _token; + private readonly User _user; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private string CommandStartSign { get; } = "!"; - public ChatCommandManager(TwitchBotAuth token, IServiceProvider serviceProvider, ILogger logger) + public ChatCommandManager(TwitchBotAuth token, User user, IServiceProvider serviceProvider, ILogger logger) { _token = token; + _user = user; _serviceProvider = serviceProvider; _logger = logger; @@ -65,7 +68,11 @@ namespace TwitchChatTTS.Chat.Commands if (!arg.StartsWith(CommandStartSign)) return ChatCommandResult.Unknown; - string[] parts = arg.Split(" "); + string[] parts = Regex.Matches(arg, "(?[^\"\\n\\s]+|\"[^\"\\n]*\")") + .Cast() + .Select(m => m.Groups["match"].Value) + .Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m) + .ToArray(); string com = parts.First().Substring(CommandStartSign.Length).ToLower(); string[] args = parts.Skip(1).ToArray(); long broadcasterId = long.Parse(_token.BroadcasterId); @@ -77,7 +84,7 @@ namespace TwitchChatTTS.Chat.Commands return ChatCommandResult.Missing; } - if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != "126224566" && !message.IsStaff) + if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != _user.OwnerId?.ToString() && !message.IsStaff) { _logger.Warning($"Chatter is missing permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][cid: {message.UserId}]"); return ChatCommandResult.Permission; @@ -108,7 +115,7 @@ namespace TwitchChatTTS.Chat.Commands return ChatCommandResult.Fail; } - _logger.Information($"Executed the {com} command with the following args: " + string.Join(" ", args)); + _logger.Information($"Executed the {com} command [arguments: {arg}]"); return ChatCommandResult.Success; } } diff --git a/Chat/Commands/OBSCommand.cs b/Chat/Commands/OBSCommand.cs index 65ba80e..bb1d92d 100644 --- a/Chat/Commands/OBSCommand.cs +++ b/Chat/Commands/OBSCommand.cs @@ -40,27 +40,45 @@ namespace TwitchChatTTS.Chat.Commands if (_user == null || _user.VoicesAvailable == null) return; - var voiceName = args[0].ToLower(); - var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; - var action = args[1].ToLower(); + var action = args[0].ToLower(); switch (action) { - case "sleep": - await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary() { { "sleepMillis", 10000 } })); - break; case "get_scene_item_id": - await _manager.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" }, { "rotation", 90 } })); + if (args.Count < 3) + return; + + _logger.Debug($"Getting scene item id via chat command [args: {string.Join(" ", args)}]"); + await _manager.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", args[1] }, { "sourceName", args[2] } })); break; case "transform": + if (args.Count < 5) + return; + + _logger.Debug($"Getting scene item transformation data via chat command [args: {string.Join(" ", args)}]"); await _manager.UpdateTransformation(args[1], args[2], (d) => { - + if (args[3].ToLower() == "rotation") + d.Rotation = int.Parse(args[4]); + else if (args[3].ToLower() == "x") + d.Rotation = int.Parse(args[4]); + else if (args[3].ToLower() == "y") + d.PositionY = int.Parse(args[4]); }); - await _manager.Send(new RequestMessage("Transform", string.Empty, new Dictionary() { { "sceneName", "Generic" }, { "sceneItemId", 90 }, { "rotation", 90 } })); break; - case "remove": - await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary() { { "sleepMillis", 10000 } })); + case "sleep": + if (args.Count < 2) + return; + + _logger.Debug($"Sending OBS to sleep via chat command [args: {string.Join(" ", args)}]"); + await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary() { { "sleepMillis", int.Parse(args[1]) } })); + break; + case "visibility": + if (args.Count < 4) + return; + + _logger.Debug($"Updating scene item visibility via chat command [args: {string.Join(" ", args)}]"); + await _manager.UpdateSceneItemVisibility(args[1], args[2], args[3].ToLower() == "true"); break; default: break; diff --git a/Chat/Commands/RefreshTTSDataCommand.cs b/Chat/Commands/RefreshTTSDataCommand.cs index 0acfe0a..0a52ea7 100644 --- a/Chat/Commands/RefreshTTSDataCommand.cs +++ b/Chat/Commands/RefreshTTSDataCommand.cs @@ -1,4 +1,5 @@ using Serilog; +using TwitchChatTTS.Twitch.Redemptions; using TwitchLib.Client.Models; namespace TwitchChatTTS.Chat.Commands @@ -6,13 +7,15 @@ namespace TwitchChatTTS.Chat.Commands public class RefreshTTSDataCommand : ChatCommand { private readonly User _user; + private readonly RedemptionManager _redemptionManager; private readonly HermesApiClient _hermesApi; private readonly ILogger _logger; - public RefreshTTSDataCommand(User user, HermesApiClient hermesApi, ILogger logger) + public RefreshTTSDataCommand(User user, RedemptionManager redemptionManager, HermesApiClient hermesApi, ILogger logger) : base("refresh", "Refreshes certain TTS related data on the client.") { _user = user; + _redemptionManager = redemptionManager; _hermesApi = hermesApi; _logger = logger; } @@ -51,7 +54,13 @@ namespace TwitchChatTTS.Chat.Commands break; case "default_voice": _user.DefaultTTSVoice = await _hermesApi.FetchTTSDefaultVoice(); - _logger.Information("Default Voice: " + _user.DefaultTTSVoice); + _logger.Information("TTS Default Voice: " + _user.DefaultTTSVoice); + break; + case "redemptions": + var redemptionActions = await _hermesApi.FetchRedeemableActions(); + var redemptions = await _hermesApi.FetchRedemptions(); + _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a)); + _logger.Information($"Redemption Manager has been refreshed with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); break; } } diff --git a/Chat/Commands/RemoveTTSVoiceCommand.cs b/Chat/Commands/RemoveTTSVoiceCommand.cs index 73aa0f7..dd7aca8 100644 --- a/Chat/Commands/RemoveTTSVoiceCommand.cs +++ b/Chat/Commands/RemoveTTSVoiceCommand.cs @@ -30,7 +30,7 @@ namespace TwitchChatTTS.Chat.Commands public override async Task CheckPermissions(ChatMessage message, long broadcasterId) { - return message.IsBroadcaster; + return false; } public override async Task Execute(IList args, ChatMessage message, long broadcasterId) diff --git a/Chat/Commands/TTSCommand.cs b/Chat/Commands/TTSCommand.cs index b95e7eb..e24e3ec 100644 --- a/Chat/Commands/TTSCommand.cs +++ b/Chat/Commands/TTSCommand.cs @@ -32,7 +32,7 @@ namespace TwitchChatTTS.Chat.Commands public override async Task CheckPermissions(ChatMessage message, long broadcasterId) { - return message.IsBroadcaster; + return message.IsModerator || message.IsBroadcaster; } public override async Task Execute(IList args, ChatMessage message, long broadcasterId) @@ -52,6 +52,7 @@ namespace TwitchChatTTS.Chat.Commands 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() @@ -59,17 +60,9 @@ namespace TwitchChatTTS.Chat.Commands Type = "update_tts_voice_state", Data = new Dictionary() { { "voice", voiceId }, { "state", false } } }); - break; - case "remove": - await _hermesClient.Send(3, new RequestMessage() - { - Type = "delete_tts_voice", - Data = new Dictionary() { { "voice", voiceId } } - }); + _logger.Information($"Disabled a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]"); break; } - - _logger.Information($"Added a new TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]"); } } } \ No newline at end of file diff --git a/Chat/Commands/VoiceCommand.cs b/Chat/Commands/VoiceCommand.cs index d88074f..4e2b25d 100644 --- a/Chat/Commands/VoiceCommand.cs +++ b/Chat/Commands/VoiceCommand.cs @@ -35,19 +35,23 @@ namespace TwitchChatTTS.Chat.Commands public override async Task Execute(IList args, ChatMessage message, long broadcasterId) { - if (_user == null || _user.VoicesSelected == null || _user.VoicesAvailable == null) + if (_user == null || _user.VoicesSelected == null || _user.VoicesEnabled == null) return; long chatterId = long.Parse(message.UserId); var voiceName = args.First().ToLower(); var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName); + var enabled = _user.VoicesEnabled.Contains(voice.Value); - await _hermesClient.Send(3, new RequestMessage() + if (enabled) { - Type = _user.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user", - Data = new Dictionary() { { "chatter", chatterId }, { "voice", voice.Key } } - }); - _logger.Information($"Updated chat TTS voice [voice: {voice.Value}][username: {message.Username}]."); + 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}]."); + } } } } \ No newline at end of file diff --git a/Hermes/Socket/Handlers/LoginAckHandler.cs b/Hermes/Socket/Handlers/LoginAckHandler.cs index d6509f1..35e45b6 100644 --- a/Hermes/Socket/Handlers/LoginAckHandler.cs +++ b/Hermes/Socket/Handlers/LoginAckHandler.cs @@ -29,6 +29,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers if (message.AnotherClient) { _logger.Warning("Another client has connected to the same account."); + return; } else { @@ -36,6 +37,8 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers _logger.Information($"Logged in as {_user.TwitchUsername}."); } + _user.OwnerId = message.OwnerId; + await client.Send(3, new RequestMessage() { Type = "get_tts_voices", diff --git a/Hermes/Socket/Handlers/RequestAckHandler.cs b/Hermes/Socket/Handlers/RequestAckHandler.cs index bf08c43..d255572 100644 --- a/Hermes/Socket/Handlers/RequestAckHandler.cs +++ b/Hermes/Socket/Handlers/RequestAckHandler.cs @@ -12,6 +12,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers { public class RequestAckHandler : IWebSocketHandler { + private User _user; private readonly IServiceProvider _serviceProvider; private readonly JsonSerializerOptions _options; private readonly ILogger _logger; @@ -20,8 +21,9 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers public int OperationCode { get; } = 4; - public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger) + public RequestAckHandler(User user, IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger) { + _user = user; _serviceProvider = serviceProvider; _options = options; _logger = logger; @@ -33,8 +35,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers return; if (message.Request == null) return; - var context = _serviceProvider.GetRequiredService(); - if (context == null) + if (_user == null) return; if (message.Request.Type == "get_tts_voices") @@ -46,7 +47,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers lock (_voicesAvailableLock) { - context.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name); + _user.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name); } _logger.Information("Updated all available voices for TTS."); } @@ -54,22 +55,28 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers { _logger.Verbose("Adding new tts voice for user."); if (!long.TryParse(message.Request.Data["user"].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(); - context.VoicesSelected.Add(chatterId, voice); + _user.VoicesSelected.Add(chatterId, voice); _logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]"); } else if (message.Request.Type == "update_tts_user") { _logger.Verbose("Updating user's voice"); 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(); - context.VoicesSelected[chatterId] = voice; + _user.VoicesSelected[chatterId] = voice; _logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]"); } else if (message.Request.Type == "create_tts_voice") @@ -82,9 +89,9 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers lock (_voicesAvailableLock) { - var list = context.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value); + var list = _user.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value); list.Add(voiceId, voice); - context.VoicesAvailable = list; + _user.VoicesAvailable = list; } _logger.Information($"Created new tts voice [voice: {voice}][id: {voiceId}]."); } @@ -92,14 +99,14 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers { _logger.Verbose("Deleting tts voice."); var voice = message.Request.Data["voice"].ToString(); - if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) + if (!_user.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) return; lock (_voicesAvailableLock) { - var dict = context.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value); + var dict = _user.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value); dict.Remove(voice); - context.VoicesAvailable.Remove(voice); + _user.VoicesAvailable.Remove(voice); } _logger.Information($"Deleted a voice [voice: {voiceName}]"); } @@ -109,10 +116,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers string voiceId = message.Request.Data["idd"].ToString(); string voice = message.Request.Data["voice"].ToString(); - if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) + if (!_user.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) return; - context.VoicesAvailable[voiceId] = voice; + _user.VoicesAvailable[voiceId] = voice; _logger.Information($"Updated TTS voice [voice: {voice}][id: {voiceId}]"); } else if (message.Request.Type == "get_tts_users") @@ -125,7 +132,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers var temp = new ConcurrentDictionary(); foreach (var entry in users) temp.TryAdd(entry.Key, entry.Value); - context.VoicesSelected = temp; + _user.VoicesSelected = temp; _logger.Information($"Updated {temp.Count()} chatters' selected voice."); } else if (message.Request.Type == "get_chatter_ids") @@ -162,18 +169,18 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers { _logger.Verbose("Updating TTS voice states."); string voiceId = message.Request.Data["voice"].ToString(); - bool state = message.Request.Data["state"].ToString() == "true"; + bool state = message.Request.Data["state"].ToString().ToLower() == "true"; - if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) + if (!_user.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) { - _logger.Warning($"Failed to find voice [id: {voiceId}]"); + _logger.Warning($"Failed to find voice by id [id: {voiceId}]"); return; } if (state) - context.VoicesEnabled.Add(voiceId); + _user.VoicesEnabled.Add(voiceId); else - context.VoicesEnabled.Remove(voiceId); + _user.VoicesEnabled.Remove(voiceId); _logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]"); } } diff --git a/Hermes/Socket/HermesSocketClient.cs b/Hermes/Socket/HermesSocketClient.cs index 17e242b..00d88eb 100644 --- a/Hermes/Socket/HermesSocketClient.cs +++ b/Hermes/Socket/HermesSocketClient.cs @@ -100,7 +100,12 @@ namespace TwitchChatTTS.Hermes.Socket LastHeartbeatReceived = DateTime.UtcNow; if (_configuration.Hermes?.Token != null) - await Send(1, new HermesLoginMessage() { ApiKey = _configuration.Hermes.Token }); + await Send(1, new HermesLoginMessage() + { + ApiKey = _configuration.Hermes.Token, + MajorVersion = TTS.MAJOR_VERSION, + MinorVersion = TTS.MINOR_VERSION, + }); } } } diff --git a/Hermes/TwitchBotAuth.cs b/Hermes/TwitchBotAuth.cs index 6576217..cd065ca 100644 --- a/Hermes/TwitchBotAuth.cs +++ b/Hermes/TwitchBotAuth.cs @@ -1,6 +1,21 @@ -public class TwitchBotAuth { +public class TwitchBotAuth +{ public string? UserId { get; set; } public string? AccessToken { get; set; } public string? RefreshToken { get; set; } public string? BroadcasterId { get; set; } + public long? ExpiresIn + { + get => _expiresIn; + set + { + _expiresIn = value; + if (value != null) + ExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds((double) value); + } + } + public DateTime ExpiresAt { get; set; } + public DateTime UpdatedAt { get; set; } + + private long? _expiresIn; } \ No newline at end of file diff --git a/OBS/Socket/Handlers/IdentifiedHandler.cs b/OBS/Socket/Handlers/IdentifiedHandler.cs index 6a2dbde..e7511c9 100644 --- a/OBS/Socket/Handlers/IdentifiedHandler.cs +++ b/OBS/Socket/Handlers/IdentifiedHandler.cs @@ -22,14 +22,6 @@ namespace TwitchChatTTS.OBS.Socket.Handlers sender.Connected = true; _logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + "."); - - await Task.Delay(TimeSpan.FromSeconds(5)); - - /*var messages = new RequestMessage[] { - //new RequestMessage("Sleep", string.Empty, new Dictionary() { { "sleepMillis", 5000 } }), - new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" } }), - }; - await _manager.Send(messages);*/ } } } \ No newline at end of file diff --git a/OBS/Socket/Handlers/RequestResponseHandler.cs b/OBS/Socket/Handlers/RequestResponseHandler.cs index 34a4d71..0e2ee0b 100644 --- a/OBS/Socket/Handlers/RequestResponseHandler.cs +++ b/OBS/Socket/Handlers/RequestResponseHandler.cs @@ -23,12 +23,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers if (data is not RequestResponseMessage message || message == null) return; - _logger.Debug($"Received an OBS request response [response id: {message.RequestId}]"); + _logger.Debug($"Received an OBS request response [obs request id: {message.RequestId}]"); var requestData = _manager.Take(message.RequestId); if (requestData == null) { - _logger.Warning($"OBS Request Response not being processed: request not stored [response id: {message.RequestId}]"); + _logger.Warning($"OBS Request Response not being processed: request not stored [obs request id: {message.RequestId}]"); return; } @@ -44,54 +44,149 @@ namespace TwitchChatTTS.OBS.Socket.Handlers if (sender is not OBSSocketClient client) return; - if (message.RequestId == "stream") - { - client.Live = message.ResponseData["outputActive"].ToString() == "True"; - _logger.Warning($"Updated stream's live status to {client.Live} [response id: {message.RequestId}]"); - } + _logger.Debug($"Fetched stream's live status [live: {client.Live}][obs request id: {message.RequestId}]"); break; case "GetSceneItemId": - if (!request.RequestData.TryGetValue("sceneName", out object sceneName)) { - _logger.Warning($"Failed to find the scene name that was requested [response id: {message.RequestId}]"); - return; - } - if (!request.RequestData.TryGetValue("sourceName", out object sourceName)) - { - _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][response id: {message.RequestId}]"); - return; - } - if (!message.ResponseData.TryGetValue("sceneItemId", out object sceneItemId)) { - _logger.Warning($"Failed to fetch the scene item id [scene: {sceneName}][scene item: {sourceName}][response id: {message.RequestId}]"); - return; - } + if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) + { + _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); + return; + } + if (!request.RequestData.TryGetValue("sourceName", out object? sourceName) || sourceName == null) + { + _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); + return; + } + if (message.ResponseData == null) + { + _logger.Warning($"OBS Response is null [scene: {sceneName}][scene item: {sourceName}][obs request id: {message.RequestId}]"); + return; + } + if (!message.ResponseData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) + { + _logger.Warning($"Failed to fetch the scene item id [scene: {sceneName}][scene item: {sourceName}][obs request id: {message.RequestId}]"); + return; + } - _logger.Information($"Added scene item id [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][response id: {message.RequestId}]."); - _manager.AddSourceId(sceneName.ToString(), sourceName.ToString(), long.Parse(sceneItemId.ToString())); + _logger.Debug($"Found the scene item by name [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][obs request id: {message.RequestId}]."); + //_manager.AddSourceId(sceneName.ToString(), sourceName.ToString(), (long) sceneItemId); - requestData.ResponseValues = new Dictionary - { - { "sceneItemId", sceneItemId } - }; - break; + requestData.ResponseValues = new Dictionary + { + { "sceneItemId", sceneItemId } + }; + break; + } case "GetSceneItemTransform": - if (!message.ResponseData.TryGetValue("sceneItemTransform", out object? transformData)) { - _logger.Warning($"Failed to find the OBS scene item [response id: {message.RequestId}]"); - return; - } + if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) + { + _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); + return; + } + if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) + { + _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); + return; + } + if (message.ResponseData == null) + { + _logger.Warning($"OBS Response is null [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); + return; + } + if (!message.ResponseData.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null) + { + _logger.Warning($"Failed to fetch the OBS transformation data [obs request id: {message.RequestId}]"); + return; + } - _logger.Verbose("Fetching OBS transformation data: " + transformData?.ToString()); - requestData.ResponseValues = new Dictionary + _logger.Debug($"Fetched OBS transformation data [scene: {sceneName}][scene item id: {sceneItemId}][transformation: {transformData}][obs request id: {message.RequestId}]"); + requestData.ResponseValues = new Dictionary + { + { "sceneItemTransform", transformData } + }; + break; + } + case "GetSceneItemEnabled": { - { "sceneItemTransform", transformData } - }; - break; + if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) + { + _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); + return; + } + if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) + { + _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); + return; + } + if (message.ResponseData == null) + { + _logger.Warning($"OBS Response is null [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); + return; + } + if (!message.ResponseData.TryGetValue("sceneItemEnabled", out object? sceneItemVisibility) || sceneItemVisibility == null) + { + _logger.Warning($"Failed to fetch the scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); + return; + } + + _logger.Debug($"Fetched OBS scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][visibility: {sceneItemVisibility}][obs request id: {message.RequestId}]"); + requestData.ResponseValues = new Dictionary + { + { "sceneItemEnabled", sceneItemVisibility } + }; + break; + } + case "SetSceneItemTransform": + { + if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) + { + _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); + return; + } + if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) + { + _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); + return; + } + _logger.Debug($"Received response from OBS for updating scene item transformation [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); + break; + } + case "SetSceneItemEnabled": + { + if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) + { + _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); + return; + } + if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) + { + _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); + return; + } + _logger.Debug($"Received response from OBS for updating scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); + break; + } + case "Sleep": + { + if (!request.RequestData.TryGetValue("sleepMillis", out object? sleepMillis) || sleepMillis == null) + { + _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); + return; + } + _logger.Debug($"Received response from OBS for sleeping [sleep: {sleepMillis}][obs request id: {message.RequestId}]"); + break; + } default: - _logger.Warning($"OBS Request Response not being processed [type: {request.RequestType}][{string.Join(Environment.NewLine, message.ResponseData?.Select(kvp => kvp.Key + " = " + kvp.Value?.ToString()) ?? new string[0])}]"); + _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; } } + catch (Exception ex) + { + _logger.Error(ex, $"Failed to process the response from OBS for a request [type: {request.RequestType}]"); + } finally { if (requestData.Callback != null) diff --git a/OBS/Socket/Manager/OBSManager.cs b/OBS/Socket/Manager/OBSManager.cs index a035785..400fabb 100644 --- a/OBS/Socket/Manager/OBSManager.cs +++ b/OBS/Socket/Manager/OBSManager.cs @@ -43,7 +43,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager public async Task Send(IEnumerable messages) { string uid = GenerateUniqueIdentifier(); - _logger.Debug($"Sending OBS request batch of {messages.Count()} messages [obsid: {uid}]."); + _logger.Debug($"Sending OBS request batch of {messages.Count()} messages [obs request id: {uid}]."); // Keep track of requests to know what we requested. foreach (var message in messages) @@ -52,7 +52,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager var data = new RequestData(message, uid); _requests.Add(message.RequestId, data); } - _logger.Debug($"Generated uid for all OBS request messages in batch [obsid: {uid}]: {string.Join(", ", messages.Select(m => m.RequestType + "=" + m.RequestId))}"); + _logger.Debug($"Generated uid for all OBS request messages in batch [obs request id: {uid}][obs request ids: {string.Join(", ", messages.Select(m => m.RequestType + "=" + m.RequestId))}]"); var client = _serviceProvider.GetRequiredKeyedService>("obs"); await client.Send(8, new RequestBatchMessage(uid, messages)); @@ -61,7 +61,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager public async Task Send(RequestMessage message, Action>? callback = null) { string uid = GenerateUniqueIdentifier(); - _logger.Debug($"Sending an OBS request [obsid: {uid}]"); + _logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]"); // Keep track of requests to know what we requested. message.RequestId = GenerateUniqueIdentifier(); @@ -87,22 +87,18 @@ namespace TwitchChatTTS.OBS.Socket.Manager public async Task UpdateTransformation(string sceneName, string sceneItemName, Action action) { - var m1 = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sourceName", sceneItemName } }); - await Send(m1, async (d) => - { - if (!d.TryGetValue("sceneItemId", out object value) || !long.TryParse(value.ToString(), out long sceneItemId)) - return; + if (action == null) + return; - _logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][obsid: {m1.RequestId}]: {sceneItemId}"); + await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => + { var m2 = new RequestMessage("GetSceneItemTransform", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); await Send(m2, async (d) => { - if (d == null) - return; - if (!d.TryGetValue("sceneItemTransform", out object transformData)) + if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null) return; - _logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obsid: {m2.RequestId}]: {transformData}"); + _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() { PropertyNameCaseInsensitive = false, @@ -110,88 +106,121 @@ namespace TwitchChatTTS.OBS.Socket.Manager }); if (transform == null) { - _logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obsid: {m2.RequestId}]."); + _logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m2.RequestId}]."); return; } - //double fr = (transform.Rotation + rotation) % 360; double w = transform.Width; double h = transform.Height; - - // double ox = w * Math.Cos(r) - h * Math.Sin(r); - // double oy = w * Math.Sin(r) + h * Math.Cos(r); - //var oo = (fr > 45 && fr < 225 ? 0 : 1); - // var ww = fr >= 135 && fr < 225 ? h : w; - // var hh = fr >= 315 || fr < 45 ? h : w; - //double dx = h * Math.Sin(r); - //double dy = w * Math.Cos(fr > 90 && fr < 270 ? Math.PI - r : r); // * (fr >= 135 && fr < 225 || fr >= 315 || fr <= 45 ? -1 : 1); - int a = transform.Alignment; bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE"; - if (hasBounds) - { - // Take care of bounds, for most cases. - // 'Crop to Bounding Box' might be unsupported. - w = transform.BoundsWidth; - h = transform.BoundsHeight; - a = transform.BoundsAlignment; - } - else if (transform.CropBottom + transform.CropLeft + transform.CropRight + transform.CropTop > 0) - { - w -= transform.CropLeft + transform.CropRight; - h -= transform.CropTop + transform.CropBottom; - } - if (a != (int)OBSAlignment.Center) { if (hasBounds) transform.BoundsAlignment = a = (int)OBSAlignment.Center; else transform.Alignment = a = (int)OBSAlignment.Center; + transform.PositionX = transform.PositionX + w / 2; transform.PositionY = transform.PositionY + h / 2; } + // 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); - // double ax = w * Math.Cos(ir) - h * Math.Sin(ir); - // double ay = w * Math.Sin(ir) + h * Math.Cos(ir); - // _logger.Information($"ax: {ax} ay: {ay}"); - - // double bx = w * Math.Cos(r) - h * Math.Sin(r); - // double by = w * Math.Sin(r) + h * Math.Cos(r); - // _logger.Information($"bx: {bx} by: {by}"); - - // double ddx = bx - ax; - // double ddy = by - ay; - // _logger.Information($"dx: {ddx} dy: {ddy}"); - - // double arctan = Math.Atan(ddy / ddx); - // _logger.Information("Angle: " + arctan); - - // var xs = new int[] { 0, 0, 1, 1 }; - // var ys = new int[] { 0, 1, 1, 0 }; - // int i = ((int)Math.Floor(fr / 90) + 8) % 4; - // double dx = xs[i] * w * Math.Cos(rad) - ys[i] * h * Math.Sin(rad); - // double dy = xs[i] * w * Math.Sin(rad) + ys[i] * h * Math.Cos(rad); - - - //transform.Rotation = fr; - //_logger.Information($"w: {w} h: {h} fr: {fr} r: {r} rot: {rotation}"); - //_logger.Information($"dx: {dx} ox: {ox} oox: {oox}"); - //_logger.Information($"dy: {dy} oy: {oy} ooy: {ooy}"); - var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } }); await Send(m3); + _logger.Debug($"New transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]"); }); }); } + public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName) + { + LogExceptions(async () => + { + await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => + { + var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); + await Send(m1, async (d) => + { + if (d == null || !d.TryGetValue("sceneItemEnabled", out object? visible) || visible == null) + return; + + var m2 = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", visible.ToString().ToLower() == "true" ? false : true } }); + await Send(m2); + }); + }); + }, "Failed to toggle OBS scene item visibility."); + } + + public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible) + { + LogExceptions(async () => + { + await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => + { + var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } }); + await Send(m); + }); + }, "Failed to update OBS scene item visibility."); + } + + public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index) + { + LogExceptions(async () => + { + await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => + { + var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } }); + await Send(m); + }); + }, "Failed to update OBS scene item index."); + } + + private async Task GetSceneItemById(string sceneName, string sceneItemName, Action action) + { + var m1 = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary() { { "sceneName", sceneName }, { "sourceName", sceneItemName } }); + await Send(m1, async (d) => + { + if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId)) + return; + + _logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sceneItemId}][obs request id: {m1.RequestId}]"); + action.Invoke(sceneItemId); + }); + } + private string GenerateUniqueIdentifier() { 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/Seven/SevenApiClient.cs b/Seven/SevenApiClient.cs index 8d2d921..e91d6f1 100644 --- a/Seven/SevenApiClient.cs +++ b/Seven/SevenApiClient.cs @@ -2,12 +2,11 @@ using System.Text.Json; using TwitchChatTTS.Helpers; using Serilog; using TwitchChatTTS.Seven; -using TwitchChatTTS; public class SevenApiClient { - public static readonly string API_URL = "https://7tv.io/v3"; - public static readonly string WEBSOCKET_URL = "wss://events.7tv.io/v3"; + public const string API_URL = "https://7tv.io/v3"; + public const string WEBSOCKET_URL = "wss://events.7tv.io/v3"; private readonly WebClientWrap _web; private readonly ILogger _logger; @@ -32,11 +31,11 @@ public class SevenApiClient } catch (JsonException e) { - _logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON."); + _logger.Error(e, "Failed to fetch channel emotes from 7tv due to improper JSON."); } catch (Exception e) { - _logger.Error(e, "Failed to fetch emotes from 7tv."); + _logger.Error(e, "Failed to fetch channel emotes from 7tv."); } return null; } @@ -50,11 +49,11 @@ public class SevenApiClient } catch (JsonException e) { - _logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON."); + _logger.Error(e, "Failed to fetch global emotes from 7tv due to improper JSON."); } catch (Exception e) { - _logger.Error(e, "Failed to fetch emotes from 7tv."); + _logger.Error(e, "Failed to fetch global emotes from 7tv."); } return null; } diff --git a/TTS.cs b/TTS.cs index d6b185e..0d4b48f 100644 --- a/TTS.cs +++ b/TTS.cs @@ -17,8 +17,11 @@ namespace TwitchChatTTS public class TTS : IHostedService { public const int MAJOR_VERSION = 3; - public const int MINOR_VERSION = 3; + public const int MINOR_VERSION = 6; + private readonly User _user; + private readonly HermesApiClient _hermesApiClient; + private readonly SevenApiClient _sevenApiClient; private readonly RedemptionManager _redemptionManager; private readonly Configuration _configuration; private readonly TTSPlayer _player; @@ -36,6 +39,9 @@ namespace TwitchChatTTS ILogger logger ) { + _user = user; + _hermesApiClient = hermesApiClient; + _sevenApiClient = sevenApiClient; _redemptionManager = redemptionManager; _configuration = configuration; _player = player; @@ -46,12 +52,14 @@ namespace TwitchChatTTS public async Task StartAsync(CancellationToken cancellationToken) { Console.Title = "TTS - Twitch Chat"; + License.iConfirmCommercialUse("abcdef"); - var user = _serviceProvider.GetRequiredService(); - var hermes = _serviceProvider.GetRequiredService(); - var seven = _serviceProvider.GetRequiredService(); + if (string.IsNullOrWhiteSpace(_configuration.Hermes.Token)) { + _logger.Error("Hermes API token not set in the configuration file."); + return; + } - var hermesVersion = await hermes.GetTTSVersion(); + var hermesVersion = await _hermesApiClient.GetTTSVersion(); if (hermesVersion.MajorVersion > TTS.MAJOR_VERSION || hermesVersion.MajorVersion == TTS.MAJOR_VERSION && hermesVersion.MinorVersion > TTS.MINOR_VERSION) { _logger.Information($"A new update for TTS is avaiable! Version {hermesVersion.MajorVersion}.{hermesVersion.MinorVersion} is available at {hermesVersion.Download}"); @@ -63,7 +71,7 @@ namespace TwitchChatTTS try { - await FetchUserData(user, hermes, seven); + await FetchUserData(_user, _hermesApiClient, _sevenApiClient); } catch (Exception ex) { @@ -71,19 +79,15 @@ namespace TwitchChatTTS await Task.Delay(30 * 1000); } - var twitchapiclient = await InitializeTwitchApiClient(user.TwitchUsername, user.TwitchUserId.ToString()); + var twitchapiclient = await InitializeTwitchApiClient(_user.TwitchUsername, _user.TwitchUserId.ToString()); if (twitchapiclient == null) { await Task.Delay(30 * 1000); return; } - var emoteSet = await seven.FetchChannelEmoteSet(user.TwitchUserId.ToString()); - user.SevenEmoteSetId = emoteSet?.Id; - - License.iConfirmCommercialUse("abcdef"); - - await InitializeEmotes(seven, emoteSet); + var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString()); + await InitializeEmotes(_sevenApiClient, emoteSet); await InitializeHermesWebsocket(); await InitializeSevenTv(emoteSet.Id); await InitializeObs(); @@ -104,7 +108,7 @@ namespace TwitchChatTTS { if (cancellationToken.IsCancellationRequested) { - _logger.Warning("TTS Buffer - Cancellation token was canceled."); + _logger.Warning("TTS Buffer -Cancellation requested."); return; } @@ -144,7 +148,7 @@ namespace TwitchChatTTS { if (cancellationToken.IsCancellationRequested) { - _logger.Warning("TTS Queue - Cancellation token was canceled."); + _logger.Warning("TTS Queue - Cancellation requested."); return; } while (_player.IsEmpty() || _player.Playing != null) @@ -160,12 +164,12 @@ namespace TwitchChatTTS if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File)) { - _logger.Information("Playing message: " + m.File); + _logger.Debug("Playing audio file via TTS: " + m.File); AudioPlaybackEngine.Instance.PlaySound(m.File); continue; } - _logger.Information("Playing message: " + m.Message); + _logger.Debug("Playing message via TTS: " + m.Message); _player.Playing = m.Audio; if (m.Audio != null) AudioPlaybackEngine.Instance.AddMixerInput(m.Audio); @@ -204,7 +208,7 @@ namespace TwitchChatTTS _logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]"); user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice(); - _logger.Information("Default Voice: " + user.DefaultTTSVoice); + _logger.Information("TTS Default Voice: " + user.DefaultTTSVoice); var wordFilters = await hermes.FetchTTSWordFilters(); user.RegexFilters = wordFilters.ToList(); @@ -232,12 +236,8 @@ namespace TwitchChatTTS var redemptionActions = await hermes.FetchRedeemableActions(); var redemptions = await hermes.FetchRedemptions(); - foreach (var action in redemptionActions) - _redemptionManager.AddAction(action); - foreach (var redemption in redemptions) - _redemptionManager.AddTwitchRedemption(redemption); - _redemptionManager.Ready(); - _logger.Information($"Redemption Manager is ready with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); + _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a)); + _logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); } private async Task InitializeHermesWebsocket() @@ -252,7 +252,9 @@ namespace TwitchChatTTS hermesClient.Connected = true; await hermesClient.Send(1, new HermesLoginMessage() { - ApiKey = _configuration.Hermes.Token + ApiKey = _configuration.Hermes.Token, + MajorVersion = TTS.MAJOR_VERSION, + MinorVersion = TTS.MINOR_VERSION, }); } catch (Exception) @@ -346,10 +348,9 @@ namespace TwitchChatTTS return twitchapiclient; } - private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet emoteSet) + private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet channelEmotes) { var emotes = _serviceProvider.GetRequiredService(); - var channelEmotes = emoteSet; var globalEmotes = await sevenapi.FetchGlobalSevenEmotes(); if (channelEmotes != null && channelEmotes.Emotes.Any()) diff --git a/Twitch/Redemptions/RedemptionManager.cs b/Twitch/Redemptions/RedemptionManager.cs index 71c24e6..2991618 100644 --- a/Twitch/Redemptions/RedemptionManager.cs +++ b/Twitch/Redemptions/RedemptionManager.cs @@ -1,4 +1,7 @@ using System.Reflection; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; using org.mariuszgromada.math.mxparser; using Serilog; using TwitchChatTTS.OBS.Socket.Data; @@ -8,43 +11,40 @@ namespace TwitchChatTTS.Twitch.Redemptions { public class RedemptionManager { - private readonly IList _redemptions; - private readonly IDictionary _actions; private readonly IDictionary> _store; + private readonly User _user; private readonly OBSManager _obsManager; + private readonly SocketClient _hermesClient; private readonly ILogger _logger; + private readonly Random _random; private bool _isReady; - public RedemptionManager(OBSManager obsManager, ILogger logger) + public RedemptionManager( + User user, + OBSManager obsManager, + [FromKeyedServices("hermes")] SocketClient hermesClient, + ILogger logger) { - _redemptions = new List(); - _actions = new Dictionary(); _store = new Dictionary>(); + _user = user; _obsManager = obsManager; + _hermesClient = hermesClient; _logger = logger; + _random = new Random(); _isReady = false; } - public void AddTwitchRedemption(Redemption redemption) - { - _redemptions.Add(redemption); - } - - public void AddAction(RedeemableAction action) - { - _actions.Add(action.Name, action); - } - private void Add(string twitchRedemptionId, RedeemableAction action) { if (!_store.TryGetValue(twitchRedemptionId, out var actions)) _store.Add(twitchRedemptionId, actions = new List()); + actions.Add(action); - _store[twitchRedemptionId] = actions.OrderBy(a => a).ToList(); + _logger.Debug($"Added redemption action [name: {action.Name}][type: {action.Type}]"); } - public async Task Execute(RedeemableAction action, string sender) + public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId) { try { @@ -52,12 +52,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"], sender)); + await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName)); _logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}]"); break; case "APPEND_TO_FILE": Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"])); - await File.AppendAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], sender)); + await File.AppendAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName)); _logger.Debug($"Appended text to file [file: {action.Data["file_path"]}]"); break; case "OBS_TRANSFORM": @@ -99,8 +99,63 @@ namespace TwitchChatTTS.Twitch.Redemptions _logger.Debug($"Finished applying the OBS transformation property changes [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}]"); }); break; + case "TOGGLE_OBS_VISIBILITY": + await _obsManager.ToggleSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"]); + break; + case "SPECIFIC_OBS_VISIBILITY": + await _obsManager.UpdateSceneItemVisibility(action.Data["scene_name"], action.Data["scene_item_name"], action.Data["obs_visible"].ToLower() == "true"); + break; + case "SPECIFIC_OBS_INDEX": + await _obsManager.UpdateSceneItemIndex(action.Data["scene_name"], action.Data["scene_item_name"], int.Parse(action.Data["obs_index"])); + break; + case "SLEEP": + _logger.Debug("Sleeping on thread due to redemption for OBS."); + 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) + { + _logger.Warning($"Voice specified is not valid [voice: {action.Data["tts_voice"]}]"); + 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}]"); + return; + } + 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", 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; + } + if (voicesEnabled.Count <= 1) + { + _logger.Warning($"There are not enough TTS voices enabled to randomize [voice pool size: {voicesEnabled.Count}]"); + return; + } + 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"])) { + if (!File.Exists(action.Data["file_path"])) + { _logger.Warning($"Cannot find audio file for Twitch channel point redeem [file: {action.Data["file_path"]}]"); return; } @@ -108,7 +163,7 @@ namespace TwitchChatTTS.Twitch.Redemptions _logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}]"); break; default: - _logger.Warning($"Unknown redeemable action has occured [type: {action.Type}]"); + _logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}]"); break; } } @@ -128,21 +183,37 @@ namespace TwitchChatTTS.Twitch.Redemptions return new List(0); } - public void Ready() + public void Initialize(IEnumerable redemptions, IDictionary actions) { - var ordered = _redemptions.OrderBy(r => r.Order); _store.Clear(); + var ordered = redemptions.OrderBy(r => r.Order); foreach (var redemption in ordered) - if (_actions.TryGetValue(redemption.ActionName, out var action) && action != null) - Add(redemption.RedemptionId, action); - + { + try + { + if (actions.TryGetValue(redemption.ActionName, out var action) && action != null) + { + _logger.Debug($"Fetched a redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); + Add(redemption.RedemptionId, action); + } + else + _logger.Warning($"Could not find redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); + } + catch (Exception e) + { + _logger.Error(e, $"Failed to add a redemption [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); + } + } + _isReady = true; - _logger.Debug("Redemption Manager is ready."); + _logger.Debug("All redemptions added. Redemption Manager is ready."); } - private string ReplaceContentText(string content, string username) { - return content.Replace("%USER%", username); + private string ReplaceContentText(string content, string username) + { + return content.Replace("%USER%", username) + .Replace("\\n", "\n"); } } } \ No newline at end of file diff --git a/Twitch/TwitchApiClient.cs b/Twitch/TwitchApiClient.cs index 3d1dacc..58fdd53 100644 --- a/Twitch/TwitchApiClient.cs +++ b/Twitch/TwitchApiClient.cs @@ -64,6 +64,7 @@ public class TwitchApiClient { try { + _logger.Debug($"Attempting to authorize Twitch API [id: {broadcasterId}]"); var authorize = await _web.GetJson("https://hermes.goblincaves.com/api/account/reauthorize"); if (authorize != null && broadcasterId == authorize.BroadcasterId) { @@ -71,7 +72,10 @@ public class TwitchApiClient _token.RefreshToken = authorize.RefreshToken; _token.UserId = authorize.UserId; _token.BroadcasterId = authorize.BroadcasterId; + _token.ExpiresIn = authorize.ExpiresIn; + _token.UpdatedAt = DateTime.Now; _logger.Information("Updated Twitch API tokens."); + _logger.Debug($"Twitch API Auth data [user id: {_token.UserId}][id: {_token.BroadcasterId}][expires in: {_token.ExpiresIn}][expires at: {_token.ExpiresAt.ToShortTimeString()}]"); } else if (authorize != null) { @@ -79,6 +83,7 @@ public class TwitchApiClient return false; } _broadcasterId = broadcasterId; + _logger.Debug($"Authorized Twitch API [id: {broadcasterId}]"); return true; } catch (HttpResponseException e) @@ -90,6 +95,7 @@ public class TwitchApiClient } catch (JsonException) { + _logger.Debug($"Failed to Authorize Twitch API due to JSON error [id: {broadcasterId}]"); } catch (Exception e) { @@ -191,7 +197,7 @@ public class TwitchApiClient foreach (var action in actions) try { - await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName); + await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName, long.Parse(e.RewardRedeemed.Redemption.User.Id)); } catch (Exception ex) { diff --git a/User.cs b/User.cs index 0b171bb..96ffd9d 100644 --- a/User.cs +++ b/User.cs @@ -12,11 +12,12 @@ namespace TwitchChatTTS public long TwitchUserId { get; set; } public string TwitchUsername { get; set; } public string SevenEmoteSetId { get; set; } + public long? OwnerId { get; set; } public string DefaultTTSVoice { get; set; } // voice id -> voice name public IDictionary VoicesAvailable { get => _voicesAvailable; set { _voicesAvailable = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } } - // chatter/twitch id -> voice name + // chatter/twitch id -> voice id public IDictionary VoicesSelected { get; set; } // voice names public HashSet VoicesEnabled { get => _voicesEnabled; set { _voicesEnabled = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } }