From d61a171e5448544df7093bf74e84de447928cf87 Mon Sep 17 00:00:00 2001 From: barelyprofessional <150058423+barelyprofessional@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:37:15 +1000 Subject: [PATCH] Big update introducing ghetto command interface, settings, database and howl.gg bet feed scraping --- KfChatDotNetKickBot/ApplicationDbContext.cs | 17 + KfChatDotNetKickBot/Commands/ICommand.cs | 15 + KfChatDotNetKickBot/Commands/JuiceCommand.cs | 46 +++ KfChatDotNetKickBot/Commands/MemeCommands.cs | 61 +++ KfChatDotNetKickBot/Commands/TimeCommand.cs | 20 + KfChatDotNetKickBot/Commands/WhoisCommand.cs | 30 ++ .../KfChatDotNetKickBot.csproj | 15 +- KfChatDotNetKickBot/KickBot.cs | 345 ++++++++--------- .../20240714132259_Initial.Designer.cs | 82 +++++ .../Migrations/20240714132259_Initial.cs | 67 ++++ .../20240716193646_Settings.Designer.cs | 115 ++++++ .../Migrations/20240716193646_Settings.cs | 39 ++ .../20240717110408_Howlgg.Designer.cs | 148 ++++++++ .../Migrations/20240717110408_Howlgg.cs | 41 +++ .../ApplicationDbContextModelSnapshot.cs | 145 ++++++++ .../Models/BuiltInSettingsModel.cs | 13 + KfChatDotNetKickBot/Models/ConfigModel.cs | 1 + .../Models/DbModels/HowlggBetsDbModel.cs | 17 + .../Models/DbModels/JuicerDbModel.cs | 9 + .../Models/DbModels/SettingDbModel.cs | 19 + .../Models/DbModels/UserDbModel.cs | 21 ++ KfChatDotNetKickBot/Models/HowlggModels.cs | 50 +++ .../Models/PocketWatchModel.cs | 26 ++ KfChatDotNetKickBot/Models/ThreeXplModels.cs | 48 +++ KfChatDotNetKickBot/NLog.config | 2 +- KfChatDotNetKickBot/Program.cs | 14 +- KfChatDotNetKickBot/Services/BotCommands.cs | 88 +++++ KfChatDotNetKickBot/Services/Discord.cs | 20 +- KfChatDotNetKickBot/Services/Howlgg.cs | 178 +++++++++ KfChatDotNetKickBot/Services/Shuffle.cs | 23 +- .../Services/ThreeXplPocketWatch.cs | 57 +++ KfChatDotNetKickBot/Services/Twitch.cs | 8 +- KfChatDotNetKickBot/Services/TwitchChat.cs | 17 +- KfChatDotNetKickBot/Settings/BuiltIn.cs | 346 ++++++++++++++++++ KfChatDotNetKickBot/Settings/Helpers.cs | 173 +++++++++ KfChatDotNetKickBot/Settings/SettingValue.cs | 9 + KfChatDotNetKickBot/Settings/Utils.cs | 59 +++ KfChatDotNetWsClient/ChatClient.cs | 3 +- .../KfChatDotNetWsClient.csproj | 2 +- .../Models/Events/MessageModel.cs | 6 + .../Models/Events/UserModel.cs | 3 + KickWsClient/KickWsClient.cs | 5 +- KickWsClient/KickWsClient.csproj | 4 +- ThreeXplCliClient/ThreeXplCliClient.csproj | 2 +- ThreeXplWsClient/ThreeXplWsClient.cs | 3 +- ThreeXplWsClient/ThreeXplWsClient.csproj | 4 +- 46 files changed, 2198 insertions(+), 218 deletions(-) create mode 100644 KfChatDotNetKickBot/ApplicationDbContext.cs create mode 100644 KfChatDotNetKickBot/Commands/ICommand.cs create mode 100644 KfChatDotNetKickBot/Commands/JuiceCommand.cs create mode 100644 KfChatDotNetKickBot/Commands/MemeCommands.cs create mode 100644 KfChatDotNetKickBot/Commands/TimeCommand.cs create mode 100644 KfChatDotNetKickBot/Commands/WhoisCommand.cs create mode 100644 KfChatDotNetKickBot/Migrations/20240714132259_Initial.Designer.cs create mode 100644 KfChatDotNetKickBot/Migrations/20240714132259_Initial.cs create mode 100644 KfChatDotNetKickBot/Migrations/20240716193646_Settings.Designer.cs create mode 100644 KfChatDotNetKickBot/Migrations/20240716193646_Settings.cs create mode 100644 KfChatDotNetKickBot/Migrations/20240717110408_Howlgg.Designer.cs create mode 100644 KfChatDotNetKickBot/Migrations/20240717110408_Howlgg.cs create mode 100644 KfChatDotNetKickBot/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 KfChatDotNetKickBot/Models/BuiltInSettingsModel.cs create mode 100644 KfChatDotNetKickBot/Models/DbModels/HowlggBetsDbModel.cs create mode 100644 KfChatDotNetKickBot/Models/DbModels/JuicerDbModel.cs create mode 100644 KfChatDotNetKickBot/Models/DbModels/SettingDbModel.cs create mode 100644 KfChatDotNetKickBot/Models/DbModels/UserDbModel.cs create mode 100644 KfChatDotNetKickBot/Models/HowlggModels.cs create mode 100644 KfChatDotNetKickBot/Models/PocketWatchModel.cs create mode 100644 KfChatDotNetKickBot/Models/ThreeXplModels.cs create mode 100644 KfChatDotNetKickBot/Services/BotCommands.cs create mode 100644 KfChatDotNetKickBot/Services/Howlgg.cs create mode 100644 KfChatDotNetKickBot/Services/ThreeXplPocketWatch.cs create mode 100644 KfChatDotNetKickBot/Settings/BuiltIn.cs create mode 100644 KfChatDotNetKickBot/Settings/Helpers.cs create mode 100644 KfChatDotNetKickBot/Settings/SettingValue.cs create mode 100644 KfChatDotNetKickBot/Settings/Utils.cs diff --git a/KfChatDotNetKickBot/ApplicationDbContext.cs b/KfChatDotNetKickBot/ApplicationDbContext.cs new file mode 100644 index 0000000..af52872 --- /dev/null +++ b/KfChatDotNetKickBot/ApplicationDbContext.cs @@ -0,0 +1,17 @@ +using KfChatDotNetKickBot.Models.DbModels; +using Microsoft.EntityFrameworkCore; + +namespace KfChatDotNetKickBot; + +public class ApplicationDbContext : DbContext +{ + protected override void OnConfiguring(DbContextOptionsBuilder builder) + { + builder.UseSqlite("Data Source=db.sqlite"); + } + + public DbSet Users { get; set; } + public DbSet Juicers { get; set; } + public DbSet Settings { get; set; } + public DbSet HowlggBets { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Commands/ICommand.cs b/KfChatDotNetKickBot/Commands/ICommand.cs new file mode 100644 index 0000000..2b8b81b --- /dev/null +++ b/KfChatDotNetKickBot/Commands/ICommand.cs @@ -0,0 +1,15 @@ +using System.Text.RegularExpressions; +using KfChatDotNetKickBot.Models.DbModels; +using KfChatDotNetWsClient.Models.Events; + +namespace KfChatDotNetKickBot.Commands; + +internal interface ICommand +{ + List Patterns { get; } + string HelpText { get; } + bool HideFromHelp { get; } + UserRight RequiredRight { get; } + + Task RunCommand(KickBot botInstance, MessageModel message, GroupCollection arguments, CancellationToken ctx); +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Commands/JuiceCommand.cs b/KfChatDotNetKickBot/Commands/JuiceCommand.cs new file mode 100644 index 0000000..10640eb --- /dev/null +++ b/KfChatDotNetKickBot/Commands/JuiceCommand.cs @@ -0,0 +1,46 @@ +using System.Text.RegularExpressions; +using Humanizer; +using KfChatDotNetKickBot.Models.DbModels; +using KfChatDotNetKickBot.Settings; +using KfChatDotNetWsClient.Models.Events; +using Microsoft.EntityFrameworkCore; + +namespace KfChatDotNetKickBot.Commands; + +public class JuiceCommand : ICommand +{ + public List Patterns { get; } = [new Regex("^juiceme$")]; + public string HelpText => "Get juice!"; + public bool HideFromHelp => false; + public UserRight RequiredRight => UserRight.Guest; + public async Task RunCommand(KickBot botInstance, MessageModel message, GroupCollection arguments, CancellationToken ctx) + { + await using var db = new ApplicationDbContext(); + var user = await db.Users.FirstOrDefaultAsync(u => u.KfId == message.Author.Id, cancellationToken: ctx); + if (user == null) return; + var juicerSettings = await Helpers.GetMultipleValues([BuiltIn.Keys.JuiceAmount, BuiltIn.Keys.JuiceCooldown]); + var cooldown = juicerSettings[BuiltIn.Keys.JuiceCooldown].ToType(); + var amount = juicerSettings[BuiltIn.Keys.JuiceAmount].ToType(); + var lastJuicer = (await db.Juicers.Where(j => j.User == user).ToListAsync(ctx)).OrderByDescending(j => j.JuicedAt).Take(1).ToList(); + if (lastJuicer.Count == 0) + { + botInstance.SendChatMessage($"!juice {message.Author.Id} {amount}", true); + await db.Juicers.AddAsync(new JuicerDbModel + { Amount = amount, User = user, JuicedAt = DateTimeOffset.UtcNow }, ctx); + await db.SaveChangesAsync(ctx); + return; + } + + var secondsRemaining = lastJuicer[0].JuicedAt.AddSeconds(cooldown) - DateTimeOffset.UtcNow; + if (secondsRemaining.TotalSeconds <= 0) + { + botInstance.SendChatMessage($"!juice {message.Author.Id} {amount}", true); + await db.Juicers.AddAsync(new JuicerDbModel + { Amount = amount, User = user, JuicedAt = DateTimeOffset.UtcNow }, ctx); + await db.SaveChangesAsync(ctx); + return; + } + + botInstance.SendChatMessage($"You gotta wait {secondsRemaining.Humanize(precision: 2)} for another juicer", true); + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Commands/MemeCommands.cs b/KfChatDotNetKickBot/Commands/MemeCommands.cs new file mode 100644 index 0000000..67ecfc1 --- /dev/null +++ b/KfChatDotNetKickBot/Commands/MemeCommands.cs @@ -0,0 +1,61 @@ +using System.Text.RegularExpressions; +using KfChatDotNetKickBot.Models.DbModels; +using KfChatDotNetWsClient.Models.Events; + +namespace KfChatDotNetKickBot.Commands; + +public class InsanityCommand : ICommand +{ + public List Patterns => [new Regex("^insanity")]; + public string HelpText => "Insanity"; + public bool HideFromHelp => false; + public UserRight RequiredRight => UserRight.Guest; + + public async Task RunCommand(KickBot botInstance, MessageModel message, GroupCollection arguments, CancellationToken ctx) + { + // ReSharper disable once StringLiteralTypo + botInstance.SendChatMessage("definition of insanity = doing the same thing over and over and over excecting a different result, and heres my dumbass trying to get rich every day and losing everythign i fucking touch every fucking time FUCK this bullshit FUCK MY LIEFdefinition of insanity = doing the same thing over and over and over excecting a different result, and heres my dumbass trying to get rich every day and losing everythign i fucking touch every fucking time FUCK this bullshit FUCK MY LIEF"); + } +} + +public class TwistedCommand : ICommand +{ + public List Patterns => [new Regex("^twisted")]; + public string HelpText => "Get it twisted"; + public bool HideFromHelp => false; + public UserRight RequiredRight => UserRight.Guest; + + public async Task RunCommand(KickBot botInstance, MessageModel message, GroupCollection arguments, CancellationToken ctx) + { + // ReSharper disable once StringLiteralTypo + botInstance.SendChatMessage("🦍 🗣 GET IT TWISTED 🌪 , GAMBLE ✅ . PLEASE START GAMBLING 👍 . GAMBLING IS AN INVESTMENT 🎰 AND AN INVESTMENT ONLY 👍 . YOU WILL PROFIT 💰 , YOU WILL WIN ❗ ️. YOU WILL DO ALL OF THAT 💯 , YOU UNDERSTAND ⁉ ️ YOU WILL BECOME A BILLIONAIRE 💵 📈 AND REBUILD YOUR FUCKING LIFE 🤯"); + } +} + +public class HelpMeCommand : ICommand +{ + public List Patterns => [new Regex("^helpme")]; + public string HelpText => "Somebody please help me"; + public bool HideFromHelp => false; + public UserRight RequiredRight => UserRight.Guest; + + public async Task RunCommand(KickBot botInstance, MessageModel message, GroupCollection arguments, CancellationToken ctx) + { + // ReSharper disable once StringLiteralTypo + botInstance.SendChatMessage("[img]https://i.postimg.cc/fTw6tGWZ/ineedmoneydumbfuck.png[/img]", true); + } +} + +public class SentCommand : ICommand +{ + public List Patterns => [new Regex("^sent$")]; + public string HelpText => "Sent love"; + public bool HideFromHelp => false; + public UserRight RequiredRight => UserRight.Guest; + + public async Task RunCommand(KickBot botInstance, MessageModel message, GroupCollection arguments, CancellationToken ctx) + { + // ReSharper disable once StringLiteralTypo + botInstance.SendChatMessage("[img]https://i.ibb.co/GHq7hb1/4373-g-N5-HEH2-Hkc.png[/img]", true); + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Commands/TimeCommand.cs b/KfChatDotNetKickBot/Commands/TimeCommand.cs new file mode 100644 index 0000000..14677b3 --- /dev/null +++ b/KfChatDotNetKickBot/Commands/TimeCommand.cs @@ -0,0 +1,20 @@ +using System.Text.RegularExpressions; +using KfChatDotNetKickBot.Models.DbModels; +using KfChatDotNetWsClient.Models.Events; + +namespace KfChatDotNetKickBot.Commands; + +public class TimeCommand : ICommand +{ + public List Patterns => [new Regex("^time")]; + public string HelpText => "Get current time in BMT"; + public bool HideFromHelp => false; + public UserRight RequiredRight => UserRight.Guest; + + public async Task RunCommand(KickBot botInstance, MessageModel message, GroupCollection arguments, CancellationToken ctx) + { + var bmt = new DateTimeOffset(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, + TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time")), TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time").BaseUtcOffset); + botInstance.SendChatMessage($"It's currently {bmt:h:mm:ss tt} BMT"); + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Commands/WhoisCommand.cs b/KfChatDotNetKickBot/Commands/WhoisCommand.cs new file mode 100644 index 0000000..2f389c9 --- /dev/null +++ b/KfChatDotNetKickBot/Commands/WhoisCommand.cs @@ -0,0 +1,30 @@ +using System.Text.RegularExpressions; +using KfChatDotNetKickBot.Models.DbModels; +using KfChatDotNetWsClient.Models.Events; +using Microsoft.EntityFrameworkCore; + +namespace KfChatDotNetKickBot.Commands; + +public class WhoisCommand : ICommand +{ + public List Patterns => [ + new Regex("^whois (?.+)") + ]; + + public string HelpText => "Lookup user IDs by username"; + public bool HideFromHelp => false; + public UserRight RequiredRight => UserRight.Guest; + + public async Task RunCommand(KickBot botInstance, MessageModel message, GroupCollection arguments, CancellationToken ctx) + { + await using var db = new ApplicationDbContext(); + var query = arguments["user"].Value.TrimStart('@').TrimEnd(',').TrimEnd(); + var user = await db.Users.FirstOrDefaultAsync(u => u.KfUsername == query, cancellationToken: ctx); + if (user == null) + { + botInstance.SendChatMessage($"Requested user '{query}' does not exist. (Note this is case-sensitive)", true); + return; + } + botInstance.SendChatMessage($"@{message.Author.Username}, {user.KfUsername}'s ID is {user.KfId}", true); + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/KfChatDotNetKickBot.csproj b/KfChatDotNetKickBot/KfChatDotNetKickBot.csproj index f10df68..c77f22c 100644 --- a/KfChatDotNetKickBot/KfChatDotNetKickBot.csproj +++ b/KfChatDotNetKickBot/KfChatDotNetKickBot.csproj @@ -8,9 +8,16 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - + + @@ -30,8 +37,4 @@ - - - - diff --git a/KfChatDotNetKickBot/KickBot.cs b/KfChatDotNetKickBot/KickBot.cs index bc8992a..b18fcdf 100644 --- a/KfChatDotNetKickBot/KickBot.cs +++ b/KfChatDotNetKickBot/KickBot.cs @@ -1,7 +1,7 @@ -using System.Net; -using System.Text.Json; -using KfChatDotNetKickBot.Models; +using KfChatDotNetKickBot.Models; +using KfChatDotNetKickBot.Models.DbModels; using KfChatDotNetKickBot.Services; +using KfChatDotNetKickBot.Settings; using KfChatDotNetWsClient; using KfChatDotNetWsClient.Models; using KfChatDotNetWsClient.Models.Events; @@ -14,12 +14,11 @@ namespace KfChatDotNetKickBot; public class KickBot { - private readonly ChatClient _kfClient; + internal readonly ChatClient KfClient; private readonly KickWsClient.KickWsClient _kickClient; private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - private readonly ConfigModel _config; private readonly bool _pingEnabled = true; - private bool _gambaSeshPresent; + internal bool GambaSeshPresent; private string _xfSessionToken = null!; // Oh no it's an ever expanding list that may never get cleaned up! // BUY MORE RAM @@ -32,43 +31,51 @@ public class KickBot private DiscordService _discord; private TwitchChat _twitchChat; private string? _lastDiscordStatus; - private bool _isBmjLive = false; + internal bool IsBmjLive = false; private bool _isBmjLiveSynced = false; - private Dictionary _userIdMapping = new(); private DateTime _lastKfEvent = DateTime.Now; + private BotCommands _botCommands; + private string _bmjTwitchUsername; + private Howlgg _howlgg; public KickBot() { _logger.Info("Bot starting!"); - const string configPath = "config.json"; - if (!Path.Exists(configPath)) - { - _logger.Error($"{configPath} is missing! Exiting"); - Environment.Exit(1); - } - _config = JsonSerializer.Deserialize(File.ReadAllText(configPath)) ?? - throw new InvalidOperationException(); - RefreshXfToken().Wait(_cancellationToken); - - _kfClient = new ChatClient(new ChatClientConfigModel + var settings = Helpers.GetMultipleValues([ + BuiltIn.Keys.KiwiFarmsWsEndpoint, BuiltIn.Keys.KiwiFarmsDomain, BuiltIn.Keys.PusherEndpoint, + BuiltIn.Keys.Proxy, BuiltIn.Keys.PusherReconnectTimeout, BuiltIn.Keys.PusherChannels, + BuiltIn.Keys.TwitchBossmanJackId, BuiltIn.Keys.DiscordToken, BuiltIn.Keys.KiwiFarmsWsReconnectTimeout, + BuiltIn.Keys.KiwiFarmsToken + ]).Result; + + _xfSessionToken = settings[BuiltIn.Keys.KiwiFarmsToken].Value ?? "unset"; + if (_xfSessionToken == "unset") { - WsUri = _config.KfWsEndpoint, + RefreshXfToken().Wait(_cancellationToken); + } + + KfClient = new ChatClient(new ChatClientConfigModel + { + WsUri = new Uri(settings[BuiltIn.Keys.KiwiFarmsWsEndpoint].Value ?? throw new InvalidOperationException($"{BuiltIn.Keys.KiwiFarmsWsEndpoint} cannot be null")), XfSessionToken = _xfSessionToken, - CookieDomain = _config.KfWsEndpoint.Host, - Proxy = _config.Proxy, - ReconnectTimeout = _config.KfReconnectTimeout + CookieDomain = settings[BuiltIn.Keys.KiwiFarmsDomain].Value ?? throw new InvalidOperationException($"{BuiltIn.Keys.KiwiFarmsDomain} cannot be null"), + Proxy = settings[BuiltIn.Keys.Proxy].Value, + ReconnectTimeout = Convert.ToInt32(settings[BuiltIn.Keys.KiwiFarmsWsReconnectTimeout].Value) }); - _kickClient = new KickWsClient.KickWsClient(_config.PusherEndpoint.ToString(), - _config.Proxy, _config.PusherReconnectTimeout); + _kickClient = new KickWsClient.KickWsClient(settings[BuiltIn.Keys.PusherEndpoint].Value!, + settings[BuiltIn.Keys.Proxy].Value, Convert.ToInt32(settings[BuiltIn.Keys.PusherReconnectTimeout].Value)); + + _logger.Debug("Creating bot command instance"); + _botCommands = new BotCommands(this, _cancellationToken); - _kfClient.OnMessages += OnKfChatMessage; - _kfClient.OnUsersParted += OnUsersParted; - _kfClient.OnUsersJoined += OnUsersJoined; - _kfClient.OnWsDisconnection += OnKfWsDisconnected; - _kfClient.OnWsReconnect += OnKfWsReconnected; - _kfClient.OnFailedToJoinRoom += OnFailedToJoinRoom; + KfClient.OnMessages += OnKfChatMessage; + KfClient.OnUsersParted += OnUsersParted; + KfClient.OnUsersJoined += OnUsersJoined; + KfClient.OnWsDisconnection += OnKfWsDisconnected; + KfClient.OnWsReconnect += OnKfWsReconnected; + KfClient.OnFailedToJoinRoom += OnFailedToJoinRoom; _kickClient.OnStreamerIsLive += OnStreamerIsLive; _kickClient.OnChatMessage += OnKickChatMessage; @@ -76,10 +83,11 @@ public class KickBot _kickClient.OnPusherSubscriptionSucceeded += OnPusherSubscriptionSucceeded; _kickClient.OnStopStreamBroadcast += OnStopStreamBroadcast; - _kfClient.StartWsClient().Wait(_cancellationToken); + KfClient.StartWsClient().Wait(_cancellationToken); _kickClient.StartWsClient().Wait(_cancellationToken); - foreach (var channel in _config.PusherChannels) + var pusherChannels = settings[BuiltIn.Keys.PusherChannels].Value ?? ""; + foreach (var channel in pusherChannels.Split(',')) { _kickClient.SendPusherSubscribe(channel); } @@ -88,47 +96,78 @@ public class KickBot var pingThread = new Thread(PingThread); pingThread.Start(); - if (_config.BossmanJackTwitchId != null) + if (settings[BuiltIn.Keys.TwitchBossmanJackId].Value != null) { _logger.Debug("Creating Twitch live stream notification client"); - _twitch = new Twitch([_config.BossmanJackTwitchId.Value], _config.Proxy, _cancellationToken); + _twitch = new Twitch([Convert.ToInt32(settings[BuiltIn.Keys.TwitchBossmanJackId].Value)], settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); _twitch.OnStreamStateUpdated += OnTwitchStreamStateUpdated; _twitch.StartWsClient().Wait(_cancellationToken); } else { - _logger.Debug("Ignoring Twitch client as TwitchChannels is not defined"); + _logger.Debug($"Ignoring Twitch client as {BuiltIn.Keys.TwitchBossmanJackId} is not defined"); } BuildShuffle(); BuildDiscord(); BuildTwitchChat(); + _howlgg = new Howlgg(settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); + _howlgg.OnHowlggBetHistory += OnHowlggBetHistory; + _howlgg.StartWsClient().Wait(_cancellationToken); + _logger.Debug("Blocking the main thread"); var exitEvent = new ManualResetEvent(false); exitEvent.WaitOne(); } + private void OnHowlggBetHistory(object sender, HowlggBetHistoryResponseModel data) + { + _logger.Debug("Received bet history from Howl.gg"); + using var db = new ApplicationDbContext(); + foreach (var bets in data.History.Data) + { + if (db.HowlggBets.Any(b => b.GameId == bets.GameId)) + { + _logger.Trace("Bet already exists in DB"); + continue; + } + + db.HowlggBets.Add(new HowlggBetsDbModel + { + UserId = data.User.Id, + BetId = bets.Id, + GameId = bets.GameId, + Bet = bets.Bet, + Profit = bets.Profit, + Date = bets.Date, + Game = bets.Game + }); + _logger.Debug("Added bet to DB"); + } + + db.SaveChanges(); + } + private void BuildShuffle() { _logger.Debug("Building Shuffle"); - _shuffle = new Shuffle(_config.Proxy, _cancellationToken); + _shuffle = new Shuffle(Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value, _cancellationToken); _shuffle.OnLatestBetUpdated += ShuffleOnLatestBetUpdated; - _shuffle.OnWsDisconnection += ShuffleOnWsDisconnection; _shuffle.StartWsClient().Wait(_cancellationToken); } private void BuildDiscord() { + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.DiscordToken, BuiltIn.Keys.Proxy]).Result; _logger.Debug("Building Discord"); - if (_config.DiscordToken == null) + if (settings[BuiltIn.Keys.DiscordToken].Value == null) { _logger.Info("Not building Discord as the token is not configured"); return; } - _discord = new DiscordService(_config.DiscordToken, _config.Proxy, _cancellationToken); + _discord = new DiscordService(settings[BuiltIn.Keys.DiscordToken].Value!, settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); _discord.OnInvalidCredentials += DiscordOnInvalidCredentials; - _discord.OnWsDisconnection += DiscordOnWsDisconnection; _discord.OnMessageReceived += DiscordOnMessageReceived; _discord.OnPresenceUpdated += DiscordOnPresenceUpdated; _discord.StartWsClient().Wait(_cancellationToken); @@ -136,105 +175,81 @@ public class KickBot private void BuildTwitchChat() { + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.TwitchBossmanJackUsername, BuiltIn.Keys.Proxy]).Result; _logger.Debug("Building Twitch Chat"); - if (_config.BossmanJackTwitchUsername == null) + if (settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value == null) { _logger.Info("Not building Twitch Chat client as BMJ's username is not configured"); return; } - _twitchChat = new TwitchChat($"#{_config.BossmanJackTwitchUsername}", _config.Proxy, _cancellationToken); + _bmjTwitchUsername = settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value!; + + _twitchChat = new TwitchChat($"#{settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value}", settings[BuiltIn.Keys.Proxy].Value, _cancellationToken); _twitchChat.OnMessageReceived += TwitchChatOnMessageReceived; - _twitchChat.OnWsDisconnection += TwitchChatOnWsDisconnection; _twitchChat.StartWsClient().Wait(_cancellationToken); } - - private void TwitchChatOnWsDisconnection(object sender, DisconnectionInfo e) - { - if (e.Type == DisconnectionType.ByServer) - { - _twitchChat.Dispose(); - BuildTwitchChat(); - } - } - + private void TwitchChatOnMessageReceived(object sender, string nick, string target, string message) { - if (nick != _config.BossmanJackTwitchUsername) + if (nick != _bmjTwitchUsername) { return; } - _sendChatMessage($"[img]https://i.postimg.cc/QMFVV2Xk/twitch16.png[/img] {nick}: {message}", true); + // 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}", true); } private void DiscordOnPresenceUpdated(object sender, DiscordPresenceUpdateModel presence) { - if (presence.User.Id != _config.DiscordBmjId) + 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; + // 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]https://i.postimg.cc/cLmQrp89/discord16.png[/img] BossmanJack has updated his Discord presence: {clientStatus}"); + SendChatMessage($"[img]{settings[BuiltIn.Keys.DiscordIcon].Value}[/img] BossmanJack has updated his Discord presence: {clientStatus}"); } private void DiscordOnMessageReceived(object sender, DiscordMessageModel message) { - if (message.Author.Id != _config.DiscordBmjId) + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.DiscordBmjId, BuiltIn.Keys.DiscordIcon]).Result; + if (message.Author.Id != settings[BuiltIn.Keys.DiscordBmjId].Value) { return; } - var result = $"[img]https://i.postimg.cc/cLmQrp89/discord16.png[/img] BossmanJack: {message.Content}"; + 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); - } - - private void DiscordOnWsDisconnection(object sender, DisconnectionInfo e) - { - _logger.Error($"Discord dropped, reason {e.Type}"); - // This is raised when the Websocket client is disposed - // Attempting to dispose it again while it is being disposed causes a loop and probably eventually a stack overflow - if (e.Type == DisconnectionType.Exit) - { - return; - } - _discord.Dispose(); - BuildDiscord(); + SendChatMessage(result); } private void DiscordOnInvalidCredentials(object sender, DiscordPacketReadModel packet) { - _logger.Error("Credentials failed to validate. Killing service."); - _discord.Dispose(); - } - - private void ShuffleOnWsDisconnection(object sender, DisconnectionInfo e) - { - if (e.Type == DisconnectionType.ByServer) - { - _shuffle.Dispose(); - BuildShuffle(); - } + _logger.Error("Credentials failed to validate."); } private void ShuffleOnLatestBetUpdated(object sender, ShuffleLatestBetModel bet) { - _logger.Debug("Shuffle bet has arrived"); - if (bet.Username != "TheBossmanJack") + var settings = Helpers + .GetMultipleValues([BuiltIn.Keys.ShuffleBmjUsername, BuiltIn.Keys.TwitchBossmanJackUsername]).Result; + _logger.Trace("Shuffle bet has arrived"); + if (bet.Username != settings[BuiltIn.Keys.ShuffleBmjUsername].Value) { return; } _logger.Info("ALERT BMJ IS BETTING"); - if (_isBmjLive) + if (IsBmjLive) { _logger.Info("Ignoring as BMJ is live"); return; @@ -245,10 +260,10 @@ public class KickBot // 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("thebossmanjack").Result; + IsBmjLive = _twitch.IsStreamLive(settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value!).Result; _isBmjLiveSynced = true; } - if (_isBmjLive) + if (IsBmjLive) { _logger.Info("Double checked and he is really online"); return; @@ -257,7 +272,7 @@ public class KickBot var payoutColor = "green"; if (float.Parse(bet.Payout) < float.Parse(bet.Amount)) payoutColor = "red"; // There will be a check for live status but ignoring that while we deal with an emergency dice situation - _sendChatMessage($"🚨🚨 {bet.Username} just bet {bet.Amount} {bet.Currency} which paid out [color={payoutColor}]{bet.Payout} {bet.Currency}[/color] ({bet.Multiplier}x) on {bet.GameName} 💰💰", true); + SendChatMessage($"🚨🚨 {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) @@ -265,13 +280,13 @@ public class KickBot _logger.Info($"BossmanJack stream event came in. isLive => {isLive}"); if (isLive) { - _sendChatMessage("BossmanJack just went live on Twitch! https://www.twitch.tv/thebossmanjack\r\n" + + SendChatMessage("BossmanJack just went live on Twitch! https://www.twitch.tv/thebossmanjack\r\n" + "Ad-free re-stream at https://bossmanjack.tv courtesy of @Kees H"); - _isBmjLive = true; + IsBmjLive = true; return; } - _sendChatMessage("BossmanJack is no longer live! :lossmanjack:"); - _isBmjLive = false; + SendChatMessage("BossmanJack is no longer live! :lossmanjack:"); + IsBmjLive = false; } private void OnFailedToJoinRoom(object sender, string message) @@ -279,10 +294,10 @@ public class KickBot _logger.Error($"Couldn't join the room. KF returned: {message}"); _logger.Error("This is likely due to the session cookie expiring. Retrieving a new one."); RefreshXfToken().Wait(_cancellationToken); - _kfClient.UpdateToken(_xfSessionToken); + KfClient.UpdateToken(_xfSessionToken); _logger.Info("Retrieved fresh token. Reconnecting."); - _kfClient.Disconnect(); - _kfClient.StartWsClient().Wait(_cancellationToken); + KfClient.Disconnect(); + KfClient.StartWsClient().Wait(_cancellationToken); _logger.Info("Client should be reconnecting now"); } @@ -291,8 +306,8 @@ public class KickBot while (_pingEnabled) { Thread.Sleep(TimeSpan.FromSeconds(15)); - _logger.Debug("Pinging KF and Pusher"); - _kfClient.SendMessage("/ping"); + _logger.Debug("Pinging KF"); + KfClient.SendMessage("/ping"); _kickClient.SendPusherPing(); if (_initialStartCooldown) _initialStartCooldown = false; var inactivityTime = DateTime.Now - _lastKfEvent; @@ -300,80 +315,55 @@ public class KickBot if (inactivityTime.TotalMinutes > 10) { _logger.Error("Forcing reconnection as bot is completely dead"); - _kfClient.Reconnect().Wait(_cancellationToken); + KfClient.Reconnect().Wait(_cancellationToken); } + _logger.Debug("Polling Bossman's Howl.gg stats"); + _howlgg.GetUserInfo("951905"); } } private async Task RefreshXfToken() { - var cookie = await KfTokenService.FetchSessionTokenAsync(_config.KfDomain, _config.KfUsername, _config.KfPassword, - _config.ChromiumPath, _config.Proxy); + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.KiwiFarmsDomain, + BuiltIn.Keys.KiwiFarmsUsername, BuiltIn.Keys.KiwiFarmsPassword, BuiltIn.Keys.KiwiFarmsChromiumPath, + BuiltIn.Keys.Proxy]).Result; + var cookie = await KfTokenService.FetchSessionTokenAsync(settings[BuiltIn.Keys.KiwiFarmsDomain].Value!, + settings[BuiltIn.Keys.KiwiFarmsUsername].Value!, settings[BuiltIn.Keys.KiwiFarmsPassword].Value!, + settings[BuiltIn.Keys.KiwiFarmsChromiumPath].Value!, settings[BuiltIn.Keys.Proxy].Value); _logger.Debug($"FetchSessionTokenAsync returned {cookie}"); _xfSessionToken = cookie; + 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); + 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); + 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]) + .Result; _lastKfEvent = DateTime.Now; _logger.Debug($"Received {messages.Count} message(s)"); foreach (var message in messages) { _logger.Info($"KF ({message.MessageDate.ToLocalTime():HH:mm:ss}) <{message.Author.Username}> {message.Message}"); - if (_config.EnableGambaSeshDetect && !_initialStartCooldown && message.Author.Id == _config.GambaSeshUserId && !_gambaSeshPresent) + if (settings[BuiltIn.Keys.GambaSeshDetectEnabled].Value == "true" && !_initialStartCooldown && message.Author.Id == Convert.ToInt32(settings[BuiltIn.Keys.GambaSeshUserId].Value) && !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; + GambaSeshPresent = true; } if (!_seenMsgIds.Contains(message.MessageId) && !_initialStartCooldown) { - if (message.MessageRaw.StartsWith("!time")) - { - var bmt = new DateTimeOffset(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, - TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time")), TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time").BaseUtcOffset); - _sendChatMessage($"It's currently {bmt:h:mm:ss tt} BMT"); - } - else if (message.MessageRaw.StartsWith("!twisted")) - { - _sendChatMessage("🦍 🗣 GET IT TWISTED 🌪 , GAMBLE ✅ . PLEASE START GAMBLING 👍 . GAMBLING IS AN INVESTMENT 🎰 AND AN INVESTMENT ONLY 👍 . YOU WILL PROFIT 💰 , YOU WILL WIN ❗ ️. YOU WILL DO ALL OF THAT 💯 , YOU UNDERSTAND ⁉ ️ YOU WILL BECOME A BILLIONAIRE 💵 📈 AND REBUILD YOUR FUCKING LIFE 🤯"); - } - else if (message.MessageRaw.StartsWith("!insanity")) - { - // ReSharper disable once StringLiteralTypo - _sendChatMessage("definition of insanity = doing the same thing over and over and over excecting a different result, and heres my dumbass trying to get rich every day and losing everythign i fucking touch every fucking time FUCK this bullshit FUCK MY LIEFdefinition of insanity = doing the same thing over and over and over excecting a different result, and heres my dumbass trying to get rich every day and losing everythign i fucking touch every fucking time FUCK this bullshit FUCK MY LIEF"); - } - else if (message.MessageRaw.StartsWith("!helpme")) - { - _sendChatMessage("[img]https://i.postimg.cc/fTw6tGWZ/ineedmoneydumbfuck.png[/img]", true); - } - else if (message.MessageRaw.StartsWith("!whois")) - { - var lookup = WebUtility.HtmlDecode(message.MessageRaw.Replace("!whois ", string.Empty) - .TrimStart('@').TrimEnd(' ').TrimEnd(',')); - if (_userIdMapping.ContainsKey(lookup)) - { - _sendChatMessage($"{lookup}'s ID is {_userIdMapping[lookup]}", true); - } - else - { - _sendChatMessage("Don't know they who they are.", true); - } - } - else if (message.MessageRaw == "!sent") - { - _sendChatMessage("[img]https://i.ibb.co/GHq7hb1/4373-g-N5-HEH2-Hkc.png[/img]", true); - } + _logger.Debug("Passing message to command interface"); + _botCommands.ProcessMessage(message); } else { @@ -383,21 +373,24 @@ public class KickBot } } - private void _sendChatMessage(string message, bool bypassSeshDetect = false) + public void SendChatMessage(string message, bool bypassSeshDetect = false) { - if (_config.SuppressChatMessages) + var settings = Helpers + .GetMultipleValues([BuiltIn.Keys.KiwiFarmsSuppressChatMessages, BuiltIn.Keys.GambaSeshDetectEnabled]) + .Result; + if (settings[BuiltIn.Keys.KiwiFarmsSuppressChatMessages].Value == "true") { _logger.Info("Not sending message as SuppressChatMessages is enabled"); _logger.Info($"Message was: {message}"); return; } - if (_gambaSeshPresent && _config.EnableGambaSeshDetect && !bypassSeshDetect) + if (GambaSeshPresent && settings[BuiltIn.Keys.GambaSeshDetectEnabled].Value == "true" && !bypassSeshDetect) { _logger.Info($"Not sending message '{message}' as GambaSesh is present"); return; } - _kfClient.SendMessage(message); + KfClient.SendMessage(message); } private void OnKickChatMessage(object sender, KickModels.ChatMessageEventModel? e) @@ -407,59 +400,81 @@ public class KickBot _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]{_config.KickIcon}[/img] BossmanJack: {e.Content.TranslateKickEmotes()}"); + 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.Now; _logger.Debug($"Received {users.Count} user join events"); + using var db = new ApplicationDbContext(); foreach (var user in users) { - if (user.Id == _config.GambaSeshUserId && _config.EnableGambaSeshDetect) + if (user.Id == Convert.ToInt32(settings[BuiltIn.Keys.GambaSeshUserId].Value) && settings[BuiltIn.Keys.GambaSeshDetectEnabled].Value == "true") { _logger.Info("GambaSesh is now present"); - _gambaSeshPresent = true; + GambaSeshPresent = true; } _logger.Info($"{user.Username} joined!"); - if (!_userIdMapping.ContainsKey(user.Username)) + + var userDb = db.Users.FirstOrDefault(u => u.KfId == user.Id); + if (userDb == null) { - _userIdMapping.Add(user.Username, user.Id); + db.Users.Add(new UserDbModel { KfId = user.Id, KfUsername = user.Username }); + _logger.Debug("Adding user to DB"); + continue; + } + // Detect a username change + if (userDb.KfUsername != user.Username) + { + _logger.Debug("Username has updated, updating DB"); + userDb.KfUsername = user.Username; + db.SaveChanges(); } } + + db.SaveChanges(); } private void OnUsersParted(object sender, List userIds) { + var settings = Helpers.GetMultipleValues([BuiltIn.Keys.GambaSeshUserId, BuiltIn.Keys.GambaSeshDetectEnabled]) + .Result; _lastKfEvent = DateTime.Now; - if (userIds.Contains(_config.GambaSeshUserId) && _config.EnableGambaSeshDetect) + if (userIds.Contains(Convert.ToInt32(settings[BuiltIn.Keys.GambaSeshUserId].Value)) && settings[BuiltIn.Keys.GambaSeshDetectEnabled].Value == "true") { _logger.Info("GambaSesh is no longer present"); - _gambaSeshPresent = false; + GambaSeshPresent = false; } } private void OnKfWsDisconnected(object sender, DisconnectionInfo disconnectionInfo) { _logger.Error($"Sneedchat disconnected due to {disconnectionInfo.Type}"); - _logger.Error(JsonSerializer.Serialize(disconnectionInfo)); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); } private void OnKfWsReconnected(object sender, ReconnectionInfo reconnectionInfo) { + var roomId = Convert.ToInt32(Helpers.GetValue(BuiltIn.Keys.KiwiFarmsRoomId).Result.Value); _logger.Error($"Sneedchat reconnected due to {reconnectionInfo.Type}"); _logger.Info("Resetting GambaSesh presence so it can resync if he crashed while the bot was DC'd"); - _gambaSeshPresent = false; - _logger.Info($"Rejoining {_config.KfChatRoomId}"); - _kfClient.JoinRoom(_config.KfChatRoomId); + GambaSeshPresent = false; + _logger.Info($"Rejoining {roomId}"); + KfClient.JoinRoom(roomId); } private void OnPusherWsReconnected(object sender, ReconnectionInfo reconnectionInfo) { _logger.Error($"Pusher reconnected due to {reconnectionInfo.Type}"); - foreach (var channel in _config.PusherChannels) + var channels = Helpers.GetValue(BuiltIn.Keys.PusherChannels).Result.Value ?? ""; + foreach (var channel in channels.Split(',')) { _logger.Info($"Rejoining {channel}"); _kickClient.SendPusherSubscribe(channel); diff --git a/KfChatDotNetKickBot/Migrations/20240714132259_Initial.Designer.cs b/KfChatDotNetKickBot/Migrations/20240714132259_Initial.Designer.cs new file mode 100644 index 0000000..512d677 --- /dev/null +++ b/KfChatDotNetKickBot/Migrations/20240714132259_Initial.Designer.cs @@ -0,0 +1,82 @@ +// +using System; +using KfChatDotNetKickBot; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace KfChatDotNetKickBot.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240714132259_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("JuicedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Juicers"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.UserDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ignored") + .HasColumnType("INTEGER"); + + b.Property("KfId") + .HasColumnType("INTEGER"); + + b.Property("KfUsername") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserRight") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + { + b.HasOne("KfChatDotNetKickBot.Models.DbModels.UserDbModel", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/KfChatDotNetKickBot/Migrations/20240714132259_Initial.cs b/KfChatDotNetKickBot/Migrations/20240714132259_Initial.cs new file mode 100644 index 0000000..3200e7a --- /dev/null +++ b/KfChatDotNetKickBot/Migrations/20240714132259_Initial.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace KfChatDotNetKickBot.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + KfUsername = table.Column(type: "TEXT", nullable: false), + KfId = table.Column(type: "INTEGER", nullable: false), + UserRight = table.Column(type: "INTEGER", nullable: false), + Ignored = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Juicers", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "INTEGER", nullable: false), + Amount = table.Column(type: "REAL", nullable: false), + JuicedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Juicers", x => x.Id); + table.ForeignKey( + name: "FK_Juicers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Juicers_UserId", + table: "Juicers", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Juicers"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/KfChatDotNetKickBot/Migrations/20240716193646_Settings.Designer.cs b/KfChatDotNetKickBot/Migrations/20240716193646_Settings.Designer.cs new file mode 100644 index 0000000..7c117c9 --- /dev/null +++ b/KfChatDotNetKickBot/Migrations/20240716193646_Settings.Designer.cs @@ -0,0 +1,115 @@ +// +using System; +using KfChatDotNetKickBot; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace KfChatDotNetKickBot.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240716193646_Settings")] + partial class Settings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("JuicedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Juicers"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.SettingDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Default") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSecret") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Regex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.UserDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ignored") + .HasColumnType("INTEGER"); + + b.Property("KfId") + .HasColumnType("INTEGER"); + + b.Property("KfUsername") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserRight") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + { + b.HasOne("KfChatDotNetKickBot.Models.DbModels.UserDbModel", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/KfChatDotNetKickBot/Migrations/20240716193646_Settings.cs b/KfChatDotNetKickBot/Migrations/20240716193646_Settings.cs new file mode 100644 index 0000000..8a82052 --- /dev/null +++ b/KfChatDotNetKickBot/Migrations/20240716193646_Settings.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace KfChatDotNetKickBot.Migrations +{ + /// + public partial class Settings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Settings", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Key = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: true), + Regex = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + Default = table.Column(type: "TEXT", nullable: true), + IsSecret = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Settings", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Settings"); + } + } +} diff --git a/KfChatDotNetKickBot/Migrations/20240717110408_Howlgg.Designer.cs b/KfChatDotNetKickBot/Migrations/20240717110408_Howlgg.Designer.cs new file mode 100644 index 0000000..ea035aa --- /dev/null +++ b/KfChatDotNetKickBot/Migrations/20240717110408_Howlgg.Designer.cs @@ -0,0 +1,148 @@ +// +using System; +using KfChatDotNetKickBot; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace KfChatDotNetKickBot.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240717110408_Howlgg")] + partial class Howlgg + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.HowlggBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bet") + .HasColumnType("INTEGER"); + + b.Property("BetId") + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Game") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("Profit") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("HowlggBets"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("JuicedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Juicers"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.SettingDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Default") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSecret") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Regex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.UserDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ignored") + .HasColumnType("INTEGER"); + + b.Property("KfId") + .HasColumnType("INTEGER"); + + b.Property("KfUsername") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserRight") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + { + b.HasOne("KfChatDotNetKickBot.Models.DbModels.UserDbModel", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/KfChatDotNetKickBot/Migrations/20240717110408_Howlgg.cs b/KfChatDotNetKickBot/Migrations/20240717110408_Howlgg.cs new file mode 100644 index 0000000..c03546b --- /dev/null +++ b/KfChatDotNetKickBot/Migrations/20240717110408_Howlgg.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace KfChatDotNetKickBot.Migrations +{ + /// + public partial class Howlgg : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "HowlggBets", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "INTEGER", nullable: false), + BetId = table.Column(type: "INTEGER", nullable: false), + GameId = table.Column(type: "INTEGER", nullable: false), + Bet = table.Column(type: "INTEGER", nullable: false), + Profit = table.Column(type: "INTEGER", nullable: false), + Date = table.Column(type: "TEXT", nullable: false), + Game = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HowlggBets", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "HowlggBets"); + } + } +} diff --git a/KfChatDotNetKickBot/Migrations/ApplicationDbContextModelSnapshot.cs b/KfChatDotNetKickBot/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..14e2d24 --- /dev/null +++ b/KfChatDotNetKickBot/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,145 @@ +// +using System; +using KfChatDotNetKickBot; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace KfChatDotNetKickBot.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.HowlggBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bet") + .HasColumnType("INTEGER"); + + b.Property("BetId") + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Game") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("Profit") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("HowlggBets"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("JuicedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Juicers"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.SettingDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Default") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSecret") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Regex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.UserDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ignored") + .HasColumnType("INTEGER"); + + b.Property("KfId") + .HasColumnType("INTEGER"); + + b.Property("KfUsername") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserRight") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + { + b.HasOne("KfChatDotNetKickBot.Models.DbModels.UserDbModel", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/KfChatDotNetKickBot/Models/BuiltInSettingsModel.cs b/KfChatDotNetKickBot/Models/BuiltInSettingsModel.cs new file mode 100644 index 0000000..f16d9e8 --- /dev/null +++ b/KfChatDotNetKickBot/Models/BuiltInSettingsModel.cs @@ -0,0 +1,13 @@ +namespace KfChatDotNetKickBot.Models; + +public class BuiltInSettingsModel +{ + // Model here largely maps to what's in SettingDbModel, the idea is that there's a set of built-in settings that get + // populated when migrating old JSON configs and updated on start if there's a schema change (e.g. regex changed) + public required string Key { get; set; } + public required string Regex { get; set; } + public required string Description { get; set; } + public string? Default { get; set; } + public required bool IsSecret { get; set; } + +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Models/ConfigModel.cs b/KfChatDotNetKickBot/Models/ConfigModel.cs index 63d4710..68d99c1 100644 --- a/KfChatDotNetKickBot/Models/ConfigModel.cs +++ b/KfChatDotNetKickBot/Models/ConfigModel.cs @@ -1,5 +1,6 @@ namespace KfChatDotNetKickBot.Models; +[Obsolete] public class ConfigModel { public Uri PusherEndpoint { get; set; } = diff --git a/KfChatDotNetKickBot/Models/DbModels/HowlggBetsDbModel.cs b/KfChatDotNetKickBot/Models/DbModels/HowlggBetsDbModel.cs new file mode 100644 index 0000000..aa3a91f --- /dev/null +++ b/KfChatDotNetKickBot/Models/DbModels/HowlggBetsDbModel.cs @@ -0,0 +1,17 @@ +namespace KfChatDotNetKickBot.Models.DbModels; + +public class HowlggBetsDbModel +{ + public int Id { get; set; } + public required int UserId { get; set; } + // Per-user bet ID, counts up based on total # of bets + public required int BetId { get; set; } + // Global bet ID + public required long GameId { get; set; } + // Cents + public required long Bet { get; set; } + // Cents + public required long Profit { get; set; } + public required DateTimeOffset Date { get; set; } + public required string Game { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Models/DbModels/JuicerDbModel.cs b/KfChatDotNetKickBot/Models/DbModels/JuicerDbModel.cs new file mode 100644 index 0000000..ca23785 --- /dev/null +++ b/KfChatDotNetKickBot/Models/DbModels/JuicerDbModel.cs @@ -0,0 +1,9 @@ +namespace KfChatDotNetKickBot.Models.DbModels; + +public class JuicerDbModel +{ + public int Id { get; set; } + public required UserDbModel User { get; set; } + public float Amount { get; set; } + public DateTimeOffset JuicedAt { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Models/DbModels/SettingDbModel.cs b/KfChatDotNetKickBot/Models/DbModels/SettingDbModel.cs new file mode 100644 index 0000000..80e5177 --- /dev/null +++ b/KfChatDotNetKickBot/Models/DbModels/SettingDbModel.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace KfChatDotNetKickBot.Models.DbModels; + +public class SettingDbModel +{ + public int Id { get; set; } + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public required string Key { get; set; } + public string? Value { get; set; } + // For validation + public required string Regex { get; set; } = @"\S+"; + // Friendly descriptor for the setting, e.g. "BossmanJack's howl.gg ID" + public required string Description { get; set; } + // Default to use when constructing the setting and nothing is supplied + public string? Default { get; set; } = null; + // Prevents the value from being revealed to Sneedchat when queried by an admin + public bool IsSecret { get; set; } = false; +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Models/DbModels/UserDbModel.cs b/KfChatDotNetKickBot/Models/DbModels/UserDbModel.cs new file mode 100644 index 0000000..6deb5dd --- /dev/null +++ b/KfChatDotNetKickBot/Models/DbModels/UserDbModel.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; + +namespace KfChatDotNetKickBot.Models.DbModels; + +public class UserDbModel +{ + public int Id { get; set; } + public required string KfUsername { get; set; } + public int KfId { get; set; } + public UserRight UserRight { get; set; } = UserRight.Guest; + public bool Ignored { get; set; } = false; +} + +public enum UserRight +{ + Admin = 1000, + [Description("True and Honest")] + TrueAndHonest = 100, + Guest = 10, + Loser = 0 +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Models/HowlggModels.cs b/KfChatDotNetKickBot/Models/HowlggModels.cs new file mode 100644 index 0000000..63d3340 --- /dev/null +++ b/KfChatDotNetKickBot/Models/HowlggModels.cs @@ -0,0 +1,50 @@ +using System.Text.Json.Serialization; + +namespace KfChatDotNetKickBot.Models; + +public class HowlggBetHistoryResponseModel +{ + [JsonPropertyName("user")] + public required HowlggUserModel User { get; set; } + [JsonPropertyName("history")] + public required HowlggHistoryModel History { get; set; } +} + +public class HowlggUserModel +{ + [JsonPropertyName("id")] + public required int Id { get; set; } + [JsonPropertyName("name")] + public required string Name { get; set; } + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + [JsonPropertyName("netProfit")] + public long NetProfit { get; set; } +} + +public class HowlggHistoryModel +{ + [JsonPropertyName("profit")] + public required long Profit { get; set; } + [JsonPropertyName("cumulative")] + public required long Cumulative { get; set; } + [JsonPropertyName("data")] + public required List Data { get; set; } +} + +public class HowlggHistoryDataModel +{ + [JsonPropertyName("id")] + public required int Id { get; set; } + [JsonPropertyName("bet")] + public required long Bet { get; set; } + [JsonPropertyName("date")] + // For some reason it has a +2 offset + public required DateTimeOffset Date { get; set; } + [JsonPropertyName("game")] + public required string Game { get; set; } + [JsonPropertyName("gameId")] + public required long GameId { get; set; } + [JsonPropertyName("profit")] + public required long Profit { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Models/PocketWatchModel.cs b/KfChatDotNetKickBot/Models/PocketWatchModel.cs new file mode 100644 index 0000000..0deb6bb --- /dev/null +++ b/KfChatDotNetKickBot/Models/PocketWatchModel.cs @@ -0,0 +1,26 @@ +namespace KfChatDotNetKickBot.Models; + +public class PocketWatchModel +{ + public required string Network { get; set; } + public required string Address { get; set; } + public required string Label { get; set; } + public required bool BypassGambaSeshPresenceDetection { get; set; } + public required int CheckIntervalSec { get; set; } + // Used internally to detect new transactions + public required DateTime LastChecked { get; set; } = DateTime.Now; +} + +public class PocketWatchEventModel +{ + public required string TransactionHash { get; set; } + public required DateTimeOffset Time { get; set; } + public required string Currency { get; set; } + public required string Effect { get; set; } + public required string Network { get; set; } + public required string Address { get; set; } + public required long Balance { get; set; } + public required float UsdRate { get; set; } + public required bool IsMempool { get; set; } + public required PocketWatchModel PocketWatch { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Models/ThreeXplModels.cs b/KfChatDotNetKickBot/Models/ThreeXplModels.cs new file mode 100644 index 0000000..4311f62 --- /dev/null +++ b/KfChatDotNetKickBot/Models/ThreeXplModels.cs @@ -0,0 +1,48 @@ +using System.Text.Json.Serialization; + +namespace KfChatDotNetKickBot.Models; + +public class ThreeXplEventModel +{ + [JsonPropertyName("block")] + public int? Block { get; set; } + [JsonPropertyName("transaction")] + public required string TransactionHash { get; set; } + [JsonPropertyName("sort_key")] + public int? SortKey { get; set; } + [JsonPropertyName("time")] + public DateTimeOffset? Time { get; set; } + [JsonPropertyName("currency")] + public required string Currency { get; set; } + [JsonPropertyName("effect")] + public required string Effect { get; set; } + [JsonPropertyName("failed")] + public bool? Failed { get; set; } + [JsonPropertyName("extra")] + public string? Extra { get; set; } +} + +public class ThreeXplCurrencyModel +{ + [JsonPropertyName("name")] + public required string Name { get; set; } + [JsonPropertyName("type")] + public string? Type { get; set; } + [JsonPropertyName("symbol")] + public required string Symbol { get; set; } + [JsonPropertyName("decimals")] + public required int Decimals { get; set; } + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +public class ThreeXplAddressModel +{ + [JsonPropertyName("address")] + public required string Address { get; set; } + [JsonPropertyName("balances")] + public required Dictionary Balances { get; set; } + [JsonPropertyName("events")] + public required Dictionary Events { get; set; } +} + diff --git a/KfChatDotNetKickBot/NLog.config b/KfChatDotNetKickBot/NLog.config index cc779d9..c2838cc 100644 --- a/KfChatDotNetKickBot/NLog.config +++ b/KfChatDotNetKickBot/NLog.config @@ -10,6 +10,6 @@ - + diff --git a/KfChatDotNetKickBot/Program.cs b/KfChatDotNetKickBot/Program.cs index 981e8e5..a1377db 100644 --- a/KfChatDotNetKickBot/Program.cs +++ b/KfChatDotNetKickBot/Program.cs @@ -1,5 +1,6 @@ -using System.Net; -using System.Text; +using System.Text; +using KfChatDotNetKickBot.Settings; +using Microsoft.EntityFrameworkCore; using NLog; namespace KfChatDotNetKickBot @@ -8,6 +9,15 @@ namespace KfChatDotNetKickBot { static void Main(string[] args) { + var logger = LogManager.GetCurrentClassLogger(); + logger.Info("Opening up DB to perform a migration (if one is needed)"); + using var db = new ApplicationDbContext(); + db.Database.Migrate(); + logger.Info("Migration done. Syncing bultin settings keys"); + BuiltIn.SyncSettingsWithDb().Wait(); + logger.Info("Migrating settings from config.json (if needed)"); + BuiltIn.MigrateJsonSettingsToDb().Wait(); + logger.Info("Handing over to bot now"); Console.OutputEncoding = Encoding.UTF8; new KickBot(); } diff --git a/KfChatDotNetKickBot/Services/BotCommands.cs b/KfChatDotNetKickBot/Services/BotCommands.cs new file mode 100644 index 0000000..e14e6b1 --- /dev/null +++ b/KfChatDotNetKickBot/Services/BotCommands.cs @@ -0,0 +1,88 @@ +using KfChatDotNetKickBot.Commands; +using KfChatDotNetWsClient.Models.Events; +using NLog; + +namespace KfChatDotNetKickBot.Services; + +// Took one look at GambaSesh's code, and it made my head explode +// This implementation is inspired by similar bot I wrote years ago +internal class BotCommands +{ + private KickBot _bot; + private Logger _logger = LogManager.GetCurrentClassLogger(); + private char CommandPrefix = '!'; + private IEnumerable Commands; + private CancellationToken _cancellationToken; + private List _commandTasks = []; + + internal BotCommands(KickBot bot, CancellationToken? ctx = null) + { + _cancellationToken = ctx ?? CancellationToken.None; + _bot = bot; + var interfaceType = typeof(ICommand); + Commands = + AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(x => x.GetTypes()) + .Where(x => interfaceType.IsAssignableFrom(x) && x is { IsInterface: false, IsAbstract: false }) + .Select(Activator.CreateInstance).Cast(); + + foreach (var command in Commands) + { + _logger.Debug($"Found command {command.GetType().Name}"); + } + } + + internal void ProcessMessage(MessageModel message) + { + if (string.IsNullOrEmpty(message.MessageRaw)) + { + return; + } + + if (!message.MessageRaw.StartsWith(CommandPrefix)) + { + return; + } + + var messageTrimmed = message.MessageRaw.TrimStart(CommandPrefix); + foreach (var command in Commands) + { + foreach (var regex in command.Patterns) + { + var match = regex.Match(messageTrimmed); + if (!match.Success) continue; + _logger.Debug($"Message matches {regex}"); + using var db = new ApplicationDbContext(); + var user = db.Users.FirstOrDefault(u => u.KfId == message.Author.Id); + // This should never happen as brand-new users are created upon join + if (user == null) return; + if (user.UserRight < command.RequiredRight) + { + _bot.SendChatMessage($"@{message.Author.Username}, you do not have access to use this command.", true); + break; + } + var task = Task.Run(() => command.RunCommand(_bot, message, match.Groups, _cancellationToken), _cancellationToken); + _commandTasks.Add(task); + } + } + + // Check on the state of the tasks, there's no way to know what error they produce if they failed otherwise + List removals = []; + foreach (var task in _commandTasks) + { + if (!task.IsCompleted) continue; + if (task.IsFaulted) + { + _logger.Error("Command task failed at some point"); + _logger.Error(task.Exception); + } + + removals.Add(task); + } + // .NET doesn't support modifying a collection you're iterating over + foreach (var removal in removals) + { + _commandTasks.Remove(removal); + } + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Services/Discord.cs b/KfChatDotNetKickBot/Services/Discord.cs index 7d48aec..36d15c9 100644 --- a/KfChatDotNetKickBot/Services/Discord.cs +++ b/KfChatDotNetKickBot/Services/Discord.cs @@ -7,7 +7,7 @@ using Websocket.Client; namespace KfChatDotNetKickBot.Services; -public class DiscordService : IDisposable +public class DiscordService { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private WebsocketClient _wsClient; @@ -111,8 +111,14 @@ public class DiscordService : IDisposable private void WsDisconnection(DisconnectionInfo disconnectionInfo) { _logger.Error($"Client disconnected from Discord (or never successfully connected). Type is {disconnectionInfo.Type}"); - _logger.Error(JsonSerializer.Serialize(disconnectionInfo)); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); OnWsDisconnection?.Invoke(this, disconnectionInfo); + if (disconnectionInfo.Type == DisconnectionType.ByServer) + { + _logger.Info("Forcing reconnection as the type was ByServer"); + _wsClient.Reconnect().Wait(_cancellationToken); + } } private void WsReconnection(ReconnectionInfo reconnectionInfo) @@ -144,7 +150,7 @@ public class DiscordService : IDisposable if (packet.OpCode == 10) { - _logger.Info("Discord op code 10 (hello) sent. Setting up heartbeat timer and sending init"); + _logger.Info("Discord op code 10 (hello) received. Setting up heartbeat timer and sending init"); _logger.Info("Sending connection_init"); var initPayload = "{\"op\":2,\"d\":{\"token\":\"" + _authorization + "\",\"capabilities\":30717,\"properties\":" + @@ -205,14 +211,6 @@ public class DiscordService : IDisposable _logger.Error("--- End of JSON Payload ---"); } } - - public void Dispose() - { - _logger.Info("Disposing Discord"); - _wsClient.Dispose(); - _pingCts.Cancel(); - GC.SuppressFinalize(this); - } } public class DiscordPacketModel diff --git a/KfChatDotNetKickBot/Services/Howlgg.cs b/KfChatDotNetKickBot/Services/Howlgg.cs new file mode 100644 index 0000000..4430240 --- /dev/null +++ b/KfChatDotNetKickBot/Services/Howlgg.cs @@ -0,0 +1,178 @@ +using System.Net; +using System.Net.Http.Json; +using System.Net.WebSockets; +using System.Text.Json; +using KfChatDotNetKickBot.Models; +using NLog; +using Websocket.Client; + +namespace KfChatDotNetKickBot.Services; + +public class Howlgg +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + 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; + 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) + { + _proxy = proxy; + if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _logger.Info("Howlgg WebSocket client created"); + } + + public async Task StartWsClient() + { + _logger.Debug("StartWsClient() called, creating client"); + await CreateWsClient(); + } + + private async Task CreateWsClient() + { + var factory = new Func(() => + { + var clientWs = new ClientWebSocket(); + clientWs.Options.SetRequestHeader("Origin", "https://howl.gg"); + clientWs.Options.SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0"); + if (_proxy == null) return clientWs; + _logger.Debug($"Using proxy address {_proxy}"); + clientWs.Options.Proxy = new WebProxy(_proxy); + return clientWs; + }); + + var client = new WebsocketClient(_wsUri, factory) + { + ReconnectTimeout = TimeSpan.FromSeconds(_reconnectTimeout) + }; + + client.ReconnectionHappened.Subscribe(WsReconnection); + client.MessageReceived.Subscribe(WsMessageReceived); + client.DisconnectionHappened.Subscribe(WsDisconnection); + + _wsClient = client; + + _logger.Debug("Websocket client has been built, about to start"); + await client.Start(); + _logger.Debug("Websocket client started!"); + } + + public bool IsConnected() + { + return _wsClient is { IsRunning: true }; + } + + public void GetUserInfo(string userId) + { + var packet = "42/main,0[\"getUserInfo\",{\"userOrSteamId\":\"" + userId + "\",\"interval\":\"lifetime\"}]"; + _logger.Debug($"Sending packet: {packet}"); + _wsClient.SendInstant(packet).Wait(_cancellationToken); + } + + private async Task HeartbeatTimer() + { + using var timer = new PeriodicTimer(_heartbeatInterval); + while (await timer.WaitForNextTickAsync(_pingCts.Token)) + { + if (_wsClient == null) + { + _logger.Debug("_wsClient doesn't exist yet, not going to try ping"); + continue; + } + if (!IsConnected()) + { + _logger.Info("Not connected not going to try send a ping actually"); + continue; + } + + _logger.Debug("Sending Howl.gg ping packet"); + await _wsClient.SendInstant("2"); + } + } + + private void WsDisconnection(DisconnectionInfo disconnectionInfo) + { + _logger.Error($"Client disconnected from Howl.gg (or never successfully connected). Type is {disconnectionInfo.Type}"); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); + OnWsDisconnection?.Invoke(this, disconnectionInfo); + if (disconnectionInfo.Type == DisconnectionType.ByServer) + { + _logger.Info("Forcing reconnection as the type was ByServer"); + _wsClient.Reconnect().Wait(_cancellationToken); + } + } + + private void WsReconnection(ReconnectionInfo reconnectionInfo) + { + _logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}"); + } + + private void WsMessageReceived(ResponseMessage message) + { + if (message.Text == null) + { + _logger.Info("Howl.gg sent a null message"); + return; + } + _logger.Trace($"Received event from Howl.gg: {message.Text}"); + + try + { + var packetType = message.Text.Split('/')[0]; + if (packetType == "3") + { + _logger.Debug("Received pong from Howl.gg"); + return; + } + + // For some reason there's no / for the initial connection + if (packetType.StartsWith("0{")) + { + // Received on initial connection + var packetData = JsonSerializer.Deserialize(message.Text.TrimStart('0')); + _heartbeatInterval = TimeSpan.FromMilliseconds(packetData.GetProperty("pingInterval").GetInt32()); + _heartbeatTask?.Dispose(); + _heartbeatTask = Task.Run(HeartbeatTimer, _cancellationToken); + _logger.Info("Received connection packet from Howl.gg. Setting up heartbeat timer"); + return; + } + + if (message.Text == "40") + { + _logger.Trace("Ready to subscribe, sending main subscription"); + _wsClient.SendInstant("40/main,").Wait(_cancellationToken); + // To indicate successful subscription it echoes back the channel to you + return; + } + + if (packetType == "43") + { + // Bet History + 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) + { + _logger.Error("Failed to handle message from Howl.gg"); + _logger.Error(e); + _logger.Error("--- Payload ---"); + _logger.Error(message.Text); + _logger.Error("--- End of Payload ---"); + } + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Services/Shuffle.cs b/KfChatDotNetKickBot/Services/Shuffle.cs index 695aae7..547ecd0 100644 --- a/KfChatDotNetKickBot/Services/Shuffle.cs +++ b/KfChatDotNetKickBot/Services/Shuffle.cs @@ -8,7 +8,7 @@ using Websocket.Client; namespace KfChatDotNetKickBot.Services; -public class Shuffle : IDisposable +public class Shuffle { private Logger _logger = LogManager.GetCurrentClassLogger(); private WebsocketClient _wsClient; @@ -99,8 +99,14 @@ public class Shuffle : IDisposable private void WsDisconnection(DisconnectionInfo disconnectionInfo) { _logger.Error($"Client disconnected from Shuffle (or never successfully connected). Type is {disconnectionInfo.Type}"); - _logger.Error(JsonSerializer.Serialize(disconnectionInfo)); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); OnWsDisconnection?.Invoke(this, disconnectionInfo); + if (disconnectionInfo.Type == DisconnectionType.ByServer) + { + _logger.Info("Forcing reconnection as the type was ByServer"); + _wsClient.Reconnect().Wait(_cancellationToken); + } } private void WsReconnection(ReconnectionInfo reconnectionInfo) @@ -120,7 +126,7 @@ public class Shuffle : IDisposable _logger.Info("Shuffle sent a null message"); return; } - _logger.Debug($"Received event from Shuffle: {message.Text}"); + _logger.Trace($"Received event from Shuffle: {message.Text}"); try { @@ -135,7 +141,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.Send(payload); + _wsClient.SendInstant(payload).Wait(_cancellationToken); return; } @@ -148,7 +154,6 @@ public class Shuffle : IDisposable // GAMBA if (packetType == "next") { - _logger.Debug("Got a bet! Deserializing it"); var bet = packet.GetProperty("payload").GetProperty("data").GetProperty("latestBetUpdated") .Deserialize(); if (bet == null) @@ -156,7 +161,6 @@ public class Shuffle : IDisposable _logger.Error("Caught a null before invoking bet event"); throw new NullReferenceException("Caught a null before invoking bet event"); } - _logger.Debug("Invoking event"); OnLatestBetUpdated?.Invoke(this, bet); return; } @@ -212,11 +216,4 @@ public class Shuffle : IDisposable } public class ShuffleUserNotFoundException : Exception; - - public void Dispose() - { - _pingCts.Cancel(); - _wsClient.Dispose(); - GC.SuppressFinalize(this); - } } \ No newline at end of file diff --git a/KfChatDotNetKickBot/Services/ThreeXplPocketWatch.cs b/KfChatDotNetKickBot/Services/ThreeXplPocketWatch.cs new file mode 100644 index 0000000..383d03d --- /dev/null +++ b/KfChatDotNetKickBot/Services/ThreeXplPocketWatch.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using KfChatDotNetKickBot.Models; +using NLog; + +namespace KfChatDotNetKickBot.Services; + +public class ThreeXplPocketWatch +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + private string _3xplToken = "3A0_t3st3xplor3rpub11cb3t4efcd21748a5e"; + private string? _proxy; + private List _addresses = []; + private CancellationToken _cancellationToken = CancellationToken.None; + public delegate void OnPocketWatchEventHandler(object sender, PocketWatchEventModel e); + public event OnPocketWatchEventHandler OnPocketWatchEvent; + + public ThreeXplPocketWatch(string? proxy = null, CancellationToken? cancellationToken = null) + { + _logger.Info("Starting the pocket watch"); + _proxy = proxy; + if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + } + + private async Task CheckAddress(PocketWatchModel addy) + { + _logger.Debug($"Getting data for {addy.Network}/{addy.Address}"); + var data = await GetAddress(addy.Network, addy.Address); + _logger.Debug("Received following data"); + _logger.Debug(data.GetRawText); + var events = data.GetProperty("events").Deserialize>>(); + if (events == null) throw new InvalidOperationException(); + foreach (var chain in events.Keys) + { + + } + } + + public async Task GetAddress(string network, string address) + { + var url = + $"https://api.3xpl.com/{network}/address/{address}?data=address,balances,events,mempool&from=all&token=3A0_t3st3xplor3rpub11cb3t4efcd21748a5e&library=currencies,rates(usd)"; + _logger.Debug($"Retrieving {url}"); + var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }; + if (_proxy != null) + { + handler.UseProxy = true; + handler.Proxy = new WebProxy(_proxy); + _logger.Debug($"Configured to use proxy {_proxy}"); + } + + using var client = new HttpClient(handler); + var response = await client.GetFromJsonAsync(url, _cancellationToken); + return response; + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Services/Twitch.cs b/KfChatDotNetKickBot/Services/Twitch.cs index 24a0f46..25e4cb9 100644 --- a/KfChatDotNetKickBot/Services/Twitch.cs +++ b/KfChatDotNetKickBot/Services/Twitch.cs @@ -84,7 +84,13 @@ public class Twitch private void WsDisconnection(DisconnectionInfo disconnectionInfo) { _logger.Error($"Client disconnected from the chat (or never successfully connected). Type is {disconnectionInfo.Type}"); - _logger.Error(JsonSerializer.Serialize(disconnectionInfo)); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); + if (disconnectionInfo.Type == DisconnectionType.ByServer) + { + _logger.Info("Forcing reconnection as the type was ByServer"); + _wsClient.Reconnect().Wait(_cancellationToken); + } } private void WsReconnection(ReconnectionInfo reconnectionInfo) diff --git a/KfChatDotNetKickBot/Services/TwitchChat.cs b/KfChatDotNetKickBot/Services/TwitchChat.cs index 96c23b7..dc48c5b 100644 --- a/KfChatDotNetKickBot/Services/TwitchChat.cs +++ b/KfChatDotNetKickBot/Services/TwitchChat.cs @@ -7,7 +7,7 @@ using Websocket.Client; namespace KfChatDotNetKickBot.Services; -public class TwitchChat : IDisposable +public class TwitchChat { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private WebsocketClient _wsClient; @@ -76,8 +76,14 @@ public class TwitchChat : IDisposable private void WsDisconnection(DisconnectionInfo disconnectionInfo) { _logger.Error($"Client disconnected from Discord (or never successfully connected). Type is {disconnectionInfo.Type}"); - _logger.Error(JsonSerializer.Serialize(disconnectionInfo)); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); OnWsDisconnection?.Invoke(this, disconnectionInfo); + if (disconnectionInfo.Type == DisconnectionType.ByServer) + { + _logger.Info("Forcing reconnection as the type was ByServer"); + _wsClient.Reconnect().Wait(_cancellationToken); + } } private void WsReconnection(ReconnectionInfo reconnectionInfo) @@ -169,11 +175,4 @@ public class TwitchChat : IDisposable _logger.Error("--- End of IRC Message ---"); } } - - public void Dispose() - { - _logger.Info("Disposing Twitch Chat"); - _wsClient.Dispose(); - GC.SuppressFinalize(this); - } } \ No newline at end of file diff --git a/KfChatDotNetKickBot/Settings/BuiltIn.cs b/KfChatDotNetKickBot/Settings/BuiltIn.cs new file mode 100644 index 0000000..4995120 --- /dev/null +++ b/KfChatDotNetKickBot/Settings/BuiltIn.cs @@ -0,0 +1,346 @@ +using System.Text.Json; +using KfChatDotNetKickBot.Models; +using KfChatDotNetKickBot.Models.DbModels; +using NLog; + +namespace KfChatDotNetKickBot.Settings; + +public static class BuiltIn +{ + // Creates DB options if they don't exist and all fields (except value) if these have changed in code + public static async Task SyncSettingsWithDb() + { + var logger = LogManager.GetCurrentClassLogger(); + await using var db = new ApplicationDbContext(); + logger.Info($"Syncing {BuiltInSettings.Count} settings with the DB"); + foreach (var builtIn in BuiltInSettings) + { + var setting = db.Settings.FirstOrDefault(setting => setting.Key == builtIn.Key); + if (setting == null) + { + logger.Info($"{builtIn.Key} doesn't exist in the DB, creating"); + db.Settings.Add(new SettingDbModel + { + Key = builtIn.Key, + Value = builtIn.Default, + Regex = builtIn.Regex, + Description = builtIn.Description, + Default = builtIn.Default, + IsSecret = builtIn.IsSecret + }); + continue; + } + logger.Debug($"{builtIn.Key} exists in the DB, now going to ensure its fields are consistent"); + setting.Key = builtIn.Key; + setting.Regex = builtIn.Regex; + setting.Description = builtIn.Description; + setting.Default = builtIn.Default; + setting.IsSecret = builtIn.IsSecret; + } + logger.Info("Saving changes to the DB"); + await db.SaveChangesAsync(); + } + + public static async Task MigrateJsonSettingsToDb() + { + var oldConfigPath = "config.json"; + var logger = LogManager.GetCurrentClassLogger(); + await using var db = new ApplicationDbContext(); + + logger.Info($"Checking {oldConfigPath} exists"); + if (!Path.Exists(oldConfigPath)) + { + logger.Info($"{oldConfigPath} does not exist. Migration already performed or was never needed"); + return; + } + + logger.Info($"Migrating {oldConfigPath}"); +#pragma warning disable CS0612 // Type or member is obsolete + var oldConfig = JsonSerializer.Deserialize(await File.ReadAllTextAsync(oldConfigPath)); +#pragma warning restore CS0612 // Type or member is obsolete + if (oldConfig == null) + { + logger.Error($"Caught a null when deserializing {oldConfigPath}"); + return; + } + + await Helpers.SetValue(Keys.PusherEndpoint, oldConfig.PusherEndpoint.ToString()); + await Helpers.SetValue(Keys.KiwiFarmsWsEndpoint, oldConfig.KfWsEndpoint.ToString()); + await Helpers.SetValueAsList(Keys.PusherChannels, oldConfig.PusherChannels); + await Helpers.SetValue(Keys.KiwiFarmsRoomId, oldConfig.KfChatRoomId); + await Helpers.SetValue(Keys.Proxy, oldConfig.Proxy); + await Helpers.SetValue(Keys.KiwiFarmsWsReconnectTimeout, oldConfig.KfReconnectTimeout); + await Helpers.SetValue(Keys.PusherReconnectTimeout, oldConfig.PusherReconnectTimeout); + await Helpers.SetValueAsBoolean(Keys.GambaSeshDetectEnabled, oldConfig.EnableGambaSeshDetect); + await Helpers.SetValue(Keys.GambaSeshUserId, oldConfig.GambaSeshUserId); + await Helpers.SetValue(Keys.KickIcon, oldConfig.KickIcon); + await Helpers.SetValue(Keys.KiwiFarmsDomain, oldConfig.KfDomain); + await Helpers.SetValue(Keys.KiwiFarmsUsername, oldConfig.KfUsername); + await Helpers.SetValue(Keys.KiwiFarmsPassword, oldConfig.KfPassword); + await Helpers.SetValue(Keys.KiwiFarmsChromiumPath, oldConfig.ChromiumPath); + await Helpers.SetValue(Keys.TwitchBossmanJackId, oldConfig.BossmanJackTwitchId); + await Helpers.SetValue(Keys.TwitchBossmanJackUsername, oldConfig.BossmanJackTwitchUsername); + await Helpers.SetValueAsBoolean(Keys.KiwiFarmsSuppressChatMessages, oldConfig.SuppressChatMessages); + await Helpers.SetValue(Keys.DiscordToken, oldConfig.DiscordToken); + await Helpers.SetValue(Keys.DiscordBmjId, oldConfig.DiscordBmjId); + logger.Info($"{oldConfigPath} migration done."); + + logger.Info("Renaming files no longer in use"); + // Utils.SafelyRenameFile will attempt to rename and swallow any exception (with logging) if it fails + Utils.SafelyRenameFile(oldConfigPath, $"{oldConfigPath}.migrated"); + + logger.Info("File renamed"); + } + + public static List BuiltInSettings = + [ + new BuiltInSettingsModel + { + Key = Keys.PusherEndpoint, + Regex = @".+", + Description = + "Pusher WebSocket endpoint URL", + Default = "wss://ws-us2.pusher.com/app/eb1d5f283081a78b932c?protocol=7&client=js&version=7.6.0&flash=false", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.KiwiFarmsWsEndpoint, + Regex = @".+", + Description = + "Kiwi Farms chat WebSocket endpoint", + Default = "wss://kiwifarms.st:9443/chat.ws", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.PusherChannels, + Regex = @".+", + Description = + "List of Pusher channels to subscribe to", + Default = null, + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.KiwiFarmsRoomId, + Regex = @"\d+", + Description = + "Kiwi Farms Keno Kasino room ID", + Default = "15", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.Proxy, + Regex = @".+", + Description = + "Proxy to use for all outgoing requests. Null to disable", + Default = null, + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.KiwiFarmsWsReconnectTimeout, + Regex = @"\d+", + Description = + "Kiwi Farms chat reconnect timeout", + Default = "30", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.PusherReconnectTimeout, + Regex = @"\d+", + Description = + "Pusher reconnect timeout", + Default = "30", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.GambaSeshDetectEnabled, + Regex = @"true|false", + Description = + "Whether to enable detection for the presence of GambaSesh", + Default = "true", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.GambaSeshUserId, + Regex = @"\d+", + Description = + "GambaSesh's uer ID for the purposes of detection", + Default = "168162", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.KickIcon, + Regex = @".+", + Description = + "Kick Icon to use for relaying chat messages", + Default = "https://i.postimg.cc/Qtw4nCPG/kick16.png", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.KiwiFarmsDomain, + Regex = @".+", + Description = + "Domain to use when retrieving a session token", + Default = "kiwifarms.st", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.KiwiFarmsUsername, + Regex = @".+", + Description = + "Username to use when authenticating with Kiwi Farms", + Default = null, + IsSecret = true + }, + new BuiltInSettingsModel + { + Key = Keys.KiwiFarmsPassword, + Regex = @".+", + Description = + "Password to use when authenticating with Kiwi Farms", + Default = null, + IsSecret = true + }, + new BuiltInSettingsModel + { + Key = Keys.KiwiFarmsChromiumPath, + Regex = @".+", + Description = + "Path to download the Chromium install used for the token grabber", + Default = "chromium_install", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.TwitchBossmanJackId, + Regex = @"\d+", + Description = + "BossmanJack's Twitch channel ID", + Default = "114122847", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.TwitchBossmanJackUsername, + Regex = @".+", + Description = + "BossmanJack's Twitch channel username", + Default = "thebossmanjack", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.KiwiFarmsSuppressChatMessages, + Regex = @"true|false", + Description = + "Enable to prevent messages from actually being sent to chat.", + Default = "false", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.DiscordToken, + Regex = @".+", + Description = + "Token to use when authenticating with Discord. Set to null to disable.", + Default = null, + IsSecret = true + }, + new BuiltInSettingsModel + { + Key = Keys.DiscordBmjId, + Regex = @"\d+", + Description = + "BossmanJack's Discord user ID", + Default = "554123642246529046", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.TwitchIcon, + Regex = ".+", + Description = "URL for the 16px Twitch icon", + Default = "https://i.postimg.cc/QMFVV2Xk/twitch16.png", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.DiscordIcon, + Regex = ".+", + Description = "URL for the 16px Discord icon", + Default = "https://i.postimg.cc/cLmQrp89/discord16.png", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.ShuffleBmjUsername, + Regex = ".+", + Description = "Bossman's Shuffle Username", + Default = "TheBossmanJack", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.JuiceCooldown, + Regex = @"\d+", + Description = "Cooldown (in seconds) until you can get juiced again", + Default = "3600", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.JuiceAmount, + Regex = @"\d+", + Description = "Amount of $KKK to juice", + Default = "50", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.KiwiFarmsToken, + Regex = ".+", + Description = "Last successfully retrieved forum token (will be refreshed automatically if expired)", + Default = null, + IsSecret = true + } + ]; + + public static class Keys + { + public static string PusherEndpoint = "Pusher.Endpoint"; + public static string KiwiFarmsWsEndpoint = "KiwiFarms.WsEndpoint"; + public static string PusherChannels = "Pusher.Channels"; + public static string KiwiFarmsRoomId = "KiwiFarms.RoomId"; + public static string Proxy = "Proxy"; + public static string KiwiFarmsWsReconnectTimeout = "KiwiFarms.WsReconnectTimeout"; + public static string PusherReconnectTimeout = "Pusher.ReconnectTimeout"; + public static string GambaSeshDetectEnabled = "GambaSesh.DetectEnabled"; + public static string GambaSeshUserId = "GambaSesh.UserId"; + public static string KickIcon = "Kick.Icon"; + public static string KiwiFarmsDomain = "KiwiFarms.Domain"; + public static string KiwiFarmsUsername = "KiwiFarms.Username"; + public static string KiwiFarmsPassword = "KiwiFarms.Password"; + public static string KiwiFarmsChromiumPath = "KiwiFarms.ChromiumPath"; + public static string TwitchBossmanJackId = "Twitch.BossmanJackId"; + public static string TwitchBossmanJackUsername = "Twitch.BossmanJackUsername"; + public static string KiwiFarmsSuppressChatMessages = "KiwiFarms.SuppressChatMessages"; + public static string DiscordToken = "Discord.Token"; + public static string DiscordBmjId = "Discord.BmjId"; + public static string TwitchIcon = "Twitch.Icon"; + public static string DiscordIcon = "Discord.Icon"; + public static string ShuffleBmjUsername = "Shuffle.BmjUsername"; + public static string JuiceCooldown = "Juice.Cooldown"; + public static string JuiceAmount = "Juice.Amount"; + public static string KiwiFarmsToken = "KiwiFarms.Token"; + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Settings/Helpers.cs b/KfChatDotNetKickBot/Settings/Helpers.cs new file mode 100644 index 0000000..b7958f0 --- /dev/null +++ b/KfChatDotNetKickBot/Settings/Helpers.cs @@ -0,0 +1,173 @@ +using KfChatDotNetKickBot.Models.DbModels; +using Microsoft.EntityFrameworkCore; +using NLog; + +namespace KfChatDotNetKickBot.Settings; + +public static class Helpers +{ + public static async Task GetValue(string key, bool caseInsensitive = false) + { + var logger = LogManager.GetCurrentClassLogger(); + await using var db = new ApplicationDbContext(); + logger.Trace($"Retrieving value for {key}"); + + SettingDbModel? setting; + if (caseInsensitive) + { + // String comparison doesn't work on EF core if I recall correctly +#pragma warning disable CA1862 + setting = await db.Settings.FirstOrDefaultAsync(s => s.Key.ToLower() == key.ToLower()); +#pragma warning restore CA1862 + } + else + { + setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == key); + } + if (setting == null) + { + logger.Debug($"{key} does not exist, throwing KeyNotFoundException"); + throw new KeyNotFoundException($"{key} does not exist"); + } + + if (setting.Value == "null") + { + logger.Debug($"{key}'s value is null so returning SettingValue(null)"); + return new SettingValue(null, null); + } + + logger.Debug($"Returning '{setting.Value}' as {typeof(SettingValue)}"); + return new SettingValue(setting.Value, setting); + } + + public static async Task> GetMultipleValues(string[] keys, bool caseInsensitive = false) + { + var logger = LogManager.GetCurrentClassLogger(); + await using var db = new ApplicationDbContext(); + logger.Trace($"Getting values for keys {string.Join(", ", keys)}"); + + Dictionary values = new Dictionary(); + foreach (var key in keys) + { + SettingDbModel? setting; + if (caseInsensitive) + { + // String comparison doesn't work on EF core if I recall correctly +#pragma warning disable CA1862 + setting = await db.Settings.FirstOrDefaultAsync(s => s.Key.ToLower() == key.ToLower()); +#pragma warning restore CA1862 + } + else + { + setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == key); + } + + if (setting == null) + { + logger.Debug($"{key} does not exist, throwing KeyNotFoundException()"); + throw new KeyNotFoundException(); + } + + if (setting.Value == "null") + { + logger.Debug($"{key}'s value is null so returning SettingValue(null)"); + values.Add(key, new SettingValue(null, null)); + continue; + } + values.Add(key, new SettingValue(setting.Value, setting)); + } + + return values; + } + + public static async Task SetValue(string key, object? value) + { + var logger = LogManager.GetCurrentClassLogger(); + await using var db = new ApplicationDbContext(); + string stringValue; + if (value == null) + { + stringValue = "null"; + } + else if (value is string) + { + stringValue = (string)value; + } + else + { + stringValue = (string)Convert.ChangeType(value, TypeCode.String); + } + logger.Debug($"Setting {key} to {stringValue}"); + + var setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == key); + if (setting == null) + { + logger.Debug($"{key} does not exist, throwing KeyNotFoundException()"); + throw new KeyNotFoundException(); + } + + setting.Value = stringValue; + await db.SaveChangesAsync(); + } + + public static async Task SetValueAsList(string key, List values, char separator = ',') + { + var logger = LogManager.GetCurrentClassLogger(); + await using var db = new ApplicationDbContext(); + List stringValues = values.Select(val => (string)Convert.ChangeType(val, TypeCode.String)).ToList(); + string joinedValue = string.Join(separator, stringValues); + logger.Debug($"Setting {key} to {joinedValue}"); + + var setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == key); + if (setting == null) + { + logger.Debug($"{key} does not exist, throwing KeyNotFoundException()"); + throw new KeyNotFoundException(); + } + + setting.Value = joinedValue; + await db.SaveChangesAsync(); + } + + public static async Task SetValueAsKeyValuePairs(string key, Dictionary data, char delimiter = ',', + char separator = '=') + { + var logger = LogManager.GetCurrentClassLogger(); + await using var db = new ApplicationDbContext(); + logger.Debug($"Building data for {key}"); + var value = data.Keys.Aggregate(string.Empty, + (current, dictKey) => current + $"{dictKey}{separator}{data[dictKey]}{delimiter}"); + + // Remove trailing delimiters that would be leftover as it doesn't account for whether it's the last key + value = value.TrimEnd(delimiter); + logger.Debug($"Setting {key} to {value}"); + + var setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == key); + if (setting == null) + { + logger.Debug($"{key} does not exist, throwing KeyNotFoundException()"); + throw new KeyNotFoundException(); + } + + setting.Value = value; + await db.SaveChangesAsync(); + } + + public static async Task SetValueAsBoolean(string key, bool value) + { + var logger = LogManager.GetCurrentClassLogger(); + await using var db = new ApplicationDbContext(); + logger.Debug($"Setting {key} to {value}"); + + var setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == key); + if (setting == null) + { + logger.Debug($"{key} does not exist, throwing KeyNotFoundException()"); + throw new KeyNotFoundException(); + } + + setting.Value = value ? "true" : "false"; + + await db.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Settings/SettingValue.cs b/KfChatDotNetKickBot/Settings/SettingValue.cs new file mode 100644 index 0000000..f5276fe --- /dev/null +++ b/KfChatDotNetKickBot/Settings/SettingValue.cs @@ -0,0 +1,9 @@ +using KfChatDotNetKickBot.Models.DbModels; + +namespace KfChatDotNetKickBot.Settings; + +public class SettingValue(string? value, SettingDbModel? dbEntry) +{ + public string? Value { get; set; } = value; + public SettingDbModel? DbEntry { get; set; } = dbEntry; +} \ No newline at end of file diff --git a/KfChatDotNetKickBot/Settings/Utils.cs b/KfChatDotNetKickBot/Settings/Utils.cs new file mode 100644 index 0000000..abdd623 --- /dev/null +++ b/KfChatDotNetKickBot/Settings/Utils.cs @@ -0,0 +1,59 @@ +using NLog; + +namespace KfChatDotNetKickBot.Settings; + +public static class Utils +{ + public static List ToList(this SettingValue settingValue, char separator = ',') + { + if (settingValue.Value == null) return new List(); + return settingValue.Value.Split(separator).ToList(); + } + + public static Dictionary ToKeyValuePairs(this SettingValue settingValue, char delimiter = ',', + char separator = '=') + { + if (settingValue.Value == null) + { + return new Dictionary(); + } + return settingValue.Value.Split(delimiter).ToDictionary(kv => kv.Split(separator)[0], + kv => ValueToType(kv.Split(separator)[1])); + } + + public static bool ToBoolean(this SettingValue settingValue) + { + var logger = LogManager.GetCurrentClassLogger(); + if (settingValue.Value is null or "null") + { + return default; + } + + return settingValue.Value.Equals("true", StringComparison.CurrentCultureIgnoreCase); + } + + public static T ValueToType(string value) + { + return (T)Convert.ChangeType(value, typeof(T)); + } + + public static T ToType(this SettingValue settingValue) + { + return (T)Convert.ChangeType(settingValue.Value, typeof(T)); + } + + public static void SafelyRenameFile(string oldName, string newName) + { + var logger = LogManager.GetCurrentClassLogger(); + logger.Debug($"Renaming {oldName} to {newName}"); + try + { + File.Move(oldName, newName); + } + catch (Exception e) + { + logger.Error($"Failed to rename {oldName} to {newName}"); + logger.Error(e); + } + } +} \ No newline at end of file diff --git a/KfChatDotNetWsClient/ChatClient.cs b/KfChatDotNetWsClient/ChatClient.cs index a8db3b6..d97d543 100644 --- a/KfChatDotNetWsClient/ChatClient.cs +++ b/KfChatDotNetWsClient/ChatClient.cs @@ -101,7 +101,8 @@ public class ChatClient private void WsDisconnection(DisconnectionInfo disconnectionInfo) { _logger.Error($"Client disconnected from the chat (or never successfully connected). Type is {disconnectionInfo.Type}"); - _logger.Error(JsonSerializer.Serialize(disconnectionInfo)); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); OnWsDisconnection?.Invoke(this, disconnectionInfo); } diff --git a/KfChatDotNetWsClient/KfChatDotNetWsClient.csproj b/KfChatDotNetWsClient/KfChatDotNetWsClient.csproj index ccd82d3..672773e 100644 --- a/KfChatDotNetWsClient/KfChatDotNetWsClient.csproj +++ b/KfChatDotNetWsClient/KfChatDotNetWsClient.csproj @@ -9,7 +9,7 @@ - + diff --git a/KfChatDotNetWsClient/Models/Events/MessageModel.cs b/KfChatDotNetWsClient/Models/Events/MessageModel.cs index fcbb1f5..a8c0285 100644 --- a/KfChatDotNetWsClient/Models/Events/MessageModel.cs +++ b/KfChatDotNetWsClient/Models/Events/MessageModel.cs @@ -3,10 +3,16 @@ namespace KfChatDotNetWsClient.Models.Events; public class MessageModel { public UserModel Author { get; set; } + /// + /// HTML formatted message + /// public string Message { get; set; } public int MessageId { get; set; } public DateTimeOffset? MessageEditDate { get; set; } public DateTimeOffset MessageDate { get; set; } + /// + /// Unformatted message with original BB code + /// public string MessageRaw { get; set; } public int RoomId { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetWsClient/Models/Events/UserModel.cs b/KfChatDotNetWsClient/Models/Events/UserModel.cs index ad69fc5..1acda99 100644 --- a/KfChatDotNetWsClient/Models/Events/UserModel.cs +++ b/KfChatDotNetWsClient/Models/Events/UserModel.cs @@ -3,6 +3,9 @@ namespace KfChatDotNetWsClient.Models.Events; public class UserModel { public int Id { get; set; } + /// + /// Forum display name. Note that it'll be HTML encoded + /// public string Username { get; set; } public Uri AvatarUrl { get; set; } // Unset if it's related to a chat message diff --git a/KickWsClient/KickWsClient.cs b/KickWsClient/KickWsClient.cs index 7643124..2e15781 100644 --- a/KickWsClient/KickWsClient.cs +++ b/KickWsClient/KickWsClient.cs @@ -92,7 +92,8 @@ public class KickWsClient private void WsDisconnection(DisconnectionInfo disconnectionInfo) { _logger.Error($"Client disconnected from the chat (or never successfully connected). Type is {disconnectionInfo.Type}"); - _logger.Error(JsonSerializer.Serialize(disconnectionInfo)); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); OnWsDisconnection?.Invoke(this, disconnectionInfo); } @@ -264,7 +265,7 @@ public class KickWsClient return; } default: - _logger.Info("Event unhandled. JOSN payload follows"); + _logger.Info("Event unhandled. JSON payload follows"); _logger.Info(message.Text); break; } diff --git a/KickWsClient/KickWsClient.csproj b/KickWsClient/KickWsClient.csproj index 15fb6a2..f95cc39 100644 --- a/KickWsClient/KickWsClient.csproj +++ b/KickWsClient/KickWsClient.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/ThreeXplCliClient/ThreeXplCliClient.csproj b/ThreeXplCliClient/ThreeXplCliClient.csproj index 16aef83..69b1173 100644 --- a/ThreeXplCliClient/ThreeXplCliClient.csproj +++ b/ThreeXplCliClient/ThreeXplCliClient.csproj @@ -10,7 +10,7 @@ - + diff --git a/ThreeXplWsClient/ThreeXplWsClient.cs b/ThreeXplWsClient/ThreeXplWsClient.cs index 9492608..723adca 100644 --- a/ThreeXplWsClient/ThreeXplWsClient.cs +++ b/ThreeXplWsClient/ThreeXplWsClient.cs @@ -125,7 +125,8 @@ public class ThreeXplWsClient private void WsDisconnection(DisconnectionInfo disconnectionInfo) { _logger.Error($"Client disconnected from the chat (or never successfully connected). Type is {disconnectionInfo.Type}"); - _logger.Error(JsonSerializer.Serialize(disconnectionInfo)); + _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}"); + _logger.Error(disconnectionInfo.Exception); OnWsDisconnection?.Invoke(this, disconnectionInfo, _connectionId); } diff --git a/ThreeXplWsClient/ThreeXplWsClient.csproj b/ThreeXplWsClient/ThreeXplWsClient.csproj index c9ea1c5..a71fcb0 100644 --- a/ThreeXplWsClient/ThreeXplWsClient.csproj +++ b/ThreeXplWsClient/ThreeXplWsClient.csproj @@ -8,8 +8,8 @@ - - + +