From 5e95abadc884cb924266d8071589f175a5853b48 Mon Sep 17 00:00:00 2001
From: Toastie <toastie@toastiet0ast.com>
Date: Sat, 1 Mar 2025 13:04:20 +1300
Subject: [PATCH] Added .xprate and .xpratereset which let users modify xp
 rates on their server, as there is no more global xp .xpexcl removed as you
 can exclude xp gain on the server or channel by setting any value in xprate
 to 0

---
 .../20250225212147_xp-excl-xp-rate.sql        |  27 ++
 ...ner.cs => 20250225212209_init.Designer.cs} |  71 ++++-
 ...2124905_init.cs => 20250225212209_init.cs} |  43 ++-
 .../PostgreSqlContextModelSnapshot.cs         |  69 ++++-
 .../Sqlite/20250225212144_xp-excl-xp-rate.sql |  25 ++
 ...ner.cs => 20250225212206_init.Designer.cs} |  55 +++-
 ...2124903_init.cs => 20250225212206_init.cs} |  44 ++-
 .../Sqlite/SqliteContextModelSnapshot.cs      |  53 +++-
 src/EllieBot/Modules/Xp/Xp.cs                 |  85 ------
 .../Modules/Xp/XpRate/XpRateCommands.cs       | 254 ++++++++++++++++++
 src/EllieBot/strings/aliases.yml              |  12 +-
 .../strings/commands/commands.en-US.yml       |  55 ++--
 .../strings/responses/responses.en-US.json    |  18 +-
 13 files changed, 680 insertions(+), 131 deletions(-)
 create mode 100644 src/EllieBot/Migrations/PostgreSql/20250225212147_xp-excl-xp-rate.sql
 rename src/EllieBot/Migrations/PostgreSql/{20250202124905_init.Designer.cs => 20250225212209_init.Designer.cs} (98%)
 rename src/EllieBot/Migrations/PostgreSql/{20250202124905_init.cs => 20250225212209_init.cs} (98%)
 create mode 100644 src/EllieBot/Migrations/Sqlite/20250225212144_xp-excl-xp-rate.sql
 rename src/EllieBot/Migrations/Sqlite/{20250202124903_init.Designer.cs => 20250225212206_init.Designer.cs} (98%)
 rename src/EllieBot/Migrations/Sqlite/{20250202124903_init.cs => 20250225212206_init.cs} (98%)
 create mode 100644 src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs

diff --git a/src/EllieBot/Migrations/PostgreSql/20250225212147_xp-excl-xp-rate.sql b/src/EllieBot/Migrations/PostgreSql/20250225212147_xp-excl-xp-rate.sql
new file mode 100644
index 0000000..16e3c9f
--- /dev/null
+++ b/src/EllieBot/Migrations/PostgreSql/20250225212147_xp-excl-xp-rate.sql
@@ -0,0 +1,27 @@
+START TRANSACTION;
+ALTER TABLE userfishstats ADD bait integer;
+
+ALTER TABLE userfishstats ADD pole integer;
+
+CREATE TABLE channelxpconfig (
+    id integer GENERATED BY DEFAULT AS IDENTITY,
+    guildid numeric(20,0) NOT NULL,
+    channelid numeric(20,0) NOT NULL,
+    xpamount integer NOT NULL,
+    cooldown real NOT NULL,
+    CONSTRAINT pk_channelxpconfig PRIMARY KEY (id),
+    CONSTRAINT ak_channelxpconfig_guildid_channelid UNIQUE (guildid, channelid)
+);
+
+CREATE TABLE guildxpconfig (
+    guildid numeric(20,0) NOT NULL,
+    xpamount integer NOT NULL,
+    cooldown integer NOT NULL,
+    xptemplateurl text,
+    CONSTRAINT pk_guildxpconfig PRIMARY KEY (guildid)
+);
+
+INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
+VALUES ('20250225212147_xp-excl-xp-rate', '9.0.1');
+
+COMMIT;
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/PostgreSql/20250202124905_init.Designer.cs b/src/EllieBot/Migrations/PostgreSql/20250225212209_init.Designer.cs
similarity index 98%
rename from src/EllieBot/Migrations/PostgreSql/20250202124905_init.Designer.cs
rename to src/EllieBot/Migrations/PostgreSql/20250225212209_init.Designer.cs
index b329303..66f7870 100644
--- a/src/EllieBot/Migrations/PostgreSql/20250202124905_init.Designer.cs
+++ b/src/EllieBot/Migrations/PostgreSql/20250225212209_init.Designer.cs
@@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 namespace EllieBot.Migrations.PostgreSql
 {
     [DbContext(typeof(PostgreSqlContext))]
-    [Migration("20250202124905_init")]
+    [Migration("20250225212209_init")]
     partial class init
     {
         /// <inheritdoc />
@@ -3456,6 +3456,14 @@ namespace EllieBot.Migrations.PostgreSql
 
                 NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 
+                b.Property<int?>("Bait")
+                    .HasColumnType("integer")
+                    .HasColumnName("bait");
+
+                b.Property<int?>("Pole")
+                    .HasColumnType("integer")
+                    .HasColumnName("pole");
+
                 b.Property<int>("Skill")
                     .HasColumnType("integer")
                     .HasColumnName("skill");
@@ -3474,6 +3482,65 @@ namespace EllieBot.Migrations.PostgreSql
                 b.ToTable("userfishstats", (string)null);
             });
 
