From f4db00246a19c1c25437370f63e0df4837ab9f00 Mon Sep 17 00:00:00 2001 From: barelyprofessional <150058423+barelyprofessional@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:51:56 +0800 Subject: [PATCH] Jackpot integration that probably works. Not tested --- KfChatDotNetKickBot/KickBot.cs | 58 +++++- KfChatDotNetKickBot/Models/JackpotModels.cs | 62 +++++++ KfChatDotNetKickBot/Services/Jackpot.cs | 187 ++++++++++++++++++++ KfChatDotNetKickBot/Settings/BuiltIn.cs | 9 + 4 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 KfChatDotNetKickBot/Models/JackpotModels.cs create mode 100644 KfChatDotNetKickBot/Services/Jackpot.cs diff --git a/KfChatDotNetKickBot/KickBot.cs b/KfChatDotNetKickBot/KickBot.cs index 0119d53..51091bf 100644 --- a/KfChatDotNetKickBot/KickBot.cs +++ b/KfChatDotNetKickBot/KickBot.cs @@ -40,6 +40,7 @@ public class KickBot private bool _kickDisabled = true; private bool _twitchDisabled = false; private Task _websocketWatchdog; + private Jackpot _jackpot; public KickBot() { @@ -175,6 +176,14 @@ public class KickBot _howlgg = null!; BuildHowlgg(); } + + if (!_jackpot.IsConnected()) + { + _logger.Error("Jackpot died, recreating it"); + _jackpot.Dispose(); + _jackpot = null!; + BuildJackpot(); + } } catch (Exception e) { @@ -185,6 +194,53 @@ public class KickBot } } + private void BuildJackpot() + { + var proxy = Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value; + _jackpot = new Jackpot(proxy, _cancellationToken); + _jackpot.OnJackpotBet += OnJackpotBet; + _jackpot.StartWsClient().Wait(_cancellationToken); + } + + private void OnJackpotBet(object sender, JackpotWsBetPayloadModel bet) + { + var settings = Helpers + .GetMultipleValues([ + BuiltIn.Keys.JackpotBmjUsername, BuiltIn.Keys.TwitchBossmanJackUsername, + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]).Result; + _logger.Trace("Jackpot bet has arrived"); + if (bet.User != settings[BuiltIn.Keys.JackpotBmjUsername].Value) + { + return; + } + _logger.Info("ALERT BMJ IS BETTING (on Jackpot)"); + if (IsBmjLive) + { + _logger.Info("Ignoring as BMJ is live"); + return; + } + + // Only check once because the bot should be tracking the Twitch stream + // This is just in case he's already live while the bot starts + // He was schizo betting on Dice, so I want to avoid a lot of API requests to Twitch in case they rate limit + if (!_isBmjLiveSynced) + { + IsBmjLive = _twitch.IsStreamLive(settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value!).Result; + _isBmjLiveSynced = true; + } + if (IsBmjLive) + { + _logger.Info("Double checked and he is really online"); + return; + } + + var payoutColor = settings[BuiltIn.Keys.KiwiFarmsGreenColor].Value; + if (bet.Payout < bet.Wager) payoutColor = settings[BuiltIn.Keys.KiwiFarmsRedColor].Value; + // There will be a check for live status but ignoring that while we deal with an emergency dice situation + SendChatMessage($"🚨🚨 JACKPOT BETTING 🚨🚨 {bet.User} just bet {bet.Wager} {bet.Currency} which paid out [color={payoutColor}]{bet.Payout} {bet.Currency}[/color] ({bet.Multiplier}x) on {bet.GameName} πŸ’°πŸ’°", false); + } + public void BuildTwitch() { var settings = Helpers.GetMultipleValues([BuiltIn.Keys.TwitchBossmanJackId, BuiltIn.Keys.Proxy]).Result; @@ -363,7 +419,7 @@ public class KickBot var payoutColor = settings[BuiltIn.Keys.KiwiFarmsGreenColor].Value; if (float.Parse(bet.Payout) < float.Parse(bet.Amount)) payoutColor = settings[BuiltIn.Keys.KiwiFarmsRedColor].Value; // There will be a check for live status but ignoring that while we deal with an emergency dice situation - SendChatMessage($"🚨🚨 {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); + SendChatMessage($"🚨🚨 Shufflebros! 🚨🚨 {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 OnTwitchStreamStateUpdated(object sender, int channelId, bool isLive) diff --git a/KfChatDotNetKickBot/Models/JackpotModels.cs b/KfChatDotNetKickBot/Models/JackpotModels.cs new file mode 100644 index 0000000..d1cb88b --- /dev/null +++ b/KfChatDotNetKickBot/Models/JackpotModels.cs @@ -0,0 +1,62 @@ +ο»Ώusing System.Text.Json; +using System.Text.Json.Serialization; + +namespace KfChatDotNetKickBot.Models; + +public class JackpotWsPacketModel +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + [JsonPropertyName("type")] + public required string Type { get; set; } + [JsonPropertyName("payload")] + public JsonElement? Payload { get; set; } +} + +public class JackpotWsBetPayloadModel +{ + [JsonPropertyName("createdAt")] + public required long CreatedAt { get; set; } + [JsonPropertyName("roundId")] + public required string RoundId { get; set; } + [JsonPropertyName("gameName")] + public required string GameName { get; set; } + [JsonPropertyName("gameSlug")] + public required string GameSlug { get; set; } + [JsonPropertyName("currency")] + public required string Currency { get; set; } + [JsonPropertyName("wager")] + public required float Wager { get; set; } + [JsonPropertyName("payout")] + public required float Payout { get; set; } + [JsonPropertyName("multiplier")] + public required float Multiplier { get; set; } + [JsonPropertyName("user")] + public required string User { get; set; } +} + +public class JackpotQuickviewModel +{ + [JsonPropertyName("createdAt")] + public required long CreatedAt { get; set; } + [JsonPropertyName("userId")] + public required string UserId { get; set; } + [JsonPropertyName("username")] + public required string Username { get; set; } + [JsonPropertyName("isPrivate")] + public bool IsPrivate { get; set; } + [JsonPropertyName("role")] + public required string Role { get; set; } + [JsonPropertyName("rankId")] + public required int RankId { get; set; } + [JsonPropertyName("rank")] + public required string Rank { get; set; } + [JsonPropertyName("rankProgress")] + public required float RankProgress { get; set; } + [JsonPropertyName("wagered")] + public required Dictionary Wagered { get; set; } + [JsonPropertyName("bets")] + public required Dictionary Bets { get; set; } + [JsonPropertyName("wins")] + public required Dictionary Wins { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Services/Jackpot.cs b/KfChatDotNetKickBot/Services/Jackpot.cs new file mode 100644 index 0000000..9eb5a8d --- /dev/null +++ b/KfChatDotNetKickBot/Services/Jackpot.cs @@ -0,0 +1,187 @@ +ο»Ώusing System.Net; +using System.Net.Http.Json; +using System.Net.WebSockets; +using System.Text.Json; +using KfChatDotNetKickBot.Models; +using NLog; +using Websocket.Client; + +namespace KfChatDotNetKickBot.Services; + +public class Jackpot : IDisposable +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + private WebsocketClient _wsClient; + private Uri _wsUri = new("wss://api.jackpot.bet/feeds/websocket"); + // Ping interval is 30 seconds + private int _reconnectTimeout = 60; + private string? _proxy; + public delegate void OnJackpotBetEventHandler(object sender, JackpotWsBetPayloadModel data); + public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e); + public event OnJackpotBetEventHandler OnJackpotBet; + public event OnWsDisconnectionEventHandler OnWsDisconnection; + private CancellationToken _cancellationToken = CancellationToken.None; + private CancellationTokenSource _pingCts = new(); + private Task? _heartbeatTask; + // There's no smarts, it just does 30-second pings + private TimeSpan _heartbeatInterval = TimeSpan.FromSeconds(30); + + public Jackpot(string? proxy = null, CancellationToken? cancellationToken = null) + { + _proxy = proxy; + if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _logger.Info("Jackpot 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://jackpot.bet"); + 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), + 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 async Task HeartbeatTimer() + { + using var timer = new PeriodicTimer(_heartbeatInterval); + while (await timer.WaitForNextTickAsync(_pingCts.Token)) + { + if (_wsClient == null) + { + _logger.Debug("_wsClient doesn't exist yet, not going to try ping"); + continue; + } + _logger.Debug("Sending Jackpot ping packet"); + _wsClient.Send("{\"id\":\"lfgkenokasino\",\"type\":\"ping\"}"); + } + } + + private void WsDisconnection(DisconnectionInfo disconnectionInfo) + { + _logger.Error($"Client disconnected from Jackpot (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}"); + if (reconnectionInfo.Type == ReconnectionType.Initial) + { + _logger.Debug("Sending initial payload"); + _wsClient.Send("{\"id\":\"lfgkenokasinoinit\",\"type\":\"connection_init\"}"); + } + } + + private void WsMessageReceived(ResponseMessage message) + { + if (message.Text == null) + { + _logger.Info("Jackpot sent a null message"); + return; + } + _logger.Debug($"Received event from Jackpot: {message.Text}"); + + try + { + var packet = JsonSerializer.Deserialize(message.Text); + if (packet == null) throw new InvalidOperationException("Caught a null when deserializing Jackpot packet"); + if (packet.Type == "pong") + { + _logger.Info("Received pong from Jackpot"); + return; + } + + if (packet.Type == "connection_ack") + { + _logger.Debug("Received ack from Jackpot. Sending subscription"); + _wsClient.Send( + "{\"id\":\"lfgkenokasinoallbets\",\"type\":\"subscribe\",\"payload\":{\"feed\":\"all_bets\"}}\n"); + _logger.Debug("Setting up heartbeat timer"); + if (_heartbeatTask != null) return; + _heartbeatTask = Task.Run(HeartbeatTimer, _cancellationToken); + return; + } + + if (packet.Type == "data") + { + _logger.Debug("Received bet from Jackpot"); + if (packet.Payload == null) + throw new InvalidOperationException("Payload can't be null when type is data"); + var data = packet.Payload.Value.Deserialize(); + if (data == null) throw new InvalidOperationException("Payload deserialized to a null"); + OnJackpotBet?.Invoke(this, data); + } + } + catch (Exception e) + { + _logger.Error("Failed to handle message from Jackpot"); + _logger.Error(e); + _logger.Error("--- Payload ---"); + _logger.Error(message.Text); + _logger.Error("--- End of Payload ---"); + } + } + + public async Task GetJackpotUser(string username) + { + var url = $"https://api.jackpot.bet/user/quickview/{username}"; + _logger.Debug($"Formatted URL for quickview: {url}"); + var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }; + if (_proxy != null) + { + handler.UseProxy = false; + handler.Proxy = new WebProxy(_proxy); + _logger.Debug($"Configured to use proxy {_proxy}"); + } + + using var client = new HttpClient(handler); + var response = await client.GetAsync(url, _cancellationToken); + var content = await response.Content.ReadFromJsonAsync(_cancellationToken); + if (content == null) throw new Exception("Failed to deserialize Jackpot quickview data"); + return content; + } + + public void Dispose() + { + _wsClient.Dispose(); + _pingCts.Cancel(); + _pingCts.Dispose(); + _heartbeatTask?.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Settings/BuiltIn.cs b/KfChatDotNetKickBot/Settings/BuiltIn.cs index 6f48916..d66ff29 100644 --- a/KfChatDotNetKickBot/Settings/BuiltIn.cs +++ b/KfChatDotNetKickBot/Settings/BuiltIn.cs @@ -344,6 +344,14 @@ public static class BuiltIn Description = "Red color used for showing negative values in chat", Default = "#f1323e", IsSecret = false + }, + new BuiltInSettingsModel() + { + Key = Keys.JackpotBmjUsername, + Regex = ".+", + Description = "Bossman's username on Jackpot", + Default = "TheBossmanJack", + IsSecret = false } ]; @@ -378,5 +386,6 @@ public static class BuiltIn public static string HowlggDivisionAmount = "Howlgg.DivisionAmount"; public static string KiwiFarmsGreenColor = "KiwiFarms.GreenColor"; public static string KiwiFarmsRedColor = "KiwiFarms.RedColor"; + public static string JackpotBmjUsername = "Jackpot.BmjUsername"; } } \ No newline at end of file