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:
Toastie 2025-03-19 11:22:58 +13:00
parent ca46786c5e
commit 1d667db598
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
19 changed files with 631 additions and 169 deletions

View file

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

View file

@ -1,3 +1,7 @@
namespace EllieBot.Db.Models; namespace EllieBot.Db.Models;
public enum ExcludedItemType { Channel, Role } public enum XpExcludedItemType
{
User,
Role
}

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

View file

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

View file

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace EllieBot.Migrations.PostgreSql namespace EllieBot.Migrations.PostgreSql
{ {
[DbContext(typeof(PostgreSqlContext))] [DbContext(typeof(PostgreSqlContext))]
[Migration("20250317063309_init")] [Migration("20250318222207_init")]
partial class init partial class init
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -3263,6 +3263,39 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("xpcurrencyreward", (string)null); 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 => modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

View file

@ -1179,6 +1179,22 @@ namespace EllieBot.Migrations.PostgreSql
table.PrimaryKey("pk_warnings", x => x.id); 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( migrationBuilder.CreateTable(
name: "xpsettings", name: "xpsettings",
columns: table => new columns: table => new
@ -2280,6 +2296,11 @@ namespace EllieBot.Migrations.PostgreSql
table: "xpcurrencyreward", table: "xpcurrencyreward",
column: "xpsettingsid"); column: "xpsettingsid");
migrationBuilder.CreateIndex(
name: "ix_xpexcludeditem_guildid",
table: "xpexcludeditem",
column: "guildid");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_xprolereward_xpsettingsid_level", name: "ix_xprolereward_xpsettingsid_level",
table: "xprolereward", table: "xprolereward",
@ -2574,6 +2595,9 @@ namespace EllieBot.Migrations.PostgreSql
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "xpcurrencyreward"); name: "xpcurrencyreward");
migrationBuilder.DropTable(
name: "xpexcludeditem");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "xprolereward"); name: "xprolereward");

View file

@ -3260,6 +3260,39 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("xpcurrencyreward", (string)null); 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 => modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

View file

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

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace EllieBot.Migrations.Sqlite namespace EllieBot.Migrations.Sqlite
{ {
[DbContext(typeof(SqliteContext))] [DbContext(typeof(SqliteContext))]
[Migration("20250317063300_init")] [Migration("20250318222152_init")]
partial class init partial class init
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -2428,6 +2428,30 @@ namespace EllieBot.Migrations.Sqlite
b.ToTable("XpCurrencyReward"); 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 => modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

View file

@ -1181,6 +1181,22 @@ namespace EllieBot.Migrations.Sqlite
table.PrimaryKey("PK_Warnings", x => x.Id); 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( migrationBuilder.CreateTable(
name: "XpSettings", name: "XpSettings",
columns: table => new columns: table => new
@ -2282,6 +2298,11 @@ namespace EllieBot.Migrations.Sqlite
table: "XpCurrencyReward", table: "XpCurrencyReward",
column: "XpSettingsId"); column: "XpSettingsId");
migrationBuilder.CreateIndex(
name: "IX_XpExcludedItem_GuildId",
table: "XpExcludedItem",
column: "GuildId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_XpRoleReward_XpSettingsId_Level", name: "IX_XpRoleReward_XpSettingsId_Level",
table: "XpRoleReward", table: "XpRoleReward",
@ -2576,6 +2597,9 @@ namespace EllieBot.Migrations.Sqlite
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "XpCurrencyReward"); name: "XpCurrencyReward");
migrationBuilder.DropTable(
name: "XpExcludedItem");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "XpRoleReward"); name: "XpRoleReward");

View file

