diff --git a/KfChatDotNetBot/Commands/AdminCommands.cs b/KfChatDotNetBot/Commands/AdminCommands.cs index e28d5cf..31bdc89 100644 --- a/KfChatDotNetBot/Commands/AdminCommands.cs +++ b/KfChatDotNetBot/Commands/AdminCommands.cs @@ -88,6 +88,7 @@ public class NewKickChannelCommand : ICommand { var channels = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KickChannels)).JsonDeserialize>(); var channelId = Convert.ToInt32(arguments["channel_id"].Value); + channels ??= []; if (channels.Any(channel => channel.ChannelId == channelId)) { await botInstance.SendChatMessageAsync("Channel is already in the database", true); @@ -119,6 +120,7 @@ public class RemoveKickChannelCommand : ICommand public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { var channels = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KickChannels)).JsonDeserialize>(); + if (channels == null) throw new Exception("Caught a null when deserializing Kick channels"); var channelId = Convert.ToInt32(arguments["channel_id"].Value); var channel = channels.FirstOrDefault(ch => ch.ChannelId == channelId); if (channel == null) @@ -144,6 +146,11 @@ public class ReconnectKickCommand : ICommand public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { + if (botInstance.BotServices.KickClient == null) + { + await botInstance.SendChatMessageAsync("Kick client is not initialized", true); + return; + } botInstance.BotServices.KickClient.Disconnect(); await botInstance.SendChatMessageAsync("Disconnected from Kick. Client should reconnect shortly.", true); } @@ -365,6 +372,11 @@ public class SetAlmanacIntervalCommand : ICommand return; } await SettingsProvider.SetValueAsync(BuiltIn.Keys.BotAlmanacInterval, arguments["interval"].Value); + if (botInstance.BotServices.AlmanacShill == null) + { + await botInstance.SendChatMessageAsync("Value has been saved but almanac shill has not been initialized", true); + return; + } await botInstance.BotServices.AlmanacShill.StopShillTaskAsync(); botInstance.BotServices.AlmanacShill.StartShillTask(); await botInstance.SendChatMessageAsync($"@{message.Author.Username}, updated interval and restarted the shill task", true); @@ -383,6 +395,11 @@ public class StopAlmanacCommand : ICommand public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { + if (botInstance.BotServices.AlmanacShill == null) + { + await botInstance.SendChatMessageAsync("AlmanacShill is null", true); + return; + } if (!botInstance.BotServices.AlmanacShill.IsShillTaskRunning()) { await botInstance.SendChatMessageAsync("Looks like the task isn't even running", true); @@ -407,6 +424,11 @@ public class StartAlmanacCommand : ICommand public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { + if (botInstance.BotServices.AlmanacShill == null) + { + await botInstance.SendChatMessageAsync("AlmanacShill is null", true); + return; + } if (botInstance.BotServices.AlmanacShill.IsShillTaskRunning()) { await botInstance.SendChatMessageAsync("Looks like the task is already running", true); diff --git a/KfChatDotNetBot/Commands/TestCommands.cs b/KfChatDotNetBot/Commands/TestCommands.cs index a4e12a5..cdcca24 100644 --- a/KfChatDotNetBot/Commands/TestCommands.cs +++ b/KfChatDotNetBot/Commands/TestCommands.cs @@ -79,7 +79,7 @@ public class ExceptionTestCommand : ICommand public UserRight RequiredRight => UserRight.Admin; // Increased timeout as it has to wait for Sneedchat to echo the message and that can be slow sometimes public TimeSpan Timeout => TimeSpan.FromSeconds(15); - public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) + public Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { throw new Exception("Caused by the test exception command"); } diff --git a/KfChatDotNetBot/Commands/UtilityCommands.cs b/KfChatDotNetBot/Commands/UtilityCommands.cs index 798d49a..e6d1a34 100644 --- a/KfChatDotNetBot/Commands/UtilityCommands.cs +++ b/KfChatDotNetBot/Commands/UtilityCommands.cs @@ -64,8 +64,14 @@ public class GetVersionCommand : ICommand public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { - var version = Assembly.GetEntryAssembly() - .GetCustomAttribute().InformationalVersion; + var version = Assembly.GetEntryAssembly()? + .GetCustomAttribute()?.InformationalVersion; + if (version == null) + { + await botInstance.SendChatMessageAsync($"Caught a null when trying to retrieve the bot's assembly version.", + true); + return; + } await botInstance.SendChatMessageAsync($"Bot compiled against {version.Split('+')[1]}", true); } } \ No newline at end of file diff --git a/KfChatDotNetBot/Models/ChipsggModels.cs b/KfChatDotNetBot/Models/ChipsggModels.cs index 557f309..3405585 100644 --- a/KfChatDotNetBot/Models/ChipsggModels.cs +++ b/KfChatDotNetBot/Models/ChipsggModels.cs @@ -5,7 +5,7 @@ public class ChipsggBetModel public DateTimeOffset Created { get; set; } // Can actually get the duration of a game from this public DateTimeOffset Updated { get; set; } - public string UserId { get; set; } + public string? UserId { get; set; } // Sometimes null for no discernible reason public string? Username { get; set; } // Win of any amount even if it's less than a 1x multi @@ -16,7 +16,7 @@ public class ChipsggBetModel public float Multiplier { get; set; } public string? Currency { get; set; } public float CurrencyPrice { get; set; } - public string BetId { get; set; } + public string? BetId { get; set; } } public class ChipsggCurrencyModel diff --git a/KfChatDotNetBot/Services/BetBolt.cs b/KfChatDotNetBot/Services/BetBolt.cs index da45768..3b6c799 100644 --- a/KfChatDotNetBot/Services/BetBolt.cs +++ b/KfChatDotNetBot/Services/BetBolt.cs @@ -12,22 +12,22 @@ namespace KfChatDotNetBot.Services; public class BetBolt : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + 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; - private IEnumerable? _cookies = null; - private string? _userAgent = null; - public BetBolt(string? proxy = null, CancellationToken? cancellationToken = null) + public event OnBetBoltBetEventHandler? OnBetBoltBet; + public event OnWsDisconnectionEventHandler? OnWsDisconnection; + private CancellationToken _cancellationToken; + private IEnumerable? _cookies; + private string? _userAgent; + public BetBolt(string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; _logger.Info("Clash.gg WebSocket client created"); } public async Task StartWsClient() @@ -86,7 +86,7 @@ public class BetBolt : IDisposable if (reconnectionInfo.Type == ReconnectionType.Initial) { _logger.Info("Sending subscribe payload to BetBolt"); - _wsClient.Send("{\"topic\":\"system/EN\",\"action\":\"subscribe\"}"); + _wsClient?.Send("{\"topic\":\"system/EN\",\"action\":\"subscribe\"}"); } } diff --git a/KfChatDotNetBot/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs index 46fa6ea..90c58c1 100644 --- a/KfChatDotNetBot/Services/BotServices.cs +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -19,29 +19,28 @@ public class BotServices private readonly CancellationToken _cancellationToken; private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - internal KickWsClient.KickWsClient KickClient; - private Twitch _twitch; - private Shuffle _shuffle; - private DiscordService _discord; - private TwitchChat _twitchChat; - private Jackpot _jackpot; - private Howlgg _howlgg; - private RainbetWs _rainbet; - private Chipsgg _chipsgg; - private Clashgg _clashgg; - private BetBolt _betBolt; - private Yeet _yeet; - public AlmanacShill AlmanacShill; + internal KickWsClient.KickWsClient? KickClient; + private Twitch? _twitch; + private Shuffle? _shuffle; + private DiscordService? _discord; + private TwitchChat? _twitchChat; + private Jackpot? _jackpot; + private Howlgg? _howlgg; + private RainbetWs? _rainbet; + private Chipsgg? _chipsgg; + private Clashgg? _clashgg; + private BetBolt? _betBolt; + private Yeet? _yeet; + public AlmanacShill? AlmanacShill; private Task? _websocketWatchdog; private Task? _howlggGetUserTimer; - private string _bmjTwitchUsername; - private bool _twitchDisabled = false; - private string? _lastDiscordStatus; - internal bool IsBmjLive = false; - private bool _isBmjLiveSynced = false; - internal bool IsChrisDjLive = false; + private string? _bmjTwitchUsername; + private bool _twitchDisabled; + internal bool IsBmjLive; + private bool _isBmjLiveSynced; + internal bool IsChrisDjLive; private Dictionary _yeetBets = new(); // lol @@ -142,7 +141,7 @@ public class BotServices _logger.Debug("Chips.gg is disabled"); return; } - _chipsgg = new Chipsgg(settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); + _chipsgg = new Chipsgg(settings[BuiltIn.Keys.Proxy].Value); _chipsgg.OnChipsggRecentBet += OnChipsggRecentBet; await _chipsgg.StartWsClient(); _logger.Info("Built Chips.gg Websocket connection"); @@ -302,7 +301,7 @@ public class BotServices ]); try { - if (!_shuffle.IsConnected()) + if (_shuffle != null && !_shuffle.IsConnected()) { _logger.Error("Shuffle died, recreating it"); _shuffle.Dispose(); @@ -310,7 +309,7 @@ public class BotServices await BuildShuffle(); } - if (!_discord.IsConnected()) + if (_discord != null && !_discord.IsConnected()) { _logger.Error("Discord died, recreating it"); _discord.Dispose(); @@ -318,7 +317,7 @@ public class BotServices await BuildDiscord(); } - if (!_twitchDisabled && !_twitch.IsConnected()) + if (!_twitchDisabled && _twitch != null && !_twitch.IsConnected()) { _logger.Error("Twitch died, recreating it"); _twitch.Dispose(); @@ -326,7 +325,7 @@ public class BotServices await BuildTwitch(); } - if (!_twitchChat.IsConnected()) + if (_twitchChat != null && !_twitchChat.IsConnected()) { _logger.Error("Twitch chat died, recreating it"); _twitchChat.Dispose(); @@ -334,7 +333,7 @@ public class BotServices await BuildTwitchChat(); } - if (settings[BuiltIn.Keys.HowlggEnabled].ToBoolean() && !_howlgg.IsConnected()) + if (settings[BuiltIn.Keys.HowlggEnabled].ToBoolean() && _howlgg != null && !_howlgg.IsConnected()) { _logger.Error("Howl.gg died, recreating it"); _howlgg.Dispose(); @@ -342,7 +341,7 @@ public class BotServices await BuildHowlgg(); } - if (!_jackpot.IsConnected()) + if (_jackpot != null && !_jackpot.IsConnected()) { _logger.Error("Jackpot died, recreating it"); _jackpot.Dispose(); @@ -350,7 +349,7 @@ public class BotServices await BuildJackpot(); } - if (settings[BuiltIn.Keys.ChipsggEnabled].ToBoolean() && !_chipsgg.IsConnected()) + if (settings[BuiltIn.Keys.ChipsggEnabled].ToBoolean() && _chipsgg != null && !_chipsgg.IsConnected()) { _logger.Error("Chips died, recreating it"); _chipsgg.Dispose(); @@ -358,7 +357,7 @@ public class BotServices await BuildChipsgg(); } - if (settings[BuiltIn.Keys.KickEnabled].ToBoolean() && !KickClient.IsConnected()) + if (settings[BuiltIn.Keys.KickEnabled].ToBoolean() && KickClient != null && !KickClient.IsConnected()) { _logger.Error("Kick died, recreating it"); KickClient.Dispose(); @@ -366,7 +365,7 @@ public class BotServices await BuildKick(); } - if (settings[BuiltIn.Keys.ClashggEnabled].ToBoolean() && !_clashgg.IsConnected()) + if (settings[BuiltIn.Keys.ClashggEnabled].ToBoolean() && _clashgg != null && !_clashgg.IsConnected()) { _logger.Error("Clash.gg died, recreating it"); _clashgg.Dispose(); @@ -374,7 +373,7 @@ public class BotServices await BuildClashgg(); } - if (settings[BuiltIn.Keys.BetBoltEnabled].ToBoolean() && !_betBolt.IsConnected()) + if (settings[BuiltIn.Keys.BetBoltEnabled].ToBoolean() && _betBolt != null && !_betBolt.IsConnected()) { _logger.Error("BetBolt died, recreating it"); _betBolt.Dispose(); @@ -382,7 +381,7 @@ public class BotServices await BuildBetBolt(); } - if (settings[BuiltIn.Keys.YeetEnabled].ToBoolean() && !_yeet.IsConnected()) + if (settings[BuiltIn.Keys.YeetEnabled].ToBoolean() && _yeet != null && !_yeet.IsConnected()) { _logger.Error("Yeet died, recreating it"); _yeet.Dispose(); @@ -390,7 +389,7 @@ public class BotServices await BuildYeet(); } - if (settings[BuiltIn.Keys.RainbetEnabled].ToBoolean() && !_rainbet.IsConnected()) + if (settings[BuiltIn.Keys.RainbetEnabled].ToBoolean() && _rainbet != null && !_rainbet.IsConnected()) { _logger.Error("Rainbet died, recreating it"); _rainbet.Dispose(); @@ -549,7 +548,6 @@ public class BotServices } _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; var msg = _chatBot.SendChatMessage($"🚨🚨 JEET BETTING 🚨🚨 {bet.Username} just bet {bet.BetAmount:C} worth of {bet.CurrencyCode} on {bet.GameName} 💩💩", true); _yeetBets.Add(bet.BetIdentifier, new SeenYeetBet {Bet = bet, Message = msg}); @@ -809,7 +807,7 @@ public class BotServices BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor ]).Result; _logger.Trace("Chips.gg bet has arrived"); - if (!settings[BuiltIn.Keys.ChipsggBmjUserIds].JsonDeserialize>()!.Contains(bet.UserId)) + if (!settings[BuiltIn.Keys.ChipsggBmjUserIds].JsonDeserialize>()!.Contains(bet.UserId ?? "0")) { return; } @@ -817,9 +815,9 @@ public class BotServices 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, + Created = bet.Created, Updated = bet.Updated, UserId = bet.UserId ?? "0", 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 + Currency = bet.Currency!, CurrencyPrice = bet.CurrencyPrice, BetId = bet.BetId ?? "0" }); db.SaveChanges(); if (CheckBmjIsLive(settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value ?? "usernamenotset").Result) return; @@ -840,7 +838,7 @@ public class BotServices if (kickChannels == null) return; foreach (var channel in kickChannels) { - KickClient.SendPusherSubscribe($"channel.{channel.ChannelId}"); + KickClient?.SendPusherSubscribe($"channel.{channel.ChannelId}"); } } @@ -951,6 +949,11 @@ public class BotServices // 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) { + if (_twitch == null) + { + _logger.Error("Twitch client has not been built!"); + throw new Exception("Twitch client not initialized"); + } IsBmjLive = await _twitch.IsStreamLive(bmjUsername); _isBmjLiveSynced = true; } diff --git a/KfChatDotNetBot/Services/Chipsgg.cs b/KfChatDotNetBot/Services/Chipsgg.cs index 2b96397..2cf8aac 100644 --- a/KfChatDotNetBot/Services/Chipsgg.cs +++ b/KfChatDotNetBot/Services/Chipsgg.cs @@ -10,22 +10,20 @@ namespace KfChatDotNetBot.Services; public class Chipsgg : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + private WebsocketClient? _wsClient; private Uri _wsUri = new("wss://api.chips.gg/prod/socket"); // Chips doesn't have a heartbeat packet private int _reconnectTimeout = 30; private string? _proxy; public delegate void OnChipsggRecentBetEventHandler(object sender, ChipsggBetModel bet); public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e); - public event OnChipsggRecentBetEventHandler OnChipsggRecentBet; - private CancellationToken _cancellationToken = CancellationToken.None; + public event OnChipsggRecentBetEventHandler? OnChipsggRecentBet; private Dictionary _currencies = new(); - private bool _authenticated = false; + private bool _authenticated; - public Chipsgg(string? proxy = null, CancellationToken? cancellationToken = null) + public Chipsgg(string? proxy = null) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; _logger.Info("Chipsgg WebSocket client created"); } @@ -83,7 +81,7 @@ public class Chipsgg : IDisposable if (reconnectionInfo.Type == ReconnectionType.Initial) { _logger.Info("Sending auth payload to Chips.gg"); - _wsClient.Send("[\"auth\",1,\"token\",[]]"); + _wsClient?.Send("[\"auth\",1,\"token\",[]]"); } } @@ -121,7 +119,7 @@ public class Chipsgg : IDisposable { var guid = firstElement[2].GetString(); _logger.Info("Received auth packet, sending back GUID auth with " + guid); - _wsClient.Send("[\"auth\",2,\"authenticate\",[\"" + guid+ "\"]]"); + _wsClient?.Send("[\"auth\",2,\"authenticate\",[\"" + guid+ "\"]]"); _authenticated = true; return; } @@ -130,7 +128,7 @@ public class Chipsgg : IDisposable { _logger.Info("Chips.gg responded to our auth with: " + firstElement[2].GetString()); _logger.Info("Sending Chips.gg recent bets subscription packet"); - _wsClient.Send("[\"stats\",12,\"on\",[{\"game\":\"bets\",\"type\":\"recentBets\"}]]"); + _wsClient?.Send("[\"stats\",12,\"on\",[{\"game\":\"bets\",\"type\":\"recentBets\"}]]"); return; } @@ -157,7 +155,7 @@ public class Chipsgg : IDisposable { _logger.Info("Received currency payload without getting the auth response. Retarded inconsistent Chips.gg behavior again, sending a made-up GUID"); var guid = Guid.NewGuid().ToString(); - _wsClient.Send("[\"auth\",2,\"authenticate\",[\"" + guid+ "\"]]"); + _wsClient?.Send("[\"auth\",2,\"authenticate\",[\"" + guid+ "\"]]"); } _logger.Info("Received initial currency payload as the path array was empty"); var currencyData = dataElement[1].Deserialize>(); @@ -172,7 +170,8 @@ public class Chipsgg : IDisposable { _logger.Info("Ignoring already defined currency"); continue; - }; + } + float? price = null; // Where a price is not set, the element is simply missing if (currencies[currency].TryGetProperty("price", out var priceElement)) @@ -206,7 +205,7 @@ public class Chipsgg : IDisposable { _logger.Debug("Ignoring packet as it contains koth"); return; - }; + } var currency = innerDataPath[1]; if (_currencies.TryGetValue(currency, out var updatedCurrency)) { @@ -227,7 +226,7 @@ public class Chipsgg : IDisposable { _logger.Info("Ignoring replay of recent bets"); return; - }; + } } // Currency data may not be known until after so hold it here til we're done parsing var amount = string.Empty; @@ -323,7 +322,7 @@ public class Chipsgg : IDisposable { _logger.Debug("Currency or GameTitle was null, ignoring"); return; - }; + } if (!_currencies.TryGetValue(bet.Currency, out var currencyData)) { throw new Exception($"Unknown currency {bet.Currency}"); @@ -352,7 +351,7 @@ public class Chipsgg : IDisposable public void Dispose() { - _wsClient.Dispose(); + _wsClient?.Dispose(); GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/KfChatDotNetBot/Services/Clashgg.cs b/KfChatDotNetBot/Services/Clashgg.cs index 13ff184..0344cd9 100644 --- a/KfChatDotNetBot/Services/Clashgg.cs +++ b/KfChatDotNetBot/Services/Clashgg.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Net.Http.Json; using System.Net.WebSockets; using System.Text.Json; using KfChatDotNetBot.Models; @@ -11,28 +10,28 @@ namespace KfChatDotNetBot.Services; public class Clashgg : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + private WebsocketClient? _wsClient; private Uri _wsUri = new("wss://ws.clash.gg/"); // Ping interval is 30 seconds private int _reconnectTimeout = 60; private string? _proxy; public delegate void OnClashBetEventHandler(object sender, ClashggBetModel data, JsonElement jsonElement); public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e); - public event OnClashBetEventHandler OnClashBet; - public event OnWsDisconnectionEventHandler OnWsDisconnection; - private CancellationToken _cancellationToken = CancellationToken.None; + public event OnClashBetEventHandler? OnClashBet; + public event OnWsDisconnectionEventHandler? OnWsDisconnection; + private CancellationToken _cancellationToken; private CancellationTokenSource _pingCts = new(); private Task? _heartbeatTask; private TimeSpan _heartbeatInterval = TimeSpan.FromSeconds(60); - private bool _isOnline = false; + private bool _isOnline; private DateTime _lastBet = DateTime.Now; // How long we can go without any gambling activity until we go 'fuck it' and reconnect private TimeSpan _lastBetTolerance = TimeSpan.FromMinutes(5); - public Clashgg(string? proxy = null, CancellationToken? cancellationToken = null) + public Clashgg(string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; _logger.Info("Clash.gg WebSocket client created"); } @@ -128,9 +127,9 @@ public class Clashgg : IDisposable if (packet[0].GetString() == "online" && !_isOnline) { _logger.Info("Received online packet from Clash.gg. Subscribing to Plinko, Mines and Keno"); - _wsClient.Send("[\"subscribe\",\"plinko\"]"); - _wsClient.Send("[\"subscribe\",\"mines\"]"); - _wsClient.Send("[\"subscribe\",\"keno\"]"); + _wsClient?.Send("[\"subscribe\",\"plinko\"]"); + _wsClient?.Send("[\"subscribe\",\"mines\"]"); + _wsClient?.Send("[\"subscribe\",\"keno\"]"); _isOnline = true; _heartbeatTask = Task.Run(HeartbeatTimer, _cancellationToken); return; @@ -221,7 +220,7 @@ public class Clashgg : IDisposable public void Dispose() { - _wsClient.Dispose(); + _wsClient?.Dispose(); _pingCts.Cancel(); _pingCts.Dispose(); _heartbeatTask?.Dispose(); diff --git a/KfChatDotNetBot/Services/Discord.cs b/KfChatDotNetBot/Services/Discord.cs index be514a7..5497adf 100644 --- a/KfChatDotNetBot/Services/Discord.cs +++ b/KfChatDotNetBot/Services/Discord.cs @@ -11,7 +11,7 @@ namespace KfChatDotNetBot.Services; public class DiscordService : IDisposable { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + private WebsocketClient? _wsClient; private readonly Uri _wsUri = new Uri("wss://gateway.discord.gg/?encoding=json&v=9"); // Not sure what a good value for this would be private const int ReconnectTimeout = 60; @@ -29,24 +29,24 @@ public class DiscordService : IDisposable public delegate void ConversationSummaryUpdateEventHandler(object sender, DiscordConversationSummaryUpdateModel summary, string guildId); - public event MessageReceivedEventHandler OnMessageReceived; - public event PresenceUpdateEventHandler OnPresenceUpdated; - public event WsDisconnectionEventHandler OnWsDisconnection; - public event InvalidCredentialsEventHandler OnInvalidCredentials; - public event ChannelCreatedEventHandler OnChannelCreated; - public event ChannelDeletedEventHandler OnChannelDeleted; - public event ConversationSummaryUpdateEventHandler OnConversationSummaryUpdate; + public event MessageReceivedEventHandler? OnMessageReceived; + public event PresenceUpdateEventHandler? OnPresenceUpdated; + public event WsDisconnectionEventHandler? OnWsDisconnection; + public event InvalidCredentialsEventHandler? OnInvalidCredentials; + public event ChannelCreatedEventHandler? OnChannelCreated; + public event ChannelDeletedEventHandler? OnChannelDeleted; + public event ConversationSummaryUpdateEventHandler? OnConversationSummaryUpdate; - private readonly CancellationToken _cancellationToken = CancellationToken.None; + private readonly CancellationToken _cancellationToken; private readonly CancellationTokenSource _pingCts = new(); private Task? _heartbeatTask; // Discord tells us the heartbeat interval to use in the op 10 response so this is just a placeholder private TimeSpan _heartbeatInterval = TimeSpan.FromSeconds(40); - public DiscordService(string authorization, string? proxy = null, CancellationToken? cancellationToken = null) + public DiscordService(string authorization, string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; _authorization = authorization; _logger.Info("Discord Service created"); } @@ -159,7 +159,7 @@ public class DiscordService : IDisposable "\"client_build_number\":306208,\"client_event_source\":null,\"design_id\":0},\"presence\":{\"status\":\"unknown\"," + "\"since\":0,\"activities\":[],\"afk\":false},\"compress\":false,\"client_state\":{\"guild_versions\":{}}}}"; _logger.Debug(initPayload); - _wsClient.SendInstant(initPayload).Wait(_cancellationToken); + _wsClient?.SendInstant(initPayload).Wait(_cancellationToken); _heartbeatInterval = TimeSpan.FromMilliseconds(packet.Data.GetProperty("heartbeat_interval").GetInt32()); if (_heartbeatTask != null) return; @@ -235,7 +235,7 @@ public class DiscordService : IDisposable public void Dispose() { _logger.Info("Disposing of the Discord service"); - _wsClient.Dispose(); + _wsClient?.Dispose(); _pingCts.Cancel(); _pingCts.Dispose(); _heartbeatTask?.Dispose(); diff --git a/KfChatDotNetBot/Services/Howlgg.cs b/KfChatDotNetBot/Services/Howlgg.cs index 84a8071..2d7339a 100644 --- a/KfChatDotNetBot/Services/Howlgg.cs +++ b/KfChatDotNetBot/Services/Howlgg.cs @@ -10,25 +10,25 @@ namespace KfChatDotNetBot.Services; public class Howlgg : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + private WebsocketClient? _wsClient; private Uri _wsUri = new("wss://howl.gg/socket.io/?EIO=3&transport=websocket"); // Howl will send its own timeout but seems it's always 30 seconds private int _reconnectTimeout = 30; private string? _proxy; public delegate void OnHowlggBetHistoryResponse(object sender, HowlggBetHistoryResponseModel data); public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e); - public event OnHowlggBetHistoryResponse OnHowlggBetHistory; - public event OnWsDisconnectionEventHandler OnWsDisconnection; - private CancellationToken _cancellationToken = CancellationToken.None; + public event OnHowlggBetHistoryResponse? OnHowlggBetHistory; + public event OnWsDisconnectionEventHandler? OnWsDisconnection; + private CancellationToken _cancellationToken; private CancellationTokenSource _pingCts = new(); private Task? _heartbeatTask; // Howl.gg tells us the heartbeat interval to use in the initial payload so this is just a placeholder private TimeSpan _heartbeatInterval = TimeSpan.FromSeconds(40); - public Howlgg(string? proxy = null, CancellationToken? cancellationToken = null) + public Howlgg(string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; _logger.Info("Howlgg WebSocket client created"); } @@ -77,7 +77,7 @@ public class Howlgg : IDisposable { var packet = "42/main,0[\"getUserInfo\",{\"userOrSteamId\":\"" + userId + "\",\"interval\":\"lifetime\"}]"; _logger.Debug($"Sending packet: {packet}"); - _wsClient.Send(packet); + _wsClient?.Send(packet); } private async Task HeartbeatTimer() @@ -141,7 +141,7 @@ public class Howlgg : IDisposable if (message.Text == "40") { _logger.Trace("Ready to subscribe, sending main subscription"); - _wsClient.Send("40/main,"); + _wsClient?.Send("40/main,"); // To indicate successful subscription it echoes back the channel to you return; } @@ -152,7 +152,6 @@ public class Howlgg : IDisposable var jsonPayload = message.Text.Replace("43/main,0[null,", string.Empty).TrimEnd(']'); var data = JsonSerializer.Deserialize(jsonPayload); if (data != null) OnHowlggBetHistory?.Invoke(this, data); - return; } } catch (Exception e) @@ -167,7 +166,7 @@ public class Howlgg : IDisposable public void Dispose() { - _wsClient.Dispose(); + _wsClient?.Dispose(); _pingCts.Cancel(); _pingCts.Dispose(); _heartbeatTask?.Dispose(); diff --git a/KfChatDotNetBot/Services/Jackpot.cs b/KfChatDotNetBot/Services/Jackpot.cs index 24c2aaf..4519a63 100644 --- a/KfChatDotNetBot/Services/Jackpot.cs +++ b/KfChatDotNetBot/Services/Jackpot.cs @@ -11,25 +11,25 @@ namespace KfChatDotNetBot.Services; public class Jackpot : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + 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; + public event OnJackpotBetEventHandler? OnJackpotBet; + public event OnWsDisconnectionEventHandler? OnWsDisconnection; + private CancellationToken _cancellationToken; 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) + public Jackpot(string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; _logger.Info("Jackpot WebSocket client created"); } @@ -103,7 +103,7 @@ public class Jackpot : IDisposable if (reconnectionInfo.Type == ReconnectionType.Initial) { _logger.Debug("Sending initial payload"); - _wsClient.Send("{\"id\":\"lfgkenokasinoinit\",\"type\":\"connection_init\"}"); + _wsClient?.Send("{\"id\":\"lfgkenokasinoinit\",\"type\":\"connection_init\"}"); } } @@ -129,7 +129,7 @@ public class Jackpot : IDisposable if (packet.Type == "connection_ack") { _logger.Debug("Received ack from Jackpot. Sending subscription"); - _wsClient.Send( + _wsClient?.Send( "{\"id\":\"lfgkenokasinoallbets\",\"type\":\"subscribe\",\"payload\":{\"feed\":\"all_bets\"}}\n"); _logger.Debug("Setting up heartbeat timer"); if (_heartbeatTask != null) return; @@ -178,7 +178,7 @@ public class Jackpot : IDisposable public void Dispose() { - _wsClient.Dispose(); + _wsClient?.Dispose(); _pingCts.Cancel(); _pingCts.Dispose(); _heartbeatTask?.Dispose(); diff --git a/KfChatDotNetBot/Services/KiwiFlare.cs b/KfChatDotNetBot/Services/KiwiFlare.cs index 11c70e9..5d62a30 100644 --- a/KfChatDotNetBot/Services/KiwiFlare.cs +++ b/KfChatDotNetBot/Services/KiwiFlare.cs @@ -75,7 +75,7 @@ public class KiwiFlare(string kfDomain, string? proxy = null, CancellationToken? return true; } - private async Task ChallengeWorker(KiwiFlareChallengeModel challenge) + private Task ChallengeWorker(KiwiFlareChallengeModel challenge) { var nonce = _random.NextInt64(); while (true) @@ -84,11 +84,11 @@ public class KiwiFlare(string kfDomain, string? proxy = null, CancellationToken? var input = Encoding.UTF8.GetBytes($"{challenge.Salt}{nonce}"); if (!TestHash(SHA256.HashData(input), challenge.Difficulty)) continue; _logger.Debug($"Hash passed the test, nonce: {nonce}"); - return new KiwiFlareChallengeSolutionModel + return Task.FromResult(new KiwiFlareChallengeSolutionModel { Nonce = nonce, Salt = challenge.Salt - }; + }); } } diff --git a/KfChatDotNetBot/Services/Rainbet.cs b/KfChatDotNetBot/Services/Rainbet.cs index 1f8269e..5e5532c 100644 --- a/KfChatDotNetBot/Services/Rainbet.cs +++ b/KfChatDotNetBot/Services/Rainbet.cs @@ -11,8 +11,8 @@ public class Rainbet : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); public delegate void OnRainbetBetEventHandler(object sender, List bets); - public event OnRainbetBetEventHandler OnRainbetBet; - private CancellationToken _cancellationToken = CancellationToken.None; + public event OnRainbetBetEventHandler? OnRainbetBet; + private CancellationToken _cancellationToken; private CancellationTokenSource _gameHistoryCts = new(); private Task? _gameHistoryTask; private TimeSpan _gameHistoryInterval = TimeSpan.FromSeconds(60); @@ -20,9 +20,9 @@ public class Rainbet : IDisposable private string? _userAgent = null; public DateTimeOffset LastSuccessfulRefresh = DateTimeOffset.MinValue; - public Rainbet(CancellationToken? cancellationToken = null) + public Rainbet(CancellationToken cancellationToken = default) { - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; _logger.Info("Rainbet client created"); } @@ -118,6 +118,7 @@ public class Rainbet : IDisposable throw new Exception("Cloudflare challenged"); } var bets = await response.Content.ReadFromJsonAsync>(cancellationToken: _cancellationToken); + if (bets == null) throw new Exception("Caught a null when deserializing Rainbet bet history"); return bets; } diff --git a/KfChatDotNetBot/Services/RainbetWs.cs b/KfChatDotNetBot/Services/RainbetWs.cs index 39d11b4..e3cf2a0 100644 --- a/KfChatDotNetBot/Services/RainbetWs.cs +++ b/KfChatDotNetBot/Services/RainbetWs.cs @@ -18,16 +18,16 @@ public class RainbetWs : IDisposable private string? _proxy; public delegate void OnRainbetBetEventHandler(object sender, RainbetWsBetModel bet); public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e); - public event OnRainbetBetEventHandler OnRainbetBet; - public event OnWsDisconnectionEventHandler OnWsDisconnection; - private CancellationToken _cancellationToken = CancellationToken.None; - private IEnumerable? _cookies = null; - private string? _userAgent = null; + public event OnRainbetBetEventHandler? OnRainbetBet; + public event OnWsDisconnectionEventHandler? OnWsDisconnection; + private CancellationToken _cancellationToken; + private IEnumerable? _cookies; + private string? _userAgent; - public RainbetWs(string? proxy = null, CancellationToken? cancellationToken = null) + public RainbetWs(string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; _logger.Info("Rainbet WebSocket client created"); } @@ -101,14 +101,14 @@ public class RainbetWs : IDisposable { // 0{"sid":"Pf940zhaAqb6BHBiSgLa","upgrades":[],"pingInterval":25000,"pingTimeout":20000,"maxPayload":100000000} _logger.Info("Received initial connection message from Rainbet, sending subscribe"); - _wsClient.Send("40/game-history,{}"); + _wsClient?.Send("40/game-history,{}"); return; } var packetType = message.Text.Split('/')[0]; if (packetType == "2") { _logger.Info("Received ping from Rainbet, replying with pong"); - _wsClient.Send("3"); + _wsClient?.Send("3"); return; } @@ -124,14 +124,14 @@ public class RainbetWs : IDisposable { var data = JsonSerializer.Deserialize>(message.Text.Replace("42/game-history,", string.Empty)); + if (data == null) throw new Exception("Caught a null when deserializing game history"); if (data[0].GetString() == "new-history") { - OnRainbetBet?.Invoke(this, data[1].Deserialize()); + OnRainbetBet?.Invoke(this, data[1].Deserialize()!); return; } _logger.Info($"Event {data[0].GetString()} from Rainbet was not handled"); _logger.Info(message.Text); - return; } } catch (Exception e) diff --git a/KfChatDotNetBot/Services/Shuffle.cs b/KfChatDotNetBot/Services/Shuffle.cs index e569619..998fb2b 100644 --- a/KfChatDotNetBot/Services/Shuffle.cs +++ b/KfChatDotNetBot/Services/Shuffle.cs @@ -11,22 +11,22 @@ namespace KfChatDotNetBot.Services; public class Shuffle : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + private WebsocketClient? _wsClient; private Uri _wsUri = new("wss://shuffle.com/main-api/bp-subscription/subscription/graphql"); private int _reconnectTimeout = 60; private string? _proxy; public delegate void OnLatestBetUpdatedEventHandler(object sender, ShuffleLatestBetModel bet); public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e); - public event OnLatestBetUpdatedEventHandler OnLatestBetUpdated; - public event OnWsDisconnectionEventHandler OnWsDisconnection; - private CancellationToken _cancellationToken = CancellationToken.None; + public event OnLatestBetUpdatedEventHandler? OnLatestBetUpdated; + public event OnWsDisconnectionEventHandler? OnWsDisconnection; + private CancellationToken _cancellationToken; private CancellationTokenSource _pingCts = new(); private Task _pingTask; - public Shuffle(string? proxy = null, CancellationToken? cancellationToken = null) + public Shuffle(string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; // Moved it up here as I'm concerned about the possibility of reconnections creating multiple ping tasks _pingTask = PeriodicPing(); } @@ -103,7 +103,7 @@ public class Shuffle : IDisposable var initPayload = "{\"type\":\"connection_init\",\"payload\":{\"x-correlation-id\":\"pdvlnd9tej-di27abvq19-1.30.2-1i0nef1m7-g::anon\",\"authorization\":\"\"}}"; _logger.Debug(initPayload); - _wsClient.Send(initPayload); + _wsClient?.Send(initPayload); } private void WsMessageReceived(ResponseMessage message) @@ -128,7 +128,7 @@ public class Shuffle : IDisposable var payload = "{\"id\":\"" + Guid.NewGuid() + "\",\"type\":\"subscribe\",\"payload\":{\"variables\":{},\"extensions\":{},\"operationName\":\"LatestBetUpdated\",\"query\":\"subscription LatestBetUpdated {\\n latestBetUpdated {\\n ...BetActivityFields\\n __typename\\n }\\n}\\n\\nfragment BetActivityFields on BetActivityPayload {\\n id\\n username\\n vipLevel\\n currency\\n amount\\n payout\\n multiplier\\n gameName\\n gameCategory\\n gameSlug\\n __typename\\n}\"}}"; _logger.Debug(payload); - _wsClient.SendInstant(payload).Wait(_cancellationToken); + _wsClient?.SendInstant(payload).Wait(_cancellationToken); return; } @@ -206,7 +206,7 @@ public class Shuffle : IDisposable public void Dispose() { - _wsClient.Dispose(); + _wsClient?.Dispose(); // Rare bug but has happened at least once try { diff --git a/KfChatDotNetBot/Services/ThreeXplPocketWatch.cs b/KfChatDotNetBot/Services/ThreeXplPocketWatch.cs index 3e12841..9ca1e17 100644 --- a/KfChatDotNetBot/Services/ThreeXplPocketWatch.cs +++ b/KfChatDotNetBot/Services/ThreeXplPocketWatch.cs @@ -12,15 +12,17 @@ public class ThreeXplPocketWatch private Logger _logger = LogManager.GetCurrentClassLogger(); private string _3xplToken = "3A0_t3st3xplor3rpub11cb3t4efcd21748a5e"; private string? _proxy; - private CancellationToken _cancellationToken = CancellationToken.None; + private CancellationToken _cancellationToken; public delegate void OnPocketWatchEventHandler(object sender, PocketWatchTransactionDbModel e); - public event OnPocketWatchEventHandler OnPocketWatchEvent; +#pragma warning disable CS0067 // Event is never used + public event OnPocketWatchEventHandler? OnPocketWatchEvent; +#pragma warning restore CS0067 // Event is never used - public ThreeXplPocketWatch(string? proxy = null, CancellationToken? cancellationToken = null) + public ThreeXplPocketWatch(string? proxy = null, CancellationToken cancellationToken = default) { _logger.Info("Starting the pocket watch"); _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; } private async Task CheckAddress(PocketWatchAddressDbModel addy) diff --git a/KfChatDotNetBot/Services/Twitch.cs b/KfChatDotNetBot/Services/Twitch.cs index e5cf671..2352b70 100644 --- a/KfChatDotNetBot/Services/Twitch.cs +++ b/KfChatDotNetBot/Services/Twitch.cs @@ -11,7 +11,7 @@ namespace KfChatDotNetBot.Services; public class Twitch : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + private WebsocketClient? _wsClient; private Uri _wsUri = new("wss://pubsub-edge.twitch.tv/v1"); private int _reconnectTimeout = 300; private string? _proxy; @@ -19,18 +19,18 @@ public class Twitch : IDisposable public delegate void OnStreamStateUpdateEventHandler(object sender, int channelId, bool isLive); public delegate void OnStreamCommercialEventHandler(object sender, int channelId, int length, bool scheduled); public delegate void OnStreamTosStrikeEventHandler(object sender, int channelId); - public event OnStreamStateUpdateEventHandler OnStreamStateUpdated; - public event OnStreamCommercialEventHandler OnStreamCommercial; - public event OnStreamTosStrikeEventHandler OnStreamTosStrike; - private CancellationToken _cancellationToken = CancellationToken.None; - private Task? _pingTask = null; + public event OnStreamStateUpdateEventHandler? OnStreamStateUpdated; + public event OnStreamCommercialEventHandler? OnStreamCommercial; + public event OnStreamTosStrikeEventHandler? OnStreamTosStrike; + private CancellationToken _cancellationToken; + private Task? _pingTask; private CancellationTokenSource _pingCts = new(); - public Twitch(List channels, string? proxy = null, CancellationToken? cancellationToken = null) + public Twitch(List channels, string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; _channels = channels; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; } public async Task StartWsClient() @@ -77,7 +77,7 @@ public class Twitch : IDisposable private void SendPing() { _logger.Info("Sending ping to Twitch"); - _wsClient.Send("{\"type\":\"PING\"}"); + _wsClient?.Send("{\"type\":\"PING\"}"); } private async Task PeriodicPing() @@ -107,7 +107,7 @@ public class Twitch : IDisposable Guid.NewGuid() + "\",\"type\":\"LISTEN\"}"; _logger.Debug("Sending the following JSON to Twitch"); _logger.Debug(payload); - _wsClient.Send(payload); + _wsClient?.Send(payload); } } @@ -131,13 +131,13 @@ public class Twitch : IDisposable var packet = JsonSerializer.Deserialize(message.Text); if (packet.GetProperty("type").GetString() != "MESSAGE") return; - var data = packet.GetProperty("data")!; - var topicString = data.GetProperty("topic")!.GetString()!; + var data = packet.GetProperty("data"); + var topicString = data.GetProperty("topic").GetString()!; if (!topicString.StartsWith("video-playback-by-id.")) return; var topicParts = topicString.Split('.'); var channelId = int.Parse(topicParts[^1]); - var twitchMessage = JsonSerializer.Deserialize(data.GetProperty("message").GetString()); + var twitchMessage = JsonSerializer.Deserialize(data.GetProperty("message").GetString()!); if (twitchMessage.GetProperty("type").GetString() == "stream-up") { @@ -242,7 +242,7 @@ public class Twitch : IDisposable public void Dispose() { - _wsClient.Dispose(); + _wsClient?.Dispose(); _pingCts.Cancel(); _pingCts.Dispose(); _pingTask?.Dispose(); diff --git a/KfChatDotNetBot/Services/TwitchChat.cs b/KfChatDotNetBot/Services/TwitchChat.cs index 50db986..3d6ac54 100644 --- a/KfChatDotNetBot/Services/TwitchChat.cs +++ b/KfChatDotNetBot/Services/TwitchChat.cs @@ -10,7 +10,7 @@ namespace KfChatDotNetBot.Services; public class TwitchChat : IDisposable { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + private WebsocketClient? _wsClient; private readonly Uri _wsUri = new("wss://irc-ws.chat.twitch.tv/"); private const int ReconnectTimeout = 600; private readonly string? _proxy; @@ -19,15 +19,15 @@ public class TwitchChat : IDisposable public delegate void MessageReceivedEventHandler(object sender, string nick, string target, string message); public delegate void WsDisconnectionEventHandler(object sender, DisconnectionInfo e); - public event MessageReceivedEventHandler OnMessageReceived; - public event WsDisconnectionEventHandler OnWsDisconnection; + public event MessageReceivedEventHandler? OnMessageReceived; + public event WsDisconnectionEventHandler? OnWsDisconnection; - private readonly CancellationToken _cancellationToken = CancellationToken.None; + private readonly CancellationToken _cancellationToken; - public TwitchChat(string channel, string? proxy = null, CancellationToken? cancellationToken = null) + public TwitchChat(string channel, string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; _channel = channel; var justinFan = new Random().Next(10000, 99999); _nick = $"justinfan{justinFan}"; @@ -87,13 +87,13 @@ public class TwitchChat : IDisposable _logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}"); _logger.Info("Sending registration info to Twitch IRC"); // I've found if you use the message queue then things come out of order, hence using SendInstant - _wsClient.SendInstant("CAP REQ :twitch.tv/tags twitch.tv/commands").Wait(_cancellationToken); + _wsClient?.SendInstant("CAP REQ :twitch.tv/tags twitch.tv/commands").Wait(_cancellationToken); // Would be an oauth token if you were signed in, but this is just guest access - _wsClient.SendInstant("PASS SCHMOOPIIE").Wait(_cancellationToken); + _wsClient?.SendInstant("PASS SCHMOOPIIE").Wait(_cancellationToken); // Guest users are just justinfan12345 where the 5 digits are random - _wsClient.SendInstant($"NICK {_nick}").Wait(_cancellationToken); + _wsClient?.SendInstant($"NICK {_nick}").Wait(_cancellationToken); // I'm ashamed I've forgotten so much IRC protocol shit that I can't remember what the USER params mean :( - _wsClient.SendInstant($"USER {_nick} 8 * :{_nick}").Wait(_cancellationToken); + _wsClient?.SendInstant($"USER {_nick} 8 * :{_nick}").Wait(_cancellationToken); } private void WsMessageReceived(ResponseMessage message) @@ -147,7 +147,7 @@ public class TwitchChat : IDisposable { case "PING": _logger.Info("Received PING, sending PONG"); - _wsClient.Send("PONG"); + _wsClient?.Send("PONG"); return; case "JOIN": _logger.Debug("Received JOIN response"); @@ -155,7 +155,7 @@ public class TwitchChat : IDisposable // MOTD case "001": _logger.Debug("Received MOTD. Sending JOIN"); - _wsClient.Send($"JOIN {_channel}"); + _wsClient?.Send($"JOIN {_channel}"); return; default: _logger.Debug($"Command {command} was not handled"); @@ -174,7 +174,7 @@ public class TwitchChat : IDisposable public void Dispose() { - _wsClient.Dispose(); + _wsClient?.Dispose(); GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/KfChatDotNetBot/Services/Yeet.cs b/KfChatDotNetBot/Services/Yeet.cs index 4853caa..fa00ac2 100644 --- a/KfChatDotNetBot/Services/Yeet.cs +++ b/KfChatDotNetBot/Services/Yeet.cs @@ -10,22 +10,22 @@ namespace KfChatDotNetBot.Services; public class Yeet : IDisposable { private Logger _logger = LogManager.GetCurrentClassLogger(); - private WebsocketClient _wsClient; + private WebsocketClient? _wsClient; private Uri _wsUri = new("wss://api.yeet.com/room-service/socket/?EIO=4&transport=websocket"); 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 event OnYeetBetEventHandler? OnYeetBet; + public event OnYeetWinEventHandler? OnYeetWin; + public event OnWsDisconnectionEventHandler? OnWsDisconnection; + private CancellationToken _cancellationToken; - public Yeet(string? proxy = null, CancellationToken? cancellationToken = null) + public Yeet(string? proxy = null, CancellationToken cancellationToken = default) { _proxy = proxy; - if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _cancellationToken = cancellationToken; _logger.Info("Yeet WebSocket client created"); } @@ -84,7 +84,7 @@ public class Yeet : IDisposable if (reconnectionInfo.Type == ReconnectionType.Initial) { _logger.Info("Sending subscribe payload to Yeet"); - _wsClient.Send("40/public,"); + _wsClient?.Send("40/public,"); } } @@ -104,7 +104,7 @@ public class Yeet : IDisposable if (packetType == "2") { _logger.Info("Received ping from Yeet, replying with pong"); - _wsClient.Send("3"); + _wsClient?.Send("3"); return; } @@ -112,14 +112,15 @@ public class Yeet : IDisposable { var data = JsonSerializer.Deserialize>(message.Text.Replace("42/public,", string.Empty)); + if (data == null) throw new Exception("Caught a null when deserializing the Yeet bet payload"); if (data[0].GetString() == "casino-bet") { - OnYeetBet?.Invoke(this, data[1].Deserialize()); + OnYeetBet?.Invoke(this, data[1].Deserialize()!); return; } if (data[0].GetString() == "casino-win") { - OnYeetWin?.Invoke(this, data[1].Deserialize()); + OnYeetWin?.Invoke(this, data[1].Deserialize()!); return; } @@ -146,7 +147,7 @@ public class Yeet : IDisposable public void Dispose() { - _wsClient.Dispose(); + _wsClient?.Dispose(); GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/KfChatDotNetBot/Settings/SettingsProvider.cs b/KfChatDotNetBot/Settings/SettingsProvider.cs index c756985..5671d9e 100644 --- a/KfChatDotNetBot/Settings/SettingsProvider.cs +++ b/KfChatDotNetBot/Settings/SettingsProvider.cs @@ -15,6 +15,11 @@ public static class SettingsProvider if (!bypassCache && cache.Contains(key)) { var cachedSetting = cache.Get(key) as SettingDbModel; + if (cachedSetting == null) + { + logger.Error($"Caught a null in spite of the cache supposedly containing {key}"); + throw new Exception("Cached Setting entry was null"); + } var value = cachedSetting.Value; if (cachedSetting.Value == "null") value = null; return new Setting(value, cachedSetting, true); diff --git a/KfChatDotNetWsClient/ChatClient.cs b/KfChatDotNetWsClient/ChatClient.cs index 757b807..290a3e1 100644 --- a/KfChatDotNetWsClient/ChatClient.cs +++ b/KfChatDotNetWsClient/ChatClient.cs @@ -1,7 +1,6 @@ using System.Net; using System.Net.WebSockets; using System.Text.Json; -using System.Xml; using KfChatDotNetWsClient.Models; using KfChatDotNetWsClient.Models.Events; using KfChatDotNetWsClient.Models.Json; @@ -14,15 +13,15 @@ namespace KfChatDotNetWsClient; public class ChatClient { - public event EventHandlers.OnMessagesEventHandler OnMessages; - public event EventHandlers.OnUsersPartedEventHandler OnUsersParted; - public event EventHandlers.OnUsersJoinedEventHandler OnUsersJoined; - public event EventHandlers.OnWsReconnectEventHandler OnWsReconnect; - public event EventHandlers.OnDeleteMessagesEventHandler OnDeleteMessages; - public event EventHandlers.OnWsDisconnectionEventHandler OnWsDisconnection; - public event EventHandlers.OnFailedToJoinRoom OnFailedToJoinRoom; - public event EventHandlers.OnUnknownCommand OnUnknownCommand; - private WebsocketClient _wsClient; + public event EventHandlers.OnMessagesEventHandler? OnMessages; + public event EventHandlers.OnUsersPartedEventHandler? OnUsersParted; + public event EventHandlers.OnUsersJoinedEventHandler? OnUsersJoined; + public event EventHandlers.OnWsReconnectEventHandler? OnWsReconnect; + public event EventHandlers.OnDeleteMessagesEventHandler? OnDeleteMessages; + public event EventHandlers.OnWsDisconnectionEventHandler? OnWsDisconnection; + public event EventHandlers.OnFailedToJoinRoom? OnFailedToJoinRoom; + public event EventHandlers.OnUnknownCommand? OnUnknownCommand; + private WebsocketClient? _wsClient; private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private ChatClientConfigModel _config; public DateTime LastPacketReceived = DateTime.UtcNow; @@ -49,6 +48,7 @@ public class ChatClient public void Disconnect() { + if (_wsClient == null) throw new WebSocketNotInitializedException(); _wsClient.Stop(WebSocketCloseStatus.NormalClosure, "Closing websocket").Wait(); } @@ -57,11 +57,13 @@ public class ChatClient // none for reconnect. public async Task DisconnectAsync() { + if (_wsClient == null) throw new WebSocketNotInitializedException(); await _wsClient.Stop(WebSocketCloseStatus.NormalClosure, "Closing websocket"); } public async Task Reconnect() { + if (_wsClient == null) throw new WebSocketNotInitializedException(); await _wsClient.Reconnect(); } @@ -144,7 +146,7 @@ public class ChatClient return; } - Dictionary packetType = new Dictionary(); + Dictionary packetType; try { packetType = JsonSerializer.Deserialize>(message.Text)!; @@ -193,30 +195,35 @@ public class ChatClient public void JoinRoom(int roomId) { _logger.Debug($"Joining {roomId}"); + if (_wsClient == null) throw new WebSocketNotInitializedException(); _wsClient.Send($"/join {roomId}"); } public void SendMessage(string message) { _logger.Debug($"Sending '{message}'"); + if (_wsClient == null) throw new WebSocketNotInitializedException(); _wsClient.Send(message); } public async Task SendMessageInstantAsync(string message) { _logger.Debug($"Sending '{message}', bypassing the queue"); + if (_wsClient == null) throw new WebSocketNotInitializedException(); await _wsClient.SendInstant(message); } public void DeleteMessage(int messageId) { _logger.Debug($"Deleting {messageId}"); + if (_wsClient == null) throw new WebSocketNotInitializedException(); _wsClient.Send($"/delete {messageId}"); } public async Task DeleteMessageAsync(int messageId) { _logger.Debug($"Deleting {messageId}"); + if (_wsClient == null) throw new WebSocketNotInitializedException(); await _wsClient.SendInstant($"/delete {messageId}"); } @@ -224,6 +231,7 @@ public class ChatClient { var payload = JsonSerializer.Serialize(new EditMessageJsonModel {Id = messageId, Message = newMessage}); _logger.Debug($"Editing {messageId} with '{newMessage}'"); + if (_wsClient == null) throw new WebSocketNotInitializedException(); _wsClient.Send($"/edit {payload}"); } @@ -231,21 +239,22 @@ public class ChatClient { var payload = JsonSerializer.Serialize(new EditMessageJsonModel {Id = messageId, Message = newMessage}); _logger.Debug($"Editing {messageId} with '{newMessage}'"); + if (_wsClient == null) throw new WebSocketNotInitializedException(); await _wsClient.SendInstant($"/edit {payload}"); } private void WsDeleteMessagesReceived(ResponseMessage message) { - var data = JsonSerializer.Deserialize(message.Text); - _logger.Debug($"Received delete packet for messages: {string.Join(',', data.MessageIdsToDelete)}"); + var data = JsonSerializer.Deserialize(message.Text!); + _logger.Debug($"Received delete packet for messages: {string.Join(',', data!.MessageIdsToDelete)}"); OnDeleteMessages?.Invoke(this, data.MessageIdsToDelete); } private void WsChatMessagesReceived(ResponseMessage message) { - var data = JsonSerializer.Deserialize(message.Text); + var data = JsonSerializer.Deserialize(message.Text!); var messages = new List(); - foreach (var chatMessage in data.Messages) + foreach (var chatMessage in data!.Messages) { var model = new MessageModel { @@ -260,10 +269,12 @@ public class ChatClient Message = chatMessage.Message, MessageId = chatMessage.MessageId, MessageRaw = chatMessage.MessageRaw, - RoomId = chatMessage.RoomId + RoomId = chatMessage.RoomId, + MessageRawHtmlDecoded = WebUtility.HtmlDecode(chatMessage.MessageRaw), + MessageDate = DateTimeOffset.FromUnixTimeSeconds(chatMessage.MessageDate) }; - if(chatMessage.MessageEditDate == 0) + if (chatMessage.MessageEditDate == 0) { model.MessageEditDate = null; } @@ -272,8 +283,6 @@ public class ChatClient model.MessageEditDate = DateTimeOffset.FromUnixTimeSeconds(chatMessage.MessageEditDate); } - model.MessageDate = DateTimeOffset.FromUnixTimeSeconds(chatMessage.MessageDate); - messages.Add(model); } _logger.Debug($"Received {messages.Count} chat messages"); @@ -286,9 +295,9 @@ public class ChatClient private void WsChatUsersJoined(ResponseMessage message) { - var data = JsonSerializer.Deserialize(message.Text); + var data = JsonSerializer.Deserialize(message.Text!); var users = new List(); - foreach (var user in data.Users.Keys) + foreach (var user in data!.Users.Keys) { users.Add(new UserModel { @@ -306,9 +315,11 @@ public class ChatClient private void WsChatUsersParted(ResponseMessage message) { // {"user":{"1337":false}} - var data = JsonSerializer.Deserialize>>(message.Text); + var data = JsonSerializer.Deserialize>>(message.Text!); var usersParted = data!["user"].Select(user => int.Parse(user.Key)).ToList(); _logger.Debug($"Following users have parted: {string.Join(',', usersParted)}"); OnUsersParted?.Invoke(this, usersParted); } -} \ No newline at end of file +} + +public class WebSocketNotInitializedException : Exception; \ No newline at end of file diff --git a/KfChatDotNetWsClient/Models/ChatClientConfigModel.cs b/KfChatDotNetWsClient/Models/ChatClientConfigModel.cs index 60cf926..965f9fb 100644 --- a/KfChatDotNetWsClient/Models/ChatClientConfigModel.cs +++ b/KfChatDotNetWsClient/Models/ChatClientConfigModel.cs @@ -5,7 +5,7 @@ public class ChatClientConfigModel // XF session token. Sent as a cookie to auth the user public string? XfSessionToken { get; set; } // Currently wss://kiwifarms.net/chat.ws - public Uri WsUri { get; set; } + public required Uri WsUri { get; set; } public int ReconnectTimeout { get; set; } = 30; public string CookieDomain { get; set; } = "kiwifarms.net"; public string? Proxy { get; set; } diff --git a/KfChatDotNetWsClient/Models/Events/MessageModel.cs b/KfChatDotNetWsClient/Models/Events/MessageModel.cs index a8c0285..5555c09 100644 --- a/KfChatDotNetWsClient/Models/Events/MessageModel.cs +++ b/KfChatDotNetWsClient/Models/Events/MessageModel.cs @@ -2,17 +2,21 @@ namespace KfChatDotNetWsClient.Models.Events; public class MessageModel { - public UserModel Author { get; set; } + public required UserModel Author { get; set; } /// /// HTML formatted message /// - public string Message { get; set; } - public int MessageId { get; set; } + public required string Message { get; set; } + public required int MessageId { get; set; } public DateTimeOffset? MessageEditDate { get; set; } - public DateTimeOffset MessageDate { get; set; } + public required DateTimeOffset MessageDate { get; set; } /// - /// Unformatted message with original BB code + /// Unformatted message with original BB code but retaining HTML encoding /// - public string MessageRaw { get; set; } - public int RoomId { get; set; } + public required string MessageRaw { get; set; } + public required int RoomId { get; set; } + /// + /// Unformatted message with original BB code and HTML entities decoded + /// + public required string MessageRawHtmlDecoded { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetWsClient/Models/Events/UserModel.cs b/KfChatDotNetWsClient/Models/Events/UserModel.cs index 1acda99..a4c03f3 100644 --- a/KfChatDotNetWsClient/Models/Events/UserModel.cs +++ b/KfChatDotNetWsClient/Models/Events/UserModel.cs @@ -6,8 +6,8 @@ public class UserModel /// /// Forum display name. Note that it'll be HTML encoded /// - public string Username { get; set; } - public Uri AvatarUrl { get; set; } + public required string Username { get; set; } + public Uri? AvatarUrl { get; set; } // Unset if it's related to a chat message public DateTimeOffset? LastActivity { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetWsClient/Models/Json/DeleteMessagesJsonModel.cs b/KfChatDotNetWsClient/Models/Json/DeleteMessagesJsonModel.cs index 5d31a05..f8c8911 100644 --- a/KfChatDotNetWsClient/Models/Json/DeleteMessagesJsonModel.cs +++ b/KfChatDotNetWsClient/Models/Json/DeleteMessagesJsonModel.cs @@ -5,5 +5,5 @@ namespace KfChatDotNetWsClient.Models.Json; public class DeleteMessagesJsonModel { [JsonPropertyName("delete")] - public List MessageIdsToDelete { get; set; } + public required List MessageIdsToDelete { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetWsClient/Models/Json/EditMessageJsonModel.cs b/KfChatDotNetWsClient/Models/Json/EditMessageJsonModel.cs index 74fe4f7..0d7aa57 100644 --- a/KfChatDotNetWsClient/Models/Json/EditMessageJsonModel.cs +++ b/KfChatDotNetWsClient/Models/Json/EditMessageJsonModel.cs @@ -5,8 +5,8 @@ namespace KfChatDotNetWsClient.Models.Json; public class EditMessageJsonModel { [JsonPropertyName("id")] - public int Id { get; set; } + public required int Id { get; set; } [JsonPropertyName("message")] - public string Message { get; set; } + public required string Message { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetWsClient/Models/Json/MessagesJsonModel.cs b/KfChatDotNetWsClient/Models/Json/MessagesJsonModel.cs index c9942d1..3ca6957 100644 --- a/KfChatDotNetWsClient/Models/Json/MessagesJsonModel.cs +++ b/KfChatDotNetWsClient/Models/Json/MessagesJsonModel.cs @@ -30,17 +30,17 @@ public class MessagesJsonModel [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("username")] - public string Username { get; set; } + public required string Username { get; set; } [JsonPropertyName("avatar_url")] - public Uri AvatarUrl { get; set; } + public Uri? AvatarUrl { get; set; } } public class MessageModel { [JsonPropertyName("author")] - public AuthorModel Author { get; set; } + public required AuthorModel Author { get; set; } [JsonPropertyName("message")] - public string Message { get; set; } + public required string Message { get; set; } [JsonPropertyName("message_id")] public int MessageId { get; set; } [JsonPropertyName("message_edit_date")] @@ -48,11 +48,10 @@ public class MessagesJsonModel [JsonPropertyName("message_date")] public int MessageDate { get; set; } [JsonPropertyName("message_raw")] - public string MessageRaw { get; set; } + public required string MessageRaw { get; set; } [JsonPropertyName("room_id")] public int RoomId { get; set; } } - - [JsonPropertyName("messages")] - public List Messages { get; set; } + + [JsonPropertyName("messages")] public List Messages { get; set; } = []; } \ No newline at end of file diff --git a/KfChatDotNetWsClient/Models/Json/UsersJsonModel.cs b/KfChatDotNetWsClient/Models/Json/UsersJsonModel.cs index 4de06cb..c75a396 100644 --- a/KfChatDotNetWsClient/Models/Json/UsersJsonModel.cs +++ b/KfChatDotNetWsClient/Models/Json/UsersJsonModel.cs @@ -19,13 +19,13 @@ public class UsersJsonModel [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("username")] - public string Username { get; set; } + public required string Username { get; set; } [JsonPropertyName("avatar_url")] - public Uri AvatarUrl { get; set; } + public Uri? AvatarUrl { get; set; } [JsonPropertyName("last_activity")] public int LastActivity { get; set; } } - + [JsonPropertyName("users")] - public Dictionary Users { get; set; } + public Dictionary Users { get; set; } = new(); } \ No newline at end of file