+            modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
+            {
+                b.Property<int>("Id")
+                    .ValueGeneratedOnAdd()
+                    .HasColumnType("integer")
+                    .HasColumnName("id");
+
+                NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                b.Property<decimal>("ChannelId")
+                    .HasColumnType("numeric(20,0)")
+                    .HasColumnName("channelid");
+
+                b.Property<float>("Cooldown")
+                    .HasColumnType("real")
+                    .HasColumnName("cooldown");
+
+                b.Property<decimal>("GuildId")
+                    .HasColumnType("numeric(20,0)")
+                    .HasColumnName("guildid");
+
+                b.Property<int>("XpAmount")
+                    .HasColumnType("integer")
+                    .HasColumnName("xpamount");
+
+                b.HasKey("Id")
+                    .HasName("pk_channelxpconfig");
+
+                b.HasAlternateKey("GuildId", "ChannelId")
+                    .HasName("ak_channelxpconfig_guildid_channelid");
+
+                b.ToTable("channelxpconfig", (string)null);
+            });
+
+            modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b =>
+            {
+                b.Property<decimal>("GuildId")
+                    .ValueGeneratedOnAdd()
+                    .HasColumnType("numeric(20,0)")
+                    .HasColumnName("guildid");
+
+                b.Property<int>("Cooldown")
+                    .HasColumnType("integer")
+                    .HasColumnName("cooldown");
+
+                b.Property<int>("XpAmount")
+                    .HasColumnType("integer")
+                    .HasColumnName("xpamount");
+
+                b.Property<string>("XpTemplateUrl")
+                    .HasColumnType("text")
+                    .HasColumnName("xptemplateurl");
+
+                b.HasKey("GuildId")
+                    .HasName("pk_guildxpconfig");
+
+                b.ToTable("guildxpconfig", (string)null);
+            });
+
             modelBuilder.Entity("EllieBot.Services.GreetSettings", b =>
             {
                 b.Property<int>("Id")
@@ -3978,4 +4045,4 @@ namespace EllieBot.Migrations.PostgreSql
 #pragma warning restore 612, 618
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/PostgreSql/20250202124905_init.cs b/src/EllieBot/Migrations/PostgreSql/20250225212209_init.cs
similarity index 98%
rename from src/EllieBot/Migrations/PostgreSql/20250202124905_init.cs
rename to src/EllieBot/Migrations/PostgreSql/20250225212209_init.cs
index 6e4032b..bf7ff89 100644
--- a/src/EllieBot/Migrations/PostgreSql/20250202124905_init.cs
+++ b/src/EllieBot/Migrations/PostgreSql/20250225212209_init.cs
@@ -187,6 +187,23 @@ namespace EllieBot.Migrations.PostgreSql
                     table.UniqueConstraint("ak_buttonrole_roleid_messageid", x => new { x.roleid, x.messageid });
                 });
 
+            migrationBuilder.CreateTable(
+                name: "channelxpconfig",
+                columns: table => new
+                {
+                    id = table.Column<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
+                    channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
+                    xpamount = table.Column<int>(type: "integer", nullable: false),
+                    cooldown = table.Column<float>(type: "real", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("pk_channelxpconfig", x => x.id);
+                    table.UniqueConstraint("ak_channelxpconfig_guildid_channelid", x => new { x.guildid, x.channelid });
+                });
+
             migrationBuilder.CreateTable(
                 name: "commandalias",
                 columns: table => new
@@ -487,6 +504,20 @@ namespace EllieBot.Migrations.PostgreSql
                     table.PrimaryKey("pk_guildfilterconfig", x => x.id);
                 });
 
+            migrationBuilder.CreateTable(
+                name: "guildxpconfig",
+                columns: table => new
+                {
+                    guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
+                    xpamount = table.Column<int>(type: "integer", nullable: false),
+                    cooldown = table.Column<int>(type: "integer", nullable: false),
+                    xptemplateurl = table.Column<string>(type: "text", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("pk_guildxpconfig", x => x.guildid);
+                });
+
             migrationBuilder.CreateTable(
                 name: "honeypotchannels",
                 columns: table => new
@@ -1033,7 +1064,9 @@ namespace EllieBot.Migrations.PostgreSql
                     id = table.Column<int>(type: "integer", nullable: false)
                         .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
                     userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
-                    skill = table.Column<int>(type: "integer", nullable: false)
+                    skill = table.Column<int>(type: "integer", nullable: false),
+                    pole = table.Column<int>(type: "integer", nullable: true),
+                    bait = table.Column<int>(type: "integer", nullable: true)
                 },
                 constraints: table =>
                 {
@@ -2310,6 +2343,9 @@ namespace EllieBot.Migrations.PostgreSql
             migrationBuilder.DropTable(
                 name: "buttonrole");
 
+            migrationBuilder.DropTable(
+                name: "channelxpconfig");
+
             migrationBuilder.DropTable(
                 name: "clubapplicants");
 
@@ -2376,6 +2412,9 @@ namespace EllieBot.Migrations.PostgreSql
             migrationBuilder.DropTable(
                 name: "guildcolors");
 
+            migrationBuilder.DropTable(
+                name: "guildxpconfig");
+
             migrationBuilder.DropTable(
                 name: "honeypotchannels");
 
@@ -2551,4 +2590,4 @@ namespace EllieBot.Migrations.PostgreSql
                 name: "discorduser");
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
index 5d110ab..d58940a 100644
--- a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
+++ b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs
@@ -3453,6 +3453,14 @@ namespace EllieBot.Migrations.PostgreSql
 
                 NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 
+                b.Property<int?>("Bait")
+                    .HasColumnType("integer")
+                    .HasColumnName("bait");
+
+                b.Property<int?>("Pole")
+                    .HasColumnType("integer")
+                    .HasColumnName("pole");
+
                 b.Property<int>("Skill")
                     .HasColumnType("integer")
                     .HasColumnName("skill");
@@ -3471,6 +3479,65 @@ namespace EllieBot.Migrations.PostgreSql
                 b.ToTable("userfishstats", (string)null);
             });
 
