From e4b1821a5bfaa4bdf142e16737c60294cfdb249f Mon Sep 17 00:00:00 2001 From: barelyprofessional <150058423+barelyprofessional@users.noreply.github.com> Date: Sun, 1 Sep 2024 00:53:44 +0800 Subject: [PATCH] All the ancillary services have been moved out of the chatbot's class and relocated to a separate file. THe code is still very messy but at least it'll make the main bot easier to navigate. Also refactored a bunch of shit * Removed the thread used for pinging, now an async timer * Kick will no longer block the bot from starting * Twitch initialization follows the same rules as other services where everything is contained to its build method * Fixed a bug where the bot's heartbeat logic would get messed up by the machine timezone if it wasn't UTC --- KfChatDotNetBot/ChatBot.cs | 633 +------------------ KfChatDotNetBot/Commands/UtilityCommands.cs | 6 +- KfChatDotNetBot/Services/BotServices.cs | 638 ++++++++++++++++++++ 3 files changed, 670 insertions(+), 607 deletions(-) create mode 100644 KfChatDotNetBot/Services/BotServices.cs diff --git a/KfChatDotNetBot/ChatBot.cs b/KfChatDotNetBot/ChatBot.cs index fd31791..529488e 100644 --- a/KfChatDotNetBot/ChatBot.cs +++ b/KfChatDotNetBot/ChatBot.cs @@ -1,6 +1,5 @@ ο»Ώusing System.Net; using System.Text.Json; -using Humanizer; using KfChatDotNetBot.Models; using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Services; @@ -9,7 +8,6 @@ using KfChatDotNetWsClient; using KfChatDotNetWsClient.Models; using KfChatDotNetWsClient.Models.Events; using KfChatDotNetWsClient.Models.Json; -using KickWsClient.Models; using NLog; using Websocket.Client; @@ -18,38 +16,19 @@ namespace KfChatDotNetBot; public class ChatBot { internal readonly ChatClient KfClient; - private readonly KickWsClient.KickWsClient _kickClient; private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - private readonly bool _pingEnabled = true; - internal bool GambaSeshPresent; - private string _xfSessionToken = null!; + private string _xfSessionToken; // Oh no it's an ever expanding list that may never get cleaned up! // BUY MORE RAM private readonly List _seenMsgIds = []; // Suppresses the command handler on initial start, so it doesn't pick up things already handled on restart - private bool _initialStartCooldown = true; + internal bool InitialStartCooldown = true; private readonly CancellationToken _cancellationToken = new(); - private Twitch _twitch; - private Shuffle _shuffle; - private DiscordService _discord; - private TwitchChat _twitchChat; - private string? _lastDiscordStatus; - internal bool IsBmjLive = false; - private bool _isBmjLiveSynced = false; - private DateTime _lastKfEvent = DateTime.UtcNow; - private BotCommands _botCommands; - private string _bmjTwitchUsername; - private Howlgg _howlgg; - private bool _kickDisabled = true; - private bool _twitchDisabled = false; - private Task _websocketWatchdog; - private Jackpot _jackpot; - private Rainbet _rainbet; - private Chipsgg _chipsgg; - private List _sentMessages = []; - // lol - internal bool TemporarilyBypassGambaSeshForDiscord = false; - internal bool TemporarilySuppressGambaMessages = false; + private readonly BotCommands _botCommands; + private readonly List _sentMessages = []; + internal bool GambaSeshPresent; + internal readonly BotServices BotServices; + private Task _kfChatPing; public ChatBot() { @@ -77,9 +56,7 @@ public class ChatBot ReconnectTimeout = settings[BuiltIn.Keys.KiwiFarmsWsReconnectTimeout].ToType() }); - _kickClient = new KickWsClient.KickWsClient(settings[BuiltIn.Keys.PusherEndpoint].Value!, - settings[BuiltIn.Keys.Proxy].Value, settings[BuiltIn.Keys.PusherReconnectTimeout].ToType()); - + _logger.Debug("Creating bot command instance"); _botCommands = new BotCommands(this, _cancellationToken); @@ -89,527 +66,21 @@ public class ChatBot KfClient.OnWsDisconnection += OnKfWsDisconnected; KfClient.OnWsReconnect += OnKfWsReconnected; KfClient.OnFailedToJoinRoom += OnFailedToJoinRoom; - - _kickClient.OnStreamerIsLive += OnStreamerIsLive; - _kickClient.OnChatMessage += OnKickChatMessage; - _kickClient.OnWsReconnect += OnPusherWsReconnected; - _kickClient.OnPusherSubscriptionSucceeded += OnPusherSubscriptionSucceeded; - _kickClient.OnStopStreamBroadcast += OnStopStreamBroadcast; KfClient.StartWsClient().Wait(_cancellationToken); - if (settings[BuiltIn.Keys.KickEnabled].ToBoolean()) - { - _kickClient.StartWsClient().Wait(_cancellationToken); - var pusherChannels = settings[BuiltIn.Keys.PusherChannels].ToList(); - foreach (var channel in pusherChannels) - { - _kickClient.SendPusherSubscribe(channel); - } - - _kickDisabled = false; - } - - _logger.Debug("Creating ping thread and starting it"); - var pingThread = new Thread(PingThread); - pingThread.Start(); - - if (settings[BuiltIn.Keys.TwitchBossmanJackId].Value != null) - { - _logger.Debug("Creating Twitch live stream notification client"); - BuildTwitch(); - } - else - { - _twitchDisabled = true; - _logger.Debug($"Ignoring Twitch client as {BuiltIn.Keys.TwitchBossmanJackId} is not defined"); - } - - BuildShuffle(); - BuildDiscord(); - BuildTwitchChat(); - BuildHowlgg(); - BuildJackpot(); - BuildRainbet(); - BuildChipsgg(); + _logger.Debug("Creating ping task"); + _kfChatPing = KfPingTask(); + + _logger.Debug("Starting services"); + BotServices = new BotServices(this, _cancellationToken); + BotServices.InitializeServices(); - _logger.Info("Starting websocket watchdog"); - _websocketWatchdog = WebsocketWatchdog(); - _logger.Debug("Blocking the main thread"); var exitEvent = new ManualResetEvent(false); exitEvent.WaitOne(); } - private async Task WebsocketWatchdog() - { - using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); - while (await timer.WaitForNextTickAsync(_cancellationToken)) - { - if (_initialStartCooldown) continue; - try - { - if (!_shuffle.IsConnected()) - { - _logger.Error("Shuffle died, recreating it"); - _shuffle.Dispose(); - _shuffle = null!; - BuildShuffle(); - } - - if (!_discord.IsConnected()) - { - _logger.Error("Discord died, recreating it"); - _discord.Dispose(); - _discord = null!; - BuildDiscord(); - } - - if (!_twitchDisabled && !_twitch.IsConnected()) - { - _logger.Error("Twitch died, recreating it"); - _twitch.Dispose(); - _twitch = null!; - BuildTwitch(); - } - - if (!_twitchChat.IsConnected()) - { - _logger.Error("Twitch chat died, recreating it"); - _twitchChat.Dispose(); - _twitchChat = null!; - BuildTwitchChat(); - } - - if (!_howlgg.IsConnected()) - { - _logger.Error("Howl.gg died, recreating it"); - _howlgg.Dispose(); - _howlgg = null!; - BuildHowlgg(); - } - - if (!_jackpot.IsConnected()) - { - _logger.Error("Jackpot died, recreating it"); - _jackpot.Dispose(); - _jackpot = null!; - BuildJackpot(); - } - - if (!_chipsgg.IsConnected()) - { - _logger.Error("Chips died, recreating it"); - _chipsgg.Dispose(); - _chipsgg = null!; - BuildChipsgg(); - } - } - catch (Exception e) - { - _logger.Error("Watchdog shit itself while trying to do something, exception follows"); - _logger.Error(e); - } - - } - } - - private void BuildRainbet() - { - _rainbet = new Rainbet(_cancellationToken); - _rainbet.OnRainbetBet += OnRainbetBet; - _rainbet.StartGameHistoryTimer(); - } - - private void BuildChipsgg() - { - var proxy = Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value; - _chipsgg = new Chipsgg(proxy, _cancellationToken); - _chipsgg.OnChipsggRecentBet += OnChipsggRecentBet; - _chipsgg.StartWsClient().Wait(_cancellationToken); - } - - private void OnChipsggRecentBet(object sender, ChipsggBetModel bet) - { - var settings = Helpers - .GetMultipleValues([ - BuiltIn.Keys.ChipsggBmjUsername, BuiltIn.Keys.TwitchBossmanJackUsername, - BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor - ]).Result; - _logger.Trace("Chips.gg bet has arrived"); - if (bet.Username != settings[BuiltIn.Keys.ChipsggBmjUsername].Value) - { - return; - } - _logger.Info("ALERT BMJ IS BETTING (on Chips.gg)"); - using var db = new ApplicationDbContext(); - db.ChipsggBets.Add(new ChipsggBetDbModel - { - Created = bet.Created, Updated = bet.Updated, UserId = bet.UserId, Username = bet.Username ?? "Unknown", Win = bet.Win, - Winnings = bet.Winnings, GameTitle = bet.GameTitle!, Amount = bet.Amount, Multiplier = bet.Multiplier, - Currency = bet.Currency!, CurrencyPrice = bet.CurrencyPrice, BetId = bet.BetId - }); - db.SaveChanges(); - if (IsBmjLive) - { - _logger.Info("Ignoring as BMJ is live"); - return; - } - - if (TemporarilySuppressGambaMessages) - { - _logger.Info("Ignoring as TemporarilySuppressGambaMessages is true"); - 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.Winnings < bet.Amount) payoutColor = settings[BuiltIn.Keys.KiwiFarmsRedColor].Value; - SendChatMessage( - $"🚨🚨 CHIPS BROS 🚨🚨 {bet.Username} just bet {bet.Amount:N} {bet.Currency!.ToUpper()} " + - $"({bet.Amount * bet.CurrencyPrice:C}) which paid out [color={payoutColor}]{bet.Winnings:N} {bet.Currency.ToUpper()} " + - $"({bet.Winnings * bet.CurrencyPrice:C})[/color] ({bet.Multiplier:N}x) on {bet.GameTitle} πŸ’°πŸ’°", - true); - } - - private void OnRainbetBet(object sender, List bets) - { - var settings = Helpers - .GetMultipleValues([ - BuiltIn.Keys.RainbetBmjPublicId, BuiltIn.Keys.TwitchBossmanJackUsername, - BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor - ]).Result; - _logger.Trace("Rainbet bet has arrived"); - using var db = new ApplicationDbContext(); - foreach (var bet in bets.Where(b => b.User.PublicId == settings[BuiltIn.Keys.RainbetBmjPublicId].Value)) - //foreach (var bet in bets) - { - if (db.RainbetBets.Any(b => b.BetId == bet.Id)) - { - _logger.Trace($"Ignoring bet {bet.Id} as we've already logged it"); - continue; - } - - db.RainbetBets.Add(new RainbetBetsDbModel - { - PublicId = bet.User.PublicId, - RainbetUserId = bet.User.Id, - GameName = bet.Game.Name, - Value = bet.Value, - Payout = bet.Payout, - Multiplier = bet.Multiplier, - BetId = bet.Id, - UpdatedAt = bet.UpdatedAt, - BetSeenAt = DateTimeOffset.UtcNow - }); - _logger.Info("Added a Bossman Rainbet bet to the database"); - } - - db.SaveChanges(); - } - - 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; - } - if (TemporarilySuppressGambaMessages) - { - _logger.Info("Ignoring as TemporarilySuppressGambaMessages is true"); - 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; - 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; - _twitch = new Twitch([settings[BuiltIn.Keys.TwitchBossmanJackId].ToType()], settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); - _twitch.OnStreamStateUpdated += OnTwitchStreamStateUpdated; - _twitch.StartWsClient().Wait(_cancellationToken); - } - - private void BuildHowlgg() - { - var proxy = Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value; - _howlgg = new Howlgg(proxy, _cancellationToken); - _howlgg.OnHowlggBetHistory += OnHowlggBetHistory; - _howlgg.StartWsClient().Wait(_cancellationToken); - } - - private void OnHowlggBetHistory(object sender, HowlggBetHistoryResponseModel data) - { - _logger.Debug("Received bet history from Howl.gg"); - using var db = new ApplicationDbContext(); - foreach (var bet in data.History.Data) - { - // Slot feature buys have an unrealized value that means they show no profit until the feature finishes - // The feed will return the correct profit later hence updating the values - var existingBet = db.HowlggBets.FirstOrDefault(b => b.BetId == bet.Id); - if (existingBet != null) - { - _logger.Trace("Bet already exists in DB"); - if (existingBet.Bet == bet.Bet && existingBet.Profit == bet.Profit) continue; - _logger.Debug("Updating fields"); - existingBet.Bet = bet.Bet; - existingBet.Profit = bet.Profit; - db.SaveChanges(); - continue; - } - - db.HowlggBets.Add(new HowlggBetsDbModel - { - UserId = data.User.Id, - BetId = bet.Id, - GameId = bet.GameId, - Bet = bet.Bet, - Profit = bet.Profit, - Date = bet.Date, - Game = bet.Game - }); - _logger.Info("Added bet to DB"); - } - - db.SaveChanges(); - } - - private void BuildShuffle() - { - _logger.Debug("Building Shuffle"); - _shuffle = new Shuffle(Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value, _cancellationToken); - _shuffle.OnLatestBetUpdated += ShuffleOnLatestBetUpdated; - _shuffle.StartWsClient().Wait(_cancellationToken); - } - - private void BuildDiscord() - { - var settings = Helpers.GetMultipleValues([BuiltIn.Keys.DiscordToken, BuiltIn.Keys.Proxy]).Result; - _logger.Debug("Building Discord"); - if (settings[BuiltIn.Keys.DiscordToken].Value == null) - { - _logger.Info("Not building Discord as the token is not configured"); - return; - } - _discord = new DiscordService(settings[BuiltIn.Keys.DiscordToken].Value!, settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); - _discord.OnInvalidCredentials += DiscordOnInvalidCredentials; - _discord.OnMessageReceived += DiscordOnMessageReceived; - _discord.OnPresenceUpdated += DiscordOnPresenceUpdated; - _discord.OnChannelCreated += DiscordOnChannelCreated; - _discord.OnChannelDeleted += DiscordOnChannelDeleted; - _discord.StartWsClient().Wait(_cancellationToken); - } - - private void DiscordOnChannelDeleted(object sender, DiscordChannelDeletionModel channel) - { - _logger.Info($"Received channel deletion event of type {channel.Type} with name {channel.Name}"); - if (channel.Type != DiscordChannelType.GuildText && channel.Type != DiscordChannelType.GuildVoice && - channel.Type != DiscordChannelType.GuildStageVoice) return; - var discordIcon = Helpers.GetValue(BuiltIn.Keys.DiscordIcon).Result; - var channelName = channel.Name ?? "Unknown name"; - SendChatMessage($"[img]{discordIcon.Value}[/img] Discord {channel.Type.Humanize()} channel '{channelName}' was deleted 🚨🚨", true); - } - - private void DiscordOnChannelCreated(object sender, DiscordChannelCreationModel channel) - { - _logger.Info($"Received channel creation event of type {channel.Type} with name {channel.Name}"); - if (channel.Type != DiscordChannelType.GuildText && channel.Type != DiscordChannelType.GuildVoice && - channel.Type != DiscordChannelType.GuildStageVoice) return; - var discordIcon = Helpers.GetValue(BuiltIn.Keys.DiscordIcon).Result; - var channelName = channel.Name ?? "Unknown name"; - SendChatMessage($"[img]{discordIcon.Value}[/img] New Discord {channel.Type.Humanize()} channel created: {channelName} 🚨🚨", true); - } - - private void BuildTwitchChat() - { - var settings = Helpers.GetMultipleValues([BuiltIn.Keys.TwitchBossmanJackUsername, BuiltIn.Keys.Proxy]).Result; - _logger.Debug("Building Twitch Chat"); - if (settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value == null) - { - _logger.Info("Not building Twitch Chat client as BMJ's username is not configured"); - return; - } - - _bmjTwitchUsername = settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value!; - - _twitchChat = new TwitchChat($"#{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value}", settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); - _twitchChat.OnMessageReceived += TwitchChatOnMessageReceived; - _twitchChat.StartWsClient().Wait(_cancellationToken); - } - - private void TwitchChatOnMessageReceived(object sender, string nick, string target, string message) - { - if (nick != _bmjTwitchUsername) - { - return; - } - // Not caching this value as it won't harm it to have to look this up in even the worst spergout sesh - var twitchIcon = Helpers.GetValue(BuiltIn.Keys.TwitchIcon).Result.Value; - SendChatMessage($"[img]{twitchIcon}[/img] {nick}: {message.TrimEnd('\r')}", true); - } - - private void DiscordOnPresenceUpdated(object sender, DiscordPresenceUpdateModel presence) - { - var settings = Helpers.GetMultipleValues([BuiltIn.Keys.DiscordBmjId, BuiltIn.Keys.DiscordIcon]).Result; - if (presence.User.Id != settings[BuiltIn.Keys.DiscordBmjId].Value) - { - return; - } - // if (_lastDiscordStatus == presence.Status) - // { - // _logger.Debug("Ignoring status update as it's the same as the last one"); - // return; - // } - // _lastDiscordStatus = presence.Status; - var clientStatus = presence.ClientStatus.Keys.Aggregate(string.Empty, (current, device) => current + $"{device} is {presence.ClientStatus[device]}; "); - SendChatMessage($"[img]{settings[BuiltIn.Keys.DiscordIcon].Value}[/img] BossmanJack has updated his Discord presence: {clientStatus}"); - } - - private void DiscordOnMessageReceived(object sender, DiscordMessageModel message) - { - var settings = Helpers.GetMultipleValues([BuiltIn.Keys.DiscordBmjId, BuiltIn.Keys.DiscordIcon]).Result; - if (message.Author.Id != settings[BuiltIn.Keys.DiscordBmjId].Value) - { - return; - } - - if (message.Type == DiscordMessageType.StageStart) - { - SendChatMessage($"[img]{settings[BuiltIn.Keys.DiscordIcon].Value}[/img] BossmanJack just started a stage called {message.Content} 🚨🚨" + - $"[br]🚨🚨 BossmanJack is [b]LIVE[/b] on Discord! 🚨🚨", - true); - return; - } - if (message.Type == DiscordMessageType.StageEnd) - { - SendChatMessage($"[img]{settings[BuiltIn.Keys.DiscordIcon].Value}[/img] BossmanJack just ended a stage called {message.Content} :lossmanjack:", - true); - return; - } - - var result = $"[img]{settings[BuiltIn.Keys.DiscordIcon].Value}[/img] BossmanJack: {message.Content}"; - foreach (var attachment in message.Attachments ?? []) - { - result += $"[br]Attachment: {attachment.GetProperty("filename").GetString()} {attachment.GetProperty("url").GetString()}"; - } - - SendChatMessage(result, TemporarilyBypassGambaSeshForDiscord); - } - - private void DiscordOnInvalidCredentials(object sender, DiscordPacketReadModel packet) - { - _logger.Error("Credentials failed to validate."); - } - - private void ShuffleOnLatestBetUpdated(object sender, ShuffleLatestBetModel bet) - { - var settings = Helpers - .GetMultipleValues([ - BuiltIn.Keys.ShuffleBmjUsername, BuiltIn.Keys.TwitchBossmanJackUsername, - BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor - ]).Result; - _logger.Trace("Shuffle bet has arrived"); - if (bet.Username != settings[BuiltIn.Keys.ShuffleBmjUsername].Value) - { - return; - } - _logger.Info("ALERT BMJ IS BETTING"); - if (IsBmjLive) - { - _logger.Info("Ignoring as BMJ is live"); - return; - } - if (TemporarilySuppressGambaMessages) - { - _logger.Info("Ignoring as TemporarilySuppressGambaMessages is true"); - 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 (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($"🚨🚨 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) - { - _logger.Info($"BossmanJack stream event came in. isLive => {isLive}"); - if (isLive) - { - var restream = Helpers.GetValue(BuiltIn.Keys.RestreamUrl).Result.Value; - SendChatMessage("BossmanJack just went live on Twitch! https://www.twitch.tv/thebossmanjack\r\n" + - $"Ad-free re-stream at {restream} courtesy of @Kees H"); - IsBmjLive = true; - return; - } - SendChatMessage("BossmanJack is no longer live! :lossmanjack:"); - IsBmjLive = false; - } - private void OnFailedToJoinRoom(object sender, string message) { _logger.Error($"Couldn't join the room. KF returned: {message}"); @@ -622,28 +93,24 @@ public class ChatBot _logger.Info("Client should be reconnecting now"); } - private void PingThread() + private async Task KfPingTask() { - while (_pingEnabled) + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + while (await timer.WaitForNextTickAsync(_cancellationToken)) { - Thread.Sleep(TimeSpan.FromSeconds(15)); _logger.Debug("Pinging KF"); KfClient.SendMessage("/ping"); - if (!_kickDisabled) - { - _kickClient.SendPusherPing(); - } - if (_initialStartCooldown) _initialStartCooldown = false; - var inactivityTime = DateTime.Now - _lastKfEvent; + if (InitialStartCooldown) InitialStartCooldown = false; + var inactivityTime = DateTime.UtcNow - KfClient.LastPacketReceived; _logger.Debug($"Last KF event was {inactivityTime:g} ago"); if (inactivityTime.TotalMinutes > 10) { - _lastKfEvent = DateTime.UtcNow; - _logger.Error("Forcing reconnection as bot is completely dead"); - KfClient.Reconnect().Wait(_cancellationToken); + // Yeah, super dodgy + KfClient.LastPacketReceived = DateTime.UtcNow; + _logger.Error("Forcing disconnect and restart as bot is completely dead"); + await KfClient.DisconnectAsync(); + await KfClient.StartWsClient(); } - _logger.Debug("Polling Bossman's Howl.gg stats"); - _howlgg?.GetUserInfo("951905"); } } @@ -660,23 +127,11 @@ public class ChatBot await Helpers.SetValue(BuiltIn.Keys.KiwiFarmsToken, _xfSessionToken); } - private void OnStreamerIsLive(object sender, KickModels.StreamerIsLiveEventModel? e) - { - if (e == null) return; - SendChatMessage($"Dirt Devils LFG! @Juhlonduss is live! {e.Livestream.SessionTitle} https://kick.com/dirtdevil-enjoyer", true); - } - - private void OnStopStreamBroadcast(object sender, KickModels.StopStreamBroadcastEventModel? e) - { - SendChatMessage("Dirt Devils felted. Stream is over. :lossmanjack:", true); - } - private void OnKfChatMessage(object sender, List messages, MessagesJsonModel jsonPayload) { var settings = Helpers.GetMultipleValues([BuiltIn.Keys.GambaSeshDetectEnabled, BuiltIn.Keys.GambaSeshUserId, BuiltIn.Keys.KiwiFarmsUsername]) .Result; - _lastKfEvent = DateTime.UtcNow; _logger.Debug($"Received {messages.Count} message(s)"); foreach (var message in messages) { @@ -702,25 +157,25 @@ public class ChatBot } } - if (message.Author.Id == settings[BuiltIn.Keys.GambaSeshUserId].ToType() && TemporarilyBypassGambaSeshForDiscord && + if (message.Author.Id == settings[BuiltIn.Keys.GambaSeshUserId].ToType() && BotServices.TemporarilyBypassGambaSeshForDiscord && message.MessageRaw.Contains("discord16")) { _logger.Info("GambaSesh fixed itself, turning off bypass"); - TemporarilyBypassGambaSeshForDiscord = false; + BotServices.TemporarilyBypassGambaSeshForDiscord = false; } - if (settings[BuiltIn.Keys.GambaSeshDetectEnabled].ToBoolean() && !_initialStartCooldown && message.Author.Id == settings[BuiltIn.Keys.GambaSeshUserId].ToType() && !GambaSeshPresent) + if (settings[BuiltIn.Keys.GambaSeshDetectEnabled].ToBoolean() && !InitialStartCooldown && message.Author.Id == settings[BuiltIn.Keys.GambaSeshUserId].ToType() && !GambaSeshPresent) { _logger.Info("Received a GambaSesh message after cooldown and while thinking he's not here. Setting the presence flag to avoid spamming chat"); GambaSeshPresent = true; } - if (!_seenMsgIds.Contains(message.MessageId) && !_initialStartCooldown) + if (!_seenMsgIds.Contains(message.MessageId) && !InitialStartCooldown) { _logger.Debug("Passing message to command interface"); _botCommands.ProcessMessage(message); } else { - _logger.Debug($"_seenMsgIds check => {!_seenMsgIds.Contains(message.MessageId)}, _initialStartCooldown => {_initialStartCooldown}"); + _logger.Debug($"_seenMsgIds check => {!_seenMsgIds.Contains(message.MessageId)}, _initialStartCooldown => {InitialStartCooldown}"); } _seenMsgIds.Add(message.MessageId); } @@ -772,24 +227,11 @@ public class ChatBot } public class SentMessageNotFoundException : Exception; - - private void OnKickChatMessage(object sender, KickModels.ChatMessageEventModel? e) - { - if (e == null) return; - _logger.Debug($"BB Code Translation: {e.Content.TranslateKickEmotes()}"); - - if (e.Sender.Slug != "bossmanjack") return; - var kickIcon = Helpers.GetValue(BuiltIn.Keys.KickIcon).Result; - - _logger.Debug("Message from BossmanJack"); - SendChatMessage($"[img]{kickIcon.Value}[/img] BossmanJack: {e.Content.TranslateKickEmotes()}"); - } private void OnUsersJoined(object sender, List users, UsersJsonModel jsonPayload) { var settings = Helpers.GetMultipleValues([BuiltIn.Keys.GambaSeshUserId, BuiltIn.Keys.GambaSeshDetectEnabled]) .Result; - _lastKfEvent = DateTime.UtcNow; _logger.Debug($"Received {users.Count} user join events"); using var db = new ApplicationDbContext(); foreach (var user in users) @@ -824,7 +266,6 @@ public class ChatBot { var settings = Helpers.GetMultipleValues([BuiltIn.Keys.GambaSeshUserId, BuiltIn.Keys.GambaSeshDetectEnabled]) .Result; - _lastKfEvent = DateTime.UtcNow; if (userIds.Contains(settings[BuiltIn.Keys.GambaSeshUserId].ToType()) && settings[BuiltIn.Keys.GambaSeshDetectEnabled].ToBoolean()) { _logger.Info("GambaSesh is no longer present"); @@ -848,20 +289,4 @@ public class ChatBot _logger.Info($"Rejoining {roomId}"); KfClient.JoinRoom(roomId); } - - private void OnPusherWsReconnected(object sender, ReconnectionInfo reconnectionInfo) - { - _logger.Error($"Pusher reconnected due to {reconnectionInfo.Type}"); - var channels = Helpers.GetValue(BuiltIn.Keys.PusherChannels).Result.Value ?? ""; - foreach (var channel in channels.Split(',')) - { - _logger.Info($"Rejoining {channel}"); - _kickClient.SendPusherSubscribe(channel); - } - } - - private void OnPusherSubscriptionSucceeded(object sender, PusherModels.BasePusherEventModel? e) - { - _logger.Info($"Pusher indicates subscription to {e?.Channel} was successful"); - } } \ No newline at end of file diff --git a/KfChatDotNetBot/Commands/UtilityCommands.cs b/KfChatDotNetBot/Commands/UtilityCommands.cs index 34a8ceb..8c4ed16 100644 --- a/KfChatDotNetBot/Commands/UtilityCommands.cs +++ b/KfChatDotNetBot/Commands/UtilityCommands.cs @@ -15,7 +15,7 @@ public class TempEnableDiscordRelayingCommand : ICommand public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { - botInstance.TemporarilyBypassGambaSeshForDiscord = true; + botInstance.BotServices.TemporarilyBypassGambaSeshForDiscord = true; botInstance.SendChatMessage("Enjoy Discord messages, stalker child", true); } } @@ -31,7 +31,7 @@ public class TempSuppressGambaMessages : ICommand public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { - botInstance.TemporarilySuppressGambaMessages = true; + botInstance.BotServices.TemporarilySuppressGambaMessages = true; botInstance.SendChatMessage("No more gamba notifs", true); } } @@ -47,7 +47,7 @@ public class EnableGambaMessages : ICommand public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { - botInstance.TemporarilySuppressGambaMessages = false; + botInstance.BotServices.TemporarilySuppressGambaMessages = false; botInstance.SendChatMessage("Gamba notifs back on the menu", true); } } \ No newline at end of file diff --git a/KfChatDotNetBot/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs new file mode 100644 index 0000000..f07d3d2 --- /dev/null +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -0,0 +1,638 @@ +ο»Ώusing Humanizer; +using KfChatDotNetBot.Models; +using KfChatDotNetBot.Models.DbModels; +using KfChatDotNetBot.Settings; +using KickWsClient.Models; +using NLog; +using Websocket.Client; + +namespace KfChatDotNetBot.Services; + +/// +/// The glue that binds the chatbot to all the third-party services beyond Sneedchat +/// Mostly just trying to move code out of ChatBot.cs as it's ridiculously large now +/// +public class BotServices +{ + private readonly ChatBot _chatBot; + private readonly CancellationToken _cancellationToken; + private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + private KickWsClient.KickWsClient _kickClient; + private Twitch _twitch; + private Shuffle _shuffle; + private DiscordService _discord; + private TwitchChat _twitchChat; + private Jackpot _jackpot; + private Howlgg _howlgg; + private Rainbet _rainbet; + private Chipsgg _chipsgg; + + private Task? _websocketWatchdog; + private Task? _howlggGetUserTimer; + + private string _bmjTwitchUsername; + private bool _twitchDisabled = false; + private string? _lastDiscordStatus; + internal bool IsBmjLive = false; + private bool _isBmjLiveSynced = false; + + // lol + internal bool TemporarilyBypassGambaSeshForDiscord = false; + internal bool TemporarilySuppressGambaMessages = false; + + public BotServices(ChatBot botInstance, CancellationToken ctx) + { + _chatBot = botInstance; + _cancellationToken = ctx; + + _logger.Info("Bot services ready to initialize!"); + } + + public void InitializeServices() + { + if (_websocketWatchdog != null) + { + _logger.Error("InitializeServices method was called but we initialized as _websocketWatchdog is not null?"); + throw new InvalidOperationException("Bot services already initialized"); + } + _logger.Info("Initializing services"); + BuildShuffle(); + BuildDiscord(); + BuildTwitchChat(); + BuildHowlgg(); + BuildJackpot(); + BuildRainbet(); + BuildChipsgg(); + BuildKick(); + BuildTwitch(); + + _logger.Info("Starting websocket watchdog and Howl.gg user stats timer"); + _websocketWatchdog = WebsocketWatchdog(); + _howlggGetUserTimer = HowlggGetUserTimer(); + } + + private void BuildShuffle() + { + _logger.Debug("Building Shuffle"); + _shuffle = new Shuffle(Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value, _cancellationToken); + _shuffle.OnLatestBetUpdated += ShuffleOnLatestBetUpdated; + _shuffle.StartWsClient().Wait(_cancellationToken); + } + + private void BuildDiscord() + { + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.DiscordToken, BuiltIn.Keys.Proxy]).Result; + _logger.Debug("Building Discord"); + if (settings[BuiltIn.Keys.DiscordToken].Value == null) + { + _logger.Info("Not building Discord as the token is not configured"); + return; + } + _discord = new DiscordService(settings[BuiltIn.Keys.DiscordToken].Value!, settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); + _discord.OnInvalidCredentials += DiscordOnInvalidCredentials; + _discord.OnMessageReceived += DiscordOnMessageReceived; + _discord.OnPresenceUpdated += DiscordOnPresenceUpdated; + _discord.OnChannelCreated += DiscordOnChannelCreated; + _discord.OnChannelDeleted += DiscordOnChannelDeleted; + _discord.StartWsClient().Wait(_cancellationToken); + } + + private void BuildRainbet() + { + _rainbet = new Rainbet(_cancellationToken); + _rainbet.OnRainbetBet += OnRainbetBet; + _rainbet.StartGameHistoryTimer(); + _logger.Info("Built Rainbet timer"); + } + + private void BuildChipsgg() + { + var proxy = Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value; + _chipsgg = new Chipsgg(proxy, _cancellationToken); + _chipsgg.OnChipsggRecentBet += OnChipsggRecentBet; + _chipsgg.StartWsClient().Wait(_cancellationToken); + _logger.Info("Built Chips.gg Websocket connection"); + } + + private void BuildJackpot() + { + var proxy = Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value; + _jackpot = new Jackpot(proxy, _cancellationToken); + _jackpot.OnJackpotBet += OnJackpotBet; + _jackpot.StartWsClient().Wait(_cancellationToken); + _logger.Info("Built Jackpot Websocket connection"); + } + + private void BuildTwitch() + { + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.TwitchBossmanJackId, BuiltIn.Keys.Proxy]).Result; + if (settings[BuiltIn.Keys.TwitchBossmanJackId].Value == null) + { + _twitchDisabled = true; + _logger.Debug($"Ignoring Twitch client as {BuiltIn.Keys.TwitchBossmanJackId} is not defined"); + return; + } + _twitch = new Twitch([settings[BuiltIn.Keys.TwitchBossmanJackId].ToType()], settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); + _twitch.OnStreamStateUpdated += OnTwitchStreamStateUpdated; + _twitch.StartWsClient().Wait(_cancellationToken); + _logger.Info("Built Twitch Websocket connection for livestream notifications"); + } + + private void BuildHowlgg() + { + var proxy = Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value; + _howlgg = new Howlgg(proxy, _cancellationToken); + _howlgg.OnHowlggBetHistory += OnHowlggBetHistory; + _howlgg.StartWsClient().Wait(_cancellationToken); + _logger.Info("Built Howl.gg Websocket connection"); + } + + private void BuildKick() + { + var settings = Helpers.GetMultipleValues([ + BuiltIn.Keys.PusherEndpoint, BuiltIn.Keys.Proxy, BuiltIn.Keys.PusherReconnectTimeout, BuiltIn.Keys.KickEnabled + ]).Result; + _kickClient = new KickWsClient.KickWsClient(settings[BuiltIn.Keys.PusherEndpoint].Value!, + settings[BuiltIn.Keys.Proxy].Value, settings[BuiltIn.Keys.PusherReconnectTimeout].ToType()); + + _kickClient.OnStreamerIsLive += OnStreamerIsLive; + _kickClient.OnChatMessage += OnKickChatMessage; + _kickClient.OnWsReconnect += OnPusherWsReconnected; + _kickClient.OnPusherSubscriptionSucceeded += OnPusherSubscriptionSucceeded; + _kickClient.OnStopStreamBroadcast += OnStopStreamBroadcast; + + if (settings[BuiltIn.Keys.KickEnabled].ToBoolean()) + { + _kickClient.StartWsClient().Wait(_cancellationToken); + var pusherChannels = settings[BuiltIn.Keys.PusherChannels].ToList(); + foreach (var channel in pusherChannels) + { + _kickClient.SendPusherSubscribe(channel); + } + } + } + + private async Task WebsocketWatchdog() + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + while (await timer.WaitForNextTickAsync(_cancellationToken)) + { + if (_chatBot.InitialStartCooldown) continue; + var settings = await Helpers.GetMultipleValues([BuiltIn.Keys.KickEnabled]); + try + { + if (!_shuffle.IsConnected()) + { + _logger.Error("Shuffle died, recreating it"); + _shuffle.Dispose(); + _shuffle = null!; + BuildShuffle(); + } + + if (!_discord.IsConnected()) + { + _logger.Error("Discord died, recreating it"); + _discord.Dispose(); + _discord = null!; + BuildDiscord(); + } + + if (!_twitchDisabled && !_twitch.IsConnected()) + { + _logger.Error("Twitch died, recreating it"); + _twitch.Dispose(); + _twitch = null!; + BuildTwitch(); + } + + if (!_twitchChat.IsConnected()) + { + _logger.Error("Twitch chat died, recreating it"); + _twitchChat.Dispose(); + _twitchChat = null!; + BuildTwitchChat(); + } + + if (!_howlgg.IsConnected()) + { + _logger.Error("Howl.gg died, recreating it"); + _howlgg.Dispose(); + _howlgg = null!; + BuildHowlgg(); + } + + if (!_jackpot.IsConnected()) + { + _logger.Error("Jackpot died, recreating it"); + _jackpot.Dispose(); + _jackpot = null!; + BuildJackpot(); + } + + if (!_chipsgg.IsConnected()) + { + _logger.Error("Chips died, recreating it"); + _chipsgg.Dispose(); + _chipsgg = null!; + BuildChipsgg(); + } + + if (settings[BuiltIn.Keys.KickEnabled].ToBoolean() && !_kickClient.IsConnected()) + { + _logger.Error("Kick died, recreating it"); + _kickClient.Dispose(); + _kickClient = null!; + BuildKick(); + } + } + catch (Exception e) + { + _logger.Error("Watchdog shit itself while trying to do something, exception follows"); + _logger.Error(e); + } + } + } + + private async Task HowlggGetUserTimer() + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30)); + while (await timer.WaitForNextTickAsync(_cancellationToken)) + { + if (_howlgg == null || !_howlgg.IsConnected()) continue; + var bmjUserId = await Helpers.GetValue(BuiltIn.Keys.HowlggBmjUserId); + _howlgg.GetUserInfo(bmjUserId.Value!); + } + } + + private void OnRainbetBet(object sender, List bets) + { + var settings = Helpers + .GetMultipleValues([ + BuiltIn.Keys.RainbetBmjPublicId, BuiltIn.Keys.TwitchBossmanJackUsername, + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]).Result; + _logger.Trace("Rainbet bet has arrived"); + using var db = new ApplicationDbContext(); + foreach (var bet in bets.Where(b => b.User.PublicId == settings[BuiltIn.Keys.RainbetBmjPublicId].Value)) + //foreach (var bet in bets) + { + if (db.RainbetBets.Any(b => b.BetId == bet.Id)) + { + _logger.Trace($"Ignoring bet {bet.Id} as we've already logged it"); + continue; + } + + db.RainbetBets.Add(new RainbetBetsDbModel + { + PublicId = bet.User.PublicId, + RainbetUserId = bet.User.Id, + GameName = bet.Game.Name, + Value = bet.Value, + Payout = bet.Payout, + Multiplier = bet.Multiplier, + BetId = bet.Id, + UpdatedAt = bet.UpdatedAt, + BetSeenAt = DateTimeOffset.UtcNow + }); + _logger.Info("Added a Bossman Rainbet bet to the database"); + } + + db.SaveChanges(); + } + + 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; + } + if (TemporarilySuppressGambaMessages) + { + _logger.Info("Ignoring as TemporarilySuppressGambaMessages is true"); + 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; + _chatBot.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} πŸ’°πŸ’°"); + } + + private void OnHowlggBetHistory(object sender, HowlggBetHistoryResponseModel data) + { + _logger.Debug("Received bet history from Howl.gg"); + using var db = new ApplicationDbContext(); + foreach (var bet in data.History.Data) + { + // Slot feature buys have an unrealized value that means they show no profit until the feature finishes + // The feed will return the correct profit later hence updating the values + var existingBet = db.HowlggBets.FirstOrDefault(b => b.BetId == bet.Id); + if (existingBet != null) + { + _logger.Trace("Bet already exists in DB"); + if (existingBet.Bet == bet.Bet && existingBet.Profit == bet.Profit) continue; + _logger.Debug("Updating fields"); + existingBet.Bet = bet.Bet; + existingBet.Profit = bet.Profit; + db.SaveChanges(); + continue; + } + + db.HowlggBets.Add(new HowlggBetsDbModel + { + UserId = data.User.Id, + BetId = bet.Id, + GameId = bet.GameId, + Bet = bet.Bet, + Profit = bet.Profit, + Date = bet.Date, + Game = bet.Game + }); + _logger.Info("Added bet to DB"); + } + + db.SaveChanges(); + } + + private void DiscordOnChannelDeleted(object sender, DiscordChannelDeletionModel channel) + { + _logger.Info($"Received channel deletion event of type {channel.Type} with name {channel.Name}"); + if (channel.Type != DiscordChannelType.GuildText && channel.Type != DiscordChannelType.GuildVoice && + channel.Type != DiscordChannelType.GuildStageVoice) return; + var discordIcon = Helpers.GetValue(BuiltIn.Keys.DiscordIcon).Result; + var channelName = channel.Name ?? "Unknown name"; + _chatBot.SendChatMessage($"[img]{discordIcon.Value}[/img] Discord {channel.Type.Humanize()} channel '{channelName}' was deleted 🚨🚨", true); + } + + private void DiscordOnChannelCreated(object sender, DiscordChannelCreationModel channel) + { + _logger.Info($"Received channel creation event of type {channel.Type} with name {channel.Name}"); + if (channel.Type != DiscordChannelType.GuildText && channel.Type != DiscordChannelType.GuildVoice && + channel.Type != DiscordChannelType.GuildStageVoice) return; + var discordIcon = Helpers.GetValue(BuiltIn.Keys.DiscordIcon).Result; + var channelName = channel.Name ?? "Unknown name"; + _chatBot.SendChatMessage($"[img]{discordIcon.Value}[/img] New Discord {channel.Type.Humanize()} channel created: {channelName} 🚨🚨", true); + } + + private void BuildTwitchChat() + { + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.TwitchBossmanJackUsername, BuiltIn.Keys.Proxy]).Result; + _logger.Debug("Building Twitch Chat"); + if (settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value == null) + { + _logger.Info("Not building Twitch Chat client as BMJ's username is not configured"); + return; + } + + _bmjTwitchUsername = settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value!; + + _twitchChat = new TwitchChat($"#{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value}", settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); + _twitchChat.OnMessageReceived += TwitchChatOnMessageReceived; + _twitchChat.StartWsClient().Wait(_cancellationToken); + } + + private void TwitchChatOnMessageReceived(object sender, string nick, string target, string message) + { + if (nick != _bmjTwitchUsername) + { + return; + } + // Not caching this value as it won't harm it to have to look this up in even the worst spergout sesh + var twitchIcon = Helpers.GetValue(BuiltIn.Keys.TwitchIcon).Result.Value; + _chatBot.SendChatMessage($"[img]{twitchIcon}[/img] {nick}: {message.TrimEnd('\r')}", true); + } + + private void DiscordOnPresenceUpdated(object sender, DiscordPresenceUpdateModel presence) + { + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.DiscordBmjId, BuiltIn.Keys.DiscordIcon]).Result; + if (presence.User.Id != settings[BuiltIn.Keys.DiscordBmjId].Value) + { + return; + } + // if (_lastDiscordStatus == presence.Status) + // { + // _logger.Debug("Ignoring status update as it's the same as the last one"); + // return; + // } + // _lastDiscordStatus = presence.Status; + var clientStatus = presence.ClientStatus.Keys.Aggregate(string.Empty, (current, device) => current + $"{device} is {presence.ClientStatus[device]}; "); + _chatBot.SendChatMessage($"[img]{settings[BuiltIn.Keys.DiscordIcon].Value}[/img] BossmanJack has updated his Discord presence: {clientStatus}"); + } + + private void DiscordOnMessageReceived(object sender, DiscordMessageModel message) + { + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.DiscordBmjId, BuiltIn.Keys.DiscordIcon]).Result; + if (message.Author.Id != settings[BuiltIn.Keys.DiscordBmjId].Value) + { + return; + } + + if (message.Type == DiscordMessageType.StageStart) + { + _chatBot.SendChatMessage($"[img]{settings[BuiltIn.Keys.DiscordIcon].Value}[/img] BossmanJack just started a stage called {message.Content} 🚨🚨" + + $"[br]🚨🚨 BossmanJack is [b]LIVE[/b] on Discord! 🚨🚨", + true); + return; + } + if (message.Type == DiscordMessageType.StageEnd) + { + _chatBot.SendChatMessage($"[img]{settings[BuiltIn.Keys.DiscordIcon].Value}[/img] BossmanJack just ended a stage called {message.Content} :lossmanjack:", + true); + return; + } + + var result = $"[img]{settings[BuiltIn.Keys.DiscordIcon].Value}[/img] BossmanJack: {message.Content}"; + foreach (var attachment in message.Attachments ?? []) + { + result += $"[br]Attachment: {attachment.GetProperty("filename").GetString()} {attachment.GetProperty("url").GetString()}"; + } + + _chatBot.SendChatMessage(result, TemporarilyBypassGambaSeshForDiscord); + } + + private void DiscordOnInvalidCredentials(object sender, DiscordPacketReadModel packet) + { + _logger.Error("Credentials failed to validate."); + } + + private void ShuffleOnLatestBetUpdated(object sender, ShuffleLatestBetModel bet) + { + var settings = Helpers + .GetMultipleValues([ + BuiltIn.Keys.ShuffleBmjUsername, BuiltIn.Keys.TwitchBossmanJackUsername, + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]).Result; + _logger.Trace("Shuffle bet has arrived"); + if (bet.Username != settings[BuiltIn.Keys.ShuffleBmjUsername].Value) + { + return; + } + _logger.Info("ALERT BMJ IS BETTING"); + if (IsBmjLive) + { + _logger.Info("Ignoring as BMJ is live"); + return; + } + if (TemporarilySuppressGambaMessages) + { + _logger.Info("Ignoring as TemporarilySuppressGambaMessages is true"); + 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 (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 + _chatBot.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) + { + _logger.Info($"BossmanJack stream event came in. isLive => {isLive}"); + if (isLive) + { + var restream = Helpers.GetValue(BuiltIn.Keys.RestreamUrl).Result.Value; + _chatBot.SendChatMessage("BossmanJack just went live on Twitch! https://www.twitch.tv/thebossmanjack\r\n" + + $"Ad-free re-stream at {restream} courtesy of @Kees H"); + IsBmjLive = true; + return; + } + _chatBot.SendChatMessage("BossmanJack is no longer live! :lossmanjack:"); + IsBmjLive = false; + } + + private void OnChipsggRecentBet(object sender, ChipsggBetModel bet) + { + var settings = Helpers + .GetMultipleValues([ + BuiltIn.Keys.ChipsggBmjUsername, BuiltIn.Keys.TwitchBossmanJackUsername, + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]).Result; + _logger.Trace("Chips.gg bet has arrived"); + if (bet.Username != settings[BuiltIn.Keys.ChipsggBmjUsername].Value) + { + return; + } + _logger.Info("ALERT BMJ IS BETTING (on Chips.gg)"); + using var db = new ApplicationDbContext(); + db.ChipsggBets.Add(new ChipsggBetDbModel + { + Created = bet.Created, Updated = bet.Updated, UserId = bet.UserId, Username = bet.Username ?? "Unknown", Win = bet.Win, + Winnings = bet.Winnings, GameTitle = bet.GameTitle!, Amount = bet.Amount, Multiplier = bet.Multiplier, + Currency = bet.Currency!, CurrencyPrice = bet.CurrencyPrice, BetId = bet.BetId + }); + db.SaveChanges(); + if (IsBmjLive) + { + _logger.Info("Ignoring as BMJ is live"); + return; + } + + if (TemporarilySuppressGambaMessages) + { + _logger.Info("Ignoring as TemporarilySuppressGambaMessages is true"); + 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.Winnings < bet.Amount) payoutColor = settings[BuiltIn.Keys.KiwiFarmsRedColor].Value; + _chatBot.SendChatMessage( + $"🚨🚨 CHIPS BROS 🚨🚨 {bet.Username} just bet {bet.Amount:N} {bet.Currency!.ToUpper()} " + + $"({bet.Amount * bet.CurrencyPrice:C}) which paid out [color={payoutColor}]{bet.Winnings:N} {bet.Currency.ToUpper()} " + + $"({bet.Winnings * bet.CurrencyPrice:C})[/color] ({bet.Multiplier:N}x) on {bet.GameTitle} πŸ’°πŸ’°", + true); + } + + private void OnPusherWsReconnected(object sender, ReconnectionInfo reconnectionInfo) + { + _logger.Error($"Pusher reconnected due to {reconnectionInfo.Type}"); + var channels = Helpers.GetValue(BuiltIn.Keys.PusherChannels).Result.ToList(); + foreach (var channel in channels) + { + _logger.Info($"Rejoining {channel}"); + _kickClient.SendPusherSubscribe(channel); + } + } + + private void OnPusherSubscriptionSucceeded(object sender, PusherModels.BasePusherEventModel? e) + { + _logger.Info($"Pusher indicates subscription to {e?.Channel} was successful"); + } + + private void OnKickChatMessage(object sender, KickModels.ChatMessageEventModel? e) + { + if (e == null) return; + _logger.Debug($"BB Code Translation: {e.Content.TranslateKickEmotes()}"); + + if (e.Sender.Slug != "bossmanjack") return; + var kickIcon = Helpers.GetValue(BuiltIn.Keys.KickIcon).Result; + + _logger.Debug("Message from BossmanJack"); + _chatBot.SendChatMessage($"[img]{kickIcon.Value}[/img] BossmanJack: {e.Content.TranslateKickEmotes()}"); + } + + private void OnStreamerIsLive(object sender, KickModels.StreamerIsLiveEventModel? e) + { + if (e == null) return; + _chatBot.SendChatMessage($"Dirt Devils LFG! @Juhlonduss is live! {e.Livestream.SessionTitle} https://kick.com/dirtdevil-enjoyer", true); + } + + private void OnStopStreamBroadcast(object sender, KickModels.StopStreamBroadcastEventModel? e) + { + _chatBot.SendChatMessage("Dirt Devils felted. Stream is over. :lossmanjack:", true); + } +} \ No newline at end of file