Compare commits

...

52 Commits

Author SHA1 Message Date
barelyprofessional d9ba821b88 Use the HTML decoded message so regex matches don't have HTML entities 2026-05-14 21:43:51 -05:00
barelyprofessional e064c37477 Change equals to endswith as I think it's getting felted by the command prefix 2026-05-14 21:37:50 -05:00
A Log in D Tractor 95608dcb12 Update PlinkoCommand.cs (#118)
fix frame not sized issue
2026-05-15 02:18:27 +02:00
barelyprofessional 66d66c8640 Show image carousel in meta so you can tell where an image belonged 2026-05-14 07:07:10 -05:00
alogindtractor 277207215a small plinko (#117)
* Update multi-tracker display format in PlanesCommand

* small plinko

small plinko

* Add Kasino Plinko Size setting
2026-05-14 14:05:23 +02:00
barelyprofessional b346a3b303 Experimental untagged and also search any carousel command 2026-05-14 07:04:50 -05:00
barelyprofessional b170065ca3 Add a trailing space to the ditto for image tagging 2026-05-13 21:22:26 -05:00
barelyprofessional 520413c3da Moved the tagnag to before the meta and removed the grid to table. It did work but was still spaced a little ugly. Having the spoiler last should fix spacing issues 2026-05-13 21:11:24 -05:00
barelyprofessional aa25b06ebe Use a table to try and deal with the ugly as hell spacing if the tagnag happens 2026-05-13 21:04:41 -05:00
barelyprofessional 9c5e364859 Added image metadata to returned images 2026-05-13 20:59:57 -05:00
alogindtractor 1d197ca495 slots update (#116)
found 120% rtp in test, idk if i fucked it up last time or this time but i nerfed feature and wild chance for now, planning a better update later

new rtp settings
RTP: 98.37% | Feach Chance: 0.14% | Hit Rate: 45.59% | Win Rate: 17.89% | Biggest Win: 38100.0x | Avg Win: 4.96x | Median Hit: 0.5x
from 32 mil tests
2026-05-12 14:05:51 +02:00
alogindtractor 00462f5a8b Update multi-tracker display format in PlanesCommand (#115) 2026-05-12 05:06:52 +02:00
alogindtractor d157d0a0a0 small planes (#114)
* small planes

small planes

* Add KasinoPlanesSize setting for Planes board

Add KasinoPlanesSize setting for Planes board
2026-05-12 05:01:03 +02:00
barelyprofessional 2ad5c46835 Vastly increase the roulette timeout so that the round doesn't prematurely end due to the cancellation token expiring 2026-05-11 21:43:40 -05:00
barelyprofessional f6581ad1d4 Drop Twitter media re-encode to 20 FPS as it's pretty slow and bump threads though it probably won't help much 2026-05-10 23:01:32 -05:00
barelyprofessional f81783019f It works! Make the size configurable so we can test various miniature kenos 2026-05-10 22:46:59 -05:00
barelyprofessional 9fd1124522 Possible fix for the grid breaking emojis. Trying again with Keno 2026-05-10 22:41:33 -05:00
barelyprofessional 1ee91e3f6c The grid rendering works but splitting up by chars doesn't work for multibyte so it breaks keno 2026-05-10 22:32:25 -05:00
barelyprofessional 1cb0ff21e4 Added missing cancellation token support to Keno and also experimenting with the table grid for it 2026-05-10 22:27:19 -05:00
barelyprofessional 32bef9f8e0 Add missing async support to ImageSharp for webp rendering in roulette as well as adding as much support for cancellation tokens as I can 2026-05-10 21:57:28 -05:00
barelyprofessional a4b740480f Use async for image rendering and time delays on slots so it's hopefully responsive to timeouts 2026-05-10 21:41:11 -05:00
barelyprofessional ec960d4cfe Added length limit to tags 2026-05-10 17:58:28 -05:00
barelyprofessional f821a96f70 Add a tag limit 2026-05-10 17:42:49 -05:00
barelyprofessional 7eb24e7517 Add missing where for image count 2026-05-10 17:16:41 -05:00
barelyprofessional 97e7e5bf17 Remove example text 2026-05-10 17:00:14 -05:00
barelyprofessional 02f9ac7551 Humanize is glitchy with telling it to use spaces 2026-05-10 16:53:51 -05:00
barelyprofessional ca322bde4d Consider existing entries when informing the user what they tagged with 2026-05-10 16:42:19 -05:00
barelyprofessional 8d06b75a57 Append tags instead of overwriting when tagging something that already has tags 2026-05-10 16:40:30 -05:00
barelyprofessional 94e7017f29 Increase FPS for Xitter media embeds to 30 2026-05-10 16:30:21 -05:00
barelyprofessional 4438175d80 Added commands to tag and untag images. List will now give actual image IDs for the purposes of tagging and there's a nag for untagged images 2026-05-10 16:28:22 -05:00
barelyprofessional e6e62388b9 Added functionality to wrap grids and multi-line text into tables so they can be resized nicely 2026-05-10 16:08:15 -05:00
barelyprofessional d71819819d Updated the tagging code from cohle
- Uses a List<string> as the underlying type which EF Core will serialize as JSON. Since SQLite doesn't have any native JSON features, it gets stored as TEXT
- Got rid of the alternate pathway used for selecting an image so it always has a degree of randomness assuming enough images were returned
- Simplified some of the existing Regex and removed the non capturing groups as they're cancerous
- Added/removed images will be spoilered.
- Added a metadata property for images. This is a JSON object that EF Core will serialize/deserialize for you and presently contains the user ID of whoever added the image as well as when it was added. Can be easily extended with no migration needed. Will be null for existing images.
- Added a migration process for moving Tags to TagList for fishtank
2026-05-10 15:30:54 -05:00
barelyprofessional 7df7e7dadf Add wager to the win size for statistics 2026-05-10 14:55:49 -05:00
barelyprofessional 07169f0837 Updated mines to hopefully fix a timing bug introduced by the 15 second delay. Extended the schedule auto delete functionality so that it can apply to any message, even ones not owned by the bot. 2026-05-10 11:23:38 -05:00
barelyprofessional 5e2dc25c77 Spoiler slots images 2026-05-10 11:01:09 -05:00
barelyprofessional 45cecb5e10 Apparently forgot how to format usernames right for my stupid bot 2026-05-10 10:57:42 -05:00
barelyprofessional 4d1f61bfdc Use spoiler titles for stats 2026-05-10 10:55:21 -05:00
cohlexyz 287e453b9e Add tagging to image carousel (#113) 2026-05-10 17:51:56 +02:00
barelyprofessional f231845320 Split msg for length 2026-05-09 23:02:50 -05:00
barelyprofessional 0b2ae9d271 Add navigation properties 2026-05-09 22:56:26 -05:00
barelyprofessional cd3e8f6147 Added big wins command 2026-05-09 22:51:27 -05:00
barelyprofessional 6b5e7d621b Delete !rain message for participants 2026-05-09 22:15:35 -05:00
cohlexyz fc6b0e2918 Add missing new line (#112) 2026-05-05 02:09:59 +02:00
cohlexyz 79286662ce No newlines for media + spoiler (#111) 2026-05-04 02:29:44 +02:00
barelyprofessional cf62274b4b Added option to disable DLive since it has shutdown 2026-05-01 21:13:26 -05:00
barelyprofessional 972e880aa9 Added missing balance format to Krash 2026-05-01 19:53:59 -05:00
barelyprofessional f5f0ba6323 Added an option to get your exact balance to help deal with rounding issues 2026-05-01 19:53:43 -05:00
barelyprofessional ab94098dd2 Remove dodgy reference to Redis connection string 2026-05-01 18:20:58 -05:00
barelyprofessional c79105bb44 Redis client never has a chance to initiate so added that to the start of the bot 2026-05-01 18:12:54 -05:00
barelyprofessional 000c87266e Fix infinite loop on scaled bet 2026-05-01 11:57:20 -05:00
barelyprofessional 7981f57a34 - Moved the Kasino shop models to their own file
- Added investments as a derivative of assets
- Added profile state flags which can retain basic states like IsSponsored
- Added profile state data using EF Core's JSON functionality so it should automatically serialize / deserialize the accompanying model for convenience (OnModelCreating code commented out due to the models not yet having a DbSet as I won't bake them in until KasinoShop is ready)
2026-04-26 20:39:49 -05:00
barelyprofessional e725ca5864 Moved to Lazy<T> and a static class for handling Redis connections with some methods to make it easier to work with JSON. Completely untested. 2026-04-26 20:30:56 -05:00
35 changed files with 3034 additions and 264 deletions
+4 -2
View File
@@ -12,8 +12,10 @@ public class ApplicationDbContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); //modelBuilder.Entity<KasinoShopProfileDbModel>()
// .OwnsOne(p => p.StateData, b => b.ToJson());
modelBuilder.Entity<ImageDbModel>().
OwnsOne(p => p.Metadata, b => b.ToJson());
} }
public DbSet<UserDbModel> Users { get; set; } public DbSet<UserDbModel> Users { get; set; }
+27
View File
@@ -658,6 +658,33 @@ public class ChatBot
}); });
} }
/// <summary>
/// Exposes the private task used to delete messages based on a TimeSpan in case you want to use it on-demand
/// e.g. for cleaning up a gambling message only after the game has finished
/// </summary>
/// <param name="messageUuid">The message you want to delete where you only have a message UUID
/// NOTE: The bot doesn't check against its sent message tracker, so you can use this with messages
/// the bot was not responsible for sending or were lost due to a restart.</param>
/// <param name="deleteAfter">When you want it deleted</param>
public void ScheduleMessageAutoDelete(string messageUuid, TimeSpan deleteAfter)
{
_scheduledDeletions.Add(new ScheduledAutoDeleteModel
{
Message = new SentMessageTrackerModel
{
ChatMessageUuid = messageUuid,
Delay = TimeSpan.Zero,
LastEdited = DateTimeOffset.UtcNow,
Message = "placeholder because I'm nigger rigging this shit big time",
Reference = Guid.NewGuid().ToString(),
SentAt = DateTimeOffset.UtcNow,
Status = SentMessageTrackerStatus.ResponseReceived,
Type = SentMessageType.ChatMessage
},
DeleteAt = DateTimeOffset.UtcNow.Add(deleteAfter)
});
}
/// <summary> /// <summary>
/// Non-async method which wraps the async method for sending a chat message /// Non-async method which wraps the async method for sending a chat message
/// </summary> /// </summary>
+233 -36
View File
@@ -1,4 +1,5 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Humanizer; using Humanizer;
using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Extensions;
@@ -6,21 +7,18 @@ using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Services; using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings; using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NLog;
namespace KfChatDotNetBot.Commands; namespace KfChatDotNetBot.Commands;
public class AddImageCommand : ICommand public class AddImageCommand : ICommand
{ {
public List<Regex> Patterns => [ public List<Regex> Patterns => [
new Regex(@"^admin image (?<key>\w+) add (?<url>.+)$"), new Regex(@"^admin (image|images) (?<key>\w+) (add|add_nigger) (?<url>\S+) (?<raw>raw) (?<tags>.+)$", RegexOptions.IgnoreCase),
new Regex(@"^admin images (?<key>\w+) add (?<url>.+)$"), new Regex(@"^admin (image|images) (?<key>\w+) (add|add_nigger) (?<url>\S+) (?<tags>.+)$", RegexOptions.IgnoreCase),
new Regex(@"^admin image (?<key>\w+) add_nigger (?<url>.+)$"), new Regex(@"^admin (image|images) (?<key>\w+) (add|add_nigger) (?<url>\S+)$", RegexOptions.IgnoreCase)
new Regex(@"^admin images (?<key>\w+) add_nigger (?<url>.+)$")
]; ];
public string? HelpText => "Add an image to the image rotation specified"; public string HelpText => "Add an image to the image rotation specified";
public UserRight RequiredRight => UserRight.TrueAndHonest; public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10); public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null; public RateLimitOptionsModel? RateLimitOptions => null;
@@ -33,11 +31,14 @@ public class AddImageCommand : ICommand
if (imageKeys == null) throw new InvalidOperationException($"{BuiltIn.Keys.BotImageAcceptableKeys} was null"); if (imageKeys == null) throw new InvalidOperationException($"{BuiltIn.Keys.BotImageAcceptableKeys} was null");
var key = arguments["key"].Value; var key = arguments["key"].Value;
var url = arguments["url"].Value; var url = arguments["url"].Value;
var tags = arguments.TryGetValue("tags", out var tagsArg) ? tagsArg.Value.ToLower().Split(" ").ToList() : [];
var niggerMode = message.Message.Contains("add_nigger"); var niggerMode = message.Message.Contains("add_nigger");
// TODO: Implement real and raw mode
//var _rawMode = arguments.ContainsKey("raw");
if (!imageKeys.Contains(key)) if (!imageKeys.Contains(key))
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"Key you specified is not supported. Available keys are: {string.Join(' ', imageKeys)}", true); $"Key you specified is not supported. Available keys are: {imageKeys.Humanize()}", true);
return; return;
} }
@@ -47,23 +48,111 @@ public class AddImageCommand : ICommand
return; return;
} }
await db.Images.AddAsync(new ImageDbModel { Key = key, Url = url, LastSeen = DateTimeOffset.MinValue }, ctx); if (!Uri.TryCreate(url, UriKind.Absolute, out _))
{
await botInstance.SendWhisperAsync(user.KfId, $"The URL '{url}' you provided is not valid");
return;
}
var result = url;
// todo add automatic compression/re-upload and raw mode option
await db.Images.AddAsync(new ImageDbModel
{
Key = key, Url = result, LastSeen = DateTimeOffset.MinValue, TagList = tags,
Metadata = new ImageMetadataModel { AddedByUserId = user.Id, WhenAdded = DateTimeOffset.UtcNow }
}, ctx);
var count = await db.Images.Where(i => i.Key == key).CountAsync(cancellationToken: ctx);
await db.SaveChangesAsync(ctx); await db.SaveChangesAsync(ctx);
//await botInstance.SendChatMessageAsync("Added image to database", true);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you added the following media to the {key} carousel\n[img]{url}[/img]", true); $"{user.FormatUsername()}, you added the following media to the {key} carousel which now has {count:N0} images[spoiler=\"Image\"][img]{url}[/img]", true);
}
}
public class AddImageTagsCommand : ICommand
{
public List<Regex> Patterns => [
new Regex(@"^admin (image|images) tag (?<id>\d+) (?<tags>.+)$", RegexOptions.IgnoreCase),
new Regex(@"^(image|images) tag (?<id>\d+) (?<tags>.+)$", RegexOptions.IgnoreCase),
];
public string HelpText => "Add tags to an image";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public bool WhisperCanInvoke => false;
public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
await using var db = new ApplicationDbContext();
var id = Convert.ToInt32(arguments["id"].Value);
var tags = arguments["tags"].Value.ToLower()
.Split(" ", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList();
var image = await db.Images.FirstOrDefaultAsync(i => i.Id == id, cancellationToken: ctx);
if (image == null)
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, the image ID you specified does not exist", true,
autoDeleteAfter: TimeSpan.FromSeconds(15));
return;
}
if (tags.Any(tag => tag.Length > 50))
{
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, tag length limit is 50 characters",
true);
return;
}
image.TagList = image.TagList.Concat(tags).Distinct().ToList();
if (image.TagList.Count > 50)
{
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, {id} has a shitload of tags already!",
true);
return;
}
await db.SaveChangesAsync(ctx);
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, updated tags for image ID {id} with {image.TagList.Humanize()}", true);
}
}
public class UntagImageCommand : ICommand
{
public List<Regex> Patterns => [
new Regex(@"^admin (image|images) untag (?<id>\d+)$", RegexOptions.IgnoreCase)
];
public string HelpText => "Remove tags from an image";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public bool WhisperCanInvoke => false;
public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
await using var db = new ApplicationDbContext();
var id = Convert.ToInt32(arguments["id"].Value);
var image = await db.Images.FirstOrDefaultAsync(i => i.Id == id, cancellationToken: ctx);
if (image == null)
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, the image ID you specified does not exist", true,
autoDeleteAfter: TimeSpan.FromSeconds(15));
return;
}
image.TagList = [];
await db.SaveChangesAsync(ctx);
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, removed tags from {id}", true);
} }
} }
public class RemoveImageCommand : ICommand public class RemoveImageCommand : ICommand
{ {
public List<Regex> Patterns => [ public List<Regex> Patterns => [
new Regex(@"^admin image (?<key>\w+) remove (?<url>.+)$"), new Regex(@"^admin (image|images) (?<key>\w+) (remove|delete) (?<url>.+)$"),
new Regex(@"^admin images (?<key>\w+) remove (?<url>.+)$"),
new Regex(@"^admin image (?<key>\w+) delete (?<url>.+)$"),
new Regex(@"^admin images (?<key>\w+) delete (?<url>.+)$")
]; ];
public string? HelpText => "Remove an image from the image rotation specified"; public string HelpText => "Remove an image from the image rotation specified";
public UserRight RequiredRight => UserRight.TrueAndHonest; public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10); public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null; public RateLimitOptionsModel? RateLimitOptions => null;
@@ -92,22 +181,26 @@ public class RemoveImageCommand : ICommand
db.Images.Remove(image); db.Images.Remove(image);
await db.SaveChangesAsync(ctx); await db.SaveChangesAsync(ctx);
// await botInstance.SendChatMessageAsync("Removed image from database", true);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you removed the following media from the {key} carousel\n[img]{url}[/img]", true); $"{user.FormatUsername()}, you removed the following media from the {key} carousel[spoiler=\"Image\"][img]{url}[/img]", true);
} }
} }
public class ListImageCommand : ICommand public class ListImageCommand : ICommand
{ {
public List<Regex> Patterns => [ public List<Regex> Patterns => [
new Regex(@"^admin image (?<key>\w+) list$"), new Regex(@"^admin (image|images) (?<key>\w+) list$"),
new Regex(@"^admin images (?<key>\w+) list$") new Regex(@"^(image|images) (?<key>\w+) list$"),
]; ];
public string? HelpText => "Remove an image from the image rotation specified"; public string HelpText => "List images for a given carousel";
public UserRight RequiredRight => UserRight.TrueAndHonest; public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10); public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null; public RateLimitOptionsModel RateLimitOptions => new()
{
Flags = RateLimitFlags.None,
MaxInvocations = 2,
Window = TimeSpan.FromSeconds(15)
};
public bool WhisperCanInvoke => false; public bool WhisperCanInvoke => false;
public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments, public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx) CancellationToken ctx)
@@ -120,17 +213,19 @@ public class ListImageCommand : ICommand
if (!imageKeys.Contains(key)) if (!imageKeys.Contains(key))
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"Key you specified is not supported. Available keys are: {string.Join(' ', imageKeys)}", true); $"Key you specified is not supported. Available keys are: {imageKeys.Humanize()}", true);
return; return;
} }
var images = db.Images.Where(i => i.Key == key); var images = db.Images.Where(i => i.Key == key);
if (await images.CountAsync(cancellationToken: ctx) > 20 && await Zipline.IsZiplineEnabled()) if (await images.CountAsync(cancellationToken: ctx) > 10 && await Zipline.IsZiplineEnabled())
{ {
var content = string.Empty; var content = string.Empty;
foreach (var image in images) foreach (var image in images)
{ {
content += image.Url + Environment.NewLine; var ts = DateTimeOffset.UtcNow - image.LastSeen;
var time = $"{ts.TotalDays:N0}d{ts.Hours:N0}h{ts.Minutes:N0}m{ts.Seconds:N0}s";
content += $"{image.Url} (ID: {image.Id}) - {time} - {image.TagList.Humanize()}" + Environment.NewLine;
} }
var paste = await Zipline.Upload(content, new MediaTypeHeaderValue("text/plain"), "1d", ctx); var paste = await Zipline.Upload(content, new MediaTypeHeaderValue("text/plain"), "1d", ctx);
@@ -144,7 +239,7 @@ public class ListImageCommand : ICommand
{ {
i++; i++;
var ts = DateTimeOffset.UtcNow - image.LastSeen; var ts = DateTimeOffset.UtcNow - image.LastSeen;
result += $"[br]{i}: {image.Url} (Last seen {ts.TotalDays:N0}d{ts.Hours:N0}h{ts.Minutes:N0}m{ts.Seconds:N0}s ago)"; result += $"[br]{i}: {image.Url} (ID: {image.Id}) (Last seen {ts.TotalDays:N0}d{ts.Hours:N0}h{ts.Minutes:N0}m{ts.Seconds:N0}s ago)";
} }
await botInstance.SendChatMessagesAsync(result.FancySplitMessage(partSeparator: "[br]"), await botInstance.SendChatMessagesAsync(result.FancySplitMessage(partSeparator: "[br]"),
@@ -152,16 +247,70 @@ public class ListImageCommand : ICommand
} }
} }
public class ManageImageKeyCommand : ICommand
{
public List<Regex> Patterns => [
new Regex(@"^admin (imagekey|imageskey) (?<operation>add|remove|delete) (?<key>\w+)$"),
];
public string HelpText => "Add or remove an acceptable image key from the BotImageAcceptableKeys setting";
public UserRight RequiredRight => UserRight.Admin;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public bool WhisperCanInvoke => true;
public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
var imageKeys = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.BotImageAcceptableKeys)).JsonDeserialize<List<string>>();
if (imageKeys == null) throw new InvalidOperationException($"{BuiltIn.Keys.BotImageAcceptableKeys} was null");
var key = arguments["key"].Value.ToLower();
var operation = arguments["operation"].Value.ToLower();
if (operation is "add")
{
if (imageKeys.Contains(key))
{
await botInstance.ReplyToUser(message, $"Key \"{key}\" is already in the acceptable keys list", true);
return;
}
imageKeys.Add(key);
await SettingsProvider.SetValueAsync(BuiltIn.Keys.BotImageAcceptableKeys, JsonSerializer.Serialize(imageKeys));
await botInstance.ReplyToUser(message,
$"Added key \"{key}\" to acceptable image keys. Current keys: {imageKeys.Humanize()}", true);
return;
}
if (operation is "remove" or "delete")
{
if (!imageKeys.Contains(key))
{
await botInstance.ReplyToUser(message,
$"Key \"{key}\" is not in the acceptable keys list. Current keys: {imageKeys.Humanize()}", true);
return;
}
imageKeys.Remove(key);
await SettingsProvider.SetValueAsync(BuiltIn.Keys.BotImageAcceptableKeys, JsonSerializer.Serialize(imageKeys));
await botInstance.ReplyToUser(message,
$"Removed key \"{key}\" from acceptable image keys. Current keys: {imageKeys.Humanize()}", true);
return;
}
await botInstance.ReplyToUser(message, $"Operation '{operation}' not supported", true);
}
}
[AllowAdditionalMatches] [AllowAdditionalMatches]
public class GetRandomImage : ICommand public class GetRandomImage : ICommand
{ {
public List<Regex> Patterns => [ public List<Regex> Patterns => [
new Regex(@"^(?<key>\w+)") new Regex(@"^(?<key>\w+)$"),
new Regex(@"^(?<key>\w+) (?<search>.+)"),
new Regex("^untagged$", RegexOptions.IgnoreCase),
new Regex("i (?<search>.+)")
]; ];
public string? HelpText => "Get a random image"; public string HelpText => "Get a random image";
public UserRight RequiredRight => UserRight.Loser; public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromMinutes(10); public TimeSpan Timeout => TimeSpan.FromMinutes(10);
public RateLimitOptionsModel? RateLimitOptions => new() public RateLimitOptionsModel RateLimitOptions => new()
{ {
Window = TimeSpan.FromSeconds(30), Window = TimeSpan.FromSeconds(30),
MaxInvocations = 7, MaxInvocations = 7,
@@ -172,8 +321,16 @@ public class GetRandomImage : ICommand
CancellationToken ctx) CancellationToken ctx)
{ {
await using var db = new ApplicationDbContext(); await using var db = new ApplicationDbContext();
var key = arguments["key"].Value.ToLower(); var untagged = message.MessageRawHtmlDecoded.EndsWith("untagged", StringComparison.CurrentCultureIgnoreCase);
var images = db.Images.Where(i => i.Key == key); var keyKnown = arguments.TryGetValue("key", out var keyGroup);
var key = "everything";
if (keyKnown) key = keyGroup!.Value;
var searchTerm = arguments.TryGetValue("search", out var searchArg) ? searchArg.Value.ToLower().Trim() : null;
var images = db.Images.AsQueryable();
if (keyKnown)
{
images = images.Where(i => i.Key == key);
}
if (!await images.AnyAsync(ctx)) if (!await images.AnyAsync(ctx))
{ {
RateLimitService.RemoveMostRecentEntry(user, this); RateLimitService.RemoveMostRecentEntry(user, this);
@@ -191,6 +348,26 @@ public class GetRandomImage : ICommand
BuiltIn.Keys.BotImagePigCubeSelfDestructMax, BuiltIn.Keys.BotImageInvertedPigCubeSelfDestructDelay, BuiltIn.Keys.BotImagePigCubeSelfDestructMax, BuiltIn.Keys.BotImageInvertedPigCubeSelfDestructDelay,
BuiltIn.Keys.BotImageChinkSelfDestruct, BuiltIn.Keys.BotImageChinkSelfDestructDelay BuiltIn.Keys.BotImageChinkSelfDestruct, BuiltIn.Keys.BotImageChinkSelfDestructDelay
]); ]);
var selection = await images.ToListAsync(ctx);
// It's buried down here instead of right up the top since it needs to be a list first as SQLite doesn't have
// native support for JSON types so it won't be able to construct a query to see if it's empty using IQueryable
if (untagged)
{
selection = selection.Where(s => s.TagList.Count == 0).ToList();
}
if (!string.IsNullOrEmpty(searchTerm))
{
var searchTokens = searchTerm.ToLower().Split(' ', StringSplitOptions.RemoveEmptyEntries);
selection = searchTokens.Aggregate(selection, (current, token) =>
current.Where(i => i.TagList.Count > 0 && i.TagList.Contains(token)).ToList());
if (selection.Count == 0)
{
RateLimitService.RemoveMostRecentEntry(user, this);
await botInstance.SendChatMessageAsync($"No image in {key} matched \"{searchTerm}\"", true);
return;
}
}
var divideBy = settings[BuiltIn.Keys.BotImageRandomSliceDivideBy].ToType<int>(); var divideBy = settings[BuiltIn.Keys.BotImageRandomSliceDivideBy].ToType<int>();
var limit = 1; var limit = 1;
var count = await images.CountAsync(ctx); var count = await images.CountAsync(ctx);
@@ -200,25 +377,45 @@ public class GetRandomImage : ICommand
} }
// EF with SQLite can't sort on dates as it's just TEXT // EF with SQLite can't sort on dates as it's just TEXT
var selection = (await images.ToListAsync(ctx)).OrderBy(i => i.LastSeen).Take(limit).ToList(); selection = selection.OrderBy(i => i.LastSeen).Take(limit).ToList();
// MaxValue is never returned by Next so you don't need to -1 for indexing // MaxValue is never returned by Next so you don't need to -1 for indexing
var image = selection[new Random().Next(0, selection.Count)]; var image = selection[new Random().Next(0, selection.Count)];
image.LastSeen = DateTimeOffset.UtcNow; image.LastSeen = DateTimeOffset.UtcNow;
db.Images.Update(image); db.Images.Update(image);
await db.SaveChangesAsync(ctx); await db.SaveChangesAsync(ctx);
TimeSpan? timeToDeletion = null; TimeSpan? timeToDeletion = null;
if (key == "pigcube" && settings[BuiltIn.Keys.BotImagePigCubeSelfDestruct].ToBoolean()) if (image.Key == "pigcube" && settings[BuiltIn.Keys.BotImagePigCubeSelfDestruct].ToBoolean())
{ {
timeToDeletion = TimeSpan.FromMilliseconds(image.Url == settings[BuiltIn.Keys.BotImageInvertedCubeUrl].Value timeToDeletion = TimeSpan.FromMilliseconds(image.Url == settings[BuiltIn.Keys.BotImageInvertedCubeUrl].Value
? settings[BuiltIn.Keys.BotImageInvertedPigCubeSelfDestructDelay].ToType<int>() ? settings[BuiltIn.Keys.BotImageInvertedPigCubeSelfDestructDelay].ToType<int>()
: new Random().Next(settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMin].ToType<int>(), : new Random().Next(settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMin].ToType<int>(),
settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMax].ToType<int>())); settings[BuiltIn.Keys.BotImagePigCubeSelfDestructMax].ToType<int>()));
} }
else if (key is "chink" or "sloppa" && settings[BuiltIn.Keys.BotImageChinkSelfDestruct].ToBoolean()) else if (image.Key is "chink" or "sloppa" && settings[BuiltIn.Keys.BotImageChinkSelfDestruct].ToBoolean())
{ {
RateLimitService.AddEntry(user, this, message.MessageRawHtmlDecoded); RateLimitService.AddEntry(user, this, message.MessageRawHtmlDecoded);
timeToDeletion = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.BotImageChinkSelfDestructDelay].ToType<int>()); timeToDeletion = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.BotImageChinkSelfDestructDelay].ToType<int>());
} }
await botInstance.SendChatMessageAsync($"[img]{image.Url}[/img]", true, autoDeleteAfter: timeToDeletion);
var addedBy = "Unknown";
var whenAdded = "Unknown";
if (image.Metadata != null)
{
var addedByUser = await db.Users.FirstOrDefaultAsync(u => u.Id == image.Metadata.AddedByUserId, cancellationToken: ctx);
addedBy = addedByUser?.KfUsername ?? $"User ID {image.Metadata.AddedByUserId} for this image didn't point to a real user?";
whenAdded = image.Metadata.WhenAdded.ToString("yyyy-MM-dd HH:mm:ss zzz");
}
var imageMeta =
$"[size=60][spoiler=\"Image Info\"][heading=1]ID: {image.Id}; Tags: {image.TagList.Humanize()}; Carousel: {image.Key}; Added By: {addedBy}; Date Added: {whenAdded}[/heading][/spoiler][/size]";
var tagNag = string.Empty;
if (image.TagList.Count == 0)
{
tagNag = $"[br]This image has no tags. You can add some using [ditto]!images tag {image.Id} [/ditto]";
}
var result = $"[img]{image.Url}[/img]{tagNag}[br]{imageMeta}";
await botInstance.SendChatMessageAsync(result, true, autoDeleteAfter: timeToDeletion);
} }
} }
@@ -0,0 +1,59 @@
using System.Text.RegularExpressions;
using Humanizer;
using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings;
using Microsoft.EntityFrameworkCore;
namespace KfChatDotNetBot.Commands.Kasino;
[KasinoCommand]
public class GetBiggestWins : ICommand
{
public List<Regex> Patterns => [
new Regex("^kasino bigwins", RegexOptions.IgnoreCase)
];
public string? HelpText => "Big wins for the current gameday";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public bool WhisperCanInvoke => false;
public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
await using var db = new ApplicationDbContext();
var gameDay = await Money.GetKasinoDate();
var wagers = await db.Wagers.Where(x => x.TimeUnixEpochSeconds > gameDay.ToUnixTimeSeconds())
.Include(x => x.Gambler).ThenInclude(x => x.User).ToListAsync(ctx);
var biggestMultees = wagers.OrderByDescending(x => x.Multiplier).Take(10).ToList();
var biggestWins = wagers.OrderByDescending(x => x.WagerEffect).Take(10).ToList();
var multeesMsg =
$"Big multees adding up to {await biggestMultees.Sum(x => x.WagerEffect).FormatKasinoCurrencyAsync()}:";
var i = 0;
foreach (var win in biggestMultees)
{
i++;
var winPlusWager = win.WagerEffect + win.WagerAmount;
multeesMsg += $"[br]{i}. {win.Gambler.User.FormatUsername()} bet {await win.WagerAmount.FormatKasinoCurrencyAsync()} on {win.Game.Humanize()} and won {await winPlusWager.FormatKasinoCurrencyAsync()} ({win.Multiplier:N2}x)";
}
var bigWinsMsg = $"Big wins adding up to {await biggestWins.Sum(x => x.WagerEffect).FormatKasinoCurrencyAsync()}:";
i = 0;
foreach (var win in biggestWins)
{
i++;
var winPlusWager = win.WagerEffect + win.WagerAmount;
bigWinsMsg += $"[br]{i}. {win.Gambler.User.FormatUsername()} bet {await win.WagerAmount.FormatKasinoCurrencyAsync()} on {win.Game.Humanize()} and won {await winPlusWager.FormatKasinoCurrencyAsync()} ({win.Multiplier:N2}x)";
}
var msgs = new List<string>
{
$"Top 10 biggest wins for game day {gameDay:yyyy-MM-dd}" +
$"[spoiler=\"Big Multees\"]{multeesMsg}[/spoiler]",
$"[spoiler=\"Big Wins\"]{bigWinsMsg}[/spoiler]"
};
await botInstance.SendChatMessagesAsync(msgs, true, autoDeleteAfter: TimeSpan.FromSeconds(60));
}
}
@@ -18,7 +18,8 @@ public class GetBalanceCommand : ICommand
{ {
public List<Regex> Patterns => [ public List<Regex> Patterns => [
new Regex("^balance", RegexOptions.IgnoreCase), new Regex("^balance", RegexOptions.IgnoreCase),
new Regex("^bal$", RegexOptions.IgnoreCase) new Regex("^bal$", RegexOptions.IgnoreCase),
new Regex("^bal exact$", RegexOptions.IgnoreCase)
]; ];
public string? HelpText => "Get your gamba balance"; public string? HelpText => "Get your gamba balance";
public UserRight RequiredRight => UserRight.Loser; public UserRight RequiredRight => UserRight.Loser;
@@ -30,8 +31,17 @@ public class GetBalanceCommand : ICommand
CancellationToken ctx) CancellationToken ctx)
{ {
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
if (message.MessageRawHtmlDecoded.EndsWith("exact"))
{
await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, your balance is {gambler!.Balance}", true);
}
else
{
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, your balance is {await gambler!.Balance.FormatKasinoCurrencyAsync()}", true); $"{user.FormatUsername()}, your balance is {await gambler!.Balance.FormatKasinoCurrencyAsync()}", true);
}
if (botInstance.BotServices.KasinoShop != null) if (botInstance.BotServices.KasinoShop != null)
{ {
+8 -12
View File
@@ -39,10 +39,12 @@ public class KenoCommand : ICommand
private const string BlankSpaceDisplay = "⬛"; private const string BlankSpaceDisplay = "⬛";
private SentMessageTrackerModel? _kenoTable; private SentMessageTrackerModel? _kenoTable;
private CancellationToken _ct;
public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments, public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx) CancellationToken ctx)
{ {
_ct = ctx;
var settings = await SettingsProvider.GetMultipleValuesAsync([ var settings = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoKenoCleanupDelay, BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoKenoCleanupDelay,
BuiltIn.Keys.KasinoKenoFrameDelay, BuiltIn.Keys.KasinoKenoEnabled BuiltIn.Keys.KasinoKenoFrameDelay, BuiltIn.Keys.KasinoKenoEnabled
@@ -249,17 +251,11 @@ public class KenoCommand : ICommand
displayMessage += "[br]"; displayMessage += "[br]";
} }
_kenoTable = await botInstance.SendChatMessageAsync(displayMessage, true); var size = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KasinoKenoSize)).ToType<int>();
var i = 0; _kenoTable = await botInstance.SendChatMessageAsync($"[size={size}]" + displayMessage.GridToTable(), true);
while (_kenoTable.ChatMessageUuid == null) var sent = await botInstance.WaitForChatMessageAsync(_kenoTable, patience: TimeSpan.FromSeconds(30), ct: _ct);
{
i++;
if (_kenoTable.Status is SentMessageTrackerStatus.NotSending or SentMessageTrackerStatus.Lost) return;
if (i > 60) return;
await Task.Delay(100);
}
if (_kenoTable.ChatMessageUuid == null) if (!sent || _kenoTable.ChatMessageUuid == null)
{ {
throw new Exception($"_kenoTable chat message ID never got populated. Tracker status is: {_kenoTable?.Status}"); throw new Exception($"_kenoTable chat message ID never got populated. Tracker status is: {_kenoTable?.Status}");
} }
@@ -293,8 +289,8 @@ public class KenoCommand : ICommand
} }
displayMessage += "[br]"; displayMessage += "[br]";
} }
await botInstance.KfClient.EditMessageAsync(_kenoTable.ChatMessageUuid, displayMessage); await botInstance.KfClient.EditMessageAsync(_kenoTable.ChatMessageUuid, $"[size={size}]" + displayMessage.GridToTable());
await Task.Delay(frameDelay); await Task.Delay(frameDelay, _ct);
if (displayMessage.Length <= 79 && displayMessage.Contains(BlankSpaceDisplay) && if (displayMessage.Length <= 79 && displayMessage.Contains(BlankSpaceDisplay) &&
(displayMessage.Contains(CasinoNumberDisplay) || displayMessage.Contains(MatchRevealDisplay) || (displayMessage.Contains(CasinoNumberDisplay) || displayMessage.Contains(MatchRevealDisplay) ||
frame == 9)) continue; //every board should have blank spaces and casino numbers or matches. player numbers might be hidden by matches frame == 9)) continue; //every board should have blank spaces and casino numbers or matches. player numbers might be hidden by matches
@@ -81,7 +81,7 @@ public class KrashBetCommand : ICommand
if (wager > gambler.Balance) if (wager > gambler.Balance)
{ {
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, your balance of {gambler.Balance} is not enough to bet {wager} on krash.", $"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} is not enough to bet {wager} on krash.",
true, autoDeleteAfter: TimeSpan.FromSeconds(5)); true, autoDeleteAfter: TimeSpan.FromSeconds(5));
return; return;
} }
@@ -119,7 +119,7 @@ public class Planes : ICommand
_riggedWin = true; _riggedWin = true;
} }
} }
var size = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KasinoPlanesSize)).ToType<int>();
var planesBoard = CreatePlanesBoard(gambler,0); var planesBoard = CreatePlanesBoard(gambler,0);
var planesBoard2 = CreatePlanesBoard(gambler); var planesBoard2 = CreatePlanesBoard(gambler);
var planesBoard3 = CreatePlanesBoard(gambler); var planesBoard3 = CreatePlanesBoard(gambler);
@@ -129,7 +129,7 @@ public class Planes : ICommand
var fullCounter = 0; var fullCounter = 0;
var noseUp = true; var noseUp = true;
var planesDisplay = GetPreGameBoard(-3, planesBoard2, plane, CarrierCount, noseUp); var planesDisplay = GetPreGameBoard(-3, planesBoard2, plane, CarrierCount, noseUp);
var msgId = await botInstance.SendChatMessageAsync(planesDisplay, true); var msgId = await botInstance.SendChatMessageAsync($"[size={size}]" + planesDisplay, true);
var num = 0; var num = 0;
while (msgId.ChatMessageUuid == null) while (msgId.ChatMessageUuid == null)
{ {
@@ -153,14 +153,14 @@ public class Planes : ICommand
if (fullCounter >= 3) if (fullCounter >= 3)
{ {
planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp); planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp);
planesDisplay += $"[br]Multi: {plane.MultiTracker}x"; planesDisplay += $"[/size][br]Multi: {plane.MultiTracker}x";
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
planesDisplay += BlankSpace; planesDisplay += BlankSpace;
} }
var winnings = plane.MultiTracker * wager; var winnings = plane.MultiTracker * wager;
planesDisplay += $"Winnings: {await winnings.FormatKasinoCurrencyAsync()}"; planesDisplay += $"Winnings: {await winnings.FormatKasinoCurrencyAsync()}";
await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, planesDisplay); await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, $"[size={size}]" + planesDisplay);
} }
var neutral = false; var neutral = false;
@@ -171,7 +171,7 @@ public class Planes : ICommand
{ {
counter = (fullCounter - 3) % 24; counter = (fullCounter - 3) % 24;
planesDisplay = GetPreGameBoard(fullCounter, planesBoard2, plane, CarrierCount, noseUp); planesDisplay = GetPreGameBoard(fullCounter, planesBoard2, plane, CarrierCount, noseUp);
await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, planesDisplay); await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, $"[size={size}]" + planesDisplay);
await Task.Delay(TimeSpan.FromMilliseconds(frameLength), ctx); await Task.Delay(TimeSpan.FromMilliseconds(frameLength), ctx);
fullCounter++; fullCounter++;
} }
@@ -240,7 +240,7 @@ public class Planes : ICommand
logger.Error(e); logger.Error(e);
throw; throw;
} }
planesDisplay += $"[br]Multi: {plane.MultiTracker}x"; planesDisplay += $"[/size][br]Multi: {plane.MultiTracker}x";
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
planesDisplay += BlankSpace; planesDisplay += BlankSpace;
@@ -248,7 +248,7 @@ public class Planes : ICommand
var winnings = plane.MultiTracker * wager; var winnings = plane.MultiTracker * wager;
planesDisplay += $"Winnings: {await winnings.FormatKasinoCurrencyAsync()}"; planesDisplay += $"Winnings: {await winnings.FormatKasinoCurrencyAsync()}";
await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, planesDisplay); await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, $"[size={size}]" + planesDisplay);
if (plane.Height > 5) if (plane.Height > 5)
{ {
break; break;
@@ -276,7 +276,7 @@ public class Planes : ICommand
var win = plane.MultiTracker * wager; var win = plane.MultiTracker * wager;
newBalance = await Money.NewWagerAsync(gambler.Id, wager, win, WagerGame.Planes, ct: ctx); newBalance = await Money.NewWagerAsync(gambler.Id, wager, win, WagerGame.Planes, ct: ctx);
planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp); planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp);
await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, planesDisplay); await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, $"[size={size}]" + planesDisplay);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]successfully landed with {await win.FormatKasinoCurrencyAsync()} from a total {plane.MultiTracker:N2}x multi![/color]. Your balance is now: {await newBalance.FormatKasinoCurrencyAsync()}", $"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]successfully landed with {await win.FormatKasinoCurrencyAsync()} from a total {plane.MultiTracker:N2}x multi![/color]. Your balance is now: {await newBalance.FormatKasinoCurrencyAsync()}",
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
@@ -294,7 +294,7 @@ public class Planes : ICommand
newBalance = await Money.NewWagerAsync(gambler.Id, wager, -wager, WagerGame.Planes, ct: ctx); newBalance = await Money.NewWagerAsync(gambler.Id, wager, -wager, WagerGame.Planes, ct: ctx);
planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp); planesDisplay = GetGameBoard(fullCounter, planesBoards, plane, CarrierCount, noseUp);
await Task.Delay(TimeSpan.FromMilliseconds(frameLength), ctx); await Task.Delay(TimeSpan.FromMilliseconds(frameLength), ctx);
await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, planesDisplay); await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageUuid, $"[size={size}]" + planesDisplay);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]crashed![/color] Your balance is now: {await newBalance.FormatKasinoCurrencyAsync()}", $"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]crashed![/color] Your balance is now: {await newBalance.FormatKasinoCurrencyAsync()}",
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
@@ -368,7 +368,7 @@ public class Planes : ICommand
output += "[br]"; output += "[br]";
} }
return output; return output.GridToTable();
} }
private string GetGameBoard(int fullCounter, List<int[,]> planesBoards, Plane plane, int carrierCount, bool noseUp) private string GetGameBoard(int fullCounter, List<int[,]> planesBoards, Plane plane, int carrierCount, bool noseUp)
@@ -445,7 +445,7 @@ public class Planes : ICommand
} }
output += "[br]"; output += "[br]";
} }
return output; return output.GridToTable();
} }
private int[,] CreatePlanesBoard(GamblerDbModel gambler, int forceTiles = -1) private int[,] CreatePlanesBoard(GamblerDbModel gambler, int forceTiles = -1)
@@ -184,8 +184,9 @@ public class PlinkoCommand : ICommand
ballsNotInPlay.Add(new PlinkoBall()); ballsNotInPlay.Add(new PlinkoBall());
} }
//game starts here //game starts here
var size = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KasinoPlinkoSize)).ToType<int>();
int breakCounter = 0; int breakCounter = 0;
var plinkoMessageID = await botInstance.SendChatMessageAsync(PlinkoBoardDisplay(ballsInPlay), true, autoDeleteAfter: cleanupDelay); var plinkoMessageID = await botInstance.SendChatMessageAsync($"[size={size}]" + PlinkoBoardDisplay(ballsInPlay), true, autoDeleteAfter: cleanupDelay);
while (plinkoMessageID.ChatMessageUuid == null && breakCounter < 1000) { while (plinkoMessageID.ChatMessageUuid == null && breakCounter < 1000) {
await Task.Delay(100, ctx); await Task.Delay(100, ctx);
breakCounter++; breakCounter++;
@@ -207,7 +208,7 @@ public class PlinkoCommand : ICommand
ballsInPlay.Add(ballsNotInPlay[0]); ballsInPlay.Add(ballsNotInPlay[0]);
ballsNotInPlay.RemoveAt(0); ballsNotInPlay.RemoveAt(0);
} }
PlinkoMessage = PlinkoBoardDisplay(ballsInPlay) + "[br]" + lastPayoutMessage; PlinkoMessage = $"[size={size}]" + PlinkoBoardDisplay(ballsInPlay) + "[/size][br]" + lastPayoutMessage;
await botInstance.KfClient.EditMessageAsync(plinkoMessageID.ChatMessageUuid!, PlinkoMessage); await botInstance.KfClient.EditMessageAsync(plinkoMessageID.ChatMessageUuid!, PlinkoMessage);
if (ballsInPlay[0].POSITION.row == DIFFICULTY - 1) //once your ball has reached the bottom calculate the payout if (ballsInPlay[0].POSITION.row == DIFFICULTY - 1) //once your ball has reached the bottom calculate the payout
{ {
@@ -231,7 +232,7 @@ public class PlinkoCommand : ICommand
} }
await Task.Delay(300, ctx); await Task.Delay(300, ctx);
PlinkoMessage = PlinkoBoardDisplay(ballsInPlay) + "[br]" + lastPayoutMessage; PlinkoMessage = $"[size={size}]"+PlinkoBoardDisplay(ballsInPlay) + "[/size][br]" + lastPayoutMessage;
await botInstance.KfClient.EditMessageAsync(plinkoMessageID.ChatMessageUuid!, PlinkoMessage); await botInstance.KfClient.EditMessageAsync(plinkoMessageID.ChatMessageUuid!, PlinkoMessage);
await Task.Delay(300, ctx); await Task.Delay(300, ctx);
@@ -293,7 +294,7 @@ public class PlinkoCommand : ICommand
board += "[br]"; board += "[br]";
} }
return board; return board.GridToTable();
} }
public class PlinkoBall public class PlinkoBall
{ {
@@ -93,6 +93,7 @@ public class RainCommand : ICommand
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"LFG {user.FormatUsername()} is now a participant! There's now {rain.Participants.Count + 1} participant{pluralSuffix}! Type [ditto]!rain[/ditto] to participate", $"LFG {user.FormatUsername()} is now a participant! There's now {rain.Participants.Count + 1} participant{pluralSuffix}! Type [ditto]!rain[/ditto] to participate",
true, autoDeleteAfter: cleanupDelay); true, autoDeleteAfter: cleanupDelay);
await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid!);
return; return;
} }
//if you're trying to start the rain //if you're trying to start the rain
@@ -6,7 +6,6 @@ using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Services; using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings; using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NLog; using NLog;
using SixLabors.Fonts; using SixLabors.Fonts;
@@ -35,7 +34,7 @@ public class RouletteCommand : ICommand
public string? HelpText => "!roulette <amount> <bet> - Bet types: number (0-36), red/black, odd/even, low/high, 1st12/2nd12/3rd12, col1/col2/col3"; public string? HelpText => "!roulette <amount> <bet> - Bet types: number (0-36), red/black, odd/even, low/high, 1st12/2nd12/3rd12, col1/col2/col3";
public UserRight RequiredRight => UserRight.Loser; public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(5); public TimeSpan Timeout => TimeSpan.FromSeconds(300);
public RateLimitOptionsModel? RateLimitOptions => new() public RateLimitOptionsModel? RateLimitOptions => new()
{ {
MaxInvocations = 10, MaxInvocations = 10,
@@ -47,6 +46,7 @@ public class RouletteCommand : ICommand
private IDatabase? _redisDb; private IDatabase? _redisDb;
private ApplicationDbContext _dbContext = new(); private ApplicationDbContext _dbContext = new();
private CancellationToken _ct;
// European Roulette wheel configuration // European Roulette wheel configuration
private static readonly HashSet<int> BlackNumbers = new() private static readonly HashSet<int> BlackNumbers = new()
@@ -58,11 +58,11 @@ public class RouletteCommand : ICommand
public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments, public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx) CancellationToken ctx)
{ {
_ct = ctx;
var settings = await SettingsProvider.GetMultipleValuesAsync([ var settings = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay,
BuiltIn.Keys.KasinoRouletteEnabled, BuiltIn.Keys.KasinoRouletteEnabled,
BuiltIn.Keys.KasinoRouletteCountdownDuration, BuiltIn.Keys.KasinoRouletteCountdownDuration
BuiltIn.Keys.BotRedisConnectionString
]); ]);
// Check if roulette is enabled // Check if roulette is enabled
@@ -77,15 +77,14 @@ public class RouletteCommand : ICommand
return; return;
} }
if (string.IsNullOrEmpty(settings[BuiltIn.Keys.BotRedisConnectionString].Value)) if (!Redis.IsAvailable)
{ {
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, roulette is not available at this time", true, await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, roulette is not available at this time", true,
autoDeleteAfter: TimeSpan.FromSeconds(15)); autoDeleteAfter: TimeSpan.FromSeconds(15));
return; return;
} }
var redis = await ConnectionMultiplexer.ConnectAsync(settings[BuiltIn.Keys.BotRedisConnectionString].Value!); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
var countdownDuration = TimeSpan.FromSeconds( var countdownDuration = TimeSpan.FromSeconds(
settings[BuiltIn.Keys.KasinoRouletteCountdownDuration].ToType<int>()); settings[BuiltIn.Keys.KasinoRouletteCountdownDuration].ToType<int>());
@@ -273,7 +272,7 @@ public class RouletteCommand : ICommand
// Wait until message is fully sent // Wait until message is fully sent
logger.Debug("Waiting for countdown message to be sent..."); logger.Debug("Waiting for countdown message to be sent...");
var success = await botInstance.WaitForChatMessageAsync(countdownMessage, TimeSpan.FromSeconds(30)); var success = await botInstance.WaitForChatMessageAsync(countdownMessage, TimeSpan.FromSeconds(30), _ct);
if (!success) if (!success)
{ {
@@ -291,7 +290,7 @@ public class RouletteCommand : ICommand
if (remaining.TotalSeconds <= 0) break; if (remaining.TotalSeconds <= 0) break;
// Wait 1 second between updates // Wait 1 second between updates
await Task.Delay(TimeSpan.FromSeconds(1)); await Task.Delay(TimeSpan.FromSeconds(1), _ct);
try try
{ {
@@ -388,7 +387,7 @@ public class RouletteCommand : ICommand
{ {
// Generate winning number using first gambler's seed // Generate winning number using first gambler's seed
var firstGambler = await _dbContext.Gamblers var firstGambler = await _dbContext.Gamblers
.FirstOrDefaultAsync(g => g.Id == round.Bets[0].GamblerId); .FirstOrDefaultAsync(g => g.Id == round.Bets[0].GamblerId, cancellationToken: _ct);
if (firstGambler == null) if (firstGambler == null)
{ {
@@ -400,7 +399,7 @@ public class RouletteCommand : ICommand
// Generate animation // Generate animation
logger.Info($"Generating roulette animation for round {round.RoundId}"); logger.Info($"Generating roulette animation for round {round.RoundId}");
var (animationDuration, animationBytes) = RouletteAnimationGenerator.GenerateAnimation(winningNumber); var (animationDuration, animationBytes) = await RouletteAnimationGenerator.GenerateAnimation(winningNumber, _ct);
logger.Info($"Animation generated: {animationBytes.Length} bytes, duration: {animationDuration}s"); logger.Info($"Animation generated: {animationBytes.Length} bytes, duration: {animationDuration}s");
// Upload animation to Zipline // Upload animation to Zipline
@@ -409,7 +408,7 @@ public class RouletteCommand : ICommand
var animationUrl = await Zipline.Upload( var animationUrl = await Zipline.Upload(
animationStream, animationStream,
new MediaTypeHeaderValue("image/webp"), new MediaTypeHeaderValue("image/webp"),
expiration: "1h"); expiration: "1h", ct: _ct);
if (string.IsNullOrEmpty(animationUrl)) if (string.IsNullOrEmpty(animationUrl))
{ {
@@ -434,7 +433,7 @@ public class RouletteCommand : ICommand
// Wait for animation duration before revealing results // Wait for animation duration before revealing results
logger.Info($"Waiting {animationDuration} seconds for animation to complete"); logger.Info($"Waiting {animationDuration} seconds for animation to complete");
await Task.Delay(TimeSpan.FromSeconds(animationDuration)); await Task.Delay(TimeSpan.FromSeconds(animationDuration), _ct);
// Process all bets and show results // Process all bets and show results
await ProcessBets(botInstance, round, winningNumber); await ProcessBets(botInstance, round, winningNumber);
@@ -461,20 +460,20 @@ public class RouletteCommand : ICommand
{ {
var wager = await _dbContext.Wagers var wager = await _dbContext.Wagers
.Include(w => w.Gambler) .Include(w => w.Gambler)
.FirstOrDefaultAsync(w => w.Id == bet.WagerId); .FirstOrDefaultAsync(w => w.Id == bet.WagerId, cancellationToken: _ct);
if (wager != null) if (wager != null)
{ {
wager.IsComplete = true; wager.IsComplete = true;
wager.WagerEffect = 0; wager.WagerEffect = 0;
wager.Multiplier = 1; wager.Multiplier = 1;
await _dbContext.SaveChangesAsync(); await _dbContext.SaveChangesAsync(_ct);
await Money.ModifyBalanceAsync( await Money.ModifyBalanceAsync(
wager.Gambler.Id, wager.Gambler.Id,
wager.WagerAmount, wager.WagerAmount,
TransactionSourceEventType.Gambling, TransactionSourceEventType.Gambling,
$"Roulette round {round.RoundId} cancelled due to error, wager {wager.Id} refunded"); $"Roulette round {round.RoundId} cancelled due to error, wager {wager.Id} refunded", ct: _ct);
totalRefunded += wager.WagerAmount; totalRefunded += wager.WagerAmount;
} }
@@ -522,7 +521,7 @@ public class RouletteCommand : ICommand
{ {
var wager = await _dbContext.Wagers var wager = await _dbContext.Wagers
.Include(w => w.Gambler) .Include(w => w.Gambler)
.FirstOrDefaultAsync(w => w.Id == bet.WagerId); .FirstOrDefaultAsync(w => w.Id == bet.WagerId, cancellationToken: _ct);
if (wager == null) if (wager == null)
{ {
@@ -539,7 +538,7 @@ public class RouletteCommand : ICommand
wager.WagerEffect = effect; wager.WagerEffect = effect;
wager.Multiplier = payout / bet.Amount; wager.Multiplier = payout / bet.Amount;
await _dbContext.SaveChangesAsync(); await _dbContext.SaveChangesAsync(_ct);
// Update balance // Update balance
var balanceAdjustment = payout; var balanceAdjustment = payout;
@@ -547,7 +546,7 @@ public class RouletteCommand : ICommand
wager.Gambler.Id, wager.Gambler.Id,
balanceAdjustment, balanceAdjustment,
TransactionSourceEventType.Gambling, TransactionSourceEventType.Gambling,
$"Roulette outcome from wager {wager.Id}"); $"Roulette outcome from wager {wager.Id}", ct: _ct);
// Track results by user // Track results by user
if (!winnersByUser.ContainsKey(bet.Username)) if (!winnersByUser.ContainsKey(bet.Username))
@@ -925,7 +924,7 @@ public static class RouletteAnimationGenerator
/// </summary> /// </summary>
/// <param name="winningNumber">The number (0-36) that the ball should land on</param> /// <param name="winningNumber">The number (0-36) that the ball should land on</param>
/// <returns>A tuple containing the animation duration in seconds and the WebP animation bytes</returns> /// <returns>A tuple containing the animation duration in seconds and the WebP animation bytes</returns>
public static (int durationSeconds, byte[] animationBytes) GenerateAnimation(int winningNumber) public static async Task<(int duration, byte[])> GenerateAnimation(int winningNumber, CancellationToken ct = default)
{ {
if (winningNumber < 0 || winningNumber > 36) if (winningNumber < 0 || winningNumber > 36)
{ {
@@ -996,7 +995,7 @@ public static class RouletteAnimationGenerator
animation.Frames.RemoveFrame(0); animation.Frames.RemoveFrame(0);
using var ms = new MemoryStream(); using var ms = new MemoryStream();
animation.SaveAsWebp(ms, new WebpEncoder { FileFormat = WebpFileFormatType.Lossy, Quality = 50 }); await animation.SaveAsWebpAsync(ms, new WebpEncoder { FileFormat = WebpFileFormatType.Lossy, Quality = 50 }, cancellationToken: ct);
return (duration, ms.ToArray()); return (duration, ms.ToArray());
} }
+23 -22
View File
@@ -14,7 +14,6 @@ using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Services; using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings; using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
namespace KfChatDotNetBot.Commands.Kasino; namespace KfChatDotNetBot.Commands.Kasino;
@@ -127,14 +126,14 @@ public class SlotsCommand : ICommand
{ {
board.LoadAssets(); board.LoadAssets();
board.ExecuteGameLoop(spins, 0, rigged); board.ExecuteGameLoop(spins, 0, rigged);
using (var finalImageStream = board.ExportAndCleanup()) await using (var finalImageStream = await board.ExportAndCleanup())
{ {
if (finalImageStream == null) if (finalImageStream == null)
{ {
throw new InvalidOperationException("board.ExportAndCleanup returned null"); throw new InvalidOperationException("board.ExportAndCleanup returned null");
} }
var imageUrl = await Zipline.Upload(finalImageStream, new MediaTypeHeaderValue("image/webp"), "1h", ctx); var imageUrl = await Zipline.Upload(finalImageStream, new MediaTypeHeaderValue("image/webp"), "1h", ctx);
await botInstance.SendChatMessageAsync($"[img]{imageUrl}[/img]", true, await botInstance.SendChatMessageAsync($"[spoiler=\"Slots game for {user.FormatUsername().Replace("\"", string.Empty)}\"][img]{imageUrl}[/img][/spoiler]", true,
autoDeleteAfter: TimeSpan.FromSeconds(60)); // delay till slots graphic deletion. autoDeleteAfter: TimeSpan.FromSeconds(60)); // delay till slots graphic deletion.
} }
@@ -145,7 +144,7 @@ public class SlotsCommand : ICommand
delayHSec += board.AnimatedImage.Frames[i].Metadata.GetWebpMetadata().FrameDelay; delayHSec += board.AnimatedImage.Frames[i].Metadata.GetWebpMetadata().FrameDelay;
} }
} }
await Task.Delay(TimeSpan.FromSeconds(delayHSec));//adds delay to stop message showing gambling win/loss too early based on total frame count of the animated image await Task.Delay(TimeSpan.FromSeconds(delayHSec), ctx);//adds delay to stop message showing gambling win/loss too early based on total frame count of the animated image
var colors = var colors =
await SettingsProvider.GetMultipleValuesAsync([ await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
@@ -157,7 +156,7 @@ public class SlotsCommand : ICommand
{ {
newBalance = await Money.NewWagerAsync(gambler.Id, wager*spins, -wager*spins, WagerGame.Slots, ct: ctx); newBalance = await Money.NewWagerAsync(gambler.Id, wager*spins, -wager*spins, WagerGame.Slots, ct: ctx);
var totalWager = wager * spins; var totalWager = wager * spins;
await Task.Delay(TimeSpan.FromSeconds(spins)); await Task.Delay(TimeSpan.FromSeconds(spins), ctx);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()} you [color={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]lost[/color] {await totalWager.FormatKasinoCurrencyAsync()} with {spins} spins. Current balance: {await newBalance.FormatKasinoCurrencyAsync()}", $"{user.FormatUsername()} you [color={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]lost[/color] {await totalWager.FormatKasinoCurrencyAsync()} with {spins} spins. Current balance: {await newBalance.FormatKasinoCurrencyAsync()}",
true, autoDeleteAfter: TimeSpan.FromSeconds(30)); true, autoDeleteAfter: TimeSpan.FromSeconds(30));
@@ -185,7 +184,7 @@ public class SlotsCommand : ICommand
await botInstance.BotServices.KasinoShop.ProcessWagerTracking(gambler, WagerGame.Slots, wager*spins, winnings, newBalance); await botInstance.BotServices.KasinoShop.ProcessWagerTracking(gambler, WagerGame.Slots, wager*spins, winnings, newBalance);
} }
//--------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------
await Task.Delay(TimeSpan.FromSeconds(spins * 2)); await Task.Delay(TimeSpan.FromSeconds(spins * 2), ctx);
await botInstance.SendChatMessageAsync( await botInstance.SendChatMessageAsync(
$"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]won[/color] {await rawWinnings.FormatKasinoCurrencyAsync()} from {spins} spins worth {await wager.FormatKasinoCurrencyAsync()}! Net: {winstr}{await winnings.FormatKasinoCurrencyAsync()} Current balance: {await newBalance.FormatKasinoCurrencyAsync()}", true, autoDeleteAfter: TimeSpan.FromSeconds(30)); $"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]won[/color] {await rawWinnings.FormatKasinoCurrencyAsync()} from {spins} spins worth {await wager.FormatKasinoCurrencyAsync()}! Net: {winstr}{await winnings.FormatKasinoCurrencyAsync()} Current balance: {await newBalance.FormatKasinoCurrencyAsync()}", true, autoDeleteAfter: TimeSpan.FromSeconds(30));
} }
@@ -233,8 +232,11 @@ public class SlotsCommand : ICommand
[(1, 0), (2, 1), (3, 2), (2, 3), (1, 4)] [(1, 0), (2, 1), (3, 2), (2, 3), (1, 4)]
]; ];
public KiwiSlotBoard(decimal bet) private CancellationToken _ct;
public KiwiSlotBoard(decimal bet, CancellationToken ct = default)
{ {
_ct = ct;
_userBet = bet; _userBet = bet;
AnimatedImage = new Image<Rgba32>(600, 800); AnimatedImage = new Image<Rgba32>(600, 800);
} }
@@ -370,7 +372,7 @@ public class SlotsCommand : ICommand
lastFrame.Metadata.GetWebpMetadata().FrameDelay = (ushort)hundredthsOfASecond; lastFrame.Metadata.GetWebpMetadata().FrameDelay = (ushort)hundredthsOfASecond;
} }
public MemoryStream? ExportAndCleanup() public async Task<MemoryStream?> ExportAndCleanup()
{ {
if (AnimatedImage.Frames.Count <= 1) return null; if (AnimatedImage.Frames.Count <= 1) return null;
@@ -378,7 +380,7 @@ public class SlotsCommand : ICommand
// Remove the blank placeholder frame // Remove the blank placeholder frame
AnimatedImage.Frames.RemoveFrame(0); AnimatedImage.Frames.RemoveFrame(0);
AnimatedImage.Save(ms, new WebpEncoder { Quality = 80 }); await AnimatedImage.SaveAsync(ms, new WebpEncoder { Quality = 80 }, _ct);
ms.Position = 0; ms.Position = 0;
// Free the animation memory now that it's encoded // Free the animation memory now that it's encoded
@@ -475,9 +477,8 @@ public class SlotsCommand : ICommand
for (var i = 0; i < 5; i++) { for (var i = 0; i < 5; i++) {
for (var j = 0; j < 5; j++) for (var j = 0; j < 5; j++)
{ {
var r = _rand.NextDouble() * 100.6; var r = _rand.NextDouble() * 100.1;
if (f != 0 && j > 2) r *= 1.1; if (f != 0 && j > 1) r *= 1.1;
if (rigged == 'L') r = _rand.NextDouble() * 97.01;
if (rigged == 'W') // guarantee max win if (rigged == 'W') // guarantee max win
{ {
@@ -607,16 +608,16 @@ public class SlotsCommand : ICommand
if (rigged == 'L') RigSlotBoard(); if (rigged == 'L') RigSlotBoard();
char PickSlotSymbol(double r, int i, int j) char PickSlotSymbol(double r, int i, int j)
{ {
if (r < 22) return 'A'; if (r < 22.5) return 'A';
else if (r < 44) return 'B'; else if (r < 44.5) return 'B';
else if (r < 52) return 'C'; else if (r < 52.5) return 'C';
else if (r < 66) return 'D'; else if (r < 66.5) return 'D';
else if (r < 78) return 'E'; else if (r < 78.5) return 'E';
else if (r < 84) return 'F'; else if (r < 84.5) return 'F';
else if (r < 89) return 'G'; else if (r < 89.5) return 'G';
else if (r < 92) return 'H'; else if (r < 92.5) return 'H';
else if (r < 95) return 'I'; else if (r < 95.5) return 'I';
else if (r < 97) return 'J'; else if (r < 97.5) return 'J';
else if (r < 98.5) return WILD; else if (r < 98.5) return WILD;
else if (r < (j <= 2 ? 99 : 99.5)) { if (!ex.Contains(j)) { return EXPANDER; } else return WILD; } else if (r < (j <= 2 ? 99 : 99.5)) { if (!ex.Contains(j)) { return EXPANDER; } else return WILD; }
else { if (fc < 5) { fc++; else { if (fc < 5) { fc++;
+11 -2
View File
@@ -248,7 +248,7 @@ public class XeetEmbedCommand : ICommand
var ffmpegPath = await SettingsProvider.GetValueAsync(BuiltIn.Keys.FFmpegBinaryPath); var ffmpegPath = await SettingsProvider.GetValueAsync(BuiltIn.Keys.FFmpegBinaryPath);
var ffmpegArgs = $"-i \"{tempVideoPath}\" -vf \"fps=10,scale='min(640,iw)':'min(480,ih)':force_original_aspect_ratio=decrease\" -c:v libwebp -lossless 0 -quality 75 -loop 0 -an \"{tempWebpPath}\""; var ffmpegArgs = $"-i \"{tempVideoPath}\" -vf \"fps=20,scale='min(640,iw)':'min(480,ih)':force_original_aspect_ratio=decrease\" -c:v libwebp -threads 4 -lossless 0 -quality 75 -loop 0 -an \"{tempWebpPath}\"";
var processInfo = new ProcessStartInfo var processInfo = new ProcessStartInfo
{ {
@@ -339,10 +339,19 @@ public class XeetEmbedCommand : ICommand
if (mediaUrls.Count > 0) if (mediaUrls.Count > 0)
{ {
if (mediaUrls.Count >= 2)
{
bodyBuilder.Append("[spoiler=\"Media attachments\"]");
}
foreach (var mediaUrl in mediaUrls) foreach (var mediaUrl in mediaUrls)
{ {
bodyBuilder.Append($"[img]{mediaUrl}[/img][br]"); bodyBuilder.Append($"[img]{mediaUrl}[/img]");
} }
if (mediaUrls.Count > 2)
{
bodyBuilder.Append("[/spoiler]");
}
bodyBuilder.Append("[br]");
} }
// Handle quote tweet (if this tweet quotes another) // Handle quote tweet (if this tweet quotes another)
+43 -1
View File
@@ -1,4 +1,5 @@
using System.Text; using System.Globalization;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Models.DbModels;
@@ -137,4 +138,45 @@ public static class Extensions
{ {
return $"@{user.KfUsername}"; return $"@{user.KfUsername}";
} }
/// <summary>
/// Format a grid of equally spaced text into a table so it can be shrunk safely by prepending a [size] tag
/// </summary>
/// <param name="s">Grid you want to format</param>
/// <returns></returns>
public static string GridToTable(this string s)
{
var table = "[table width=\"1%\"]";
foreach (var row in s.Split(["[br]", "[BR]", "\n"], StringSplitOptions.None))
{
table += "[tr]";
var enumerator = StringInfo.GetTextElementEnumerator(row);
while (enumerator.MoveNext())
{
table += $"[td]{enumerator.Current}[/td]";
}
table += "[/tr]";
}
table += "[/table]";
return table;
}
/// <summary>
/// Format a string with multiple lines of text into a table so it can be shrunk without ugly spacing issues by prepending a [size] tag
/// </summary>
/// <param name="s">Multi-line text you want to format</param>
/// <returns></returns>
public static string MultilineToTable(this string s)
{
// No width on this one or it'll wrap text
var table = "[table]";
// Never use a th instead of a tr as it has a more prominent text style
foreach (var row in s.Split(["[br]", "[BR]", "\n"], StringSplitOptions.None))
{
table += $"[tr][td]{row}[/td][/tr]";
}
table += "[/table]";
return table;
}
} }
@@ -0,0 +1,636 @@
// <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("20260510073657_AddImageTags")]
partial class AddImageTags
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
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.GamblerDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("Balance")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<decimal>("NextVipLevelWagerRequirement")
.HasColumnType("TEXT");
b.Property<string>("RandomSeed")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("State")
.HasColumnType("INTEGER");
b.Property<decimal>("TotalWagered")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Gamblers");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerExclusionDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Expires")
.HasColumnType("TEXT");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<int>("Source")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GamblerId");
b.ToTable("Exclusions");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerPerkDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<string>("Metadata")
.HasColumnType("TEXT");
b.Property<decimal?>("Payout")
.HasColumnType("TEXT");
b.Property<string>("PerkName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int?>("PerkTier")
.HasColumnType("INTEGER");
b.Property<int>("PerkType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GamblerId");
b.ToTable("Perks");
});
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.ImageDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("LastSeen")
.HasColumnType("TEXT");
b.Property<string>("Tags")
.HasColumnType("TEXT");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Images");
});
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.MomDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Moms");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.RainbetBetsDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BetId")
.IsRequired()
.HasColumnType("TEXT");
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<double>("CacheDuration")
.HasColumnType("REAL");
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.Property<int>("ValueType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.StreamDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AutoCapture")
.HasColumnType("INTEGER");
b.Property<string>("Metadata")
.HasColumnType("TEXT");
b.Property<int>("Service")
.HasColumnType("INTEGER");
b.Property<string>("StreamUrl")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Streams");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TransactionDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Comment")
.HasColumnType("TEXT");
b.Property<decimal>("Effect")
.HasColumnType("TEXT");
b.Property<int>("EventSource")
.HasColumnType("INTEGER");
b.Property<int?>("FromId")
.HasColumnType("INTEGER");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<decimal>("NewBalance")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<long>("TimeUnixEpochSeconds")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("FromId");
b.HasIndex("GamblerId");
b.ToTable("Transactions");
});
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.UserWhoWasDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ActivityType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("FirstOccurence")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("LatestOccurence")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UsersWhoWere");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.WagerDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<int>("Game")
.HasColumnType("INTEGER");
b.Property<string>("GameMeta")
.HasColumnType("TEXT");
b.Property<bool>("IsComplete")
.HasColumnType("INTEGER");
b.Property<decimal>("Multiplier")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<long>("TimeUnixEpochSeconds")
.HasColumnType("INTEGER");
b.Property<decimal>("WagerAmount")
.HasColumnType("TEXT");
b.Property<decimal>("WagerEffect")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GamblerId");
b.ToTable("Wagers");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerExclusionDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Gambler");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerPerkDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Gambler");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.MomDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.StreamDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TransactionDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "From")
.WithMany()
.HasForeignKey("FromId");
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("From");
b.Navigation("Gambler");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserWhoWasDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.WagerDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Gambler");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace KfChatDotNetBot.Migrations
{
/// <inheritdoc />
public partial class AddImageTags : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Tags",
table: "Images",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Tags",
table: "Images");
}
}
}
@@ -0,0 +1,640 @@
// <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("20260510172529_ImageTagList")]
partial class ImageTagList
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
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.GamblerDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("Balance")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<decimal>("NextVipLevelWagerRequirement")
.HasColumnType("TEXT");
b.Property<string>("RandomSeed")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("State")
.HasColumnType("INTEGER");
b.Property<decimal>("TotalWagered")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Gamblers");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerExclusionDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Expires")
.HasColumnType("TEXT");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<int>("Source")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GamblerId");
b.ToTable("Exclusions");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerPerkDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<string>("Metadata")
.HasColumnType("TEXT");
b.Property<decimal?>("Payout")
.HasColumnType("TEXT");
b.Property<string>("PerkName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int?>("PerkTier")
.HasColumnType("INTEGER");
b.Property<int>("PerkType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GamblerId");
b.ToTable("Perks");
});
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.ImageDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("LastSeen")
.HasColumnType("TEXT");
b.PrimitiveCollection<string>("TagList")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Tags")
.HasColumnType("TEXT");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Images");
});
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.MomDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Moms");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.RainbetBetsDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BetId")
.IsRequired()
.HasColumnType("TEXT");
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<double>("CacheDuration")
.HasColumnType("REAL");
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.Property<int>("ValueType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.StreamDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AutoCapture")
.HasColumnType("INTEGER");
b.Property<string>("Metadata")
.HasColumnType("TEXT");
b.Property<int>("Service")
.HasColumnType("INTEGER");
b.Property<string>("StreamUrl")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Streams");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TransactionDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Comment")
.HasColumnType("TEXT");
b.Property<decimal>("Effect")
.HasColumnType("TEXT");
b.Property<int>("EventSource")
.HasColumnType("INTEGER");
b.Property<int?>("FromId")
.HasColumnType("INTEGER");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<decimal>("NewBalance")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<long>("TimeUnixEpochSeconds")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("FromId");
b.HasIndex("GamblerId");
b.ToTable("Transactions");
});
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.UserWhoWasDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ActivityType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("FirstOccurence")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("LatestOccurence")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UsersWhoWere");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.WagerDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<int>("Game")
.HasColumnType("INTEGER");
b.Property<string>("GameMeta")
.HasColumnType("TEXT");
b.Property<bool>("IsComplete")
.HasColumnType("INTEGER");
b.Property<decimal>("Multiplier")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<long>("TimeUnixEpochSeconds")
.HasColumnType("INTEGER");
b.Property<decimal>("WagerAmount")
.HasColumnType("TEXT");
b.Property<decimal>("WagerEffect")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GamblerId");
b.ToTable("Wagers");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerExclusionDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Gambler");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerPerkDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Gambler");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.MomDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.StreamDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TransactionDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "From")
.WithMany()
.HasForeignKey("FromId");
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("From");
b.Navigation("Gambler");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserWhoWasDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.WagerDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Gambler");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace KfChatDotNetBot.Migrations
{
/// <inheritdoc />
public partial class ImageTagList : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "TagList",
table: "Images",
type: "TEXT",
nullable: false,
defaultValue: "[]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TagList",
table: "Images");
}
}
}
@@ -0,0 +1,665 @@
// <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("20260510194844_ImageMetadata")]
partial class ImageMetadata
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
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.GamblerDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("Balance")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<decimal>("NextVipLevelWagerRequirement")
.HasColumnType("TEXT");
b.Property<string>("RandomSeed")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("State")
.HasColumnType("INTEGER");
b.Property<decimal>("TotalWagered")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Gamblers");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerExclusionDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Expires")
.HasColumnType("TEXT");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<int>("Source")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GamblerId");
b.ToTable("Exclusions");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerPerkDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<string>("Metadata")
.HasColumnType("TEXT");
b.Property<decimal?>("Payout")
.HasColumnType("TEXT");
b.Property<string>("PerkName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int?>("PerkTier")
.HasColumnType("INTEGER");
b.Property<int>("PerkType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GamblerId");
b.ToTable("Perks");
});
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.ImageDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("LastSeen")
.HasColumnType("TEXT");
b.PrimitiveCollection<string>("TagList")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Tags")
.HasColumnType("TEXT");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Images");
});
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.MomDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Moms");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.RainbetBetsDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BetId")
.IsRequired()
.HasColumnType("TEXT");
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<double>("CacheDuration")
.HasColumnType("REAL");
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.Property<int>("ValueType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.StreamDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AutoCapture")
.HasColumnType("INTEGER");
b.Property<string>("Metadata")
.HasColumnType("TEXT");
b.Property<int>("Service")
.HasColumnType("INTEGER");
b.Property<string>("StreamUrl")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Streams");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TransactionDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Comment")
.HasColumnType("TEXT");
b.Property<decimal>("Effect")
.HasColumnType("TEXT");
b.Property<int>("EventSource")
.HasColumnType("INTEGER");
b.Property<int?>("FromId")
.HasColumnType("INTEGER");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<decimal>("NewBalance")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<long>("TimeUnixEpochSeconds")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("FromId");
b.HasIndex("GamblerId");
b.ToTable("Transactions");
});
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.UserWhoWasDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ActivityType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("FirstOccurence")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("LatestOccurence")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UsersWhoWere");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.WagerDbModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GamblerId")
.HasColumnType("INTEGER");
b.Property<int>("Game")
.HasColumnType("INTEGER");
b.Property<string>("GameMeta")
.HasColumnType("TEXT");
b.Property<bool>("IsComplete")
.HasColumnType("INTEGER");
b.Property<decimal>("Multiplier")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<long>("TimeUnixEpochSeconds")
.HasColumnType("INTEGER");
b.Property<decimal>("WagerAmount")
.HasColumnType("TEXT");
b.Property<decimal>("WagerEffect")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GamblerId");
b.ToTable("Wagers");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerExclusionDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Gambler");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerPerkDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Gambler");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ImageDbModel", b =>
{
b.OwnsOne("KfChatDotNetBot.Models.DbModels.ImageMetadataModel", "Metadata", b1 =>
{
b1.Property<int>("ImageDbModelId");
b1.Property<int>("AddedByUserId");
b1.Property<DateTimeOffset>("WhenAdded");
b1.HasKey("ImageDbModelId");
b1.ToTable("Images");
b1
.ToJson("Metadata")
.HasColumnType("TEXT");
b1.WithOwner()
.HasForeignKey("ImageDbModelId");
});
b.Navigation("Metadata");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.MomDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.StreamDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TransactionDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "From")
.WithMany()
.HasForeignKey("FromId");
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("From");
b.Navigation("Gambler");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserWhoWasDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.WagerDbModel", b =>
{
b.HasOne("KfChatDotNetBot.Models.DbModels.GamblerDbModel", "Gambler")
.WithMany()
.HasForeignKey("GamblerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Gambler");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace KfChatDotNetBot.Migrations
{
/// <inheritdoc />
public partial class ImageMetadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Metadata",
table: "Images",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Metadata",
table: "Images");
}
}
}
@@ -15,7 +15,7 @@ namespace KfChatDotNetBot.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ChipsggBetDbModel", b => modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ChipsggBetDbModel", b =>
{ {
@@ -212,6 +212,13 @@ namespace KfChatDotNetBot.Migrations
b.Property<DateTimeOffset>("LastSeen") b.Property<DateTimeOffset>("LastSeen")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.PrimitiveCollection<string>("TagList")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Tags")
.HasColumnType("TEXT");
b.Property<string>("Url") b.Property<string>("Url")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@@ -555,6 +562,31 @@ namespace KfChatDotNetBot.Migrations
b.Navigation("Gambler"); b.Navigation("Gambler");
}); });
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ImageDbModel", b =>
{
b.OwnsOne("KfChatDotNetBot.Models.DbModels.ImageMetadataModel", "Metadata", b1 =>
{
b1.Property<int>("ImageDbModelId");
b1.Property<int>("AddedByUserId");
b1.Property<DateTimeOffset>("WhenAdded");
b1.HasKey("ImageDbModelId");
b1.ToTable("Images");
b1
.ToJson("Metadata")
.HasColumnType("TEXT");
b1.WithOwner()
.HasForeignKey("ImageDbModelId");
});
b.Navigation("Metadata");
});
modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b => modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b =>
{ {
b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User") b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User")
@@ -6,4 +6,29 @@ public class ImageDbModel
public required string Key { get; set; } public required string Key { get; set; }
public required string Url { get; set; } public required string Url { get; set; }
public required DateTimeOffset LastSeen { get; set; } public required DateTimeOffset LastSeen { get; set; }
[Obsolete("Use TagList instead")]
public string? Tags { get; set; }
/// <summary>
/// List of image tags for recalling specific images
/// </summary>
public required List<string> TagList { get; set; } = [];
/// <summary>
/// JSON object containing whatever bullshit metadata we want to attach to this image
/// Value will be null for images that were added prior to metadata being introduced
/// </summary>
public required ImageMetadataModel? Metadata { get; set; }
}
public class ImageMetadataModel
{
/// <summary>
/// User ID (IN THE BOT, NOT KIWI FARMS USER ID) of whoever added this image
/// </summary>
public required int AddedByUserId { get; set; }
/// <summary>
/// When the image was added to the database
/// </summary>
public required DateTimeOffset WhenAdded { get; set; }
} }
@@ -0,0 +1,305 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace KfChatDotNetBot.Models.DbModels;
public class KasinoShopProfileDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Shop profiles belong to a user, not their gambler ID
/// they persist even if the user abandons their profile
/// </summary>
public required UserDbModel User { get; set; }
/// <summary>
/// Assets held by this profile
/// </summary>
public required List<KasinoShopProfileAssetDbModel> Assets { get; set; }
/// <summary>
/// Loans taken out by this profile
/// </summary>
[InverseProperty(nameof(KasinoShopProfileLoanDbModel.Borrower))]
public required List<KasinoShopProfileLoanDbModel> LoansTaken { get; set; }
/// <summary>
/// Loans owed to this profile
/// </summary>
[InverseProperty(nameof(KasinoShopProfileLoanDbModel.Lender))]
public required List<KasinoShopProfileLoanDbModel> LoansOwed { get; set; }
/// <summary>
/// State of the profile
/// </summary>
public required KasinoShopProfileStateFlags State { get; set; } = KasinoShopProfileStateFlags.None;
/// <summary>
/// JSON object containing data related to the above states
/// </summary>
public required KasinoShopProfileStateDataModel StateData { get; set; }
/// <summary>
/// Profile balance in the "Krypto" currency
/// </summary>
public required decimal KryptoBalance { get; set; }
}
// Note this is serialized to JSON by Entity Framework so you can go wild shoving random bullshit in here
public class KasinoShopProfileStateDataModel
{
/// <summary>
/// Profile credit score for determining creditworthiness etc.
/// </summary>
// Actually considered making this uint but I like the idea of negative credit
public required int KreditScore { get; set; }
/// <summary>
/// Amount this user has wagered towards their sponsor requirement
/// </summary>
public decimal? SponsorWagerAmount { get; set; } = null;
/// <summary>
/// The sponsor's wager requirement
/// </summary>
public decimal? SponsorWagerRequirement { get; set; } = null;
/// <summary>
/// Modifier that alters the house edge for your gambler entity
/// </summary>
public required decimal HouseEdgeModifier { get; set; } = 1;
/// <summary>
/// How much crack you've smoked?
/// </summary>
public required int CrackCounter { get; set; } = 0;
/// <summary>
/// How many floor nugs you got embedded in the carpet
/// </summary>
public required int FloorNugs { get; set; } = 0;
/// <summary>
/// Time when your weed buff ends
/// </summary>
public required DateTimeOffset WeedBuffEnds { get; set; } = DateTimeOffset.UtcNow;
/// <summary>
/// Time when your crack buff ends
/// </summary>
public required DateTimeOffset CrackBuffEnds { get; set; } = DateTimeOffset.UtcNow;
/// <summary>
/// Dodgy stat tracking
/// </summary>
public required KasinoShopStatTrackerModel StatTracker { get; set; } = new();
}
public class KasinoShopStatTrackerModel
{
public decimal TotalDeposited { get; set; } = 0;
public decimal TotalWithdrawn { get; set; } = 0;
public decimal TotalLossback { get; set; } = 0;
/// <summary>
/// Track wager statistics by game
/// </summary>
public Dictionary<WagerGame, decimal> StatTracker { get; set; } = new();
}
public class KasinoShopProfileLoanDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Profile of the user who owns this loan
/// </summary>
public required KasinoShopProfileDbModel Borrower { get; set; }
/// <summary>
/// Profile of the user to whom this loan is owed/payable to
/// </summary>
public required KasinoShopProfileDbModel Lender { get; set; }
/// <summary>
/// Amount loaned
/// </summary>
public required decimal Amount { get; set; }
/// <summary>
/// Amount to be paid out to the loaner
/// </summary>
public required decimal PayoutAmount { get; set; }
/// <summary>
/// Date and time loan entry was created
/// </summary>
public required DateTimeOffset Created { get; set; }
/// <summary>
/// State of this loan
/// </summary>
public required LoanState State { get; set; }
}
public class KasinoShopProfileAssetDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Profile of the user who owns this asset
/// </summary>
public required KasinoShopProfileDbModel Profile { get; set; }
/// <summary>
/// Value of the item at the time of acquisition in Krypto
/// </summary>
public required decimal OriginalValue { get; set; }
/// <summary>
/// What the value of the item is right now
/// </summary>
public required decimal CurrentValue { get; set; }
/// <summary>
/// Asset name
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Asset type
/// </summary>
public required AssetType AssetType { get; set; }
/// <summary>
/// Date and time the asset was acquired
/// </summary>
public required DateTimeOffset Acquired { get; set; }
/// <summary>
/// History of value changes (e.g. interest events)
/// </summary>
public required List<KasinoShopProfileAssetValueChangeDbModel> ValueChangeReports { get; set; }
/// <summary>
/// Use this to store enum values for assets that have a subtype (e.g. Car Type)
/// but were otherwise not special enough to have their own table (e.g. Car)
/// </summary>
public int? AssetSubType { get; set; } = null;
/// <summary>
/// Serialized JSON for extra information where the schema can't accommodate for you
/// </summary>
public string? Extra { get; set; } = null;
}
public class KasinoShopProfileAssetInvestmentDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Related asset for this investment
/// </summary>
public required KasinoShopProfileAssetDbModel Asset { get; set; }
/// <summary>
/// What type of investment it is
/// </summary>
public required InvestmentType InvestmentType { get; set; }
/// <summary>
/// Last time interest was calculated
/// </summary>
public required DateTimeOffset LastInterestCalculation { get; set; }
/// <summary>
/// Low point for interest calculations
/// </summary>
public required float InterestRangeMin { get; set; }
/// <summary>
/// High point for interest calculations
/// </summary>
public required float InterestRangeMax { get; set; }
/// <summary>
/// Use this to store enum values for investments that have a subtype (e.g. Shoe Brand)
/// but were otherwise not special enough to have their own table (e.g. Shoe)
/// </summary>
public int? InvestmentSubType { get; set; } = null;
/// <summary>
/// Serialized JSON for extra information where the schema can't accommodate for you
/// </summary>
public string? Extra { get; set; } = null;
}
public class KasinoShopProfileAssetValueChangeDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Related asset
/// </summary>
public required KasinoShopProfileAssetDbModel Asset { get; set; }
/// <summary>
/// Effect of the change
/// </summary>
public required decimal ValueChangeEffect { get; set; }
/// <summary>
/// Change percent as a decimal fraction?
/// </summary>
public required decimal ValueChangePercent { get; set; }
/// <summary>
/// Descriptive text for the value change (like the source of it)
/// </summary>
public required string Description { get; set; }
}
[Flags]
public enum KasinoShopProfileStateFlags : ulong
{
None,
IsSponsored,
IsWeeded,
IsCracked,
IsInWithdrawal,
IsLoanable
}
[Flags]
public enum KasinoShopProfileAssetState : ulong
{
None,
/// <summary>
/// Only applicable to smashable objects (e.g. PC peripherals)
/// </summary>
IsSmashed
}
public enum AssetType
{
Investment,
Smashable,
Car,
Random
}
public enum InvestmentType
{
Shoes,
Stake,
Gold,
Silver,
Skin,
House,
Random
}
public enum LoanState
{
/// <summary>
/// Loan not fully paid but borrower still in good standing
/// </summary>
Active,
/// <summary>
/// Past due but not yet a serious violation of terms
/// </summary>
Delinquent,
/// <summary>
/// Loan terms violated, time to collect
/// </summary>
Default,
/// <summary>
/// Loan settled by agreement to amended terms (e.g. paid off less than the full amount)
/// </summary>
Settled,
/// <summary>
/// Loan fully repaid for the total amount and closed out
/// </summary>
Repaid,
/// <summary>
/// Written off debt due to being unable to collect
/// </summary>
Uncollectible,
/// <summary>
/// Administrative state for loans canceled due to serious malfeasance
/// </summary>
Canceled
}
@@ -202,113 +202,6 @@ public class GamblerPerkDbModel
public decimal? Payout { get; set; } public decimal? Payout { get; set; }
} }
public class KasinoShopProfileDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Shop profiles belong to a user, not their gambler ID
/// they persist even if the user abandons their profile
/// </summary>
public required UserDbModel User { get; set; }
public required List<KasinoShopProfileAssetDbModel> Assets { get; set; }
public required List<KasinoShopProfileLoanDbModel> Loans { get; set; }
}
public class KasinoShopProfileLoanDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
// Foreign key that the powers that be told me I need for the fancy navigation property
public int ProfileId { get; set; }
/// <summary>
/// Profile of the user who owns this loan
/// </summary>
public required KasinoShopProfileDbModel Profile { get; set; }
// Foreign key that the powers that be told me I need for the fancy navigation property
public int PayableToId { get; set; }
/// <summary>
/// Profile of the user to whom this loan is owed/payable to
/// </summary>
public required KasinoShopProfileDbModel PayableTo { get; set; }
/// <summary>
/// Amount loaned
/// </summary>
public required decimal Amount { get; set; }
/// <summary>
/// Amount to be paid out to the loaner
/// </summary>
public required decimal PayoutAmount { get; set; }
/// <summary>
/// Date and time loan entry was created
/// </summary>
public required DateTimeOffset Created { get; set; }
}
public class KasinoShopProfileAssetDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Profile of the user who owns this asset
/// </summary>
public required KasinoShopProfileDbModel Profile { get; set; }
/// <summary>
/// Value of the item at the time of acquisition in Krypto
/// </summary>
public required decimal OriginalValue { get; set; }
/// <summary>
/// Asset name
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Asset type
/// </summary>
public required AssetType AssetType { get; set; }
/// <summary>
/// Date and time the asset was acquired
/// </summary>
public required DateTimeOffset Acquired { get; set; }
/// <summary>
/// History of value changes (e.g. interest events)
/// </summary>
public required List<KasinoShopProfileAssetValueChangeDbModel> ValueChangeReports { get; set; }
/// <summary>
/// Serialized JSON for extra information useful for certain assets (e.g. car model)
/// </summary>
public string? Extra { get; set; } = null;
}
public class KasinoShopProfileAssetValueChangeDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Related asset
/// </summary>
public required KasinoShopProfileAssetDbModel Asset { get; set; }
/// <summary>
/// Effect of the change
/// </summary>
public required decimal ValueChangeEffect { get; set; }
/// <summary>
/// Change percent as a decimal fraction?
/// </summary>
public required decimal ValueChangePercent { get; set; }
/// <summary>
/// Descriptive text for the value change (like the source of it)
/// </summary>
public required string Description { get; set; }
}
public enum GamblerPerkType public enum GamblerPerkType
{ {
/// <summary> /// <summary>
@@ -0,0 +1,9 @@
namespace KfChatDotNetBot.Models;
/// <summary>
/// Holds state information for the kasino shop
/// </summary>
public class KasinoShopStateModel
{
public required decimal DefaultHouseEdgeModifier { get; set; } = 0;
}
+28 -1
View File
@@ -20,6 +20,8 @@
*/ */
using System.Text; using System.Text;
using KfChatDotNetBot.Migrations;
using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings; using KfChatDotNetBot.Settings;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NLog; using NLog;
@@ -38,9 +40,34 @@ namespace KfChatDotNetBot
await BuiltIn.SyncSettingsWithDb(); await BuiltIn.SyncSettingsWithDb();
logger.Info("Migrating settings from config.json (if needed)"); logger.Info("Migrating settings from config.json (if needed)");
await BuiltIn.MigrateJsonSettingsToDb(); await BuiltIn.MigrateJsonSettingsToDb();
logger.Info("Attempting to grab the Redis connection multiplexer so it's built");
try
{
_ = Redis.Multiplexer;
}
catch (Exception e)
{
logger.Error("Caught an error when attempting to grab the Redis multiplexer");
logger.Error(e);
}
if (await db.Images.AnyAsync())
{
logger.Info("Checking to see if we need to migrate Tags to TagList");
#pragma warning disable CS0618 // Type or member is obsolete
var scope = (await db.Images.Where(i => i.Tags != null).ToListAsync()).Where(i => i.TagList.Count == 0).ToList();
foreach (var item in scope)
{
// Ignoring the null as my query literally filters for != null
item.TagList = item.Tags!.Split(" ").ToList();
#pragma warning restore CS0618 // Type or member is obsolete
logger.Info($"Migrated tags for image {item.Id}");
}
await db.SaveChangesAsync();
}
logger.Info("Handing over to bot now"); logger.Info("Handing over to bot now");
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;
new ChatBot(); _ = new ChatBot();
} }
} }
} }
+3 -3
View File
@@ -42,16 +42,16 @@ internal class BotCommands
internal void ProcessMessage(BotCommandMessageModel message) internal void ProcessMessage(BotCommandMessageModel message)
{ {
if (string.IsNullOrEmpty(message.MessageRaw)) if (string.IsNullOrEmpty(message.MessageRawHtmlDecoded))
{ {
return; return;
} }
var messageTrimmed = message.MessageRaw.TrimStart(CommandPrefix); var messageTrimmed = message.MessageRawHtmlDecoded.TrimStart(CommandPrefix);
foreach (var command in Commands) foreach (var command in Commands)
{ {
var noPrefixCommand = HasAttribute<NoPrefixRequired>(command); var noPrefixCommand = HasAttribute<NoPrefixRequired>(command);
if (!noPrefixCommand && !message.MessageRaw.StartsWith(CommandPrefix)) continue; if (!noPrefixCommand && !message.MessageRawHtmlDecoded.StartsWith(CommandPrefix)) continue;
foreach (var regex in command.Patterns) foreach (var regex in command.Patterns)
{ {
var match = regex.Match(messageTrimmed); var match = regex.Match(messageTrimmed);
+3 -2
View File
@@ -390,12 +390,13 @@ public class BotServices
_logger.Info("Built the almanac shill task"); _logger.Info("Built the almanac shill task");
} }
private Task BuildDLiveStatusCheck() private async Task BuildDLiveStatusCheck()
{ {
var enabled = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.DLiveEnabled)).ToBoolean();
if (!enabled) return;
_dliveStatusCheck = new DLive(_chatBot); _dliveStatusCheck = new DLive(_chatBot);
_dliveStatusCheck.StartLiveStatusCheck(); _dliveStatusCheck.StartLiveStatusCheck();
_logger.Info("Built the DLive livestream status check task"); _logger.Info("Built the DLive livestream status check task");
return Task.CompletedTask;
} }
private Task BuildPeerTubeLiveStatusCheck() private Task BuildPeerTubeLiveStatusCheck()
@@ -37,15 +37,14 @@ public class ConversationContextManager
public ConversationContextManager() public ConversationContextManager()
{ {
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; if (!Redis.IsAvailable)
if (string.IsNullOrEmpty(connectionString.Value))
{ {
Logger.Error($"Can't initialize the Nora ConversationContextManager service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); Logger.Error($"Can't initialize the Nora ConversationContextManager service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
$"or the Redis client failed to connect");
throw new InvalidOperationException("Redis isn't configured"); throw new InvalidOperationException("Redis isn't configured");
} }
var redis = ConnectionMultiplexer.Connect(connectionString.Value); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
} }
public static string GetContextKeyAsync(string mode, int userId, int roomId) public static string GetContextKeyAsync(string mode, int userId, int roomId)
+4 -6
View File
@@ -23,15 +23,14 @@ public class KasinoKrash : IDisposable
{ {
_kfChatBot = kfChatBot; _kfChatBot = kfChatBot;
_ct = ct; _ct = ct;
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; if (!Redis.IsAvailable)
if (string.IsNullOrEmpty(connectionString.Value))
{ {
_logger.Error($"Can't initialize the Kasino Krash service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); _logger.Error($"Can't initialize the Kasino Krash service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
$"or the Redis service failed to connect");
return; return;
} }
var redis = ConnectionMultiplexer.Connect(connectionString.Value); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
//attempt to pull a game from the db in case the bot crashed while a game was ongoing. if so it will restart the run //attempt to pull a game from the db in case the bot crashed while a game was ongoing. if so it will restart the run
TheGame = GetKrashState().Result; TheGame = GetKrashState().Result;
if (TheGame != null) _ = RunGame(); if (TheGame != null) _ = RunGame();
@@ -203,7 +202,6 @@ public class KasinoKrash : IDisposable
await _kfChatBot.SendChatMessageAsync( await _kfChatBot.SendChatMessageAsync(
$"{bet.Gambler.User.FormatUsername()}, due to your poor gambling skills, your bet was scaled down to {await bet.Wager.FormatKasinoCurrencyAsync()} to match your remaining balance.", $"{bet.Gambler.User.FormatUsername()}, due to your poor gambling skills, your bet was scaled down to {await bet.Wager.FormatKasinoCurrencyAsync()} to match your remaining balance.",
true, autoDeleteAfter: TimeSpan.FromSeconds(10)); true, autoDeleteAfter: TimeSpan.FromSeconds(10));
continue;
} }
} }
else if (bet.Multi <= TheGame.FinalMulti && bet.Multi != -1) else if (bet.Multi <= TheGame.FinalMulti && bet.Multi != -1)
+9 -7
View File
@@ -241,15 +241,14 @@ public class KasinoMines
public KasinoMines(ChatBot kfChatBot, int gamblerId) public KasinoMines(ChatBot kfChatBot, int gamblerId)
{ {
_kfChatBot = kfChatBot; _kfChatBot = kfChatBot;
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; if (!Redis.IsAvailable)
if (string.IsNullOrEmpty(connectionString.Value))
{ {
_logger.Error($"Can't initialize the Kasino Mines service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); _logger.Error($"Can't initialize the Kasino Mines service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
$"or the Redis service failed to connect");
return; return;
} }
var redis = ConnectionMultiplexer.Connect(connectionString.Value); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
GetSavedGames(gamblerId).Wait(); GetSavedGames(gamblerId).Wait();
} }
@@ -299,7 +298,11 @@ public class KasinoMines
{ {
await GetSavedGames(gamblerId); await GetSavedGames(gamblerId);
//attempt to delete the message if its there //attempt to delete the message if its there
if (ActiveGames[gamblerId].LastMessageId != null) await _kfChatBot.KfClient.DeleteMessageAsync(ActiveGames[gamblerId].LastMessageId!); var lastMsgId = ActiveGames[gamblerId].LastMessageId;
if (lastMsgId != null)
{
_kfChatBot.ScheduleMessageAutoDelete(lastMsgId, TimeSpan.FromSeconds(15));
}
ActiveGames.Remove(gamblerId); ActiveGames.Remove(gamblerId);
await SaveActiveGames(gamblerId); await SaveActiveGames(gamblerId);
} }
@@ -338,7 +341,6 @@ public class KasinoMines
await _kfChatBot.SendChatMessageAsync( await _kfChatBot.SendChatMessageAsync(
$"{game.Creator.User.FormatUsername()}, you won {await payout.FormatKasinoCurrencyAsync()} from your {await game.Wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.BetsPlaced.Count} gems while avoiding {game.Mines} mines. Net: {await net.FormatKasinoCurrencyAsync()}. Balance: {await newBalance.FormatKasinoCurrencyAsync()}", true, autoDeleteAfter: TimeSpan.FromSeconds(15)); $"{game.Creator.User.FormatUsername()}, you won {await payout.FormatKasinoCurrencyAsync()} from your {await game.Wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.BetsPlaced.Count} gems while avoiding {game.Mines} mines. Net: {await net.FormatKasinoCurrencyAsync()}. Balance: {await newBalance.FormatKasinoCurrencyAsync()}", true, autoDeleteAfter: TimeSpan.FromSeconds(15));
await Task.Delay(TimeSpan.FromSeconds(15));
await RemoveGame(game.Creator.Id); await RemoveGame(game.Creator.Id);
} }
+4 -5
View File
@@ -22,15 +22,14 @@ public class KasinoRain : IDisposable
{ {
_kfChatBot = kfChatBot; _kfChatBot = kfChatBot;
_ct = ct; _ct = ct;
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; if (!Redis.IsAvailable)
if (string.IsNullOrEmpty(connectionString.Value))
{ {
_logger.Error($"Can't initialize the Kasino Rain service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); _logger.Error($"Can't initialize the Kasino Rain service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
$"or the Redis service failed to connect");
return; return;
} }
var redis = ConnectionMultiplexer.Connect(connectionString.Value); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
_rainTimerTask = Task.Run(RainTimerTask, ct); _rainTimerTask = Task.Run(RainTimerTask, ct);
} }
-9
View File
@@ -1,18 +1,9 @@
using System.Text.Json; using System.Text.Json;
using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings; using KfChatDotNetBot.Settings;
using NLog; using NLog;
using StackExchange.Redis; using StackExchange.Redis;
using System.Text.Json.Serialization;
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; using Microsoft.EntityFrameworkCore;
using RandN; using RandN;
using RandN.Compat; using RandN.Compat;
+110
View File
@@ -0,0 +1,110 @@
using KfChatDotNetBot.Settings;
using System.Text.Json;
using NLog;
using StackExchange.Redis;
namespace KfChatDotNetBot.Services;
public static class Redis
{
public static bool IsAvailable => LazyMultiplexer.IsValueCreated;
// Claude told me this will act like a singleton ConnectionMultiplexer
// while keeping things nice and convenient with static methods
// FYI the exception will be thrown once, cached for the lifetime of the application
// If you configure a Redis connection string, you MUST restart the application
// https://learn.microsoft.com/en-us/dotnet/api/system.lazy-1?view=net-10.0#:~:text=Exception%20caching,-When
private static readonly Lazy<ConnectionMultiplexer> LazyMultiplexer =
new(() =>
{
var logger = LogManager.GetCurrentClassLogger();
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result.Value;
if (string.IsNullOrEmpty(connectionString))
{
logger.Error($"Could not initiate the lazy connection multiplexer for the Redis service as the " +
$"connection string is not configured in {BuiltIn.Keys.BotRedisConnectionString}. " +
$"Redis won't be available to anything that relies on it. " +
$"If you do configure {BuiltIn.Keys.BotRedisConnectionString}, YOU MUST RESTART THE BOT");
throw new InvalidOperationException();
}
try
{
return ConnectionMultiplexer.Connect(
SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result.Value ??
throw new InvalidOperationException(
$"{BuiltIn.Keys.BotRedisConnectionString} not defined, cannot connect to Redis"));
}
catch (Exception e)
{
logger.Error($"Caught an exception when connecting to Redis at {connectionString}");
logger.Error(e);
throw;
}
});
// You can just grab this from wherever if you want a ready to go Redis connection
// ReSharper disable once MemberCanBePrivate.Global
public static ConnectionMultiplexer Multiplexer => LazyMultiplexer.Value;
private static IDatabase Db => Multiplexer.GetDatabase();
/// <summary>
/// Fetches a key from Redis asynchronously and deserializes its JSON value to T.
/// Returns default(T) if the key doesn't exist.
/// </summary>
/// <param name="key">Redis key</param>
public static async Task<T?> GetJsonAsync<T>(string key)
{
var value = await Db.StringGetAsync(key);
if (value.IsNullOrEmpty)
return default;
return JsonSerializer.Deserialize<T>(value.ToString());
}
/// <summary>
/// Fetches a key from Redis synchronously and deserializes its JSON value to T.
/// Returns default(T) if the key doesn't exist.
/// </summary>
/// <param name="key">Redis key</param>
public static T? GetJson<T>(string key)
{
var value = Db.StringGet(key);
if (value.IsNullOrEmpty)
return default;
return JsonSerializer.Deserialize<T>(value.ToString());
}
/// <summary>
/// Asynchronously set a key to a given object serialized using JSON
/// </summary>
/// <param name="key">Redis key</param>
/// <param name="value">Object that you wish to serialize</param>
/// <param name="expires">Expiration (null means never expires)</param>
/// <param name="when">Redis behavior whether the key has a value or not
/// When.Always = set the value regardless of whether the key has a value
/// When.Exists = only set the value if the key has a value already
/// When.NotExists = only set the value if the key has no value
/// </param>
public static async Task SetJsonAsync(string key, object value, TimeSpan? expires = null, When when = When.Always)
{
await Db.StringSetAsync(key, JsonSerializer.Serialize(value), expires, when);
}
/// <summary>
/// Synchronously set a key to a given object serialized using JSON
/// </summary>
/// <param name="key">Redis key</param>
/// <param name="value">Object that you wish to serialize</param>
/// <param name="expires">Expiration (null means never expires)</param>
/// <param name="when">Redis behavior whether the key has a value or not
/// When.Always = set the value regardless of whether the key has a value
/// When.Exists = only set the value if the key has a value already
/// When.NotExists = only set the value if the key has no value
/// </param>
public static void SetJson(string key, object value, TimeSpan? expires = null, When when = When.Always)
{
Db.StringSet(key, JsonSerializer.Serialize(value), expires, when);
}
}
+9
View File
@@ -586,6 +586,15 @@ public static class BuiltIn
public static string WinnaBmjUsername = "Winna.BmjUsername"; public static string WinnaBmjUsername = "Winna.BmjUsername";
[BuiltInSetting("Array of cookies as a shitty hack to get Winna going", SettingValueType.Array, "[]")] [BuiltInSetting("Array of cookies as a shitty hack to get Winna going", SettingValueType.Array, "[]")]
public static string WinnaCookies = "Winna.Cookies"; public static string WinnaCookies = "Winna.Cookies";
[BuiltInSetting("Whether the DLive livestream check is enabled", SettingValueType.Boolean, "false",
BooleanRegex)]
public static string DLiveEnabled = "DLive.Enabled";
[BuiltInSetting("Size (%) of the Keno board", SettingValueType.Text, "70", WholeNumberRegex)]
public static string KasinoKenoSize = "Kasino.Keno.Size";
[BuiltInSetting("Size (%) of the Planes board", SettingValueType.Text, "70", WholeNumberRegex)]
public static string KasinoPlanesSize = "Kasino.Planes.Size";
[BuiltInSetting("Size (%) of the Plinko board", SettingValueType.Text, "70", WholeNumberRegex)]
public static string KasinoPlinkoSize = "Kasino.Plinko.Size";
} }
} }