From d71819819d50810c917aa0eeb7bc2a0ff0cf463b Mon Sep 17 00:00:00 2001 From: barelyprofessional <150058423+barelyprofessional@users.noreply.github.com> Date: Sun, 10 May 2026 15:30:54 -0500 Subject: [PATCH] Updated the tagging code from cohle - Uses a List 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 --- KfChatDotNetBot/ApplicationDbContext.cs | 2 + KfChatDotNetBot/Commands/ImageCommands.cs | 145 ++-- .../20260510172529_ImageTagList.Designer.cs | 640 +++++++++++++++++ .../Migrations/20260510172529_ImageTagList.cs | 29 + .../20260510194844_ImageMetadata.Designer.cs | 665 ++++++++++++++++++ .../20260510194844_ImageMetadata.cs | 28 + .../ApplicationDbContextModelSnapshot.cs | 31 +- .../Models/DbModels/ImageDbModel.cs | 24 + KfChatDotNetBot/Program.cs | 16 + 9 files changed, 1504 insertions(+), 76 deletions(-) create mode 100644 KfChatDotNetBot/Migrations/20260510172529_ImageTagList.Designer.cs create mode 100644 KfChatDotNetBot/Migrations/20260510172529_ImageTagList.cs create mode 100644 KfChatDotNetBot/Migrations/20260510194844_ImageMetadata.Designer.cs create mode 100644 KfChatDotNetBot/Migrations/20260510194844_ImageMetadata.cs diff --git a/KfChatDotNetBot/ApplicationDbContext.cs b/KfChatDotNetBot/ApplicationDbContext.cs index 6b8e6ff..efb4f40 100644 --- a/KfChatDotNetBot/ApplicationDbContext.cs +++ b/KfChatDotNetBot/ApplicationDbContext.cs @@ -14,6 +14,8 @@ public class ApplicationDbContext : DbContext { //modelBuilder.Entity() // .OwnsOne(p => p.StateData, b => b.ToJson()); + modelBuilder.Entity(). + OwnsOne(p => p.Metadata, b => b.ToJson()); } public DbSet Users { get; set; } diff --git a/KfChatDotNetBot/Commands/ImageCommands.cs b/KfChatDotNetBot/Commands/ImageCommands.cs index bd48a6b..4a6302b 100644 --- a/KfChatDotNetBot/Commands/ImageCommands.cs +++ b/KfChatDotNetBot/Commands/ImageCommands.cs @@ -7,21 +7,18 @@ using KfChatDotNetBot.Models; using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Services; using KfChatDotNetBot.Settings; -using KfChatDotNetWsClient.Models.Events; using Microsoft.EntityFrameworkCore; -using NLog; namespace KfChatDotNetBot.Commands; public class AddImageCommand : ICommand { public List Patterns => [ - new Regex(@"^admin image (?\w+) add (?\S+)(?:\s+(?raw))?(?:\s+(?.+))?$"), - new Regex(@"^admin images (?\w+) add (?\S+)(?:\s+(?raw))?(?:\s+(?.+))?$"), - new Regex(@"^admin image (?\w+) add_nigger (?\S+)(?:\s+(?raw))?(?:\s+(?.+))?$"), - new Regex(@"^admin images (?\w+) add_nigger (?\S+)(?:\s+(?raw))?(?:\s+(?.+))?$") + new Regex(@"^admin (image|images) (?\w+) (add|add_nigger) (?\S+) (?raw) (?.+)$", RegexOptions.IgnoreCase), + new Regex(@"^admin (image|images) (?\w+) (add|add_nigger) (?\S+) (?.+)$", RegexOptions.IgnoreCase), + new Regex(@"^admin (image|images) (?\w+) (add|add_nigger) (?\S+)$", RegexOptions.IgnoreCase) ]; - 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 TimeSpan Timeout => TimeSpan.FromSeconds(10); public RateLimitOptionsModel? RateLimitOptions => null; @@ -34,13 +31,14 @@ public class AddImageCommand : ICommand if (imageKeys == null) throw new InvalidOperationException($"{BuiltIn.Keys.BotImageAcceptableKeys} was null"); var key = arguments["key"].Value; var url = arguments["url"].Value; - var tags = arguments["tags"].Success ? arguments["tags"].Value.Trim() : null; + var tags = arguments.TryGetValue("tags", out var tagsArg) ? tagsArg.Value.ToLower().Split(" ").ToList() : []; var niggerMode = message.Message.Contains("add_nigger"); - var _rawMode = arguments["raw"].Success; + // TODO: Implement real and raw mode + //var _rawMode = arguments.ContainsKey("raw"); if (!imageKeys.Contains(key)) { 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; } @@ -49,32 +47,34 @@ public class AddImageCommand : ICommand await botInstance.SendChatMessageAsync("This image already exists in the database with this key", true); return; } - - if (!Regex.IsMatch(url, @"^https?://\S+$")) + + if (!Uri.TryCreate(url, UriKind.Absolute, out _)) { await botInstance.SendWhisperAsync(user.KfId, $"The URL '{url}' you provided is not valid"); return; } - string result = url; + 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, Tags = tags }, ctx); + 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.CountAsync(cancellationToken: ctx); await db.SaveChangesAsync(ctx); 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 RemoveImageCommand : ICommand { public List Patterns => [ - new Regex(@"^admin image (?\w+) remove (?.+)$"), - new Regex(@"^admin images (?\w+) remove (?.+)$"), - new Regex(@"^admin image (?\w+) delete (?.+)$"), - new Regex(@"^admin images (?\w+) delete (?.+)$") + new Regex(@"^admin (image|images) (?\w+) (remove|delete) (?.+)$"), ]; - 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 TimeSpan Timeout => TimeSpan.FromSeconds(10); public RateLimitOptionsModel? RateLimitOptions => null; @@ -103,22 +103,25 @@ public class RemoveImageCommand : ICommand db.Images.Remove(image); await db.SaveChangesAsync(ctx); - // await botInstance.SendChatMessageAsync("Removed image from database", true); 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 List Patterns => [ - new Regex(@"^admin image (?\w+) list$"), - new Regex(@"^admin images (?\w+) list$") + new Regex(@"^admin (image|images) (?\w+) list$"), ]; - public string? HelpText => "Remove an image from the image rotation specified"; - public UserRight RequiredRight => UserRight.TrueAndHonest; + public string HelpText => "List images for a given carousel"; + public UserRight RequiredRight => UserRight.Guest; 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 async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) @@ -131,19 +134,19 @@ public class ListImageCommand : ICommand if (!imageKeys.Contains(key)) { 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; } 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; foreach (var image in images) { 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} - {time} - {image.Tags}" + Environment.NewLine; + content += $"{image.Url} - {time} - {image.TagList.Humanize(" ")}" + Environment.NewLine; } var paste = await Zipline.Upload(content, new MediaTypeHeaderValue("text/plain"), "1d", ctx); @@ -168,51 +171,51 @@ public class ListImageCommand : ICommand public class ManageImageKeyCommand : ICommand { public List Patterns => [ - new Regex(@"^admin imagekey add (?\w+)$"), - new Regex(@"^admin imagekey remove (?\w+)$"), - new Regex(@"^admin imagekey delete (?\w+)$"), - new Regex(@"^admin imageskey add (?\w+)$"), - new Regex(@"^admin imageskey remove (?\w+)$"), - new Regex(@"^admin imageskey delete (?\w+)$") + new Regex(@"^admin (imagekey|imageskey) (?add|remove|delete) (?\w+)$"), ]; - public string? HelpText => "Add or remove an acceptable image key from the BotImageAcceptableKeys setting"; + 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 => false; + 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>(); if (imageKeys == null) throw new InvalidOperationException($"{BuiltIn.Keys.BotImageAcceptableKeys} was null"); var key = arguments["key"].Value.ToLower(); - var isAdd = message.Message.Contains(" add "); + var operation = arguments["operation"].Value.ToLower(); - if (isAdd) + if (operation is "add") { if (imageKeys.Contains(key)) { - await botInstance.SendChatMessageAsync($"Key \"{key}\" is already in the acceptable keys list", true); + 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.SendChatMessageAsync( - $"Added key \"{key}\" to acceptable image keys. Current keys: {string.Join(' ', imageKeys)}", true); + await botInstance.ReplyToUser(message, + $"Added key \"{key}\" to acceptable image keys. Current keys: {imageKeys.Humanize()}", true); + return; } - else + + if (operation is "remove" or "delete") { if (!imageKeys.Contains(key)) { - await botInstance.SendChatMessageAsync( - $"Key \"{key}\" is not in the acceptable keys list. Current keys: {string.Join(' ', imageKeys)}", true); + 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.SendChatMessageAsync( - $"Removed key \"{key}\" from acceptable image keys. Current keys: {string.Join(' ', imageKeys)}", true); + 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); } } @@ -220,12 +223,13 @@ public class ManageImageKeyCommand : ICommand public class GetRandomImage : ICommand { public List Patterns => [ - new Regex(@"^(?\w+)(?:\s+(?.+))?") + new Regex(@"^(?\w+)$"), + new Regex(@"^(?\w+) (?.+)") ]; - public string? HelpText => "Get a random image"; + public string HelpText => "Get a random image"; public UserRight RequiredRight => UserRight.Loser; public TimeSpan Timeout => TimeSpan.FromMinutes(10); - public RateLimitOptionsModel? RateLimitOptions => new() + public RateLimitOptionsModel RateLimitOptions => new() { Window = TimeSpan.FromSeconds(30), MaxInvocations = 7, @@ -237,7 +241,7 @@ public class GetRandomImage : ICommand { await using var db = new ApplicationDbContext(); var key = arguments["key"].Value.ToLower(); - var searchTerm = arguments["search"].Success ? arguments["search"].Value.Trim() : null; + var searchTerm = arguments.TryGetValue("search", out var searchArg) ? searchArg.Value.ToLower().Trim() : null; var images = db.Images.Where(i => i.Key == key); if (!await images.AnyAsync(ctx)) { @@ -257,40 +261,31 @@ public class GetRandomImage : ICommand BuiltIn.Keys.BotImageChinkSelfDestruct, BuiltIn.Keys.BotImageChinkSelfDestructDelay ]); - ImageDbModel image; + var selection = await images.ToListAsync(ctx); if (!string.IsNullOrEmpty(searchTerm)) { - var allImages = await images.ToListAsync(ctx); var searchTokens = searchTerm.ToLower().Split(' ', StringSplitOptions.RemoveEmptyEntries); - var matches = allImages.Where(i => - { - if (i.Tags == null) return false; - var tagTokens = i.Tags.ToLower().Split(' ', StringSplitOptions.RemoveEmptyEntries); - return searchTokens.All(st => tagTokens.Contains(st)); - }).ToList(); - if (matches.Count == 0) + 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; } - image = matches.OrderBy(i => i.LastSeen).First(); } - else + var divideBy = settings[BuiltIn.Keys.BotImageRandomSliceDivideBy].ToType(); + var limit = 1; + var count = await images.CountAsync(ctx); + if (count > divideBy) { - var divideBy = settings[BuiltIn.Keys.BotImageRandomSliceDivideBy].ToType(); - var limit = 1; - var count = await images.CountAsync(ctx); - if (count > divideBy) - { - limit = count / divideBy; - } - - // 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(); - // MaxValue is never returned by Next so you don't need to -1 for indexing - image = selection[new Random().Next(0, selection.Count)]; + limit = count / divideBy; } + + // EF with SQLite can't sort on dates as it's just TEXT + selection = selection.OrderBy(i => i.LastSeen).Take(limit).ToList(); + // MaxValue is never returned by Next so you don't need to -1 for indexing + var image = selection[new Random().Next(0, selection.Count)]; image.LastSeen = DateTimeOffset.UtcNow; db.Images.Update(image); await db.SaveChangesAsync(ctx); diff --git a/KfChatDotNetBot/Migrations/20260510172529_ImageTagList.Designer.cs b/KfChatDotNetBot/Migrations/20260510172529_ImageTagList.Designer.cs new file mode 100644 index 0000000..8e80f2c --- /dev/null +++ b/KfChatDotNetBot/Migrations/20260510172529_ImageTagList.Designer.cs @@ -0,0 +1,640 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("BetId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CurrencyPrice") + .HasColumnType("REAL"); + + b.Property("GameTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Multiplier") + .HasColumnType("REAL"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Win") + .HasColumnType("INTEGER"); + + b.Property("Winnings") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("ChipsggBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Balance") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("NextVipLevelWagerRequirement") + .HasColumnType("TEXT"); + + b.Property("RandomSeed") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("TotalWagered") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Gamblers"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerExclusionDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("GamblerId") + .HasColumnType("INTEGER"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GamblerId"); + + b.ToTable("Exclusions"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerPerkDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GamblerId") + .HasColumnType("INTEGER"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Payout") + .HasColumnType("TEXT"); + + b.Property("PerkName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PerkTier") + .HasColumnType("INTEGER"); + + b.Property("PerkType") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GamblerId"); + + b.ToTable("Perks"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.HowlggBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bet") + .HasColumnType("INTEGER"); + + b.Property("BetId") + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Game") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("Profit") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("HowlggBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ImageDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastSeen") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("TagList") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("JuicedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Juicers"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.MomDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Moms"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.RainbetBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BetId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BetSeenAt") + .HasColumnType("TEXT"); + + b.Property("GameName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Multiplier") + .HasColumnType("REAL"); + + b.Property("Payout") + .HasColumnType("REAL"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("RainbetUserId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("RainbetBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.SettingDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CacheDuration") + .HasColumnType("REAL"); + + b.Property("Default") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSecret") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Regex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.Property("ValueType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.StreamDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoCapture") + .HasColumnType("INTEGER"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Service") + .HasColumnType("INTEGER"); + + b.Property("StreamUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Streams"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TransactionDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Effect") + .HasColumnType("TEXT"); + + b.Property("EventSource") + .HasColumnType("INTEGER"); + + b.Property("FromId") + .HasColumnType("INTEGER"); + + b.Property("GamblerId") + .HasColumnType("INTEGER"); + + b.Property("NewBalance") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("TimeUnixEpochSeconds") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FromId"); + + b.HasIndex("GamblerId"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TwitchViewCountDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServerTime") + .HasColumnType("REAL"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Viewers") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TwitchViewCounts"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ignored") + .HasColumnType("INTEGER"); + + b.Property("KfId") + .HasColumnType("INTEGER"); + + b.Property("KfUsername") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserRight") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserWhoWasDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivityType") + .HasColumnType("INTEGER"); + + b.Property("FirstOccurence") + .HasColumnType("TEXT"); + + b.Property("LatestOccurence") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UsersWhoWere"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.WagerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GamblerId") + .HasColumnType("INTEGER"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("GameMeta") + .HasColumnType("TEXT"); + + b.Property("IsComplete") + .HasColumnType("INTEGER"); + + b.Property("Multiplier") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("TimeUnixEpochSeconds") + .HasColumnType("INTEGER"); + + b.Property("WagerAmount") + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/KfChatDotNetBot/Migrations/20260510172529_ImageTagList.cs b/KfChatDotNetBot/Migrations/20260510172529_ImageTagList.cs new file mode 100644 index 0000000..19c0d42 --- /dev/null +++ b/KfChatDotNetBot/Migrations/20260510172529_ImageTagList.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace KfChatDotNetBot.Migrations +{ + /// + public partial class ImageTagList : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TagList", + table: "Images", + type: "TEXT", + nullable: false, + defaultValue: "[]"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TagList", + table: "Images"); + } + } +} diff --git a/KfChatDotNetBot/Migrations/20260510194844_ImageMetadata.Designer.cs b/KfChatDotNetBot/Migrations/20260510194844_ImageMetadata.Designer.cs new file mode 100644 index 0000000..fdaaca5 --- /dev/null +++ b/KfChatDotNetBot/Migrations/20260510194844_ImageMetadata.Designer.cs @@ -0,0 +1,665 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("BetId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CurrencyPrice") + .HasColumnType("REAL"); + + b.Property("GameTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Multiplier") + .HasColumnType("REAL"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Win") + .HasColumnType("INTEGER"); + + b.Property("Winnings") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("ChipsggBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Balance") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("NextVipLevelWagerRequirement") + .HasColumnType("TEXT"); + + b.Property("RandomSeed") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("TotalWagered") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Gamblers"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerExclusionDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("GamblerId") + .HasColumnType("INTEGER"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GamblerId"); + + b.ToTable("Exclusions"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.GamblerPerkDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GamblerId") + .HasColumnType("INTEGER"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Payout") + .HasColumnType("TEXT"); + + b.Property("PerkName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PerkTier") + .HasColumnType("INTEGER"); + + b.Property("PerkType") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GamblerId"); + + b.ToTable("Perks"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.HowlggBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bet") + .HasColumnType("INTEGER"); + + b.Property("BetId") + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Game") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("Profit") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("HowlggBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ImageDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastSeen") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("TagList") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("JuicedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Juicers"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.MomDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Moms"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.RainbetBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BetId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BetSeenAt") + .HasColumnType("TEXT"); + + b.Property("GameName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Multiplier") + .HasColumnType("REAL"); + + b.Property("Payout") + .HasColumnType("REAL"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("RainbetUserId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("RainbetBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.SettingDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CacheDuration") + .HasColumnType("REAL"); + + b.Property("Default") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSecret") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Regex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.Property("ValueType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.StreamDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoCapture") + .HasColumnType("INTEGER"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Service") + .HasColumnType("INTEGER"); + + b.Property("StreamUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Streams"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TransactionDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Effect") + .HasColumnType("TEXT"); + + b.Property("EventSource") + .HasColumnType("INTEGER"); + + b.Property("FromId") + .HasColumnType("INTEGER"); + + b.Property("GamblerId") + .HasColumnType("INTEGER"); + + b.Property("NewBalance") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("TimeUnixEpochSeconds") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FromId"); + + b.HasIndex("GamblerId"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TwitchViewCountDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServerTime") + .HasColumnType("REAL"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Viewers") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TwitchViewCounts"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ignored") + .HasColumnType("INTEGER"); + + b.Property("KfId") + .HasColumnType("INTEGER"); + + b.Property("KfUsername") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserRight") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserWhoWasDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivityType") + .HasColumnType("INTEGER"); + + b.Property("FirstOccurence") + .HasColumnType("TEXT"); + + b.Property("LatestOccurence") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UsersWhoWere"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.WagerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GamblerId") + .HasColumnType("INTEGER"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("GameMeta") + .HasColumnType("TEXT"); + + b.Property("IsComplete") + .HasColumnType("INTEGER"); + + b.Property("Multiplier") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("TimeUnixEpochSeconds") + .HasColumnType("INTEGER"); + + b.Property("WagerAmount") + .HasColumnType("TEXT"); + + b.Property("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("ImageDbModelId"); + + b1.Property("AddedByUserId"); + + b1.Property("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 + } + } +} diff --git a/KfChatDotNetBot/Migrations/20260510194844_ImageMetadata.cs b/KfChatDotNetBot/Migrations/20260510194844_ImageMetadata.cs new file mode 100644 index 0000000..d12d458 --- /dev/null +++ b/KfChatDotNetBot/Migrations/20260510194844_ImageMetadata.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace KfChatDotNetBot.Migrations +{ + /// + public partial class ImageMetadata : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Metadata", + table: "Images", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Metadata", + table: "Images"); + } + } +} diff --git a/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs b/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs index f70983a..c1ac434 100644 --- a/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace KfChatDotNetBot.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ChipsggBetDbModel", b => { @@ -212,6 +212,10 @@ namespace KfChatDotNetBot.Migrations b.Property("LastSeen") .HasColumnType("TEXT"); + b.PrimitiveCollection("TagList") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("Tags") .HasColumnType("TEXT"); @@ -558,6 +562,31 @@ namespace KfChatDotNetBot.Migrations b.Navigation("Gambler"); }); + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ImageDbModel", b => + { + b.OwnsOne("KfChatDotNetBot.Models.DbModels.ImageMetadataModel", "Metadata", b1 => + { + b1.Property("ImageDbModelId"); + + b1.Property("AddedByUserId"); + + b1.Property("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") diff --git a/KfChatDotNetBot/Models/DbModels/ImageDbModel.cs b/KfChatDotNetBot/Models/DbModels/ImageDbModel.cs index 8a330f7..6f3b41d 100644 --- a/KfChatDotNetBot/Models/DbModels/ImageDbModel.cs +++ b/KfChatDotNetBot/Models/DbModels/ImageDbModel.cs @@ -6,5 +6,29 @@ public class ImageDbModel public required string Key { get; set; } public required string Url { get; set; } public required DateTimeOffset LastSeen { get; set; } + [Obsolete("Use TagList instead")] public string? Tags { get; set; } + /// + /// List of image tags for recalling specific images + /// + public required List TagList { get; set; } = []; + + /// + /// 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 + /// + public required ImageMetadataModel? Metadata { get; set; } +} + +public class ImageMetadataModel +{ + /// + /// User ID (IN THE BOT, NOT KIWI FARMS USER ID) of whoever added this image + /// + public required int AddedByUserId { get; set; } + + /// + /// When the image was added to the database + /// + public required DateTimeOffset WhenAdded { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetBot/Program.cs b/KfChatDotNetBot/Program.cs index 0f8dde9..75b436f 100644 --- a/KfChatDotNetBot/Program.cs +++ b/KfChatDotNetBot/Program.cs @@ -20,6 +20,7 @@ */ using System.Text; +using KfChatDotNetBot.Migrations; using KfChatDotNetBot.Services; using KfChatDotNetBot.Settings; using Microsoft.EntityFrameworkCore; @@ -49,6 +50,21 @@ namespace KfChatDotNetBot 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"); Console.OutputEncoding = Encoding.UTF8; _ = new ChatBot();