diff --git a/src/NadekoBot/Db/Extensions/GuildConfigExtensions.cs b/src/NadekoBot/Db/Extensions/GuildConfigExtensions.cs index 1433f1185..d8979e032 100644 --- a/src/NadekoBot/Db/Extensions/GuildConfigExtensions.cs +++ b/src/NadekoBot/Db/Extensions/GuildConfigExtensions.cs @@ -54,7 +54,6 @@ namespace NadekoBot.Db .Include(gc => gc.CommandCooldowns) .Include(gc => gc.FollowedStreams) .Include(gc => gc.StreamRole) - .Include(gc => gc.NsfwBlacklistedTags) .Include(gc => gc.XpSettings) .ThenInclude(x => x.ExclusionList) .Include(gc => gc.DelMsgOnCmdChannels) diff --git a/src/NadekoBot/Db/Models/GuildConfig.cs b/src/NadekoBot/Db/Models/GuildConfig.cs index 4d29666c8..5b0c38cba 100644 --- a/src/NadekoBot/Db/Models/GuildConfig.cs +++ b/src/NadekoBot/Db/Models/GuildConfig.cs @@ -91,7 +91,6 @@ namespace NadekoBot.Services.Database.Models public bool WarningsInitialized { get; set; } public HashSet SlowmodeIgnoredUsers { get; set; } public HashSet SlowmodeIgnoredRoles { get; set; } - public HashSet NsfwBlacklistedTags { get; set; } = new HashSet(); public List ShopEntries { get; set; } public ulong? GameVoiceChannel { get; set; } = null; diff --git a/src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs b/src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs new file mode 100644 index 000000000..04582cb48 --- /dev/null +++ b/src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs @@ -0,0 +1,16 @@ +using System; + +namespace NadekoBot.Services.Database.Models +{ + public class NsfwBlacklistedTag : DbEntity + { + public ulong GuildId { get; set; } + public string Tag { get; set; } + + public override int GetHashCode() + => Tag.GetHashCode(StringComparison.InvariantCulture); + + public override bool Equals(object obj) + => obj is NsfwBlacklistedTag x && x.Tag == Tag; + } +} diff --git a/src/NadekoBot/Db/Models/NsfwBlacklitedTag.cs b/src/NadekoBot/Db/Models/NsfwBlacklitedTag.cs deleted file mode 100644 index 55ec2dc60..000000000 --- a/src/NadekoBot/Db/Models/NsfwBlacklitedTag.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace NadekoBot.Services.Database.Models -{ - public class NsfwBlacklitedTag : DbEntity - { - public string Tag { get; set; } - - public override int GetHashCode() - { - return Tag.GetHashCode(StringComparison.InvariantCulture); - } - - public override bool Equals(object obj) - { - return obj is NsfwBlacklitedTag x - ? x.Tag == Tag - : false; - } - } -} diff --git a/src/NadekoBot/Db/NadekoContext.cs b/src/NadekoBot/Db/NadekoContext.cs index bb223b9df..4fe33c0ea 100644 --- a/src/NadekoBot/Db/NadekoContext.cs +++ b/src/NadekoBot/Db/NadekoContext.cs @@ -59,6 +59,7 @@ namespace NadekoBot.Services.Database public DbSet Poll { get; set; } public DbSet WaifuInfo { get; set; } public DbSet ImageOnlyChannels { get; set; } + public DbSet NsfwBlacklistedTags { get; set; } public NadekoContext(DbContextOptions options) : base(options) { @@ -357,6 +358,10 @@ namespace NadekoBot.Services.Database modelBuilder.Entity(ioc => ioc .HasIndex(x => x.ChannelId) .IsUnique()); + + modelBuilder.Entity(nbt => nbt + .HasIndex(x => x.GuildId) + .IsUnique(false)); } } } diff --git a/src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.Designer.cs b/src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.Designer.cs new file mode 100644 index 000000000..1cb261842 --- /dev/null +++ b/src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.Designer.cs @@ -0,0 +1,2648 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NadekoBot.Services.Database; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20211015232708_nsfw-blacklist-tags")] + partial class nsfwblacklisttags + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.8"); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b => + { + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubApplicants"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubBans", b => + { + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubBans"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Discrim") + .HasColumnType("INTEGER"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("MinimumLevelReq") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("Xp") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name", "Discrim"); + + b.HasIndex("OwnerId") + .IsUnique(); + + b.ToTable("Clubs"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvatarId") + .HasColumnType("TEXT"); + + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("CurrencyAmount") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .HasColumnType("TEXT"); + + b.Property("IsClubAdmin") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now')"); + + b.Property("LastXpGain") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', '-1 years')"); + + b.Property("NotifyOnLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("TotalXp") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.HasIndex("ClubId"); + + b.HasIndex("CurrencyAmount"); + + b.HasIndex("TotalXp"); + + b.HasIndex("UserId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("ActionDurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("MinAge") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiAltSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("PunishDuration") + .HasColumnType("INTEGER"); + + b.Property("Seconds") + .HasColumnType("INTEGER"); + + b.Property("UserThreshold") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AntiSpamSettingId") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("MessageThreshold") + .HasColumnType("INTEGER"); + + b.Property("MuteTime") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ChannelName") + .HasColumnType("TEXT"); + + b.Property("CommandText") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("GuildName") + .HasColumnType("TEXT"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("VoiceChannelId") + .HasColumnType("INTEGER"); + + b.Property("VoiceChannelName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AutoCommands"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Text") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("BanTemplates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Blacklist"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Mapping") + .HasColumnType("TEXT"); + + b.Property("Trigger") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommandName") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Seconds") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowTarget") + .HasColumnType("INTEGER"); + + b.Property("AutoDeleteTrigger") + .HasColumnType("INTEGER"); + + b.Property("ContainsAnywhere") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DmResponse") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Reactions") + .HasColumnType("TEXT"); + + b.Property("Response") + .HasColumnType("TEXT"); + + b.Property("Trigger") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DelMsgOnCmdChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("DelMsgOnCmdChannel"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordPermOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Command") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Perm") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "Command") + .IsUnique(); + + b.ToTable("DiscordPermOverrides"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("ItemType") + .HasColumnType("INTEGER"); + + b.Property("XpSettingsId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("ExcludedItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasAlternateKey("GuildConfigId", "Url"); + + b.ToTable("FeedSub"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("GuildConfigId1") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterLinksChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilterLinksChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Word") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GroupName", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId", "Number") + .IsUnique(); + + b.ToTable("GroupName"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoAssignRoleIds") + .HasColumnType("TEXT"); + + b.Property("AutoDeleteByeMessages") + .HasColumnType("INTEGER"); + + b.Property("AutoDeleteByeMessagesTimer") + .HasColumnType("INTEGER"); + + b.Property("AutoDeleteGreetMessages") + .HasColumnType("INTEGER"); + + b.Property("AutoDeleteGreetMessagesTimer") + .HasColumnType("INTEGER"); + + b.Property("AutoDeleteSelfAssignedRoleMessages") + .HasColumnType("INTEGER"); + + b.Property("BoostMessage") + .HasColumnType("TEXT"); + + b.Property("BoostMessageChannelId") + .HasColumnType("INTEGER"); + + b.Property("BoostMessageDeleteAfter") + .HasColumnType("INTEGER"); + + b.Property("ByeMessageChannelId") + .HasColumnType("INTEGER"); + + b.Property("ChannelByeMessageText") + .HasColumnType("TEXT"); + + b.Property("ChannelGreetMessageText") + .HasColumnType("TEXT"); + + b.Property("CleverbotEnabled") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DeleteMessageOnCommand") + .HasColumnType("INTEGER"); + + b.Property("DmGreetMessageText") + .HasColumnType("TEXT"); + + b.Property("ExclusiveSelfAssignedRoles") + .HasColumnType("INTEGER"); + + b.Property("FilterInvites") + .HasColumnType("INTEGER"); + + b.Property("FilterLinks") + .HasColumnType("INTEGER"); + + b.Property("FilterWords") + .HasColumnType("INTEGER"); + + b.Property("GameVoiceChannel") + .HasColumnType("INTEGER"); + + b.Property("GreetMessageChannelId") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .HasColumnType("TEXT"); + + b.Property("MuteRoleName") + .HasColumnType("TEXT"); + + b.Property("NotifyStreamOffline") + .HasColumnType("INTEGER"); + + b.Property("PermissionRole") + .HasColumnType("TEXT"); + + b.Property("Prefix") + .HasColumnType("TEXT"); + + b.Property("SendBoostMessage") + .HasColumnType("INTEGER"); + + b.Property("SendChannelByeMessage") + .HasColumnType("INTEGER"); + + b.Property("SendChannelGreetMessage") + .HasColumnType("INTEGER"); + + b.Property("SendDmGreetMessage") + .HasColumnType("INTEGER"); + + b.Property("TimeZoneId") + .HasColumnType("TEXT"); + + b.Property("VerboseErrors") + .HasColumnType("INTEGER"); + + b.Property("VerbosePermissions") + .HasColumnType("INTEGER"); + + b.Property("WarnExpireAction") + .HasColumnType("INTEGER"); + + b.Property("WarnExpireHours") + .HasColumnType("INTEGER"); + + b.Property("WarningsInitialized") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("WarnExpireHours"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ItemType") + .HasColumnType("INTEGER"); + + b.Property("LogItemId") + .HasColumnType("INTEGER"); + + b.Property("LogSettingId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId", "LogItemId", "ItemType") + .IsUnique(); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("LogSettingId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ImageOnlyChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId") + .IsUnique(); + + b.ToTable("ImageOnlyChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelCreatedId") + .HasColumnType("INTEGER"); + + b.Property("ChannelDestroyedId") + .HasColumnType("INTEGER"); + + b.Property("ChannelUpdatedId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("LogOtherId") + .HasColumnType("INTEGER"); + + b.Property("LogUserPresenceId") + .HasColumnType("INTEGER"); + + b.Property("LogVoicePresenceId") + .HasColumnType("INTEGER"); + + b.Property("LogVoicePresenceTTSId") + .HasColumnType("INTEGER"); + + b.Property("MessageDeletedId") + .HasColumnType("INTEGER"); + + b.Property("MessageUpdatedId") + .HasColumnType("INTEGER"); + + b.Property("UserBannedId") + .HasColumnType("INTEGER"); + + b.Property("UserJoinedId") + .HasColumnType("INTEGER"); + + b.Property("UserLeftId") + .HasColumnType("INTEGER"); + + b.Property("UserMutedId") + .HasColumnType("INTEGER"); + + b.Property("UserUnbannedId") + .HasColumnType("INTEGER"); + + b.Property("UserUpdatedId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlayerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoDisconnect") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("MusicChannelId") + .HasColumnType("INTEGER"); + + b.Property("PlayerRepeat") + .HasColumnType("INTEGER"); + + b.Property("QualityPreset") + .HasColumnType("INTEGER"); + + b.Property("Volume") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(100); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("MusicPlayerSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("NsfwBlacklistedTags"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("IsCustomCommand") + .HasColumnType("INTEGER"); + + b.Property("PrimaryTarget") + .HasColumnType("INTEGER"); + + b.Property("PrimaryTargetId") + .HasColumnType("INTEGER"); + + b.Property("SecondaryTarget") + .HasColumnType("INTEGER"); + + b.Property("SecondaryTargetName") + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlantedCurrency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("MessageId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("MessageId") + .IsUnique(); + + b.ToTable("PlantedCurrency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("MusicPlaylistId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("TEXT"); + + b.Property("ProviderType") + .HasColumnType("INTEGER"); + + b.Property("Query") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Poll", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Question") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("Poll"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("PollId") + .HasColumnType("INTEGER"); + + b.Property("Text") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PollId"); + + b.ToTable("PollAnswer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollVote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("PollId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("VoteIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PollId"); + + b.ToTable("PollVote"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Keyword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("Keyword"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("EmoteName") + .HasColumnType("TEXT"); + + b.Property("ReactionRoleMessageId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ReactionRoleMessageId"); + + b.ToTable("ReactionRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Exclusive") + .HasColumnType("INTEGER"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MessageId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ReactionRoleMessage"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IsPrivate") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("When"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Repeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("TEXT"); + + b.Property("LastMessageId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("NoRedundant") + .HasColumnType("INTEGER"); + + b.Property("StartTimeOfDay") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Repeaters"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AmountRewardedThisMonth") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("LastReward") + .HasColumnType("TEXT"); + + b.Property("PatreonUserId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PatreonUserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RotatingPlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RotatingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Group") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("LevelRequirement") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("RoleName") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ShopEntryId") + .HasColumnType("INTEGER"); + + b.Property("Text") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("StreamRoleSettingsId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddRoleId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("FromRoleId") + .HasColumnType("INTEGER"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Keyword") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("StreamRoleSettingsId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnbanTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("UnbanAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnbanTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("UnmuteAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnroleTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("UnbanAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnroleTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AwardedXp") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Xp") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AwardedXp"); + + b.HasIndex("GuildId"); + + b.HasIndex("UserId"); + + b.HasIndex("Xp"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserXpStats"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("VoiceChannelId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AffinityId") + .HasColumnType("INTEGER"); + + b.Property("ClaimerId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("WaifuId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("Price"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ItemEmoji") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("WaifuInfoId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("NewId") + .HasColumnType("INTEGER"); + + b.Property("OldId") + .HasColumnType("INTEGER"); + + b.Property("UpdateType") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Forgiven") + .HasColumnType("INTEGER"); + + b.Property("ForgivenBy") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Moderator") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DateAdded"); + + b.HasIndex("GuildId"); + + b.HasIndex("UserId"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Punishment") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpCurrencyReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("XpSettingsId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("XpCurrencyReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Remove") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("XpSettingsId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId", "Level") + .IsUnique(); + + b.ToTable("XpRoleReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("ServerExcluded") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Club"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Club"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Db.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Users") + .HasForeignKey("ClubId"); + + b.Navigation("Club"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithOne("AntiAltSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiAltSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting", null) + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DelMsgOnCmdChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("DelMsgOnCmdChannels") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", null) + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("FeedSubs") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterLinksChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterLinksChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GroupName", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("SelfAssignableRoleGroupNames") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("LogIgnores") + .HasForeignKey("LogSettingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LogSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.Navigation("LogSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist", null) + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollAnswer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Poll", null) + .WithMany("Answers") + .HasForeignKey("PollId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollVote", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Poll", null) + .WithMany("Votes") + .HasForeignKey("PollId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ReactionRoleMessage", null) + .WithMany("ReactionRoles") + .HasForeignKey("ReactionRoleMessageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("ReactionRoleMessages") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry", null) + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings", null) + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings", null) + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnbanTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnbanTimer") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnroleTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnroleTimer") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Affinity"); + + b.Navigation("Claimer"); + + b.Navigation("Waifu"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo", null) + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("New"); + + b.Navigation("Old"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpCurrencyReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") + .WithMany("CurrencyRewards") + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.Navigation("Applicants"); + + b.Navigation("Bans"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Navigation("IgnoredChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Navigation("AntiAltSetting"); + + b.Navigation("AntiRaidSetting"); + + b.Navigation("AntiSpamSetting"); + + b.Navigation("CommandAliases"); + + b.Navigation("CommandCooldowns"); + + b.Navigation("DelMsgOnCmdChannels"); + + b.Navigation("FeedSubs"); + + b.Navigation("FilteredWords"); + + b.Navigation("FilterInvitesChannelIds"); + + b.Navigation("FilterLinksChannelIds"); + + b.Navigation("FilterWordsChannelIds"); + + b.Navigation("FollowedStreams"); + + b.Navigation("GenerateCurrencyChannelIds"); + + b.Navigation("MutedUsers"); + + b.Navigation("Permissions"); + + b.Navigation("ReactionRoleMessages"); + + b.Navigation("SelfAssignableRoleGroupNames"); + + b.Navigation("ShopEntries"); + + b.Navigation("SlowmodeIgnoredRoles"); + + b.Navigation("SlowmodeIgnoredUsers"); + + b.Navigation("StreamRole"); + + b.Navigation("UnbanTimer"); + + b.Navigation("UnmuteTimers"); + + b.Navigation("UnroleTimer"); + + b.Navigation("VcRoleInfos"); + + b.Navigation("WarnPunishments"); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Navigation("LogIgnores"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Navigation("Songs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Poll", b => + { + b.Navigation("Answers"); + + b.Navigation("Votes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b => + { + b.Navigation("ReactionRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Navigation("Blacklist"); + + b.Navigation("Whitelist"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Navigation("CurrencyRewards"); + + b.Navigation("ExclusionList"); + + b.Navigation("RoleRewards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.cs b/src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.cs new file mode 100644 index 000000000..1cf2c246e --- /dev/null +++ b/src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.cs @@ -0,0 +1,76 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class nsfwblacklisttags : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + + migrationBuilder.CreateTable( + name: "NsfwBlacklistedTags", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + GuildId = table.Column(type: "INTEGER", nullable: false), + Tag = table.Column(type: "TEXT", nullable: true), + DateAdded = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_NsfwBlacklistedTags", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_NsfwBlacklistedTags_GuildId", + table: "NsfwBlacklistedTags", + column: "GuildId"); + + migrationBuilder.Sql(@"INSERT INTO NsfwBlacklistedTags(Id, GuildId, Tag, DateAdded) +SELECT + Id, + (SELECT GuildId From GuildConfigs WHERE Id=GuildConfigId), + Tag, + DateAdded +FROM NsfwBlacklitedTag +WHERE GuildConfigId in (SELECT Id from GuildConfigs);"); + + migrationBuilder.DropTable( + name: "NsfwBlacklitedTag"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "NsfwBlacklistedTags"); + + migrationBuilder.CreateTable( + name: "NsfwBlacklitedTag", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(type: "TEXT", nullable: true), + GuildConfigId = table.Column(type: "INTEGER", nullable: true), + Tag = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_NsfwBlacklitedTag", x => x.Id); + table.ForeignKey( + name: "FK_NsfwBlacklitedTag_GuildConfigs_GuildConfigId", + column: x => x.GuildConfigId, + principalTable: "GuildConfigs", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_NsfwBlacklitedTag_GuildConfigId", + table: "NsfwBlacklitedTag", + column: "GuildConfigId"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoContextModelSnapshot.cs index b4349bdd0..f6f064e95 100644 --- a/src/NadekoBot/Migrations/NadekoContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoContextModelSnapshot.cs @@ -1062,7 +1062,7 @@ namespace NadekoBot.Migrations b.ToTable("MutedUserId"); }); - modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1071,7 +1071,7 @@ namespace NadekoBot.Migrations b.Property("DateAdded") .HasColumnType("TEXT"); - b.Property("GuildConfigId") + b.Property("GuildId") .HasColumnType("INTEGER"); b.Property("Tag") @@ -1079,9 +1079,9 @@ namespace NadekoBot.Migrations b.HasKey("Id"); - b.HasIndex("GuildConfigId"); + b.HasIndex("GuildId"); - b.ToTable("NsfwBlacklitedTag"); + b.ToTable("NsfwBlacklistedTags"); }); modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => @@ -2300,13 +2300,6 @@ namespace NadekoBot.Migrations .HasForeignKey("GuildConfigId"); }); - modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => - { - b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) - .WithMany("NsfwBlacklistedTags") - .HasForeignKey("GuildConfigId"); - }); - modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => { b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) @@ -2573,8 +2566,6 @@ namespace NadekoBot.Migrations b.Navigation("MutedUsers"); - b.Navigation("NsfwBlacklistedTags"); - b.Navigation("Permissions"); b.Navigation("ReactionRoleMessages"); diff --git a/src/NadekoBot/Modules/Nsfw/Common/Booru.cs b/src/NadekoBot/Modules/Nsfw/Common/Booru.cs new file mode 100644 index 000000000..f172746e0 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Booru.cs @@ -0,0 +1,15 @@ +namespace NadekoBot.Modules.Nsfw.Common +{ + public enum Booru + { + Safebooru, + E621, + Derpibooru, + Rule34, + Gelbooru, + Konachan, + Yandere, + Danbooru, + Sankaku + } +} diff --git a/src/NadekoBot/Modules/Nsfw/Common/DapiImageObject.cs b/src/NadekoBot/Modules/Nsfw/Common/DapiImageObject.cs new file mode 100644 index 000000000..9c3198021 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/DapiImageObject.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class DapiImageObject : IImageData + { + [JsonPropertyName("File_Url")] + public string FileUrl { get; set; } + public string Tags { get; set; } + [JsonPropertyName("Tag_String")] + public string TagString { get; set; } + public int Score { get; set; } + public string Rating { get; set; } + + public ImageData ToCachedImageData(Booru type) + => new ImageData(this.FileUrl, type, this.Tags?.Split(' ') ?? this.TagString?.Split(' '), Score.ToString() ?? Rating); + } +} diff --git a/src/NadekoBot/Modules/Nsfw/Common/DapiTag.cs b/src/NadekoBot/Modules/Nsfw/Common/DapiTag.cs new file mode 100644 index 000000000..464f313c0 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/DapiTag.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public readonly struct DapiTag + { + public string Name { get; } + + [JsonConstructor] + public DapiTag(string name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/DerpiContainer.cs b/src/NadekoBot/Modules/Nsfw/Common/DerpiContainer.cs new file mode 100644 index 000000000..aa1bae6f5 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/DerpiContainer.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class DerpiContainer + { + public DerpiImageObject[] Images { get; set; } + } + + public class DerpiImageObject : IImageData + { + [JsonPropertyName("view_url")] + public string ViewUrl { get; set; } + public string[] Tags { get; set; } + public int Score { get; set; } + public ImageData ToCachedImageData(Booru type) + => new(ViewUrl, type, Tags, Score.ToString("F1")); + } +} diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DanbooruImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DanbooruImageDownloader.cs new file mode 100644 index 000000000..88df6e481 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DanbooruImageDownloader.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public sealed class DanbooruImageDownloader : DapiImageDownloader + { + // using them as concurrent hashsets, value doesn't matter + private static readonly ConcurrentDictionary _existentTags = new(); + private static readonly ConcurrentDictionary _nonexistentTags = new(); + + public override async Task IsTagValid(string tag, CancellationToken cancel = default) + { + if (_existentTags.ContainsKey(tag)) + return true; + + if (_nonexistentTags.ContainsKey(tag)) + return false; + + var tags = await _http.GetFromJsonAsync(_baseUrl + + "/tags.json" + + $"?search[name_or_alias_matches]={tag}", + options: this._serializerOptions, + cancellationToken: cancel); + if (tags is {Length: > 0}) + { + return _existentTags[tag] = true; + } + + return _nonexistentTags[tag] = false; + } + + public DanbooruImageDownloader(HttpClient http) + : base(Booru.Danbooru, http, "http://danbooru.donmai.us") + { + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DapiImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DapiImageDownloader.cs new file mode 100644 index 000000000..bcf6d19e7 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DapiImageDownloader.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public abstract class DapiImageDownloader : ImageDownloader + { + protected readonly string _baseUrl; + + public DapiImageDownloader(Booru booru, HttpClient http, string baseUrl) : base(booru, http) + { + _baseUrl = baseUrl; + } + + public abstract Task IsTagValid(string tag, CancellationToken cancel = default); + protected async Task AllTagsValid(string[] tags, CancellationToken cancel = default) + { + var results = await Task.WhenAll(tags.Select(tag => IsTagValid(tag, cancel))); + + // if any of the tags is not valid, the query is not valid + foreach (var result in results) + { + if (!result) + return false; + } + + return true; + } + + public override async Task> DownloadImagesAsync(string[] tags, int page, + bool isExplicit = false, CancellationToken cancel = default) + { + // up to 2 tags allowed on danbooru + if (tags.Length > 2) + return new(); + + if (!await AllTagsValid(tags, cancel).ConfigureAwait(false)) + return new(); + + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); + + var uri = $"{_baseUrl}/posts.json?limit=200&tags={tagString}&page={page}"; + var imageObjects = await _http.GetFromJsonAsync(uri, _serializerOptions, cancel) + .ConfigureAwait(false); + if (imageObjects is null) + return new(); + return imageObjects + .Where(x => x.FileUrl is not null) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DerpibooruImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DerpibooruImageDownloader.cs new file mode 100644 index 000000000..f0e1569ce --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DerpibooruImageDownloader.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; +using NadekoBot.Extensions; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class DerpibooruImageDownloader : ImageDownloader + { + public DerpibooruImageDownloader(HttpClient http) : base(Booru.Derpibooru, http) + { + } + + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); + var uri = $"https://www.derpibooru.org/api/v1/json/search/images?q={tagString.Replace('+', ',')}&per_page=49&page={page}"; + using var req = new HttpRequestMessage(HttpMethod.Get, uri); + req.Headers.AddFakeHeaders(); + using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false); + res.EnsureSuccessStatusCode(); + + var container = await res.Content.ReadFromJsonAsync(_serializerOptions, cancel).ConfigureAwait(false); + if (container?.Images is null) + return new(); + + return container.Images + .Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl)) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621ImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621ImageDownloader.cs new file mode 100644 index 000000000..f5d91df4b --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621ImageDownloader.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; +using NadekoBot.Extensions; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class E621ImageDownloader : ImageDownloader + { + public E621ImageDownloader(HttpClient http) : base(Booru.E621, http) + { + } + + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: isExplicit); + var uri = $"https://e621.net/posts.json?limit=32&tags={tagString}&page={page}"; + using var req = new HttpRequestMessage(HttpMethod.Get, uri); + req.Headers.AddFakeHeaders(); + using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false); + res.EnsureSuccessStatusCode(); + + var data = await res.Content.ReadFromJsonAsync(_serializerOptions, cancel).ConfigureAwait(false); + if (data?.Posts is null) + return new(); + + return data.Posts + .Where(x => !string.IsNullOrWhiteSpace(x.File?.Url)) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621Response.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621Response.cs new file mode 100644 index 000000000..807e2db97 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621Response.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class E621Response + { + public List Posts { get; set; } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/GelbooruImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/GelbooruImageDownloader.cs new file mode 100644 index 000000000..5a0645f88 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/GelbooruImageDownloader.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class GelbooruImageDownloader : ImageDownloader + { + public GelbooruImageDownloader(HttpClient http) : base(Booru.Gelbooru, http) + { + } + + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); + var uri = $"http://gelbooru.com/index.php?page=dapi&s=post&json=1&q=index&limit=100" + + $"&tags={tagString}&pid={page}"; + using var req = new HttpRequestMessage(HttpMethod.Get, uri); + using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false); + res.EnsureSuccessStatusCode(); + var resString = await res.Content.ReadAsStringAsync(cancel); + if (string.IsNullOrWhiteSpace(resString)) + return new(); + + var images = JsonSerializer.Deserialize>(resString, _serializerOptions); + if (images is null) + return new(); + + return images.Where(x => x.FileUrl is not null).ToList(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/IImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/IImageDownloader.cs new file mode 100644 index 000000000..c05bd35b6 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/IImageDownloader.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public interface IImageDownloader + { + Task> DownloadImageDataAsync(string[] tags, int page = 0, + bool isExplicit = false, CancellationToken cancel = default); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloader.cs new file mode 100644 index 000000000..fa74a68a9 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloader.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public abstract class ImageDownloader : IImageDownloader + where T : IImageData + { + protected readonly HttpClient _http; + + protected JsonSerializerOptions _serializerOptions = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString, + + }; + + public Booru Booru { get; } + + public ImageDownloader(Booru booru, HttpClient http) + { + _http = http; + this.Booru = booru; + } + + public abstract Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default); + + public async Task> DownloadImageDataAsync(string[] tags, int page, bool isExplicit = false, + CancellationToken cancel = default) + { + var images = await DownloadImagesAsync(tags, page, isExplicit, cancel).ConfigureAwait(false); + return images.Select(x => x.ToCachedImageData(Booru)).ToList(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloaderHelper.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloaderHelper.cs new file mode 100644 index 000000000..592b1defd --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloaderHelper.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public static class ImageDownloaderHelper + { + public static string GetTagString(IEnumerable tags, bool isExplicit = false) + { + if (isExplicit) + tags = tags.Append("rating:explicit"); + + return string.Join('+', tags.Select(x => x.ToLowerInvariant())); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/KonachanImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/KonachanImageDownloader.cs new file mode 100644 index 000000000..292b9898f --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/KonachanImageDownloader.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public sealed class KonachanImageDownloader : ImageDownloader + { + private readonly string _baseUrl; + + public KonachanImageDownloader(HttpClient http) + : base(Booru.Konachan, http) + { + _baseUrl = "https://konachan.com"; + } + + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); + var uri = $"{_baseUrl}/post.json?s=post&q=index&limit=200&tags={tagString}&page={page}"; + var imageObjects = await _http.GetFromJsonAsync(uri, _serializerOptions, cancel) + .ConfigureAwait(false); + if (imageObjects is null) + return new(); + return imageObjects + .Where(x => x.FileUrl is not null) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/Rule34ImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/Rule34ImageDownloader.cs new file mode 100644 index 000000000..7e2a5576d --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/Rule34ImageDownloader.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class Rule34ImageDownloader : ImageDownloader + { + public Rule34ImageDownloader(HttpClient http) : base(Booru.Rule34, http) + { + } + + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags); + var uri = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&json=1&limit=100" + + $"&tags={tagString}&pid={page}"; + var images = await _http.GetFromJsonAsync>(uri, _serializerOptions, cancel).ConfigureAwait(false); + + if (images is null) + return new(); + + return images + .Where(img => !string.IsNullOrWhiteSpace(img.Directory) && !string.IsNullOrWhiteSpace(img.Image)) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SafebooruImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SafebooruImageDownloader.cs new file mode 100644 index 000000000..081695428 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SafebooruImageDownloader.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class SafebooruImageDownloader : ImageDownloader + { + public SafebooruImageDownloader(HttpClient http) : base(Booru.Safebooru, http) + { + } + + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: false); + var uri = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=200&tags={tagString}&json=1&pid={page}"; + var images = await _http.GetFromJsonAsync>(uri, _serializerOptions, cancellationToken: cancel); + if (images is null) + return new(); + + return images; + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SankakuImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SankakuImageDownloader.cs new file mode 100644 index 000000000..0221b8934 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SankakuImageDownloader.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using NadekoBot.Extensions; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public sealed class SankakuImageDownloader : ImageDownloader + { + private readonly string _baseUrl; + + public SankakuImageDownloader(HttpClient http) + : base(Booru.Sankaku, http) + { + _baseUrl = "https://capi-v2.sankakucomplex.com"; + _http.AddFakeHeaders(); + } + + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + // explicit probably not supported + var tagString = ImageDownloaderHelper.GetTagString(tags, false); + + var uri = $"{_baseUrl}/posts?tags={tagString}&limit=50"; + var data = await _http.GetStringAsync(uri).ConfigureAwait(false); + return JsonSerializer.Deserialize(data, _serializerOptions) + .Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image")) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/YandereImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/YandereImageDownloader.cs new file mode 100644 index 000000000..5c3bb7aae --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/YandereImageDownloader.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; +using NadekoBot.Extensions; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public sealed class YandereImageDownloader : ImageDownloader + { + private readonly string _baseUrl; + + public YandereImageDownloader(HttpClient http) + : base(Booru.Yandere, http) + { + _baseUrl = "https://yande.re"; + } + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); + + var uri = $"{_baseUrl}/post.json?limit=200&tags={tagString}&page={page}"; + var imageObjects = await _http.GetFromJsonAsync(uri, _serializerOptions, cancel) + .ConfigureAwait(false); + if (imageObjects is null) + return new(); + return imageObjects + .Where(x => x.FileUrl is not null) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/E621Object.cs b/src/NadekoBot/Modules/Nsfw/Common/E621Object.cs new file mode 100644 index 000000000..39919b304 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/E621Object.cs @@ -0,0 +1,27 @@ +namespace NadekoBot.Modules.Nsfw.Common +{ + public class E621Object : IImageData + { + public class FileData + { + public string Url { get; set; } + } + + public class TagData + { + public string[] General { get; set; } + } + + public class ScoreData + { + public int Total { get; set; } + } + + public FileData File { get; set; } + public TagData Tags { get; set; } + public ScoreData Score { get; set; } + + public ImageData ToCachedImageData(Booru type) + => new(File.Url, Booru.E621, Tags.General, Score.Total.ToString()); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/IImageData.cs b/src/NadekoBot/Modules/Nsfw/Common/IImageData.cs new file mode 100644 index 000000000..bb595f038 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/IImageData.cs @@ -0,0 +1,7 @@ +namespace NadekoBot.Modules.Nsfw.Common +{ + public interface IImageData + { + ImageData ToCachedImageData(Booru type); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/ImageData.cs b/src/NadekoBot/Modules/Nsfw/Common/ImageData.cs new file mode 100644 index 000000000..a5781c888 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/ImageData.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class ImageData : IComparable + { + public Booru SearchType { get; } + public string FileUrl { get; } + public HashSet Tags { get; } + public string Rating { get; } + + public ImageData(string url, Booru type, string[] tags, string rating) + { + if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + this.FileUrl = "https://danbooru.donmai.us" + url; + } + else + { + this.FileUrl = url.StartsWith("http", StringComparison.InvariantCulture) ? url : "https:" + url; + } + + this.SearchType = type; + this.FileUrl = url; + this.Tags = tags.ToHashSet(); + this.Rating = rating; + } + + public override string ToString() + { + return FileUrl; + } + + public override int GetHashCode() => FileUrl.GetHashCode(); + public override bool Equals(object obj) + => obj is ImageData ico && ico.FileUrl == this.FileUrl; + + public int CompareTo(ImageData other) + => string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture); + } +} diff --git a/src/NadekoBot/Modules/Nsfw/Common/Rule34Object.cs b/src/NadekoBot/Modules/Nsfw/Common/Rule34Object.cs new file mode 100644 index 000000000..153e186a3 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/Rule34Object.cs @@ -0,0 +1,19 @@ +namespace NadekoBot.Modules.Nsfw.Common +{ + public class Rule34Object : IImageData + { + public string Image { get; init; } + public string Directory { get; init; } + public string Tags { get; init; } + public int Score { get; init; } + + public ImageData ToCachedImageData(Booru type) + { + return new ImageData( + $"https://img.rule34.xxx//images/{Directory}/{Image}", + Booru.Rule34, + Tags.Split(' '), + Score.ToString()); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/SafebooruElement.cs b/src/NadekoBot/Modules/Nsfw/Common/SafebooruElement.cs new file mode 100644 index 000000000..98496a854 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/SafebooruElement.cs @@ -0,0 +1,15 @@ +namespace NadekoBot.Modules.Nsfw.Common +{ + public class SafebooruElement : IImageData + { + public string Directory { get; set; } + public string Image { get; set; } + + + public string FileUrl => $"https://safebooru.org/images/{Directory}/{Image}"; + public string Rating { get; set; } + public string Tags { get; set; } + public ImageData ToCachedImageData(Booru type) + => new ImageData(FileUrl, Booru.Safebooru, this.Tags.Split(' '), Rating); + } +} diff --git a/src/NadekoBot/Modules/Nsfw/Common/SankakuImageObject.cs b/src/NadekoBot/Modules/Nsfw/Common/SankakuImageObject.cs new file mode 100644 index 000000000..978ebd473 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/Common/SankakuImageObject.cs @@ -0,0 +1,27 @@ +using System.Linq; +using System.Text.Json.Serialization; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class SankakuImageObject : IImageData + { + public class Tag + { + public string Name { get; set; } + } + + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } + + [JsonPropertyName("file_type")] + public string FileType { get; set; } + + public Tag[] Tags { get; set; } + + [JsonPropertyName("total_score")] + public int Score { get; set; } + + public ImageData ToCachedImageData(Nsfw.Common.Booru type) + => new(FileUrl, Nsfw.Common.Booru.Sankaku, Tags.Select(x => x.Name).ToArray(), Score.ToString()); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Exceptions/TagBlacklistedException.cs b/src/NadekoBot/Modules/Nsfw/Exceptions/TagBlacklistedException.cs deleted file mode 100644 index 2a57719ce..000000000 --- a/src/NadekoBot/Modules/Nsfw/Exceptions/TagBlacklistedException.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace NadekoBot.Modules.NSFW.Exceptions -{ -} diff --git a/src/NadekoBot/Modules/Nsfw/ISearchImagesService.cs b/src/NadekoBot/Modules/Nsfw/ISearchImagesService.cs new file mode 100644 index 000000000..55db22f15 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/ISearchImagesService.cs @@ -0,0 +1,31 @@ +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using NadekoBot.Modules.Nsfw.Common; +using NadekoBot.Modules.Searches.Common; + +namespace NadekoBot.Modules.Nsfw +{ + public interface ISearchImagesService + { + Task Gelbooru(ulong? guildId, bool forceExplicit, string[] tags); + Task Danbooru(ulong? guildId, bool forceExplicit, string[] tags); + Task Konachan(ulong? guildId, bool forceExplicit, string[] tags); + Task Yandere(ulong? guildId, bool forceExplicit, string[] tags); + Task Rule34(ulong? guildId, bool forceExplicit, string[] tags); + Task E621(ulong? guildId, bool forceExplicit, string[] tags); + Task DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags); + Task Sankaku(ulong? guildId, bool forceExplicit, string[] tags); + Task SafeBooru(ulong? guildId, bool forceExplicit, string[] tags); + Task Hentai(ulong? guildId, bool forceExplicit, string[] tags); + Task Boobs(); + ValueTask ToggleBlacklistTag(ulong guildId, string tag); + ValueTask GetBlacklistedTags(ulong guildId); + Task Butts(); + Task GetNhentaiByIdAsync(uint id); + Task GetNhentaiBySearchAsync(string search); + ConcurrentDictionary AutoHentaiTimers { get; } + ConcurrentDictionary AutoBoobTimers { get; } + ConcurrentDictionary AutoButtTimers { get; } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Nsfw.cs b/src/NadekoBot/Modules/Nsfw/Nsfw.cs index 04470dbfd..6a153839a 100644 --- a/src/NadekoBot/Modules/Nsfw/Nsfw.cs +++ b/src/NadekoBot/Modules/Nsfw/Nsfw.cs @@ -5,68 +5,28 @@ using NadekoBot.Common.Attributes; using NadekoBot.Common.Collections; using NadekoBot.Extensions; using NadekoBot.Modules.Searches.Common; -using NadekoBot.Modules.Searches.Services; using Newtonsoft.Json.Linq; using System; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using CommandLine; using Serilog; -namespace NadekoBot.Modules.NSFW +namespace NadekoBot.Modules.Nsfw { - // thanks to halitalf for adding autoboob and autobutt features :D - public class NSFW : NadekoModule + [NoPublicBot] + public class NSFW : NadekoModule { private static readonly ConcurrentHashSet _hentaiBombBlacklist = new ConcurrentHashSet(); private readonly IHttpClientFactory _httpFactory; + private readonly NadekoRandom _rng; public NSFW(IHttpClientFactory factory) { _httpFactory = factory; - } - - private async Task InternalHentai(IMessageChannel channel, string tag) - { - // create a random number generator - var rng = new NadekoRandom(); - - // get all of the DAPI search types, except first 3 - // which are safebooru (not nsfw), and 2 furry ones 🤢 - var listOfProviders = Enum.GetValues(typeof(DapiSearchType)) - .Cast() - .Skip(3) - .ToList(); - - // now try to get an image, if it fails return an error, - // keep trying for each provider until one of them is successful, or until - // we run out of providers. If we run out, then return an error - ImageCacherObject img; - do - { - // random index of the providers - var num = rng.Next(0, listOfProviders.Count); - // get the type - var type = listOfProviders[num]; - // remove it - listOfProviders.RemoveAt(num); - // get the image - img = await _service.DapiSearch(tag, type, ctx.Guild?.Id, true).ConfigureAwait(false); - // if i can't find the image, ran out of providers, or tag is blacklisted - // return the error - if (img is null && !listOfProviders.Any()) - { - await ReplyErrorLocalizedAsync(strs.no_results).ConfigureAwait(false); - return; - } - - } while (img is null); - - await channel.EmbedAsync(_eb.Create().WithOkColor() - .WithImageUrl(img.FileUrl) - .WithDescription($"[{GetText(strs.tag)}: {tag}]({img})")) - .ConfigureAwait(false); + _rng = new NadekoRandom(); } private async Task InternalBoobs() @@ -76,8 +36,11 @@ namespace NadekoBot.Modules.NSFW JToken obj; using (var http = _httpFactory.CreateClient()) { - obj = JArray.Parse(await http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}").ConfigureAwait(false))[0]; + obj = JArray.Parse(await http + .GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}") + .ConfigureAwait(false))[0]; } + await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false); } catch (Exception ex) @@ -85,6 +48,7 @@ namespace NadekoBot.Modules.NSFW await SendErrorAsync(ex.Message).ConfigureAwait(false); } } + private async Task InternalButts(IMessageChannel Channel) { try @@ -92,8 +56,11 @@ namespace NadekoBot.Modules.NSFW JToken obj; using (var http = _httpFactory.CreateClient()) { - obj = JArray.Parse(await http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}").ConfigureAwait(false))[0]; + obj = JArray.Parse(await http + .GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}") + .ConfigureAwait(false))[0]; } + await Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false); } catch (Exception ex) @@ -102,12 +69,11 @@ namespace NadekoBot.Modules.NSFW } } -#if !GLOBAL_NADEKO [NadekoCommand, Aliases] [RequireNsfw] [RequireContext(ContextType.Guild)] [UserPerm(ChannelPerm.ManageMessages)] - public async Task AutoHentai(int interval = 0, string tags = null) + public async Task AutoHentai(int interval = 0, [Leftover] string tags = null) { Timer t; @@ -123,16 +89,18 @@ namespace NadekoBot.Modules.NSFW if (interval < 20) return; - var tagsArr = tags?.Split('|'); - t = new Timer(async (state) => { try { - if (tagsArr is null || tagsArr.Length == 0) - await InternalHentai(ctx.Channel, null).ConfigureAwait(false); + if (tags is null || tags.Length == 0) + await InternalDapiCommand(null, true, _service.Hentai).ConfigureAwait(false); else - await InternalHentai(ctx.Channel, tagsArr[new NadekoRandom().Next(0, tagsArr.Length)]).ConfigureAwait(false); + { + var groups = tags.Split('|'); + var group = groups[_rng.Next(0, groups.Length)]; + await InternalDapiCommand(group.Split(' '), true, _service.Hentai).ConfigureAwait(false); + } } catch { @@ -148,7 +116,7 @@ namespace NadekoBot.Modules.NSFW await ReplyConfirmLocalizedAsync(strs.autohentai_started( interval, - string.Join(", ", tagsArr))); + string.Join(", ", tags))); } [NadekoCommand, Aliases] @@ -231,25 +199,24 @@ namespace NadekoBot.Modules.NSFW await ReplyConfirmLocalizedAsync(strs.started(interval)); } -#endif [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Hentai([Leftover] string tag = null) => - InternalHentai(ctx.Channel, tag); + public Task Hentai(params string[] tags) + => InternalDapiCommand(tags, true, _service.Hentai); [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public async Task HentaiBomb([Leftover] string tag = null) + public async Task HentaiBomb(params string[] tags) { if (!_hentaiBombBlacklist.Add(ctx.Guild?.Id ?? ctx.User.Id)) return; try { - var images = await Task.WhenAll(_service.DapiSearch(tag, DapiSearchType.Gelbooru, ctx.Guild?.Id, true), - _service.DapiSearch(tag, DapiSearchType.Danbooru, ctx.Guild?.Id, true), - _service.DapiSearch(tag, DapiSearchType.Konachan, ctx.Guild?.Id, true), - _service.DapiSearch(tag, DapiSearchType.Yandere, ctx.Guild?.Id, true)).ConfigureAwait(false); + var images = await Task.WhenAll(_service.Yandere(ctx.Guild?.Id, true, tags), + _service.Danbooru(ctx.Guild?.Id, true, tags), + _service.Konachan(ctx.Guild?.Id, true, tags), + _service.Gelbooru(ctx.Guild?.Id, true, tags)); var linksEnum = images?.Where(l => l != null).ToArray(); if (images is null || !linksEnum.Any()) @@ -258,7 +225,8 @@ namespace NadekoBot.Modules.NSFW return; } - await ctx.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.FileUrl))).ConfigureAwait(false); + await ctx.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.Url))) + .ConfigureAwait(false); } finally { @@ -268,43 +236,48 @@ namespace NadekoBot.Modules.NSFW [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Yandere([Leftover] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Yandere, false); + public Task Yandere(params string[] tags) + => InternalDapiCommand(tags, false, _service.Yandere); [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Konachan([Leftover] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Konachan, false); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Sankaku([Leftover] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Sankaku, false); + public Task Konachan(params string[] tags) + => InternalDapiCommand(tags, false, _service.Konachan); [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task E621([Leftover] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.E621, false); + public Task Sankaku(params string[] tags) + => InternalDapiCommand(tags, false, _service.Sankaku); [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Rule34([Leftover] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Rule34, false); + public Task E621(params string[] tags) + => InternalDapiCommand(tags, false, _service.E621); [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Danbooru([Leftover] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Danbooru, false); + public Task Rule34(params string[] tags) + => InternalDapiCommand(tags, false, _service.Rule34); [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Gelbooru([Leftover] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Gelbooru, false); + public Task Danbooru(params string[] tags) + => InternalDapiCommand(tags, false, _service.Danbooru); [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Derpibooru([Leftover] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Derpibooru, false); + public Task Gelbooru(params string[] tags) + => InternalDapiCommand(tags, false, _service.Gelbooru); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Derpibooru(params string[] tags) + => InternalDapiCommand(tags, false, _service.DerpiBooru); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Safebooru(params string[] tags) + => InternalDapiCommand(tags, false, _service.SafeBooru); [NadekoCommand, Aliases] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] @@ -315,8 +288,11 @@ namespace NadekoBot.Modules.NSFW JToken obj; using (var http = _httpFactory.CreateClient()) { - obj = JArray.Parse(await http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 12000)}").ConfigureAwait(false))[0]; + obj = JArray.Parse(await http + .GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 12000)}") + .ConfigureAwait(false))[0]; } + await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false); } catch (Exception ex) @@ -334,8 +310,11 @@ namespace NadekoBot.Modules.NSFW JToken obj; using (var http = _httpFactory.CreateClient()) { - obj = JArray.Parse(await http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 6100)}").ConfigureAwait(false))[0]; + obj = JArray.Parse(await http + .GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 6100)}") + .ConfigureAwait(false))[0]; } + await ctx.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false); } catch (Exception ex) @@ -351,16 +330,16 @@ namespace NadekoBot.Modules.NSFW { if (string.IsNullOrWhiteSpace(tag)) { - var blTags = _service.GetBlacklistedTags(ctx.Guild.Id); + var blTags = await _service.GetBlacklistedTags(ctx.Guild.Id); await SendConfirmAsync(GetText(strs.blacklisted_tag_list), blTags.Any() - ? string.Join(", ", blTags) - : "-").ConfigureAwait(false); + ? string.Join(", ", blTags) + : "-").ConfigureAwait(false); } else { tag = tag.Trim().ToLowerInvariant(); - var added = _service.ToggleBlacklistedTag(ctx.Guild.Id, tag); + var added = await _service.ToggleBlacklistTag(ctx.Guild.Id, tag); if (added) await ReplyPendingLocalizedAsync(strs.blacklisted_tag_add(tag)); @@ -369,15 +348,6 @@ namespace NadekoBot.Modules.NSFW } } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public Task NsfwClearCache() - { - _service.ClearCache(); - return ctx.OkAsync(); - } - [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] @@ -394,12 +364,12 @@ namespace NadekoBot.Modules.NSFW await SendNhentaiGalleryInternalAsync(g); } - + [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] [Priority(0)] - public async Task Nhentai([Leftover]string query) + public async Task Nhentai([Leftover] string query) { var g = await _service.GetNhentaiBySearchAsync(query); @@ -420,7 +390,7 @@ namespace NadekoBot.Modules.NSFW .Select(tag => $"[{tag.Name}]({tag.Url})") .TakeWhile(tag => (count += tag.Length) < 1000) .JoinWith(" "); - + var embed = _eb.Create() .WithTitle(g.Title) .WithDescription(g.FullTitle) @@ -435,27 +405,23 @@ namespace NadekoBot.Modules.NSFW await ctx.Channel.EmbedAsync(embed); } - public async Task InternalDapiCommand(string tag, DapiSearchType type, bool forceExplicit) + private async Task InternalDapiCommand(string[] tags, + bool forceExplicit, + Func> func) { - ImageCacherObject imgObj; - - imgObj = await _service.DapiSearch(tag, type, ctx.Guild?.Id, forceExplicit).ConfigureAwait(false); - - if (imgObj is null) - await ReplyErrorLocalizedAsync(strs.no_results).ConfigureAwait(false); - else + var data = await func(ctx.Guild?.Id, forceExplicit, tags); + + if (data is null || !string.IsNullOrWhiteSpace(data.Error)) { - var embed = _eb.Create().WithOkColor() - .WithDescription($"{ctx.User} [{tag ?? "url"}]({imgObj}) ") - .WithFooter(type.ToString()); - - if (Uri.IsWellFormedUriString(imgObj.FileUrl, UriKind.Absolute)) - embed.WithImageUrl(imgObj.FileUrl); - else - Log.Error($"Image link from {type} is not a proper Url: {imgObj.FileUrl}"); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + await ReplyErrorLocalizedAsync(strs.no_results); + return; } + await ctx.Channel.EmbedAsync(_eb + .Create(ctx) + .WithOkColor() + .WithImageUrl(data.Url) + .WithDescription($"[link]({data.Url})") + .WithFooter($"{data.Rating} ({data.Provider}) | {string.Join(" | ", data.Tags.Where(x => !string.IsNullOrWhiteSpace(x)).Take(5))}")); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/NsfwService.cs b/src/NadekoBot/Modules/Nsfw/NsfwService.cs new file mode 100644 index 000000000..1f5bc2d53 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/NsfwService.cs @@ -0,0 +1,12 @@ +namespace NadekoBot.Modules.Nsfw +{ + public interface INsfwService + { + + } + + public class NsfwService + { + + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/SearchImageCacher.cs b/src/NadekoBot/Modules/Nsfw/SearchImageCacher.cs new file mode 100644 index 000000000..df8011c66 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/SearchImageCacher.cs @@ -0,0 +1,328 @@ +using Serilog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Services; + +namespace NadekoBot.Modules.Nsfw.Common +{ + public class SearchImageCacher : INService + { + private readonly IHttpClientFactory _httpFactory; + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private readonly Random _rng; + + private static readonly ISet defaultTagBlacklist = new HashSet() + { + "loli", + "lolicon", + "shota", + "shotacon", + "cub" + }; + + private readonly Dictionary _typeLocks = new(); + private readonly Dictionary> _usedTags = new(); + private readonly IMemoryCache _cache; + + public SearchImageCacher(IHttpClientFactory httpFactory, IMemoryCache cache) + { + _httpFactory = httpFactory; + _rng = new NadekoRandom(); + _cache = cache; + + // initialize new cache with empty values + foreach (var type in Enum.GetValues()) + { + _typeLocks[type] = new(); + _usedTags[type] = new(); + } + } + + private string Key(Booru boory, string tag) + => $"booru:{boory}__tag:{tag}"; + + /// + /// Download images of the specified type, and cache them. + /// + /// Required tags + /// Whether images will be forced to be explicit + /// Provider type + /// Cancellation token + /// Whether any image is found. + private async Task UpdateImagesInternalAsync(string[] tags, bool forceExplicit, Booru type, CancellationToken cancel) + { + var images = await DownloadImagesAsync(tags, forceExplicit, type, cancel).ConfigureAwait(false); + if (images is null || images.Count == 0) + { + // Log.Warning("Got no images for {0}, tags: {1}", type, string.Join(", ", tags)); + return false; + } + + Log.Information("Updating {0}...", type); + lock (_typeLocks[type]) + { + var typeUsedTags = _usedTags[type]; + foreach (var tag in tags) + typeUsedTags.Add(tag); + + // if user uses no tags for the hentai command and there are no used + // tags atm, just select 50 random tags from downloaded images to seed + if (typeUsedTags.Count == 0) + images.SelectMany(x => x.Tags) + .Distinct() + .Shuffle() + .Take(50) + .ForEach(x => typeUsedTags.Add(x)); + + foreach (var img in images) + { + // if any of the tags is a tag banned by discord + // do not put that image in the cache + if (defaultTagBlacklist.Overlaps(img.Tags)) + continue; + + // if image doesn't have a proper absolute uri, skip it + if (!Uri.IsWellFormedUriString(img.FileUrl, UriKind.Absolute)) + continue; + + // i'm appending current tags because of tag aliasing + // this way, if user uses tag alias, for example 'kissing' - + // both 'kiss' (real tag returned by the image) and 'kissing' will be populated with + // retreived images + foreach (var tag in img.Tags.Concat(tags).Distinct()) + { + if (typeUsedTags.Contains(tag)) + { + var set = _cache.GetOrCreate>(Key(type, tag), e => + { + e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); + return new(); + }); + + if(set.Count < 100) + set.Add(img); + } + } + } + } + + return true; + } + + private ImageData QueryLocal(string[] tags, bool forceExplicit, Booru type, HashSet blacklistedTags) + { + var setList = new List>(); + + // ofc make sure no changes are happening while we're getting a random one + lock (_typeLocks[type]) + { + // if no tags are provided, get a random tag + if (tags.Length == 0) + { + // get all tags in the cache + if (_usedTags.TryGetValue(type, out var allTags) + && allTags.Count > 0) + { + tags = new[] {allTags.ToList()[_rng.Next(0, allTags.Count)]}; + } + else + { + return null; + } + } + + foreach (var tag in tags) + { + // if any tag is missing from cache, that means there is no result + if (_cache.TryGetValue>(Key(type, tag), out var set)) + setList.Add(set); + else + return null; + } + + if (setList.Count == 0) + return null; + + + List resultList; + // if multiple tags, we need to interesect sets + if (setList.Count > 1) + { + // now that we have sets, interesect them to find eligible items + // make a copy of the 1st set + var resultSet = new HashSet(setList[0]); + + // go through all other sets, and + for (var i = 1; i < setList.Count; ++i) + { + // if any of the elements in result set are not present in the current set + // remove it from the result set + resultSet.IntersectWith(setList[i]); + } + + resultList = resultSet.ToList(); + + } + else + { + // if only one tag, use that set + resultList = setList[0].ToList(); + } + + // return a random one which doesn't have blacklisted tags in it + resultList = resultList.Where(x => !blacklistedTags.Overlaps(x.Tags)).ToList(); + + // if no items in the set -> not found + if (resultList.Count == 0) + return null; + + var toReturn = resultList[_rng.Next(0, resultList.Count)]; + + // remove from cache + foreach (var tag in tags) + { + if (_cache.TryGetValue>(Key(type, tag), out var items)) + { + items.Remove(toReturn); + } + } + return toReturn; + } + } + + public async Task GetImageNew(string[] tags, bool forceExplicit, Booru type, + HashSet blacklistedTags, CancellationToken cancel) + { + // make sure tags are proper + tags = tags + .Where(x => x is not null) + .Select(tag => tag.ToLowerInvariant().Trim()) + .Distinct() + .ToArray(); + + if (tags.Length > 2 && type == Booru.Danbooru) + tags = tags[..2]; + + // use both tags banned by discord and tags banned on the server + if (blacklistedTags.Overlaps(tags) || defaultTagBlacklist.Overlaps(tags)) + return default; + + // query for an image + var image = QueryLocal(tags, forceExplicit, type, blacklistedTags); + if (image is not null) + return image; + + var success = false; + try + { + // if image is not found, update the cache and query again + success = await UpdateImagesInternalAsync(tags, forceExplicit, type, cancel).ConfigureAwait(false); + } + catch (HttpRequestException) + { + return default; + } + + if (!success) + return default; + image = QueryLocal(tags, forceExplicit, type, blacklistedTags); + + return image; + } + + private readonly ConcurrentDictionary<(Booru, string), int> maxPages = new(); + + public async Task> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, CancellationToken cancel) + { + var tagStr = string.Join(' ', tags.OrderByDescending(x => x)); + var page = 0; + + var attempt = 0; + while (attempt++ <= 10) + { + if (maxPages.TryGetValue((type, tagStr), out var maxPage)) + { + if (maxPage == 0) + { + Log.Information("Tag {0} yields no result on {1}, skipping.", tagStr, type); + return new(); + } + + page = _rng.Next(0, maxPage); + } + else + { + page = _rng.Next(0, 11); + } + + var result = await DownloadImagesAsync(tags, isExplicit, type, page, cancel).ConfigureAwait(false); + + if (result is null or { Count: 0 }) + { + Log.Information("Tag {0}, page {1} has no result on {2}.", string.Join(", ", tags), page, type.ToString()); + continue; + } + + return result; + } + + return new(); + } + + private IImageDownloader GetImageDownloader(Booru booru, HttpClient http) + => booru switch + { + Booru.Danbooru => new DanbooruImageDownloader(http), + Booru.Yandere => new YandereImageDownloader(http), + Booru.Konachan => new KonachanImageDownloader(http), + Booru.Safebooru => new SafebooruImageDownloader(http), + Booru.E621 => new E621ImageDownloader(http), + Booru.Derpibooru => new DerpibooruImageDownloader(http), + Booru.Gelbooru => new GelbooruImageDownloader(http), + Booru.Rule34 => new Rule34ImageDownloader(http), + Booru.Sankaku => new SankakuImageDownloader(http), + _ => throw new NotImplementedException($"{booru} downloader not implemented.") + }; + + private async Task> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, int page, CancellationToken cancel) + { + try + { + Log.Information("Downloading from {0} (page {1})...", type, page); + + using var http = _httpFactory.CreateClient(); + var downloader = GetImageDownloader(type, http); + + var images = await downloader.DownloadImageDataAsync(tags, page, isExplicit, cancel); + if (images.Count == 0) + { + var tagStr = string.Join(' ', tags.OrderByDescending(x => x)); + maxPages[(type, tagStr)] = page; + } + + return images; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + Log.Error(ex, "Error downloading an image:\nTags: {0}\nType: {1}\nPage: {2}\nMessage: {3}", + string.Join(", ", tags), + type, + page, + ex.Message); + return new(); + } + } + } +} diff --git a/src/NadekoBot/Modules/Nsfw/SearchImagesService.cs b/src/NadekoBot/Modules/Nsfw/SearchImagesService.cs new file mode 100644 index 000000000..08e973cc0 --- /dev/null +++ b/src/NadekoBot/Modules/Nsfw/SearchImagesService.cs @@ -0,0 +1,393 @@ +using Newtonsoft.Json.Linq; +using Serilog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Threading; +using LinqToDB; +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Modules.Searches.Common; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using Newtonsoft.Json; +using Booru = NadekoBot.Modules.Nsfw.Common.Booru; +using SearchImageCacher = NadekoBot.Modules.Nsfw.Common.SearchImageCacher; + +namespace NadekoBot.Modules.Nsfw +{ + public record TagRequest(ulong GuildId, bool ForceExplicit, Booru SearchType, params string[] Tags); + public record UrlReply + { + public string Error { get; init; } + public string Url { get; init; } + public string Rating { get; init; } + public string Provider { get; init; } + public List Tags { get; } = new List(); + } + + public class SearchImagesService : ISearchImagesService, INService + { + private readonly Random _rng; + private readonly HttpClient _http; + private readonly SearchImageCacher _cache; + private readonly IHttpClientFactory _httpFactory; + private readonly DbService _db; + private ConcurrentDictionary> BlacklistedTags { get; } + + public ConcurrentDictionary AutoHentaiTimers { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary AutoBoobTimers { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary AutoButtTimers { get; } = new ConcurrentDictionary(); + + public SearchImagesService(DbService db, + IHttpClientFactory http, + SearchImageCacher cacher, + IHttpClientFactory httpFactory) + { + _db = db; + _rng = new NadekoRandom(); + _http = http.CreateClient(); + _http.AddFakeHeaders(); + _cache = cacher; + _httpFactory = httpFactory; + + using var uow = db.GetDbContext(); + BlacklistedTags = new( + uow.NsfwBlacklistedTags + .AsEnumerable() + .GroupBy(x => x.GuildId) + .ToDictionary( + x => x.Key, + x => new HashSet(x.Select(x => x.Tag)))); + } + + private Task GetNsfwImageAsync(ulong? guildId, bool forceExplicit, string[] tags, Booru dapi, CancellationToken cancel = default) + { + return GetNsfwImageAsync(guildId ?? 0, tags ?? Array.Empty(), forceExplicit, dapi, cancel); + } + + private bool IsValidTag(string tag) => tag.All(x => x != '+' && x != '?' && x != '/'); // tags mustn't contain + or ? or / + + private async Task GetNsfwImageAsync( + ulong guildId, + string[] tags, + bool forceExplicit, + Booru dapi, + CancellationToken cancel) + { + if (!tags.All(x => IsValidTag(x))) + { + return new UrlReply + { + Error = "One or more tags are invalid.", + Url = "" + }; + } + + Log.Information("Getting {V} image for Guild: {GuildId}...", dapi.ToString(), guildId); + try + { + BlacklistedTags.TryGetValue(guildId, out var blTags); + + if (dapi == Booru.E621) { + for (var i = 0; i < tags.Length; ++i) + if (tags[i] == "yuri") + tags[i] = "female/female"; + } + + if (dapi == Booru.Derpibooru) + { + for (var i = 0; i < tags.Length; ++i) + if (tags[i] == "yuri") + tags[i] = "lesbian"; + } + + var result = await _cache.GetImageNew(tags, forceExplicit, dapi, blTags ?? new HashSet(), cancel) + .ConfigureAwait(false); + + if (result is null) + { + return new UrlReply + { + Error = "Image not found.", + Url = "" + }; + } + + var reply = new UrlReply + { + Error = "", + Url = result.FileUrl, + Rating = result.Rating, + Provider = result.SearchType.ToString() + }; + + reply.Tags.AddRange(result.Tags); + + return reply; + + } + catch (Exception ex) + { + Log.Error(ex, "Failed getting {Dapi} image: {Message}", dapi, ex.Message); + return new UrlReply + { + Error = ex.Message, + Url = "" + }; + } + } + + public Task Gelbooru(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Gelbooru); + + public Task Danbooru(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Danbooru); + + public Task Konachan(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Konachan); + + public Task Yandere(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Yandere); + + public Task Rule34(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Rule34); + + public Task E621(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.E621); + + public Task DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Derpibooru); + + public Task SafeBooru(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Safebooru); + + public Task Sankaku(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Sankaku); + + public async Task Hentai(ulong? guildId, bool forceExplicit, string[] tags) + { + var providers = new[] { + Booru.Danbooru, + Booru.Konachan, + Booru.Gelbooru, + Booru.Yandere + }; + + using var cancelSource = new CancellationTokenSource(); + + // create a task for each type + var tasks = providers.Select(type => GetNsfwImageAsync(guildId, forceExplicit, tags, type)).ToList(); + do + { + // wait for any of the tasks to complete + var task = await Task.WhenAny(tasks); + + // get its result + var result = task.GetAwaiter().GetResult(); + if(result.Error == "") + { + // if we have a non-error result, cancel other searches and return the result + cancelSource.Cancel(); + return result; + } + + // if the result is an error, remove that task from the waiting list, + // and wait for another task to complete + tasks.Remove(task); + } + while (tasks.Count > 0); // keep looping as long as there is any task remaining to be attempted + + // if we ran out of tasks, that means all tasks failed - return an error + return new UrlReply() + { + Error = "No hentai image found." + }; + } + + public async Task Boobs() + { + try + { + JToken obj; + obj = JArray.Parse(await _http.GetStringAsync($"http://api.oboobs.ru/boobs/{_rng.Next(0, 12000)}").ConfigureAwait(false))[0]; + return new UrlReply + { + Error = "", + Url = $"http://media.oboobs.ru/{obj["preview"]}", + }; + } + catch (Exception ex) + { + Log.Error(ex, "Error retreiving boob image: {Message}", ex.Message); + return new UrlReply + { + Error = ex.Message, + Url = "", + }; + } + } + + private readonly object taglock = new object(); + public ValueTask ToggleBlacklistTag(ulong guildId, string tag) + { + lock (taglock) + { + tag = tag.Trim().ToLowerInvariant(); + var blacklistedTags = BlacklistedTags.GetOrAdd(guildId, new HashSet()); + var isAdded = blacklistedTags.Add(tag); + + using var uow = _db.GetDbContext(); + if (!isAdded) + { + blacklistedTags.Remove(tag); + uow.NsfwBlacklistedTags.DeleteAsync(x => x.GuildId == guildId && x.Tag == tag); + uow.SaveChanges(); + } + else + { + uow.NsfwBlacklistedTags.Add(new NsfwBlacklistedTag() + { + Tag = tag, + GuildId = guildId + }); + + uow.SaveChanges(); + } + + return new(isAdded); + } + + } + + public ValueTask GetBlacklistedTags(ulong guildId) + { + lock (taglock) + { + if (BlacklistedTags.TryGetValue(guildId, out var tags)) + { + return new(tags.ToArray()); + } + + return new(Array.Empty()); + } + } + + public async Task Butts() + { + try + { + JToken obj; + obj = JArray.Parse(await _http.GetStringAsync($"http://api.obutts.ru/butts/{_rng.Next(0, 6100)}"))[0]; + return new UrlReply + { + Error = "", + Url = $"http://media.obutts.ru/{obj["preview"]}", + }; + } + catch (Exception ex) + { + Log.Error(ex, "Error retreiving butt image: {Message}", ex.Message); + return new UrlReply + { + Error = ex.Message, + Url = "", + }; + } + } + + #region Nhentai + private string GetNhentaiExtensionInternal(string s) + => s switch + { + "j" => "jpg", + "p" => "png", + "g" => "gif", + _ => "jpg" + }; + + private Gallery ModelToGallery(NhentaiApiModel.Gallery model) + { + var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb." + + GetNhentaiExtensionInternal(model.Images.Thumbnail.T); + + var url = $"https://nhentai.net/g/{model.Id}"; + return new Gallery( + model.Id.ToString(), + url, + model.Title.English, + model.Title.Pretty, + thumbnail, + model.NumPages, + model.NumFavorites, + model.UploadDate.ToUnixTimestamp().UtcDateTime, + model.Tags.Map(x => new Tag() + { + Name = x.Name, + Url = "https://nhentai.com/" + x.Url + })); + } + + private async Task GetNhentaiByIdInternalAsync(uint id) + { + using var http = _httpFactory.CreateClient(); + try + { + var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id); + return JsonConvert.DeserializeObject(res); + } + catch (HttpRequestException) + { + Log.Warning("Nhentai with id {NhentaiId} not found", id); + return null; + } + } + + private async Task SearchNhentaiInternalAsync(string search) + { + using var http = _httpFactory.CreateClient(); + try + { + var res = await http.GetStringAsync("https://nhentai.net/api/galleries/search?query=" + search); + return JsonConvert.DeserializeObject(res).Result; + } + catch (HttpRequestException) + { + Log.Warning("Nhentai with search {NhentaiSearch} not found", search); + return null; + } + } + + public async Task GetNhentaiByIdAsync(uint id) + { + var model = await GetNhentaiByIdInternalAsync(id); + + return ModelToGallery(model); + } + + private static readonly string[] _bannedTags = + { + "loli", + "lolicon", + "shota", + "shotacon", + "cub" + }; + + public async Task GetNhentaiBySearchAsync(string search) + { + var models = await SearchNhentaiInternalAsync(search); + + models = models.Where(x => !x.Tags.Any(t => _bannedTags.Contains(t.Name))).ToArray(); + + if (models.Length == 0) + return null; + + return ModelToGallery(models[_rng.Next(0, models.Length)]); + } + #endregion + } +} diff --git a/src/NadekoBot/Modules/Searches/Common/ImageCacherObject.cs b/src/NadekoBot/Modules/Searches/Common/ImageCacherObject.cs index ebff6db51..be4e066f1 100644 --- a/src/NadekoBot/Modules/Searches/Common/ImageCacherObject.cs +++ b/src/NadekoBot/Modules/Searches/Common/ImageCacherObject.cs @@ -1,18 +1,19 @@ using System; using System.Collections.Generic; +using NadekoBot.Modules.Nsfw.Common; namespace NadekoBot.Modules.Searches.Common { public class ImageCacherObject : IComparable { - public DapiSearchType SearchType { get; } + public Booru SearchType { get; } public string FileUrl { get; } public HashSet Tags { get; } public string Rating { get; } - public ImageCacherObject(DapiImageObject obj, DapiSearchType type) + public ImageCacherObject(DapiImageObject obj, Booru type) { - if (type == DapiSearchType.Danbooru && !Uri.IsWellFormedUriString(obj.FileUrl, UriKind.Absolute)) + if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(obj.FileUrl, UriKind.Absolute)) { this.FileUrl = "https://danbooru.donmai.us" + obj.FileUrl; } @@ -25,7 +26,7 @@ namespace NadekoBot.Modules.Searches.Common this.Tags = new HashSet((obj.Tags ?? obj.TagString).Split(' ')); } - public ImageCacherObject(string url, DapiSearchType type, string tags, string rating) + public ImageCacherObject(string url, Booru type, string tags, string rating) { this.SearchType = type; this.FileUrl = url; diff --git a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs deleted file mode 100644 index 7f63e7351..000000000 --- a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs +++ /dev/null @@ -1,312 +0,0 @@ -using NadekoBot.Extensions; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using Serilog; - -namespace NadekoBot.Modules.Searches.Common -{ - // note: this is not the code that public nadeko is using - public class SearchImageCacher - { - private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); - private readonly IHttpClientFactory _httpFactory; - private readonly Random _rng; - private readonly SortedSet _cache; - private static readonly List defaultTagBlacklist = new List() { - "loli", - "lolicon", - "shota" - }; - - public SearchImageCacher(IHttpClientFactory http) - { - _httpFactory = http; - _rng = new Random(); - _cache = new SortedSet(); - } - - public async Task GetImage(string[] tags, bool forceExplicit, DapiSearchType type, - HashSet blacklistedTags = null) - { - tags = tags.Select(tag => tag?.ToLowerInvariant()).ToArray(); - - blacklistedTags = blacklistedTags ?? new HashSet(); - - foreach (var item in defaultTagBlacklist) - { - blacklistedTags.Add(item); - } - - blacklistedTags = blacklistedTags.Select(t => t.ToLowerInvariant()).ToHashSet(); - - if (tags.Any(x => blacklistedTags.Contains(x))) - { - throw new Exception("One of the specified tags is blacklisted"); - } - - if (type == DapiSearchType.E621) - tags = tags.Select(tag => tag?.Replace("yuri", "female/female", StringComparison.InvariantCulture)) - .ToArray(); - - await _lock.WaitAsync().ConfigureAwait(false); - try - { - ImageCacherObject[] imgs; - if (tags.Any()) - { - imgs = _cache.Where(x => x.Tags.IsSupersetOf(tags) && x.SearchType == type && (!forceExplicit || x.Rating == "e")).ToArray(); - } - else - { - imgs = _cache.Where(x => x.SearchType == type).ToArray(); - } - imgs = imgs.Where(x => x.Tags.All(t => !blacklistedTags.Contains(t.ToLowerInvariant()))).ToArray(); - ImageCacherObject img; - if (imgs.Length == 0) - img = null; - else - img = imgs[_rng.Next(imgs.Length)]; - - if (img != null) - { - _cache.Remove(img); - return img; - } - else - { - var images = await DownloadImagesAsync(tags, forceExplicit, type).ConfigureAwait(false); - images = images - .Where(x => x.Tags.All(t => !blacklistedTags.Contains(t.ToLowerInvariant()))) - .ToArray(); - if (images.Length == 0) - return null; - var toReturn = images[_rng.Next(images.Length)]; - foreach (var dledImg in images) - { - if (dledImg != toReturn) - _cache.Add(dledImg); - } - return toReturn; - } - } - finally - { - _lock.Release(); - } - } - - public async Task DownloadImagesAsync(string[] tags, bool isExplicit, DapiSearchType type) - { - isExplicit = type == DapiSearchType.Safebooru - ? false - : isExplicit; - var tag = ""; - tag += string.Join('+', tags.Select(x => x.Replace(" ", "_", StringComparison.InvariantCulture).ToLowerInvariant())); - if (isExplicit) - tag = "rating%3Aexplicit+" + tag; - var website = ""; - switch (type) - { - case DapiSearchType.Safebooru: - website = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=1000&tags={tag}&json=1"; - break; - case DapiSearchType.E621: - website = $"https://e621.net/posts.json?limit=200&tags={tag}"; - break; - case DapiSearchType.Danbooru: - website = $"http://danbooru.donmai.us/posts.json?limit=100&tags={tag}"; - break; - case DapiSearchType.Gelbooru: - website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}"; - break; - case DapiSearchType.Rule34: - website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}"; - break; - case DapiSearchType.Konachan: - website = $"https://konachan.com/post.json?s=post&q=index&limit=100&tags={tag}"; - break; - case DapiSearchType.Yandere: - website = $"https://yande.re/post.json?limit=100&tags={tag}"; - break; - case DapiSearchType.Derpibooru: - tag = string.IsNullOrWhiteSpace(tag) ? "safe" : tag; - website = $"https://www.derpibooru.org/api/v1/json/search/images?q={tag?.Replace('+', ',')}&per_page=49"; - break; - case DapiSearchType.Sankaku: - website = $"https://capi-v2.sankakucomplex.com/posts?tags={tag}&limit=50"; - break; - } - - try - { - using (var _http = _httpFactory.CreateClient()) - { - _http.AddFakeHeaders(); - if (type == DapiSearchType.Konachan || type == DapiSearchType.Yandere || type == DapiSearchType.Danbooru) - { - var data = await _http.GetStringAsync(website).ConfigureAwait(false); - return JsonConvert.DeserializeObject(data) - .Where(x => x.FileUrl != null) - .Select(x => new ImageCacherObject(x, type)) - .ToArray(); - } - - if (type == DapiSearchType.Sankaku) - { - var data = await _http.GetStringAsync(website).ConfigureAwait(false); - return JsonConvert.DeserializeObject(data) - .Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image")) - .Select(x => new ImageCacherObject( - x.FileUrl, - DapiSearchType.Sankaku, - x.Tags.Select(x => x.Name).JoinWith(','), - x.Score)) - .ToArray(); - } - - if (type == DapiSearchType.E621) - { - var data = await _http.GetStringAsync(website).ConfigureAwait(false); - return JsonConvert.DeserializeAnonymousType(data, new { posts = new List() }) - .posts - .Where(x => !string.IsNullOrWhiteSpace(x.File?.Url)) - .Select(x => new ImageCacherObject(x.File.Url, - type, string.Join(' ', x.Tags.General), x.Score.Total)) - .ToArray(); - } - - if (type == DapiSearchType.Derpibooru) - { - var data = await _http.GetStringAsync(website).ConfigureAwait(false); - return JsonConvert.DeserializeObject(data) - .Images - .Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl)) - .Select(x => new ImageCacherObject(x.ViewUrl, - type, string.Join("\n", x.Tags), x.Score)) - .ToArray(); - } - - if (type == DapiSearchType.Safebooru) - { - var data = await _http.GetStringAsync(website).ConfigureAwait(false); - return JsonConvert.DeserializeObject(data) - .Select(x => new ImageCacherObject(x.FileUrl, type, x.Tags, x.Rating)) - .ToArray(); - } - - return (await LoadXmlAsync(website, type).ConfigureAwait(false)).ToArray(); - } - } - catch (Exception ex) - { - Log.Warning(ex, "Error downloading an image: {Message}", ex.Message); - return Array.Empty(); - } - } - - private async Task LoadXmlAsync(string website, DapiSearchType type) - { - var list = new List(); - using (var http = _httpFactory.CreateClient()) - using (var stream = await http.GetStreamAsync(website).ConfigureAwait(false)) - using (var reader = XmlReader.Create(stream, new XmlReaderSettings() - { - Async = true, - })) - { - while (await reader.ReadAsync().ConfigureAwait(false)) - { - if (reader.NodeType == XmlNodeType.Element && - reader.Name == "post") - { - list.Add(new ImageCacherObject(new DapiImageObject() - { - FileUrl = reader["file_url"], - Tags = reader["tags"], - Rating = reader["rating"] ?? "e" - - }, type)); - } - } - } - return list.ToArray(); - } - - public void Clear() - { - _cache.Clear(); - } - } - - - public class DapiImageObject - { - [JsonProperty("File_Url")] - public string FileUrl { get; set; } - public string Tags { get; set; } - [JsonProperty("Tag_String")] - public string TagString { get; set; } - public string Rating { get; set; } - } - - public class DerpiContainer - { - public DerpiImageObject[] Images { get; set; } - } - - public class DerpiImageObject - { - [JsonProperty("view_url")] - public string ViewUrl { get; set; } - public string[] Tags { get; set; } - public string Score { get; set; } - } - - public class SankakuImageObject - { - public class Tag - { - public string Name { get; set; } - } - - [JsonProperty("file_url")] - public string FileUrl { get; set; } - - [JsonProperty("file_type")] - public string FileType { get; set; } - - public Tag[] Tags { get; set; } - - [JsonProperty("total_score")] - public string Score { get; set; } - } - - public enum DapiSearchType - { - Safebooru, - E621, - Derpibooru, - Gelbooru, - Konachan, - Rule34, - Yandere, - Danbooru, - Sankaku, - } - public class SafebooruElement - { - public string Directory { get; set; } - public string Image { get; set; } - - - public string FileUrl => $"https://safebooru.org/images/{Directory}/{Image}"; - public string Rating { get; set; } - public string Tags { get; set; } - } -} diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index af67e082f..bc3bd0794 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -22,6 +22,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; using NadekoBot.Modules.Administration.Services; +using NadekoBot.Modules.Nsfw.Common; using Serilog; using Configuration = AngleSharp.Configuration; @@ -589,10 +590,6 @@ namespace NadekoBot.Modules.Searches await SendConfirmAsync($"https://images.google.com/searchbyimage?image_url={imageLink}").ConfigureAwait(false); } - [NadekoCommand, Aliases] - public Task Safebooru([Leftover] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Safebooru); - [NadekoCommand, Aliases] public async Task Wiki([Leftover] string query = null) { @@ -760,21 +757,6 @@ namespace NadekoBot.Modules.Searches await ctx.Channel.SendMessageAsync($"https://store.steampowered.com/app/{appId}").ConfigureAwait(false); } - public async Task InternalDapiCommand(string tag, DapiSearchType type) - { - tag = tag?.Trim() ?? ""; - - var imgObj = await _service.DapiSearch(tag, type, ctx.Guild?.Id).ConfigureAwait(false); - - if (imgObj is null) - await SendErrorAsync(ctx.User.Mention + " " + GetText(strs.no_results)).ConfigureAwait(false); - else - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithDescription($"{ctx.User.Mention} [{tag ?? "url"}]({imgObj.FileUrl})") - .WithImageUrl(imgObj.FileUrl) - .WithFooter(type.ToString())).ConfigureAwait(false); - } - public async Task ValidateQuery(IMessageChannel ch, string query) { if (!string.IsNullOrWhiteSpace(query)) diff --git a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs index c75518535..258a079a0 100644 --- a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs @@ -50,14 +50,6 @@ namespace NadekoBot.Modules.Searches.Services public List WowJokes { get; } = new List(); public List MagicItems { get; } = new List(); - - private readonly ConcurrentDictionary _imageCacher = new ConcurrentDictionary(); - - public ConcurrentDictionary AutoHentaiTimers { get; } = new ConcurrentDictionary(); - public ConcurrentDictionary AutoBoobTimers { get; } = new ConcurrentDictionary(); - public ConcurrentDictionary AutoButtTimers { get; } = new ConcurrentDictionary(); - - private readonly ConcurrentDictionary> _blacklistedTags = new ConcurrentDictionary>(); private readonly List _yomamaJokes; public SearchesService(DiscordSocketClient client, IGoogleApiService google, @@ -75,11 +67,6 @@ namespace NadekoBot.Modules.Searches.Services _eb = eb; _rng = new NadekoRandom(); - _blacklistedTags = new ConcurrentDictionary>( - bot.AllGuildConfigs.ToDictionary( - x => x.GuildId, - x => new HashSet(x.NsfwBlacklistedTags.Select(y => y.Tag)))); - //translate commands _client.MessageReceived += (msg) => { @@ -366,80 +353,6 @@ namespace NadekoBot.Modules.Searches.Services return (await _google.Translate(text, from, to).ConfigureAwait(false)).SanitizeMentions(true); } - public Task DapiSearch(string tag, DapiSearchType type, ulong? guild, bool isExplicit = false) - { - tag = tag ?? ""; - if (string.IsNullOrWhiteSpace(tag) - && (tag.Contains("loli") || tag.Contains("shota"))) - { - return null; - } - - var tags = tag - .Split('+') - .Select(x => x.ToLowerInvariant().Replace(' ', '_')) - .ToArray(); - - if (guild.HasValue) - { - var blacklistedTags = GetBlacklistedTags(guild.Value); - - var cacher = _imageCacher.GetOrAdd(guild.Value, (key) => new SearchImageCacher(_httpFactory)); - - return cacher.GetImage(tags, isExplicit, type, blacklistedTags); - } - else - { - var cacher = _imageCacher.GetOrAdd(guild ?? 0, (key) => new SearchImageCacher(_httpFactory)); - - return cacher.GetImage(tags, isExplicit, type); - } - } - - public HashSet GetBlacklistedTags(ulong guildId) - { - if (_blacklistedTags.TryGetValue(guildId, out var tags)) - return tags; - return new HashSet(); - } - - public bool ToggleBlacklistedTag(ulong guildId, string tag) - { - var tagObj = new NsfwBlacklitedTag - { - Tag = tag - }; - - bool added; - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set.Include(y => y.NsfwBlacklistedTags)); - if (gc.NsfwBlacklistedTags.Add(tagObj)) - added = true; - else - { - gc.NsfwBlacklistedTags.Remove(tagObj); - var toRemove = gc.NsfwBlacklistedTags.FirstOrDefault(x => x.Equals(tagObj)); - if (toRemove != null) - uow.Remove(toRemove); - added = false; - } - var newTags = new HashSet(gc.NsfwBlacklistedTags.Select(x => x.Tag)); - _blacklistedTags.AddOrUpdate(guildId, newTags, delegate { return newTags; }); - - uow.SaveChanges(); - } - return added; - } - - public void ClearCache() - { - foreach (var c in _imageCacher) - { - c.Value?.Clear(); - } - } - private readonly object yomamaLock = new object(); private int yomamaJokeIndex = 0; public Task GetYomamaJoke() @@ -838,95 +751,5 @@ namespace NadekoBot.Modules.Searches.Services fullQueryLink, "0"); } - #region Nhentai - private string GetNhentaiExtensionInternal(string s) - => s switch - { - "j" => "jpg", - "p" => "png", - "g" => "gif", - _ => "jpg" - }; - - private Gallery ModelToGallery(NhentaiApiModel.Gallery model) - { - var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb." - + GetNhentaiExtensionInternal(model.Images.Thumbnail.T); - - var url = $"https://nhentai.net/g/{model.Id}"; - return new Gallery( - model.Id.ToString(), - url, - model.Title.English, - model.Title.Pretty, - thumbnail, - model.NumPages, - model.NumFavorites, - model.UploadDate.ToUnixTimestamp().UtcDateTime, - model.Tags.Map(x => new Tag() - { - Name = x.Name, - Url = "https://nhentai.com/" + x.Url - })); - } - - public async Task GetNhentaiByIdInternalAsync(uint id) - { - using var http = _httpFactory.CreateClient(); - try - { - var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id); - return JsonConvert.DeserializeObject(res); - } - catch (HttpRequestException) - { - Log.Warning("Nhentai with id {NhentaiId} not found", id); - return null; - } - } - - private async Task SearchNhentaiInternalAsync(string search) - { - using var http = _httpFactory.CreateClient(); - try - { - var res = await http.GetStringAsync("https://nhentai.net/api/galleries/search?query=" + search); - return JsonConvert.DeserializeObject(res).Result; - } - catch (HttpRequestException) - { - Log.Warning("Nhentai with search {NhentaiSearch} not found", search); - return null; - } - } - - public async Task GetNhentaiByIdAsync(uint id) - { - var model = await GetNhentaiByIdInternalAsync(id); - - return ModelToGallery(model); - } - - private static readonly string[] _bannedTags = - { - "loli", - "lolicon", - "shota", - "shotacon", - "cub" - }; - - public async Task GetNhentaiBySearchAsync(string search) - { - var models = await SearchNhentaiInternalAsync(search); - - models = models.Where(x => !x.Tags.Any(t => _bannedTags.Contains(t.Name))).ToArray(); - - if (models.Length == 0) - return null; - - return ModelToGallery(models[_rng.Next(0, models.Length)]); - } - #endregion } } diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index ce85b4ab7..21a24a0eb 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -1132,8 +1132,6 @@ clubbans: clubleaderboard: - clublb - clubs -nsfwclearcache: -- nsfwcc clubadmin: - clubadmin autoboobs: diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index 1fe4a9484..cc61b6b9e 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -1069,10 +1069,6 @@ revimg: desc: "Returns a Google reverse image search for an image from a link." args: - "Image link" -safebooru: - desc: "Shows a random image from safebooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +)" - args: - - "yuri+kissing" wiki: desc: "Gives you back a wikipedia link" args: @@ -1087,33 +1083,55 @@ avatar: args: - "@Someone" hentai: - desc: "Shows a hentai image from a random website (gelbooru, danbooru, konachan or yandere) with a given tag. Tag is optional but preferred. Only 1 tag allowed." + desc: "Shows a hentai image from a random website (gelbooru, danbooru, konachan or yandere) with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags. Only 1 tag allowed." args: - "yuri" +nhentai: + desc: "Shows basic information about a hentai with the specified id, or a valid nhentai search query." + args: + - "273426" + - "cute girl" +autohentai: + desc: "Posts a hentai every X seconds with a random tag from the provided tags. Use `|` to separate tag groups. Random group will be chosen every time the image is sent. Max 2 tags per group. 20 seconds minimum. Provide no parameters to disable." + args: + - "30 yuri kissing|tail long_hair" + - "" +hentaibomb: + desc: "Shows a total 5 images (from gelbooru, danbooru, konachan and yandere). Tag(s) are optional but preferred. Maximum is usually 2 tags." + args: + - "yuri" +yandere: + desc: "Shows a random image from yandere with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags." + args: + - "yuri kissing" danbooru: - desc: "Shows a random hentai image from danbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +)" + desc: "Shows a random hentai image from danbooru with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags." args: - - "yuri+kissing" + - "yuri kissing" derpibooru: - desc: "Shows a random image from derpibooru with a given tag. Tag is optional but preferred." + desc: "Shows a random image from derpibooru with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags." args: - - "yuri+kissing" + - "yuri kissing" gelbooru: - desc: "Shows a random hentai image from gelbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +)" + desc: "Shows a random hentai image from gelbooru with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags." args: - - "yuri+kissing" + - "yuri kissing" sankaku: - desc: "Shows a random hentai image from chan.sankakucomplex.com with a given tag. Tag is optional but preferred. (multiple tags are appended with +)" + desc: "Shows a random hentai image from chan.sankakucomplex.com with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags." args: - - "yuri+kiss" + - "yuri kiss" rule34: - desc: "Shows a random image from rule34.xx with a given tag. Tag is optional but preferred. (multiple tags are appended with +)" + desc: "Shows a random image from rule34.xx with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags." args: - - "yuri+kissing" + - "yuri kissing" e621: - desc: "Shows a random hentai image from e621.net with a given tag. Tag is optional but preferred. (multiple tags are appended with +)" + desc: "Shows a random hentai image from e621.net with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags." args: - - "yuri+kissing" + - "yuri kissing" +safebooru: + desc: "Shows a random image from safebooru with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags." + args: + - "yuri kissing" boobs: desc: "Real adult content." args: @@ -1229,7 +1247,7 @@ voicemute: - "1h30m @Someone" - "1h @Someone silence" konachan: - desc: "Shows a random hentai image from konachan with a given tag. Tag is optional but preferred." + desc: "Shows a random hentai image from konachan with a given tag. Tag(s) are optional but preferred. Maximum is usually 2 tags." args: - "yuri" muterole: @@ -1299,10 +1317,6 @@ listservers: desc: "Lists servers the bot is on with some basic info. 15 per page." args: - "3" -hentaibomb: - desc: "Shows a total 5 images (from gelbooru, danbooru, konachan and yandere). Tag is optional but preferred." - args: - - "yuri" cleverbot: desc: "Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Custom reactions starting with %bot.mention% won't work if cleverbot is enabled." args: @@ -1316,10 +1330,6 @@ wikia: args: - "mtg Vigilance" - "mlp Dashy" -yandere: - desc: "Shows a random image from yandere with a given tag. Tag is optional but preferred. (multiple tags are appended with +)" - args: - - "tag1+tag2" magicthegathering: desc: "Searches for a Magic The Gathering card." args: @@ -1368,11 +1378,6 @@ activity: desc: "Checks for spammers." args: - "" -autohentai: - desc: "Posts a hentai every X seconds with a random tag from the provided tags. Use `|` to separate tag groups. Random group will be chosen every time the image is sent. Use `+` for multiple tags (max 2 per group). 20 seconds minimum. Provide no parameters to disable." - args: - - "30 yuri+kissing|tail+long_hair" - - "" setstatus: desc: "Sets the bot's status. (Online/Idle/Dnd/Invisible)" args: @@ -1899,10 +1904,6 @@ clublb: desc: "Shows club rankings on the specified page." args: - "2" -nsfwcc: - desc: "Clears nsfw cache." - args: - - "" clubadmin: desc: "Assigns (or unassigns) staff role to the member of the club. Admins can ban, kick and accept applications." args: @@ -2118,11 +2119,6 @@ purgeuser: desc: "Purge user from the database completely. This includes currency, xp, clubs that user owns, waifu info" args: - "@Oblivion" -nhentai: - desc: "Shows basic information about a hentai with the specified id, or a valid nhentai search query." - args: - - "273426" - - "cute girl" imageonlychannel: desc: |- Toggles whether the channel only allows images.