diff --git a/KfChatDotNetBot/Models/YeetModels.cs b/KfChatDotNetBot/Models/YeetModels.cs new file mode 100644 index 0000000..e9df755 --- /dev/null +++ b/KfChatDotNetBot/Models/YeetModels.cs @@ -0,0 +1,36 @@ +ο»Ώusing System.Text.Json.Serialization; + +namespace KfChatDotNetBot.Models; + +// {"betIdentifier":"SOL-5190-o36-2hbsgogl7-14000227177584","gameName":"Le Pharaoh","casinoGameId":5190,"username":"xPxrtaL","isPrivate":false,"tierImage":"https://cdn.dev.yeet.com/concierge/tier/Copper.png","thumbnailImage":"https://cdn.yeet.com/casinoGames/assets/s3/hacksaw/LePharaoh96.png","betAmount":1.8,"currencyCode":"SOL","createdAt":"2025-05-11T04:17:31.029Z","actionType":"bet"} +public class YeetCasinoBetModel +{ + [JsonPropertyName("betIdentifier")] + public required string BetIdentifier { get; set; } + [JsonPropertyName("gameName")] + public required string GameName { get; set; } + [JsonPropertyName("casinoGameId")] + public int? CasinoGameId { get; set; } + // Set to "hidden" for private accounts + [JsonPropertyName("username")] + public required string Username { get; set; } + [JsonPropertyName("isPrivate")] + public required bool IsPrivate { get; set; } + [JsonPropertyName("betAmount")] + public required double BetAmount { get; set; } + [JsonPropertyName("currencyCode")] + public required string CurrencyCode { get; set; } + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + [JsonPropertyName("actionType")] + public required string ActionType { get; set; } +} + +// {"betIdentifier":"SOL-2204-hv08k-12a92tcxa-109402263616008","casinoGameId":2204,"gameName":"Mega Roulette","username":"CaoSan","isPrivate":false,"tierImage":"https://cdn.dev.yeet.com/concierge/tier/Silver.png","thumbnailImage":"https://cdn.yeet.com/casinoGames/assets/s3/pragmaticexternal/MegaRoulette.png","winAmount":19.5,"betAmount":4.698795180722891,"currencyCode":"SOL","createdAt":"2025-05-11T04:17:33.142Z","multiplier":4.15,"actionType":"win"} +public class YeetCasinoWinModel : YeetCasinoBetModel +{ + [JsonPropertyName("winAmount")] + public required double WinAmount { get; set; } + [JsonPropertyName("multiplier")] + public required double Multiplier { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs index dcd7d5d..61da97d 100644 --- a/KfChatDotNetBot/Services/BotServices.cs +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -30,6 +30,7 @@ public class BotServices private Chipsgg _chipsgg; private Clashgg _clashgg; private BetBolt _betBolt; + private Yeet _yeet; public AlmanacShill AlmanacShill; private Task? _websocketWatchdog; @@ -76,7 +77,8 @@ public class BotServices BuildTwitch(), BuildClashgg(), BuildAlmanacShill(), - BuildBetBolt() + BuildBetBolt(), + BuildYeet() ]; try { @@ -165,6 +167,21 @@ public class BotServices await _betBolt.StartWsClient(); _logger.Info("Built BetBolt Websocket connection"); } + + private async Task BuildYeet() + { + var settings = await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.Proxy, BuiltIn.Keys.YeetEnabled]); + if (!settings[BuiltIn.Keys.YeetEnabled].ToBoolean()) + { + _logger.Debug("Yeet is disabled"); + return; + } + _yeet = new Yeet(settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); + _yeet.OnYeetBet += OnYeetBet; + _yeet.OnYeetWin += OnYeetWin; + await _yeet.StartWsClient(); + _logger.Info("Built Yeet Websocket connection"); + } private async Task BuildClashgg() { @@ -276,7 +293,7 @@ public class BotServices if (_chatBot.InitialStartCooldown) continue; var settings = await SettingsProvider.GetMultipleValuesAsync([ BuiltIn.Keys.KickEnabled, BuiltIn.Keys.HowlggEnabled, BuiltIn.Keys.ChipsggEnabled, - BuiltIn.Keys.ClashggEnabled, BuiltIn.Keys.BetBoltEnabled + BuiltIn.Keys.ClashggEnabled, BuiltIn.Keys.BetBoltEnabled, BuiltIn.Keys.YeetEnabled ]); try { @@ -359,6 +376,14 @@ public class BotServices _betBolt = null!; await BuildBetBolt(); } + + if (settings[BuiltIn.Keys.YeetEnabled].ToBoolean() && !_yeet.IsConnected()) + { + _logger.Error("Yeet died, recreating it"); + _yeet.Dispose(); + _yeet = null!; + await BuildYeet(); + } } catch (Exception e) { @@ -515,10 +540,49 @@ public class BotServices 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 " + + _chatBot.SendChatMessage($"🚨🚨 JEETBOLT BETTING 🚨🚨 {bet.Username} 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 OnYeetBet(object sender, YeetCasinoBetModel bet) + { + var settings = SettingsProvider + .GetMultipleValuesAsync([ + BuiltIn.Keys.YeetBmjUsernames, BuiltIn.Keys.TwitchBossmanJackUsername, + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]).Result; + _logger.Trace("Yeet bet has arrived"); + if (!settings[BuiltIn.Keys.YeetBmjUsernames].JsonDeserialize>()!.Contains(bet.Username)) + { + return; + } + _logger.Info("ALERT BMJ IS BETTING (on Yeet)"); + 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($"🚨🚨 JEET BETTING 🚨🚨 {bet.Username} just bet {bet.BetAmount:N2} {bet.CurrencyCode} on {bet.GameName} πŸ’©πŸ’©", true); + } + + private void OnYeetWin(object sender, YeetCasinoWinModel bet) + { + var settings = SettingsProvider + .GetMultipleValuesAsync([ + BuiltIn.Keys.YeetBmjUsernames, BuiltIn.Keys.TwitchBossmanJackUsername, + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]).Result; + _logger.Trace("Yeet bet has arrived"); + if (!settings[BuiltIn.Keys.YeetBmjUsernames].JsonDeserialize>()!.Contains(bet.Username)) + { + return; + } + _logger.Info("ALERT BMJ IS BETTING (on Yeet)"); + if (CheckBmjIsLive(settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value ?? "usernamenotset").Result) return; + var payoutColor = settings[BuiltIn.Keys.KiwiFarmsGreenColor].Value; + if (bet.Multiplier < 1) payoutColor = settings[BuiltIn.Keys.KiwiFarmsRedColor].Value; + _chatBot.SendChatMessage($"🚨🚨 JEET BETTING 🚨🚨 {bet.Username} just bet {bet.BetAmount:N2} {bet.CurrencyCode} and got " + + $"[color={payoutColor}]{bet.WinAmount:N2} {bet.CurrencyCode}[/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/Services/Yeet.cs b/KfChatDotNetBot/Services/Yeet.cs new file mode 100644 index 0000000..8164537 --- /dev/null +++ b/KfChatDotNetBot/Services/Yeet.cs @@ -0,0 +1,152 @@ +ο»Ώusing System.Net; +using System.Net.WebSockets; +using System.Text.Json; +using KfChatDotNetBot.Models; +using NLog; +using Websocket.Client; + +namespace KfChatDotNetBot.Services; + +public class Yeet : IDisposable +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + private WebsocketClient _wsClient; + private Uri _wsUri = new("wss://api.yeet.com/room-service/socket/?EIO=4&transport=websocket\n"); + private int _reconnectTimeout = 30; + private string? _proxy; + public delegate void OnYeetBetEventHandler(object sender, YeetCasinoBetModel data); + public delegate void OnYeetWinEventHandler(object sender, YeetCasinoWinModel data); + public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e); + public event OnYeetBetEventHandler OnYeetBet; + public event OnYeetWinEventHandler OnYeetWin; + public event OnWsDisconnectionEventHandler OnWsDisconnection; + private CancellationToken _cancellationToken = CancellationToken.None; + + public Yeet(string? proxy = null, CancellationToken? cancellationToken = null) + { + _proxy = proxy; + if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _logger.Info("Yeet 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://yeet.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), + 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 Yeet (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 Yeet"); + _wsClient.Send("40/public,"); + + } + } + + private void WsMessageReceived(ResponseMessage message) + { + if (message.Text == null) + { + _logger.Info("Yeet sent a null message"); + return; + } + _logger.Trace($"Received event from Yeet: {message.Text}"); + + try + { + var packetType = message.Text.Split('/')[0]; + if (packetType == "2") + { + _logger.Info("Received ping from Yeet, replying with pong"); + _wsClient.Send("3"); + return; + } + + if (packetType == "42") + { + var data = JsonSerializer.Deserialize>(message.Text.Replace("42/public,", + string.Empty)); + if (data[0].GetString() == "casino-bet") + { + OnYeetBet?.Invoke(this, data[1].Deserialize()); + return; + } + if (data[0].GetString() == "casino-win") + { + OnYeetWin?.Invoke(this, data[1].Deserialize()); + return; + + } + _logger.Info($"Event {data[0].GetString()} from Yeet was not handled"); + _logger.Info(message.Text); + return; + } + + if (message.Text == "40") + { + _logger.Info("Yeet has replied to the subscription packet"); + return; + } + } + catch (Exception e) + { + _logger.Error("Failed to handle message from Yeet"); + _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 5a1c29a..7339090 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -857,6 +857,26 @@ public static class BuiltIn IsSecret = false, CacheDuration = TimeSpan.FromHours(1), ValueType = SettingValueType.Array + }, + new BuiltInSettingsModel + { + Key = Keys.YeetEnabled, + Regex = "(true|false)", + Description = "Whether to enable the Yeet bet feed tracking", + Default = "true", + IsSecret = false, + CacheDuration = TimeSpan.FromHours(1), + ValueType = SettingValueType.Boolean + }, + new BuiltInSettingsModel + { + Key = Keys.YeetBmjUsernames, + Regex = ".+", + Description = "Austin's usernames on Yeet", + Default = "[\"Bossmanjack\"]", + IsSecret = false, + CacheDuration = TimeSpan.FromHours(1), + ValueType = SettingValueType.Array } ]; @@ -935,5 +955,7 @@ public static class BuiltIn public static string BotImageInvertedPigCubeSelfDestructDelay = "Bot.Image.InvertedPigCubeSelfDestructDelay"; public static string BetBoltEnabled = "BetBolt.Enabled"; public static string BetBoltBmjUsernames = "BetBolt.BmjUsernames"; + public static string YeetEnabled = "Yeet.Enabled"; + public static string YeetBmjUsernames = "Yeet.BmjUsernames"; } } \ No newline at end of file