diff --git a/.idea/.idea.KfChatDotNet/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.KfChatDotNet/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..1aaccf7 --- /dev/null +++ b/.idea/.idea.KfChatDotNet/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/KfChatDotNetBot/ApplicationDbContext.cs b/KfChatDotNetBot/ApplicationDbContext.cs index 05038b9..1da2ce4 100644 --- a/KfChatDotNetBot/ApplicationDbContext.cs +++ b/KfChatDotNetBot/ApplicationDbContext.cs @@ -14,4 +14,5 @@ public class ApplicationDbContext : DbContext public DbSet Juicers { get; set; } public DbSet Settings { get; set; } public DbSet HowlggBets { get; set; } + public DbSet RainbetBets { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetBot/ChatBot.cs b/KfChatDotNetBot/ChatBot.cs index 0c0c5b8..bfbfe6f 100644 --- a/KfChatDotNetBot/ChatBot.cs +++ b/KfChatDotNetBot/ChatBot.cs @@ -41,6 +41,7 @@ public class ChatBot private bool _twitchDisabled = false; private Task _websocketWatchdog; private Jackpot _jackpot; + private Rainbet _rainbet; public ChatBot() { @@ -121,6 +122,7 @@ public class ChatBot BuildTwitchChat(); BuildHowlgg(); BuildJackpot(); + BuildRainbet(); _logger.Info("Starting websocket watchdog"); _websocketWatchdog = WebsocketWatchdog(); @@ -185,6 +187,14 @@ public class ChatBot _jackpot = null!; BuildJackpot(); } + + // if (!_rainbet.IsConnected()) + // { + // _logger.Error("Rainbet died, recreating it"); + // _rainbet.Dispose(); + // _rainbet = null!; + // BuildRainbet(); + // } } catch (Exception e) { @@ -194,6 +204,49 @@ public class ChatBot } } + + private void BuildRainbet() + { + _rainbet = new Rainbet(_cancellationToken); + _rainbet.OnRainbetBet += OnRainbetBet; + _rainbet.StartGameHistoryTimer(); + } + + private void OnRainbetBet(object sender, List bets) + { + var settings = Helpers + .GetMultipleValues([ + BuiltIn.Keys.RainbetBmjPublicId, BuiltIn.Keys.TwitchBossmanJackUsername, + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]).Result; + _logger.Trace("Rainbet bet has arrived"); + using var db = new ApplicationDbContext(); + foreach (var bet in bets.Where(b => b.User.PublicId == settings[BuiltIn.Keys.RainbetBmjPublicId].Value)) + //foreach (var bet in bets) + { + if (db.RainbetBets.Any(b => b.BetId == bet.Id)) + { + _logger.Trace($"Ignoring bet {bet.Id} as we've already logged it"); + continue; + } + + db.RainbetBets.Add(new RainbetBetsDbModel + { + PublicId = bet.User.PublicId, + RainbetUserId = bet.User.Id, + GameName = bet.Game.Name, + Value = bet.Value, + Payout = bet.Payout, + Multiplier = bet.Multiplier, + BetId = bet.Id, + UpdatedAt = bet.UpdatedAt, + BetSeenAt = DateTimeOffset.UtcNow + }); + _logger.Info("Added a Bossman Rainbet bet to the database"); + } + + db.SaveChanges(); + } private void BuildJackpot() { diff --git a/KfChatDotNetBot/KfChatDotNetBot.csproj b/KfChatDotNetBot/KfChatDotNetBot.csproj index 1725843..38a433a 100644 --- a/KfChatDotNetBot/KfChatDotNetBot.csproj +++ b/KfChatDotNetBot/KfChatDotNetBot.csproj @@ -8,6 +8,7 @@ + diff --git a/KfChatDotNetBot/Migrations/20240802172200_Rainbet.Designer.cs b/KfChatDotNetBot/Migrations/20240802172200_Rainbet.Designer.cs new file mode 100644 index 0000000..471191b --- /dev/null +++ b/KfChatDotNetBot/Migrations/20240802172200_Rainbet.Designer.cs @@ -0,0 +1,187 @@ +// +using System; +using KfChatDotNetBot; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace KfChatDotNetBot.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240802172200_Rainbet")] + partial class Rainbet + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + + modelBuilder.Entity("KfChatDotNetBot.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("KfChatDotNetBot.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("KfChatDotNetBot.Models.DbModels.RainbetBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BetId") + .HasColumnType("INTEGER"); + + b.Property("BetSeenAt") + .HasColumnType("TEXT"); + + b.Property("GameName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Multiplier") + .HasColumnType("REAL"); + + b.Property("Payout") + .HasColumnType("REAL"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("RainbetUserId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("RainbetBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.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("KfChatDotNetBot.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("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b => + { + b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/KfChatDotNetBot/Migrations/20240802172200_Rainbet.cs b/KfChatDotNetBot/Migrations/20240802172200_Rainbet.cs new file mode 100644 index 0000000..adc42e6 --- /dev/null +++ b/KfChatDotNetBot/Migrations/20240802172200_Rainbet.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace KfChatDotNetBot.Migrations +{ + /// + public partial class Rainbet : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RainbetBets", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PublicId = table.Column(type: "TEXT", nullable: true), + RainbetUserId = table.Column(type: "INTEGER", nullable: false), + GameName = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "REAL", nullable: false), + Payout = table.Column(type: "REAL", nullable: false), + Multiplier = table.Column(type: "REAL", nullable: false), + BetId = table.Column(type: "INTEGER", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + BetSeenAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RainbetBets", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RainbetBets"); + } + } +} diff --git a/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs b/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs index c566c0b..a1f76f6 100644 --- a/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace KfChatDotNetBot.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.HowlggBetsDbModel", b => + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.HowlggBetsDbModel", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -50,7 +50,7 @@ namespace KfChatDotNetBot.Migrations b.ToTable("HowlggBets"); }); - modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -72,7 +72,46 @@ namespace KfChatDotNetBot.Migrations b.ToTable("Juicers"); }); - modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.SettingDbModel", b => + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.RainbetBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BetId") + .HasColumnType("INTEGER"); + + b.Property("BetSeenAt") + .HasColumnType("TEXT"); + + b.Property("GameName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Multiplier") + .HasColumnType("REAL"); + + b.Property("Payout") + .HasColumnType("REAL"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("RainbetUserId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("RainbetBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.SettingDbModel", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -105,7 +144,7 @@ namespace KfChatDotNetBot.Migrations b.ToTable("Settings"); }); - modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.UserDbModel", b => + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserDbModel", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -129,9 +168,9 @@ namespace KfChatDotNetBot.Migrations b.ToTable("Users"); }); - modelBuilder.Entity("KfChatDotNetKickBot.Models.DbModels.JuicerDbModel", b => + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b => { - b.HasOne("KfChatDotNetKickBot.Models.DbModels.UserDbModel", "User") + b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) diff --git a/KfChatDotNetBot/Models/DbModels/RainbetBetsDbModel.cs b/KfChatDotNetBot/Models/DbModels/RainbetBetsDbModel.cs new file mode 100644 index 0000000..eb0b7c4 --- /dev/null +++ b/KfChatDotNetBot/Models/DbModels/RainbetBetsDbModel.cs @@ -0,0 +1,18 @@ +namespace KfChatDotNetBot.Models.DbModels; + +public class RainbetBetsDbModel +{ + public int Id { get; set; } + // Weird gibberish identifier given to users, may be hidden on bet feeds for users who opt out of the social shit + // Null if the user has opted out + public string? PublicId { get; set; } + // This is always set. Rainbet never omits the user's ID even if they're anonymous + public required int RainbetUserId { get; set; } + public required string GameName { get; set; } + public required float Value { get; set; } + public required float Payout { get; set; } + public required float Multiplier { get; set; } + public required long BetId { get; set; } + public required DateTimeOffset UpdatedAt { get; set; } + public required DateTimeOffset BetSeenAt { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Models/RainbetModels.cs b/KfChatDotNetBot/Models/RainbetModels.cs new file mode 100644 index 0000000..f854124 --- /dev/null +++ b/KfChatDotNetBot/Models/RainbetModels.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace KfChatDotNetBot.Models; + +public class RainbetBetHistoryModel +{ + [JsonPropertyName("id")] + public required int Id { get; set; } + [JsonPropertyName("value")] + public required float Value { get; set; } + [JsonPropertyName("payout")] + public required float Payout { get; set; } + [JsonPropertyName("multiplier")] + public required float Multiplier { get; set; } + [JsonPropertyName("updated_at")] + public required DateTimeOffset UpdatedAt { get; set; } + [JsonPropertyName("user")] + public required RainbetBetHistoryUserModel User { get; set; } + [JsonPropertyName("game")] + public required RainbetBetHistoryGameModel Game { get; set; } +} + +public class RainbetBetHistoryGameModel +{ + [JsonPropertyName("id")] + public required int Id { get; set; } + // It's actually a slug + [JsonPropertyName("url")] + public required string Url { get; set; } + [JsonPropertyName("name")] + public required string Name { get; set; } +} + +public class RainbetBetHistoryUserRankModel +{ + [JsonPropertyName("id")] + public required int Id { get; set; } + [JsonPropertyName("name")] + public required string Name { get; set; } + [JsonPropertyName("level")] + public required int Level { get; set; } + [JsonPropertyName("threshold")] + public required int Threshold { get; set; } +} + +public class RainbetBetHistoryUserModel +{ + // Can still uniquely identify users even if they're private. Bossman is ID 50 + [JsonPropertyName("id")] + public required int Id { get; set; } + // Set to null on private-profiles + [JsonPropertyName("publicId")] + public string? PublicId { get; set; } + // Set to null on private profiles + [JsonPropertyName("username")] + public string? Username { get; set; } + [JsonPropertyName("wageredAmount")] + public required float WageredAmount { get; set; } + [JsonPropertyName("public_profile")] + public required int PublicProfile { get; set; } + // Null when they have no rank + [JsonPropertyName("rank")] + public RainbetBetHistoryUserRankModel? Rank { get; set; } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Services/Rainbet.cs b/KfChatDotNetBot/Services/Rainbet.cs new file mode 100644 index 0000000..5a9e9ce --- /dev/null +++ b/KfChatDotNetBot/Services/Rainbet.cs @@ -0,0 +1,120 @@ +using System.Net; +using System.Net.Http.Json; +using FlareSolverrSharp; +using FlareSolverrSharp.Exceptions; +using KfChatDotNetBot.Models; +using KfChatDotNetBot.Settings; +using NLog; + +namespace KfChatDotNetBot.Services; + +public class Rainbet : IDisposable +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + public delegate void OnRainbetBetEventHandler(object sender, List bets); + public event OnRainbetBetEventHandler OnRainbetBet; + private CancellationToken _cancellationToken = CancellationToken.None; + private CancellationTokenSource _gameHistoryCts = new(); + private Task? _gameHistoryTask; + private TimeSpan _gameHistoryInterval = TimeSpan.FromSeconds(60); + + public Rainbet(CancellationToken? cancellationToken = null) + { + if (cancellationToken != null) _cancellationToken = cancellationToken.Value; + _logger.Info("Rainbet client created"); + } + + public void StartGameHistoryTimer() + { + _gameHistoryTask = GameHistoryTimer(); + } + + private async Task GameHistoryTimer() + { + using var timer = new PeriodicTimer(_gameHistoryInterval); + while (await timer.WaitForNextTickAsync(_gameHistoryCts.Token)) + { + try + { + _logger.Info("Retrieving game history from Rainbet"); + var bets = await GetGameHistory(1000); + OnRainbetBet?.Invoke(this, bets); + } + catch (FlareSolverrException e) + { + _logger.Error("Caught a FlareSolverrException, probably that retarded cookie bug that has been unfixed for 3+ years"); + _logger.Error("Trying again immediately as it's pretty rare it happens twice in a row"); + _logger.Error(e); + try + { + var bets = await GetGameHistory(1000); + OnRainbetBet?.Invoke(this, bets); + } + catch (Exception ee) + { + _logger.Error("Fuck my life, failed again. We'll just wait until the next tick"); + _logger.Error(ee); + } + } + catch (Exception e) + { + _logger.Error("Caught error when retrieving bets and invoking the event"); + _logger.Error(e); + } + } + } + + // FlareSolverr C# client does not support POSTing application/json so this method involves + // 1. Getting the home page (as you're unlikely to get a CF challenge checkbox. Probably due to some config to limit + // friction for degens on VPNs, which is like 99% of the traffic for these shit casinos) + // 2. Using the cookies from that request to do the actual POST + // Cookies and UA must match or the trannies at Cloudflare will reject your cookies + // take = 10 is the default, but it can go higher + public async Task> GetGameHistory(int take = 10) + { + var settings = + await Helpers.GetMultipleValues([BuiltIn.Keys.FlareSolverrApiUrl, BuiltIn.Keys.FlareSolverrProxy]); + var flareSolverrUrl = settings[BuiltIn.Keys.FlareSolverrApiUrl]; + var flareSolverrProxy = settings[BuiltIn.Keys.FlareSolverrProxy]; + var handler = new ClearanceHandler(flareSolverrUrl.Value) + { + // Generally takes <5 seconds + MaxTimeout = 30000, + }; + _logger.Debug($"Configured clearance handler to use FlareSolverr endpoint: {flareSolverrUrl.Value}"); + // I would suggest not using a proxy. It's pretty much a miracle this works at all. + if (flareSolverrProxy.Value != null) + { + handler.ProxyUrl = flareSolverrProxy.Value; + _logger.Debug($"Configured clearance handler to use {flareSolverrProxy.Value} for proxying the request"); + } + var gameHistoryUrl = "https://sportsbook.rainbet.com/v1/game-history"; + var client = new HttpClient(handler); + var jsonBody = new Dictionary { {"take", take} }; + var postData = JsonContent.Create(jsonBody); + // You get CF checkbox'd if you go directly to sportsbook.rainbet.com but works ok for root + var getResponse = await client.GetAsync("https://rainbet.com/", _cancellationToken); + var postClientHandler = new HttpClientHandler(); + if (flareSolverrProxy.Value != null) + { + postClientHandler.Proxy = new WebProxy(flareSolverrProxy.Value); + postClientHandler.UseProxy = true; + _logger.Debug($"Configured API request to use {flareSolverrProxy.Value}"); + } + var postClient = new HttpClient(postClientHandler); + postClient.DefaultRequestHeaders.Add("Cookie", getResponse.Headers.GetValues("Set-Cookie")); + postClient.DefaultRequestHeaders.UserAgent.Clear(); + postClient.DefaultRequestHeaders.UserAgent.ParseAdd(getResponse.RequestMessage.Headers.UserAgent.ToString()); + var response = await postClient.PostAsync(gameHistoryUrl, postData, _cancellationToken); + var bets = await response.Content.ReadFromJsonAsync>(cancellationToken: _cancellationToken); + return bets; + } + + public void Dispose() + { + _gameHistoryCts.Cancel(); + _gameHistoryCts.Dispose(); + _gameHistoryTask?.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Services/Shuffle.cs b/KfChatDotNetBot/Services/Shuffle.cs index 25a8691..6eb323e 100644 --- a/KfChatDotNetBot/Services/Shuffle.cs +++ b/KfChatDotNetBot/Services/Shuffle.cs @@ -207,7 +207,16 @@ public class Shuffle : IDisposable public void Dispose() { _wsClient.Dispose(); - _pingCts.Cancel(); + // Rare bug but has happened at least once + try + { + _pingCts.Cancel(); + } + catch (ObjectDisposedException e) + { + _logger.Error("Caught object disposed exception when trying to send a cancellation to the ping task"); + _logger.Error(e); + } _pingCts.Dispose(); _pingTask.Dispose(); GC.SuppressFinalize(this); diff --git a/KfChatDotNetBot/Settings/BuiltIn.cs b/KfChatDotNetBot/Settings/BuiltIn.cs index dcac89b..4b6a18a 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -352,6 +352,30 @@ public static class BuiltIn Description = "Bossman's username on Jackpot", Default = "TheBossmanJack", IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.RainbetBmjPublicId, + Regex = ".+", + Description = "Bossman's rainbet public ID", + Default = "Ir04170wLulcjtePCL7P6lmeOlepRaNp", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.FlareSolverrApiUrl, + Regex = ".+", + Description = "URL for your FlareSolverr service API", + Default = "http://localhost:8191/", + IsSecret = false + }, + new BuiltInSettingsModel + { + Key = Keys.FlareSolverrProxy, + Regex = ".+", + Description = "Proxy in use specifically for FlareSolverr", + Default = null, + IsSecret = false } ]; @@ -387,5 +411,8 @@ public static class BuiltIn public static string KiwiFarmsGreenColor = "KiwiFarms.GreenColor"; public static string KiwiFarmsRedColor = "KiwiFarms.RedColor"; public static string JackpotBmjUsername = "Jackpot.BmjUsername"; + public static string RainbetBmjPublicId = "Rainbet.BmjPublicId"; + public static string FlareSolverrApiUrl = "FlareSolverr.ApiUrl"; + public static string FlareSolverrProxy = "FlareSolverr.Proxy"; } } \ No newline at end of file