diff --git a/Startup.cs b/Startup.cs index f234a42..61c8b0d 100644 --- a/Startup.cs +++ b/Startup.cs @@ -35,6 +35,7 @@ using TwitchChatTTS.Chat.Observers; using TwitchChatTTS.Chat.Commands.Limits; using TwitchChatTTS.Hermes.Socket.Requests; using TwitchChatTTS.Bus; +using TwitchChatTTS.Veadotube; // dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true @@ -147,6 +148,10 @@ s.AddKeyedSingleton("7tv"); s.AddKeyedSingleton, SevenMessageTypeManager>("7tv"); s.AddKeyedSingleton, SevenSocketClient>("7tv"); +// Veadotube +s.AddKeyedSingleton, VeadoMessageTypeManager>("veadotube"); +s.AddKeyedSingleton, VeadoSocketClient>("veadotube"); + // Nightbot s.AddSingleton(); diff --git a/TTS.cs b/TTS.cs index fe6e7c7..1577af2 100644 --- a/TTS.cs +++ b/TTS.cs @@ -14,6 +14,7 @@ using TwitchChatTTS.Twitch.Socket; using TwitchChatTTS.Chat.Commands; using System.Text; using TwitchChatTTS.Chat.Speech; +using TwitchChatTTS.Veadotube; namespace TwitchChatTTS { @@ -29,6 +30,7 @@ namespace TwitchChatTTS private readonly OBSSocketClient _obs; private readonly SevenSocketClient _seven; private readonly TwitchWebsocketClient _twitch; + private readonly VeadoSocketClient _veado; private readonly ICommandFactory _commandFactory; private readonly ICommandManager _commandManager; private readonly IEmoteDatabase _emotes; @@ -45,6 +47,7 @@ namespace TwitchChatTTS [FromKeyedServices("obs")] SocketClient obs, [FromKeyedServices("7tv")] SocketClient seven, [FromKeyedServices("twitch")] SocketClient twitch, + [FromKeyedServices("veadotube")] SocketClient veado, ICommandFactory commandFactory, ICommandManager commandManager, IEmoteDatabase emotes, @@ -61,6 +64,7 @@ namespace TwitchChatTTS _obs = (obs as OBSSocketClient)!; _seven = (seven as SevenSocketClient)!; _twitch = (twitch as TwitchWebsocketClient)!; + _veado = (veado as VeadoSocketClient)!; _commandFactory = commandFactory; _commandManager = commandManager; _emotes = emotes; @@ -132,6 +136,15 @@ namespace TwitchChatTTS } }); + try + { + _veado.Initialize(); + await _veado.Connect(); + } + catch (Exception e) { + _logger.Warning(e, "Failed to connect to Veado websocket server."); + } + try { await _twitch.Connect(); diff --git a/Twitch/Redemptions/RedemptionManager.cs b/Twitch/Redemptions/RedemptionManager.cs index 1a35adc..0472de1 100644 --- a/Twitch/Redemptions/RedemptionManager.cs +++ b/Twitch/Redemptions/RedemptionManager.cs @@ -10,6 +10,7 @@ using TwitchChatTTS.Bus.Data; using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.OBS.Socket; using TwitchChatTTS.OBS.Socket.Data; +using TwitchChatTTS.Veadotube; namespace TwitchChatTTS.Twitch.Redemptions { @@ -20,6 +21,7 @@ namespace TwitchChatTTS.Twitch.Redemptions private readonly User _user; private readonly OBSSocketClient _obs; private readonly HermesSocketClient _hermes; + private readonly VeadoSocketClient _veado; private readonly NightbotApiClient _nightbot; private readonly AudioPlaybackEngine _playback; private readonly ILogger _logger; @@ -32,6 +34,7 @@ namespace TwitchChatTTS.Twitch.Redemptions User user, [FromKeyedServices("obs")] SocketClient obs, [FromKeyedServices("hermes")] SocketClient hermes, + [FromKeyedServices("veadotube")] SocketClient veado, NightbotApiClient nightbot, AudioPlaybackEngine playback, ILogger logger) @@ -41,6 +44,7 @@ namespace TwitchChatTTS.Twitch.Redemptions _user = user; _obs = (obs as OBSSocketClient)!; _hermes = (hermes as HermesSocketClient)!; + _veado = (veado as VeadoSocketClient)!; _nightbot = nightbot; _playback = playback; _logger = logger; @@ -220,6 +224,15 @@ namespace TwitchChatTTS.Twitch.Redemptions case "NIGHTBOT_CLEAR_QUEUE": await _nightbot.ClearQueue(); break; + case "VEADOTUBE_SET_STATE": + await _veado.SetCurrentState(action.Data["state"]); + break; + case "VEADOTUBE_PUSH_STATE": + await _veado.PushState(action.Data["state"]); + break; + case "VEADOTUBE_POP_STATE": + await _veado.PopState(action.Data["state"]); + break; default: _logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); break; diff --git a/TwitchChatTTS.csproj b/TwitchChatTTS.csproj index d2a9e91..237e05f 100644 --- a/TwitchChatTTS.csproj +++ b/TwitchChatTTS.csproj @@ -12,6 +12,7 @@ + diff --git a/Veadotube/VeadoInstanceInfo.cs b/Veadotube/VeadoInstanceInfo.cs new file mode 100644 index 0000000..5287200 --- /dev/null +++ b/Veadotube/VeadoInstanceInfo.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace TwitchChatTTS.Veadotube +{ + public class VeadoInstanceInfo + { + [JsonPropertyName("time")] + public long Time { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("server")] + public string Server { get; set; } + } +} \ No newline at end of file diff --git a/Veadotube/VeadoMessage.cs b/Veadotube/VeadoMessage.cs new file mode 100644 index 0000000..20a6d15 --- /dev/null +++ b/Veadotube/VeadoMessage.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using Newtonsoft.Json; + +namespace TwitchChatTTS.Veadotube +{ + public class VeadoPayloadMessage + { + public string Event { get; set; } + public string Type { get; set; } + public string Id { get; set; } + public object Payload { get; set; } + } + + public class VeadoEventMessage + { + [JsonPropertyName("event")] + public string Event { get; set; } + } + + public class VeadoNodeState { + public string Id { get; set; } + public string Name { get; set; } + } + + public class VeadoNodeStateListMessage : VeadoEventMessage { + public IEnumerable Entries { get; set; } + } + + public class VeadoNodeStateMessage : VeadoEventMessage { + public string State { get; set; } + } + + public class VeadoNodeThumbMessage { + public int Width { get; set; } + public int Height { get; set; } + public string Png { get; set; } + } +} \ No newline at end of file diff --git a/Veadotube/VeadoMessageTypeManager.cs b/Veadotube/VeadoMessageTypeManager.cs new file mode 100644 index 0000000..669083d --- /dev/null +++ b/Veadotube/VeadoMessageTypeManager.cs @@ -0,0 +1,17 @@ +using CommonSocketLibrary.Common; +using CommonSocketLibrary.Socket.Manager; +using Microsoft.Extensions.DependencyInjection; +using Serilog; + +namespace TwitchChatTTS.Veadotube +{ + public class VeadoMessageTypeManager : WebSocketMessageTypeManager + { + public VeadoMessageTypeManager( + [FromKeyedServices("veadotube")] IEnumerable handlers, + ILogger logger + ) : base(handlers, logger) + { + } + } +} \ No newline at end of file diff --git a/Veadotube/VeadoSocketClient.cs b/Veadotube/VeadoSocketClient.cs new file mode 100644 index 0000000..a1bf67d --- /dev/null +++ b/Veadotube/VeadoSocketClient.cs @@ -0,0 +1,222 @@ +using CommonSocketLibrary.Common; +using CommonSocketLibrary.Abstract; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using System.Text.Json; +using CommonSocketLibrary.Backoff; +using System.Text; +using System.Net.WebSockets; + +namespace TwitchChatTTS.Veadotube +{ + public class VeadoSocketClient : SocketClient + { + private VeadoInstanceInfo? Instance; + + 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 + }) + { + } + + 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 += (sender, e) => + { + Connected = true; + _logger.Information("Veadotube websocket client connected."); + }; + + 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 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}]"); + } + } + + 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 Task OnResponseReceived(object? content) + { + var contentAsString = JsonSerializer.Serialize(content); + _logger.Debug("VEADO RX: " + contentAsString); + return Task.CompletedTask; + } + } +} \ No newline at end of file