mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-04-30 03:22:04 -04:00
Added Chips.gg integration. It basically works but needs more testing and also smashes the DB with how fast their feed updates.
This commit is contained in:
@@ -16,4 +16,5 @@ public class ApplicationDbContext : DbContext
|
||||
public DbSet<HowlggBetsDbModel> HowlggBets { get; set; }
|
||||
public DbSet<RainbetBetsDbModel> RainbetBets { get; set; }
|
||||
public DbSet<TwitchViewCountDbModel> TwitchViewCounts { get; set; }
|
||||
public DbSet<ChipsggBetDbModel> ChipsggBets { get; set; }
|
||||
}
|
||||
@@ -44,6 +44,7 @@ public class ChatBot
|
||||
private Task _websocketWatchdog;
|
||||
private Jackpot _jackpot;
|
||||
private Rainbet _rainbet;
|
||||
private Chipsgg _chipsgg;
|
||||
private List<SentMessageTrackerModel> _sentMessages = [];
|
||||
|
||||
public ChatBot()
|
||||
@@ -126,6 +127,7 @@ public class ChatBot
|
||||
BuildHowlgg();
|
||||
BuildJackpot();
|
||||
BuildRainbet();
|
||||
BuildChipsgg();
|
||||
|
||||
_logger.Info("Starting websocket watchdog");
|
||||
_websocketWatchdog = WebsocketWatchdog();
|
||||
@@ -190,6 +192,14 @@ public class ChatBot
|
||||
_jackpot = null!;
|
||||
BuildJackpot();
|
||||
}
|
||||
|
||||
if (!_chipsgg.IsConnected())
|
||||
{
|
||||
_logger.Error("Chips died, recreating it");
|
||||
_chipsgg.Dispose();
|
||||
_chipsgg = null!;
|
||||
BuildChipsgg();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -206,6 +216,65 @@ public class ChatBot
|
||||
_rainbet.OnRainbetBet += OnRainbetBet;
|
||||
_rainbet.StartGameHistoryTimer();
|
||||
}
|
||||
|
||||
private void BuildChipsgg()
|
||||
{
|
||||
var proxy = Helpers.GetValue(BuiltIn.Keys.Proxy).Result.Value;
|
||||
_chipsgg = new Chipsgg(proxy, _cancellationToken);
|
||||
_chipsgg.OnChipsggRecentBet += OnChipsggRecentBet;
|
||||
_chipsgg.StartWsClient().Wait(_cancellationToken);
|
||||
}
|
||||
|
||||
private void OnChipsggRecentBet(object sender, ChipsggBetModel bet)
|
||||
{
|
||||
var settings = Helpers
|
||||
.GetMultipleValues([
|
||||
BuiltIn.Keys.ChipsggBmjUsername, BuiltIn.Keys.TwitchBossmanJackUsername,
|
||||
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
|
||||
]).Result;
|
||||
_logger.Debug("Chips.gg bet has arrived");
|
||||
if (bet.Username != settings[BuiltIn.Keys.ChipsggBmjUsername].Value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_logger.Info("ALERT BMJ IS BETTING (on Chips.gg)");
|
||||
using var db = new ApplicationDbContext();
|
||||
db.ChipsggBets.Add(new ChipsggBetDbModel
|
||||
{
|
||||
Created = bet.Created, Updated = bet.Updated, UserId = bet.UserId, Username = bet.Username ?? "Unknown", Win = bet.Win,
|
||||
Winnings = bet.Winnings, GameTitle = bet.GameTitle!, Amount = bet.Amount, Multiplier = bet.Multiplier,
|
||||
Currency = bet.Currency!, CurrencyPrice = bet.CurrencyPrice, BetId = bet.BetId
|
||||
});
|
||||
db.SaveChanges();
|
||||
if (IsBmjLive)
|
||||
{
|
||||
_logger.Info("Ignoring as BMJ is live");
|
||||
return;
|
||||
}
|
||||
|
||||
// Only check once because the bot should be tracking the Twitch stream
|
||||
// This is just in case he's already live while the bot starts
|
||||
// He was schizo betting on Dice, so I want to avoid a lot of API requests to Twitch in case they rate limit
|
||||
if (!_isBmjLiveSynced)
|
||||
{
|
||||
IsBmjLive = _twitch.IsStreamLive(settings[BuiltIn.Keys.TwitchBossmanJackUsername].Value!).Result;
|
||||
_isBmjLiveSynced = true;
|
||||
}
|
||||
if (IsBmjLive)
|
||||
{
|
||||
_logger.Info("Double checked and he is really online");
|
||||
return;
|
||||
}
|
||||
|
||||
var payoutColor = settings[BuiltIn.Keys.KiwiFarmsGreenColor].Value;
|
||||
if (bet.Winnings < bet.Amount) payoutColor = settings[BuiltIn.Keys.KiwiFarmsRedColor].Value;
|
||||
return; // Remove when I'm certain this is working properly
|
||||
SendChatMessage(
|
||||
$"🚨🚨 CHIPS BROS 🚨🚨 {bet.Username} just bet {bet.Amount:N} {bet.Currency!.ToUpper()} " +
|
||||
$"({bet.Amount * bet.CurrencyPrice:C}) which paid out [color={payoutColor}]{bet.Winnings} {bet.Currency.ToUpper()} " +
|
||||
$"({bet.Winnings / bet.CurrencyPrice:C})[/color] ({bet.Multiplier:N}x) on {bet.GameTitle} 💰💰",
|
||||
true);
|
||||
}
|
||||
|
||||
private void OnRainbetBet(object sender, List<RainbetBetHistoryModel> bets)
|
||||
{
|
||||
@@ -286,7 +355,6 @@ public class ChatBot
|
||||
|
||||
var payoutColor = settings[BuiltIn.Keys.KiwiFarmsGreenColor].Value;
|
||||
if (bet.Payout < bet.Wager) payoutColor = settings[BuiltIn.Keys.KiwiFarmsRedColor].Value;
|
||||
// There will be a check for live status but ignoring that while we deal with an emergency dice situation
|
||||
SendChatMessage($"🚨🚨 JACKPOT BETTING 🚨🚨 {bet.User} just bet {bet.Wager} {bet.Currency} which paid out [color={payoutColor}]{bet.Payout} {bet.Currency}[/color] ({bet.Multiplier}x) on {bet.GameName} 💰💰", false);
|
||||
}
|
||||
|
||||
@@ -602,7 +670,7 @@ public class ChatBot
|
||||
var messageTracker = new SentMessageTrackerModel
|
||||
{
|
||||
Reference = reference,
|
||||
Message = message,
|
||||
Message = message.TrimEnd(), // Sneedchat trims trailing spaces
|
||||
Status = SentMessageTrackerStatus.Unknown,
|
||||
};
|
||||
if (settings[BuiltIn.Keys.KiwiFarmsSuppressChatMessages].ToBoolean())
|
||||
|
||||
263
KfChatDotNetBot/Migrations/20240817130843_Chipsgg.Designer.cs
generated
Normal file
263
KfChatDotNetBot/Migrations/20240817130843_Chipsgg.Designer.cs
generated
Normal file
@@ -0,0 +1,263 @@
|
||||
// <auto-generated />
|
||||
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("20240817130843_Chipsgg")]
|
||||
partial class Chipsgg
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
|
||||
|
||||
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ChipsggBetDbModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("Amount")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("BetId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<float>("CurrencyPrice")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("GameTitle")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<float>("Multiplier")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateTimeOffset>("Updated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Win")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("Winnings")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ChipsggBets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.HowlggBetsDbModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Bet")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BetId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Game")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("GameId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Profit")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("HowlggBets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("Amount")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateTimeOffset>("JuicedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Juicers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.RainbetBetsDbModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("BetId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("BetSeenAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("GameName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<float>("Multiplier")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Payout")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("PublicId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RainbetUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<float>("Value")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("RainbetBets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.SettingDbModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Default")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSecret")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Regex")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TwitchViewCountDbModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("ServerTime")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Topic")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Viewers")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TwitchViewCounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserDbModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Ignored")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("KfId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KfUsername")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
46
KfChatDotNetBot/Migrations/20240817130843_Chipsgg.cs
Normal file
46
KfChatDotNetBot/Migrations/20240817130843_Chipsgg.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace KfChatDotNetBot.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Chipsgg : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChipsggBets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Created = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Username = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Win = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Winnings = table.Column<double>(type: "REAL", nullable: false),
|
||||
GameTitle = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Amount = table.Column<double>(type: "REAL", nullable: false),
|
||||
Multiplier = table.Column<float>(type: "REAL", nullable: false),
|
||||
Currency = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CurrencyPrice = table.Column<float>(type: "REAL", nullable: false),
|
||||
BetId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChipsggBets", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChipsggBets");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,58 @@ namespace KfChatDotNetBot.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
|
||||
|
||||
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ChipsggBetDbModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("Amount")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("BetId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<float>("CurrencyPrice")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("GameTitle")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<float>("Multiplier")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateTimeOffset>("Updated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Win")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("Winnings")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ChipsggBets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.HowlggBetsDbModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
28
KfChatDotNetBot/Models/ChipsggModels.cs
Normal file
28
KfChatDotNetBot/Models/ChipsggModels.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace KfChatDotNetBot.Models;
|
||||
|
||||
public class ChipsggBetModel
|
||||
{
|
||||
public DateTimeOffset Created { get; set; }
|
||||
// Can actually get the duration of a game from this
|
||||
public DateTimeOffset Updated { get; set; }
|
||||
public string UserId { get; set; }
|
||||
// Sometimes null for no discernible reason
|
||||
public string? Username { get; set; }
|
||||
// Win of any amount even if it's less than a 1x multi
|
||||
public bool Win { get; set; }
|
||||
public double Winnings { get; set; }
|
||||
public string? GameTitle { get; set; }
|
||||
public double Amount { get; set; }
|
||||
public float Multiplier { get; set; }
|
||||
public string? Currency { get; set; }
|
||||
public float CurrencyPrice { get; set; }
|
||||
public string BetId { get; set; }
|
||||
}
|
||||
|
||||
public class ChipsggCurrencyModel
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required int Decimals { get; set; }
|
||||
public float? Price { get; set; }
|
||||
public required bool Hidden { get; set; }
|
||||
}
|
||||
18
KfChatDotNetBot/Models/DbModels/ChipsggBetDbModel.cs
Normal file
18
KfChatDotNetBot/Models/DbModels/ChipsggBetDbModel.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace KfChatDotNetBot.Models.DbModels;
|
||||
|
||||
public class ChipsggBetDbModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required DateTimeOffset Created { get; set; }
|
||||
public required DateTimeOffset Updated { get; set; }
|
||||
public required string UserId { get; set; }
|
||||
public required string Username { get; set; }
|
||||
public required bool Win { get; set; }
|
||||
public required double Winnings { get; set; }
|
||||
public required string GameTitle { get; set; }
|
||||
public required double Amount { get; set; }
|
||||
public required float Multiplier { get; set; }
|
||||
public required string Currency { get; set; }
|
||||
public required float CurrencyPrice { get; set; }
|
||||
public required string BetId { get; set; }
|
||||
}
|
||||
332
KfChatDotNetBot/Services/Chipsgg.cs
Normal file
332
KfChatDotNetBot/Services/Chipsgg.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
using KfChatDotNetBot.Models;
|
||||
using NLog;
|
||||
using Websocket.Client;
|
||||
|
||||
namespace KfChatDotNetBot.Services;
|
||||
|
||||
public class Chipsgg : IDisposable
|
||||
{
|
||||
private Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
private WebsocketClient _wsClient;
|
||||
private Uri _wsUri = new("wss://api.chips.gg/prod/socket");
|
||||
// Chips doesn't have a heartbeat packet
|
||||
private int _reconnectTimeout = 30;
|
||||
private string? _proxy;
|
||||
public delegate void OnChipsggRecentBetEventHandler(object sender, ChipsggBetModel bet);
|
||||
public delegate void OnWsDisconnectionEventHandler(object sender, DisconnectionInfo e);
|
||||
public event OnChipsggRecentBetEventHandler OnChipsggRecentBet;
|
||||
private CancellationToken _cancellationToken = CancellationToken.None;
|
||||
private Dictionary<string, ChipsggCurrencyModel> _currencies = new();
|
||||
|
||||
public Chipsgg(string? proxy = null, CancellationToken? cancellationToken = null)
|
||||
{
|
||||
_proxy = proxy;
|
||||
if (cancellationToken != null) _cancellationToken = cancellationToken.Value;
|
||||
_logger.Info("Chipsgg WebSocket client created");
|
||||
}
|
||||
|
||||
public async Task StartWsClient()
|
||||
{
|
||||
_logger.Debug("StartWsClient() called, creating client");
|
||||
await CreateWsClient();
|
||||
}
|
||||
|
||||
private async Task CreateWsClient()
|
||||
{
|
||||
var factory = new Func<ClientWebSocket>(() =>
|
||||
{
|
||||
var clientWs = new ClientWebSocket();
|
||||
clientWs.Options.SetRequestHeader("Origin", "https://chips.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),
|
||||
IsReconnectionEnabled = false
|
||||
};
|
||||
|
||||
client.ReconnectionHappened.Subscribe(WsReconnection);
|
||||
client.MessageReceived.Subscribe(WsMessageReceived);
|
||||
client.DisconnectionHappened.Subscribe(WsDisconnection);
|
||||
|
||||
_wsClient = client;
|
||||
|
||||
_logger.Debug("Websocket client has been built, about to start");
|
||||
await client.Start();
|
||||
_logger.Debug("Websocket client started!");
|
||||
}
|
||||
|
||||
public bool IsConnected()
|
||||
{
|
||||
return _wsClient is { IsRunning: true };
|
||||
}
|
||||
|
||||
private void WsDisconnection(DisconnectionInfo disconnectionInfo)
|
||||
{
|
||||
_logger.Error($"Client disconnected from 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);
|
||||
}
|
||||
|
||||
private void WsReconnection(ReconnectionInfo reconnectionInfo)
|
||||
{
|
||||
_logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}");
|
||||
if (reconnectionInfo.Type == ReconnectionType.Initial)
|
||||
{
|
||||
_logger.Info("Sending auth payload to Chips.gg");
|
||||
_wsClient.Send("[\"auth\",1,\"token\",[]]");
|
||||
}
|
||||
}
|
||||
|
||||
private void WsMessageReceived(ResponseMessage message)
|
||||
{
|
||||
if (message.Text == null)
|
||||
{
|
||||
_logger.Info("Chips.gg sent a null message");
|
||||
return;
|
||||
}
|
||||
_logger.Trace($"Received event from Chips.gg: {message.Text}");
|
||||
|
||||
try
|
||||
{
|
||||
// Chipsgg has literally the most retarded "structure" to their packets I have ever seen
|
||||
// I was hoping BMJ would dump their lousy site, but it hasn't happened yet
|
||||
// Everything is arrays within arrays.
|
||||
// For bets, each element of the outer array is a property related to the bet
|
||||
// For auth / currency stuff there's only one element which contains more arrays
|
||||
var payload = JsonSerializer.Deserialize<List<JsonElement>>(message.Text);
|
||||
if (payload == null || payload.Count == 0)
|
||||
{
|
||||
throw new Exception("Chips.gg sent us an empty array or I could not deserialize it");
|
||||
}
|
||||
|
||||
var firstElement = payload[0].Deserialize<List<JsonElement>>();
|
||||
if (firstElement == null || firstElement.Count < 3)
|
||||
{
|
||||
throw new Exception("Chips.gg's first element was smaller than expected or null");
|
||||
}
|
||||
|
||||
if (firstElement[0].GetString() == "auth")
|
||||
{
|
||||
if (firstElement[1].GetInt32() == 1)
|
||||
{
|
||||
var guid = firstElement[2].GetString();
|
||||
_logger.Debug("Received auth packet, sending back GUID auth with " + guid);
|
||||
_wsClient.Send("[\"auth\",2,\"authenticate\",[\"" + guid+ "\"]]");
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstElement[1].GetInt32() == 2)
|
||||
{
|
||||
_logger.Info("Chips.gg responded to our auth with: " + firstElement[2].GetString());
|
||||
_logger.Info("Sending Chips.gg recent bets subscription packet");
|
||||
_wsClient.Send("[\"stats\",12,\"on\",[{\"game\":\"bets\",\"type\":\"recentBets\"}]]");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception("Auth packet was unhandled");
|
||||
}
|
||||
|
||||
// First packet after auth is a currency + settings payload
|
||||
// This will also match for periodic currency updates
|
||||
if (firstElement[0].GetString() == "public")
|
||||
{
|
||||
var dataElement = firstElement[2].Deserialize<List<JsonElement>>();
|
||||
if (dataElement == null || dataElement.Count < 2)
|
||||
{
|
||||
throw new Exception(
|
||||
"Caught a null when grabbing data from the first element of the array or got fewer items than expected");
|
||||
}
|
||||
|
||||
var path = dataElement[0].Deserialize<List<string>>();
|
||||
if (path == null)
|
||||
throw new Exception("Caught a null when deserializing the path element of the array");
|
||||
if (path.Count == 0)
|
||||
{
|
||||
_logger.Debug("Received initial currency payload as the path array was empty");
|
||||
var currencyData = dataElement[1].Deserialize<Dictionary<string, JsonElement>>();
|
||||
if (currencyData == null) throw new Exception("Caught a null when deserializing currency data");
|
||||
if (!currencyData.TryGetValue("currencies", out var val)) throw new Exception("Currency object didn't contain expected currencies property");
|
||||
var currencies = val.Deserialize<Dictionary<string, JsonElement>>();
|
||||
if (currencies == null) throw new Exception("Caught a null when deserializing currency dictionary");
|
||||
foreach (var currency in currencies.Keys)
|
||||
{
|
||||
// Should never happen but you never know
|
||||
if (_currencies.ContainsKey(currency)) return;
|
||||
float? price = null;
|
||||
// Where a price is not set, the element is simply missing
|
||||
if (currencies[currency].TryGetProperty("price", out var priceElement))
|
||||
{
|
||||
price = priceElement.GetSingle();
|
||||
}
|
||||
_currencies.Add(currency, new ChipsggCurrencyModel
|
||||
{
|
||||
Decimals = currencies[currency].GetProperty("decimals").GetInt32(),
|
||||
Name = currency,
|
||||
// Hidden is only present when it's true
|
||||
Hidden = currencies[currency].TryGetProperty("hidden", out _),
|
||||
Price = price
|
||||
});
|
||||
_logger.Debug($"Ingested currency data for {currency}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var element in payload)
|
||||
{
|
||||
var data = element.Deserialize<List<JsonElement>>();
|
||||
if (data == null || data.Count < 3) throw new Exception("Caught null or received fewer than 3 elements in the data array");
|
||||
var innerData = data[2].Deserialize<List<JsonElement>>();
|
||||
if (innerData == null || data.Count < 2) throw new Exception("Caught null or received fewer than 2 elements in the inner data array");
|
||||
var innerDataPath = innerData[0].Deserialize<List<string>>();
|
||||
if (innerDataPath == null || innerDataPath.Count == 0) throw new Exception("innerDataPath was null or contained no elements");
|
||||
if (innerDataPath.Contains("metrics")) continue;
|
||||
var currency = innerDataPath[1];
|
||||
if (_currencies.TryGetValue(currency, out var updatedCurrency))
|
||||
{
|
||||
updatedCurrency.Price = innerData[1].GetSingle();
|
||||
}
|
||||
_logger.Debug($"Updated currency data for {currency}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstElement[0].GetString() == "stats")
|
||||
{
|
||||
if (firstElement[1].ValueKind == JsonValueKind.Number && firstElement[1].TryGetInt32(out var type))
|
||||
{
|
||||
// 12 is the replay of recent bets
|
||||
if (type == 12) return;
|
||||
}
|
||||
// Currency data may not be known until after so hold it here til we're done parsing
|
||||
var amount = string.Empty;
|
||||
var winnings = string.Empty;
|
||||
var bet = new ChipsggBetModel();
|
||||
foreach (var element in payload)
|
||||
{
|
||||
var data = element.Deserialize<List<JsonElement>>();
|
||||
if (data == null || data.Count < 3) throw new Exception("Caught null or received fewer than 3 elements in the data array");
|
||||
var innerData = data[2].Deserialize<List<JsonElement>>();
|
||||
if (innerData == null || data.Count < 2) throw new Exception("Caught null or received fewer than 2 elements in the inner data array");
|
||||
var innerDataPath = innerData[0].Deserialize<List<string>>();
|
||||
if (innerDataPath == null || innerDataPath.Count == 0) throw new Exception("innerDataPath was null or contained no elements");
|
||||
var innerDataPathJoined = string.Join(':', innerDataPath);
|
||||
// For some reason there are ghostly bets sent alongside real bets whose values are all null
|
||||
if (innerData[1].ValueKind == JsonValueKind.Null) continue;
|
||||
if (innerDataPathJoined.EndsWith("bet:done"))
|
||||
{
|
||||
if (innerData[1].GetBoolean() == false)
|
||||
{
|
||||
_logger.Debug("Bet not yet complete, ignoring");
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Just piggybacking it for a reliable path to grab the bet ID from
|
||||
if (innerDataPathJoined.EndsWith("bet:created"))
|
||||
{
|
||||
bet.BetId = innerDataPath[2];
|
||||
bet.Created = DateTimeOffset.FromUnixTimeMilliseconds(innerData[1].GetInt64());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerDataPathJoined.EndsWith("bet:updated"))
|
||||
{
|
||||
bet.Updated = DateTimeOffset.FromUnixTimeMilliseconds(innerData[1].GetInt64());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerDataPathJoined.EndsWith("bet:userid"))
|
||||
{
|
||||
bet.UserId = innerData[1].GetString()!;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerDataPathJoined.EndsWith("player:username"))
|
||||
{
|
||||
bet.Username = innerData[1].GetString()!;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerDataPathJoined.EndsWith("bet:win"))
|
||||
{
|
||||
bet.Win = innerData[1].GetBoolean();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerDataPathJoined.EndsWith("bet:winnings"))
|
||||
{
|
||||
winnings = innerData[1].GetString()!;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerDataPathJoined.EndsWith("game:title"))
|
||||
{
|
||||
bet.GameTitle = innerData[1].GetString()!;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerDataPathJoined.EndsWith("bet:amount"))
|
||||
{
|
||||
amount = innerData[1].GetString()!;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerDataPathJoined.EndsWith("bet:multiplier"))
|
||||
{
|
||||
bet.Multiplier = innerData[1].GetSingle();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerDataPathJoined.EndsWith("bet:currency"))
|
||||
{
|
||||
bet.Currency = innerData[1].GetString()!;
|
||||
}
|
||||
}
|
||||
|
||||
// Just something that randomly happens where incomplete bets are sent
|
||||
// It seems that occasionally a bet is sent through with no proper game title or username
|
||||
// Since the feed in theory can't display these, I'm assuming it's another ghost and not a real bet
|
||||
if (bet.Currency == null || bet.GameTitle == null) return;
|
||||
if (!_currencies.TryGetValue(bet.Currency, out var currencyData))
|
||||
{
|
||||
throw new Exception($"Unknown currency {bet.Currency}");
|
||||
}
|
||||
|
||||
// Another mysterious thing where winnings are sometimes sent and sometimes not. Presumed to be 0
|
||||
if (winnings == string.Empty) winnings = "0";
|
||||
bet.Winnings = double.Parse(winnings) / double.Parse(1.ToString().PadRight(currencyData.Decimals, '0'));
|
||||
bet.Amount = double.Parse(amount) / double.Parse(1.ToString().PadRight(currencyData.Decimals, '0'));
|
||||
bet.CurrencyPrice = currencyData.Price ?? 0;
|
||||
OnChipsggRecentBet?.Invoke(this, bet);
|
||||
return;
|
||||
}
|
||||
_logger.Debug("Unhandled event from Chips.gg");
|
||||
_logger.Debug(message.Text);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error("Failed to handle message from Chips.gg");
|
||||
_logger.Error(e);
|
||||
_logger.Error("--- Payload ---");
|
||||
_logger.Error(message.Text);
|
||||
_logger.Error("--- End of Payload ---");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_wsClient.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -376,6 +376,14 @@ public static class BuiltIn
|
||||
Description = "Proxy in use specifically for FlareSolverr",
|
||||
Default = null,
|
||||
IsSecret = false
|
||||
},
|
||||
new BuiltInSettingsModel
|
||||
{
|
||||
Key = Keys.ChipsggBmjUsername,
|
||||
Regex = ".+",
|
||||
Description = "Bossman's Chips.gg username",
|
||||
Default = "TheBossmanJack",
|
||||
IsSecret = false
|
||||
}
|
||||
];
|
||||
|
||||
@@ -414,5 +422,6 @@ public static class BuiltIn
|
||||
public static string RainbetBmjPublicId = "Rainbet.BmjPublicId";
|
||||
public static string FlareSolverrApiUrl = "FlareSolverr.ApiUrl";
|
||||
public static string FlareSolverrProxy = "FlareSolverr.Proxy";
|
||||
public static string ChipsggBmjUsername = "Chipsgg.BmjUsername";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user