using System.Collections.Concurrent; using System.Text.Json; using System.Text.RegularExpressions; 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.Chat.Emotes; using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Twitch.Redemptions; namespace TwitchChatTTS.Hermes.Socket.Handlers { public class RequestAckHandler : IWebSocketHandler { private User _user; private readonly ICallbackManager _callbackManager; private readonly TwitchApiClient _twitch; private readonly NightbotApiClient _nightbot; private readonly IServiceProvider _serviceProvider; private readonly JsonSerializerOptions _options; private readonly ILogger _logger; private readonly object _voicesAvailableLock = new object(); public int OperationCode { get; } = 4; public RequestAckHandler( ICallbackManager callbackManager, TwitchApiClient twitch, NightbotApiClient nightbot, IServiceProvider serviceProvider, User user, JsonSerializerOptions options, ILogger logger ) { _callbackManager = callbackManager; _twitch = twitch; _nightbot = nightbot; _serviceProvider = serviceProvider; _user = user; _options = options; _logger = logger; } public async Task Execute(SocketClient sender, Data data) { 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; } 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") { var voices = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (voices == null) return; lock (_voicesAvailableLock) { _user.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name); } _logger.Information("Updated all available voices for TTS."); } else if (message.Request.Type == "create_tts_user") { 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 voiceId = message.Request.Data["voice"].ToString(); _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") { 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 voiceId = message.Request.Data["voice"].ToString(); _user.VoicesSelected[chatterId] = voiceId; _logger.Information($"Updated TTS voice [voice: {voiceId}] for user [user id: {userId}]"); } else if (message.Request.Type == "create_tts_voice") { string? voice = message.Request.Data["voice"].ToString(); string? voiceId = message.Data.ToString(); if (voice == null || voiceId == null) return; lock (_voicesAvailableLock) { var list = _user.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value); list.Add(voiceId, voice); _user.VoicesAvailable = list; } _logger.Information($"Created new tts voice [voice: {voice}][id: {voiceId}]."); } else if (message.Request.Type == "delete_tts_voice") { var voice = message.Request.Data["voice"].ToString(); if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null) return; lock (_voicesAvailableLock) { var dict = _user.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value); dict.Remove(voice); _user.VoicesAvailable.Remove(voice); } _logger.Information($"Deleted a voice [voice: {voiceName}]"); } else if (message.Request.Type == "update_tts_voice") { string voiceId = message.Request.Data["idd"].ToString(); string voice = message.Request.Data["voice"].ToString(); if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null) return; _user.VoicesAvailable[voiceId] = voice; _logger.Information($"Updated TTS voice [voice: {voice}][id: {voiceId}]"); } else if (message.Request.Type == "get_connections") { var connections = JsonSerializer.Deserialize>(message.Data?.ToString(), _options); if (connections == null) { _logger.Error("Null value was given when attempting to fetch connections."); _logger.Debug(message.Data?.ToString()); return; } _user.TwitchConnection = connections.FirstOrDefault(c => c.Type == "twitch" && c.Default); _user.NightbotConnection = connections.FirstOrDefault(c => c.Type == "nightbot" && c.Default); if (_user.TwitchConnection != null) _twitch.Initialize(_user.TwitchConnection.ClientId, _user.TwitchConnection.AccessToken); if (_user.NightbotConnection != null) _nightbot.Initialize(_user.NightbotConnection.ClientId, _user.NightbotConnection.AccessToken); _logger.Information($"Fetched connections from TTS account [count: {connections.Count()}][twitch: {_user.TwitchConnection != null}][nightbot: {_user.NightbotConnection != null}]"); } else if (message.Request.Type == "get_tts_users") { var users = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (users == null) return; var temp = new ConcurrentDictionary(); foreach (var entry in users) temp.TryAdd(entry.Key, entry.Value); _user.VoicesSelected = temp; _logger.Information($"Updated {temp.Count()} chatters' selected voice."); } else if (message.Request.Type == "get_chatter_ids") { var chatters = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (chatters == null) return; _user.Chatters = [.. chatters]; _logger.Information($"Fetched {chatters.Count()} chatters' id."); } else if (message.Request.Type == "get_emotes") { var emotes = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (emotes == null) return; var emoteDb = _serviceProvider.GetRequiredService(); var count = 0; var duplicateNames = 0; foreach (var emote in emotes) { if (emoteDb.Get(emote.Name) == null) { 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 == "get_enabled_tts_voices") { var enabledTTSVoices = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (enabledTTSVoices == null) { _logger.Error("Failed to load enabled tts voices."); return; } if (_user.VoicesEnabled == null) _user.VoicesEnabled = enabledTTSVoices.ToHashSet(); else _user.VoicesEnabled.Clear(); foreach (var voice in enabledTTSVoices) _user.VoicesEnabled.Add(voice); _logger.Information($"TTS voices [count: {_user.VoicesEnabled.Count}] have been enabled."); } else if (message.Request.Type == "get_permissions") { var groupInfo = JsonSerializer.Deserialize(message.Data.ToString(), _options); if (groupInfo == null) { _logger.Error("Failed to load groups & permissions."); return; } var chatterGroupManager = _serviceProvider.GetRequiredService(); var permissionManager = _serviceProvider.GetRequiredService(); permissionManager.Clear(); chatterGroupManager.Clear(); var groupsById = groupInfo.Groups.ToDictionary(g => g.Id, g => g); foreach (var group in groupInfo.Groups) chatterGroupManager.Add(group); foreach (var permission in groupInfo.GroupPermissions) { _logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]"); if (!groupsById.TryGetValue(permission.GroupId, out var group)) { _logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); continue; } var path = $"{group.Name}.{permission.Path}"; permissionManager.Set(path, permission.Allow); _logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); } _logger.Information($"Groups [count: {groupInfo.Groups.Count()}] & Permissions [count: {groupInfo.GroupPermissions.Count()}] have been loaded."); foreach (var chatter in groupInfo.GroupChatters) if (groupsById.TryGetValue(chatter.GroupId, out var group)) chatterGroupManager.Add(chatter.ChatterId, group.Name); _logger.Information($"Users in each group [count: {groupInfo.GroupChatters.Count()}] have been loaded."); } else if (message.Request.Type == "get_tts_word_filters") { var wordFilters = JsonSerializer.Deserialize>(message.Data.ToString(), _options); if (wordFilters == null) { _logger.Error("Failed to load word filters."); return; } var filters = wordFilters.Where(f => f.Search != null && f.Replace != null).ToArray(); foreach (var filter in filters) { try { var re = new Regex(filter.Search!, RegexOptions.Compiled); re.Match(string.Empty); filter.Regex = re; } catch (Exception) { } } _user.RegexFilters = filters; _logger.Information($"TTS word filters [count: {_user.RegexFilters.Count()}] have been refreshed."); } else if (message.Request.Type == "update_tts_voice_state") { 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) { _logger.Warning($"Failed to find voice by id [id: {voiceId}]"); return; } if (state) _user.VoicesEnabled.Add(voiceId); else _user.VoicesEnabled.Remove(voiceId); _logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]"); } else if (message.Request.Type == "get_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") { 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 not 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; } } }