254 lines
8.6 KiB
C#
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;
|
|
}
|
|
}
|
|
} |