@ -2425,6 +2425,30 @@ namespace EllieBot.Migrations.Sqlite
b.ToTable("XpCurrencyReward"); 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 => modelBuilder.Entity("EllieBot.Db.Models.XpRoleReward", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

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

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

View file

@ -9,7 +9,7 @@ namespace EllieBot.Modules.Xp;
using GuildXpRates = (IReadOnlyList<GuildXpConfig> GuildRates, IReadOnlyList<ChannelXpConfig> ChannelRates); 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<(XpRateType RateType, ulong GuildId), XpRate> _guildRates = new();
private ConcurrentDictionary<ulong, ConcurrentDictionary<(XpRateType, ulong), XpRate>> _channelRates = new(); private ConcurrentDictionary<ulong, ConcurrentDictionary<(XpRateType, ulong), XpRate>> _channelRates = new();

View file

@ -3,7 +3,7 @@ namespace EllieBot.Modules.Xp;
public partial class Xp public partial class Xp
{ {
[RequireUserPermission(GuildPermission.ManageGuild)] [RequireUserPermission(GuildPermission.ManageGuild)]
public class XpRateCommands : EllieModule<GuildConfigXpService> public class XpRateCommands : EllieModule<XpRateService>
{ {
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]

View file

@ -40,7 +40,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
private readonly INotifySubscriber _notifySub; private readonly INotifySubscriber _notifySub;
private readonly IMemoryCache _memCache; private readonly IMemoryCache _memCache;
private readonly XpTemplateService _templateService; private readonly XpTemplateService _templateService;
private readonly GuildConfigXpService _xpRateService; private readonly XpRateService _xpRateRateService;
private readonly XpExclusionService _xpExcl;
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100); private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100);
@ -60,7 +61,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
IMemoryCache memCache, IMemoryCache memCache,
ShardData shardData, ShardData shardData,
XpTemplateService templateService, XpTemplateService templateService,
GuildConfigXpService xpRateService) XpRateService xpRateRateService,
XpExclusionService xpExcl
)
{ {
_db = db; _db = db;
_images = images; _images = images;
@ -72,7 +75,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
_notifySub = notifySub; _notifySub = notifySub;
_memCache = memCache; _memCache = memCache;
_templateService = templateService; _templateService = templateService;
_xpRateService = xpRateService; _xpRateRateService = xpRateRateService;
_xpExcl = xpExcl;
_client = client; _client = client;
_ps = ps; _ps = ps;
_c = c; _c = c;
@ -143,7 +147,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (!IsVoiceChannelActive(vc)) if (!IsVoiceChannelActive(vc))
continue; continue;
var rate = _xpRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id); var rate = _xpRateRateService.GetXpRate(XpRateType.Voice, g.Id, vc.Id);
if (rate.IsExcluded()) if (rate.IsExcluded())
continue; continue;
@ -153,6 +157,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (!UserParticipatingInVoiceChannel(u)) if (!UserParticipatingInVoiceChannel(u))
continue; continue;
if (IsUserExcluded(g, u))
continue;
if (oldBatch.Contains(u)) if (oldBatch.Contains(u))
{ {
validUsers.Add(new(u, rate.Amount, vc.Id)); validUsers.Add(new(u, rate.Amount, vc.Id));
@ -509,15 +516,18 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
if (IsUserExcluded(guild, user))
return;
var isImage = arg.Attachments.Any(a => a.Height >= 16 && a.Width >= 16); var isImage = arg.Attachments.Any(a => a.Height >= 16 && a.Width >= 16);
var isText = arg.Content.Contains(' ') || arg.Content.Length >= 5; 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; XpRate rate;
if (isImage) 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()) if (imageRate.IsExcluded())
return; return;
@ -544,6 +554,20 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
return Task.CompletedTask; 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) public async Task<int> AddXpToUsersAsync(ulong guildId, long amount, params ulong[] userIds)
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
@ -614,49 +638,54 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
} }
var avatarUrl = stats.User.RealAvatarUrl(); var avatarUrl = stats.User.RealAvatarUrl();
byte[] avatarImageData = null; var avatarFetchTask = Task.Run(async () =>
if (avatarUrl is not null)
{ {
var result = await _c.GetImageDataAsync(avatarUrl); try
if (!result.TryPickT0(out avatarImageData, out _))
{ {
using (var http = _httpFactory.CreateClient()) if (avatarUrl is null)
{ return null;
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();
}
}
}
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); using var img = Image.Load<Rgba32>(bgBytes);
if (template.User.Name.Show)
img.Mutate(x =>
{ {
var fontSize = (int)(template.User.Name.FontSize * 0.9); if (template.User.Name.Show)
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 =>
{ {
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) x.DrawText(new RichTextOptions(usernameFont)
{ {
HorizontalAlignment = HorizontalAlignment.Left, HorizontalAlignment = HorizontalAlignment.Left,
@ -666,126 +695,129 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
}, },
"@" + username, "@" + username,
Brushes.Solid(template.User.Name.Color)); 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, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top, VerticalAlignment = VerticalAlignment.Center,
FallbackFontFamilies = _fonts.FallBackFonts, Origin = new(template.User.Xp.Guild.Pos.X, template.User.Xp.Guild.Pos.Y)
Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
}, },
clubName, $"{guild.LevelXp}/{guild.RequiredXp}",
Brushes.Solid(template.Club.Name.Color)); Brushes.Solid(template.User.Xp.Guild.Color));
} }
Font GetTruncatedFont( var rankPen = new SolidPen(Color.White, 1);
FontFamily fontFamily, //ranking
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);
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) x.DrawText(
{ new RichTextOptions(guildRankFont)
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
{ {
using var toDraw = Image.Load(avatarImageData); Origin = new(template.User.Rank.Pos.X, template.User.Rank.Pos.Y)
if (toDraw.Size != new Size(template.User.Icon.Size.X, template.User.Icon.Size.Y)) },
toDraw.Mutate(x guildRankStr,
=> x.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y)); Brushes.Solid(template.User.Rank.Color),
rankPen
);
}
});
x.DrawImage(toDraw, if (template.User.Icon.Show)
new Point(template.User.Icon.Pos.X, template.User.Icon.Pos.Y), {
1); var avImageData = await avatarFetchTask;
} img.Mutate(mut =>
catch (Exception ex) {
{ try
Log.Warning(ex, "Error drawing avatar image"); {
} 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");
} }
}); });
} }

View file

@ -1626,4 +1626,10 @@ scheduledelete:
scheduleadd: scheduleadd:
- scheduleadd - scheduleadd
- scha - scha
- schadd - schadd
xpexclusion:
- xpexclusion
- xpexl
xpexclude:
- xpexclude
- xpex

View file

@ -1854,7 +1854,7 @@ playlistload:
- 5 - 5
params: params:
- id: - id:
desc: "The id of the playlist to be loaded." desc: "The id of the playlist to be loaded."
playlistsave: playlistsave:
desc: Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes. desc: Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes.
ex: ex:
@ -5100,4 +5100,25 @@ scheduleadd:
- time: - time:
desc: "How long it takes for the command to execute. Example: 1h30m = 1 hour and 30 minutes" desc: "How long it takes for the command to execute. Example: 1h30m = 1 hour and 30 minutes"
- command: - 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."

View file

@ -1217,5 +1217,9 @@
"schedule_command": "Command", "schedule_command": "Command",
"schedule_when": "Executes At", "schedule_when": "Executes At",
"schedule_add_success": "Scheduled command successfully added.", "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."
} }