Compare commits

...

50 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
28 changed files with 2590 additions and 122 deletions
+2
View File
@@ -14,6 +14,8 @@ public class ApplicationDbContext : DbContext
{ {
//modelBuilder.Entity<KasinoShopProfileDbModel>() //modelBuilder.Entity<KasinoShopProfileDbModel>()
// .OwnsOne(p => p.StateData, b => b.ToJson()); // .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,6 +58,7 @@ 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,
@@ -76,7 +77,7 @@ 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));
@@ -271,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)
{ {
@@ -289,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
{ {
@@ -386,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)
{ {
@@ -398,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
@@ -407,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))
{ {
@@ -432,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);
@@ -459,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;
} }
@@ -520,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)
{ {
@@ -537,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;
@@ -545,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))
@@ -923,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)
{ {
@@ -994,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; }
} }
+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()
-1
View File
@@ -202,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)
+5 -2
View File
@@ -298,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);
} }
@@ -337,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);
} }
+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";
} }
} }