+            modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
+            {
+                b.Property<int>("Id")
+                    .ValueGeneratedOnAdd()
+                    .HasColumnType("integer")
+                    .HasColumnName("id");
+
+                NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                b.Property<decimal>("ChannelId")
+                    .HasColumnType("numeric(20,0)")
+                    .HasColumnName("channelid");
+
+                b.Property<float>("Cooldown")
+                    .HasColumnType("real")
+                    .HasColumnName("cooldown");
+
+                b.Property<decimal>("GuildId")
+                    .HasColumnType("numeric(20,0)")
+                    .HasColumnName("guildid");
+
+                b.Property<int>("XpAmount")
+                    .HasColumnType("integer")
+                    .HasColumnName("xpamount");
+
+                b.HasKey("Id")
+                    .HasName("pk_channelxpconfig");
+
+                b.HasAlternateKey("GuildId", "ChannelId")
+                    .HasName("ak_channelxpconfig_guildid_channelid");
+
+                b.ToTable("channelxpconfig", (string)null);
+            });
+
+            modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b =>
+            {
+                b.Property<decimal>("GuildId")
+                    .ValueGeneratedOnAdd()
+                    .HasColumnType("numeric(20,0)")
+                    .HasColumnName("guildid");
+
+                b.Property<int>("Cooldown")
+                    .HasColumnType("integer")
+                    .HasColumnName("cooldown");
+
+                b.Property<int>("XpAmount")
+                    .HasColumnType("integer")
+                    .HasColumnName("xpamount");
+
+                b.Property<string>("XpTemplateUrl")
+                    .HasColumnType("text")
+                    .HasColumnName("xptemplateurl");
+
+                b.HasKey("GuildId")
+                    .HasName("pk_guildxpconfig");
+
+                b.ToTable("guildxpconfig", (string)null);
+            });
+
             modelBuilder.Entity("EllieBot.Services.GreetSettings", b =>
             {
                 b.Property<int>("Id")
@@ -3975,4 +4042,4 @@ namespace EllieBot.Migrations.PostgreSql
 #pragma warning restore 612, 618
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/Sqlite/20250225212144_xp-excl-xp-rate.sql b/src/EllieBot/Migrations/Sqlite/20250225212144_xp-excl-xp-rate.sql
new file mode 100644
index 0000000..26ee2fa
--- /dev/null
+++ b/src/EllieBot/Migrations/Sqlite/20250225212144_xp-excl-xp-rate.sql
@@ -0,0 +1,25 @@
+BEGIN TRANSACTION;
+ALTER TABLE "UserFishStats" ADD "Bait" INTEGER NULL;
+
+ALTER TABLE "UserFishStats" ADD "Pole" INTEGER NULL;
+
+CREATE TABLE "ChannelXpConfig" (
+    "Id" INTEGER NOT NULL CONSTRAINT "PK_ChannelXpConfig" PRIMARY KEY AUTOINCREMENT,
+    "GuildId" INTEGER NOT NULL,
+    "ChannelId" INTEGER NOT NULL,
+    "XpAmount" INTEGER NOT NULL,
+    "Cooldown" REAL NOT NULL,
+    CONSTRAINT "AK_ChannelXpConfig_GuildId_ChannelId" UNIQUE ("GuildId", "ChannelId")
+);
+
+CREATE TABLE "GuildXpConfig" (
+    "GuildId" INTEGER NOT NULL CONSTRAINT "PK_GuildXpConfig" PRIMARY KEY AUTOINCREMENT,
+    "XpAmount" INTEGER NOT NULL,
+    "Cooldown" INTEGER NOT NULL,
+    "XpTemplateUrl" TEXT NULL
+);
+
+INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
+VALUES ('20250225212144_xp-excl-xp-rate', '9.0.1');
+
+COMMIT;
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/Sqlite/20250202124903_init.Designer.cs b/src/EllieBot/Migrations/Sqlite/20250225212206_init.Designer.cs
similarity index 98%
rename from src/EllieBot/Migrations/Sqlite/20250202124903_init.Designer.cs
rename to src/EllieBot/Migrations/Sqlite/20250225212206_init.Designer.cs
index ffb15cb..483367d 100644
--- a/src/EllieBot/Migrations/Sqlite/20250202124903_init.Designer.cs
+++ b/src/EllieBot/Migrations/Sqlite/20250225212206_init.Designer.cs
@@ -11,7 +11,7 @@ using EllieBot.Db;
 namespace EllieBot.Migrations.Sqlite
 {
     [DbContext(typeof(SqliteContext))]
-    [Migration("20250202124903_init")]
+    [Migration("20250225212206_init")]
     partial class init
     {
         /// <inheritdoc />
@@ -2571,6 +2571,12 @@ namespace EllieBot.Migrations.Sqlite
                     .ValueGeneratedOnAdd()
                     .HasColumnType("INTEGER");
 
+                b.Property<int?>("Bait")
+                    .HasColumnType("INTEGER");
+
+                b.Property<int?>("Pole")
+                    .HasColumnType("INTEGER");
+
                 b.Property<int>("Skill")
                     .HasColumnType("INTEGER");
 
@@ -2585,6 +2591,51 @@ namespace EllieBot.Migrations.Sqlite
                 b.ToTable("UserFishStats");
             });
 
+            modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
+            {
+                b.Property<int>("Id")
+                    .ValueGeneratedOnAdd()
+                    .HasColumnType("INTEGER");
+
+                b.Property<ulong>("ChannelId")
+                    .HasColumnType("INTEGER");
+
+                b.Property<float>("Cooldown")
+                    .HasColumnType("REAL");
+
+                b.Property<ulong>("GuildId")
+                    .HasColumnType("INTEGER");
+
+                b.Property<int>("XpAmount")
+                    .HasColumnType("INTEGER");
+
+                b.HasKey("Id");
+
+                b.HasAlternateKey("GuildId", "ChannelId");
+
+                b.ToTable("ChannelXpConfig");
+            });
+
+            modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b =>
+            {
+                b.Property<ulong>("GuildId")
+                    .ValueGeneratedOnAdd()
+                    .HasColumnType("INTEGER");
+
+                b.Property<int>("Cooldown")
+                    .HasColumnType("INTEGER");
+
+                b.Property<int>("XpAmount")
+                    .HasColumnType("INTEGER");
+
+                b.Property<string>("XpTemplateUrl")
+                    .HasColumnType("TEXT");
+
+                b.HasKey("GuildId");
+
+                b.ToTable("GuildXpConfig");
+            });
+
             modelBuilder.Entity("EllieBot.Services.GreetSettings", b =>
             {
                 b.Property<int>("Id")
@@ -3030,4 +3081,4 @@ namespace EllieBot.Migrations.Sqlite
 #pragma warning restore 612, 618
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/Sqlite/20250202124903_init.cs b/src/EllieBot/Migrations/Sqlite/20250225212206_init.cs
similarity index 98%
rename from src/EllieBot/Migrations/Sqlite/20250202124903_init.cs
rename to src/EllieBot/Migrations/Sqlite/20250225212206_init.cs
index 1f7ecdc..c18b049 100644
--- a/src/EllieBot/Migrations/Sqlite/20250202124903_init.cs
+++ b/src/EllieBot/Migrations/Sqlite/20250225212206_init.cs
@@ -186,6 +186,23 @@ namespace EllieBot.Migrations.Sqlite
                     table.UniqueConstraint("AK_ButtonRole_RoleId_MessageId", x => new { x.RoleId, x.MessageId });
                 });
 
