using CommonSocketLibrary.Abstract; using Microsoft.Extensions.DependencyInjection; using Serilog; using System.Text.Json; using CommonSocketLibrary.Backoff; using System.Text; using System.Net.WebSockets; using TwitchChatTTS.Veadotube.Handlers; namespace TwitchChatTTS.Veadotube { public class VeadoSocketClient : SocketClient { private VeadoInstanceInfo? Instance; private IDictionary _handlers; private IDictionary _states; public bool Connected { get; set; } public bool Identified { get; set; } public bool Streaming { get; set; } public VeadoSocketClient( [FromKeyedServices("veadotube")] IEnumerable handlers, //[FromKeyedServices("veadotube")] MessageTypeManager typeManager, ILogger logger ) : base(logger, new JsonSerializerOptions() { PropertyNameCaseInsensitive = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) { _handlers = handlers.ToDictionary(h => h.Name, h => h); _states = new Dictionary(); } protected override async Task Deserialize(Stream stream) { using StreamReader reader = new StreamReader(stream); string content = await reader.ReadToEndAsync(); int index = content.IndexOf(':'); string json = content.Substring(index + 1).Replace("\0", string.Empty); T? value = JsonSerializer.Deserialize(json, _options); return value!; } public void Initialize() { _logger.Information($"Initializing Veadotube websocket client."); OnConnected += async (sender, e) => { Connected = true; _logger.Information("Veadotube websocket client connected."); await FetchStates(); }; OnDisconnected += async (sender, e) => { _logger.Information($"Veadotube websocket client disconnected [status: {e.Status}][reason: {e.Reason}] " + (Identified ? "Will be attempting to reconnect every 30 seconds." : "Will not be attempting to reconnect.")); Connected = false; Identified = false; Streaming = false; await Reconnect(new ExponentialBackoff(5000, 300000)); }; } public override async Task Connect() { if (!UpdateURL() || string.IsNullOrEmpty(Instance?.Server) || string.IsNullOrEmpty(Instance.Name)) { _logger.Warning("Lacking connection info for Veadotube websockets. Not connecting to Veadotube."); return; } string url = $"ws://{Instance.Server}?n={Instance.Name}"; _logger.Debug($"Veadotube websocket client attempting to connect to {url}"); try { await ConnectAsync(url); } catch (Exception) { _logger.Warning("Connecting to Veadotube failed. Skipping Veadotube websockets."); } } public async Task FetchStates() { await Send(new VeadoPayloadMessage() { Event = "payload", Type = "stateEvents", Id = "mini", Payload = new VeadoEventMessage() { Event = "list", } }); } public string? GetStateId(string state) { if (_states.TryGetValue(state, out var id)) return id; return null; } public async Task SetCurrentState(string stateId) { await Send(new VeadoPayloadMessage() { Event = "payload", Type = "stateEvents", Id = "mini", Payload = new VeadoNodeStateMessage() { Event = "set", State = stateId } }); } public async Task PushState(string stateId) { await Send(new VeadoPayloadMessage() { Event = "payload", Type = "stateEvents", Id = "mini", Payload = new VeadoNodeStateMessage() { Event = "push", State = stateId } }); } public async Task PopState(string stateId) { await Send(new VeadoPayloadMessage() { Event = "payload", Type = "stateEvents", Id = "mini", Payload = new VeadoNodeStateMessage() { Event = "pop", State = stateId } }); } private async Task Send(T data) { if (_socket == null || data == null) return; if (!Connected) { _logger.Debug("Not sending Veadotube message due to no connection."); return; } try { var content = "nodes:" + JsonSerializer.Serialize(data, _options); var bytes = Encoding.UTF8.GetBytes(content); var array = new ArraySegment(bytes); var total = bytes.Length; var current = 0; while (current < total) { var size = Encoding.UTF8.GetBytes(content.Substring(current), array); await _socket.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts!.Token); current += size; } _logger.Debug($"Veado TX [message type: {typeof(T).Name}]: " + content); } catch (Exception e) { if (_socket.State.ToString().Contains("Close") || _socket.State == WebSocketState.Aborted) { await DisconnectAsync(new SocketDisconnectionEventArgs(_socket.CloseStatus.ToString()!, _socket.CloseStatusDescription ?? string.Empty)); _logger.Warning($"Socket state on closing = {_socket.State} | {_socket.CloseStatus?.ToString()} | {_socket.CloseStatusDescription}"); } _logger.Error(e, $"Failed to send a websocket message to Veado [message type: {typeof(T).Name}]"); } } public void UpdateState(IDictionary states) { _states = states; } private bool UpdateURL() { string path = Environment.ExpandEnvironmentVariables("%userprofile%/.veadotube/instances"); try { if (Directory.Exists(path)) { var directory = Directory.CreateDirectory(path); var files = directory.GetFiles() .Where(f => f.Name.StartsWith("mini-")) .OrderByDescending(f => f.CreationTime); if (files.Any()) { _logger.Debug("Veadotube's instance file exists: " + files.First().FullName); var data = File.ReadAllText(files.First().FullName); var instance = JsonSerializer.Deserialize(data); if (instance != null) { Instance = instance; return true; } } } } catch (Exception ex) { _logger.Error(ex, "Failed to find Veadotube instance information."); } return false; } protected override async Task OnResponseReceived(object? content) { var contentAsString = JsonSerializer.Serialize(content, _options); _logger.Debug("VEADO RX: " + contentAsString); var data = JsonSerializer.Deserialize(contentAsString, _options); if (data == null) { return; } var payload = JsonSerializer.Deserialize(data.Payload.ToString()!, _options); if (_handlers.TryGetValue(payload?.Event ?? string.Empty, out var handler)) { await handler.Handle(this, data); } return; } } }