forked from EllieBotDevs/elliebot
re-added .xpex and .xpexl commands as there was no way to exclude users and roles from the xp system anymore
This commit is contained in:
parent
ca46786c5e
commit
1d667db598
19 changed files with 631 additions and 169 deletions
src/EllieBot
Db/Models/xp
Migrations
PostgreSql
20250318221943_xpexclusion.sql20250318222207_init.Designer.cs20250318222207_init.csPostgreSqlContextModelSnapshot.cs
Sqlite
Modules/Xp
strings
|
@ -1,14 +0,0 @@
|
|||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class ExcludedItem : DbEntity
|
||||
{
|
||||
public int? XpSettingsId { get; set; }
|
||||
public ulong ItemId { get; set; }
|
||||
public ExcludedItemType ItemType { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
=> ItemId.GetHashCode() ^ ItemType.GetHashCode();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is ExcludedItem ei && ei.ItemId == ItemId && ei.ItemType == ItemType;
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
namespace EllieBot.Db.Models;
|
||||
|
||||
public enum ExcludedItemType { Channel, Role }
|
||||
public enum XpExcludedItemType
|
||||
{
|
||||
User,
|
||||
Role
|
||||
}
|
31
src/EllieBot/Db/Models/xp/XpExcludedItem.cs
Normal file
31
src/EllieBot/Db/Models/xp/XpExcludedItem.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class XpExcludedItem
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public ulong GuildId { get; set; }
|
||||
|
||||
public XpExcludedItemType ItemType { get; set; }
|
||||
public ulong ItemId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class XpExclusionEntityConfig : IEntityTypeConfiguration<XpExcludedItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<XpExcludedItem> builder)
|
||||
{
|
||||
builder.HasIndex(x => x.GuildId);
|
||||
|
||||
builder.HasAlternateKey(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.ItemType,
|
||||
x.ItemId
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
START TRANSACTION;
|
||||
CREATE TABLE xpexcludeditem (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
guildid numeric(20,0) NOT NULL,
|
||||
itemtype integer NOT NULL,
|
||||
itemid numeric(20,0) NOT NULL,
|
||||
CONSTRAINT pk_xpexcludeditem PRIMARY KEY (id),
|
||||
CONSTRAINT ak_xpexcludeditem_guildid_itemtype_itemid UNIQUE (guildid, itemtype, itemid)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_xpexcludeditem_guildid ON xpexcludeditem (guildid);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250318221943_xpexclusion', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||
namespace EllieBot.Migrations.PostgreSql
|
||||
{
|
||||
[DbContext(typeof(PostgreSqlContext))]
|
||||
[Migration("20250317063309_init")]
|
||||
[Migration("20250318222207_init")]
|
||||
partial class init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -3263,6 +3263,39 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("xpcurrencyreward", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<decimal>("ItemId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("itemid");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("itemtype");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_xpexcludeditem");
|
||||
|
||||
b.HasAlternateKey("GuildId", "ItemType", "ItemId")
|
||||
.HasName("ak_xpexcludeditem_guildid_itemtype_itemid");
|
||||
|
||||
b.HasIndex("GuildId")
|
||||
.HasDatabaseName("ix_xpexcludeditem_guildid");
|
||||
|
||||
b.ToTable("xpexcludeditem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
|
@ -1179,6 +1179,22 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table.PrimaryKey("pk_warnings", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "xpexcludeditem",
|
||||
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),
|
||||
itemtype = table.Column<int>(type: "integer", nullable: false),
|
||||
itemid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_xpexcludeditem", x => x.id);
|
||||
table.UniqueConstraint("ak_xpexcludeditem_guildid_itemtype_itemid", x => new { x.guildid, x.itemtype, x.itemid });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "xpsettings",
|
||||
columns: table => new
|
||||
|
@ -2280,6 +2296,11 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
table: "xpcurrencyreward",
|
||||
column: "xpsettingsid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_xpexcludeditem_guildid",
|
||||
table: "xpexcludeditem",
|
||||
column: "guildid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_xprolereward_xpsettingsid_level",
|
||||
table: "xprolereward",
|
||||
|
@ -2574,6 +2595,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "xpcurrencyreward");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "xpexcludeditem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "xprolereward");
|
||||
|
|
@ -3260,6 +3260,39 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("xpcurrencyreward", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<decimal>("ItemId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("itemid");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("itemtype");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_xpexcludeditem");
|
||||
|
||||
b.HasAlternateKey("GuildId", "ItemType", "ItemId")
|
||||
.HasName("ak_xpexcludeditem_guildid_itemtype_itemid");
|
||||
|
||||
b.HasIndex("GuildId")
|
||||
.HasDatabaseName("ix_xpexcludeditem_guildid");
|
||||
|
||||
b.ToTable("xpexcludeditem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "XpExcludedItem" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_XpExcludedItem" PRIMARY KEY AUTOINCREMENT,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"ItemType" INTEGER NOT NULL,
|
||||
"ItemId" INTEGER NOT NULL,
|
||||
CONSTRAINT "AK_XpExcludedItem_GuildId_ItemType_ItemId" UNIQUE ("GuildId", "ItemType", "ItemId")
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_XpExcludedItem_GuildId" ON "XpExcludedItem" ("GuildId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250318221922_xpexclusion', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace EllieBot.Migrations.Sqlite
|
||||
{
|
||||
[DbContext(typeof(SqliteContext))]
|
||||
[Migration("20250317063300_init")]
|
||||
[Migration("20250318222152_init")]
|
||||
partial class init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -2428,6 +2428,30 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("XpCurrencyReward");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasAlternateKey("GuildId", "ItemType", "ItemId");
|
||||
|
||||
b.HasIndex("GuildId");
|
||||
|
||||
b.ToTable("XpExcludedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
|
@ -1181,6 +1181,22 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table.PrimaryKey("PK_Warnings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "XpExcludedItem",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ItemType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ItemId = table.Column<ulong>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_XpExcludedItem", x => x.Id);
|
||||
table.UniqueConstraint("AK_XpExcludedItem_GuildId_ItemType_ItemId", x => new { x.GuildId, x.ItemType, x.ItemId });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "XpSettings",
|
||||
columns: table => new
|
||||
|
@ -2282,6 +2298,11 @@ namespace EllieBot.Migrations.Sqlite
|
|||
table: "XpCurrencyReward",
|
||||
column: "XpSettingsId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_XpExcludedItem_GuildId",
|
||||
table: "XpExcludedItem",
|
||||
column: "GuildId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_XpRoleReward_XpSettingsId_Level",
|
||||
table: "XpRoleReward",
|
||||
|
@ -2576,6 +2597,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "XpCurrencyReward");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "XpExcludedItem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "XpRoleReward");
|
||||
|
|
@ -2425,6 +2425,30 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("XpCurrencyReward");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.XpExcludedItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasAlternateKey("GuildId", "ItemType", "ItemId");
|
||||
|
||||
b.HasIndex("GuildId");
|
||||
|
||||
b.ToTable("XpExcludedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
84
src/EllieBot/Modules/Xp/XpExclusion/XpExclusionCommands.cs
Normal file
84
src/EllieBot/Modules/Xp/XpExclusion/XpExclusionCommands.cs
Normal file
|
@ -0,0 +1,84 @@
|
|||
using EllieBot.Db.Models;
|
||||
|
||||
namespace EllieBot.Modules.Xp;
|
||||
|
||||
public partial class Xp
|
||||
{
|
||||
[RequireUserPermission(GuildPermission.Administrator)]
|
||||
public class XpExclusionCommands : EllieModule<XpExclusionService>
|
||||
{
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task XpExclusion()
|
||||
{
|
||||
var exclusions = await _service.GetExclusionsAsync(ctx.Guild.Id);
|
||||
|
||||
if (!exclusions.Any())
|
||||
{
|
||||
await Response().Pending(strs.xp_exclusion_none).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(exclusions.OrderBy(x => x.ItemType).ToList())
|
||||
.PageSize(10)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.xp_exclusion_title));
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var itemType = item.ItemType;
|
||||
var mention = GetMention(itemType, item.ItemId);
|
||||
|
||||
eb.AddField(itemType.ToString(), mention);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task XpExclude([Leftover] IRole role)
|
||||
=> await XpExclude(XpExcludedItemType.Role, role.Id);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task XpExclude([Leftover] IUser user)
|
||||
=> await XpExclude(XpExcludedItemType.User, user.Id);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task XpExclude(XpExcludedItemType type, ulong itemId)
|
||||
{
|
||||
var isExcluded = await _service.ToggleExclusionAsync(ctx.Guild.Id, type, itemId);
|
||||
|
||||
if (isExcluded)
|
||||
await Response()
|
||||
.Confirm(strs.xp_exclude_added(type.ToString(), GetMention(type, itemId)))
|
||||
.SendAsync();
|
||||
else
|
||||
await Response()
|
||||
.Confirm(strs.xp_exclude_removed(type.ToString(), GetMention(type, itemId)))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private string GetMention(XpExcludedItemType itemType, ulong itemId)
|
||||
=> itemType switch
|
||||
{
|
||||
XpExcludedItemType.Role => ctx.Guild.GetRole(itemId)?.ToString() ?? itemId.ToString(),
|
||||
XpExcludedItemType.User => (ctx.Guild as SocketGuild)?.GetUser(itemId)?.ToString() ??
|
||||
itemId.ToString(),
|
||||
_ => itemId.ToString()
|
||||
};
|
||||
}
|
||||
}
|
99
src/EllieBot/Modules/Xp/XpExclusion/XpExclusionService.cs
Normal file
99
src/EllieBot/Modules/Xp/XpExclusion/XpExclusionService.cs
Normal file
|
@ -0,0 +1,99 @@
|
|||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db.Models;
|
||||
using LinqToDB;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EllieBot.Modules.Xp;
|
||||
|
||||
public class XpExclusionService(DbService db, ShardData shardData) : IReadyExecutor, IEService
|
||||
{
|
||||
private ConcurrentHashSet<(ulong GuildId, XpExcludedItemType ItemType, ulong ItemId)> _exclusions = new();
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
await using var uow = db.GetDbContext();
|
||||
_exclusions = await uow.GetTable<XpExcludedItem>()
|
||||
.Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId))
|
||||
.ToListAsyncLinqToDB()
|
||||
.Fmap(x => x
|
||||
.Select(x => (x.GuildId, x.ItemType, x.ItemId))
|
||||
.ToHashSet()
|
||||
.ToConcurrentSet());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles exclusion for the specified item. If the item was excluded, it will be included
|
||||
/// and vice versa.
|
||||
/// </summary>
|
||||
/// <param name="guildId">ID of the guild</param>
|
||||
/// <param name="itemType">Type of the item to toggle exclusion for</param>
|
||||
/// <param name="itemId">ID of the item to toggle exclusion for</param>
|
||||
/// <returns>True if the item is now excluded, false if it's no longer excluded</returns>
|
||||
public async Task<bool> ToggleExclusionAsync(ulong guildId, XpExcludedItemType itemType, ulong itemId)
|
||||
{
|
||||
var key = (guildId, itemType, itemId);
|
||||
var isExcluded = false;
|
||||
|
||||
await using var uow = db.GetDbContext();
|
||||
if (_exclusions.Contains(key))
|
||||
{
|
||||
isExcluded = false;
|
||||
// item exists, remove it
|
||||
await uow.GetTable<XpExcludedItem>()
|
||||
.Where(x => x.GuildId == guildId
|
||||
&& x.ItemType == itemType
|
||||
&& x.ItemId == itemId)
|
||||
.DeleteAsync();
|
||||
|
||||
_exclusions.TryRemove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
isExcluded = true;
|
||||
// item doesn't exist, add it
|
||||
await uow.GetTable<XpExcludedItem>()
|
||||
.InsertOrUpdateAsync(() => new XpExcludedItem
|
||||
{
|
||||
GuildId = guildId,
|
||||
ItemType = itemType,
|
||||
ItemId = itemId
|
||||
},
|
||||
_ => new(),
|
||||
() => new XpExcludedItem
|
||||
{
|
||||
GuildId = guildId,
|
||||
ItemType = itemType,
|
||||
ItemId = itemId
|
||||
});
|
||||
|
||||
_exclusions.Add(key);
|
||||
}
|
||||
|
||||
return isExcluded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all excluded items for a guild.
|
||||
/// </summary>
|
||||
/// <param name="guildId">ID of the guild</param>
|
||||
/// <returns>List of excluded items in the guild</returns>
|
||||
public async Task<IReadOnlyList<XpExcludedItem>> GetExclusionsAsync(ulong guildId)
|
||||
{
|
||||
await using var uow = db.GetDbContext();
|
||||
return await uow.GetTable<XpExcludedItem>()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.ToListAsyncLinqToDB();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the specified item is excluded from XP gain.
|
||||
/// </summary>
|
||||
/// <param name="guildId">ID of the guild</param>
|
||||
/// <param name="itemType">Type of the item</param>
|
||||
/// <param name="itemId">ID of the item</param>
|
||||
/// <returns>True if the item is excluded, otherwise false</returns>
|
||||
public bool IsExcluded(ulong guildId, XpExcludedItemType itemType, ulong itemId)
|
||||
=> _exclusions.Contains((guildId, itemType, itemId));
|
||||
}
|
|
@ -9,7 +9,7 @@ namespace EllieBot.Modules.Xp;
|
|||
|
||||
using GuildXpRates = (IReadOnlyList<GuildXpConfig> GuildRates, IReadOnlyList<ChannelXpConfig> ChannelRates);
|
||||
|
||||
public class GuildConfigXpService(DbService db, ShardData shardData, XpConfigService xcs) : IReadyExecutor, IEService
|
||||
public class XpRateService(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();
|
||||
|
|
|
@ -3,7 +3,7 @@ namespace EllieBot.Modules.Xp;
|
|||
public partial class Xp
|
||||
{
|
||||
[RequireUserPermission(GuildPermission.ManageGuild)]
|
||||
public class XpRateCommands : EllieModule<GuildConfigXpService>
|
||||
public class XpRateCommands : EllieModule<XpRateService>
|
||||
{
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
|
|
|
@ -40,7 +40,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
private readonly INotifySubscriber _notifySub;
|
||||
private readonly IMemoryCache _memCache;
|
||||
private readonly XpTemplateService _templateService;
|
||||
private readonly GuildConfigXpService _xpRateService;
|
||||
private readonly XpRateService _xpRateRateService;
|
||||
private readonly XpExclusionService _xpExcl;
|
||||
|
||||
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100);
|
||||
|
||||
|
@ -60,7 +61,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
IMemoryCache memCache,
|
||||
ShardData shardData,
|
||||
XpTemplateService templateService,
|
||||
GuildConfigXpService xpRateService)
|
||||
XpRateService xpRateRateService,
|
||||
XpExclusionService xpExcl
|
||||
)
|
||||
{
|
||||
_db = db;
|
||||
_images = images;
|
||||
|
@ -72,7 +75,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
_notifySub = notifySub;
|
||||
_memCache = memCache;
|
||||
_templateService = templateService;
|
||||
_xpRateService = xpRateService;
|
||||
_xpRateRateService = xpRateRateService;
|
||||
_xpExcl = xpExcl;
|
||||
_client = client;
|
||||
_ps = ps;
|
||||
_c = c;
|
||||
|
@ -143,7 +147,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
if (!IsVoiceChannelActive(vc))
|
||||
continue;
|
||||
|
||||
var rate = _xpRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id);
|
||||
var rate = _xpRateRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id);
|
||||
|
||||
if (rate.IsExcluded())
|
||||
continue;
|
||||
|
@ -153,6 +157,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
if (!UserParticipatingInVoiceChannel(u))
|
||||
continue;
|
||||
|
||||
if (IsUserExcluded(g, u))
|
||||
continue;
|
||||
|
||||
if (oldBatch.Contains(u))
|
||||
{
|
||||
validUsers.Add(new(u, rate.Amount, vc.Id));
|
||||
|
@ -509,15 +516,18 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (IsUserExcluded(guild, user))
|
||||
return;
|
||||
|
||||
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);
|
||||
var textRate = _xpRateRateService.GetXpRate(XpRateType.Text, guild.Id, gc.Id);
|
||||
|
||||
XpRate rate;
|
||||
if (isImage)
|
||||
{
|
||||
var imageRate = _xpRateService.GetXpRate(XpRateType.Image, guild.Id, gc.Id);
|
||||
var imageRate = _xpRateRateService.GetXpRate(XpRateType.Image, guild.Id, gc.Id);
|
||||
if (imageRate.IsExcluded())
|
||||
return;
|
||||
|
||||
|
@ -544,6 +554,20 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool IsUserExcluded(IGuild guild, SocketGuildUser user)
|
||||
{
|
||||
if (_xpExcl.IsExcluded(guild.Id, XpExcludedItemType.User, user.Id))
|
||||
return true;
|
||||
|
||||
foreach (var role in user.Roles)
|
||||
{
|
||||
if (_xpExcl.IsExcluded(guild.Id, XpExcludedItemType.Role, role.Id))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<int> AddXpToUsersAsync(ulong guildId, long amount, params ulong[] userIds)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
|
@ -614,49 +638,54 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
}
|
||||
|
||||
var avatarUrl = stats.User.RealAvatarUrl();
|
||||
byte[] avatarImageData = null;
|
||||
|
||||
if (avatarUrl is not null)
|
||||
var avatarFetchTask = Task.Run(async () =>
|
||||
{
|
||||
var result = await _c.GetImageDataAsync(avatarUrl);
|
||||
if (!result.TryPickT0(out avatarImageData, out _))
|
||||
try
|
||||
{
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
{
|
||||
var avatarData = await http.GetByteArrayAsync(avatarUrl);
|
||||
using (var tempDraw = Image.Load<Rgba32>(avatarData))
|
||||
{
|
||||
tempDraw.Mutate(x => x
|
||||
.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y)
|
||||
.ApplyRoundedCorners(Math.Max(template.User.Icon.Size.X,
|
||||
template.User.Icon.Size.Y)
|
||||
/ 2.0f));
|
||||
await using (var stream = await tempDraw.ToStreamAsync())
|
||||
{
|
||||
avatarImageData = stream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (avatarUrl is null)
|
||||
return null;
|
||||
|
||||
await _c.SetImageDataAsync(avatarUrl, avatarImageData);
|
||||
var result = await _c.GetImageDataAsync(avatarUrl);
|
||||
if (result.TryPickT0(out var imgData, out _))
|
||||
return imgData;
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
|
||||
var avatarData = await http.GetByteArrayAsync(avatarUrl);
|
||||
using var tempDraw = Image.Load<Rgba32>(avatarData);
|
||||
|
||||
tempDraw.Mutate(x => x
|
||||
.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y)
|
||||
.ApplyRoundedCorners(Math.Max(template.User.Icon.Size.X,
|
||||
template.User.Icon.Size.Y)
|
||||
/ 2.0f));
|
||||
await using var stream = await tempDraw.ToStreamAsync();
|
||||
var data = stream.ToArray();
|
||||
await _c.SetImageDataAsync(avatarUrl, data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
using var img = Image.Load<Rgba32>(bgBytes);
|
||||
|
||||
if (template.User.Name.Show)
|
||||
|
||||
img.Mutate(x =>
|
||||
{
|
||||
var fontSize = (int)(template.User.Name.FontSize * 0.9);
|
||||
var username = stats.User.ToString();
|
||||
var usernameFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
|
||||
|
||||
var size = TextMeasurer.MeasureSize($"@{username}", new(usernameFont));
|
||||
var scale = 400f / size.Width;
|
||||
if (scale < 1)
|
||||
usernameFont = _fonts.NotoSans.CreateFont(template.User.Name.FontSize * scale, FontStyle.Bold);
|
||||
|
||||
img.Mutate(x =>
|
||||
if (template.User.Name.Show)
|
||||
{
|
||||
var fontSize = (int)(template.User.Name.FontSize * 0.9);
|
||||
var username = stats.User.ToString();
|
||||
var usernameFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
|
||||
|
||||
var size = TextMeasurer.MeasureSize($"@{username}", new(usernameFont));
|
||||
var scale = 400f / size.Width;
|
||||
if (scale < 1)
|
||||
usernameFont = _fonts.NotoSans.CreateFont(template.User.Name.FontSize * scale, FontStyle.Bold);
|
||||
|
||||
x.DrawText(new RichTextOptions(usernameFont)
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
|
@ -666,126 +695,129 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|||
},
|
||||
"@" + username,
|
||||
Brushes.Solid(template.User.Name.Color));
|
||||
}
|
||||
|
||||
|
||||
//club name
|
||||
//club name
|
||||
|
||||
if (template.Club.Name.Show)
|
||||
if (template.Club.Name.Show)
|
||||
{
|
||||
var clubName = stats.User.Club?.ToString() ?? "-";
|
||||
|
||||
var clubFont = _fonts.NotoSans.CreateFont(template.Club.Name.FontSize, FontStyle.Regular);
|
||||
|
||||
x.DrawText(new RichTextOptions(clubFont)
|
||||
{
|
||||
var clubName = stats.User.Club?.ToString() ?? "-";
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
FallbackFontFamilies = _fonts.FallBackFonts,
|
||||
Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
|
||||
},
|
||||
clubName,
|
||||
Brushes.Solid(template.Club.Name.Color));
|
||||
}
|
||||
|
||||
var clubFont = _fonts.NotoSans.CreateFont(template.Club.Name.FontSize, FontStyle.Regular);
|
||||
Font GetTruncatedFont(
|
||||
FontFamily fontFamily,
|
||||
int fontSize,
|
||||
FontStyle style,
|
||||
string text,
|
||||
int maxSize)
|
||||
{
|
||||
var font = fontFamily.CreateFont(fontSize, style);
|
||||
var size = TextMeasurer.MeasureSize(text, new(font));
|
||||
var scale = maxSize / size.Width;
|
||||
if (scale < 1)
|
||||
font = fontFamily.CreateFont(fontSize * scale, style);
|
||||
|
||||
x.DrawText(new RichTextOptions(clubFont)
|
||||
return font;
|
||||
}
|
||||
|
||||
|
||||
if (template.User.Level.Show)
|
||||
{
|
||||
var guildLevelFont = GetTruncatedFont(
|
||||
_fonts.NotoSans,
|
||||
template.User.Level.FontSize,
|
||||
FontStyle.Bold,
|
||||
stats.Guild.Level.ToString(),
|
||||
33);
|
||||
|
||||
|
||||
x.DrawText(stats.Guild.Level.ToString(),
|
||||
guildLevelFont,
|
||||
template.User.Level.Color,
|
||||
new(template.User.Level.Pos.X, template.User.Level.Pos.Y));
|
||||
}
|
||||
|
||||
|
||||
var guild = stats.Guild;
|
||||
|
||||
//xp bar
|
||||
if (template.User.Xp.Bar.Show)
|
||||
{
|
||||
var xpPercent = guild.LevelXp / (float)guild.RequiredXp;
|
||||
DrawXpBar(xpPercent, template.User.Xp.Bar.Guild, img);
|
||||
}
|
||||
|
||||
if (template.User.Xp.Guild.Show)
|
||||
{
|
||||
x.DrawText(
|
||||
new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Guild.FontSize,
|
||||
FontStyle.Bold))
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
FallbackFontFamilies = _fonts.FallBackFonts,
|
||||
Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Origin = new(template.User.Xp.Guild.Pos.X, template.User.Xp.Guild.Pos.Y)
|
||||
},
|
||||
clubName,
|
||||
Brushes.Solid(template.Club.Name.Color));
|
||||
}
|
||||
$"{guild.LevelXp}/{guild.RequiredXp}",
|
||||
Brushes.Solid(template.User.Xp.Guild.Color));
|
||||
}
|
||||
|
||||
Font GetTruncatedFont(
|
||||
FontFamily fontFamily,
|
||||
int fontSize,
|
||||
FontStyle style,
|
||||
string text,
|
||||
int maxSize)
|
||||
{
|
||||
var font = fontFamily.CreateFont(fontSize, style);
|
||||
var size = TextMeasurer.MeasureSize(text, new(font));
|
||||
var scale = maxSize / size.Width;
|
||||
if (scale < 1)
|
||||
font = fontFamily.CreateFont(fontSize * scale, style);
|
||||
var rankPen = new SolidPen(Color.White, 1);
|
||||
//ranking
|
||||
|
||||
return font;
|
||||
}
|
||||
if (template.User.Rank.Show)
|
||||
{
|
||||
var guildRankStr = stats.GuildRanking.ToString();
|
||||
|
||||
var guildRankFont = GetTruncatedFont(
|
||||
_fonts.NotoSans,
|
||||
template.User.Rank.FontSize,
|
||||
FontStyle.Bold,
|
||||
guildRankStr,
|
||||
22);
|
||||
|
||||
if (template.User.Level.Show)
|
||||
{
|
||||
var guildLevelFont = GetTruncatedFont(
|
||||
_fonts.NotoSans,
|
||||
template.User.Level.FontSize,
|
||||
FontStyle.Bold,
|
||||
stats.Guild.Level.ToString(),
|
||||
33);
|
||||
|
||||
|
||||
x.DrawText(stats.Guild.Level.ToString(),
|
||||
guildLevelFont,
|
||||
template.User.Level.Color,
|
||||
new(template.User.Level.Pos.X, template.User.Level.Pos.Y));
|
||||
}
|
||||
|
||||
|
||||
var guild = stats.Guild;
|
||||
|
||||
//xp bar
|
||||
if (template.User.Xp.Bar.Show)
|
||||
{
|
||||
var xpPercent = guild.LevelXp / (float)guild.RequiredXp;
|
||||
DrawXpBar(xpPercent, template.User.Xp.Bar.Guild, img);
|
||||
}
|
||||
|
||||
if (template.User.Xp.Guild.Show)
|
||||
{
|
||||
x.DrawText(
|
||||
new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Guild.FontSize,
|
||||
FontStyle.Bold))
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Origin = new(template.User.Xp.Guild.Pos.X, template.User.Xp.Guild.Pos.Y)
|
||||
},
|
||||
$"{guild.LevelXp}/{guild.RequiredXp}",
|
||||
Brushes.Solid(template.User.Xp.Guild.Color));
|
||||
}
|
||||
|
||||
var rankPen = new SolidPen(Color.White, 1);
|
||||
//ranking
|
||||
|
||||
if (template.User.Rank.Show)
|
||||
{
|
||||
var guildRankStr = stats.GuildRanking.ToString();
|
||||
|
||||
var guildRankFont = GetTruncatedFont(
|
||||
_fonts.NotoSans,
|
||||
template.User.Rank.FontSize,
|
||||
FontStyle.Bold,
|
||||
guildRankStr,
|
||||
22);
|
||||
|
||||
x.DrawText(
|
||||
new RichTextOptions(guildRankFont)
|
||||
{
|
||||
Origin = new(template.User.Rank.Pos.X, template.User.Rank.Pos.Y)
|
||||
},
|
||||
guildRankStr,
|
||||
Brushes.Solid(template.User.Rank.Color),
|
||||
rankPen
|
||||
);
|
||||
}
|
||||
|
||||
if (template.User.Icon.Show)
|
||||
{
|
||||
try
|
||||
x.DrawText(
|
||||
new RichTextOptions(guildRankFont)
|
||||
{
|
||||
using var toDraw = Image.Load(avatarImageData);
|
||||
if (toDraw.Size != new Size(template.User.Icon.Size.X, template.User.Icon.Size.Y))
|
||||
toDraw.Mutate(x
|
||||
=> x.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y));
|
||||
Origin = new(template.User.Rank.Pos.X, template.User.Rank.Pos.Y)
|
||||
},
|
||||
guildRankStr,
|
||||
Brushes.Solid(template.User.Rank.Color),
|
||||
rankPen
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
x.DrawImage(toDraw,
|
||||
new Point(template.User.Icon.Pos.X, template.User.Icon.Pos.Y),
|
||||
1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error drawing avatar image");
|
||||
}
|
||||
if (template.User.Icon.Show)
|
||||
{
|
||||
var avImageData = await avatarFetchTask;
|
||||
img.Mutate(mut =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var toDraw = Image.Load(avImageData);
|
||||
if (toDraw.Size != new Size(template.User.Icon.Size.X, template.User.Icon.Size.Y))
|
||||
toDraw.Mutate(x => x.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y));
|
||||
|
||||
mut.DrawImage(toDraw,
|
||||
new Point(template.User.Icon.Pos.X, template.User.Icon.Pos.Y),
|
||||
1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error drawing avatar image");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1626,4 +1626,10 @@ scheduledelete:
|
|||
scheduleadd:
|
||||
- scheduleadd
|
||||
- scha
|
||||
- schadd
|
||||
- schadd
|
||||
xpexclusion:
|
||||
- xpexclusion
|
||||
- xpexl
|
||||
xpexclude:
|
||||
- xpexclude
|
||||
- xpex
|
|
@ -1854,7 +1854,7 @@ playlistload:
|
|||
- 5
|
||||
params:
|
||||
- id:
|
||||
desc: "The id of the playlist to be loaded."
|
||||
desc: "The id of the playlist to be loaded."
|
||||
playlistsave:
|
||||
desc: Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes.
|
||||
ex:
|
||||
|
@ -5100,4 +5100,25 @@ scheduleadd:
|
|||
- time:
|
||||
desc: "How long it takes for the command to execute. Example: 1h30m = 1 hour and 30 minutes"
|
||||
- command:
|
||||
desc: "Command that will be executed after the specified time has elapsed"
|
||||
desc: "Command that will be executed after the specified time has elapsed"
|
||||
xpexclusion:
|
||||
desc: |-
|
||||
Shows a list of all XP exclusions in the server.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- { }
|
||||
xpexclude:
|
||||
desc: |-
|
||||
Toggles XP gain exclusion for a specified item.
|
||||
Item types can be Role or User.
|
||||
ex:
|
||||
- '@CoolRole'
|
||||
- '@User'
|
||||
- 'role 123123123'
|
||||
- 'user 123123123'
|
||||
params:
|
||||
- type:
|
||||
desc: "Type of the item to exclude: role or user"
|
||||
itemId:
|
||||
desc: "ID or mention of the item to exclude."
|
|
@ -1217,5 +1217,9 @@
|
|||
"schedule_command": "Command",
|
||||
"schedule_when": "Executes At",
|
||||
"schedule_add_success": "Scheduled command successfully added.",
|
||||
"schedule_add_error": "You already have 5 scheduled commands. Please delete some before adding more."
|
||||
"schedule_add_error": "You already have 5 scheduled commands. Please delete some before adding more.",
|
||||
"xp_exclusion_none": "There are no exclusions set for this server.",
|
||||
"xp_exclusion_title": "XP Exclusions",
|
||||
"xp_exclude_added": "{0}: {1} has been excluded from the XP system.",
|
||||
"xp_exclude_removed": "{0}: {1} is no longer excluded from the XP system."
|
||||
}
|
Loading…
Add table
Reference in a new issue