+            migrationBuilder.CreateTable(
+                name: "ChannelXpConfig",
+                columns: table => new
+                {
+                    Id = table.Column<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
+                    ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
+                    XpAmount = table.Column<int>(type: "INTEGER", nullable: false),
+                    Cooldown = table.Column<float>(type: "REAL", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_ChannelXpConfig", x => x.Id);
+                    table.UniqueConstraint("AK_ChannelXpConfig_GuildId_ChannelId", x => new { x.GuildId, x.ChannelId });
+                });
+
             migrationBuilder.CreateTable(
                 name: "CommandAlias",
                 columns: table => new
@@ -486,6 +503,21 @@ namespace EllieBot.Migrations.Sqlite
                     table.PrimaryKey("PK_GuildFilterConfig", x => x.Id);
                 });
 
+            migrationBuilder.CreateTable(
+                name: "GuildXpConfig",
+                columns: table => new
+                {
+                    GuildId = table.Column<ulong>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    XpAmount = table.Column<int>(type: "INTEGER", nullable: false),
+                    Cooldown = table.Column<int>(type: "INTEGER", nullable: false),
+                    XpTemplateUrl = table.Column<string>(type: "TEXT", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_GuildXpConfig", x => x.GuildId);
+                });
+
             migrationBuilder.CreateTable(
                 name: "HoneyPotChannels",
                 columns: table => new
@@ -1035,7 +1067,9 @@ namespace EllieBot.Migrations.Sqlite
                     Id = table.Column<int>(type: "INTEGER", nullable: false)
                         .Annotation("Sqlite:Autoincrement", true),
                     UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
-                    Skill = table.Column<int>(type: "INTEGER", nullable: false)
+                    Skill = table.Column<int>(type: "INTEGER", nullable: false),
+                    Pole = table.Column<int>(type: "INTEGER", nullable: true),
+                    Bait = table.Column<int>(type: "INTEGER", nullable: true)
                 },
                 constraints: table =>
                 {
@@ -2312,6 +2346,9 @@ namespace EllieBot.Migrations.Sqlite
             migrationBuilder.DropTable(
                 name: "ButtonRole");
 
+            migrationBuilder.DropTable(
+                name: "ChannelXpConfig");
+
             migrationBuilder.DropTable(
                 name: "ClubApplicants");
 
@@ -2378,6 +2415,9 @@ namespace EllieBot.Migrations.Sqlite
             migrationBuilder.DropTable(
                 name: "GuildColors");
 
+            migrationBuilder.DropTable(
+                name: "GuildXpConfig");
+
             migrationBuilder.DropTable(
                 name: "HoneyPotChannels");
 
@@ -2553,4 +2593,4 @@ namespace EllieBot.Migrations.Sqlite
                 name: "DiscordUser");
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs
index 084a0ee..1b4a98c 100644
--- a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs
+++ b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs
@@ -2568,6 +2568,12 @@ namespace EllieBot.Migrations.Sqlite
                     .ValueGeneratedOnAdd()
                     .HasColumnType("INTEGER");
 
+                b.Property<int?>("Bait")
+                    .HasColumnType("INTEGER");
+
+                b.Property<int?>("Pole")
+                    .HasColumnType("INTEGER");
+
                 b.Property<int>("Skill")
                     .HasColumnType("INTEGER");
 
@@ -2582,6 +2588,51 @@ namespace EllieBot.Migrations.Sqlite
                 b.ToTable("UserFishStats");
             });
 
