From 39be005d381f0427eb952493cc8c57879de9e358 Mon Sep 17 00:00:00 2001 From: barelyprofessional <150058423+barelyprofessional@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:44:56 -0500 Subject: [PATCH] Experimental Winna support --- KfChatDotNetBot/Models/WinnaModels.cs | 39 +++++++ KfChatDotNetBot/Services/BotServices.cs | 42 ++++++- KfChatDotNetBot/Services/Winna.cs | 149 ++++++++++++++++++++++++ KfChatDotNetBot/Settings/BuiltIn.cs | 4 + 4 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 KfChatDotNetBot/Models/WinnaModels.cs create mode 100644 KfChatDotNetBot/Services/Winna.cs diff --git a/KfChatDotNetBot/Models/WinnaModels.cs b/KfChatDotNetBot/Models/WinnaModels.cs new file mode 100644 index 0000000..9d2e087 --- /dev/null +++ b/KfChatDotNetBot/Models/WinnaModels.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace KfChatDotNetBot.Models; + +public class WinnaBetModel +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + [JsonPropertyName("currency")] + public required string Currency { get; set; } + [JsonPropertyName("userName")] + public required string Username { get; set; } + [JsonPropertyName("isVip")] + public required bool IsVip { get; set; } + [JsonPropertyName("tier")] + public string? Tier { get; set; } + [JsonPropertyName("level")] + public required int Level { get; set; } + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + [JsonPropertyName("multiplier")] + public required float Multiplier { get; set; } + [JsonPropertyName("betAmount")] + public required float BetAmount { get; set; } + [JsonPropertyName("payout")] + public required float Payout { get; set; } + [JsonPropertyName("gameName")] + public required string GameName { get; set; } + [JsonPropertyName("amounts")] + public required Dictionary Amounts { get; set; } +} + +public class WinnaCurrencyModel +{ + [JsonPropertyName("betAmount")] + public required float BetAmount { get; set; } + [JsonPropertyName("payout")] + public required float Payout { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs index 8ece493..0564fc6 100644 --- a/KfChatDotNetBot/Services/BotServices.cs +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -43,6 +43,7 @@ public class BotServices public KasinoRain? KasinoRain; public KasinoShop? KasinoShop; public KasinoKrash? KasinoKrash; + private Winna? _winna; private Task? _websocketWatchdog; private Task? _howlggGetUserTimer; @@ -97,7 +98,8 @@ public class BotServices BuildYouTubePubSub(), BuildKasinoRain(), BuildKasinoShop(), - BuildKasinoKrash() + BuildKasinoKrash(), + BuildWinna() ]; try { @@ -141,6 +143,14 @@ public class BotServices await _shuffle.StartWsClient(); } + private async Task BuildWinna() + { + _logger.Debug("Building Winna"); + _winna = new Winna((await SettingsProvider.GetValueAsync(BuiltIn.Keys.Proxy)).Value); + _winna.OnWinnaBet += OnWinnaBet; + await _winna.StartWsClient(); + } + private async Task BuildShuffleDotUs() { _logger.Debug("Building Shuffle.us"); @@ -426,7 +436,7 @@ public class BotServices BuiltIn.Keys.KickEnabled, BuiltIn.Keys.HowlggEnabled, BuiltIn.Keys.ChipsggEnabled, BuiltIn.Keys.ClashggEnabled, BuiltIn.Keys.BetBoltEnabled, BuiltIn.Keys.YeetEnabled, BuiltIn.Keys.RainbetEnabled, BuiltIn.Keys.PartiEnabled, BuiltIn.Keys.JackpotEnabled, - BuiltIn.Keys.YouTubePubSubEnabled + BuiltIn.Keys.YouTubePubSubEnabled, BuiltIn.Keys.WinnaEnabled ]); try { @@ -550,6 +560,14 @@ public class BotServices _youTubePubSub = null; await BuildYouTubePubSub(); } + + if (settings[BuiltIn.Keys.WinnaEnabled].ToBoolean() && _winna != null && !_winna.IsConnected()) + { + _logger.Error("Winna died, recreating it"); + _winna.Dispose(); + _winna = null!; + await BuildWinna(); + } } catch (Exception e) { @@ -1066,7 +1084,25 @@ public class BotServices preamble = "🦅🦅 Shuffle US! 🦅🦅"; } // There will be a check for live status but ignoring that while we deal with an emergency dice situation - _chatBot.SendChatMessage($"{preamble} {bet.Username} just bet {bet.Amount} {bet.Currency} which paid out [color={payoutColor}]{bet.Payout} {bet.Currency}[/color] ({bet.Multiplier}x) on {bet.GameName} 💰💰", true); + _chatBot.SendChatMessage($"{preamble} {bet.Username} just bet {bet.Amount} {bet.Currency} which paid out " + + $"[color={payoutColor}]{bet.Payout} {bet.Currency}[/color] ({bet.Multiplier}x) on {bet.GameName} 💰💰", true); + } + + private void OnWinnaBet(object sender, WinnaBetModel bet) + { + var settings = SettingsProvider + .GetMultipleValuesAsync([ + BuiltIn.Keys.WinnaBmjUsername, BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor, + ]).Result; + if (bet.Username != settings[BuiltIn.Keys.WinnaBmjUsername].Value) return; + var usd = bet.Amounts["USD"]; + _ = UpdateBossmanLastSighting($"betting {bet.BetAmount:N} {bet.Currency} on {bet.GameName} at Winna"); + var payoutColor = settings[BuiltIn.Keys.KiwiFarmsGreenColor].Value; + if (bet.Multiplier < 1) payoutColor = settings[BuiltIn.Keys.KiwiFarmsRedColor].Value; + _chatBot.SendChatMessage( + $"🚨🚨 Winnafags! 🚨🚨 {bet.Username} just bet {bet.BetAmount:N} {bet.Currency} ({usd.BetAmount:C}) which paid out " + + $"[color={payoutColor}]{bet.Payout:N} {bet.Currency} ({usd.Payout:C})[/color] ({bet.Multiplier:N2} on {bet.GameName} 💰💰", + true); } private void OnTwitchStreamStateUpdated(object sender, string channelName, bool isLive) diff --git a/KfChatDotNetBot/Services/Winna.cs b/KfChatDotNetBot/Services/Winna.cs new file mode 100644 index 0000000..4836c82 --- /dev/null +++ b/KfChatDotNetBot/Services/Winna.cs @@ -0,0 +1,149 @@ +using System.Net; +using System.Net.WebSockets; +using System.Text.Json; +using KfChatDotNetBot.Models; +using NLog; +using Websocket.Client; + +namespace KfChatDotNetBot.Services; + +public class Winna : IDisposable +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + private WebsocketClient? _wsClient; + private Uri _wsUri = new("wss://games-content-prod.winna.com/ws/?EIO=4&transport=websocket"); + private int _reconnectTimeout = 30; + private string? _proxy; + public delegate void OnWinnaBetEventHandler(object sender, WinnaBetModel bet); + public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e); + public event OnWinnaBetEventHandler? OnWinnaBet; + public event OnWsDisconnectionEventHandler? OnWsDisconnection; + private string? _userAgent; + + public Winna(string? proxy = null) + { + _proxy = proxy; + _logger.Info("Winna WebSocket client created"); + } + + 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.SetRequestHeader("Origin", "https://winna.com"); + clientWs.Options.SetRequestHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.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), + IsReconnectionEnabled = false + }; + + 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 WsDisconnection(DisconnectionInfo disconnectionInfo) + { + _logger.Error($"Client disconnected from Winna (or never successfully connected). Type is {disconnectionInfo.Type}"); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); + OnWsDisconnection?.Invoke(this, disconnectionInfo); + } + + private void WsReconnection(ReconnectionInfo reconnectionInfo) + { + _logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}"); + } + + private void WsMessageReceived(ResponseMessage message) + { + if (message.Text == null) + { + _logger.Info("Winna sent a null message"); + return; + } + _logger.Trace($"Received event from Winna: {message.Text}"); + + try + { + if (message.Text.StartsWith("0{")) + { + // 0{"sid":"5Q2cD9HxTUT3Kx-pAMMW","upgrades":[],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000} + _logger.Info("Received initial connection message from Winna, sending subscribe"); + _wsClient?.Send("42[\"feed.subscribe\",{\"feedType\":\"allBets\"}]"); + return; + } + var packetType = message.Text.Split('/')[0]; + if (packetType == "2") + { + _logger.Info("Received ping from Winna, replying with pong"); + _wsClient?.Send("3"); + return; + } + + if (packetType == "42") + { + var data = JsonSerializer.Deserialize>(message.Text[..2]); + if (data == null) throw new Exception("Caught a null when deserializing feed.update"); + if (data[0].GetString() == "feed.update") + { + var isSnapshot = data[1].TryGetProperty("snapshot", out _); + if (isSnapshot) + { + _logger.Info("Received a bet history snapshot from Winna, ignoring"); + return; + } + + var item = data[1].GetProperty("item").Deserialize(); + if (item == null) + { + throw new Exception("Caught a null when deserializing feed.update item"); + } + OnWinnaBet?.Invoke(this, item); + return; + } + _logger.Info($"Event {data[0].GetString()} from Winna was not handled"); + _logger.Info(message.Text); + } + } + catch (Exception e) + { + _logger.Error("Failed to handle message from Winna"); + _logger.Error(e); + _logger.Error("--- Payload ---"); + _logger.Error(message.Text); + _logger.Error("--- End of Payload ---"); + } + } + + public void Dispose() + { + _wsClient?.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Settings/BuiltIn.cs b/KfChatDotNetBot/Settings/BuiltIn.cs index d7554f5..024c60c 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -574,6 +574,10 @@ public static class BuiltIn public static string KasinoKrashEnabled = "Kasino.Krash.Enabled"; [BuiltInSetting("Delay in milliseconds before cleaning up krash", SettingValueType.Text, "10000", WholeNumberRegex)] public static string KasinoKrashCleanupDelay = "Kasino.Krash.CleanupDelay"; + [BuiltInSetting("Whether Winna is enabled", SettingValueType.Boolean, "false", BooleanRegex)] + public static string WinnaEnabled = "Winna.Enabled"; + [BuiltInSetting("BossmanJack's Winna username", SettingValueType.Text, "ImBossmanJack")] + public static string WinnaBmjUsername = "Winna.BmjUsername"; } }