From b8df0d8b063edf8b8a43d2d26a6d095eef1a226d Mon Sep 17 00:00:00 2001
From: Toastie <toastie@toastiet0ast.com>
Date: Sat, 6 Apr 2024 23:24:57 +1300
Subject: [PATCH] Finally finished the Db potion of the EllieBot code

---
 src/EllieBot/Db/EllieContext.cs               | 491 ++++++++++++++++++
 src/EllieBot/Db/Extensions/ClubExtensions.cs  |  34 ++
 .../CurrencyTransactionExtensions.cs          |  20 +
 src/EllieBot/Db/Extensions/DbExtensions.cs    |  12 +
 .../Db/Extensions/DiscordUserExtensions.cs    | 179 +++++++
 .../Extensions/EllieExpressionExtensions.cs   |  15 +
 .../Db/Extensions/GuildConfigExtensions.cs    | 227 ++++++++
 .../MusicPlayerSettingsExtensions.cs          |  27 +
 .../Db/Extensions/MusicPlaylistExtensions.cs  |  19 +
 src/EllieBot/Db/Extensions/PollExtensions.cs  |  28 +
 src/EllieBot/Db/Extensions/QuoteExtensions.cs |  57 ++
 .../Db/Extensions/ReminderExtensions.cs       |  23 +
 .../SelfAssignableRolesExtensions.cs          |  22 +
 .../Db/Extensions/UserXpExtensions.cs         |  63 +++
 src/EllieBot/Db/Extensions/WaifuExtensions.cs | 145 ++++++
 .../Db/Extensions/WarningExtensions.cs        |  60 +++
 src/EllieBot/Db/Models/AntiProtection.cs      |  65 +++
 src/EllieBot/Db/Models/AutoCommand.cs         |  14 +
 src/EllieBot/Db/Models/AutoPublishChannel.cs  |   9 +
 .../Db/Models/AutoTranslateChannel.cs         |  10 +
 src/EllieBot/Db/Models/AutoTranslateUser.cs   |  11 +
 src/EllieBot/Db/Models/BanTemplate.cs         |   9 +
 src/EllieBot/Db/Models/BankUser.cs            |   9 +
 src/EllieBot/Db/Models/BlacklistEntry.cs      |  14 +
 src/EllieBot/Db/Models/ClubInfo.cs            |  42 ++
 src/EllieBot/Db/Models/CommandAlias.cs        |   8 +
 src/EllieBot/Db/Models/CommandCooldown.cs     |   8 +
 src/EllieBot/Db/Models/CurrencyTransaction.cs |  12 +
 src/EllieBot/Db/Models/DbEntity.cs            |  12 +
 src/EllieBot/Db/Models/DelMsgOnCmdChannel.cs  |  14 +
 src/EllieBot/Db/Models/DiscordPermOverride.cs |  10 +
 src/EllieBot/Db/Models/DiscordUser.cs         |  31 ++
 src/EllieBot/Db/Models/EllieExpression.cs     |  26 +
 src/EllieBot/Db/Models/FeedSub.cs             |  19 +
 src/EllieBot/Db/Models/FilterChannelId.cs     |  30 ++
 .../Db/Models/FilterLinksChannelId.cs         |  13 +
 src/EllieBot/Db/Models/FilteredWord.cs        |   7 +
 src/EllieBot/Db/Models/FollowedStream.cs      |  37 ++
 src/EllieBot/Db/Models/GCChannelId.cs         |  14 +
 src/EllieBot/Db/Models/GamblingStats.cs       |   9 +
 src/EllieBot/Db/Models/GroupName.cs           |  11 +
 src/EllieBot/Db/Models/GuildConfig.cs         | 108 ++++
 src/EllieBot/Db/Models/IgnoredLogItem.cs      |  16 +
 .../Db/Models/IgnoredVoicePresenceChannel.cs  |   8 +
 src/EllieBot/Db/Models/ImageOnlyChannel.cs    |  15 +
 src/EllieBot/Db/Models/LogSetting.cs          |  37 ++
 src/EllieBot/Db/Models/MusicPlaylist.cs       |  10 +
 src/EllieBot/Db/Models/MusicSettings.cs       |  61 +++
 src/EllieBot/Db/Models/MutedUserId.cs         |  13 +
 src/EllieBot/Db/Models/NsfwBlacklistedTag.cs  |  14 +
 src/EllieBot/Db/Models/PatronQuota.cs         |  48 ++
 src/EllieBot/Db/Models/Permission.cs          |  56 ++
 src/EllieBot/Db/Models/PlantedCurrency.cs     |  12 +
 src/EllieBot/Db/Models/PlaylistSong.cs        |  19 +
 src/EllieBot/Db/Models/Poll.cs                |  17 +
 src/EllieBot/Db/Models/PollVote.cs            |  14 +
 src/EllieBot/Db/Models/Quote.cs               |  26 +
 src/EllieBot/Db/Models/ReactionRole.cs        |  18 +
 src/EllieBot/Db/Models/Reminder.cs            |  12 +
 src/EllieBot/Db/Models/Repeater.cs            |  15 +
 src/EllieBot/Db/Models/RewardedUser.cs        |  10 +
 .../Db/Models/RotatingPlayingStatus.cs        |   8 +
 src/EllieBot/Db/Models/SelfAssignableRole.cs  |  11 +
 src/EllieBot/Db/Models/ShopEntry.cs           |  43 ++
 src/EllieBot/Db/Models/SlowmodeIgnoredRole.cs |  20 +
 src/EllieBot/Db/Models/SlowmodeIgnoredUser.cs |  20 +
 src/EllieBot/Db/Models/StreamOnlineMessage.cs |  13 +
 src/EllieBot/Db/Models/StreamRoleSettings.cs  |  68 +++
 src/EllieBot/Db/Models/UnbanTimer.cs          |  14 +
 src/EllieBot/Db/Models/UnmuteTimer.cs         |  14 +
 src/EllieBot/Db/Models/UnroleTimer.cs         |  15 +
 src/EllieBot/Db/Models/UserXpStats.cs         |  13 +
 src/EllieBot/Db/Models/VcRoleInfo.cs          |   8 +
 src/EllieBot/Db/Models/Waifu.cs               |  75 +++
 src/EllieBot/Db/Models/WaifuItem.cs           |  10 +
 src/EllieBot/Db/Models/WaifuUpdate.cs         |  23 +
 src/EllieBot/Db/Models/WarnExpireAction.cs    |   8 +
 src/EllieBot/Db/Models/Warning.cs             |  13 +
 src/EllieBot/Db/Models/WarningPunishment.cs   |  10 +
 src/EllieBot/Db/Models/XpSettings.cs          |  62 +++
 src/EllieBot/Db/Models/XpShotOwnedItem.cs     |  18 +
 src/EllieBot/Db/MysqlContext.cs               |  38 ++
 src/EllieBot/Db/PostgreSqlContext.cs          |  26 +
 src/EllieBot/Db/SqliteContext.cs              |  26 +
 84 files changed, 2951 insertions(+)
 create mode 100644 src/EllieBot/Db/EllieContext.cs
 create mode 100644 src/EllieBot/Db/Extensions/ClubExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/CurrencyTransactionExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/DbExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/DiscordUserExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/EllieExpressionExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/GuildConfigExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/MusicPlayerSettingsExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/MusicPlaylistExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/PollExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/QuoteExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/ReminderExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/SelfAssignableRolesExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/UserXpExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/WaifuExtensions.cs
 create mode 100644 src/EllieBot/Db/Extensions/WarningExtensions.cs
 create mode 100644 src/EllieBot/Db/Models/AntiProtection.cs
 create mode 100644 src/EllieBot/Db/Models/AutoCommand.cs
 create mode 100644 src/EllieBot/Db/Models/AutoPublishChannel.cs
 create mode 100644 src/EllieBot/Db/Models/AutoTranslateChannel.cs
 create mode 100644 src/EllieBot/Db/Models/AutoTranslateUser.cs
 create mode 100644 src/EllieBot/Db/Models/BanTemplate.cs
 create mode 100644 src/EllieBot/Db/Models/BankUser.cs
 create mode 100644 src/EllieBot/Db/Models/BlacklistEntry.cs
 create mode 100644 src/EllieBot/Db/Models/ClubInfo.cs
 create mode 100644 src/EllieBot/Db/Models/CommandAlias.cs
 create mode 100644 src/EllieBot/Db/Models/CommandCooldown.cs
 create mode 100644 src/EllieBot/Db/Models/CurrencyTransaction.cs
 create mode 100644 src/EllieBot/Db/Models/DbEntity.cs
 create mode 100644 src/EllieBot/Db/Models/DelMsgOnCmdChannel.cs
 create mode 100644 src/EllieBot/Db/Models/DiscordPermOverride.cs
 create mode 100644 src/EllieBot/Db/Models/DiscordUser.cs
 create mode 100644 src/EllieBot/Db/Models/EllieExpression.cs
 create mode 100644 src/EllieBot/Db/Models/FeedSub.cs
 create mode 100644 src/EllieBot/Db/Models/FilterChannelId.cs
 create mode 100644 src/EllieBot/Db/Models/FilterLinksChannelId.cs
 create mode 100644 src/EllieBot/Db/Models/FilteredWord.cs
 create mode 100644 src/EllieBot/Db/Models/FollowedStream.cs
 create mode 100644 src/EllieBot/Db/Models/GCChannelId.cs
 create mode 100644 src/EllieBot/Db/Models/GamblingStats.cs
 create mode 100644 src/EllieBot/Db/Models/GroupName.cs
 create mode 100644 src/EllieBot/Db/Models/GuildConfig.cs
 create mode 100644 src/EllieBot/Db/Models/IgnoredLogItem.cs
 create mode 100644 src/EllieBot/Db/Models/IgnoredVoicePresenceChannel.cs
 create mode 100644 src/EllieBot/Db/Models/ImageOnlyChannel.cs
 create mode 100644 src/EllieBot/Db/Models/LogSetting.cs
 create mode 100644 src/EllieBot/Db/Models/MusicPlaylist.cs
 create mode 100644 src/EllieBot/Db/Models/MusicSettings.cs
 create mode 100644 src/EllieBot/Db/Models/MutedUserId.cs
 create mode 100644 src/EllieBot/Db/Models/NsfwBlacklistedTag.cs
 create mode 100644 src/EllieBot/Db/Models/PatronQuota.cs
 create mode 100644 src/EllieBot/Db/Models/Permission.cs
 create mode 100644 src/EllieBot/Db/Models/PlantedCurrency.cs
 create mode 100644 src/EllieBot/Db/Models/PlaylistSong.cs
 create mode 100644 src/EllieBot/Db/Models/Poll.cs
 create mode 100644 src/EllieBot/Db/Models/PollVote.cs
 create mode 100644 src/EllieBot/Db/Models/Quote.cs
 create mode 100644 src/EllieBot/Db/Models/ReactionRole.cs
 create mode 100644 src/EllieBot/Db/Models/Reminder.cs
 create mode 100644 src/EllieBot/Db/Models/Repeater.cs
 create mode 100644 src/EllieBot/Db/Models/RewardedUser.cs
 create mode 100644 src/EllieBot/Db/Models/RotatingPlayingStatus.cs
 create mode 100644 src/EllieBot/Db/Models/SelfAssignableRole.cs
 create mode 100644 src/EllieBot/Db/Models/ShopEntry.cs
 create mode 100644 src/EllieBot/Db/Models/SlowmodeIgnoredRole.cs
 create mode 100644 src/EllieBot/Db/Models/SlowmodeIgnoredUser.cs
 create mode 100644 src/EllieBot/Db/Models/StreamOnlineMessage.cs
 create mode 100644 src/EllieBot/Db/Models/StreamRoleSettings.cs
 create mode 100644 src/EllieBot/Db/Models/UnbanTimer.cs
 create mode 100644 src/EllieBot/Db/Models/UnmuteTimer.cs
 create mode 100644 src/EllieBot/Db/Models/UnroleTimer.cs
 create mode 100644 src/EllieBot/Db/Models/UserXpStats.cs
 create mode 100644 src/EllieBot/Db/Models/VcRoleInfo.cs
 create mode 100644 src/EllieBot/Db/Models/Waifu.cs
 create mode 100644 src/EllieBot/Db/Models/WaifuItem.cs
 create mode 100644 src/EllieBot/Db/Models/WaifuUpdate.cs
 create mode 100644 src/EllieBot/Db/Models/WarnExpireAction.cs
 create mode 100644 src/EllieBot/Db/Models/Warning.cs
 create mode 100644 src/EllieBot/Db/Models/WarningPunishment.cs
 create mode 100644 src/EllieBot/Db/Models/XpSettings.cs
 create mode 100644 src/EllieBot/Db/Models/XpShotOwnedItem.cs
 create mode 100644 src/EllieBot/Db/MysqlContext.cs
 create mode 100644 src/EllieBot/Db/PostgreSqlContext.cs
 create mode 100644 src/EllieBot/Db/SqliteContext.cs