+            modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
+            {
+                b.Property<int>("Id")
+                    .ValueGeneratedOnAdd()
+                    .HasColumnType("INTEGER");
+
+                b.Property<ulong>("ChannelId")
+                    .HasColumnType("INTEGER");
+
+                b.Property<float>("Cooldown")
+                    .HasColumnType("REAL");
+
+                b.Property<ulong>("GuildId")
+                    .HasColumnType("INTEGER");
+
+                b.Property<int>("XpAmount")
+                    .HasColumnType("INTEGER");
+
+                b.HasKey("Id");
+
+                b.HasAlternateKey("GuildId", "ChannelId");
+
+                b.ToTable("ChannelXpConfig");
+            });
+
+            modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b =>
+            {
+                b.Property<ulong>("GuildId")
+                    .ValueGeneratedOnAdd()
+                    .HasColumnType("INTEGER");
+
+                b.Property<int>("Cooldown")
+                    .HasColumnType("INTEGER");
+
+                b.Property<int>("XpAmount")
+                    .HasColumnType("INTEGER");
+
+                b.Property<string>("XpTemplateUrl")
+                    .HasColumnType("TEXT");
+
+                b.HasKey("GuildId");
+
+                b.ToTable("GuildXpConfig");
+            });
+
             modelBuilder.Entity("EllieBot.Services.GreetSettings", b =>
             {
                 b.Property<int>("Id")
@@ -3027,4 +3078,4 @@ namespace EllieBot.Migrations.Sqlite
 #pragma warning restore 612, 618
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/Xp.cs b/src/EllieBot/Modules/Xp/Xp.cs
index 3e6801a..12b9ea5 100644
--- a/src/EllieBot/Modules/Xp/Xp.cs
+++ b/src/EllieBot/Modules/Xp/Xp.cs
@@ -53,91 +53,6 @@ public partial class Xp : EllieModule<XpService>
         }
     }
 
