hermes-client/Veadotube/VeadoSocketClient.cs

254 lines
8.6 KiB
C#

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<object>
{
private VeadoInstanceInfo? Instance;
private IDictionary<string, IVeadotubeMessageHandler> _handlers;
private IDictionary<string, string> _states;
public bool Connected { get; set; }
public bool Identified { get; set; }
public bool Streaming { get; set; }
public VeadoSocketClient(
[FromKeyedServices("veadotube")] IEnumerable<IVeadotubeMessageHandler> handlers,
//[FromKeyedServices("veadotube")] MessageTypeManager<IVeadotubeMessageHandler> typeManager,
ILogger logger
) : base(logger, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
})
{
_handlers = handlers.ToDictionary(h => h.Name, h => h);
_states = new Dictionary<string, string>();
}
protected override async Task<T> Deserialize<T>(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<T>(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>(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<byte>(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<string, string> 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<VeadoInstanceInfo>(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<VeadoPayloadMessage>(contentAsString, _options);
if (data == null)
{
return;
}
var payload = JsonSerializer.Deserialize<VeadoEventMessage>(data.Payload.ToString()!, _options);
if (_handlers.TryGetValue(payload?.Event ?? string.Empty, out var handler))
{
await handler.Handle(this, data);
}
return;
}
}
}