mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
Experimental convoluted rain refactor to use Redis instead of semaphores
This commit is contained in:
@@ -529,6 +529,31 @@ public class ChatBot
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for a chat message to be successfully delivered or not
|
||||
/// </summary>
|
||||
/// <param name="message">Reference to the message you're waiting for</param>
|
||||
/// <param name="patience">How long to wait</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>True if the message was echoed, false otherwise</returns>
|
||||
public async Task<bool> WaitForChatMessageAsync(SentMessageTrackerModel message, TimeSpan? patience = null, CancellationToken ct = default)
|
||||
{
|
||||
if (patience == null)
|
||||
{
|
||||
patience = TimeSpan.FromSeconds(60);
|
||||
}
|
||||
|
||||
var patienceEnds = DateTimeOffset.UtcNow.Add(patience.Value);
|
||||
while (message.ChatMessageId == null)
|
||||
{
|
||||
if (DateTimeOffset.UtcNow > patienceEnds) return false;
|
||||
if (message.Status is SentMessageTrackerStatus.Lost or SentMessageTrackerStatus.NotSending) return false;
|
||||
await Task.Delay(100, ct);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public class SentMessageNotFoundException : Exception;
|
||||
|
||||
private void OnUsersJoined(object sender, List<UserModel> users, UsersJsonModel jsonPayload)
|
||||
|
||||
@@ -1,166 +1,15 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using KfChatDotNetBot.Extensions;
|
||||
using KfChatDotNetBot.Models;
|
||||
using KfChatDotNetBot.Models.DbModels;
|
||||
using KfChatDotNetBot.Services;
|
||||
using KfChatDotNetBot.Settings;
|
||||
using KfChatDotNetWsClient.Models.Events;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
namespace KfChatDotNetBot.Commands.Kasino;
|
||||
|
||||
[KasinoCommand]
|
||||
[WagerCommand]
|
||||
public class RainCommand : ICommand
|
||||
{
|
||||
public static class RainManager
|
||||
{
|
||||
public static Dictionary<int,Rain> rainLobbies { get; } = new(); //dictionary of lobbies with the creators id and the lobby
|
||||
public static readonly SemaphoreSlim _lock = new(1, 1);
|
||||
public static ChatBot botInstance;
|
||||
|
||||
|
||||
public static async Task<bool> AddParticipant(GamblerDbModel gambler, UserDbModel user)
|
||||
{
|
||||
await _lock.WaitAsync();
|
||||
bool openLobby = false;
|
||||
try
|
||||
{
|
||||
foreach (var lobby in rainLobbies.Values)
|
||||
{
|
||||
if (lobby.open)
|
||||
{
|
||||
lobby.AddParticipant(gambler);
|
||||
openLobby = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
return openLobby;
|
||||
}
|
||||
|
||||
public static async Task<bool> AddRain(int gamblerId, Rain rain)
|
||||
{
|
||||
await _lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!rainLobbies.TryGetValue(gamblerId, out var lobby))
|
||||
{
|
||||
rainLobbies.Add(gamblerId, lobby);
|
||||
}
|
||||
else //can't start multiple rains at the same time
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{lobby.creator.FormatUsername()}, you can't start multiple rains at the same time.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task PayoutRain(Rain rain)
|
||||
{
|
||||
if (rain.participants.Count == 0)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync($"{rain.creator.FormatUsername()} made it rain on nobody.");
|
||||
await Money.ModifyBalanceAsync(rain.creatorGambler.Id, -rain.rainAmount, TransactionSourceEventType.Rain);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
decimal payout = rain.rainAmount / rain.participants.Count;
|
||||
string rainParticipants = "";
|
||||
int counter = 0;
|
||||
foreach (var participant in rain.participants)
|
||||
{
|
||||
rainParticipants += (rain.participants.Count > 1 && counter == rain.participants.Count - 1) ? $"and {participant.User.FormatUsername()}" : $"{participant.User.FormatUsername()}, ";
|
||||
await Money.ModifyBalanceAsync(participant.Id, payout, TransactionSourceEventType.Rain);
|
||||
counter++;
|
||||
}
|
||||
if (rain.participants.Count == 1) rainParticipants = rain.participants[0].User.FormatUsername();
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{rain.creator.FormatUsername()} made it rain {payout.FormatKasinoCurrencyAsync()} on {rainParticipants}",
|
||||
true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
||||
await Money.ModifyBalanceAsync(rain.creatorGambler.Id, -rain.rainAmount, TransactionSourceEventType.Rain);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task RemoveRain(Rain rain)
|
||||
{
|
||||
await _lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
rainLobbies.Remove(rain.creatorGambler.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
public class Rain
|
||||
{
|
||||
public List<GamblerDbModel> participants = new List<GamblerDbModel>();
|
||||
public UserDbModel creator;
|
||||
public GamblerDbModel creatorGambler;
|
||||
public DateTime startTime { get; } = DateTime.UtcNow;
|
||||
public bool open = true;
|
||||
public decimal rainAmount;
|
||||
|
||||
public Rain(UserDbModel user, GamblerDbModel gambler, decimal wager)
|
||||
{
|
||||
creator = user;
|
||||
creatorGambler = gambler;
|
||||
rainAmount = wager;
|
||||
_ = RunRain();
|
||||
}
|
||||
|
||||
public void AddParticipant(GamblerDbModel gambler)
|
||||
{
|
||||
participants.Add(gambler);
|
||||
}
|
||||
|
||||
public async Task RunRain()
|
||||
{
|
||||
var chatTimer = TimeSpan.FromSeconds(5);
|
||||
int rainTimer = 60;
|
||||
var msgId = await botInstance.SendChatMessageAsync(
|
||||
$"{creator.FormatUsername()} is making it rain with {rainAmount.FormatKasinoCurrencyAsync()}! You have {rainTimer} seconds left to join!", true,
|
||||
autoDeleteAfter: TimeSpan.FromSeconds(rainTimer));
|
||||
int num = 0;
|
||||
while (msgId.ChatMessageId == null)
|
||||
{
|
||||
num++;
|
||||
if (msgId.Status is SentMessageTrackerStatus.NotSending or SentMessageTrackerStatus.Lost) return;
|
||||
if (num > 100) return;
|
||||
await Task.Delay(100);
|
||||
}
|
||||
for (int i = 0; i < rainTimer / Convert.ToInt32(chatTimer); i++)
|
||||
{
|
||||
await Task.Delay(chatTimer);
|
||||
await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageId!.Value,
|
||||
$"{creator.FormatUsername()} is making it rain with {rainAmount.FormatKasinoCurrencyAsync()}! You have {rainTimer} seconds left to join!");
|
||||
}
|
||||
open = false;
|
||||
await PayoutRain(this);
|
||||
await RemoveRain(this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public List<Regex> Patterns => [
|
||||
new Regex(@"^rain (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
||||
new Regex(@"^rain (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
||||
@@ -169,17 +18,21 @@ public class RainCommand : ICommand
|
||||
|
||||
public string? HelpText => "!rain <amount> to start a rain, !rain to join all active rains";
|
||||
public UserRight RequiredRight => UserRight.Loser;
|
||||
public TimeSpan Timeout => TimeSpan.FromSeconds(30);
|
||||
public TimeSpan Timeout => TimeSpan.FromSeconds(60);
|
||||
public RateLimitOptionsModel? RateLimitOptions => null;
|
||||
|
||||
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
|
||||
CancellationToken ctx)
|
||||
{
|
||||
if (!RainManager.botInstance.Equals(botInstance))
|
||||
{
|
||||
RainManager.botInstance = botInstance;
|
||||
}
|
||||
var cleanupDelay = TimeSpan.FromSeconds(30);
|
||||
if (botInstance.BotServices.KasinoRain == null || !botInstance.BotServices.KasinoRain.IsInitialized())
|
||||
{
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, rain is not available at this time", true,
|
||||
autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
var rain = await botInstance.BotServices.KasinoRain.GetRainState();
|
||||
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
||||
if (gambler == null)
|
||||
{
|
||||
@@ -187,62 +40,75 @@ public class RainCommand : ICommand
|
||||
}
|
||||
if (!arguments.TryGetValue("amount", out var amount)) //if you're trying to join a rain
|
||||
{
|
||||
if (RainManager.rainLobbies.Count == 0) //if there are no lobbies
|
||||
if (rain == null) //if there are no lobbies
|
||||
{
|
||||
botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, there are no rain lobbies currently running. !rain <amount> to start a new rain lobby",
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, there's no rain currently running. !rain <amount> to start a new rain",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
if (rain.Participants.Contains(user.Id))
|
||||
{
|
||||
bool success = await RainManager.AddParticipant(gambler, user);
|
||||
if (!success)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, there are no rain lobbies currently running. !rain <amount> to start a new rain lobby",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
string rainCreators = "";
|
||||
await RainManager._lock.WaitAsync();
|
||||
int lobbyCount = 0;
|
||||
try
|
||||
{
|
||||
foreach (var lobby in RainManager.rainLobbies.Values)
|
||||
{
|
||||
rainCreators += (RainManager.rainLobbies.Count > 1 && lobbyCount == RainManager.rainLobbies.Count - 1) ? $"and {lobby.creator.FormatUsername()}": $"{lobby.creator.FormatUsername()}, ";
|
||||
lobbyCount++;
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
RainManager._lock.Release();
|
||||
}
|
||||
|
||||
string areIs = RainManager.rainLobbies.Count > 1 ? "are" : "is";
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{rainCreators} {areIs} making it rain on {user.FormatUsername()}!",true, autoDeleteAfter: cleanupDelay);
|
||||
|
||||
}
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, you're already participating in this rain!", true,
|
||||
autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
await botInstance.BotServices.KasinoRain.AddParticipant(user.Id);
|
||||
var pluralSuffix = string.Empty;
|
||||
if (rain.Participants.Count > 0) pluralSuffix = "s";
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"LFG {user.FormatUsername()} is now a participant! There's now {rain.Participants.Count} participant{pluralSuffix}",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
//if you're trying to start the rain
|
||||
decimal decAmount = Convert.ToDecimal(amount.Value);
|
||||
if (decAmount <= 0)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you can't make it rain with nothing.", true, autoDeleteAfter: cleanupDelay);
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you can't make it rain with nothing.",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
if (gambler.Balance < decAmount)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, your balance ${gambler.Balance} KKK is not enough to make it rain for ${decAmount} KKK.", true, autoDeleteAfter: cleanupDelay);
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, your balance {await gambler.Balance.FormatKasinoCurrencyAsync()} is not enough to make it rain for {await decAmount.FormatKasinoCurrencyAsync()}.",
|
||||
true, autoDeleteAfter: cleanupDelay);
|
||||
return;
|
||||
}
|
||||
await RainManager.AddRain(gambler.Id,new RainManager.Rain(user, gambler, decAmount));
|
||||
|
||||
rain = new KasinoRainModel
|
||||
{
|
||||
Participants = [],
|
||||
Creator = user.Id,
|
||||
Started = DateTimeOffset.UtcNow,
|
||||
RainAmount = decAmount,
|
||||
PayoutWhen = DateTimeOffset.MaxValue
|
||||
};
|
||||
var timer = 60;
|
||||
var msg = await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()} is making it rain with {await decAmount.FormatKasinoCurrencyAsync()}! Type !rain in the next {timer} seconds to join.",
|
||||
true);
|
||||
var result = await botInstance.WaitForChatMessageAsync(msg, ct: ctx);
|
||||
if (!result)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to send chat message for the rain. Not going to proceed with it");
|
||||
}
|
||||
|
||||
// Wait to set a real payout deadline only when chyat echoes the message out of fairness
|
||||
// (and also so the timer doesn't overlap with the payout deadline)
|
||||
rain.PayoutWhen = DateTimeOffset.UtcNow.AddSeconds(60);
|
||||
await botInstance.BotServices.KasinoRain.SaveRainState(rain);
|
||||
while (timer > 0)
|
||||
{
|
||||
timer--;
|
||||
await botInstance.KfClient.EditMessageAsync(msg.ChatMessageId!.Value,
|
||||
$"{user.FormatUsername()} is making it rain with {await decAmount.FormatKasinoCurrencyAsync()}! Type !rain in the next {timer} seconds to join.");
|
||||
}
|
||||
await botInstance.KfClient.DeleteMessageAsync(msg.ChatMessageId!.Value);
|
||||
// At this point the timer should take care of things but truthfully it's
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public class BotServices
|
||||
private Owncast? _owncastStatusCheck;
|
||||
private ShuffleDotUs? _shuffleDotUs;
|
||||
private YouTubePubSub? _youTubePubSub;
|
||||
public KasinoRain? KasinoRain;
|
||||
|
||||
private Task? _websocketWatchdog;
|
||||
private Task? _howlggGetUserTimer;
|
||||
@@ -91,7 +92,8 @@ public class BotServices
|
||||
BuildPeerTubeLiveStatusCheck(),
|
||||
BuildOwncastLiveStatusCheck(),
|
||||
BuildShuffleDotUs(),
|
||||
BuildYouTubePubSub()
|
||||
BuildYouTubePubSub(),
|
||||
BuildKasinoRain()
|
||||
];
|
||||
try
|
||||
{
|
||||
@@ -107,6 +109,12 @@ public class BotServices
|
||||
_websocketWatchdog = WebsocketWatchdog();
|
||||
_howlggGetUserTimer = HowlggGetUserTimer();
|
||||
}
|
||||
|
||||
private async Task BuildKasinoRain()
|
||||
{
|
||||
_logger.Debug("Building the Kasino Rain thingy");
|
||||
KasinoRain = new KasinoRain(_chatBot, _cancellationToken);
|
||||
}
|
||||
|
||||
private async Task BuildShuffle()
|
||||
{
|
||||
|
||||
145
KfChatDotNetBot/Services/KasinoRain.cs
Normal file
145
KfChatDotNetBot/Services/KasinoRain.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.Text.Json;
|
||||
using KfChatDotNetBot.Extensions;
|
||||
using KfChatDotNetBot.Models.DbModels;
|
||||
using KfChatDotNetBot.Settings;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NLog;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace KfChatDotNetBot.Services;
|
||||
|
||||
public class KasinoRain : IDisposable
|
||||
{
|
||||
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
private Task? _rainTimerTask;
|
||||
private IDatabase? _redisDb;
|
||||
private ChatBot _kfChatBot;
|
||||
private CancellationToken _ct;
|
||||
private CancellationTokenSource _rainCts = new();
|
||||
|
||||
public KasinoRain(ChatBot kfChatBot, CancellationToken ct = default)
|
||||
{
|
||||
_kfChatBot = kfChatBot;
|
||||
_ct = ct;
|
||||
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result;
|
||||
if (string.IsNullOrEmpty(connectionString.Value))
|
||||
{
|
||||
_logger.Error($"Can't initialize the Kasino Rain service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}");
|
||||
return;
|
||||
}
|
||||
|
||||
var redis = ConnectionMultiplexer.Connect(connectionString.Value);
|
||||
_redisDb = redis.GetDatabase();
|
||||
_rainTimerTask = Task.Run(RainTimerTask, ct);
|
||||
}
|
||||
|
||||
public bool IsInitialized()
|
||||
{
|
||||
return _redisDb != null;
|
||||
}
|
||||
|
||||
public async Task AddParticipant(int userId)
|
||||
{
|
||||
var data = await GetRainState();
|
||||
if (data == null) throw new InvalidOperationException("Failed to retrieve state or no rain is in progress");
|
||||
if (data.Participants.Contains(userId)) return;
|
||||
data.Participants.Add(userId);
|
||||
await SaveRainState(data);
|
||||
}
|
||||
|
||||
public async Task RemoveRainState()
|
||||
{
|
||||
if (_redisDb == null) throw new InvalidOperationException("Kasino Rain service isn't initialized");
|
||||
await _redisDb.KeyDeleteAsync("Rain.State");
|
||||
}
|
||||
|
||||
public async Task<KasinoRainModel?> GetRainState()
|
||||
{
|
||||
if (_redisDb == null) throw new InvalidOperationException("Kasino Rain service isn't initialized");
|
||||
var json = await _redisDb.StringGetAsync("Rain.State");
|
||||
if (string.IsNullOrEmpty(json)) return null;
|
||||
var data = JsonSerializer.Deserialize<KasinoRainModel>(json.ToString());
|
||||
return data;
|
||||
}
|
||||
|
||||
public async Task SaveRainState(KasinoRainModel rain)
|
||||
{
|
||||
if (_redisDb == null) throw new InvalidOperationException("Kasino Rain service isn't initialized");
|
||||
var json = JsonSerializer.Serialize(rain);
|
||||
await _redisDb.StringSetAsync("Rain.State", json, null, When.Always);
|
||||
}
|
||||
|
||||
private async Task RainTimerTask()
|
||||
{
|
||||
var interval = TimeSpan.FromSeconds(1);
|
||||
using var timer = new PeriodicTimer(interval);
|
||||
while (await timer.WaitForNextTickAsync(_ct))
|
||||
{
|
||||
var rain = await GetRainState();
|
||||
if (rain == null) continue;
|
||||
if (DateTimeOffset.UtcNow < rain.PayoutWhen) continue;
|
||||
var creator = await Money.GetGamblerEntityAsync(rain.Creator, ct: _ct);
|
||||
if (creator == null)
|
||||
{
|
||||
_logger.Error($"Somehow this rain was created by a non-existent (or banned) gambler with user ID {rain.Creator}? Wiping this fucked up state");
|
||||
await RemoveRainState();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rain.Participants.Count == 0)
|
||||
{
|
||||
await _kfChatBot.SendChatMessageAsync(
|
||||
$"Nobody participated in {creator.User.FormatUsername()}'s rain!",
|
||||
true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rain.RainAmount > creator.Balance)
|
||||
{
|
||||
await _kfChatBot.SendChatMessageAsync(
|
||||
$"{creator.User.FormatUsername()} lost it all before he could bless everyone! The giveaway is canceled! :lossmanjack:",
|
||||
true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
||||
await RemoveRainState();
|
||||
continue;
|
||||
}
|
||||
|
||||
List<string> participantNames = [];
|
||||
var payout = rain.RainAmount / rain.Participants.Count;
|
||||
decimal failedPayoutAmount = 0;
|
||||
foreach (var participant in rain.Participants)
|
||||
{
|
||||
var gambler = await Money.GetGamblerEntityAsync(participant, ct: _ct);
|
||||
if (gambler == null)
|
||||
{
|
||||
_logger.Error($"Somehow this participant ({participant}) does not have a gambler entity or has been banned");
|
||||
failedPayoutAmount += payout;
|
||||
continue;
|
||||
}
|
||||
participantNames.Add(gambler.User.FormatUsername());
|
||||
await Money.ModifyBalanceAsync(gambler.Id, payout, TransactionSourceEventType.Rain,
|
||||
"Payout from rain event", creator.Id, _ct);
|
||||
}
|
||||
|
||||
await Money.ModifyBalanceAsync(creator.Id, -rain.RainAmount + failedPayoutAmount, TransactionSourceEventType.Rain,
|
||||
$"Rained on {participantNames.Count} people", ct: _ct);
|
||||
await _kfChatBot.SendChatMessageAsync(
|
||||
$"{creator.User.FormatUsername()} made it rain {await payout.FormatKasinoCurrencyAsync()} on {string.Join(' ', participantNames)}",
|
||||
true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
||||
await RemoveRainState();
|
||||
}
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
_rainTimerTask?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class KasinoRainModel
|
||||
{
|
||||
public required List<int> Participants { get; set; } = [];
|
||||
public required int Creator { get; set; }
|
||||
public required DateTimeOffset Started { get; set; }
|
||||
public required decimal RainAmount { get; set; }
|
||||
public required DateTimeOffset PayoutWhen { get; set; }
|
||||
}
|
||||
@@ -504,6 +504,8 @@ public static class BuiltIn
|
||||
public static string KasinoPlinkoEnabled = "Kasino.Plinko.Enabled";
|
||||
[BuiltInSetting("Whether Xeet posting is enabled", SettingValueType.Boolean, "true", BooleanRegex)]
|
||||
public static string XeetEnabled = "Xeet.Enabled";
|
||||
[BuiltInSetting("Connection string for bot's Redis", SettingValueType.Text)]
|
||||
public static string BotRedisConnectionString = "Bot.RedisConnectionString";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user