-    [Cmd]
-    [RequireContext(ContextType.Guild)]
-    [UserPerm(GuildPerm.Administrator)]
-    public async Task XpExclude(Server _)
-    {
-        var ex = await _service.ToggleExcludeServerAsync(ctx.Guild.Id);
-
-        if (ex)
-            await Response().Confirm(strs.excluded(Format.Bold(ctx.Guild.ToString()))).SendAsync();
-        else
-            await Response().Confirm(strs.not_excluded(Format.Bold(ctx.Guild.ToString()))).SendAsync();
-    }
-
-    [Cmd]
-    [UserPerm(GuildPerm.ManageRoles)]
-    [RequireContext(ContextType.Guild)]
-    public async Task XpExclude(Role _, [Leftover] IRole role)
-    {
-        var ex = await _service.ToggleExcludeRoleAsync(ctx.Guild.Id, role.Id);
-
-        if (ex)
-            await Response().Confirm(strs.excluded(Format.Bold(role.ToString()))).SendAsync();
-        else
-            await Response().Confirm(strs.not_excluded(Format.Bold(role.ToString()))).SendAsync();
-    }
-
-    [Cmd]
-    [UserPerm(GuildPerm.ManageChannels)]
-    [RequireContext(ContextType.Guild)]
-    public async Task XpExclude(Channel _, [Leftover] IChannel? channel = null)
-    {
-        if (channel is null)
-            channel = ctx.Channel;
-
-        var ex = await _service.ToggleExcludeChannelAsync(ctx.Guild.Id, channel.Id);
-
-        if (ex)
-            await Response().Confirm(strs.excluded(Format.Bold(channel.ToString()))).SendAsync();
-        else
-            await Response().Confirm(strs.not_excluded(Format.Bold(channel.ToString()))).SendAsync();
-    }
-
-    [Cmd]
-    [RequireContext(ContextType.Guild)]
-    public async Task XpExclusionList()
-    {
-        var serverExcluded = _service.IsServerExcluded(ctx.Guild.Id);
-        var roles = _service.GetExcludedRoles(ctx.Guild.Id)
-                            .Select(x => ctx.Guild.GetRole(x))
-                            .Where(x => x is not null)
-                            .Select(x => $"`role`   {x.Mention}")
-                            .ToList();
-
-        var chans = (await _service.GetExcludedChannels(ctx.Guild.Id)
-                                   .Select(x => ctx.Guild.GetChannelAsync(x))
-                                   .WhenAll()).Where(x => x is not null)
-                                              .Select(x => $"`channel` <#{x.Id}>")
-                                              .ToList();
-
-        var rolesStr = roles.Any() ? string.Join("\n", roles) + "\n" : string.Empty;
-        var chansStr = chans.Count > 0 ? string.Join("\n", chans) + "\n" : string.Empty;
-        var desc = Format.Code(serverExcluded
-            ? GetText(strs.server_is_excluded)
-            : GetText(strs.server_is_not_excluded));
-
-        desc += "\n\n" + rolesStr + chansStr;
-
-        var lines = desc.Split('\n');
-        await Response()
-              .Paginated()
-              .Items(lines)
-              .PageSize(15)
-              .CurrentPage(0)
-              .Page((items, _) =>
-              {
-                  var embed = CreateEmbed()
-                              .WithTitle(GetText(strs.exclusion_list))
-                              .WithDescription(string.Join('\n', items))
-                              .WithOkColor();
-
-                  return embed;
-              })
-              .SendAsync();
-    }
-
     [Cmd]
     [EllieOptions<LbOpts>]
     [Priority(0)]
