From 11b3705939351f9d26ab82c866d5ce57594e0e0d Mon Sep 17 00:00:00 2001 From: Toastie <toastie@toastiet0ast.com> Date: Sat, 1 Mar 2025 13:56:17 +1300 Subject: [PATCH] Fixed xprate presentation and reworked it internally to support voice, text and image xp --- .../Db/Extensions/GuildConfigExtensions.cs | 1 - src/EllieBot/Db/Models/xp/UserXpStats.cs | 1 - src/EllieBot/Db/Models/xp/XpSettings.cs | 5 - .../xp/XpSettingsEntityConfiguration.cs | 3 - .../20250226215159_xp-rate-rework.sql | 31 ++ ...ner.cs => 20250226215222_init.Designer.cs} | 85 ++---- ...5212209_init.cs => 20250226215222_init.cs} | 45 +-- .../PostgreSqlContextModelSnapshot.cs | 83 ++---- .../Sqlite/20250226215156_xp-rate-rework.sql | 75 +++++ ...ner.cs => 20250226215220_init.Designer.cs} | 62 ++-- ...5212206_init.cs => 20250226215220_init.cs} | 46 +-- .../Sqlite/SqliteContextModelSnapshot.cs | 60 ++-- src/EllieBot/Modules/Xp/Db/UserXpBatch.cs | 1 + .../Modules/Xp/XpRate/GuildConfigXpService.cs | 172 +++++++++++ src/EllieBot/Modules/Xp/XpRate/XpRate.cs | 7 + .../Modules/Xp/XpRate/XpRateCommands.cs | 208 +++---------- src/EllieBot/Modules/Xp/XpRate/XpRateType.cs | 8 + .../Modules/Xp/XpRate/db/ChannelXpConfig.cs | 30 ++ .../Modules/Xp/XpRate/db/GuildXpConfig.cs | 30 ++ src/EllieBot/Modules/Xp/XpService.cs | 281 +++++------------- src/EllieBot/Services/GrpcApi/XpSvc.cs | 70 +---- .../_common/Interaction/EllieInteraction.cs | 4 +- .../ResponseBuilder.PaginationSender.cs | 2 +- .../strings/commands/commands.en-US.yml | 13 +- .../strings/responses/responses.en-US.json | 12 +- 25 files changed, 595 insertions(+), 740 deletions(-) create mode 100644 src/EllieBot/Migrations/PostgreSql/20250226215159_xp-rate-rework.sql rename src/EllieBot/Migrations/PostgreSql/{20250225212209_init.Designer.cs => 20250226215222_init.Designer.cs} (98%) rename src/EllieBot/Migrations/PostgreSql/{20250225212209_init.cs => 20250226215222_init.cs} (98%) create mode 100644 src/EllieBot/Migrations/Sqlite/20250226215156_xp-rate-rework.sql rename src/EllieBot/Migrations/Sqlite/{20250225212206_init.Designer.cs => 20250226215220_init.Designer.cs} (98%) rename src/EllieBot/Migrations/Sqlite/{20250225212206_init.cs => 20250226215220_init.cs} (98%) create mode 100644 src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs create mode 100644 src/EllieBot/Modules/Xp/XpRate/XpRate.cs create mode 100644 src/EllieBot/Modules/Xp/XpRate/XpRateType.cs create mode 100644 src/EllieBot/Modules/Xp/XpRate/db/ChannelXpConfig.cs create mode 100644 src/EllieBot/Modules/Xp/XpRate/db/GuildXpConfig.cs diff --git a/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs b/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs index a8f9329..cf8bfd8 100644 --- a/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs +++ b/src/EllieBot/Db/Extensions/GuildConfigExtensions.cs @@ -121,7 +121,6 @@ public static class GuildConfigExtensions .InsertWithOutputAsync(() => new() { GuildId = guildId, - ServerExcluded = false, }); return srs; diff --git a/src/EllieBot/Db/Models/xp/UserXpStats.cs b/src/EllieBot/Db/Models/xp/UserXpStats.cs index df39e14..0032c01 100644 --- a/src/EllieBot/Db/Models/xp/UserXpStats.cs +++ b/src/EllieBot/Db/Models/xp/UserXpStats.cs @@ -1,4 +1,3 @@ -#nullable disable namespace EllieBot.Db.Models; public class UserXpStats : DbEntity diff --git a/src/EllieBot/Db/Models/xp/XpSettings.cs b/src/EllieBot/Db/Models/xp/XpSettings.cs index f5af7e9..aa9aaef 100644 --- a/src/EllieBot/Db/Models/xp/XpSettings.cs +++ b/src/EllieBot/Db/Models/xp/XpSettings.cs @@ -1,14 +1,9 @@ #nullable disable -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - namespace EllieBot.Db.Models; public class XpSettings : DbEntity { public ulong GuildId { get; set; } - public bool ServerExcluded { get; set; } - public HashSet<ExcludedItem> ExclusionList { get; set; } = new(); public HashSet<XpRoleReward> RoleRewards { get; set; } = new(); public HashSet<XpCurrencyReward> CurrencyRewards { get; set; } = new(); diff --git a/src/EllieBot/Db/Models/xp/XpSettingsEntityConfiguration.cs b/src/EllieBot/Db/Models/xp/XpSettingsEntityConfiguration.cs index a5fd6b8..83b5621 100644 --- a/src/EllieBot/Db/Models/xp/XpSettingsEntityConfiguration.cs +++ b/src/EllieBot/Db/Models/xp/XpSettingsEntityConfiguration.cs @@ -15,8 +15,5 @@ public class XpSettingsEntityConfiguration : IEntityTypeConfiguration<XpSettings builder.HasMany(x => x.RoleRewards) .WithOne(); - - builder.HasMany(x => x.ExclusionList) - .WithOne(); } } \ No newline at end of file diff --git a/src/EllieBot/Migrations/PostgreSql/20250226215159_xp-rate-rework.sql b/src/EllieBot/Migrations/PostgreSql/20250226215159_xp-rate-rework.sql new file mode 100644 index 0000000..e810882 --- /dev/null +++ b/src/EllieBot/Migrations/PostgreSql/20250226215159_xp-rate-rework.sql @@ -0,0 +1,31 @@ +START TRANSACTION; +DROP TABLE excludeditem; + +ALTER TABLE guildxpconfig DROP CONSTRAINT pk_guildxpconfig; + +ALTER TABLE channelxpconfig DROP CONSTRAINT ak_channelxpconfig_guildid_channelid; + +ALTER TABLE xpsettings DROP COLUMN serverexcluded; + +ALTER TABLE guildxpconfig ALTER COLUMN xpamount TYPE bigint; + +ALTER TABLE guildxpconfig ALTER COLUMN cooldown TYPE real; + +ALTER TABLE guildxpconfig ADD id integer GENERATED BY DEFAULT AS IDENTITY; + +ALTER TABLE guildxpconfig ADD ratetype integer NOT NULL DEFAULT 0; + +ALTER TABLE channelxpconfig ALTER COLUMN xpamount TYPE bigint; + +ALTER TABLE channelxpconfig ADD ratetype integer NOT NULL DEFAULT 0; + +ALTER TABLE guildxpconfig ADD CONSTRAINT ak_guildxpconfig_guildid_ratetype UNIQUE (guildid, ratetype); + +ALTER TABLE guildxpconfig ADD CONSTRAINT pk_guildxpconfig PRIMARY KEY (id); + +ALTER TABLE channelxpconfig ADD CONSTRAINT ak_channelxpconfig_guildid_channelid_ratetype UNIQUE (guildid, channelid, ratetype); + +INSERT INTO "__EFMigrationsHistory" (migrationid, productversion) +VALUES ('20250226215159_xp-rate-rework', '9.0.1'); + +COMMIT; diff --git a/src/EllieBot/Migrations/PostgreSql/20250225212209_init.Designer.cs b/src/EllieBot/Migrations/PostgreSql/20250226215222_init.Designer.cs similarity index 98% rename from src/EllieBot/Migrations/PostgreSql/20250225212209_init.Designer.cs rename to src/EllieBot/Migrations/PostgreSql/20250226215222_init.Designer.cs index 66f7870..4bc0e7f 100644 --- a/src/EllieBot/Migrations/PostgreSql/20250225212209_init.Designer.cs +++ b/src/EllieBot/Migrations/PostgreSql/20250226215222_init.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace EllieBot.Migrations.PostgreSql { [DbContext(typeof(PostgreSqlContext))] - [Migration("20250225212209_init")] + [Migration("20250226215222_init")] partial class init { /// <inheritdoc /> @@ -877,40 +877,6 @@ namespace EllieBot.Migrations.PostgreSql b.ToTable("discorduser", (string)null); }); - modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); - - b.Property<DateTime?>("DateAdded") - .HasColumnType("timestamp without time zone") - .HasColumnName("dateadded"); - - b.Property<decimal>("ItemId") - .HasColumnType("numeric(20,0)") - .HasColumnName("itemid"); - - b.Property<int>("ItemType") - .HasColumnType("integer") - .HasColumnName("itemtype"); - - b.Property<int?>("XpSettingsId") - .HasColumnType("integer") - .HasColumnName("xpsettingsid"); - - b.HasKey("Id") - .HasName("pk_excludeditem"); - - b.HasIndex("XpSettingsId") - .HasDatabaseName("ix_excludeditem_xpsettingsid"); - - b.ToTable("excludeditem", (string)null); - }); - modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b => { b.Property<int>("Id") @@ -3359,10 +3325,6 @@ namespace EllieBot.Migrations.PostgreSql .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); - b.Property<bool>("ServerExcluded") - .HasColumnType("boolean") - .HasColumnName("serverexcluded"); - b.HasKey("Id") .HasName("pk_xpsettings"); @@ -3503,41 +3465,58 @@ namespace EllieBot.Migrations.PostgreSql .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); - b.Property<int>("XpAmount") + b.Property<int>("RateType") .HasColumnType("integer") + .HasColumnName("ratetype"); + + b.Property<long>("XpAmount") + .HasColumnType("bigint") .HasColumnName("xpamount"); b.HasKey("Id") .HasName("pk_channelxpconfig"); - b.HasAlternateKey("GuildId", "ChannelId") - .HasName("ak_channelxpconfig_guildid_channelid"); + b.HasAlternateKey("GuildId", "ChannelId", "RateType") + .HasName("ak_channelxpconfig_guildid_channelid_ratetype"); b.ToTable("channelxpconfig", (string)null); }); modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b => { - b.Property<decimal>("GuildId") + b.Property<int>("Id") .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<float>("Cooldown") + .HasColumnType("real") + .HasColumnName("cooldown"); + + b.Property<decimal>("GuildId") .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); - b.Property<int>("Cooldown") + b.Property<int>("RateType") .HasColumnType("integer") - .HasColumnName("cooldown"); + .HasColumnName("ratetype"); - b.Property<int>("XpAmount") - .HasColumnType("integer") + b.Property<long>("XpAmount") + .HasColumnType("bigint") .HasColumnName("xpamount"); b.Property<string>("XpTemplateUrl") .HasColumnType("text") .HasColumnName("xptemplateurl"); - b.HasKey("GuildId") + b.HasKey("Id") .HasName("pk_guildxpconfig"); + b.HasAlternateKey("GuildId", "RateType") + .HasName("ak_guildxpconfig_guildid_ratetype"); + b.ToTable("guildxpconfig", (string)null); }); @@ -3744,14 +3723,6 @@ namespace EllieBot.Migrations.PostgreSql b.Navigation("Club"); }); - modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => - { - b.HasOne("EllieBot.Db.Models.XpSettings", null) - .WithMany("ExclusionList") - .HasForeignKey("XpSettingsId") - .HasConstraintName("fk_excludeditem_xpsettings_xpsettingsid"); - }); - modelBuilder.Entity("EllieBot.Db.Models.FilterChannelId", b => { b.HasOne("EllieBot.Db.Models.GuildFilterConfig", null) @@ -4038,8 +4009,6 @@ namespace EllieBot.Migrations.PostgreSql { b.Navigation("CurrencyRewards"); - b.Navigation("ExclusionList"); - b.Navigation("RoleRewards"); }); #pragma warning restore 612, 618 diff --git a/src/EllieBot/Migrations/PostgreSql/20250225212209_init.cs b/src/EllieBot/Migrations/PostgreSql/20250226215222_init.cs similarity index 98% rename from src/EllieBot/Migrations/PostgreSql/20250225212209_init.cs rename to src/EllieBot/Migrations/PostgreSql/20250226215222_init.cs index bf7ff89..9d717f0 100644 --- a/src/EllieBot/Migrations/PostgreSql/20250225212209_init.cs +++ b/src/EllieBot/Migrations/PostgreSql/20250226215222_init.cs @@ -193,15 +193,16 @@ namespace EllieBot.Migrations.PostgreSql { id = table.Column<int>(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ratetype = table.Column<int>(type: "integer", nullable: false), 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), + xpamount = table.Column<long>(type: "bigint", 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 }); + table.UniqueConstraint("ak_channelxpconfig_guildid_channelid_ratetype", x => new { x.guildid, x.channelid, x.ratetype }); }); migrationBuilder.CreateTable( @@ -508,14 +509,18 @@ namespace EllieBot.Migrations.PostgreSql name: "guildxpconfig", 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), - xpamount = table.Column<int>(type: "integer", nullable: false), - cooldown = table.Column<int>(type: "integer", nullable: false), + ratetype = table.Column<int>(type: "integer", nullable: false), + xpamount = table.Column<long>(type: "bigint", nullable: false), + cooldown = table.Column<float>(type: "real", nullable: false), xptemplateurl = table.Column<string>(type: "text", nullable: true) }, constraints: table => { - table.PrimaryKey("pk_guildxpconfig", x => x.guildid); + table.PrimaryKey("pk_guildxpconfig", x => x.id); + table.UniqueConstraint("ak_guildxpconfig_guildid_ratetype", x => new { x.guildid, x.ratetype }); }); migrationBuilder.CreateTable( @@ -1151,7 +1156,6 @@ namespace EllieBot.Migrations.PostgreSql id = table.Column<int>(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false), - serverexcluded = table.Column<bool>(type: "boolean", nullable: false), dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true) }, constraints: table => @@ -1506,27 +1510,6 @@ namespace EllieBot.Migrations.PostgreSql onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateTable( - name: "excludeditem", - columns: table => new - { - id = table.Column<int>(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - xpsettingsid = table.Column<int>(type: "integer", nullable: true), - itemid = table.Column<decimal>(type: "numeric(20,0)", nullable: false), - itemtype = table.Column<int>(type: "integer", nullable: false), - dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_excludeditem", x => x.id); - table.ForeignKey( - name: "fk_excludeditem_xpsettings_xpsettingsid", - column: x => x.xpsettingsid, - principalTable: "xpsettings", - principalColumn: "id"); - }); - migrationBuilder.CreateTable( name: "xpcurrencyreward", columns: table => new @@ -1860,11 +1843,6 @@ namespace EllieBot.Migrations.PostgreSql table: "discorduser", column: "username"); - migrationBuilder.CreateIndex( - name: "ix_excludeditem_xpsettingsid", - table: "excludeditem", - column: "xpsettingsid"); - migrationBuilder.CreateIndex( name: "ix_feedsub_guildid_url", table: "feedsub", @@ -2367,9 +2345,6 @@ namespace EllieBot.Migrations.PostgreSql migrationBuilder.DropTable( name: "discordpermoverrides"); - migrationBuilder.DropTable( - name: "excludeditem"); - migrationBuilder.DropTable( name: "expressions"); diff --git a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs index d58940a..5c0c910 100644 --- a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs +++ b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs @@ -874,40 +874,6 @@ namespace EllieBot.Migrations.PostgreSql b.ToTable("discorduser", (string)null); }); - modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); - - b.Property<DateTime?>("DateAdded") - .HasColumnType("timestamp without time zone") - .HasColumnName("dateadded"); - - b.Property<decimal>("ItemId") - .HasColumnType("numeric(20,0)") - .HasColumnName("itemid"); - - b.Property<int>("ItemType") - .HasColumnType("integer") - .HasColumnName("itemtype"); - - b.Property<int?>("XpSettingsId") - .HasColumnType("integer") - .HasColumnName("xpsettingsid"); - - b.HasKey("Id") - .HasName("pk_excludeditem"); - - b.HasIndex("XpSettingsId") - .HasDatabaseName("ix_excludeditem_xpsettingsid"); - - b.ToTable("excludeditem", (string)null); - }); - modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b => { b.Property<int>("Id") @@ -3356,10 +3322,6 @@ namespace EllieBot.Migrations.PostgreSql .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); - b.Property<bool>("ServerExcluded") - .HasColumnType("boolean") - .HasColumnName("serverexcluded"); - b.HasKey("Id") .HasName("pk_xpsettings"); @@ -3500,41 +3462,58 @@ namespace EllieBot.Migrations.PostgreSql .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); - b.Property<int>("XpAmount") + b.Property<int>("RateType") .HasColumnType("integer") + .HasColumnName("ratetype"); + + b.Property<long>("XpAmount") + .HasColumnType("bigint") .HasColumnName("xpamount"); b.HasKey("Id") .HasName("pk_channelxpconfig"); - b.HasAlternateKey("GuildId", "ChannelId") - .HasName("ak_channelxpconfig_guildid_channelid"); + b.HasAlternateKey("GuildId", "ChannelId", "RateType") + .HasName("ak_channelxpconfig_guildid_channelid_ratetype"); b.ToTable("channelxpconfig", (string)null); }); modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b => { - b.Property<decimal>("GuildId") + b.Property<int>("Id") .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<float>("Cooldown") + .HasColumnType("real") + .HasColumnName("cooldown"); + + b.Property<decimal>("GuildId") .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); - b.Property<int>("Cooldown") + b.Property<int>("RateType") .HasColumnType("integer") - .HasColumnName("cooldown"); + .HasColumnName("ratetype"); - b.Property<int>("XpAmount") - .HasColumnType("integer") + b.Property<long>("XpAmount") + .HasColumnType("bigint") .HasColumnName("xpamount"); b.Property<string>("XpTemplateUrl") .HasColumnType("text") .HasColumnName("xptemplateurl"); - b.HasKey("GuildId") + b.HasKey("Id") .HasName("pk_guildxpconfig"); + b.HasAlternateKey("GuildId", "RateType") + .HasName("ak_guildxpconfig_guildid_ratetype"); + b.ToTable("guildxpconfig", (string)null); }); @@ -3741,14 +3720,6 @@ namespace EllieBot.Migrations.PostgreSql b.Navigation("Club"); }); - modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => - { - b.HasOne("EllieBot.Db.Models.XpSettings", null) - .WithMany("ExclusionList") - .HasForeignKey("XpSettingsId") - .HasConstraintName("fk_excludeditem_xpsettings_xpsettingsid"); - }); - modelBuilder.Entity("EllieBot.Db.Models.FilterChannelId", b => { b.HasOne("EllieBot.Db.Models.GuildFilterConfig", null) @@ -4035,8 +4006,6 @@ namespace EllieBot.Migrations.PostgreSql { b.Navigation("CurrencyRewards"); - b.Navigation("ExclusionList"); - b.Navigation("RoleRewards"); }); #pragma warning restore 612, 618 diff --git a/src/EllieBot/Migrations/Sqlite/20250226215156_xp-rate-rework.sql b/src/EllieBot/Migrations/Sqlite/20250226215156_xp-rate-rework.sql new file mode 100644 index 0000000..371fe1a --- /dev/null +++ b/src/EllieBot/Migrations/Sqlite/20250226215156_xp-rate-rework.sql @@ -0,0 +1,75 @@ +BEGIN TRANSACTION; +DROP TABLE "ExcludedItem"; + +ALTER TABLE "GuildXpConfig" ADD "Id" INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE "GuildXpConfig" ADD "RateType" INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE "ChannelXpConfig" ADD "RateType" INTEGER NOT NULL DEFAULT 0; + +CREATE TABLE "ef_temp_GuildXpConfig" ( + "Id" INTEGER NOT NULL CONSTRAINT "PK_GuildXpConfig" PRIMARY KEY AUTOINCREMENT, + "Cooldown" REAL NOT NULL, + "GuildId" INTEGER NOT NULL, + "RateType" INTEGER NOT NULL, + "XpAmount" INTEGER NOT NULL, + "XpTemplateUrl" TEXT NULL, + CONSTRAINT "AK_GuildXpConfig_GuildId_RateType" UNIQUE ("GuildId", "RateType") +); + +INSERT INTO "ef_temp_GuildXpConfig" ("Id", "Cooldown", "GuildId", "RateType", "XpAmount", "XpTemplateUrl") +SELECT "Id", "Cooldown", "GuildId", "RateType", "XpAmount", "XpTemplateUrl" +FROM "GuildXpConfig"; + +CREATE TABLE "ef_temp_ChannelXpConfig" ( + "Id" INTEGER NOT NULL CONSTRAINT "PK_ChannelXpConfig" PRIMARY KEY AUTOINCREMENT, + "ChannelId" INTEGER NOT NULL, + "Cooldown" REAL NOT NULL, + "GuildId" INTEGER NOT NULL, + "RateType" INTEGER NOT NULL, + "XpAmount" INTEGER NOT NULL, + CONSTRAINT "AK_ChannelXpConfig_GuildId_ChannelId_RateType" UNIQUE ("GuildId", "ChannelId", "RateType") +); + +INSERT INTO "ef_temp_ChannelXpConfig" ("Id", "ChannelId", "Cooldown", "GuildId", "RateType", "XpAmount") +SELECT "Id", "ChannelId", "Cooldown", "GuildId", "RateType", "XpAmount" +FROM "ChannelXpConfig"; + +CREATE TABLE "ef_temp_XpSettings" ( + "Id" INTEGER NOT NULL CONSTRAINT "PK_XpSettings" PRIMARY KEY AUTOINCREMENT, + "DateAdded" TEXT NULL, + "GuildId" INTEGER NOT NULL +); + +INSERT INTO "ef_temp_XpSettings" ("Id", "DateAdded", "GuildId") +SELECT "Id", "DateAdded", "GuildId" +FROM "XpSettings"; + +COMMIT; + +PRAGMA foreign_keys = 0; + +BEGIN TRANSACTION; +DROP TABLE "GuildXpConfig"; + +ALTER TABLE "ef_temp_GuildXpConfig" RENAME TO "GuildXpConfig"; + +DROP TABLE "ChannelXpConfig"; + +ALTER TABLE "ef_temp_ChannelXpConfig" RENAME TO "ChannelXpConfig"; + +DROP TABLE "XpSettings"; + +ALTER TABLE "ef_temp_XpSettings" RENAME TO "XpSettings"; + +COMMIT; + +PRAGMA foreign_keys = 1; + +BEGIN TRANSACTION; +CREATE UNIQUE INDEX "IX_XpSettings_GuildId" ON "XpSettings" ("GuildId"); + +COMMIT; + +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20250226215156_xp-rate-rework', '9.0.1'); diff --git a/src/EllieBot/Migrations/Sqlite/20250225212206_init.Designer.cs b/src/EllieBot/Migrations/Sqlite/20250226215220_init.Designer.cs similarity index 98% rename from src/EllieBot/Migrations/Sqlite/20250225212206_init.Designer.cs rename to src/EllieBot/Migrations/Sqlite/20250226215220_init.Designer.cs index 483367d..18ebf28 100644 --- a/src/EllieBot/Migrations/Sqlite/20250225212206_init.Designer.cs +++ b/src/EllieBot/Migrations/Sqlite/20250226215220_init.Designer.cs @@ -11,7 +11,7 @@ using EllieBot.Db; namespace EllieBot.Migrations.Sqlite { [DbContext(typeof(SqliteContext))] - [Migration("20250225212206_init")] + [Migration("20250226215220_init")] partial class init { /// <inheritdoc /> @@ -657,31 +657,6 @@ namespace EllieBot.Migrations.Sqlite b.ToTable("DiscordUser"); }); - modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<DateTime?>("DateAdded") - .HasColumnType("TEXT"); - - b.Property<ulong>("ItemId") - .HasColumnType("INTEGER"); - - b.Property<int>("ItemType") - .HasColumnType("INTEGER"); - - b.Property<int?>("XpSettingsId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("XpSettingsId"); - - b.ToTable("ExcludedItem"); - }); - modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b => { b.Property<int>("Id") @@ -2499,9 +2474,6 @@ namespace EllieBot.Migrations.Sqlite b.Property<ulong>("GuildId") .HasColumnType("INTEGER"); - b.Property<bool>("ServerExcluded") - .HasColumnType("INTEGER"); - b.HasKey("Id"); b.HasIndex("GuildId") @@ -2606,32 +2578,43 @@ namespace EllieBot.Migrations.Sqlite b.Property<ulong>("GuildId") .HasColumnType("INTEGER"); - b.Property<int>("XpAmount") + b.Property<int>("RateType") + .HasColumnType("INTEGER"); + + b.Property<long>("XpAmount") .HasColumnType("INTEGER"); b.HasKey("Id"); - b.HasAlternateKey("GuildId", "ChannelId"); + b.HasAlternateKey("GuildId", "ChannelId", "RateType"); b.ToTable("ChannelXpConfig"); }); modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b => { - b.Property<ulong>("GuildId") + b.Property<int>("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property<int>("Cooldown") + b.Property<float>("Cooldown") + .HasColumnType("REAL"); + + b.Property<ulong>("GuildId") .HasColumnType("INTEGER"); - b.Property<int>("XpAmount") + b.Property<int>("RateType") + .HasColumnType("INTEGER"); + + b.Property<long>("XpAmount") .HasColumnType("INTEGER"); b.Property<string>("XpTemplateUrl") .HasColumnType("TEXT"); - b.HasKey("GuildId"); + b.HasKey("Id"); + + b.HasAlternateKey("GuildId", "RateType"); b.ToTable("GuildXpConfig"); }); @@ -2803,13 +2786,6 @@ namespace EllieBot.Migrations.Sqlite b.Navigation("Club"); }); - modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => - { - b.HasOne("EllieBot.Db.Models.XpSettings", null) - .WithMany("ExclusionList") - .HasForeignKey("XpSettingsId"); - }); - modelBuilder.Entity("EllieBot.Db.Models.FilterChannelId", b => { b.HasOne("EllieBot.Db.Models.GuildFilterConfig", null) @@ -3074,8 +3050,6 @@ namespace EllieBot.Migrations.Sqlite { b.Navigation("CurrencyRewards"); - b.Navigation("ExclusionList"); - b.Navigation("RoleRewards"); }); #pragma warning restore 612, 618 diff --git a/src/EllieBot/Migrations/Sqlite/20250225212206_init.cs b/src/EllieBot/Migrations/Sqlite/20250226215220_init.cs similarity index 98% rename from src/EllieBot/Migrations/Sqlite/20250225212206_init.cs rename to src/EllieBot/Migrations/Sqlite/20250226215220_init.cs index c18b049..566a6e2 100644 --- a/src/EllieBot/Migrations/Sqlite/20250225212206_init.cs +++ b/src/EllieBot/Migrations/Sqlite/20250226215220_init.cs @@ -192,15 +192,16 @@ namespace EllieBot.Migrations.Sqlite { Id = table.Column<int>(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), + RateType = table.Column<int>(type: "INTEGER", nullable: false), GuildId = table.Column<ulong>(type: "INTEGER", nullable: false), ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false), - XpAmount = table.Column<int>(type: "INTEGER", nullable: false), + XpAmount = table.Column<long>(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 }); + table.UniqueConstraint("AK_ChannelXpConfig_GuildId_ChannelId_RateType", x => new { x.GuildId, x.ChannelId, x.RateType }); }); migrationBuilder.CreateTable( @@ -507,15 +508,18 @@ namespace EllieBot.Migrations.Sqlite name: "GuildXpConfig", columns: table => new { - GuildId = table.Column<ulong>(type: "INTEGER", nullable: false) + Id = table.Column<int>(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - XpAmount = table.Column<int>(type: "INTEGER", nullable: false), - Cooldown = table.Column<int>(type: "INTEGER", nullable: false), + GuildId = table.Column<ulong>(type: "INTEGER", nullable: false), + RateType = table.Column<int>(type: "INTEGER", nullable: false), + XpAmount = table.Column<long>(type: "INTEGER", nullable: false), + Cooldown = table.Column<float>(type: "REAL", nullable: false), XpTemplateUrl = table.Column<string>(type: "TEXT", nullable: true) }, constraints: table => { - table.PrimaryKey("PK_GuildXpConfig", x => x.GuildId); + table.PrimaryKey("PK_GuildXpConfig", x => x.Id); + table.UniqueConstraint("AK_GuildXpConfig_GuildId_RateType", x => new { x.GuildId, x.RateType }); }); migrationBuilder.CreateTable( @@ -1154,7 +1158,6 @@ namespace EllieBot.Migrations.Sqlite Id = table.Column<int>(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), GuildId = table.Column<ulong>(type: "INTEGER", nullable: false), - ServerExcluded = table.Column<bool>(type: "INTEGER", nullable: false), DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true) }, constraints: table => @@ -1509,27 +1512,6 @@ namespace EllieBot.Migrations.Sqlite onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateTable( - name: "ExcludedItem", - columns: table => new - { - Id = table.Column<int>(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - XpSettingsId = table.Column<int>(type: "INTEGER", nullable: true), - ItemId = table.Column<ulong>(type: "INTEGER", nullable: false), - ItemType = table.Column<int>(type: "INTEGER", nullable: false), - DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ExcludedItem", x => x.Id); - table.ForeignKey( - name: "FK_ExcludedItem_XpSettings_XpSettingsId", - column: x => x.XpSettingsId, - principalTable: "XpSettings", - principalColumn: "Id"); - }); - migrationBuilder.CreateTable( name: "XpCurrencyReward", columns: table => new @@ -1863,11 +1845,6 @@ namespace EllieBot.Migrations.Sqlite table: "DiscordUser", column: "Username"); - migrationBuilder.CreateIndex( - name: "IX_ExcludedItem_XpSettingsId", - table: "ExcludedItem", - column: "XpSettingsId"); - migrationBuilder.CreateIndex( name: "IX_FeedSub_GuildId_Url", table: "FeedSub", @@ -2370,9 +2347,6 @@ namespace EllieBot.Migrations.Sqlite migrationBuilder.DropTable( name: "DiscordPermOverrides"); - migrationBuilder.DropTable( - name: "ExcludedItem"); - migrationBuilder.DropTable( name: "Expressions"); diff --git a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs index 1b4a98c..6f16565 100644 --- a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs +++ b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs @@ -654,31 +654,6 @@ namespace EllieBot.Migrations.Sqlite b.ToTable("DiscordUser"); }); - modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => - { - b.Property<int>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property<DateTime?>("DateAdded") - .HasColumnType("TEXT"); - - b.Property<ulong>("ItemId") - .HasColumnType("INTEGER"); - - b.Property<int>("ItemType") - .HasColumnType("INTEGER"); - - b.Property<int?>("XpSettingsId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("XpSettingsId"); - - b.ToTable("ExcludedItem"); - }); - modelBuilder.Entity("EllieBot.Db.Models.FeedSub", b => { b.Property<int>("Id") @@ -2496,9 +2471,6 @@ namespace EllieBot.Migrations.Sqlite b.Property<ulong>("GuildId") .HasColumnType("INTEGER"); - b.Property<bool>("ServerExcluded") - .HasColumnType("INTEGER"); - b.HasKey("Id"); b.HasIndex("GuildId") @@ -2603,32 +2575,43 @@ namespace EllieBot.Migrations.Sqlite b.Property<ulong>("GuildId") .HasColumnType("INTEGER"); - b.Property<int>("XpAmount") + b.Property<int>("RateType") + .HasColumnType("INTEGER"); + + b.Property<long>("XpAmount") .HasColumnType("INTEGER"); b.HasKey("Id"); - b.HasAlternateKey("GuildId", "ChannelId"); + b.HasAlternateKey("GuildId", "ChannelId", "RateType"); b.ToTable("ChannelXpConfig"); }); modelBuilder.Entity("EllieBot.Modules.Xp.GuildXpConfig", b => { - b.Property<ulong>("GuildId") + b.Property<int>("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property<int>("Cooldown") + b.Property<float>("Cooldown") + .HasColumnType("REAL"); + + b.Property<ulong>("GuildId") .HasColumnType("INTEGER"); - b.Property<int>("XpAmount") + b.Property<int>("RateType") + .HasColumnType("INTEGER"); + + b.Property<long>("XpAmount") .HasColumnType("INTEGER"); b.Property<string>("XpTemplateUrl") .HasColumnType("TEXT"); - b.HasKey("GuildId"); + b.HasKey("Id"); + + b.HasAlternateKey("GuildId", "RateType"); b.ToTable("GuildXpConfig"); }); @@ -2800,13 +2783,6 @@ namespace EllieBot.Migrations.Sqlite b.Navigation("Club"); }); - modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => - { - b.HasOne("EllieBot.Db.Models.XpSettings", null) - .WithMany("ExclusionList") - .HasForeignKey("XpSettingsId"); - }); - modelBuilder.Entity("EllieBot.Db.Models.FilterChannelId", b => { b.HasOne("EllieBot.Db.Models.GuildFilterConfig", null) @@ -3071,8 +3047,6 @@ namespace EllieBot.Migrations.Sqlite { b.Navigation("CurrencyRewards"); - b.Navigation("ExclusionList"); - b.Navigation("RoleRewards"); }); #pragma warning restore 612, 618 diff --git a/src/EllieBot/Modules/Xp/Db/UserXpBatch.cs b/src/EllieBot/Modules/Xp/Db/UserXpBatch.cs index 4ecffe7..7ba9b23 100644 --- a/src/EllieBot/Modules/Xp/Db/UserXpBatch.cs +++ b/src/EllieBot/Modules/Xp/Db/UserXpBatch.cs @@ -11,4 +11,5 @@ public sealed class UserXpBatch public ulong GuildId { get; set; } public string Username { get; set; } = string.Empty; public string AvatarId { get; set; } = string.Empty; + public long XpToGain { get; set; } = 0; } \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs b/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs new file mode 100644 index 0000000..acf7fbb --- /dev/null +++ b/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs @@ -0,0 +1,172 @@ +using System.Runtime.CompilerServices; +using LinqToDB; +using LinqToDB.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using EllieBot.Common.ModuleBehaviors; +using EllieBot.Modules.Xp.Services; + +namespace EllieBot.Modules.Xp; + +using GuildXpRates = (IReadOnlyList<GuildXpConfig> GuildRates, IReadOnlyList<ChannelXpConfig> ChannelRates); + +public class GuildConfigXpService(DbService db, ShardData shardData, XpConfigService xcs) : IReadyExecutor, IEService +{ + private ConcurrentDictionary<(XpRateType RateType, ulong GuildId), XpRate> _guildRates = new(); + private ConcurrentDictionary<ulong, ConcurrentDictionary<(XpRateType, ulong), XpRate>> _channelRates = new(); + + public async Task OnReadyAsync() + { + await using var uow = db.GetDbContext(); + _guildRates = await uow.GetTable<GuildXpConfig>() + .AsNoTracking() + .Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId)) + .ToListAsyncLinqToDB() + .Fmap(list => + list + .ToDictionary( + x => (x.RateType, x.GuildId), + x => new XpRate(x.RateType, x.XpAmount, x.Cooldown)) + .ToConcurrent()); + + _channelRates = await uow.GetTable<ChannelXpConfig>() + .AsNoTracking() + .Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId)) + .ToListAsyncLinqToDB() + .Fmap(x => + x.GroupBy(x => x.GuildId) + .ToDictionary( + x => x.Key, + x => x.ToDictionary( + y => (y.RateType, y.ChannelId), + y => new XpRate(y.RateType, y.XpAmount, y.Cooldown)) + .ToConcurrent()) + .ToConcurrent()); + } + + public async Task<GuildXpRates> GetGuildXpRatesAsync(ulong guildId) + { + await using var uow = db.GetDbContext(); + var guildConfig = await uow.GetTable<GuildXpConfig>() + .AsNoTracking() + .Where(x => x.GuildId == guildId) + .ToListAsyncLinqToDB(); + + var channelRates = await uow.GetTable<ChannelXpConfig>() + .AsNoTracking() + .Where(x => x.GuildId == guildId) + .ToListAsyncLinqToDB(); + + return (guildConfig, channelRates); + } + + public async Task SetGuildXpRateAsync(ulong guildId, XpRateType type, long amount, float cooldown) + { + AmountAndCooldownChecks(amount, cooldown); + + if (type == XpRateType.Voice) + cooldown = 1.0f; + + await using var uow = db.GetDbContext(); + await uow.GetTable<GuildXpConfig>() + .InsertOrUpdateAsync(() => new() + { + GuildId = guildId, + RateType = type, + XpAmount = amount, + Cooldown = cooldown, + }, + (_) => new() + { + Cooldown = cooldown, + XpAmount = amount, + }, + () => new() + { + GuildId = guildId, + RateType = type, + }); + } + + public async Task SetChannelXpRateAsync(ulong guildId, + XpRateType type, + ulong channelId, + long amount, + float cooldown) + { + AmountAndCooldownChecks(amount, cooldown); + + if (type == XpRateType.Voice) + cooldown = 1.0f; + + await using var uow = db.GetDbContext(); + await uow.GetTable<ChannelXpConfig>() + .InsertOrUpdateAsync(() => new() + { + GuildId = guildId, + ChannelId = channelId, + XpAmount = amount, + Cooldown = cooldown, + RateType = type + }, + (_) => new() + { + Cooldown = cooldown, + XpAmount = amount, + }, + () => new() + { + GuildId = guildId, + ChannelId = channelId, + RateType = type, + }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AmountAndCooldownChecks(long amount, float cooldown) + { + ArgumentOutOfRangeException.ThrowIfNegative(amount, nameof(amount)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, 1000, nameof(amount)); + + ArgumentOutOfRangeException.ThrowIfNegative(cooldown, nameof(cooldown)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(cooldown, 1440, nameof(cooldown)); + } + + 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 XpRate GetXpRate(XpRateType type, ulong guildId, ulong channelId) + { + if (_channelRates.TryGetValue(guildId, out var guildChannelRates)) + { + if (guildChannelRates.TryGetValue((type, channelId), out var rate)) + return rate; + } + + if (_guildRates.TryGetValue((type, guildId), out var guildRate)) + return guildRate; + + var conf = xcs.Data; + + return type switch + { + XpRateType.Image => new XpRate(XpRateType.Image, conf.TextXpFromImage, conf.TextXpCooldown / 60.0f), + XpRateType.Voice => new XpRate(XpRateType.Voice, conf.VoiceXpPerMinute, 1.0f), + _ => new XpRate(XpRateType.Text, conf.TextXpPerMessage, conf.TextXpCooldown / 60.0f), + }; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/XpRate/XpRate.cs b/src/EllieBot/Modules/Xp/XpRate/XpRate.cs new file mode 100644 index 0000000..2b283c1 --- /dev/null +++ b/src/EllieBot/Modules/Xp/XpRate/XpRate.cs @@ -0,0 +1,7 @@ +namespace EllieBot.Modules.Xp; + +public readonly record struct XpRate(XpRateType Type, long Amount, float Cooldown) +{ + public bool IsExcluded() + => Amount == 0 || Cooldown == 0; +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs b/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs index c60e15e..4cb471a 100644 --- a/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs +++ b/src/EllieBot/Modules/Xp/XpRate/XpRateCommands.cs @@ -1,10 +1,3 @@ -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 @@ -17,45 +10,49 @@ public partial class Xp public async Task XpRate() { var rates = await _service.GetGuildXpRatesAsync(ctx.Guild.Id); - if (rates.GuildConfig is null && !rates.ChannelRates.Any()) + if (!rates.GuildRates.Any() && !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)); - } + await Response() + .Paginated() + .Items(rates.ChannelRates.GroupBy(x => x.ChannelId).ToList()) + .PageSize(5) + .Page((items, _) => + { + var eb = CreateEmbed() + .WithOkColor(); - if (rates.ChannelRates.Any()) - { - var channelRates = rates.ChannelRates - .Select(c => $"<#{c.ChannelId}>: {GetRateString(c.XpAmount, c.Cooldown)}") - .Join('\n'); + if (rates.GuildRates is not { Count: <= 0 }) + { + eb.AddField(GetText(strs.xp_rate_server), + rates.GuildRates + .Select(x => GetText(strs.xp_rate_str(x.RateType, x.XpAmount, x.Cooldown))) + .Join('\n')); + } - eb.AddField(GetText(strs.xp_rate_channels), channelRates); - } + if (items.Any()) + { + var channelRates = items + .Select(x => $""" + <#{x.Key}> + {x.Select(c => $"- {GetText(strs.xp_rate_str(c.RateType, c.XpAmount, c.Cooldown))}").Join('\n')} + """) + .Join('\n'); - await Response().Embed(eb).SendAsync(); - } + eb.AddField(GetText(strs.xp_rate_channels), channelRates); + } - 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))); + return eb; + }) + .SendAsync(); } [Cmd] [RequireContext(ContextType.Guild)] - public async Task XpRate(int amount, float minutes) + public async Task XpRate(XpRateType type, int amount, float minutes) { if (amount is < 0 or > 1000) { @@ -69,13 +66,13 @@ public partial class Xp return; } - await _service.SetGuildXpRateAsync(ctx.Guild.Id, amount, (int)Math.Ceiling(minutes)); + await _service.SetGuildXpRateAsync(ctx.Guild.Id, type, 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) + public async Task XpRate(IMessageChannel channel, XpRateType type, int amount, float minutes) { if (amount is < 0 or > 1000) { @@ -89,10 +86,10 @@ public partial class Xp return; } - await _service.SetChannelXpRateAsync(ctx.Guild.Id, channel.Id, amount, (int)Math.Ceiling(minutes)); + await _service.SetChannelXpRateAsync(ctx.Guild.Id, type, channel.Id, amount, (int)Math.Ceiling(minutes)); await Response() - .Confirm(strs.xp_rate_channel_set(Format.Bold(channel.ToString()), amount, minutes)) - .SendAsync(); + .Confirm(strs.xp_rate_channel_set(Format.Bold(channel.ToString()), amount, minutes)) + .SendAsync(); } [Cmd] @@ -116,139 +113,4 @@ public partial class Xp 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/Modules/Xp/XpRate/XpRateType.cs b/src/EllieBot/Modules/Xp/XpRate/XpRateType.cs new file mode 100644 index 0000000..f435644 --- /dev/null +++ b/src/EllieBot/Modules/Xp/XpRate/XpRateType.cs @@ -0,0 +1,8 @@ +namespace EllieBot.Modules.Xp; + +public enum XpRateType +{ + Text, + Voice, + Image, +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/XpRate/db/ChannelXpConfig.cs b/src/EllieBot/Modules/Xp/XpRate/db/ChannelXpConfig.cs new file mode 100644 index 0000000..f2b3f9b --- /dev/null +++ b/src/EllieBot/Modules/Xp/XpRate/db/ChannelXpConfig.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace EllieBot.Modules.Xp; + +public class ChannelXpConfig +{ + [Key] + public int Id { get; set; } + + public XpRateType RateType { get; set; } + public ulong GuildId { get; set; } + public ulong ChannelId { get; set; } + public long 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, + x.RateType, + }); + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/XpRate/db/GuildXpConfig.cs b/src/EllieBot/Modules/Xp/XpRate/db/GuildXpConfig.cs new file mode 100644 index 0000000..7bf510d --- /dev/null +++ b/src/EllieBot/Modules/Xp/XpRate/db/GuildXpConfig.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace EllieBot.Modules.Xp; + +public class GuildXpConfig +{ + [Key] + public int Id { get; set; } + + public ulong GuildId { get; set; } + public XpRateType RateType { get; set; } + + public long XpAmount { get; set; } + public float Cooldown { get; set; } + public string? XpTemplateUrl { get; set; } +} + +public sealed class GuildXpConfigEntity : IEntityTypeConfiguration<GuildXpConfig> +{ + public void Configure(EntityTypeBuilder<GuildXpConfig> builder) + { + builder.HasAlternateKey(x => new + { + x.GuildId, + x.RateType, + }); + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs index 552c3fe..a503632 100644 --- a/src/EllieBot/Modules/Xp/XpService.cs +++ b/src/EllieBot/Modules/Xp/XpService.cs @@ -32,11 +32,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand private readonly ICurrencyService _cs; private readonly IHttpClientFactory _httpFactory; private readonly XpConfigService _xpConfig; - private readonly IPubSub _pubSub; - - private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedRoles = new(); - private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedChannels = new(); - private readonly ConcurrentHashSet<ulong> _excludedServers; private readonly DiscordSocketClient _client; @@ -45,8 +40,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand private readonly INotifySubscriber _notifySub; private readonly IMemoryCache _memCache; - private readonly ShardData _shardData; private readonly XpTemplateService _templateService; + private readonly GuildConfigXpService _xpRateService; private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100); @@ -65,7 +60,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand INotifySubscriber notifySub, IMemoryCache memCache, ShardData shardData, - XpTemplateService templateService) + XpTemplateService templateService, + GuildConfigXpService xpRateService) { _db = db; _images = images; @@ -74,47 +70,17 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand _cs = cs; _httpFactory = http; _xpConfig = xpConfig; - _pubSub = pubSub; _notifySub = notifySub; _memCache = memCache; - _shardData = shardData; _templateService = templateService; - _excludedServers = []; - _excludedChannels = new(); + _xpRateService = xpRateService; _client = client; _ps = ps; _c = c; - - if (client.ShardId == 0) - { - } } public async Task OnReadyAsync() { - // initialize ignored - await using (var ctx = _db.GetDbContext()) - { - var xps = await ctx.GetTable<XpSettings>() - .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId)) - .LoadWith(x => x.ExclusionList) - .ToListAsyncLinqToDB(); - - foreach (var xp in xps) - { - if (xp.ServerExcluded) - _excludedServers.Add(xp.GuildId); - - foreach (var item in xp.ExclusionList) - { - if (item.ItemType == ExcludedItemType.Channel) - _excludedChannels.GetOrAdd(xp.GuildId, static _ => []).Add(item.ItemId); - else if (item.ItemType == ExcludedItemType.Role) - _excludedRoles.GetOrAdd(xp.GuildId, static _ => []).Add(item.ItemId); - } - } - } - await Task.WhenAll(UpdateTimer(), VoiceUpdateTimer(), _levelUpQueue.RunAsync()); return; @@ -156,65 +122,66 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand /// <summary> /// The current batch of users that will gain xp /// </summary> - private readonly ConcurrentHashSet<IGuildUser> _usersBatch = []; + private readonly ConcurrentHashSet<XpQueueEntry> _usersBatch = []; /// <summary> /// The current batch of users that will gain voice xp /// </summary> - private readonly ConcurrentHashSet<IGuildUser> _voiceXpBatch = []; + private readonly HashSet<IGuildUser> _voiceXpBatch = []; private async Task UpdateVoiceXp() { - var xpAmount = _xpConfig.Data.VoiceXpPerMinute; - - if (xpAmount <= 0) - return; - - var oldBatch = _voiceXpBatch.ToArray(); + var oldBatch = _voiceXpBatch.ToHashSet(); _voiceXpBatch.Clear(); - var validUsers = new HashSet<IGuildUser>(); + var validUsers = new List<XpQueueEntry>(oldBatch.Count); var guilds = _client.Guilds; foreach (var g in guilds) { - if (IsServerExcluded(g.Id)) - continue; - foreach (var vc in g.VoiceChannels) { if (!IsVoiceChannelActive(vc)) continue; - if (IsChannelExcluded(vc)) + var rate = _xpRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id); + + if (rate.IsExcluded()) continue; foreach (var u in vc.ConnectedUsers) { - if (IsServerOrRoleExcluded(u) || !UserParticipatingInVoiceChannel(u)) + if (!UserParticipatingInVoiceChannel(u)) continue; if (oldBatch.Contains(u)) - validUsers.Add(u); + { + validUsers.Add(new(u, rate.Amount)); + } _voiceXpBatch.Add(u); } } } - await UpdateXpInternalAsync(validUsers.DistinctBy(x => x.Id).ToArray(), xpAmount); + await UpdateXpInternalAsync(validUsers.ToArray()); } private async Task UpdateXp() { - var xpAmount = _xpConfig.Data.TextXpPerMessage; + // might want to lock this, but it's not a big deal + + // or do something like this + // foreach (var item in currentBatch) + // _usersBatch.TryRemove(item); + var currentBatch = _usersBatch.ToArray(); _usersBatch.Clear(); - await UpdateXpInternalAsync(currentBatch, xpAmount); + await UpdateXpInternalAsync(currentBatch); } - private async Task UpdateXpInternalAsync(IGuildUser[] currentBatch, int xpAmount) + private async Task UpdateXpInternalAsync(XpQueueEntry[] currentBatch) { if (currentBatch.Length == 0) return; @@ -227,16 +194,17 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand await batchTable.BulkCopyAsync(currentBatch.Select(x => new UserXpBatch() { - GuildId = x.GuildId, - UserId = x.Id, - Username = x.Username, - AvatarId = x.DisplayAvatarId + GuildId = x.User.GuildId, + UserId = x.User.Id, + Username = x.User.Username, + AvatarId = x.User.DisplayAvatarId, + XpToGain = x.Xp })); await lctx.ExecuteAsync( $""" INSERT INTO UserXpStats (GuildId, UserId, Xp) - SELECT "{tempTableName}"."GuildId", "{tempTableName}"."UserId", {xpAmount} + SELECT "{tempTableName}"."GuildId", "{tempTableName}"."UserId", "XpToGain" FROM {tempTableName} WHERE TRUE ON CONFLICT (GuildId, UserId) DO UPDATE @@ -251,9 +219,13 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand (batch, stats) => stats) .ToListAsyncLinqToDB(); + var userToXp = currentBatch.ToDictionary(x => x.User.Id, x => x.Xp); foreach (var u in updated) { - var oldStats = new LevelStats(u.Xp - xpAmount); + if (!userToXp.TryGetValue(u.UserId, out var xpGained)) + continue; + + var oldStats = new LevelStats(u.Xp - xpGained); var newStats = new LevelStats(u.Xp); Log.Information("User {User} xp updated from {OldLevel} to {NewLevel}", @@ -531,8 +503,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand { if (UserParticipatingInVoiceChannel(user)) { - count++; - if (count >= 2) + if (++count >= 2) return true; } } @@ -543,27 +514,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand private static bool UserParticipatingInVoiceChannel(SocketGuildUser user) => !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted; - private bool IsServerOrRoleExcluded(SocketGuildUser user) - { - if (_excludedServers.Contains(user.Guild.Id)) - return true; - - if (_excludedRoles.TryGetValue(user.Guild.Id, out var roles) && user.Roles.Any(x => roles.Contains(x.Id))) - return true; - - return false; - } - - private bool IsChannelExcluded(IGuildChannel channel) - { - if (_excludedChannels.TryGetValue(channel.Guild.Id, out var chans) - && (chans.Contains(channel.Id) - || (channel is SocketThreadChannel tc && chans.Contains(tc.ParentChannel.Id)))) - return true; - - return false; - } - public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage arg) { if (arg.Author is not SocketGuildUser user || user.IsBot) @@ -574,27 +524,36 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand _ = Task.Run(async () => { - if (IsChannelExcluded(gc)) + var isImage = arg.Attachments.Any(a => a.Height >= 16 && a.Width >= 16); + var isText = arg.Content.Contains(' ') || arg.Content.Length >= 5; + + var textRate = _xpRateService.GetXpRate(XpRateType.Text, guild.Id, gc.Id); + + XpRate rate; + if (isImage) + { + var imageRate = _xpRateService.GetXpRate(XpRateType.Image, guild.Id, gc.Id); + if (imageRate.IsExcluded()) + return; + + rate = imageRate; + } + else if (isText) + { + if (textRate.IsExcluded()) + return; + + rate = textRate; + } + else + { + return; + } + + if (!await TryAddUserGainedXpAsync(user.Id, rate.Cooldown)) return; - if (IsServerOrRoleExcluded(user)) - return; - - var xpConf = _xpConfig.Data; - var xp = 0; - if (arg.Attachments.Any(a => a.Height >= 128 && a.Width >= 128)) - xp = xpConf.TextXpFromImage; - - if (arg.Content.Contains(' ') || arg.Content.Length >= 5) - xp = Math.Max(xp, xpConf.TextXpPerMessage); - - if (xp <= 0) - return; - - if (!await TryAddUserGainedXpAsync(user.Id, xpConf.TextXpCooldown)) - return; - - _usersBatch.Add(user); + _usersBatch.Add(new(user, rate.Amount)); }); return Task.CompletedTask; @@ -621,28 +580,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand await uow.SaveChangesAsync(); } - public bool IsServerExcluded(ulong id) - => _excludedServers.Contains(id); - - public IEnumerable<ulong> GetExcludedRoles(ulong id) + private Task<bool> TryAddUserGainedXpAsync(ulong userId, float cdInMinutes) { - if (_excludedRoles.TryGetValue(id, out var val)) - return val.ToArray(); - - return []; - } - - public IEnumerable<ulong> GetExcludedChannels(ulong id) - { - if (_excludedChannels.TryGetValue(id, out var val)) - return val.ToArray(); - - return []; - } - - private Task<bool> TryAddUserGainedXpAsync(ulong userId, int cdInSeconds) - { - if (cdInSeconds <= 0) + if (cdInMinutes <= float.Epsilon) return Task.FromResult(true); if (_memCache.TryGetValue(userId, out _)) @@ -651,7 +591,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand using var entry = _memCache.CreateEntry(userId); entry.Value = true; - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(cdInSeconds); + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(cdInMinutes); return Task.FromResult(true); } @@ -674,81 +614,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand guildRank); } - public async Task<bool> ToggleExcludeServerAsync(ulong id) - { - await using var uow = _db.GetDbContext(); - var xpSetting = await uow.XpSettingsFor(id); - if (_excludedServers.Add(id)) - { - xpSetting.ServerExcluded = true; - await uow.SaveChangesAsync(); - return true; - } - - _excludedServers.TryRemove(id); - xpSetting.ServerExcluded = false; - await uow.SaveChangesAsync(); - return false; - } - - public async Task<bool> ToggleExcludeRoleAsync(ulong guildId, ulong rId) - { - var roles = _excludedRoles.GetOrAdd(guildId, _ => []); - await using var uow = _db.GetDbContext(); - var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList)); - var excludeObj = new ExcludedItem - { - ItemId = rId, - ItemType = ExcludedItemType.Role - }; - - if (roles.Add(rId)) - { - if (xpSetting.ExclusionList.Add(excludeObj)) - await uow.SaveChangesAsync(); - - return true; - } - - roles.TryRemove(rId); - - var toDelete = xpSetting.ExclusionList.FirstOrDefault(x => x.Equals(excludeObj)); - if (toDelete is not null) - { - uow.Remove(toDelete); - await uow.SaveChangesAsync(); - } - - return false; - } - - public async Task<bool> ToggleExcludeChannelAsync(ulong guildId, ulong chId) - { - var channels = _excludedChannels.GetOrAdd(guildId, _ => []); - await using var uow = _db.GetDbContext(); - var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList)); - var excludeObj = new ExcludedItem - { - ItemId = chId, - ItemType = ExcludedItemType.Channel - }; - - if (channels.Add(chId)) - { - if (xpSetting.ExclusionList.Add(excludeObj)) - await uow.SaveChangesAsync(); - - return true; - } - - channels.TryRemove(chId); - - if (xpSetting.ExclusionList.Remove(excludeObj)) - await uow.SaveChangesAsync(); - - return false; - } - public async Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(IGuildUser user) { var stats = await GetUserStatsAsync(user); @@ -1224,8 +1089,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand if (item is null || item.Price < 0) return BuyResult.UnknownItem; - if (item.Price > 0 && - !await _cs.RemoveAsync(userId, item.Price, new("xpshop", "buy", $"Background {key}"))) + if (item.Price > 0 && !await _cs.RemoveAsync(userId, item.Price, new("xpshop", "buy", $"Background {key}"))) return BuyResult.InsufficientFunds; @@ -1438,4 +1302,13 @@ public sealed class XpTemplateService : IEService, IReadyExecutor public XpTemplate GetTemplate() => _template; +} + +public readonly record struct XpQueueEntry(IGuildUser User, long Xp) +{ + public bool Equals(XpQueueEntry? other) + => other?.User == User; + + public override int GetHashCode() + => User.GetHashCode(); } \ No newline at end of file diff --git a/src/EllieBot/Services/GrpcApi/XpSvc.cs b/src/EllieBot/Services/GrpcApi/XpSvc.cs index 3cf9167..674dc48 100644 --- a/src/EllieBot/Services/GrpcApi/XpSvc.cs +++ b/src/EllieBot/Services/GrpcApi/XpSvc.cs @@ -35,9 +35,9 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService if (guild is null) throw new RpcException(new Status(StatusCode.NotFound, "Guild not found")); - var excludedChannels = _xp.GetExcludedChannels(request.GuildId); - var excludedRoles = _xp.GetExcludedRoles(request.GuildId); - var isServerExcluded = _xp.IsServerExcluded(request.GuildId); + var excludedChannels = new List<ulong>(); + var excludedRoles = new List<ulong>(); + var isServerExcluded = false; var reply = new GetXpSettingsReply(); @@ -82,59 +82,6 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService return reply; } - public override async Task<AddExclusionReply> AddExclusion(AddExclusionRequest request, ServerCallContext context) - { - await Task.Yield(); - - var success = false; - var guild = _client.GetGuild(request.GuildId); - - if (guild is null) - throw new RpcException(new Status(StatusCode.NotFound, "Guild not found")); - - if (request.Type == "Role") - { - if (guild.GetRole(request.Id) is null) - return new() - { - Success = false - }; - - success = await _xp.ToggleExcludeRoleAsync(request.GuildId, request.Id); - } - else if (request.Type == "Channel") - { - if (guild.GetTextChannel(request.Id) is null) - return new() - { - Success = false - }; - - success = await _xp.ToggleExcludeChannelAsync(request.GuildId, request.Id); - } - - return new() - { - Success = success - }; - } - - public override async Task<DeleteExclusionReply> DeleteExclusion( - DeleteExclusionRequest request, - ServerCallContext context) - { - var success = false; - if (request.Type == "Role") - success = await _xp.ToggleExcludeRoleAsync(request.GuildId, request.Id); - else - success = await _xp.ToggleExcludeChannelAsync(request.GuildId, request.Id); - - return new DeleteExclusionReply - { - Success = success - }; - } - public override async Task<AddRewardReply> AddReward(AddRewardRequest request, ServerCallContext context) { await Task.Yield(); @@ -267,15 +214,4 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService return reply; } - - public override async Task<SetServerExclusionReply> SetServerExclusion( - SetServerExclusionRequest request, - ServerCallContext context) - { - var newValue = await _xp.ToggleExcludeServerAsync(request.GuildId); - return new() - { - Success = newValue - }; - } } \ No newline at end of file diff --git a/src/EllieBot/_common/Interaction/EllieInteraction.cs b/src/EllieBot/_common/Interaction/EllieInteraction.cs index 0206599..a3f67db 100644 --- a/src/EllieBot/_common/Interaction/EllieInteraction.cs +++ b/src/EllieBot/_common/Interaction/EllieInteraction.cs @@ -40,7 +40,7 @@ public abstract class EllieInteractionBase message = msg; Client.InteractionCreated += OnInteraction; - await Task.WhenAny(Task.Delay(30_000), _interactionCompletedSource.Task); + await Task.WhenAny(Task.Delay(600_000), _interactionCompletedSource.Task); Client.InteractionCreated -= OnInteraction; if (_clearAfter) @@ -130,7 +130,7 @@ public sealed class EllieModalSubmitHandler message = msg; Client.ModalSubmitted += OnInteraction; - await Task.WhenAny(Task.Delay(300_000), _interactionCompletedSource.Task); + await Task.WhenAny(Task.Delay(600_000), _interactionCompletedSource.Task); Client.ModalSubmitted -= OnInteraction; await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build()); diff --git a/src/EllieBot/_common/Sender/ResponseBuilder.PaginationSender.cs b/src/EllieBot/_common/Sender/ResponseBuilder.PaginationSender.cs index 92d871a..915a3ed 100644 --- a/src/EllieBot/_common/Sender/ResponseBuilder.PaginationSender.cs +++ b/src/EllieBot/_common/Sender/ResponseBuilder.PaginationSender.cs @@ -159,7 +159,7 @@ public partial class ResponseBuilder await Task.WhenAll(left.RunAsync(msg), extra?.RunAsync(msg) ?? Task.CompletedTask, right.RunAsync(msg)); - await Task.Delay(30_000); + await Task.Delay(600_000); await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build()); } diff --git a/src/EllieBot/strings/commands/commands.en-US.yml b/src/EllieBot/strings/commands/commands.en-US.yml index cb691ca..e366798 100644 --- a/src/EllieBot/strings/commands/commands.en-US.yml +++ b/src/EllieBot/strings/commands/commands.en-US.yml @@ -4935,20 +4935,25 @@ fishspot: xprate: desc: |- Sets the xp rate for the server or the specified channel. - First specify the amount, and then the cooldown in minutes. + First specify the type, amount, and then the cooldown in minutes. Provide no parameters to see the current rates. + Cooldown has no effect on voice xp, as any amount is gained per minute. ex: - '' - - '3 5' - - '#channel 50 1' + - 'text 3 5' + - '#channel voice 50 1' params: - { } - - amount: + - type: + desc: "The type of rate to set. One of: text, voice or image." + 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." + type: + desc: "The type of rate to set. One of: text, voice or image." amount: desc: "The amount of xp to give per message." minutes: diff --git a/src/EllieBot/strings/responses/responses.en-US.json b/src/EllieBot/strings/responses/responses.en-US.json index e5dadaf..886c8eb 100644 --- a/src/EllieBot/strings/responses/responses.en-US.json +++ b/src/EllieBot/strings/responses/responses.en-US.json @@ -1170,12 +1170,12 @@ "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_cooldown_invalid": "Cooldown must be between 0 and 1440.", + "xp_rate_server": "Server xp rates", + "xp_rate_str": "`{0}:` {1} xp every {2} min.", + "xp_rate_channels": "Channel xp rates", + "xp_rate_server_set": "Server xp rate set to **{0}** xp per every **{1}** min.", + "xp_rate_channel_set": "Channel **{0}** xp rate set to **{1}** xp per every **{2}** min.", "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"