diff --git a/KfChatDotNetBot/Models/BetBoltModels.cs b/KfChatDotNetBot/Models/BetBoltModels.cs new file mode 100644 index 0000000..7ee4411 --- /dev/null +++ b/KfChatDotNetBot/Models/BetBoltModels.cs @@ -0,0 +1,40 @@ +namespace KfChatDotNetBot.Models; + +public class BetBoltBetPayloadModel +{ + // I've always seen this sent but never know if it'll be null for privacy at some point + public string? Username { get; set; } + public string? GameCode { get; set; } + public required string GameName { get; set; } + public string? Rank { get; set; } + public required DateTimeOffset Time { get; set; } + public required string CryptoCode { get; set; } + public required string BetAmountFiat { get; set; } + public required string BetAmountCrypto { get; set; } + // Negatives for losses + public required string WinAmountFiat { get; set; } + public required string WinAmountCrypto { get; set; } + // null on losses + public string? Multiplier { get; set; } + public string? CategoryIcon { get; set; } + public List? Types { get; set; } + public string? Type { get; set; } + public required string Topic { get; set; } +} + +public class BetBoltBetModel +{ + // I won't pass through bets with a null username as there's no way to tie it to Austin + public required string Username { get; set; } + public required string GameName { get; set; } + public required DateTimeOffset Time { get; set; } + public required string Crypto { get; set; } + public required double BetAmountFiat { get; set; } + public required double BetAmountCrypto { get; set; } + // Negative if it's a loss + public required double WinAmountFiat { get; set; } + public required double WinAmountCrypto { get; set; } + public required double Multiplier { get; set; } + // Eh never know when you'll need it + public required BetBoltBetPayloadModel Payload { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Services/BetBolt.cs b/KfChatDotNetBot/Services/BetBolt.cs new file mode 100644 index 0000000..680365e --- /dev/null +++ b/KfChatDotNetBot/Services/BetBolt.cs @@ -0,0 +1,161 @@ +using System.Net; +using System.Net.WebSockets; +using System.Text.Json; +using KfChatDotNetBot.Models; +using NLog; +using Websocket.Client; + +namespace KfChatDotNetBot.Services; + +public class BetBolt : IDisposable +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + private WebsocketClient _wsClient; + private Uri _wsUri = new("wss://betbolt.com/api/ws"); + // Pings every 5 seconds so 15 seconds should be reasonable + private int _reconnectTimeout = 15; + private string? _proxy; + public delegate void OnBetBoltBetEventHandler(object sender, BetBoltBetModel bet); + public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e); + public event OnBetBoltBetEventHandler OnBetBoltBet; + public event OnWsDisconnectionEventHandler OnWsDisconnection; + private CancellationToken _cancellationToken = CancellationToken.None; + public BetBolt(string? proxy = null, CancellationToken? cancellationToken = null) + { + _proxy = proxy; + if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _logger.Info("Clash.gg 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://betbolt.com"); + clientWs.Options.SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.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 BetBolt (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.Info("Sending subscribe payload to BetBolt"); + _wsClient.Send("{\"topic\":\"system/EN\",\"action\":\"subscribe\"}"); + + } + } + + private void WsMessageReceived(ResponseMessage message) + { + if (message.Text == null) + { + _logger.Info("JeetBolt sent us null message, ignoring"); + return; + } + _logger.Debug($"Received event from BetBolt: {message.Text}"); + try + { + var packet = JsonSerializer.Deserialize>(message.Text); + if (packet == null) throw new InvalidOperationException("Caught a null when deserializing BetBolt packet"); + if (packet.ContainsKey("success")) + { + if (packet["success"].GetBoolean()) + { + _logger.Info("Successfully connected to BetBolt"); + return; + } + _logger.Error("BetBolt says our connection wasn't successful, JSON payload follows"); + _logger.Error(message.Text); + return; + } + + if (packet.ContainsKey("ping")) + { + _logger.Info("Received ping from BetBolt"); + return; + } + + if (packet.ContainsKey("topic") && packet["topic"].GetString() == "new-bet-event") + { + _logger.Debug("Received a bet event from BetBolt"); + var betPayload = JsonSerializer.Deserialize(message.Text); + if (betPayload == null) + throw new InvalidOperationException("Failed to deserialize bet payload for BetBolt"); + if (betPayload.Username == null) + throw new InvalidOperationException("Username in BetBolt bet payload was null"); + var bet = new BetBoltBetModel + { + Username = betPayload.Username, + Time = betPayload.Time, + GameName = betPayload.GameName, + Crypto = betPayload.CryptoCode, + BetAmountFiat = double.Parse(betPayload.BetAmountFiat), + BetAmountCrypto = double.Parse(betPayload.BetAmountCrypto), + WinAmountCrypto = double.Parse(betPayload.WinAmountCrypto), + WinAmountFiat = double.Parse(betPayload.WinAmountFiat), + Multiplier = double.Parse(betPayload.Multiplier ?? "0"), + Payload = betPayload + }; + OnBetBoltBet?.Invoke(this, bet); + return; + } + _logger.Debug("Unhandled event from BetBolt. Payload follows"); + _logger.Debug(message.Text); + } + catch (Exception e) + { + _logger.Error("Failed to handle message from BetBolt"); + _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/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs index 8eb8ea1..37d43b9 100644 --- a/KfChatDotNetBot/Services/BotServices.cs +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -29,6 +29,7 @@ public class BotServices private Rainbet _rainbet; private Chipsgg _chipsgg; private Clashgg _clashgg; + private BetBolt _betBolt; public AlmanacShill AlmanacShill; private Task? _websocketWatchdog; @@ -74,7 +75,8 @@ public class BotServices BuildKick(), BuildTwitch(), BuildClashgg(), - BuildAlmanacShill() + BuildAlmanacShill(), + BuildBetBolt() ]; try { @@ -143,13 +145,27 @@ public class BotServices private async Task BuildJackpot() { - var proxy = Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value; + var proxy = (await Helpers.GetValue(BuiltIn.Keys.Proxy)).Value; _jackpot = new Jackpot(proxy, _cancellationToken); _jackpot.OnJackpotBet += OnJackpotBet; await _jackpot.StartWsClient(); _logger.Info("Built Jackpot Websocket connection"); } + private async Task BuildBetBolt() + { + var settings = await Helpers.GetMultipleValues([BuiltIn.Keys.Proxy, BuiltIn.Keys.BetBoltEnabled]); + if (!settings[BuiltIn.Keys.BetBoltEnabled].ToBoolean()) + { + _logger.Debug("BetBolt is disabled"); + return; + } + _betBolt = new BetBolt(settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); + _betBolt.OnBetBoltBet += OnBetBoltBet; + await _betBolt.StartWsClient(); + _logger.Info("Built BetBolt Websocket connection"); + } + private async Task BuildClashgg() { var settings = await Helpers.GetMultipleValues([BuiltIn.Keys.Proxy, BuiltIn.Keys.ClashggEnabled]); @@ -263,7 +279,10 @@ public class BotServices while (await timer.WaitForNextTickAsync(_cancellationToken)) { if (_chatBot.InitialStartCooldown) continue; - var settings = await Helpers.GetMultipleValues([BuiltIn.Keys.KickEnabled, BuiltIn.Keys.HowlggEnabled, BuiltIn.Keys.ChipsggEnabled, BuiltIn.Keys.ClashggEnabled]); + var settings = await Helpers.GetMultipleValues([ + BuiltIn.Keys.KickEnabled, BuiltIn.Keys.HowlggEnabled, BuiltIn.Keys.ChipsggEnabled, + BuiltIn.Keys.ClashggEnabled, BuiltIn.Keys.BetBoltEnabled + ]); try { if (!_shuffle.IsConnected()) @@ -337,6 +356,14 @@ public class BotServices _clashgg = null!; await BuildClashgg(); } + + if (settings[BuiltIn.Keys.BetBoltEnabled].ToBoolean() && !_betBolt.IsConnected()) + { + _logger.Error("BetBolt died, recreating it"); + _betBolt.Dispose(); + _betBolt = null!; + await BuildBetBolt(); + } } catch (Exception e) { @@ -477,6 +504,26 @@ public class BotServices $"[color={payoutColor}]{bet.Payout / 100.0:N2} {bet.Currency.Humanize()} Money[/color] ({bet.Multiplier}x) on {bet.Game.Humanize()} 💰💰", true); } + private void OnBetBoltBet(object sender, BetBoltBetModel bet) + { + var settings = Helpers + .GetMultipleValues([ + BuiltIn.Keys.BetBoltBmjUsernames, BuiltIn.Keys.TwitchBossmanJackUsername, + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]).Result; + _logger.Trace("BetBolt bet has arrived"); + if (!settings[BuiltIn.Keys.BetBoltBmjUsernames].JsonDeserialize>()!.Contains(bet.Username)) + { + return; + } + _logger.Info("ALERT BMJ IS BETTING (on BetBolt)"); + if (CheckBmjIsLive(settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value ?? "usernamenotset").Result) return; + var payoutColor = settings[BuiltIn.Keys.KiwiFarmsGreenColor].Value; + if (bet.WinAmountFiat < 0) payoutColor = settings[BuiltIn.Keys.KiwiFarmsRedColor].Value; + _chatBot.SendChatMessage($"🚨🚨 JEETBOLT BETTING 🚨🚨 {settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value} just bet {bet.BetAmountFiat:C} ({bet.BetAmountCrypto:N2} {bet.Crypto}) and won " + + $"[color={payoutColor}]{bet.WinAmountFiat:C} ({bet.WinAmountCrypto:N2} {bet.Crypto})[/color] ({bet.Multiplier:N2}x) on {bet.GameName} 💰💰", true); + } + private void OnHowlggBetHistory(object sender, HowlggBetHistoryResponseModel data) { _logger.Debug("Received bet history from Howl.gg"); diff --git a/KfChatDotNetBot/Settings/BuiltIn.cs b/KfChatDotNetBot/Settings/BuiltIn.cs index e7dac15..8be676c 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -834,6 +834,26 @@ public static class BuiltIn IsSecret = false, CacheDuration = TimeSpan.FromHours(1), ValueType = SettingValueType.Text + }, + new BuiltInSettingsModel + { + Key = Keys.BetBoltEnabled, + Regex = "(true|false)", + Description = "Whether to enable the BetBolt bet feed tracking", + Default = "true", + IsSecret = false, + CacheDuration = TimeSpan.FromHours(1), + ValueType = SettingValueType.Boolean + }, + new BuiltInSettingsModel + { + Key = Keys.BetBoltBmjUsernames, + Regex = ".+", + Description = "Austin's usernames on BetBolt", + Default = "[\"AustinGambles\"]", + IsSecret = false, + CacheDuration = TimeSpan.FromHours(1), + ValueType = SettingValueType.Array } ]; @@ -911,5 +931,7 @@ public static class BuiltIn public static string BotImagePigCubeSelfDestructMin = "Bot.Image.PigCubeSelfDestructMin"; public static string BotImagePigCubeSelfDestructMax = "Bot.Image.PigCubeSelfDestructMax"; public static string BotImageInvertedPigCubeSelfDestructDelay = "Bot.Image.InvertedPigCubeSelfDestructDelay"; + public static string BetBoltEnabled = "BetBolt.Enabled"; + public static string BetBoltBmjUsernames = "BetBolt.BmjUsernames"; } } \ No newline at end of file