diff --git a/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs b/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs
new file mode 100644
index 0000000..c60e15e
--- /dev/null
+++ b/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs
@@ -0,0 +1,254 @@
+using LinqToDB;
+using LinqToDB.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using EllieBot.Common.ModuleBehaviors;
+using System.ComponentModel.DataAnnotations;
+
+namespace EllieBot.Modules.Xp;
+
+public partial class Xp
+{
+    [RequireUserPermission(GuildPermission.ManageGuild)]
+    public class XpRateCommands : EllieModule<GuildConfigXpService>
+    {
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        public async Task XpRate()
+        {
+            var rates = await _service.GetGuildXpRatesAsync(ctx.Guild.Id);
+            if (rates.GuildConfig is null && !rates.ChannelRates.Any())
+            {
+                await Response().Pending(strs.xp_rate_none).SendAsync();
+                return;
+            }
+
+            var eb = CreateEmbed()
+                     .WithOkColor();
+            if (rates.GuildConfig is not null)
+            {
+                eb.AddField(GetText(strs.xp_rate_server),
+                    strs.xp_rate_amount_cooldown(
+                        rates.GuildConfig.XpAmount,
+                        rates.GuildConfig.Cooldown));
+            }
+
+            if (rates.ChannelRates.Any())
+            {
+                var channelRates = rates.ChannelRates
+                                        .Select(c => $"<#{c.ChannelId}>: {GetRateString(c.XpAmount, c.Cooldown)}")
+                                        .Join('\n');
+
+                eb.AddField(GetText(strs.xp_rate_channels), channelRates);
+            }
+
+            await Response().Embed(eb).SendAsync();
+        }
+
+        private string GetRateString(int argXpAmount, float cd)
+        {
+            if (argXpAmount == 0 || cd == 0)
+                return GetText(strs.xp_rate_no_gain);
+
+            return GetText(strs.xp_rate_amount_cooldown(argXpAmount, Math.Round(cd, 1).ToString(Culture)));
+        }
+
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        public async Task XpRate(int amount, float minutes)
+        {
+            if (amount is < 0 or > 1000)
+            {
+                await Response().Error(strs.xp_rate_amount_invalid).SendAsync();
+                return;
+            }
+
+            if (minutes is < 0 or > 1440)
+            {
+                await Response().Error(strs.xp_rate_cooldown_invalid).SendAsync();
+                return;
+            }
+
+            await _service.SetGuildXpRateAsync(ctx.Guild.Id, amount, (int)Math.Ceiling(minutes));
+            await Response().Confirm(strs.xp_rate_server_set(amount, minutes)).SendAsync();
+        }
+
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        public async Task XpRate(IMessageChannel channel, int amount, float minutes)
+        {
+            if (amount is < 0 or > 1000)
+            {
+                await Response().Error(strs.xp_rate_amount_invalid).SendAsync();
+                return;
+            }
+
+            if (minutes is < 0 or > 1440)
+            {
+                await Response().Error(strs.xp_rate_cooldown_invalid).SendAsync();
+                return;
+            }
+
+            await _service.SetChannelXpRateAsync(ctx.Guild.Id, channel.Id, amount, (int)Math.Ceiling(minutes));
+            await Response()
+                  .Confirm(strs.xp_rate_channel_set(Format.Bold(channel.ToString()), amount, minutes))
+                  .SendAsync();
+        }
+
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        public async Task XpRateReset()
+        {
+            await _service.ResetGuildXpRateAsync(ctx.Guild.Id);
+            await Response().Confirm(strs.xp_rate_server_reset).SendAsync();
+        }
+
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        public async Task XpRateReset(IMessageChannel channel)
+            => await XpRateReset(channel.Id);
+
+        [Cmd]
+        [RequireContext(ContextType.Guild)]
+        public async Task XpRateReset(ulong channelId)
+        {
+            await _service.ResetChannelXpRateAsync(ctx.Guild.Id, channelId);
+            await Response().Confirm(strs.xp_rate_channel_reset($"<#{channelId}>")).SendAsync();
+        }
+    }
+}
+
+public class GuildConfigXpService : IReadyExecutor, IEService
+{
+    private readonly DbService _db;
+
+    public GuildConfigXpService(DbService db)
+    {
+        _db = db;
+    }
+
+    public async Task<(GuildXpConfig? GuildConfig, List<ChannelXpConfig> ChannelRates)> GetGuildXpRatesAsync(
+        ulong guildId)
+    {
+        await using var uow = _db.GetDbContext();
+        var guildConfig =
+            await AsyncExtensions.FirstOrDefaultAsync(uow.GetTable<GuildXpConfig>(), x => x.GuildId == guildId);
+
+        var channelRates = await AsyncExtensions.ToListAsync(uow.GetTable<ChannelXpConfig>()
+                                                                .Where(x => x.GuildId == guildId));
+
+        return (guildConfig, channelRates);
+    }
+
+    public async Task SetGuildXpRateAsync(ulong guildId, int amount, int cooldown)
+    {
+        await using var uow = _db.GetDbContext();
+        await uow.GetTable<GuildXpConfig>()
+                 .InsertOrUpdateAsync(() => new()
+                 {
+                     GuildId = guildId,
+                     XpAmount = amount,
+                     Cooldown = cooldown
+                 },
+                     (_) => new()
+                     {
+                         Cooldown = cooldown,
+                         XpAmount = amount,
+                         GuildId = guildId
+                     },
+                     () => new()
+                     {
+                         GuildId = guildId
+                     });
+    }
+
+    public async Task SetChannelXpRateAsync(
+        ulong guildId,
+        ulong channelId,
+        int amount,
+        int cooldown)
+    {
+        await using var uow = _db.GetDbContext();
+        await uow.GetTable<ChannelXpConfig>()
+                 .InsertOrUpdateAsync(() => new()
+                 {
+                     GuildId = guildId,
+                     ChannelId = channelId,
+                     XpAmount = amount,
+                     Cooldown = cooldown
+                 },
+                     (_) => new()
+                     {
+                         Cooldown = cooldown,
+                         XpAmount = amount,
+                         GuildId = guildId,
+                         ChannelId = channelId
+                     },
+                     () => new()
+                     {
+                         GuildId = guildId,
+                         ChannelId = channelId
+                     });
+    }
+
+    public async Task<bool> ResetGuildXpRateAsync(ulong guildId)
+    {
+        await using var uow = _db.GetDbContext();
+        var deleted = await uow.GetTable<GuildXpConfig>()
+                               .Where(x => x.GuildId == guildId)
+                               .DeleteAsync();
+        return deleted > 0;
+    }
+
+    public async Task<bool> ResetChannelXpRateAsync(ulong guildId, ulong channelId)
+    {
+        await using var uow = _db.GetDbContext();
+        var deleted = await uow.GetTable<ChannelXpConfig>()
+                               .Where(x => x.GuildId == guildId && x.ChannelId == channelId)
+                               .DeleteAsync();
+        return deleted > 0;
+    }
+
+    public Task OnReadyAsync()
+        => Task.CompletedTask;
+}
+
+public class GuildXpConfig
+{
+    [Key]
+    public ulong GuildId { get; set; }
+
+    public int XpAmount { get; set; }
+    public int Cooldown { get; set; }
+    public string? XpTemplateUrl { get; set; }
+}
+
+public sealed class GuildXpConfigEntity : IEntityTypeConfiguration<GuildXpConfig>
+{
+    public void Configure(EntityTypeBuilder<GuildXpConfig> builder)
+    {
+    }
+}
+
+public class ChannelXpConfig
+{
+    [Key]
+    public int Id { get; set; }
+
+    public ulong GuildId { get; set; }
+    public ulong ChannelId { get; set; }
+    public int XpAmount { get; set; }
+    public float Cooldown { get; set; }
+}
+
+public sealed class ChannelXpConfigEntity : IEntityTypeConfiguration<ChannelXpConfig>
+{
+    public void Configure(EntityTypeBuilder<ChannelXpConfig> builder)
+    {
+        builder.HasAlternateKey(x => new
+        {
+            x.GuildId,
+            x.ChannelId
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/EllieBot/strings/aliases.yml b/src/EllieBot/strings/aliases.yml
index 77e41e6..3e76803 100644
--- a/src/EllieBot/strings/aliases.yml
+++ b/src/EllieBot/strings/aliases.yml
@@ -1091,12 +1091,6 @@ experience:
 xptemplatereload:
   - xptempreload
   - xptr
-xpexclusionlist:
-  - xpexclusionlist
-  - xpexl
-xpexclude:
-  - xpexclude
-  - xpex
 xpleveluprewards:
   - xplvluprewards
   - xprews
@@ -1581,4 +1575,8 @@ fishlist:
 fishspot:
   - fishspot 
   - fisp
-  - fish?
\ No newline at end of file
+  - fish?
+xprate:
+  - xprate
+xpratereset:
+  - xpratereset
\ No newline at end of file
diff --git a/src/EllieBot/strings/commands/commands.en-US.yml b/src/EllieBot/strings/commands/commands.en-US.yml
index a390f53..cb691ca 100644
--- a/src/EllieBot/strings/commands/commands.en-US.yml
+++ b/src/EllieBot/strings/commands/commands.en-US.yml
@@ -3517,28 +3517,6 @@ xptemplatereload:
     - ''
   params:
     - { }
-xpexclusionlist:
-  desc: Shows the roles and channels excluded from the XP system on this server, as well as whether the whole server is excluded.
-  ex:
-    - ''
-  params:
-    - { }
-xpexclude:
-  desc: Exclude a channel, role or current server from the xp system.
-  ex:
-    - Role Excluded-Role
-    - Server
-  params:
-    - _:
-        desc: "The ID of the server to exclude from the XP system."
-    - _:
-        desc: "The role that should not receive XP rewards."
-      role:
-        desc: "The role that should not receive XP rewards."
-    - _:
-        desc: "The ID of the channel to exclude from XP tracking."
-      channel:
-        desc: "The ID of the channel to exclude from XP tracking."
 xpleveluprewards:
   desc: Shows currently set level up rewards.
   ex:
@@ -4953,4 +4931,35 @@ fishspot:
   ex:
     - ''
   params:
-    - { }
\ No newline at end of file
+    - { }
+xprate:
+  desc: |-
+    Sets the xp rate for the server or the specified channel.
+    First specify the amount, and then the cooldown in minutes.
+    Provide no parameters to see the current rates.
+  ex:
+    - ''
+    - '3 5'
+    - '#channel 50 1'
+  params:
+    - { }
+    - amount:
+        desc: "The amount of xp to give per message."
+      minutes:
+        desc: "The cooldown in minutes. Allows decimal values."
+    - channel:
+        desc: "The channel to set the rate for."
+      amount:
+        desc: "The amount of xp to give per message."
+      minutes:
+        desc: "The cooldown in minutes. Allows decimal values."
+xpratereset:
+  desc: |-
+    Resets the xp rate for the server or the specified channel.
+  ex:
+    - ''
+    - '#channel'
+  params:
+    - { }
+    - channel:
+        desc: "The channel to reset the rate for."
\ No newline at end of file
diff --git a/src/EllieBot/strings/responses/responses.en-US.json b/src/EllieBot/strings/responses/responses.en-US.json
index 13c177e..e5dadaf 100644
--- a/src/EllieBot/strings/responses/responses.en-US.json
+++ b/src/EllieBot/strings/responses/responses.en-US.json
@@ -836,11 +836,6 @@
   "xpn_notif_dm": "In a direct message channel.",
   "xpn_notif_disabled": "Nowhere.",
   "xprewsreset_confirm": "Are you sure you want to delete ALL xp level up rewards from this server? This action is irreversible.",
-  "excluded": "{0} has been excluded from the XP system on this server.",
-  "not_excluded": "{0} is no longer excluded from the XP system on this server.",
-  "exclusion_list": "Exclusion List",
-  "server_is_excluded": "This server is excluded.",
-  "server_is_not_excluded": "This server is not excluded.",
   "level_up_channel": "Congratulations {0}, You've reached level {1}!",
   "level_up_dm": "Congratulations {0}, You've reached level {1} on {2} server!",
   "level_up_global": "Congratulations {0}, You've reached global level {1}!",
@@ -1172,5 +1167,16 @@
   "fish_weather_forecast": "Forecast",
   "fish_tod": "Time of Day",
   "fish_skill_up": "Fishing skill increased to **{0} / {1}**",
-  "fish_list_title": "Fishing"
+  "fish_list_title": "Fishing",
+  "xp_rate_none": "No xp rate overrides on this server.",
+  "xp_rate_amount_invalid": "Amount must be between 0 and 1000.",
+  "xp_rate_cooldown_invalid": "Cooldown must be between 0 and 1440 minutes.",
+  "xp_rate_server": "Server xp Rate",
+  "xp_rate_amount_cooldown": "{0} xp per every {1} minutes",
+  "xp_rate_channels": "Channel XP Rates",
+  "xp_rate_server_set": "Server xp rate set to {0} xp per every {1} minutes.",
+  "xp_rate_channel_set": "Channel {0} xp rate set to {1} xp per every {2} minutes.",
+  "xp_rate_server_reset": "Server xp rate has been reset to global defaults.",
+  "xp_rate_channel_reset": "Channel {0} xp rate has been reset.",
+  "xp_rate_no_gain": "No xp gain"
 }