mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-12 09:09:35 -04:00
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
This commit is contained in:
@@ -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<Regex> Patterns => [
|
||||
new Regex(@"^admin image (?<key>\w+) add (?<url>\S+)(?:\s+(?<raw>raw))?(?:\s+(?<tags>.+))?$"),
|
||||
new Regex(@"^admin images (?<key>\w+) add (?<url>\S+)(?:\s+(?<raw>raw))?(?:\s+(?<tags>.+))?$"),
|
||||
new Regex(@"^admin image (?<key>\w+) add_nigger (?<url>\S+)(?:\s+(?<raw>raw))?(?:\s+(?<tags>.+))?$"),
|
||||
new Regex(@"^admin images (?<key>\w+) add_nigger (?<url>\S+)(?:\s+(?<raw>raw))?(?:\s+(?<tags>.+))?$")
|
||||
new Regex(@"^admin (image|images) (?<key>\w+) (add|add_nigger) (?<url>\S+) (?<raw>raw) (?<tags>.+)$", RegexOptions.IgnoreCase),
|
||||
new Regex(@"^admin (image|images) (?<key>\w+) (add|add_nigger) (?<url>\S+) (?<tags>.+)$", RegexOptions.IgnoreCase),
|
||||
new Regex(@"^admin (image|images) (?<key>\w+) (add|add_nigger) (?<url>\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<Regex> Patterns => [
|
||||
new Regex(@"^admin image (?<key>\w+) remove (?<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>.+)$")
|
||||
new Regex(@"^admin (image|images) (?<key>\w+) (remove|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 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<Regex> Patterns => [
|
||||
new Regex(@"^admin image (?<key>\w+) list$"),
|
||||
new Regex(@"^admin images (?<key>\w+) list$")
|
||||
new Regex(@"^admin (image|images) (?<key>\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<Regex> Patterns => [
|
||||
new Regex(@"^admin imagekey add (?<key>\w+)$"),
|
||||
new Regex(@"^admin imagekey remove (?<key>\w+)$"),
|
||||
new Regex(@"^admin imagekey delete (?<key>\w+)$"),
|
||||
new Regex(@"^admin imageskey add (?<key>\w+)$"),
|
||||
new Regex(@"^admin imageskey remove (?<key>\w+)$"),
|
||||
new Regex(@"^admin imageskey delete (?<key>\w+)$")
|
||||
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 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<List<string>>();
|
||||
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<Regex> Patterns => [
|
||||
new Regex(@"^(?<key>\w+)(?:\s+(?<search>.+))?")
|
||||
new Regex(@"^(?<key>\w+)$"),
|
||||
new Regex(@"^(?<key>\w+) (?<search>.+)")
|
||||
];
|
||||
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<int>();
|
||||
var limit = 1;
|
||||
var count = await images.CountAsync(ctx);
|
||||
if (count > divideBy)
|
||||
{
|
||||
var divideBy = settings[BuiltIn.Keys.BotImageRandomSliceDivideBy].ToType<int>();
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user