diff --git a/src/EllieBot/Db/EllieContext.cs b/src/EllieBot/Db/EllieContext.cs
new file mode 100644
index 0000000..f995daa
--- /dev/null
+++ b/src/EllieBot/Db/EllieContext.cs
@@ -0,0 +1,491 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using EllieBot.Db.Models;
+using EllieBot.Services.Database.Models;
+
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+
+namespace EllieBot.Services.Database;
+
+public abstract class EllieContext : DbContext
+{
+    public DbSet<GuildConfig> GuildConfigs { get; set; }
+    
+    public DbSet<Quote> Quotes { get; set; }
+    public DbSet<Reminder> Reminders { get; set; }
+    public DbSet<SelfAssignedRole> SelfAssignableRoles { get; set; }
+    public DbSet<MusicPlaylist> MusicPlaylists { get; set; }
+    public DbSet<EllieExpression> Expressions { get; set; }
+    public DbSet<CurrencyTransaction> CurrencyTransactions { get; set; }
+    public DbSet<WaifuUpdate> WaifuUpdates { get; set; }
+    public DbSet<WaifuItem> WaifuItem { get; set; }
+    public DbSet<Warning> Warnings { get; set; }
+    public DbSet<UserXpStats> UserXpStats { get; set; }
+    public DbSet<ClubInfo> Clubs { get; set; }
+    public DbSet<ClubBans> ClubBans { get; set; }
+    public DbSet<ClubApplicants> ClubApplicants { get; set; }
+
+
+    //logging
+    public DbSet<LogSetting> LogSettings { get; set; }
+    public DbSet<IgnoredVoicePresenceChannel> IgnoredVoicePresenceCHannels { get; set; }
+    public DbSet<IgnoredLogItem> IgnoredLogChannels { get; set; }
+
+    public DbSet<RotatingPlayingStatus> RotatingStatus { get; set; }
+    public DbSet<BlacklistEntry> Blacklist { get; set; }
+    public DbSet<AutoCommand> AutoCommands { get; set; }
+    public DbSet<RewardedUser> RewardedUsers { get; set; }
+    public DbSet<PlantedCurrency> PlantedCurrency { get; set; }
+    public DbSet<BanTemplate> BanTemplates { get; set; }
+    public DbSet<DiscordPermOverride> DiscordPermOverrides { get; set; }
+    public DbSet<DiscordUser> DiscordUser { get; set; }
+    public DbSet<MusicPlayerSettings> MusicPlayerSettings { get; set; }
+    public DbSet<Repeater> Repeaters { get; set; }
+    public DbSet<Poll> Poll { get; set; }
+    public DbSet<WaifuInfo> WaifuInfo { get; set; }
+    public DbSet<ImageOnlyChannel> ImageOnlyChannels { get; set; }
+    public DbSet<NsfwBlacklistedTag> NsfwBlacklistedTags { get; set; }
+    public DbSet<AutoTranslateChannel> AutoTranslateChannels { get; set; }
+    public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; }
+
+    public DbSet<Permissionv2> Permissions { get; set; }
+
+    public DbSet<BankUser> BankUsers { get; set; }
+
+    public DbSet<ReactionRoleV2> ReactionRoles { get; set; }
+
+    public DbSet<PatronUser> Patrons { get; set; }
+
+    public DbSet<PatronQuota> PatronQuotas { get; set; }
+    
+    public DbSet<StreamOnlineMessage> StreamOnlineMessages { get; set; }
+    
+    
+    #region Mandatory Provider-Specific Values
+
+    protected abstract string CurrencyTransactionOtherIdDefaultValue { get; }
+
+    #endregion
+
+    protected override void OnModelCreating(ModelBuilder modelBuilder)
+    {
+        #region QUOTES
+
+        var quoteEntity = modelBuilder.Entity<Quote>();
+        quoteEntity.HasIndex(x => x.GuildId);
+        quoteEntity.HasIndex(x => x.Keyword);
+
+        #endregion
+
+        #region GuildConfig
+
+        var configEntity = modelBuilder.Entity<GuildConfig>();
+        configEntity.HasIndex(c => c.GuildId)
+                    .IsUnique();
+
+        configEntity.Property(x => x.VerboseErrors)
+                    .HasDefaultValue(true);
+
+        modelBuilder.Entity<AntiSpamSetting>().HasOne(x => x.GuildConfig).WithOne(x => x.AntiSpamSetting);
+
+        modelBuilder.Entity<AntiRaidSetting>().HasOne(x => x.GuildConfig).WithOne(x => x.AntiRaidSetting);
+
+        modelBuilder.Entity<GuildConfig>()
+                    .HasOne(x => x.AntiAltSetting)
+                    .WithOne()
+                    .HasForeignKey<AntiAltSetting>(x => x.GuildConfigId)
+                    .OnDelete(DeleteBehavior.Cascade);
+
+        modelBuilder.Entity<FeedSub>()
+                    .HasAlternateKey(x => new
+                    {
+                        x.GuildConfigId,
+                        x.Url
+                    });
+
+        modelBuilder.Entity<PlantedCurrency>().HasIndex(x => x.MessageId).IsUnique();
+
+        modelBuilder.Entity<PlantedCurrency>().HasIndex(x => x.ChannelId);
+
+        configEntity.HasIndex(x => x.WarnExpireHours).IsUnique(false);
+
+        #endregion
+
+        #region streamrole
+
+        modelBuilder.Entity<StreamRoleSettings>().HasOne(x => x.GuildConfig).WithOne(x => x.StreamRole);
+
+        #endregion
+
+        #region Self Assignable Roles
+
+        var selfassignableRolesEntity = modelBuilder.Entity<SelfAssignedRole>();
+
+        selfassignableRolesEntity.HasIndex(s => new
+                                 {
+                                     s.GuildId,
+                                     s.RoleId
+                                 })
+                                 .IsUnique();
+
+        selfassignableRolesEntity.Property(x => x.Group).HasDefaultValue(0);
+
+        #endregion
+
+        #region MusicPlaylists
+
+        var musicPlaylistEntity = modelBuilder.Entity<MusicPlaylist>();
+
+        musicPlaylistEntity.HasMany(p => p.Songs).WithOne().OnDelete(DeleteBehavior.Cascade);
+
+        #endregion
+
+        #region Waifus
+
+        var wi = modelBuilder.Entity<WaifuInfo>();
+        wi.HasOne(x => x.Waifu).WithOne();
+
+        wi.HasIndex(x => x.Price);
+        wi.HasIndex(x => x.ClaimerId);
+        // wi.HasMany(x => x.Items)
+        //     .WithOne()
+        //     .OnDelete(DeleteBehavior.Cascade);
+
+        #endregion
+
+        #region DiscordUser
+
+        modelBuilder.Entity<DiscordUser>(du =>
+        {
+            du.Property(x => x.IsClubAdmin)
+              .HasDefaultValue(false);
+
+            du.Property(x => x.NotifyOnLevelUp)
+              .HasDefaultValue(XpNotificationLocation.None);
+
+            du.Property(x => x.TotalXp)
+              .HasDefaultValue(0);
+
+            du.Property(x => x.CurrencyAmount)
+              .HasDefaultValue(0);
+
+            du.HasAlternateKey(w => w.UserId);
+            du.HasOne(x => x.Club)
+              .WithMany(x => x.Members)
+              .IsRequired(false)
+              .OnDelete(DeleteBehavior.NoAction);
+
+            du.HasIndex(x => x.TotalXp);
+            du.HasIndex(x => x.CurrencyAmount);
+            du.HasIndex(x => x.UserId);
+        });
+
+        #endregion
+
+        #region Warnings
+
+        modelBuilder.Entity<Warning>(warn =>
+        {
+            warn.HasIndex(x => x.GuildId);
+            warn.HasIndex(x => x.UserId);
+            warn.HasIndex(x => x.DateAdded);
+            warn.Property(x => x.Weight).HasDefaultValue(1);
+        });
+
+        #endregion
+
+        #region XpStats
+
+        var xps = modelBuilder.Entity<UserXpStats>();
+        xps.HasIndex(x => new
+           {
+               x.UserId,
+               x.GuildId
+           })
+           .IsUnique();
+
+        xps.HasIndex(x => x.UserId);
+        xps.HasIndex(x => x.GuildId);
+        xps.HasIndex(x => x.Xp);
+        xps.HasIndex(x => x.AwardedXp);
+
+        #endregion
+
+        #region XpSettings
+
+        modelBuilder.Entity<XpSettings>().HasOne(x => x.GuildConfig).WithOne(x => x.XpSettings);
+
+        #endregion
+
+        #region XpRoleReward
+
+        modelBuilder.Entity<XpRoleReward>()
+                    .HasIndex(x => new
+                    {
+                        x.XpSettingsId,
+                        x.Level
+                    })
+                    .IsUnique();
+
+        #endregion
+
+        #region Club
+
+        var ci = modelBuilder.Entity<ClubInfo>();
+        ci.HasOne(x => x.Owner)
+          .WithOne()
+          .HasForeignKey<ClubInfo>(x => x.OwnerId)
+          .OnDelete(DeleteBehavior.SetNull);
+
+        ci.HasAlternateKey(x => new
+        {
+            x.Name
+        });
+
+        #endregion
+
+        #region ClubManytoMany
+
+        modelBuilder.Entity<ClubApplicants>()
+                    .HasKey(t => new
+                    {
+                        t.ClubId,
+                        t.UserId
+                    });
+
+        modelBuilder.Entity<ClubApplicants>()
+                    .HasOne(pt => pt.User)
+                    .WithMany();
+
+        modelBuilder.Entity<ClubApplicants>()
+                    .HasOne(pt => pt.Club)
+                    .WithMany(x => x.Applicants);
+
+        modelBuilder.Entity<ClubBans>()
+                    .HasKey(t => new
+                    {
+                        t.ClubId,
+                        t.UserId
+                    });
+
+        modelBuilder.Entity<ClubBans>()
+                    .HasOne(pt => pt.User)
+                    .WithMany();
+
+        modelBuilder.Entity<ClubBans>()
+                    .HasOne(pt => pt.Club)
+                    .WithMany(x => x.Bans);
+
+        #endregion
+
+        #region Polls
+
+        modelBuilder.Entity<Poll>().HasIndex(x => x.GuildId).IsUnique();
+
+        #endregion
+
+        #region CurrencyTransactions
+
+        modelBuilder.Entity<CurrencyTransaction>(e =>
+        {
+            e.HasIndex(x => x.UserId)
+             .IsUnique(false);
+
+            e.Property(x => x.OtherId)
+             .HasDefaultValueSql(CurrencyTransactionOtherIdDefaultValue);
+
+            e.Property(x => x.Type)
+             .IsRequired();
+
+            e.Property(x => x.Extra)
+             .IsRequired();
+        });
+
+        #endregion
+
+        #region Reminders
+
+        modelBuilder.Entity<Reminder>().HasIndex(x => x.When);
+
+        #endregion
+
+        #region GroupName
+
+        modelBuilder.Entity<GroupName>()
+                    .HasIndex(x => new
+                    {
+                        x.GuildConfigId,
+                        x.Number
+                    })
+                    .IsUnique();
+
+        modelBuilder.Entity<GroupName>()
+                    .HasOne(x => x.GuildConfig)
+                    .WithMany(x => x.SelfAssignableRoleGroupNames)
+                    .IsRequired();
+
+        #endregion
+
+        #region BanTemplate
+
+        modelBuilder.Entity<BanTemplate>().HasIndex(x => x.GuildId).IsUnique();
+        modelBuilder.Entity<BanTemplate>()
+            .Property(x => x.PruneDays)
+            .HasDefaultValue(null)
+            .IsRequired(false);
+
+        #endregion
+
+        #region Perm Override
+
+        modelBuilder.Entity<DiscordPermOverride>()
+                    .HasIndex(x => new
+                    {
+                        x.GuildId,
+                        x.Command
+                    })
+                    .IsUnique();
+
+        #endregion
+
+        #region Music
+
+        modelBuilder.Entity<MusicPlayerSettings>().HasIndex(x => x.GuildId).IsUnique();
+
+        modelBuilder.Entity<MusicPlayerSettings>().Property(x => x.Volume).HasDefaultValue(100);
+
+        #endregion
+
+        #region Reaction roles
+
+        modelBuilder.Entity<ReactionRoleV2>(rr2 =>
+        {
+            rr2.HasIndex(x => x.GuildId)
+               .IsUnique(false);
+
+            rr2.HasIndex(x => new
+               {
+                   x.MessageId,
+                   x.Emote
+               })
+               .IsUnique();
+        });
+
+        #endregion
+
+        #region LogSettings
+
+        modelBuilder.Entity<LogSetting>(ls => ls.HasIndex(x => x.GuildId).IsUnique());
+
+        modelBuilder.Entity<LogSetting>(ls => ls
+                                              .HasMany(x => x.LogIgnores)
+                                              .WithOne(x => x.LogSetting)
+                                              .OnDelete(DeleteBehavior.Cascade));
+
+        modelBuilder.Entity<IgnoredLogItem>(ili => ili
+                                                   .HasIndex(x => new
+                                                   {
+                                                       x.LogSettingId,
+                                                       x.LogItemId,
+                                                       x.ItemType
+                                                   })
+                                                   .IsUnique());
+
+        #endregion
+
+        modelBuilder.Entity<ImageOnlyChannel>(ioc => ioc.HasIndex(x => x.ChannelId).IsUnique());
+
+        modelBuilder.Entity<NsfwBlacklistedTag>(nbt => nbt.HasIndex(x => x.GuildId).IsUnique(false));
+
+        var atch = modelBuilder.Entity<AutoTranslateChannel>();
+        atch.HasIndex(x => x.GuildId).IsUnique(false);
+
+        atch.HasIndex(x => x.ChannelId).IsUnique();
+
+        atch.HasMany(x => x.Users).WithOne(x => x.Channel).OnDelete(DeleteBehavior.Cascade);
+
+        modelBuilder.Entity<AutoTranslateUser>(atu => atu.HasAlternateKey(x => new
+        {
+            x.ChannelId,
+            x.UserId
+        }));
+
+        #region BANK
+
+        modelBuilder.Entity<BankUser>(bu => bu.HasIndex(x => x.UserId).IsUnique());
+
+        #endregion
+
+
+        #region Patron
+
+        // currency rewards
+        var pr = modelBuilder.Entity<RewardedUser>();
+        pr.HasIndex(x => x.PlatformUserId).IsUnique();
+
+        // patrons
+        // patrons are not identified by their user id, but by their platform user id
+        // as multiple accounts (even maybe on different platforms) could have
+        // the same account connected to them
+        modelBuilder.Entity<PatronUser>(pu =>
+        {
+            pu.HasIndex(x => x.UniquePlatformUserId).IsUnique();
+            pu.HasKey(x => x.UserId);
+        });
+
+        // quotes are per user id
+        modelBuilder.Entity<PatronQuota>(pq =>
+        {
+            pq.HasIndex(x => x.UserId).IsUnique(false);
+            pq.HasKey(x => new
+            {
+                x.UserId,
+                x.FeatureType,
+                x.Feature
+            });
+        });
+
+        #endregion
+ 
+        #region Xp Item Shop
+
+        modelBuilder.Entity<XpShopOwnedItem>(
+            x =>
+            {
+                // user can own only one of each item
+                x.HasIndex(model => new
+                    {
+                        model.UserId,
+                        model.ItemType,
+                        model.ItemKey
+                    })
+                    .IsUnique();
+            });
+
+        #endregion
+        
+        #region AutoPublish
+
+        modelBuilder.Entity<AutoPublishChannel>(apc => apc
+            .HasIndex(x => x.GuildId)
+            .IsUnique());
+
+        #endregion
+        
+        #region GamblingStats
+
+        modelBuilder.Entity<GamblingStats>(gs => gs
+            .HasIndex(x => x.Feature)
+            .IsUnique());
+
+        #endregion
+    }
+
+#if DEBUG
+    private static readonly ILoggerFactory _debugLoggerFactory = LoggerFactory.Create(x => x.AddConsole());
+
+    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+        => optionsBuilder.UseLoggerFactory(_debugLoggerFactory);
+#endif
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/ClubExtensions.cs b/src/EllieBot/Db/Extensions/ClubExtensions.cs
new file mode 100644
index 0000000..d8183fc
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/ClubExtensions.cs
@@ -0,0 +1,34 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Db.Models;
+
+namespace EllieBot.Db;
+
+public static class ClubExtensions
+{
+    private static IQueryable<ClubInfo> Include(this DbSet<ClubInfo> clubs)
+        => clubs.Include(x => x.Owner)
+                .Include(x => x.Applicants)
+                .ThenInclude(x => x.User)
+                .Include(x => x.Bans)
+                .ThenInclude(x => x.User)
+                .Include(x => x.Members)
+                .AsQueryable();
+
+    public static ClubInfo GetByOwner(this DbSet<ClubInfo> clubs, ulong userId)
+        => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId);
+
+    public static ClubInfo GetByOwnerOrAdmin(this DbSet<ClubInfo> clubs, ulong userId)
+        => Include(clubs)
+            .FirstOrDefault(c => c.Owner.UserId == userId || c.Members.Any(u => u.UserId == userId && u.IsClubAdmin));
+
+    public static ClubInfo GetByMember(this DbSet<ClubInfo> clubs, ulong userId)
+        => Include(clubs).FirstOrDefault(c => c.Members.Any(u => u.UserId == userId));
+
+    public static ClubInfo GetByName(this DbSet<ClubInfo> clubs, string name)
+        => Include(clubs)
+            .FirstOrDefault(c => c.Name == name);
+
+    public static List<ClubInfo> GetClubLeaderboardPage(this DbSet<ClubInfo> clubs, int page)
+        => clubs.AsNoTracking().OrderByDescending(x => x.Xp).Skip(page * 9).Take(9).ToList();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/CurrencyTransactionExtensions.cs b/src/EllieBot/Db/Extensions/CurrencyTransactionExtensions.cs
new file mode 100644
index 0000000..c6659ca
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/CurrencyTransactionExtensions.cs
@@ -0,0 +1,20 @@
+#nullable disable
+using LinqToDB.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class CurrencyTransactionExtensions
+{
+    public static Task<List<CurrencyTransaction>> GetPageFor(
+        this DbSet<CurrencyTransaction> set,
+        ulong userId,
+        int page)
+        => set.ToLinqToDBTable()
+              .Where(x => x.UserId == userId)
+              .OrderByDescending(x => x.DateAdded)
+              .Skip(15 * page)
+              .Take(15)
+              .ToListAsyncLinqToDB();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/DbExtensions.cs b/src/EllieBot/Db/Extensions/DbExtensions.cs
new file mode 100644
index 0000000..6878b31
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/DbExtensions.cs
@@ -0,0 +1,12 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class DbExtensions
+{
+    public static T GetById<T>(this DbSet<T> set, int id)
+        where T : DbEntity
+        => set.FirstOrDefault(x => x.Id == id);
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/DiscordUserExtensions.cs b/src/EllieBot/Db/Extensions/DiscordUserExtensions.cs
new file mode 100644
index 0000000..fc165c5
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/DiscordUserExtensions.cs
@@ -0,0 +1,179 @@
+#nullable disable
+using LinqToDB;
+using LinqToDB.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Db.Models;
+using EllieBot.Services.Database;
+using System.Collections.Immutable;
+
+namespace EllieBot.Db;
+
+public static class DiscordUserExtensions
+{
+    /// <summary>
+    /// Adds the specified <paramref name="users"/> to the database. If a database user with placeholder name
+    /// and discriminator is present in <paramref name="users"/>, their name and discriminator get updated accordingly.
+    /// </summary>
+    /// <param name="ctx">This database context.</param>
+    /// <param name="users">The users to add or update in the database.</param>
+    /// <returns>A tuple with the amount of new users added and old users updated.</returns>
+    public static async Task<(long UsersAdded, long UsersUpdated)> RefreshUsersAsync(this EllieContext ctx, List<IUser> users)
+    {
+        var presentDbUsers = await ctx.DiscordUser
+            .Select(x => new { x.UserId, x.Username, x.Discriminator })
+            .Where(x => users.Select(y => y.Id).Contains(x.UserId))
+            .ToArrayAsyncEF();
+
+        var usersToAdd = users
+            .Where(x => !presentDbUsers.Select(x => x.UserId).Contains(x.Id))
+            .Select(x => new DiscordUser()
+            {
+                UserId = x.Id,
+                AvatarId = x.AvatarId,
+                Username = x.Username,
+                Discriminator = x.Discriminator
+            });
+
+        var added = (await ctx.BulkCopyAsync(usersToAdd)).RowsCopied;
+        var toUpdateUserIds = presentDbUsers
+            .Where(x => x.Username == "Unknown" && x.Discriminator == "????")
+            .Select(x => x.UserId)
+            .ToArray();
+
+        foreach (var user in users.Where(x => toUpdateUserIds.Contains(x.Id)))
+        {
+            await ctx.DiscordUser
+                .Where(x => x.UserId == user.Id)
+                .UpdateAsync(x => new DiscordUser()
+                {
+                    Username = user.Username,
+                    Discriminator = user.Discriminator,
+
+                    // .award tends to set AvatarId and DateAdded to NULL, so account for that.
+                    AvatarId = user.AvatarId,
+                    DateAdded = x.DateAdded ?? DateTime.UtcNow
+                });
+        }
+
+        return (added, toUpdateUserIds.Length);
+    }
+
+    public static Task<DiscordUser> GetByUserIdAsync(
+        this IQueryable<DiscordUser> set,
+        ulong userId)
+        => set.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId);
+    
+    public static void EnsureUserCreated(
+        this EllieContext ctx,
+        ulong userId,
+        string username,
+        string discrim,
+        string avatarId)
+        => ctx.DiscordUser.ToLinqToDBTable()
+              .InsertOrUpdate(
+                  () => new()
+                  {
+                      UserId = userId,
+                      Username = username,
+                      Discriminator = discrim,
+                      AvatarId = avatarId,
+                      TotalXp = 0,
+                      CurrencyAmount = 0
+                  },
+                  old => new()
+                  {
+                      Username = username,
+                      Discriminator = discrim,
+                      AvatarId = avatarId
+                  },
+                  () => new()
+                  {
+                      UserId = userId
+                  });
+
+    public static Task EnsureUserCreatedAsync(
+        this EllieContext ctx,
+        ulong userId)
+        => ctx.DiscordUser
+              .ToLinqToDBTable()
+              .InsertOrUpdateAsync(
+                  () => new()
+                  {
+                      UserId = userId,
+                      Username = "Unknown",
+                      Discriminator = "????",
+                      AvatarId = string.Empty,
+                      TotalXp = 0,
+                      CurrencyAmount = 0
+                  },
+                  old => new()
+                  {
+
+                  },
+                  () => new()
+                  {
+                      UserId = userId
+                  });
+    
+    //temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown
+    public static DiscordUser GetOrCreateUser(
+        this EllieContext ctx,
+        ulong userId,
+        string username,
+        string discrim,
+        string avatarId,
+        Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>> includes = null)
+    {
+        ctx.EnsureUserCreated(userId, username, discrim, avatarId);
+
+        IQueryable<DiscordUser> queryable = ctx.DiscordUser;
+        if (includes is not null)
+            queryable = includes(queryable);
+        return queryable.First(u => u.UserId == userId);
+    }
+
+    public static DiscordUser GetOrCreateUser(this EllieContext ctx, IUser original, Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>> includes = null)
+        => ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId, includes);
+
+    public static int GetUserGlobalRank(this DbSet<DiscordUser> users, ulong id)
+        => users.AsQueryable()
+                .Where(x => x.TotalXp
+                            > users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault())
+                .Count()
+           + 1;
+
+    public static DiscordUser[] GetUsersXpLeaderboardFor(this DbSet<DiscordUser> users, int page)
+        => users.AsQueryable().OrderByDescending(x => x.TotalXp).Skip(page * 9).Take(9).AsEnumerable().ToArray();
+
+    public static List<DiscordUser> GetTopRichest(
+        this DbSet<DiscordUser> users,
+        ulong botId,
+        int count,
+        int page = 0)
+        => users.AsQueryable()
+                .Where(c => c.CurrencyAmount > 0 && botId != c.UserId)
+                .OrderByDescending(c => c.CurrencyAmount)
+                .Skip(page * 9)
+                .Take(count)
+                .ToList();
+
+    public static async Task<long> GetUserCurrencyAsync(this DbSet<DiscordUser> users, ulong userId)
+        => (await users.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId))?.CurrencyAmount ?? 0;
+
+    public static void RemoveFromMany(this DbSet<DiscordUser> users, IEnumerable<ulong> ids)
+    {
+        var items = users.AsQueryable().Where(x => ids.Contains(x.UserId));
+        foreach (var item in items)
+            item.CurrencyAmount = 0;
+    }
+
+    public static decimal GetTotalCurrency(this DbSet<DiscordUser> users)
+        => users.Sum((Func<DiscordUser, decimal>)(x => x.CurrencyAmount));
+
+    public static decimal GetTopOnePercentCurrency(this DbSet<DiscordUser> users, ulong botId)
+        => users.AsQueryable()
+                .Where(x => x.UserId != botId)
+                .OrderByDescending(x => x.CurrencyAmount)
+                .Take(users.Count() / 100 == 0 ? 1 : users.Count() / 100)
+                .Sum(x => x.CurrencyAmount);
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/EllieExpressionExtensions.cs b/src/EllieBot/Db/Extensions/EllieExpressionExtensions.cs
new file mode 100644
index 0000000..7eaf707
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/EllieExpressionExtensions.cs
@@ -0,0 +1,15 @@
+#nullable disable
+using LinqToDB;
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class EllieExpressionExtensions
+{
+    public static int ClearFromGuild(this DbSet<EllieExpression> exprs, ulong guildId)
+        => exprs.Delete(x => x.GuildId == guildId);
+
+    public static IEnumerable<EllieExpression> ForId(this DbSet<EllieExpression> exprs, ulong id)
+        => exprs.AsNoTracking().AsQueryable().Where(x => x.GuildId == id).ToList();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs b/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs
new file mode 100644
index 0000000..2b6272a
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs
@@ -0,0 +1,227 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Db.Models;
+using EllieBot.Services.Database;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class GuildConfigExtensions
+{
+    private static List<WarningPunishment> DefaultWarnPunishments
+        => new()
+        {
+            new()
+            {
+                Count = 3,
+                Punishment = PunishmentAction.Kick
+            },
+            new()
+            {
+                Count = 5,
+                Punishment = PunishmentAction.Ban
+            }
+        };
+
+    /// <summary>
+    ///     Gets full stream role settings for the guild with the specified id.
+    /// </summary>
+    /// <param name="ctx">Db Context</param>
+    /// <param name="guildId">Id of the guild to get stream role settings for.</param>
+    /// <returns>Guild'p stream role settings</returns>
+    public static StreamRoleSettings GetStreamRoleSettings(this EllieContext ctx, ulong guildId)
+    {
+        var conf = ctx.GuildConfigsForId(guildId,
+            set => set.Include(y => y.StreamRole)
+                      .Include(y => y.StreamRole.Whitelist)
+                      .Include(y => y.StreamRole.Blacklist));
+
+        if (conf.StreamRole is null)
+            conf.StreamRole = new();
+
+        return conf.StreamRole;
+    }
+
+    private static IQueryable<GuildConfig> IncludeEverything(this DbSet<GuildConfig> configs)
+        => configs.AsQueryable()
+                  .AsSplitQuery()
+                  .Include(gc => gc.CommandCooldowns)
+                  .Include(gc => gc.FollowedStreams)
+                  .Include(gc => gc.StreamRole)
+                  .Include(gc => gc.XpSettings)
+                  .ThenInclude(x => x.ExclusionList)
+                  .Include(gc => gc.DelMsgOnCmdChannels);
+
+    public static IEnumerable<GuildConfig> GetAllGuildConfigs(
+        this DbSet<GuildConfig> configs,
+        List<ulong> availableGuilds)
+        => configs.IncludeEverything().AsNoTracking().Where(x => availableGuilds.Contains(x.GuildId)).ToList();
+
+    /// <summary>
+    ///     Gets and creates if it doesn't exist a config for a guild.
+    /// </summary>
+    /// <param name="ctx">Context</param>
+    /// <param name="guildId">Id of the guide</param>
+    /// <param name="includes">Use to manipulate the set however you want. Pass null to include everything</param>
+    /// <returns>Config for the guild</returns>
+    public static GuildConfig GuildConfigsForId(
+        this EllieContext ctx,
+        ulong guildId,
+        Func<DbSet<GuildConfig>, IQueryable<GuildConfig>> includes)
+    {
+        GuildConfig config;
+
+        if (includes is null)
+            config = ctx.GuildConfigs.IncludeEverything().FirstOrDefault(c => c.GuildId == guildId);
+        else
+        {
+            var set = includes(ctx.GuildConfigs);
+            config = set.FirstOrDefault(c => c.GuildId == guildId);
+        }
+
+        if (config is null)
+        {
+            ctx.GuildConfigs.Add(config = new()
+            {
+                GuildId = guildId,
+                Permissions = Permissionv2.GetDefaultPermlist,
+                WarningsInitialized = true,
+                WarnPunishments = DefaultWarnPunishments
+            });
+            ctx.SaveChanges();
+        }
+
+        if (!config.WarningsInitialized)
+        {
+            config.WarningsInitialized = true;
+            config.WarnPunishments = DefaultWarnPunishments;
+        }
+
+        return config;
+
+        // ctx.GuildConfigs
+        //    .ToLinqToDBTable()
+        //    .InsertOrUpdate(() => new()
+        //        {
+        //            GuildId = guildId,
+        //            Permissions = Permissionv2.GetDefaultPermlist,
+        //            WarningsInitialized = true,
+        //            WarnPunishments = DefaultWarnPunishments
+        //        },
+        //        _ => new(),
+        //        () => new()
+        //        {
+        //            GuildId = guildId
+        //        });
+        //
+        // if(includes is null)
+        // return ctx.GuildConfigs
+        //    .ToLinqToDBTable()
+        //    .First(x => x.GuildId == guildId);
+    }
+
+    public static LogSetting LogSettingsFor(this EllieContext ctx, ulong guildId)
+    {
+        var logSetting = ctx.LogSettings.AsQueryable()
+                            .Include(x => x.LogIgnores)
+                            .Where(x => x.GuildId == guildId)
+                            .FirstOrDefault();
+
+        if (logSetting is null)
+        {
+            ctx.LogSettings.Add(logSetting = new()
+            {
+                GuildId = guildId
+            });
+            ctx.SaveChanges();
+        }
+
+        return logSetting;
+    }
+
+    public static IEnumerable<GuildConfig> PermissionsForAll(this DbSet<GuildConfig> configs, List<ulong> include)
+    {
+        var query = configs.AsQueryable().Where(x => include.Contains(x.GuildId)).Include(gc => gc.Permissions);
+
+        return query.ToList();
+    }
+
+    public static GuildConfig GcWithPermissionsFor(this EllieContext ctx, ulong guildId)
+    {
+        var config = ctx.GuildConfigs.AsQueryable()
+                        .Where(gc => gc.GuildId == guildId)
+                        .Include(gc => gc.Permissions)
+                        .FirstOrDefault();
+
+        if (config is null) // if there is no guildconfig, create new one
+        {
+            ctx.GuildConfigs.Add(config = new()
+            {
+                GuildId = guildId,
+                Permissions = Permissionv2.GetDefaultPermlist
+            });
+            ctx.SaveChanges();
+        }
+        else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones
+        {
+            config.Permissions = Permissionv2.GetDefaultPermlist;
+            ctx.SaveChanges();
+        }
+
+        return config;
+    }
+
+    public static IEnumerable<FollowedStream> GetFollowedStreams(this DbSet<GuildConfig> configs)
+        => configs.AsQueryable().Include(x => x.FollowedStreams).SelectMany(gc => gc.FollowedStreams).ToArray();
+
+    public static IEnumerable<FollowedStream> GetFollowedStreams(this DbSet<GuildConfig> configs, List<ulong> included)
+        => configs.AsQueryable()
+                  .Where(gc => included.Contains(gc.GuildId))
+                  .Include(gc => gc.FollowedStreams)
+                  .SelectMany(gc => gc.FollowedStreams)
+                  .ToList();
+
+    public static void SetCleverbotEnabled(this DbSet<GuildConfig> configs, ulong id, bool cleverbotEnabled)
+    {
+        var conf = configs.FirstOrDefault(gc => gc.GuildId == id);
+
+        if (conf is null)
+            return;
+
+        conf.CleverbotEnabled = cleverbotEnabled;
+    }
+
+    public static XpSettings XpSettingsFor(this EllieContext ctx, ulong guildId)
+    {
+        var gc = ctx.GuildConfigsForId(guildId,
+            set => set.Include(x => x.XpSettings)
+                      .ThenInclude(x => x.RoleRewards)
+                      .Include(x => x.XpSettings)
+                      .ThenInclude(x => x.CurrencyRewards)
+                      .Include(x => x.XpSettings)
+                      .ThenInclude(x => x.ExclusionList));
+
+        if (gc.XpSettings is null)
+            gc.XpSettings = new();
+
+        return gc.XpSettings;
+    }
+
+    public static IEnumerable<GeneratingChannel> GetGeneratingChannels(this DbSet<GuildConfig> configs)
+        => configs.AsQueryable()
+                  .Include(x => x.GenerateCurrencyChannelIds)
+                  .Where(x => x.GenerateCurrencyChannelIds.Any())
+                  .SelectMany(x => x.GenerateCurrencyChannelIds)
+                  .Select(x => new GeneratingChannel
+                  {
+                      ChannelId = x.ChannelId,
+                      GuildId = x.GuildConfig.GuildId
+                  })
+                  .ToArray();
+
+    public class GeneratingChannel
+    {
+        public ulong GuildId { get; set; }
+        public ulong ChannelId { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/MusicPlayerSettingsExtensions.cs b/src/EllieBot/Db/Extensions/MusicPlayerSettingsExtensions.cs
new file mode 100644
index 0000000..bbefa11
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/MusicPlayerSettingsExtensions.cs
@@ -0,0 +1,27 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class MusicPlayerSettingsExtensions
+{
+    public static async Task<MusicPlayerSettings> ForGuildAsync(this DbSet<MusicPlayerSettings> settings, ulong guildId)
+    {
+        var toReturn = await settings.AsQueryable().FirstOrDefaultAsync(x => x.GuildId == guildId);
+
+        if (toReturn is null)
+        {
+            var newSettings = new MusicPlayerSettings
+            {
+                GuildId = guildId,
+                PlayerRepeat = PlayerRepeatType.Queue
+            };
+
+            await settings.AddAsync(newSettings);
+            return newSettings;
+        }
+
+        return toReturn;
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/MusicPlaylistExtensions.cs b/src/EllieBot/Db/Extensions/MusicPlaylistExtensions.cs
new file mode 100644
index 0000000..747d218
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/MusicPlaylistExtensions.cs
@@ -0,0 +1,19 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class MusicPlaylistExtensions
+{
+    public static List<MusicPlaylist> GetPlaylistsOnPage(this DbSet<MusicPlaylist> playlists, int num)
+    {
+        if (num < 0)
+            throw new ArgumentOutOfRangeException(nameof(num));
+
+        return playlists.AsQueryable().Skip((num - 1) * 20).Take(20).Include(pl => pl.Songs).ToList();
+    }
+
+    public static MusicPlaylist GetWithSongs(this DbSet<MusicPlaylist> playlists, int id)
+        => playlists.Include(mpl => mpl.Songs).FirstOrDefault(mpl => mpl.Id == id);
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/PollExtensions.cs b/src/EllieBot/Db/Extensions/PollExtensions.cs
new file mode 100644
index 0000000..abbf992
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/PollExtensions.cs
@@ -0,0 +1,28 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class PollExtensions
+{
+    public static IEnumerable<Poll> GetAllPolls(this DbSet<Poll> polls)
+        => polls.Include(x => x.Answers).Include(x => x.Votes).ToArray();
+
+    public static void RemovePoll(this EllieContext ctx, int id)
+    {
+        var p = ctx.Poll.Include(x => x.Answers).Include(x => x.Votes).FirstOrDefault(x => x.Id == id);
+
+        if (p is null)
+            return;
+
+        if (p.Votes is not null)
+        {
+            ctx.RemoveRange(p.Votes);
+            p.Answers.Clear();
+        }
+        
+        ctx.Poll.Remove(p);
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/QuoteExtensions.cs b/src/EllieBot/Db/Extensions/QuoteExtensions.cs
new file mode 100644
index 0000000..59cae90
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/QuoteExtensions.cs
@@ -0,0 +1,57 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class QuoteExtensions
+{
+    public static IEnumerable<Quote> GetForGuild(this DbSet<Quote> quotes, ulong guildId)
+        => quotes.AsQueryable().Where(x => x.GuildId == guildId);
+
+    public static IReadOnlyCollection<Quote> GetGroup(
+        this DbSet<Quote> quotes,
+        ulong guildId,
+        int page,
+        OrderType order)
+    {
+        var q = quotes.AsQueryable().Where(x => x.GuildId == guildId);
+        if (order == OrderType.Keyword)
+            q = q.OrderBy(x => x.Keyword);
+        else
+            q = q.OrderBy(x => x.Id);
+
+        return q.Skip(15 * page).Take(15).ToArray();
+    }
+
+    public static async Task<Quote> GetRandomQuoteByKeywordAsync(
+        this DbSet<Quote> quotes,
+        ulong guildId,
+        string keyword)
+    {
+        var rng = new EllieRandom();
+        return (await quotes.AsQueryable().Where(q => q.GuildId == guildId && q.Keyword == keyword).ToListAsync())
+               .OrderBy(_ => rng.Next())
+               .FirstOrDefault();
+    }
+
+    public static async Task<Quote> SearchQuoteKeywordTextAsync(
+        this DbSet<Quote> quotes,
+        ulong guildId,
+        string keyword,
+        string text)
+    {
+        var rngk = new EllieRandom();
+        return (await quotes.AsQueryable()
+                            .Where(q => q.GuildId == guildId
+                                        && (keyword == null || q.Keyword == keyword)
+                                        && (EF.Functions.Like(q.Text.ToUpper(), $"%{text.ToUpper()}%")
+                                            || EF.Functions.Like(q.AuthorName, text)))
+                            .ToListAsync())
+               .OrderBy(_ => rngk.Next())
+               .FirstOrDefault();
+    }
+
+    public static void RemoveAllByKeyword(this DbSet<Quote> quotes, ulong guildId, string keyword)
+        => quotes.RemoveRange(quotes.AsQueryable().Where(x => x.GuildId == guildId && x.Keyword.ToUpper() == keyword));
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/ReminderExtensions.cs b/src/EllieBot/Db/Extensions/ReminderExtensions.cs
new file mode 100644
index 0000000..33a79ca
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/ReminderExtensions.cs
@@ -0,0 +1,23 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class ReminderExtensions
+{
+    public static IEnumerable<Reminder> GetIncludedReminders(
+        this DbSet<Reminder> reminders,
+        IEnumerable<ulong> guildIds)
+        => reminders.AsQueryable().Where(x => guildIds.Contains(x.ServerId) || x.ServerId == 0).ToList();
+
+    public static IEnumerable<Reminder> RemindersFor(this DbSet<Reminder> reminders, ulong userId, int page)
+        => reminders.AsQueryable().Where(x => x.UserId == userId).OrderBy(x => x.DateAdded).Skip(page * 10).Take(10);
+
+    public static IEnumerable<Reminder> RemindersForServer(this DbSet<Reminder> reminders, ulong serverId, int page)
+        => reminders.AsQueryable()
+                    .Where(x => x.ServerId == serverId)
+                    .OrderBy(x => x.DateAdded)
+                    .Skip(page * 10)
+                    .Take(10);
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/SelfAssignableRolesExtensions.cs b/src/EllieBot/Db/Extensions/SelfAssignableRolesExtensions.cs
new file mode 100644
index 0000000..ee6ef7b
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/SelfAssignableRolesExtensions.cs
@@ -0,0 +1,22 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class SelfAssignableRolesExtensions
+{
+    public static bool DeleteByGuildAndRoleId(this DbSet<SelfAssignedRole> roles, ulong guildId, ulong roleId)
+    {
+        var role = roles.FirstOrDefault(s => s.GuildId == guildId && s.RoleId == roleId);
+
+        if (role is null)
+            return false;
+
+        roles.Remove(role);
+        return true;
+    }
+
+    public static IReadOnlyCollection<SelfAssignedRole> GetFromGuild(this DbSet<SelfAssignedRole> roles, ulong guildId)
+        => roles.AsQueryable().Where(s => s.GuildId == guildId).ToArray();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/UserXpExtensions.cs b/src/EllieBot/Db/Extensions/UserXpExtensions.cs
new file mode 100644
index 0000000..be1d97f
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/UserXpExtensions.cs
@@ -0,0 +1,63 @@
+#nullable disable
+using LinqToDB;
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class UserXpExtensions
+{
+    public static UserXpStats GetOrCreateUserXpStats(this EllieContext ctx, ulong guildId, ulong userId)
+    {
+        var usr = ctx.UserXpStats.FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId);
+
+        if (usr is null)
+        {
+            ctx.Add(usr = new()
+            {
+                Xp = 0,
+                UserId = userId,
+                NotifyOnLevelUp = XpNotificationLocation.None,
+                GuildId = guildId
+            });
+        }
+
+        return usr;
+    }
+
+    public static List<UserXpStats> GetUsersFor(this DbSet<UserXpStats> xps, ulong guildId, int page)
+        => xps.AsQueryable()
+              .AsNoTracking()
+              .Where(x => x.GuildId == guildId)
+              .OrderByDescending(x => x.Xp + x.AwardedXp)
+              .Skip(page * 9)
+              .Take(9)
+              .ToList();
+
+    public static List<UserXpStats> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
+        => xps.AsQueryable()
+              .AsNoTracking()
+              .Where(x => x.GuildId == guildId)
+              .OrderByDescending(x => x.Xp + x.AwardedXp)
+              .Take(count)
+              .ToList();
+
+    public static int GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
+        => xps.AsQueryable()
+              .AsNoTracking()
+              .Where(x => x.GuildId == guildId
+                          && x.Xp + x.AwardedXp
+                          > xps.AsQueryable()
+                               .Where(y => y.UserId == userId && y.GuildId == guildId)
+                               .Select(y => y.Xp + y.AwardedXp)
+                               .FirstOrDefault())
+              .Count()
+           + 1;
+
+    public static void ResetGuildUserXp(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
+        => xps.Delete(x => x.UserId == userId && x.GuildId == guildId);
+
+    public static void ResetGuildXp(this DbSet<UserXpStats> xps, ulong guildId)
+        => xps.Delete(x => x.GuildId == guildId);
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/WaifuExtensions.cs b/src/EllieBot/Db/Extensions/WaifuExtensions.cs
new file mode 100644
index 0000000..ee1fddf
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/WaifuExtensions.cs
@@ -0,0 +1,145 @@
+#nullable disable
+using LinqToDB;
+using LinqToDB.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Db.Models;
+using EllieBot.Services.Database;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public class WaifuInfoStats
+{
+    public int WaifuId { get; init; }
+    public string FullName { get; init; }
+    public long Price { get; init; }
+    public string ClaimerName { get; init; }
+    public string AffinityName { get; init; }
+    public int AffinityCount { get; init; }
+    public int DivorceCount { get; init; }
+    public int ClaimCount { get; init; }
+}
+
+public static class WaifuExtensions
+{
+    public static WaifuInfo ByWaifuUserId(
+        this DbSet<WaifuInfo> waifus,
+        ulong userId,
+        Func<DbSet<WaifuInfo>, IQueryable<WaifuInfo>> includes = null)
+    {
+        if (includes is null)
+        {
+            return waifus.Include(wi => wi.Waifu)
+                         .Include(wi => wi.Affinity)
+                         .Include(wi => wi.Claimer)
+                         .Include(wi => wi.Items)
+                         .FirstOrDefault(wi => wi.Waifu.UserId == userId);
+        }
+
+        return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId);
+    }
+
+    public static IEnumerable<WaifuLbResult> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0)
+    {
+        if (count < 0)
+            throw new ArgumentOutOfRangeException(nameof(count));
+        if (count == 0)
+            return new List<WaifuLbResult>();
+
+        return waifus.Include(wi => wi.Waifu)
+                     .Include(wi => wi.Affinity)
+                     .Include(wi => wi.Claimer)
+                     .OrderByDescending(wi => wi.Price)
+                     .Skip(skip)
+                     .Take(count)
+                     .Select(x => new WaifuLbResult
+                     {
+                         Affinity = x.Affinity == null ? null : x.Affinity.Username,
+                         AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator,
+                         Claimer = x.Claimer == null ? null : x.Claimer.Username,
+                         ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator,
+                         Username = x.Waifu.Username,
+                         Discrim = x.Waifu.Discriminator,
+                         Price = x.Price
+                     })
+                     .ToList();
+    }
+
+    public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus)
+        => waifus.AsQueryable().Where(x => x.ClaimerId != null).Sum(x => x.Price);
+
+    public static ulong GetWaifuUserId(this DbSet<WaifuInfo> waifus, ulong ownerId, string name)
+        => waifus.AsQueryable()
+                 .AsNoTracking()
+                 .Where(x => x.Claimer.UserId == ownerId && x.Waifu.Username + "#" + x.Waifu.Discriminator == name)
+                 .Select(x => x.Waifu.UserId)
+                 .FirstOrDefault();
+
+    public static async Task<WaifuInfoStats> GetWaifuInfoAsync(this EllieContext ctx, ulong userId)
+    {
+        await ctx.WaifuInfo
+                 .ToLinqToDBTable()
+                 .InsertOrUpdateAsync(() => new()
+                     {
+                         AffinityId = null,
+                         ClaimerId = null,
+                         Price = 1,
+                         WaifuId = ctx.DiscordUser.Where(x => x.UserId == userId).Select(x => x.Id).First()
+                     },
+                     _ => new(),
+                     () => new()
+                     {
+                         WaifuId = ctx.DiscordUser.Where(x => x.UserId == userId).Select(x => x.Id).First()
+                     });
+
+        var toReturn = ctx.WaifuInfo.AsQueryable()
+                          .Where(w => w.WaifuId
+                                      == ctx.Set<DiscordUser>()
+                                            .AsQueryable()
+                                            .Where(u => u.UserId == userId)
+                                            .Select(u => u.Id)
+                                            .FirstOrDefault())
+                          .Select(w => new WaifuInfoStats
+                          {
+                              WaifuId = w.WaifuId,
+                              FullName =
+                                  ctx.Set<DiscordUser>()
+                                     .AsQueryable()
+                                     .Where(u => u.UserId == userId)
+                                     .Select(u => u.Username + "#" + u.Discriminator)
+                                     .FirstOrDefault(),
+                              AffinityCount =
+                                  ctx.Set<WaifuUpdate>()
+                                     .AsQueryable()
+                                     .Count(x => x.UserId == w.WaifuId
+                                                 && x.UpdateType == WaifuUpdateType.AffinityChanged
+                                                 && x.NewId != null),
+                              AffinityName =
+                                  ctx.Set<DiscordUser>()
+                                     .AsQueryable()
+                                     .Where(u => u.Id == w.AffinityId)
+                                     .Select(u => u.Username + "#" + u.Discriminator)
+                                     .FirstOrDefault(),
+                              ClaimCount = ctx.WaifuInfo.AsQueryable().Count(x => x.ClaimerId == w.WaifuId),
+                              ClaimerName =
+                                  ctx.Set<DiscordUser>()
+                                     .AsQueryable()
+                                     .Where(u => u.Id == w.ClaimerId)
+                                     .Select(u => u.Username + "#" + u.Discriminator)
+                                     .FirstOrDefault(),
+                              DivorceCount =
+                                  ctx.Set<WaifuUpdate>()
+                                     .AsQueryable()
+                                     .Count(x => x.OldId == w.WaifuId
+                                                 && x.NewId == null
+                                                 && x.UpdateType == WaifuUpdateType.Claimed),
+                              Price = w.Price,
+                          })
+                          .FirstOrDefault();
+
+        if (toReturn is null)
+            return null;
+
+        return toReturn;
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Extensions/WarningExtensions.cs b/src/EllieBot/Db/Extensions/WarningExtensions.cs
new file mode 100644
index 0000000..59474d2
--- /dev/null
+++ b/src/EllieBot/Db/Extensions/WarningExtensions.cs
@@ -0,0 +1,60 @@
+#nullable disable
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db;
+
+public static class WarningExtensions
+{
+    public static Warning[] ForId(this DbSet<Warning> warnings, ulong guildId, ulong userId)
+    {
+        var query = warnings.AsQueryable()
+                            .Where(x => x.GuildId == guildId && x.UserId == userId)
+                            .OrderByDescending(x => x.DateAdded);
+
+        return query.ToArray();
+    }
+
+    public static bool Forgive(
+        this DbSet<Warning> warnings,
+        ulong guildId,
+        ulong userId,
+        string mod,
+        int index)
+    {
+        if (index < 0)
+            throw new ArgumentOutOfRangeException(nameof(index));
+
+        var warn = warnings.AsQueryable()
+                           .Where(x => x.GuildId == guildId && x.UserId == userId)
+                           .OrderByDescending(x => x.DateAdded)
+                           .Skip(index)
+                           .FirstOrDefault();
+
+        if (warn is null || warn.Forgiven)
+            return false;
+
+        warn.Forgiven = true;
+        warn.ForgivenBy = mod;
+        return true;
+    }
+
+    public static async Task ForgiveAll(
+        this DbSet<Warning> warnings,
+        ulong guildId,
+        ulong userId,
+        string mod)
+        => await warnings.AsQueryable()
+                         .Where(x => x.GuildId == guildId && x.UserId == userId)
+                         .ForEachAsync(x =>
+                         {
+                             if (x.Forgiven != true)
+                             {
+                                 x.Forgiven = true;
+                                 x.ForgivenBy = mod;
+                             }
+                         });
+
+    public static Warning[] GetForGuild(this DbSet<Warning> warnings, ulong id)
+        => warnings.AsQueryable().Where(x => x.GuildId == id).ToArray();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/AntiProtection.cs b/src/EllieBot/Db/Models/AntiProtection.cs
new file mode 100644
index 0000000..2301b42
--- /dev/null
+++ b/src/EllieBot/Db/Models/AntiProtection.cs
@@ -0,0 +1,65 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class AntiRaidSetting : DbEntity
+{
+    public int GuildConfigId { get; set; }
+    public GuildConfig GuildConfig { get; set; }
+
+    public int UserThreshold { get; set; }
+    public int Seconds { get; set; }
+    public PunishmentAction Action { get; set; }
+
+    /// <summary>
+    ///     Duration of the punishment, in minutes. This works only in supported Actions, like:
+    ///     Mute, ChatMute, VoiceMute, etc...
+    /// </summary>
+    public int PunishmentDuration { get; set; }
+}
+
+public class AntiSpamSetting : DbEntity
+{
+    public int GuildConfigId { get; set; }
+    public GuildConfig GuildConfig { get; set; }
+
+    public PunishmentAction Action { get; set; }
+    public int MessageThreshold { get; set; } = 3;
+    public int MuteTime { get; set; }
+    public ulong? RoleId { get; set; }
+    public HashSet<AntiSpamIgnore> IgnoredChannels { get; set; } = new();
+}
+
+public class AntiAltSetting
+{
+    public int Id { get; set; }
+    public int GuildConfigId { get; set; }
+    public TimeSpan MinAge { get; set; }
+    public PunishmentAction Action { get; set; }
+    public int ActionDurationMinutes { get; set; }
+    public ulong? RoleId { get; set; }
+}
+
+public enum PunishmentAction
+{
+    Mute,
+    Kick,
+    Ban,
+    Softban,
+    RemoveRoles,
+    ChatMute,
+    VoiceMute,
+    AddRole,
+    Warn,
+    TimeOut
+}
+
+public class AntiSpamIgnore : DbEntity
+{
+    public ulong ChannelId { get; set; }
+
+    public override int GetHashCode() 
+        => ChannelId.GetHashCode();
+
+    public override bool Equals(object obj) 
+        => obj is AntiSpamIgnore inst ? inst.ChannelId == ChannelId : false;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/AutoCommand.cs b/src/EllieBot/Db/Models/AutoCommand.cs
new file mode 100644
index 0000000..3738c37
--- /dev/null
+++ b/src/EllieBot/Db/Models/AutoCommand.cs
@@ -0,0 +1,14 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class AutoCommand : DbEntity
+{
+    public string CommandText { get; set; }
+    public ulong ChannelId { get; set; }
+    public string ChannelName { get; set; }
+    public ulong? GuildId { get; set; }
+    public string GuildName { get; set; }
+    public ulong? VoiceChannelId { get; set; }
+    public string VoiceChannelName { get; set; }
+    public int Interval { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/AutoPublishChannel.cs b/src/EllieBot/Db/Models/AutoPublishChannel.cs
new file mode 100644
index 0000000..fa81769
--- /dev/null
+++ b/src/EllieBot/Db/Models/AutoPublishChannel.cs
@@ -0,0 +1,9 @@
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db.Models;
+
+public class AutoPublishChannel : DbEntity
+{
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/AutoTranslateChannel.cs b/src/EllieBot/Db/Models/AutoTranslateChannel.cs
new file mode 100644
index 0000000..a90df1e
--- /dev/null
+++ b/src/EllieBot/Db/Models/AutoTranslateChannel.cs
@@ -0,0 +1,10 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class AutoTranslateChannel : DbEntity
+{
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+    public bool AutoDelete { get; set; }
+    public IList<AutoTranslateUser> Users { get; set; } = new List<AutoTranslateUser>();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/AutoTranslateUser.cs b/src/EllieBot/Db/Models/AutoTranslateUser.cs
new file mode 100644
index 0000000..ebe6754
--- /dev/null
+++ b/src/EllieBot/Db/Models/AutoTranslateUser.cs
@@ -0,0 +1,11 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class AutoTranslateUser : DbEntity
+{
+    public int ChannelId { get; set; }
+    public AutoTranslateChannel Channel { get; set; }
+    public ulong UserId { get; set; }
+    public string Source { get; set; }
+    public string Target { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/BanTemplate.cs b/src/EllieBot/Db/Models/BanTemplate.cs
new file mode 100644
index 0000000..cc72275
--- /dev/null
+++ b/src/EllieBot/Db/Models/BanTemplate.cs
@@ -0,0 +1,9 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class BanTemplate : DbEntity
+{
+    public ulong GuildId { get; set; }
+    public string Text { get; set; }
+    public int? PruneDays { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/BankUser.cs b/src/EllieBot/Db/Models/BankUser.cs
new file mode 100644
index 0000000..7fa6418
--- /dev/null
+++ b/src/EllieBot/Db/Models/BankUser.cs
@@ -0,0 +1,9 @@
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db.Models;
+
+public class BankUser : DbEntity
+{
+    public ulong UserId { get; set; }
+    public long Balance { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/BlacklistEntry.cs b/src/EllieBot/Db/Models/BlacklistEntry.cs
new file mode 100644
index 0000000..849c3f7
--- /dev/null
+++ b/src/EllieBot/Db/Models/BlacklistEntry.cs
@@ -0,0 +1,14 @@
+namespace EllieBot.Services.Database.Models;
+
+public class BlacklistEntry : DbEntity
+{
+    public ulong ItemId { get; set; }
+    public BlacklistType Type { get; set; }
+}
+
+public enum BlacklistType
+{
+    Server,
+    Channel,
+    User
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/ClubInfo.cs b/src/EllieBot/Db/Models/ClubInfo.cs
new file mode 100644
index 0000000..75d2d1b
--- /dev/null
+++ b/src/EllieBot/Db/Models/ClubInfo.cs
@@ -0,0 +1,42 @@
+#nullable disable
+using EllieBot.Services.Database.Models;
+using System.ComponentModel.DataAnnotations;
+
+namespace EllieBot.Db.Models;
+
+public class ClubInfo : DbEntity
+{
+    [MaxLength(20)]
+    public string Name { get; set; }
+    public string Description { get; set; }
+    public string ImageUrl { get; set; }
+
+    public int Xp { get; set; } = 0;
+    public int? OwnerId { get; set; }
+    public DiscordUser Owner { get; set; }
+
+    public List<DiscordUser> Members { get; set; } = new();
+    public List<ClubApplicants> Applicants { get; set; } = new();
+    public List<ClubBans> Bans { get; set; } = new();
+
+    public override string ToString()
+        => Name;
+}
+
+public class ClubApplicants
+{
+    public int ClubId { get; set; }
+    public ClubInfo Club { get; set; }
+    
+    public int UserId { get; set; }
+    public DiscordUser User { get; set; }
+}
+
+public class ClubBans
+{
+    public int ClubId { get; set; }
+    public ClubInfo Club { get; set; }
+    
+    public int UserId { get; set; }
+    public DiscordUser User { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/CommandAlias.cs b/src/EllieBot/Db/Models/CommandAlias.cs
new file mode 100644
index 0000000..be2a6f1
--- /dev/null
+++ b/src/EllieBot/Db/Models/CommandAlias.cs
@@ -0,0 +1,8 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class CommandAlias : DbEntity
+{
+    public string Trigger { get; set; }
+    public string Mapping { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/CommandCooldown.cs b/src/EllieBot/Db/Models/CommandCooldown.cs
new file mode 100644
index 0000000..2b824ef
--- /dev/null
+++ b/src/EllieBot/Db/Models/CommandCooldown.cs
@@ -0,0 +1,8 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class CommandCooldown : DbEntity
+{
+    public int Seconds { get; set; }
+    public string CommandName { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/CurrencyTransaction.cs b/src/EllieBot/Db/Models/CurrencyTransaction.cs
new file mode 100644
index 0000000..9153520
--- /dev/null
+++ b/src/EllieBot/Db/Models/CurrencyTransaction.cs
@@ -0,0 +1,12 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class CurrencyTransaction : DbEntity
+{
+    public long Amount { get; set; }
+    public string Note { get; set; }
+    public ulong UserId { get; set; }
+    public string Type { get; set; }
+    public string Extra { get; set; }
+    public ulong? OtherId { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/DbEntity.cs b/src/EllieBot/Db/Models/DbEntity.cs
new file mode 100644
index 0000000..035ff76
--- /dev/null
+++ b/src/EllieBot/Db/Models/DbEntity.cs
@@ -0,0 +1,12 @@
+#nullable disable
+using System.ComponentModel.DataAnnotations;
+
+namespace EllieBot.Services.Database.Models;
+
+public class DbEntity
+{
+    [Key]
+    public int Id { get; set; }
+
+    public DateTime? DateAdded { get; set; } = DateTime.UtcNow;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/DelMsgOnCmdChannel.cs b/src/EllieBot/Db/Models/DelMsgOnCmdChannel.cs
new file mode 100644
index 0000000..59ac41d
--- /dev/null
+++ b/src/EllieBot/Db/Models/DelMsgOnCmdChannel.cs
@@ -0,0 +1,14 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class DelMsgOnCmdChannel : DbEntity
+{
+    public ulong ChannelId { get; set; }
+    public bool State { get; set; }
+
+    public override int GetHashCode()
+        => ChannelId.GetHashCode();
+
+    public override bool Equals(object obj) 
+        => obj is DelMsgOnCmdChannel x && x.ChannelId == ChannelId;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/DiscordPermOverride.cs b/src/EllieBot/Db/Models/DiscordPermOverride.cs
new file mode 100644
index 0000000..461dcc4
--- /dev/null
+++ b/src/EllieBot/Db/Models/DiscordPermOverride.cs
@@ -0,0 +1,10 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class DiscordPermOverride : DbEntity
+{
+    public GuildPerm Perm { get; set; }
+
+    public ulong? GuildId { get; set; }
+    public string Command { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/DiscordUser.cs b/src/EllieBot/Db/Models/DiscordUser.cs
new file mode 100644
index 0000000..aa52e19
--- /dev/null
+++ b/src/EllieBot/Db/Models/DiscordUser.cs
@@ -0,0 +1,31 @@
+#nullable disable
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db.Models;
+
+// FUTURE remove LastLevelUp from here and UserXpStats
+public class DiscordUser : DbEntity
+{
+    public ulong UserId { get; set; }
+    public string Username { get; set; }
+    public string Discriminator { get; set; }
+    public string AvatarId { get; set; }
+    
+    public int? ClubId { get; set; }
+    public ClubInfo Club { get; set; }
+    public bool IsClubAdmin { get; set; }
+    
+    public long TotalXp { get; set; }
+    public XpNotificationLocation NotifyOnLevelUp { get; set; }
+    
+    public long CurrencyAmount { get; set; }
+
+    public override bool Equals(object obj)
+        => obj is DiscordUser du ? du.UserId == UserId : false;
+
+    public override int GetHashCode() 
+        => UserId.GetHashCode();
+
+    public override string ToString()
+        => Username + "#" + Discriminator;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/EllieExpression.cs b/src/EllieBot/Db/Models/EllieExpression.cs
new file mode 100644
index 0000000..9218e0c
--- /dev/null
+++ b/src/EllieBot/Db/Models/EllieExpression.cs
@@ -0,0 +1,26 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class EllieExpression : DbEntity
+{
+    public ulong? GuildId { get; set; }
+    public string Response { get; set; }
+    public string Trigger { get; set; }
+    
+    public bool AutoDeleteTrigger { get; set; }
+    public bool DmResponse { get; set; }
+    public bool ContainsAnywhere { get; set; }
+    public bool AllowTarget { get; set; }
+
+    public string[] GetReactions()
+        => string.IsNullOrWhiteSpace(Reaactions) ? Array.Empty<string>() : Reaactions.Split("@@@");
+
+    public bool IsGlobal()
+        => GuildId is null or 0;
+}
+
+public class ReactionResponse : DbEntity
+{
+    public bool OwnerOnly { get; set; }
+    public string Text { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/FeedSub.cs b/src/EllieBot/Db/Models/FeedSub.cs
new file mode 100644
index 0000000..f15aa81
--- /dev/null
+++ b/src/EllieBot/Db/Models/FeedSub.cs
@@ -0,0 +1,19 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class FeedSub : DbEntity
+{
+    public int GuildConfigId { get; set; }
+    public GuildConfig GuildConfig { get; set; }
+
+    public ulong ChannelId { get; set; }
+    public string Url { get; set; }
+
+    public string Message { get; set; }
+
+    public override int GetHashCode()
+        => Url.GetHashCode(StringComparison.InvariantCulture) ^ GuildConfigId.GetHashCode();
+
+    public override bool Equals(object obj)
+        => obj is FeedSub s && s.Url.ToLower() == Url.ToLower() && s.GuildConfigId == GuildConfigId;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/FilterChannelId.cs b/src/EllieBot/Db/Models/FilterChannelId.cs
new file mode 100644
index 0000000..e674ec2
--- /dev/null
+++ b/src/EllieBot/Db/Models/FilterChannelId.cs
@@ -0,0 +1,30 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class FilterChannelId : DbEntity
+{
+    public ulong ChannelId { get; set; }
+
+    public bool Equals(FilterChannelId other)
+        => ChannelId == other.ChannelId;
+
+    public override bool Equals(object obj)
+        => obj is FilterChannelId fci && Equals(fci);
+
+    public override int GetHashCode() 
+        => ChannelId.GetHashCode();
+}
+
+public class FilterWordsChannelId : DbEntity
+{
+    public ulong ChannelId { get; set; }
+
+    public bool Equals(FilterWordsChannelId other)
+        => ChannelId == other.ChannelId;
+
+    public override bool Equals(object obj) 
+        => obj is FilterWordsChannelId fci && Equals(fci);
+
+    public override int GetHashCode()
+        => ChannelId.GetHashCode();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/FilterLinksChannelId.cs b/src/EllieBot/Db/Models/FilterLinksChannelId.cs
new file mode 100644
index 0000000..440085e
--- /dev/null
+++ b/src/EllieBot/Db/Models/FilterLinksChannelId.cs
@@ -0,0 +1,13 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class FilterLinksChannelId : DbEntity
+{
+    public ulong ChannelId { get; set; }
+
+    public override bool Equals(object obj) 
+        => obj is FilterLinksChannelId f && f.ChannelId == ChannelId;
+
+    public override int GetHashCode() 
+        => ChannelId.GetHashCode();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/FilteredWord.cs b/src/EllieBot/Db/Models/FilteredWord.cs
new file mode 100644
index 0000000..5a5ee93
--- /dev/null
+++ b/src/EllieBot/Db/Models/FilteredWord.cs
@@ -0,0 +1,7 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class FilteredWord : DbEntity
+{
+    public string Word { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/FollowedStream.cs b/src/EllieBot/Db/Models/FollowedStream.cs
new file mode 100644
index 0000000..3a96846
--- /dev/null
+++ b/src/EllieBot/Db/Models/FollowedStream.cs
@@ -0,0 +1,37 @@
+#nullable disable
+using EllieBot.Modules.Searches.Common;
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db.Models;
+
+public class FollowedStream : DbEntity
+{
+    public enum FType
+    {
+        Twitch = 0,
+        Picarto = 3,
+        Youtube = 4,
+        Facebook = 5,
+        Trovo = 6
+    }
+
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+    public string Username { get; set; }
+    public FType Type { get; set; }
+    public string Message { get; set; }
+
+    protected bool Equals(FollowedStream other)
+        => ChannelId == other.ChannelId
+           && Username.Trim().ToUpperInvariant() == other.Username.Trim().ToUpperInvariant()
+           && Type == other.Type;
+
+    public override int GetHashCode()
+        => HashCode.Combine(ChannelId, Username, (int)Type);
+
+    public override bool Equals(object obj)
+        => obj is FollowedStream fs && Equals(fs);
+
+    public StreamDataKey CreateKey()
+        => new(Type, Username.ToLower());
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/GCChannelId.cs b/src/EllieBot/Db/Models/GCChannelId.cs
new file mode 100644
index 0000000..387605c
--- /dev/null
+++ b/src/EllieBot/Db/Models/GCChannelId.cs
@@ -0,0 +1,14 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class GCChannelId : DbEntity
+{
+    public GuildConfig GuildConfig { get; set; }
+    public ulong ChannelId { get; set; }
+
+    public override bool Equals(object obj) 
+        => obj is GCChannelId gc && gc.ChannelId == ChannelId;
+
+    public override int GetHashCode() 
+        => ChannelId.GetHashCode();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/GamblingStats.cs b/src/EllieBot/Db/Models/GamblingStats.cs
new file mode 100644
index 0000000..8a4654e
--- /dev/null
+++ b/src/EllieBot/Db/Models/GamblingStats.cs
@@ -0,0 +1,9 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class GamblingStats : DbEntity
+{
+    public string Feature { get; set; }
+    public decimal Bet { get; set; }
+    public decimal PaidOut { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/GroupName.cs b/src/EllieBot/Db/Models/GroupName.cs
new file mode 100644
index 0000000..52ef07b
--- /dev/null
+++ b/src/EllieBot/Db/Models/GroupName.cs
@@ -0,0 +1,11 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class GroupName : DbEntity
+{
+    public int GuildConfigId { get; set; }
+    public GuildConfig GuildConfig { get; set; }
+
+    public int Number { get; set; }
+    public string Name { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/GuildConfig.cs b/src/EllieBot/Db/Models/GuildConfig.cs
new file mode 100644
index 0000000..5346489
--- /dev/null
+++ b/src/EllieBot/Db/Models/GuildConfig.cs
@@ -0,0 +1,108 @@
+#nullable disable
+using EllieBot.Db.Models;
+
+namespace EllieBot.Services.Database.Models;
+
+public class GuildConfig : DbEntity
+{
+    public ulong GuildId { get; set; }
+
+    public string Prefix { get; set; }
+
+    public bool DeleteMessageOnCommand { get; set; }
+    public HashSet<DelMsgOnCmdChannel> DelMsgOnCmdChannels { get; set; } = new();
+
+    public string AutoAssignRoleIds { get; set; }
+
+    //greet stuff
+    public int AutoDeleteGreetMessagesTimer { get; set; } = 30;
+    public int AutoDeleteByeMessagesTimer { get; set; } = 30;
+
+    public ulong GreetMessageChannelId { get; set; }
+    public ulong ByeMessageChannelId { get; set; }
+
+    public bool SendDmGreetMessage { get; set; }
+    public string DmGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
+
+    public bool SendChannelGreetMessage { get; set; }
+    public string ChannelGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
+
+    public bool SendChannelByeMessage { get; set; }
+    public string ChannelByeMessageText { get; set; } = "%user% has left!";
+
+    //self assignable roles
+    public bool ExclusiveSelfAssignedRoles { get; set; }
+    public bool AutoDeleteSelfAssignedRoleMessages { get; set; }
+
+    //stream notifications
+    public HashSet<FollowedStream> FollowedStreams { get; set; } = new();
+
+    //currencyGeneration
+    public HashSet<GCChannelId> GenerateCurrencyChannelIds { get; set; } = new();
+
+    public List<Permissionv2> Permissions { get; set; }
+    public bool VerbosePermissions { get; set; } = true;
+    public string PermissionRole { get; set; }
+
+    public HashSet<CommandCooldown> CommandCooldowns { get; set; } = new();
+
+    //filtering
+    public bool FilterInvites { get; set; }
+    public bool FilterLinks { get; set; }
+    public HashSet<FilterChannelId> FilterInvitesChannelIds { get; set; } = new();
+    public HashSet<FilterLinksChannelId> FilterLinksChannelIds { get; set; } = new();
+
+    //public bool FilterLinks { get; set; }
+    //public HashSet<FilterLinksChannelId> FilterLinksChannels { get; set; } = new HashSet<FilterLinksChannelId>();
+
+    public bool FilterWords { get; set; }
+    public HashSet<FilteredWord> FilteredWords { get; set; } = new();
+    public HashSet<FilterWordsChannelId> FilterWordsChannelIds { get; set; } = new();
+
+    public HashSet<MutedUserId> MutedUsers { get; set; } = new();
+
+    public string MuteRoleName { get; set; }
+    public bool CleverbotEnabled { get; set; }
+
+    public AntiRaidSetting AntiRaidSetting { get; set; }
+    public AntiSpamSetting AntiSpamSetting { get; set; }
+    public AntiAltSetting AntiAltSetting { get; set; }
+
+    public string Locale { get; set; }
+    public string TimeZoneId { get; set; }
+
+    public HashSet<UnmuteTimer> UnmuteTimers { get; set; } = new();
+    public HashSet<UnbanTimer> UnbanTimer { get; set; } = new();
+    public HashSet<UnroleTimer> UnroleTimer { get; set; } = new();
+    public HashSet<VcRoleInfo> VcRoleInfos { get; set; }
+    public HashSet<CommandAlias> CommandAliases { get; set; } = new();
+    public List<WarningPunishment> WarnPunishments { get; set; } = new();
+    public bool WarningsInitialized { get; set; }
+    public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
+    public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }
+
+    public List<ShopEntry> ShopEntries { get; set; }
+    public ulong? GameVoiceChannel { get; set; }
+    public bool VerboseErrors { get; set; } = true;
+
+    public StreamRoleSettings StreamRole { get; set; }
+
+    public XpSettings XpSettings { get; set; }
+    public List<FeedSub> FeedSubs { get; set; } = new();
+    public bool NotifyStreamOffline { get; set; }
+    public bool DeleteStreamOnlineMessage { get; set; }
+    public List<GroupName> SelfAssignableRoleGroupNames { get; set; }
+    public int WarnExpireHours { get; set; }
+    public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear;
+
+    public bool DisableGlobalExpressions { get; set; } = false;
+
+    #region Boost Message
+
+    public bool SendBoostMessage { get; set; }
+    public string BoostMessage { get; set; } = "%user% just boosted this server!";
+    public ulong BoostMessageChannelId { get; set; }
+    public int BoostMessageDeleteAfter { get; set; }
+
+    #endregion
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/IgnoredLogItem.cs b/src/EllieBot/Db/Models/IgnoredLogItem.cs
new file mode 100644
index 0000000..060ef32
--- /dev/null
+++ b/src/EllieBot/Db/Models/IgnoredLogItem.cs
@@ -0,0 +1,16 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class IgnoredLogItem : DbEntity
+{
+    public int LogSettingId { get; set; }
+    public LogSetting LogSetting { get; set; }
+    public ulong LogItemId { get; set; }
+    public IgnoredItemType ItemType { get; set; }
+}
+
+public enum IgnoredItemType
+{
+    Channel,
+    User
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/IgnoredVoicePresenceChannel.cs b/src/EllieBot/Db/Models/IgnoredVoicePresenceChannel.cs
new file mode 100644
index 0000000..e25c38a
--- /dev/null
+++ b/src/EllieBot/Db/Models/IgnoredVoicePresenceChannel.cs
@@ -0,0 +1,8 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class IgnoredVoicePresenceChannel : DbEntity
+{
+    public LogSetting LogSetting { get; set; }
+    public ulong ChannelId { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/ImageOnlyChannel.cs b/src/EllieBot/Db/Models/ImageOnlyChannel.cs
new file mode 100644
index 0000000..2fce8d1
--- /dev/null
+++ b/src/EllieBot/Db/Models/ImageOnlyChannel.cs
@@ -0,0 +1,15 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class ImageOnlyChannel : DbEntity
+{
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+    public OnlyChannelType Type { get; set; }
+}
+
+public enum OnlyChannelType
+{
+    Image,
+    Link
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/LogSetting.cs b/src/EllieBot/Db/Models/LogSetting.cs
new file mode 100644
index 0000000..b09371a
--- /dev/null
+++ b/src/EllieBot/Db/Models/LogSetting.cs
@@ -0,0 +1,37 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class LogSetting : DbEntity
+{
+    public List<IgnoredLogItem> LogIgnores { get; set; } = new();
+    
+    public ulong GuildId { get; set; }
+    public ulong? LogOtherId { get; set; }
+    public ulong? MessageUpdatedId { get; set; }
+    public ulong? MessageDeletedId { get; set; }
+
+    public ulong? UserJoinedId { get; set; }
+    public ulong? UserLeftId { get; set; }
+    public ulong? UserBannedId { get; set; }
+    public ulong? UserUnbannedId { get; set; }
+    public ulong? UserUpdatedId { get; set; }
+
+    public ulong? ChannelCreatedId { get; set; }
+    public ulong? ChannelDestroyedId { get; set; }
+    public ulong? ChannelUpdatedId { get; set; }
+    
+    
+    public ulong? ThreadDeletedId { get; set; }
+    public ulong? ThreadCreatedId { get; set; }
+
+    public ulong? UserMutedId { get; set; }
+
+    // userpresence
+    public ulong? LogUserPresenceId { get; set; }
+
+    // voicepresence
+
+    public ulong? LogVoicePresenceId { get; set; }
+    public ulong? LogVoicePresenceTTSId { get; set; }
+    public ulong? LogWarnsId { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/MusicPlaylist.cs b/src/EllieBot/Db/Models/MusicPlaylist.cs
new file mode 100644
index 0000000..3f96d05
--- /dev/null
+++ b/src/EllieBot/Db/Models/MusicPlaylist.cs
@@ -0,0 +1,10 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class MusicPlaylist : DbEntity
+{
+    public string Name { get; set; }
+    public string Author { get; set; }
+    public ulong AuthorId { get; set; }
+    public List<PlaylistSong> Songs { get; set; } = new();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/MusicSettings.cs b/src/EllieBot/Db/Models/MusicSettings.cs
new file mode 100644
index 0000000..b81d433
--- /dev/null
+++ b/src/EllieBot/Db/Models/MusicSettings.cs
@@ -0,0 +1,61 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class MusicPlayerSettings
+{
+    /// <summary>
+    ///     Auto generated Id
+    /// </summary>
+    public int Id { get; set; }
+
+    /// <summary>
+    ///     Id of the guild
+    /// </summary>
+    public ulong GuildId { get; set; }
+
+    /// <summary>
+    ///     Queue repeat type
+    /// </summary>
+    public PlayerRepeatType PlayerRepeat { get; set; } = PlayerRepeatType.Queue;
+
+    /// <summary>
+    ///     Channel id the bot will always try to send track related messages to
+    /// </summary>
+    public ulong? MusicChannelId { get; set; }
+
+    /// <summary>
+    ///     Default volume player will be created with
+    /// </summary>
+    public int Volume { get; set; } = 100;
+
+    /// <summary>
+    ///     Whether the bot should auto disconnect from the voice channel once the queue is done
+    ///     This only has effect if
+    /// </summary>
+    public bool AutoDisconnect { get; set; }
+
+    /// <summary>
+    ///     Selected quality preset for the music player
+    /// </summary>
+    public QualityPreset QualityPreset { get; set; }
+
+    /// <summary>
+    ///     Whether the bot will automatically queue related songs
+    /// </summary>
+    public bool AutoPlay { get; set; }
+}
+
+public enum QualityPreset
+{
+    Highest,
+    High,
+    Medium,
+    Low
+}
+
+public enum PlayerRepeatType
+{
+    None,
+    Track,
+    Queue
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/MutedUserId.cs b/src/EllieBot/Db/Models/MutedUserId.cs
new file mode 100644
index 0000000..2d752f6
--- /dev/null
+++ b/src/EllieBot/Db/Models/MutedUserId.cs
@@ -0,0 +1,13 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class MutedUserId : DbEntity
+{
+    public ulong UserId { get; set; }
+
+    public override int GetHashCode()
+        => UserId.GetHashCode();
+
+    public override bool Equals(object obj)
+        => obj is MutedUserId mui ? mui.UserId == UserId : false;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/NsfwBlacklistedTag.cs b/src/EllieBot/Db/Models/NsfwBlacklistedTag.cs
new file mode 100644
index 0000000..3617132
--- /dev/null
+++ b/src/EllieBot/Db/Models/NsfwBlacklistedTag.cs
@@ -0,0 +1,14 @@
+#nullable disable
+namespace EllieBot.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;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/PatronQuota.cs b/src/EllieBot/Db/Models/PatronQuota.cs
new file mode 100644
index 0000000..ba5129d
--- /dev/null
+++ b/src/EllieBot/Db/Models/PatronQuota.cs
@@ -0,0 +1,48 @@
+#nullable disable
+namespace EllieBot.Db.Models;
+
+/// <summary>
+/// Contains data about usage of Patron-Only commands per user
+/// in order to provide support for quota limitations
+/// (allow user x who is pledging amount y to use the specified command only
+///  x amount of times in the specified time period)
+/// </summary>
+public class PatronQuota
+{
+    public ulong UserId { get; set; }
+    public FeatureType FeatureType { get; set; }
+    public string Feature { get; set; }
+    public uint HourlyCount { get; set; }
+    public uint DailyCount { get; set; }
+    public uint MonthlyCount { get; set; }
+}
+
+public enum FeatureType
+{
+    Command,
+    Group,
+    Module,
+    Limit
+}
+
+public class PatronUser
+{
+    public string UniquePlatformUserId { get; set; }
+    public ulong UserId { get; set; }
+    public int AmountCents { get; set; }
+    
+    public DateTime LastCharge { get; set; }
+    
+    // Date Only component
+    public DateTime ValidThru { get; set; }
+    
+    public PatronUser Clone()
+        => new PatronUser()
+        {
+            UniquePlatformUserId = this.UniquePlatformUserId,
+            UserId = this.UserId,
+            AmountCents = this.AmountCents,
+            LastCharge = this.LastCharge,
+            ValidThru = this.ValidThru
+        };
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/Permission.cs b/src/EllieBot/Db/Models/Permission.cs
new file mode 100644
index 0000000..61089f6
--- /dev/null
+++ b/src/EllieBot/Db/Models/Permission.cs
@@ -0,0 +1,56 @@
+#nullable disable
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Diagnostics;
+
+namespace EllieBot.Services.Database.Models;
+
+
+[DebuggerDisplay("{PrimaryTarget}{SecondaryTarget} {SecondaryTargetName} {State} {PrimaryTargetId}")]
+public class Permissionv2 : DbEntity, IIndexed
+{
+    public int? GuildConfigId { get; set; }
+    public int Index { get; set; }
+
+    public PrimaryPermissionType PrimaryTarget { get; set; }
+    public ulong PrimaryTargetId { get; set; }
+
+    public SecondaryPermissionType SecondaryTarget { get; set; }
+    public string SecondaryTargetName { get; set; }
+
+    public bool IsCustomCommand { get; set; }
+
+    public bool State { get; set; }
+
+    [NotMapped]
+    public static Permissionv2 AllowAllPerm
+        => new()
+        {
+            PrimaryTarget = PrimaryPermissionType.Server,
+            PrimaryTargetId = 0,
+            SecondaryTarget = SecondaryPermissionType.AllModules,
+            SecondaryTargetName = "*",
+            State = true,
+            Index = 0
+        };
+
+    public static List<Permissionv2> GetDefaultPermlist
+        => new()
+        {
+            AllowAllPerm
+        };
+}
+
+public enum PrimaryPermissionType
+{
+    User,
+    Channel,
+    Role,
+    Server
+}
+
+public enum SecondaryPermissionType
+{
+    Module,
+    Command,
+    AllModules
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/PlantedCurrency.cs b/src/EllieBot/Db/Models/PlantedCurrency.cs
new file mode 100644
index 0000000..3741530
--- /dev/null
+++ b/src/EllieBot/Db/Models/PlantedCurrency.cs
@@ -0,0 +1,12 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class PlantedCurrency : DbEntity
+{
+    public long Amount { get; set; }
+    public string Password { get; set; }
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+    public ulong UserId { get; set; }
+    public ulong MessageId { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/PlaylistSong.cs b/src/EllieBot/Db/Models/PlaylistSong.cs
new file mode 100644
index 0000000..7a2078d
--- /dev/null
+++ b/src/EllieBot/Db/Models/PlaylistSong.cs
@@ -0,0 +1,19 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class PlaylistSong : DbEntity
+{
+    public string Provider { get; set; }
+    public MusicType ProviderType { get; set; }
+    public string Title { get; set; }
+    public string Uri { get; set; }
+    public string Query { get; set; }
+}
+
+public enum MusicType
+{
+    Radio,
+    YouTube,
+    Local,
+    Soundcloud
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/Poll.cs b/src/EllieBot/Db/Models/Poll.cs
new file mode 100644
index 0000000..10e988c
--- /dev/null
+++ b/src/EllieBot/Db/Models/Poll.cs
@@ -0,0 +1,17 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class Poll : DbEntity
+{
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+    public string Question { get; set; }
+    public IndexedCollection<PollAnswer> Answers { get; set; }
+    public HashSet<PollVote> Votes { get; set; } = new();
+}
+
+public class PollAnswer : DbEntity, IIndexed
+{
+    public int Index { get; set; }
+    public string Text { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/PollVote.cs b/src/EllieBot/Db/Models/PollVote.cs
new file mode 100644
index 0000000..e78dadf
--- /dev/null
+++ b/src/EllieBot/Db/Models/PollVote.cs
@@ -0,0 +1,14 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class PollVote : DbEntity
+{
+    public ulong UserId { get; set; }
+    public int VoteIndex { get; set; }
+
+    public override int GetHashCode()
+        => UserId.GetHashCode();
+
+    public override bool Equals(object obj)
+        => obj is PollVote p ? p.UserId == UserId : false;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/Quote.cs b/src/EllieBot/Db/Models/Quote.cs
new file mode 100644
index 0000000..529dc46
--- /dev/null
+++ b/src/EllieBot/Db/Models/Quote.cs
@@ -0,0 +1,26 @@
+#nullable disable
+using System.ComponentModel.DataAnnotations;
+
+namespace EllieBot.Services.Database.Models;
+
+public class Quote : DbEntity
+{
+    public ulong GuildId { get; set; }
+
+    [Required]
+    public string Keyword { get; set; }
+
+    [Required]
+    public string AuthorName { get; set; }
+
+    public ulong AuthorId { get; set; }
+
+    [Required]
+    public string Text { get; set; }
+}
+
+public enum OrderType
+{
+    Id = -1,
+    Keyword = -2
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/ReactionRole.cs b/src/EllieBot/Db/Models/ReactionRole.cs
new file mode 100644
index 0000000..4e1e4f6
--- /dev/null
+++ b/src/EllieBot/Db/Models/ReactionRole.cs
@@ -0,0 +1,18 @@
+#nullable disable
+using System.ComponentModel.DataAnnotations;
+
+namespace EllieBot.Services.Database.Models;
+
+public class ReactionRoleV2 : DbEntity
+{
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+    
+    public ulong MessageId { get; set; }
+    
+    [MaxLength(100)]
+    public string Emote { get; set; }
+    public ulong RoleId { get; set; }
+    public int Group { get; set; }
+    public int LevelReq { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/Reminder.cs b/src/EllieBot/Db/Models/Reminder.cs
new file mode 100644
index 0000000..df51f6d
--- /dev/null
+++ b/src/EllieBot/Db/Models/Reminder.cs
@@ -0,0 +1,12 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class Reminder : DbEntity
+{
+    public DateTime When { get; set; }
+    public ulong ChannelId { get; set; }
+    public ulong ServerId { get; set; }
+    public ulong UserId { get; set; }
+    public string Message { get; set; }
+    public bool IsPrivate { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/Repeater.cs b/src/EllieBot/Db/Models/Repeater.cs
new file mode 100644
index 0000000..7ac971e
--- /dev/null
+++ b/src/EllieBot/Db/Models/Repeater.cs
@@ -0,0 +1,15 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class Repeater
+{
+    public int Id { get; set; }
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+    public ulong? LastMessageId { get; set; }
+    public string Message { get; set; }
+    public TimeSpan Interval { get; set; }
+    public TimeSpan? StartTimeOfDay { get; set; }
+    public bool NoRedundant { get; set; }
+    public DateTime DateAdded { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/RewardedUser.cs b/src/EllieBot/Db/Models/RewardedUser.cs
new file mode 100644
index 0000000..9ac6e78
--- /dev/null
+++ b/src/EllieBot/Db/Models/RewardedUser.cs
@@ -0,0 +1,10 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class RewardedUser : DbEntity
+{
+    public ulong UserId { get; set; }
+    public string PlatformUserId { get; set; }
+    public long AmountRewardedThisMonth { get; set; }
+    public DateTime LastReward { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/RotatingPlayingStatus.cs b/src/EllieBot/Db/Models/RotatingPlayingStatus.cs
new file mode 100644
index 0000000..a76c38f
--- /dev/null
+++ b/src/EllieBot/Db/Models/RotatingPlayingStatus.cs
@@ -0,0 +1,8 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class RotatingPlayingStatus : DbEntity
+{
+    public string Status { get; set; }
+    public ActivityType Type { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/SelfAssignableRole.cs b/src/EllieBot/Db/Models/SelfAssignableRole.cs
new file mode 100644
index 0000000..df6cd48
--- /dev/null
+++ b/src/EllieBot/Db/Models/SelfAssignableRole.cs
@@ -0,0 +1,11 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class SelfAssignedRole : DbEntity
+{
+    public ulong GuildId { get; set; }
+    public ulong RoleId { get; set; }
+    
+    public int Group { get; set; }
+    public int LevelRequirement { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/ShopEntry.cs b/src/EllieBot/Db/Models/ShopEntry.cs
new file mode 100644
index 0000000..ed83cb1
--- /dev/null
+++ b/src/EllieBot/Db/Models/ShopEntry.cs
@@ -0,0 +1,43 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public enum ShopEntryType
+{
+    Role,
+
+    List
+    //Infinite_List,
+}
+
+public class ShopEntry : DbEntity, IIndexed
+{
+    public int Index { get; set; }
+    public int Price { get; set; }
+    public string Name { get; set; }
+    public ulong AuthorId { get; set; }
+
+    public ShopEntryType Type { get; set; }
+
+    //role
+    public string RoleName { get; set; }
+    public ulong RoleId { get; set; }
+
+    //list
+    public HashSet<ShopEntryItem> Items { get; set; } = new();
+    public ulong? RoleRequirement { get; set; }
+}
+
+public class ShopEntryItem : DbEntity
+{
+    public string Text { get; set; }
+
+    public override bool Equals(object obj)
+    {
+        if (obj is null || GetType() != obj.GetType())
+            return false;
+        return ((ShopEntryItem)obj).Text == Text;
+    }
+
+    public override int GetHashCode()
+        => Text.GetHashCode(StringComparison.InvariantCulture);
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/SlowmodeIgnoredRole.cs b/src/EllieBot/Db/Models/SlowmodeIgnoredRole.cs
new file mode 100644
index 0000000..a2d6bdd
--- /dev/null
+++ b/src/EllieBot/Db/Models/SlowmodeIgnoredRole.cs
@@ -0,0 +1,20 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class SlowmodeIgnoredRole : DbEntity
+{
+    public ulong RoleId { get; set; }
+
+    // override object.Equals
+    public override bool Equals(object obj)
+    {
+        if (obj is null || GetType() != obj.GetType())
+            return false;
+
+        return ((SlowmodeIgnoredRole)obj).RoleId == RoleId;
+    }
+
+    // override object.GetHashCode
+    public override int GetHashCode()
+        => RoleId.GetHashCode();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/SlowmodeIgnoredUser.cs b/src/EllieBot/Db/Models/SlowmodeIgnoredUser.cs
new file mode 100644
index 0000000..92597c3
--- /dev/null
+++ b/src/EllieBot/Db/Models/SlowmodeIgnoredUser.cs
@@ -0,0 +1,20 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class SlowmodeIgnoredUser : DbEntity
+{
+    public ulong UserId { get; set; }
+
+    // override object.Equals
+    public override bool Equals(object obj)
+    {
+        if (obj is null || GetType() != obj.GetType())
+            return false;
+
+        return ((SlowmodeIgnoredUser)obj).UserId == UserId;
+    }
+
+    // override object.GetHashCode
+    public override int GetHashCode()
+        => UserId.GetHashCode();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/StreamOnlineMessage.cs b/src/EllieBot/Db/Models/StreamOnlineMessage.cs
new file mode 100644
index 0000000..e9b1e24
--- /dev/null
+++ b/src/EllieBot/Db/Models/StreamOnlineMessage.cs
@@ -0,0 +1,13 @@
+#nullable disable
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db.Models;
+
+public class StreamOnlineMessage : DbEntity
+{
+    public ulong ChannelId { get; set; }
+    public ulong MessageId { get; set; }
+
+    public FollowedStream.FType Type { get; set; }
+    public string Name { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/StreamRoleSettings.cs b/src/EllieBot/Db/Models/StreamRoleSettings.cs
new file mode 100644
index 0000000..dbab2d0
--- /dev/null
+++ b/src/EllieBot/Db/Models/StreamRoleSettings.cs
@@ -0,0 +1,68 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class StreamRoleSettings : DbEntity
+{
+    public int GuildConfigId { get; set; }
+    public GuildConfig GuildConfig { get; set; }
+
+    /// <summary>
+    ///     Whether the feature is enabled in the guild.
+    /// </summary>
+    public bool Enabled { get; set; }
+
+    /// <summary>
+    ///     Id of the role to give to the users in the role 'FromRole' when they start streaming
+    /// </summary>
+    public ulong AddRoleId { get; set; }
+
+    /// <summary>
+    ///     Id of the role whose users are eligible to get the 'AddRole'
+    /// </summary>
+    public ulong FromRoleId { get; set; }
+
+    /// <summary>
+    ///     If set, feature will only apply to users who have this keyword in their streaming status.
+    /// </summary>
+    public string Keyword { get; set; }
+
+    /// <summary>
+    ///     A collection of whitelisted users' IDs. Whitelisted users don't require 'keyword' in
+    ///     order to get the stream role.
+    /// </summary>
+    public HashSet<StreamRoleWhitelistedUser> Whitelist { get; set; } = new();
+
+    /// <summary>
+    ///     A collection of blacklisted users' IDs. Blacklisted useres will never get the stream role.
+    /// </summary>
+    public HashSet<StreamRoleBlacklistedUser> Blacklist { get; set; } = new();
+}
+
+public class StreamRoleBlacklistedUser : DbEntity
+{
+    public ulong UserId { get; set; }
+    public string Username { get; set; }
+
+    public override bool Equals(object obj)
+    {
+        if (obj is not StreamRoleBlacklistedUser x)
+            return false;
+
+        return x.UserId == UserId;
+    }
+
+    public override int GetHashCode()
+        => UserId.GetHashCode();
+}
+
+public class StreamRoleWhitelistedUser : DbEntity
+{
+    public ulong UserId { get; set; }
+    public string Username { get; set; }
+
+    public override bool Equals(object obj)
+        => obj is StreamRoleWhitelistedUser x ? x.UserId == UserId : false;
+
+    public override int GetHashCode()
+        => UserId.GetHashCode();
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/UnbanTimer.cs b/src/EllieBot/Db/Models/UnbanTimer.cs
new file mode 100644
index 0000000..b017624
--- /dev/null
+++ b/src/EllieBot/Db/Models/UnbanTimer.cs
@@ -0,0 +1,14 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class UnbanTimer : DbEntity
+{
+    public ulong UserId { get; set; }
+    public DateTime UnbanAt { get; set; }
+
+    public override int GetHashCode()
+        => UserId.GetHashCode();
+
+    public override bool Equals(object obj)
+        => obj is UnbanTimer ut ? ut.UserId == UserId : false;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/UnmuteTimer.cs b/src/EllieBot/Db/Models/UnmuteTimer.cs
new file mode 100644
index 0000000..4619b68
--- /dev/null
+++ b/src/EllieBot/Db/Models/UnmuteTimer.cs
@@ -0,0 +1,14 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class UnmuteTimer : DbEntity
+{
+    public ulong UserId { get; set; }
+    public DateTime UnmuteAt { get; set; }
+
+    public override int GetHashCode()
+        => UserId.GetHashCode();
+
+    public override bool Equals(object obj) 
+        => obj is UnmuteTimer ut ? ut.UserId == UserId : false;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/UnroleTimer.cs b/src/EllieBot/Db/Models/UnroleTimer.cs
new file mode 100644
index 0000000..f7c7f8d
--- /dev/null
+++ b/src/EllieBot/Db/Models/UnroleTimer.cs
@@ -0,0 +1,15 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class UnroleTimer : DbEntity
+{
+    public ulong UserId { get; set; }
+    public ulong RoleId { get; set; }
+    public DateTime UnbanAt { get; set; }
+
+    public override int GetHashCode()
+        => UserId.GetHashCode() ^ RoleId.GetHashCode();
+
+    public override bool Equals(object obj)
+        => obj is UnroleTimer ut ? ut.UserId == UserId && ut.RoleId == RoleId : false;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/UserXpStats.cs b/src/EllieBot/Db/Models/UserXpStats.cs
new file mode 100644
index 0000000..e79f735
--- /dev/null
+++ b/src/EllieBot/Db/Models/UserXpStats.cs
@@ -0,0 +1,13 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class UserXpStats : DbEntity
+{
+    public ulong UserId { get; set; }
+    public ulong GuildId { get; set; }
+    public long Xp { get; set; }
+    public long AwardedXp { get; set; }
+    public XpNotificationLocation NotifyOnLevelUp { get; set; }
+}
+
+public enum XpNotificationLocation { None, Dm, Channel }
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/VcRoleInfo.cs b/src/EllieBot/Db/Models/VcRoleInfo.cs
new file mode 100644
index 0000000..b80fc17
--- /dev/null
+++ b/src/EllieBot/Db/Models/VcRoleInfo.cs
@@ -0,0 +1,8 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class VcRoleInfo : DbEntity
+{
+    public ulong VoiceChannelId { get; set; }
+    public ulong RoleId { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/Waifu.cs b/src/EllieBot/Db/Models/Waifu.cs
new file mode 100644
index 0000000..a140be5
--- /dev/null
+++ b/src/EllieBot/Db/Models/Waifu.cs
@@ -0,0 +1,75 @@
+#nullable disable
+using EllieBot.Db.Models;
+
+namespace EllieBot.Services.Database.Models;
+
+public class WaifuInfo : DbEntity
+{
+    public int WaifuId { get; set; }
+    public DiscordUser Waifu { get; set; }
+    
+    public int? ClaimerId { get; set; }
+    public DiscordUser Claimer { get; set; }
+
+    public int? AffinityId { get; set; }
+    public DiscordUser Affinity { get; set; }
+
+    public long Price { get; set; }
+    public List<WaifuItem> Items { get; set; } = new();
+
+    public override string ToString()
+    {
+        var claimer = "no one";
+        var status = string.Empty;
+
+        var waifuUsername = Waifu.Username.TrimTo(20);
+        var claimerUsername = Claimer?.Username.TrimTo(20);
+
+        if (ClaimerId is not null)
+            claimer = $"{claimerUsername}#{Claimer.Discriminator}";
+        if (AffinityId is null)
+            status = $"... but {waifuUsername}'s heart is empty";
+        else if (AffinityId == ClaimerId)
+            status = $"... and {waifuUsername} likes {claimerUsername} too <3";
+        else
+        {
+            status =
+                $"... but {waifuUsername}'s heart belongs to {Affinity.Username.TrimTo(20)}#{Affinity.Discriminator}";
+        }
+
+        return $"**{waifuUsername}#{Waifu.Discriminator}** - claimed by **{claimer}**\n\t{status}";
+    }
+}
+
+public class WaifuLbResult
+{
+    public string Username { get; set; }
+    public string Discrim { get; set; }
+
+    public string Claimer { get; set; }
+    public string ClaimerDiscrim { get; set; }
+
+    public string Affinity { get; set; }
+    public string AffinityDiscrim { get; set; }
+
+    public long Price { get; set; }
+
+    public override string ToString()
+    {
+        var claimer = "no one";
+        var status = string.Empty;
+
+        var waifuUsername = Username.TrimTo(20);
+        var claimerUsername = Claimer?.TrimTo(20);
+
+        if (Claimer is not null)
+            claimer = $"{claimerUsername}#{ClaimerDiscrim}";
+        if (Affinity is null)
+            status = $"... but {waifuUsername}'s heart is empty";
+        else if (Affinity + AffinityDiscrim == Claimer + ClaimerDiscrim)
+            status = $"... and {waifuUsername} likes {claimerUsername} too <3";
+        else
+            status = $"... but {waifuUsername}'s heart belongs to {Affinity.TrimTo(20)}#{AffinityDiscrim}";
+        return $"**{waifuUsername}#{Discrim}** - claimed by **{claimer}**\n\t{status}";
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/WaifuItem.cs b/src/EllieBot/Db/Models/WaifuItem.cs
new file mode 100644
index 0000000..32df5ba
--- /dev/null
+++ b/src/EllieBot/Db/Models/WaifuItem.cs
@@ -0,0 +1,10 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class WaifuItem
+{
+    public WaifuInfo WaifuInfo { get; set; }
+    public int? WaifuInfoId { get; set; }
+    public string ItemEmoji { get; set; }
+    public string Name { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/WaifuUpdate.cs b/src/EllieBot/Db/Models/WaifuUpdate.cs
new file mode 100644
index 0000000..70e8960
--- /dev/null
+++ b/src/EllieBot/Db/Models/WaifuUpdate.cs
@@ -0,0 +1,23 @@
+#nullable disable
+using EllieBot.Db.Models;
+
+namespace EllieBot.Services.Database.Models;
+
+public class WaifuUpdate : DbEntity
+{
+    public int UserId { get; set; }
+    public DiscordUser User { get; set; }
+    public WaifuUpdateType UpdateType { get; set; }
+
+    public int? OldId { get; set; }
+    public DiscordUser Old { get; set; }
+
+    public int? NewId { get; set; }
+    public DiscordUser New { get; set; }
+}
+
+public enum WaifuUpdateType
+{
+    AffinityChanged,
+    Claimed
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/WarnExpireAction.cs b/src/EllieBot/Db/Models/WarnExpireAction.cs
new file mode 100644
index 0000000..caa595f
--- /dev/null
+++ b/src/EllieBot/Db/Models/WarnExpireAction.cs
@@ -0,0 +1,8 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public enum WarnExpireAction
+{
+    Clear,
+    Delete
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/Warning.cs b/src/EllieBot/Db/Models/Warning.cs
new file mode 100644
index 0000000..9bd9b41
--- /dev/null
+++ b/src/EllieBot/Db/Models/Warning.cs
@@ -0,0 +1,13 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class Warning : DbEntity
+{
+    public ulong GuildId { get; set; }
+    public ulong UserId { get; set; }
+    public string Reason { get; set; }
+    public bool Forgiven { get; set; }
+    public string ForgivenBy { get; set; }
+    public string Moderator { get; set; }
+    public long Weight { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/WarningPunishment.cs b/src/EllieBot/Db/Models/WarningPunishment.cs
new file mode 100644
index 0000000..02590cd
--- /dev/null
+++ b/src/EllieBot/Db/Models/WarningPunishment.cs
@@ -0,0 +1,10 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class WarningPunishment : DbEntity
+{
+    public int Count { get; set; }
+    public PunishmentAction Punishment { get; set; }
+    public int Time { get; set; }
+    public ulong? RoleId { get; set; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/XpSettings.cs b/src/EllieBot/Db/Models/XpSettings.cs
new file mode 100644
index 0000000..066cd07
--- /dev/null
+++ b/src/EllieBot/Db/Models/XpSettings.cs
@@ -0,0 +1,62 @@
+#nullable disable
+namespace EllieBot.Services.Database.Models;
+
+public class XpSettings : DbEntity
+{
+    public int GuildConfigId { get; set; }
+    public GuildConfig GuildConfig { get; set; }
+
+    public HashSet<XpRoleReward> RoleRewards { get; set; } = new();
+    public HashSet<XpCurrencyReward> CurrencyRewards { get; set; } = new();
+    public HashSet<ExcludedItem> ExclusionList { get; set; } = new();
+    public bool ServerExcluded { get; set; }
+}
+
+public enum ExcludedItemType { Channel, Role }
+
+public class XpRoleReward : DbEntity
+{
+    public int XpSettingsId { get; set; }
+    public XpSettings XpSettings { get; set; }
+
+    public int Level { get; set; }
+    public ulong RoleId { get; set; }
+
+    /// <summary>
+    ///     Whether the role should be removed (true) or added (false)
+    /// </summary>
+    public bool Remove { get; set; }
+
+    public override int GetHashCode()
+        => Level.GetHashCode() ^ XpSettingsId.GetHashCode();
+
+    public override bool Equals(object obj)
+        => obj is XpRoleReward xrr && xrr.Level == Level && xrr.XpSettingsId == XpSettingsId;
+}
+
+public class XpCurrencyReward : DbEntity
+{
+    public int XpSettingsId { get; set; }
+    public XpSettings XpSettings { get; set; }
+
+    public int Level { get; set; }
+    public int Amount { get; set; }
+
+    public override int GetHashCode()
+        => Level.GetHashCode() ^ XpSettingsId.GetHashCode();
+
+    public override bool Equals(object obj)
+        => obj is XpCurrencyReward xrr && xrr.Level == Level && xrr.XpSettingsId == XpSettingsId;
+}
+
+public class ExcludedItem : DbEntity
+{
+    public ulong ItemId { get; set; }
+    public ExcludedItemType ItemType { get; set; }
+
+    public override int GetHashCode()
+        => ItemId.GetHashCode() ^ ItemType.GetHashCode();
+
+    public override bool Equals(object obj)
+        => obj is ExcludedItem ei && ei.ItemId == ItemId && ei.ItemType == ItemType;
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/Models/XpShotOwnedItem.cs b/src/EllieBot/Db/Models/XpShotOwnedItem.cs
new file mode 100644
index 0000000..aaba2f6
--- /dev/null
+++ b/src/EllieBot/Db/Models/XpShotOwnedItem.cs
@@ -0,0 +1,18 @@
+#nullable disable
+using EllieBot.Services.Database.Models;
+
+namespace EllieBot.Db.Models;
+
+public class XpShopOwnedItem : DbEntity
+{
+    public ulong UserId { get; set; }
+    public XpShopItemType ItemType { get; set; }
+    public bool IsUsing { get; set; }
+    public string ItemKey { get; set; }
+}
+
+public enum XpShopItemType
+{
+    Background = 0,
+    Frame = 1,
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/MysqlContext.cs b/src/EllieBot/Db/MysqlContext.cs
new file mode 100644
index 0000000..5406c1d
--- /dev/null
+++ b/src/EllieBot/Db/MysqlContext.cs
@@ -0,0 +1,38 @@
+using Microsoft.EntityFrameworkCore;
+using EllieBot.Db.Models;
+
+namespace EllieBot.Services.Database;
+
+public sealed class MysqlContext : EllieContext
+{
+    private readonly string _connStr;
+    private readonly string _version;
+
+    protected override string CurrencyTransactionOtherIdDefaultValue 
+        => "NULL";
+
+    public MysqlContext(string connStr = "Server=localhost", string version = "8.0")
+    {
+        _connStr = connStr;
+        _version = version;
+    }
+
+    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+    {
+        base.OnConfiguring(optionsBuilder);
+        optionsBuilder
+            .UseLowerCaseNamingConvention()
+            .UseMySql(_connStr, ServerVersion.Parse(_version));
+    }
+
+    protected override void OnModelCreating(ModelBuilder modelBuilder)
+    {
+        base.OnModelCreating(modelBuilder);
+        
+        // mysql is case insensitive by default
+        // we can set binary collation to change that
+        modelBuilder.Entity<ClubInfo>()
+                    .Property(x => x.Name)
+                    .UseCollation("utf8mb4_bin");
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/PostgreSqlContext.cs b/src/EllieBot/Db/PostgreSqlContext.cs
new file mode 100644
index 0000000..f254aaf
--- /dev/null
+++ b/src/EllieBot/Db/PostgreSqlContext.cs
@@ -0,0 +1,26 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace EllieBot.Services.Database;
+
+public class PostgreSqlContext : EllieContext
+{
+    private readonly string _connStr;
+
+    protected override string CurrencyTransactionOtherIdDefaultValue 
+        => "NULL";
+
+    public PostgreSqlContext(string connStr = "Host=localhost")
+    {
+        _connStr = connStr;
+    }
+
+    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+    {
+        AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
+        
+        base.OnConfiguring(optionsBuilder);
+        optionsBuilder
+            .UseLowerCaseNamingConvention()
+            .UseNpgsql(_connStr);
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Db/SqliteContext.cs b/src/EllieBot/Db/SqliteContext.cs
new file mode 100644
index 0000000..6744423
--- /dev/null
+++ b/src/EllieBot/Db/SqliteContext.cs
@@ -0,0 +1,26 @@
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+
+namespace EllieBot.Services.Database;
+
+public sealed class SqliteContext : EllieContext
+{
+    private readonly string _connectionString;
+
+    protected override string CurrencyTransactionOtherIdDefaultValue 
+        => "NULL";
+
+    public SqliteContext(string connectionString = "Data Source=data/EllieBot.db", int commandTimeout = 60)
+    {
+        _connectionString = connectionString;
+        Database.SetCommandTimeout(commandTimeout);
+    }
+
+    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+    {
+        base.OnConfiguring(optionsBuilder);
+        var builder = new SqliteConnectionStringBuilder(_connectionString);
+        builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource);
+        optionsBuilder.UseSqlite(builder.ToString());
+    }
+}
\ No newline at end of file