From 57584918d018bc3b0decd5996eec8b3cf81065d1 Mon Sep 17 00:00:00 2001 From: barelyprofessional <150058423+barelyprofessional@users.noreply.github.com> Date: Wed, 19 Jun 2024 23:32:56 +0800 Subject: [PATCH] Shuffle gamba watching --- KfChatDotNetKickBot/Models/ShuffleModels.cs | 27 +++ KfChatDotNetKickBot/Services/Shuffle.cs | 176 ++++++++++++++++++++ KfChatDotNetKickBot/Services/Twitch.cs | 47 ++++++ 3 files changed, 250 insertions(+) create mode 100644 KfChatDotNetKickBot/Models/ShuffleModels.cs create mode 100644 KfChatDotNetKickBot/Services/Shuffle.cs diff --git a/KfChatDotNetKickBot/Models/ShuffleModels.cs b/KfChatDotNetKickBot/Models/ShuffleModels.cs new file mode 100644 index 0000000..fd6243e --- /dev/null +++ b/KfChatDotNetKickBot/Models/ShuffleModels.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace KfChatDotNetKickBot.Models; + +public class ShuffleLatestBetModel +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + [JsonPropertyName("username")] + public string? Username { get; set; } + [JsonPropertyName("vipLevel")] + public required string VipLevel { get; set; } + [JsonPropertyName("currency")] + public required string Currency { get; set; } + [JsonPropertyName("amount")] + public required string Amount { get; set; } + [JsonPropertyName("payout")] + public required string Payout { get; set; } + [JsonPropertyName("multiplier")] + public required string Multiplier { get; set; } + [JsonPropertyName("gameName")] + public required string GameName { get; set; } + [JsonPropertyName("gameCategory")] + public required string GameCategory { get; set; } + [JsonPropertyName("gameSlug")] + public required string GameSlug { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Services/Shuffle.cs b/KfChatDotNetKickBot/Services/Shuffle.cs new file mode 100644 index 0000000..c27ff13 --- /dev/null +++ b/KfChatDotNetKickBot/Services/Shuffle.cs @@ -0,0 +1,176 @@ +using System.Net; +using System.Net.WebSockets; +using System.Text.Json; +using KfChatDotNetKickBot.Models; +using NLog; +using Websocket.Client; + +namespace KfChatDotNetKickBot.Services; + +public class Shuffle +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + private WebsocketClient _wsClient; + private Uri _wsUri = new("wss://subscription-temp.shuffle.com/graphql"); + private int _reconnectTimeout = 60; + private string? _proxy; + public delegate void OnLatestBetUpdatedEventHandler(object sender, ShuffleLatestBetModel bet); + public event OnLatestBetUpdatedEventHandler OnLatestBetUpdated; + private CancellationToken _cancellationToken = CancellationToken.None; + + public Shuffle(string? proxy = null, CancellationToken? cancellationToken = null) + { + _proxy = proxy; + if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + // Moved it up here as I'm concerned about the possibility of reconnections creating multiple ping tasks + _ = PeriodicPing(); + } + + public async Task StartWsClient() + { + _logger.Debug("StartWsClient() called, creating client"); + await CreateWsClient(); + } + + private async Task CreateWsClient() + { + var factory = new Func(() => + { + var clientWs = new ClientWebSocket(); + clientWs.Options.AddSubProtocol("graphql-transport-ws"); + clientWs.Options.SetRequestHeader("Origin", "https://shuffle.com"); + clientWs.Options.SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0"); + if (_proxy == null) return clientWs; + _logger.Debug($"Using proxy address {_proxy}"); + clientWs.Options.Proxy = new WebProxy(_proxy); + return clientWs; + }); + + var client = new WebsocketClient(_wsUri, factory) + { + ReconnectTimeout = TimeSpan.FromSeconds(_reconnectTimeout) + }; + + client.ReconnectionHappened.Subscribe(WsReconnection); + client.MessageReceived.Subscribe(WsMessageReceived); + client.DisconnectionHappened.Subscribe(WsDisconnection); + + _wsClient = client; + + _logger.Debug("Websocket client has been built, about to start"); + await client.Start(); + _logger.Debug("Websocket client started!"); + } + + public bool IsConnected() + { + return _wsClient is { IsRunning: true }; + } + + private void SendPing() + { + _logger.Debug("Sending ping to Shuffle"); + _wsClient.Send("{\"type\":\"ping\"}"); + } + + private async Task PeriodicPing() + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + while (await timer.WaitForNextTickAsync(_cancellationToken)) + { + if (_wsClient == null) + { + _logger.Debug("_wsClient doesn't exist yet, not going to try ping"); + continue; + } + if (!IsConnected()) + { + _logger.Debug("Not connected not going to try send a ping actually"); + continue; + } + SendPing(); + } + } + + private void WsDisconnection(DisconnectionInfo disconnectionInfo) + { + _logger.Error($"Client disconnected from the chat (or never successfully connected). Type is {disconnectionInfo.Type}"); + _logger.Error(disconnectionInfo.Exception); + } + + private void WsReconnection(ReconnectionInfo reconnectionInfo) + { + _logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}"); + _logger.Info("Sending connection_init"); + var initPayload = + "{\"type\":\"connection_init\",\"payload\":{\"x-correlation-id\":\"pdvlnd9tej-di27abvq19-1.30.2-1i0nef1m7-g::anon\",\"authorization\":\"\"}}"; + _logger.Debug(initPayload); + _wsClient.SendInstant(initPayload).Wait(_cancellationToken); + } + + // Stream start JSON + // {"type":"MESSAGE","data":{"topic":"video-playback-by-id.114122847","message":"{\"server_time\":1718631487,\"play_delay\":0,\"type\":\"stream-up\"}"}} + // View count update (every 30 seconds) + // {"type":"MESSAGE","data":{"topic":"video-playback-by-id.114122847","message":"{\"type\":\"viewcount\",\"server_time\":1718631500.636146,\"viewers\":62}"}} + // {"type":"MESSAGE","data":{"topic":"video-playback-by-id.114122847","message":"{\"type\":\"viewcount\",\"server_time\":1718631530.654308,\"viewers\":162}"}} + // {"type":"MESSAGE","data":{"topic":"video-playback-by-id.114122847","message":"{\"type\":\"viewcount\",\"server_time\":1718631560.551188,\"viewers\":179}"}} + private void WsMessageReceived(ResponseMessage message) + { + if (message.Text == null) + { + _logger.Info("Shuffle sent a null message"); + return; + } + _logger.Debug($"Received event from Shuffle: {message.Text}"); + + try + { + var packet = JsonSerializer.Deserialize(message.Text); + var packetType = packet.GetProperty("type").GetString(); + + if (packetType == "connection_ack") + { + _logger.Debug("connection_ack packet, sending subscribe payload"); + _logger.Info("Sending subscription request"); + // we're super ghetto today + var payload = "{\"id\":\"" + Guid.NewGuid() + + "\",\"type\":\"subscribe\",\"payload\":{\"variables\":{},\"extensions\":{},\"operationName\":\"LatestBetUpdated\",\"query\":\"subscription LatestBetUpdated {\\n latestBetUpdated {\\n ...BetActivityFields\\n __typename\\n }\\n}\\n\\nfragment BetActivityFields on BetActivityPayload {\\n id\\n username\\n vipLevel\\n currency\\n amount\\n payout\\n multiplier\\n gameName\\n gameCategory\\n gameSlug\\n __typename\\n}\"}}"; + _logger.Debug(payload); + _wsClient.Send(payload); + return; + } + + if (packetType == "pong") + { + _logger.Info("Shuffle pong packet"); + return; + } + + // GAMBA + if (packetType == "next") + { + _logger.Debug("Got a bet! Deserializing it"); + var bet = packet.GetProperty("payload").GetProperty("data").GetProperty("latestBetUpdated") + .Deserialize(); + if (bet == null) + { + _logger.Error("Caught a null before invoking bet event"); + throw new NullReferenceException("Caught a null before invoking bet event"); + } + _logger.Debug("Invoking event"); + OnLatestBetUpdated?.Invoke(this, bet); + return; + } + _logger.Info("Message from Shuffle was unhandled"); + _logger.Info(message.Text); + } + catch (Exception e) + { + _logger.Error("Failed to handle message from Shuffle"); + _logger.Error(e); + _logger.Error("--- JSON Payload ---"); + _logger.Error(message.Text); + _logger.Error("--- End of JSON Payload ---"); + } + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Services/Twitch.cs b/KfChatDotNetKickBot/Services/Twitch.cs index a300c5f..f9165e5 100644 --- a/KfChatDotNetKickBot/Services/Twitch.cs +++ b/KfChatDotNetKickBot/Services/Twitch.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http.Json; using System.Net.WebSockets; using System.Text.Json; using NLog; @@ -152,4 +153,50 @@ public class Twitch _logger.Error("--- End of JSON Payload ---"); } } + + public async Task IsStreamLive(string channel) + { + var clientId = "kimne78kx3ncx6brgo4mv6wki5h1ko"; + var graphQl = "query {\n user(login: \"" + channel + "\") {\n stream {\n id\n }\n }\n}"; + _logger.Debug($"Built GraphQL query string: {graphQl}"); + var jsonBody = new Dictionary + { + { "query", graphQl }, + { "variables", new object() } + }; + _logger.Debug("Created dictionary object for the JSON payload, should serialize to following value:"); + _logger.Debug(JsonSerializer.Serialize(jsonBody)); + var handler = new HttpClientHandler {AutomaticDecompression = DecompressionMethods.All}; + if (_proxy != null) + { + handler.UseProxy = true; + handler.Proxy = new WebProxy(_proxy); + _logger.Debug($"Configured to use proxy {_proxy}"); + } + + using var client = new HttpClient(handler); + client.DefaultRequestHeaders.Add("client-id", clientId); + var postBody = JsonContent.Create(jsonBody); + var response = await client.PostAsync("https://gql.twitch.tv/gql", postBody, _cancellationToken); + //response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadFromJsonAsync(cancellationToken: _cancellationToken); + _logger.Debug("Twitch API returned following JSON"); + _logger.Debug(responseContent.GetRawText); + if (responseContent.GetProperty("data").GetProperty("user").ValueKind == JsonValueKind.Null) + { + _logger.Debug("data.user was null"); + throw new TwitchUserNotFoundException(); + } + + if (responseContent.GetProperty("data").GetProperty("user").GetProperty("stream").ValueKind == + JsonValueKind.Null) + { + _logger.Debug("stream property was null. Means streamer is not live"); + return false; + } + + return true; + } + + public class TwitchUserNotFoundException : Exception; } \ No newline at end of file