Fixed xprate presentation and reworked it internally to support voice, text and image xp

This commit is contained in:
Toastie 2025-03-01 13:56:17 +13:00
parent 5e95abadc8
commit 11b3705939
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
25 changed files with 595 additions and 740 deletions

View file

@ -121,7 +121,6 @@ public static class GuildConfigExtensions
.InsertWithOutputAsync(() => new()
{
GuildId = guildId,
ServerExcluded = false,
});
return srs;

View file

@ -1,4 +1,3 @@
#nullable disable
namespace EllieBot.Db.Models;
public class UserXpStats : DbEntity

View file

@ -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();

View file

@ -15,8 +15,5 @@ public class XpSettingsEntityConfiguration : IEntityTypeConfiguration<XpSettings
builder.HasMany(x => x.RoleRewards)
.WithOne();
builder.HasMany(x => x.ExclusionList)
.WithOne();
}
}

View file

@ -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;

View file

@ -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

View file

@ -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");

View file

@ -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

View file

@ -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');

View file

@ -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

View file

@ -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");

View file

@ -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

View file

@ -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;
}

View file

@ -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),
};
}
}

View file

@ -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;
}

View file

@ -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
});
}
}

View file

@ -0,0 +1,8 @@
namespace EllieBot.Modules.Xp;
public enum XpRateType
{
Text,
Voice,
Image,
}

View file

@ -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,
});
}
}

View file

@ -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,
});
}
}

View file

@ -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();
}

View file

@ -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
};
}
}

View file

@ -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());

View file

@ -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());
}

View file

@ -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:

View file

@ -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"