elliebot/src/EllieBot/Modules/Xp/XpService.cs

1441 lines
48 KiB
C#
Raw Normal View History

2024-09-21 00:46:59 +12:00
#nullable disable warnings
using LinqToDB;
using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using Newtonsoft.Json;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
2025-02-06 12:34:22 +13:00
using LinqToDB.Data;
2024-09-21 00:46:59 +12:00
using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using Microsoft.Extensions.Caching.Memory;
using EllieBot.Modules.Administration;
2024-09-21 00:46:59 +12:00
using EllieBot.Modules.Patronage;
2025-02-06 12:34:22 +13:00
using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException;
2024-09-21 00:46:59 +12:00
using Color = SixLabors.ImageSharp.Color;
using Exception = System.Exception;
using Image = SixLabors.ImageSharp.Image;
namespace EllieBot.Modules.Xp.Services;
public class XpService : IEService, IReadyExecutor, IExecNoCommand
{
private readonly DbService _db;
private readonly IImageCache _images;
private readonly FontProvider _fonts;
private readonly IBotCreds _creds;
2024-09-21 00:46:59 +12:00
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();
2024-09-21 00:46:59 +12:00
private readonly ConcurrentHashSet<ulong> _excludedServers;
private readonly DiscordSocketClient _client;
private readonly IPatronageService _ps;
private readonly IBotCache _c;
private readonly INotifySubscriber _notifySub;
private readonly IMemoryCache _memCache;
private readonly ShardData _shardData;
2025-02-08 16:35:53 +13:00
private readonly XpTemplateService _templateService;
2024-09-21 00:46:59 +12:00
2025-02-06 12:51:47 +13:00
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100);
2024-09-21 00:46:59 +12:00
public XpService(
DiscordSocketClient client,
DbService db,
IImageCache images,
IBotCache c,
FontProvider fonts,
IBotCreds creds,
2024-09-21 00:46:59 +12:00
ICurrencyService cs,
IHttpClientFactory http,
XpConfigService xpConfig,
IPubSub pubSub,
IPatronageService ps,
INotifySubscriber notifySub,
IMemoryCache memCache,
2025-02-08 16:35:53 +13:00
ShardData shardData,
XpTemplateService templateService)
2024-09-21 00:46:59 +12:00
{
_db = db;
_images = images;
_fonts = fonts;
_creds = creds;
_cs = cs;
_httpFactory = http;
_xpConfig = xpConfig;
_pubSub = pubSub;
_notifySub = notifySub;
_memCache = memCache;
_shardData = shardData;
2025-02-08 16:35:53 +13:00
_templateService = templateService;
_excludedServers = [];
2024-09-21 00:46:59 +12:00
_excludedChannels = new();
_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>()
2025-02-06 12:34:22 +13:00
.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());
2024-09-21 00:46:59 +12:00
2025-02-06 12:34:22 +13:00
return;
2024-09-21 00:46:59 +12:00
2025-02-06 12:54:30 +13:00
async Task VoiceUpdateTimer()
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync())
{
try
{
await UpdateVoiceXp();
}
catch (Exception ex)
{
Log.Error(ex, "Error updating voice xp");
}
}
}
2025-02-06 12:34:22 +13:00
async Task UpdateTimer()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
while (await timer.WaitForNextTickAsync())
2024-09-21 00:46:59 +12:00
{
2025-02-06 12:51:47 +13:00
try
{
await UpdateXp();
}
catch (Exception ex)
{
Log.Error(ex, "Error updating xp");
await Task.Delay(30_000);
}
2024-09-21 00:46:59 +12:00
}
2025-02-06 12:34:22 +13:00
}
}
2024-09-21 00:46:59 +12:00
2025-02-06 12:34:22 +13:00
/// <summary>
/// The current batch of users that will gain xp
/// </summary>
private readonly ConcurrentHashSet<IGuildUser> _usersBatch = [];
2024-09-21 00:46:59 +12:00
/// <summary>
/// The current batch of users that will gain voice xp
/// </summary>
private readonly ConcurrentHashSet<IGuildUser> _voiceXpBatch = [];
2025-02-06 12:54:30 +13:00
private async Task UpdateVoiceXp()
{
var xpAmount = _xpConfig.Data.VoiceXpPerMinute;
if (xpAmount <= 0)
return;
var oldBatch = _voiceXpBatch.ToArray();
_voiceXpBatch.Clear();
2025-02-06 12:54:30 +13:00
var validUsers = new HashSet<IGuildUser>();
var guilds = _client.Guilds;
foreach (var g in guilds)
{
if (IsServerExcluded(g.Id))
continue;
2025-02-06 12:54:30 +13:00
foreach (var vc in g.VoiceChannels)
{
if (!IsVoiceChannelActive(vc))
continue;
if (IsChannelExcluded(vc))
continue;
2025-02-06 12:54:30 +13:00
foreach (var u in vc.ConnectedUsers)
{
if (IsServerOrRoleExcluded(u) || !UserParticipatingInVoiceChannel(u))
continue;
2025-02-06 12:54:30 +13:00
if (oldBatch.Contains(u))
validUsers.Add(u);
_voiceXpBatch.Add(u);
}
}
}
2025-02-06 12:54:30 +13:00
await UpdateXpInternalAsync(validUsers.DistinctBy(x => x.Id).ToArray(), xpAmount);
}
2025-02-06 12:34:22 +13:00
private async Task UpdateXp()
{
var xpAmount = _xpConfig.Data.TextXpPerMessage;
2025-02-06 12:34:22 +13:00
var currentBatch = _usersBatch.ToArray();
_usersBatch.Clear();
2024-09-21 00:46:59 +12:00
2025-02-06 12:54:30 +13:00
await UpdateXpInternalAsync(currentBatch, xpAmount);
}
private async Task UpdateXpInternalAsync(IGuildUser[] currentBatch, int xpAmount)
{
2025-02-06 12:51:47 +13:00
if (currentBatch.Length == 0)
return;
2025-02-06 12:34:22 +13:00
await using var ctx = _db.GetDbContext();
await using var lctx = ctx.CreateLinqToDBConnection();
2024-09-21 00:46:59 +12:00
var tempTableName = "xptemp_" + Guid.NewGuid().ToString().Replace("-", string.Empty);
2025-02-06 12:51:47 +13:00
await using var batchTable = await lctx.CreateTempTableAsync<UserXpBatch>(tempTableName);
2025-02-06 12:34:22 +13:00
await batchTable.BulkCopyAsync(currentBatch.Select(x => new UserXpBatch()
2024-09-21 00:46:59 +12:00
{
2025-02-06 12:34:22 +13:00
GuildId = x.GuildId,
UserId = x.Id,
2025-02-06 12:51:47 +13:00
Username = x.Username,
AvatarId = x.DisplayAvatarId
2025-02-06 12:34:22 +13:00
}));
await lctx.ExecuteAsync(
$"""
2025-02-06 12:51:47 +13:00
INSERT INTO UserXpStats (GuildId, UserId, Xp)
SELECT "{tempTableName}"."GuildId", "{tempTableName}"."UserId", {xpAmount}
FROM {tempTableName}
WHERE TRUE
ON CONFLICT (GuildId, UserId) DO UPDATE
SET
Xp = UserXpStats.Xp + EXCLUDED.Xp;
""");
2025-02-06 12:34:22 +13:00
var updated = await batchTable
.InnerJoin(lctx.GetTable<UserXpStats>(),
(u, s) => u.GuildId == s.GuildId && u.UserId == s.UserId,
(batch, stats) => stats)
.ToListAsyncLinqToDB();
foreach (var u in updated)
2025-02-06 12:51:47 +13:00
{
var oldStats = new LevelStats(u.Xp - xpAmount);
var newStats = new LevelStats(u.Xp);
2025-02-06 12:51:47 +13:00
Log.Information("User {User} xp updated from {OldLevel} to {NewLevel}",
u.UserId,
oldStats.TotalXp,
newStats.TotalXp);
2025-02-06 12:51:47 +13:00
if (oldStats.Level < newStats.Level)
2025-02-06 12:51:47 +13:00
{
await _levelUpQueue.EnqueueAsync(NotifyUser(u.GuildId,
0,
u.UserId,
true,
oldStats.Level,
newStats.Level));
2025-02-06 12:51:47 +13:00
}
}
2024-09-21 00:46:59 +12:00
}
private Func<Task> NotifyUser(
ulong guildId,
ulong channelId,
ulong userId,
bool isServer,
long oldLevel,
long newLevel)
2024-09-21 00:46:59 +12:00
=> async () =>
{
if (isServer)
{
await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel);
}
await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel);
2024-09-21 00:46:59 +12:00
};
private async Task HandleRewardsInternalAsync(
ulong guildId,
ulong userId,
long oldLevel,
long newLevel)
{
2025-02-06 12:51:47 +13:00
var settings = await GetFullXpSettingsFor(guildId);
var rrews = settings.RoleRewards;
var crews = settings.CurrencyRewards;
2024-09-21 00:46:59 +12:00
//loop through levels since last level up, so if a high amount of xp is gained, reward are still applied.
for (var i = oldLevel + 1; i <= newLevel; i++)
{
var rrew = rrews.FirstOrDefault(x => x.Level == i);
if (rrew is not null)
{
var guild = _client.GetGuild(guildId);
var role = guild?.GetRole(rrew.RoleId);
var user = guild?.GetUser(userId);
if (role is not null && user is not null)
{
if (rrew.Remove)
{
try
{
await user.RemoveRoleAsync(role);
await _notifySub.NotifyAsync(new RemoveRoleRewardNotifyModel(guild.Id,
role.Id,
user.Id,
newLevel),
isShardLocal: true);
}
catch (Exception ex)
{
Log.Warning(ex,
"Unable to remove role {RoleId} from user {UserId}: {Message}",
role.Id,
user.Id,
ex.Message);
}
}
2024-09-21 00:46:59 +12:00
else
{
try
{
await user.AddRoleAsync(role);
await _notifySub.NotifyAsync(new AddRoleRewardNotifyModel(guild.Id,
role.Id,
user.Id,
newLevel),
isShardLocal: true);
}
catch (Exception ex)
{
Log.Warning(ex,
"Unable to add role {RoleId} to user {UserId}: {Message}",
role.Id,
user.Id,
ex.Message);
}
}
2024-09-21 00:46:59 +12:00
}
}
//get currency reward for this level
var crew = crews.FirstOrDefault(x => x.Level == i);
if (crew is not null)
{
//give the user the reward if it exists
await _cs.AddAsync(userId, crew.Amount, new("xp", "level-up"));
}
}
}
private async Task HandleNotifyInternalAsync(
ulong guildId,
ulong channelId,
ulong userId,
bool isServer,
long newLevel)
2024-09-21 00:46:59 +12:00
{
var guild = _client.GetGuild(guildId);
var user = guild?.GetUser(userId);
if (guild is null || user is null)
return;
if (isServer)
{
var model = new LevelUpNotifyModel()
2024-09-21 00:46:59 +12:00
{
GuildId = guildId,
UserId = userId,
ChannelId = channelId,
Level = newLevel
2024-09-21 00:46:59 +12:00
};
await _notifySub.NotifyAsync(model, true);
return;
2024-09-21 00:46:59 +12:00
}
}
public async Task SetCurrencyReward(ulong guildId, int level, int amount)
2024-09-21 00:46:59 +12:00
{
await using var uow = _db.GetDbContext();
2025-02-06 12:51:47 +13:00
var settings = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.CurrencyRewards));
2024-09-21 00:46:59 +12:00
if (amount <= 0)
{
var toRemove = settings.CurrencyRewards.FirstOrDefault(x => x.Level == level);
if (toRemove is not null)
{
uow.Remove(toRemove);
settings.CurrencyRewards.Remove(toRemove);
}
}
else
{
var rew = settings.CurrencyRewards.FirstOrDefault(x => x.Level == level);
if (rew is not null)
rew.Amount = amount;
else
{
settings.CurrencyRewards.Add(new()
{
Level = level,
Amount = amount
});
}
}
await uow.SaveChangesAsync();
2024-09-21 00:46:59 +12:00
}
2025-02-06 12:51:47 +13:00
public async Task<XpSettings> GetFullXpSettingsFor(ulong guildId)
2024-09-21 00:46:59 +12:00
{
await using var uow = _db.GetDbContext();
2025-02-06 12:51:47 +13:00
return await uow.XpSettingsFor(guildId,
set => set
.LoadWith(x => x.CurrencyRewards)
.LoadWith(x => x.RoleRewards));
2024-09-21 00:46:59 +12:00
}
public async Task ResetRoleRewardAsync(ulong guildId, int level)
2024-09-21 00:46:59 +12:00
{
await using var uow = _db.GetDbContext();
2025-02-06 12:51:47 +13:00
var settings = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.RoleRewards));
2024-09-21 00:46:59 +12:00
var toRemove = settings.RoleRewards.FirstOrDefault(x => x.Level == level);
if (toRemove is not null)
{
uow.Remove(toRemove);
settings.RoleRewards.Remove(toRemove);
}
2025-02-06 12:51:47 +13:00
await uow.SaveChangesAsync();
2024-09-21 00:46:59 +12:00
}
public async Task SetRoleRewardAsync(
2024-09-21 00:46:59 +12:00
ulong guildId,
int level,
ulong roleId,
bool remove)
{
await using var uow = _db.GetDbContext();
2025-02-06 12:51:47 +13:00
var settings = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.RoleRewards));
2024-09-21 00:46:59 +12:00
var rew = settings.RoleRewards.FirstOrDefault(x => x.Level == level);
if (rew is not null)
{
rew.RoleId = roleId;
rew.Remove = remove;
}
else
{
settings.RoleRewards.Add(new()
{
Level = level,
RoleId = roleId,
Remove = remove,
});
}
2025-02-06 12:54:30 +13:00
await uow.SaveChangesAsync();
2024-09-21 00:46:59 +12:00
}
public async Task<IReadOnlyCollection<UserXpStats>> GetGuildUserXps(ulong guildId, int page)
2024-09-21 00:46:59 +12:00
{
await using var uow = _db.GetDbContext();
return await uow
2025-02-06 12:34:22 +13:00
.UserXpStats
.Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
2024-09-21 00:46:59 +12:00
}
public async Task<IReadOnlyCollection<UserXpStats>> GetGuildUserXps(ulong guildId, List<ulong> users, int page)
2024-09-21 00:46:59 +12:00
{
await using var uow = _db.GetDbContext();
return await uow.Set<UserXpStats>()
2025-02-06 12:34:22 +13:00
.Where(x => x.GuildId == guildId && x.UserId.In(users))
.OrderByDescending(x => x.Xp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
2024-09-21 00:46:59 +12:00
}
public async Task<IReadOnlyCollection<DiscordUser>> GetGlobalUserXps(int page)
2024-09-21 00:46:59 +12:00
{
await using var uow = _db.GetDbContext();
return await uow.GetTable<DiscordUser>()
2025-02-06 12:34:22 +13:00
.OrderByDescending(x => x.TotalXp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
}
public async Task<IReadOnlyCollection<DiscordUser>> GetGlobalUserXps(int page, List<ulong> users)
{
await using var uow = _db.GetDbContext();
return await uow.GetTable<DiscordUser>()
2025-02-06 12:34:22 +13:00
.Where(x => x.UserId.In(users))
.OrderByDescending(x => x.TotalXp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
2024-09-21 00:46:59 +12:00
}
private bool IsVoiceChannelActive(SocketVoiceChannel channel)
2024-09-21 00:46:59 +12:00
{
var count = 0;
foreach (var user in channel.ConnectedUsers)
2024-09-21 00:46:59 +12:00
{
if (UserParticipatingInVoiceChannel(user))
2025-02-06 12:54:30 +13:00
{
count++;
if (count >= 2)
return true;
2025-02-06 12:54:30 +13:00
}
2024-09-21 00:46:59 +12:00
}
return false;
}
2024-09-21 00:46:59 +12:00
private static bool UserParticipatingInVoiceChannel(SocketGuildUser user)
2024-09-21 00:46:59 +12:00
=> !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted;
private bool IsServerOrRoleExcluded(SocketGuildUser user)
2024-09-21 00:46:59 +12:00
{
if (_excludedServers.Contains(user.Guild.Id))
return true;
2024-09-21 00:46:59 +12:00
if (_excludedRoles.TryGetValue(user.Guild.Id, out var roles) && user.Roles.Any(x => roles.Contains(x.Id)))
return true;
return false;
2024-09-21 00:46:59 +12:00
}
private bool IsChannelExcluded(IGuildChannel channel)
2024-09-21 00:46:59 +12:00
{
if (_excludedChannels.TryGetValue(channel.Guild.Id, out var chans)
&& (chans.Contains(channel.Id)
2024-09-21 00:46:59 +12:00
|| (channel is SocketThreadChannel tc && chans.Contains(tc.ParentChannel.Id))))
return true;
2024-09-21 00:46:59 +12:00
return false;
2024-09-21 00:46:59 +12:00
}
public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage arg)
{
if (arg.Author is not SocketGuildUser user || user.IsBot)
return Task.CompletedTask;
if (arg.Channel is not IGuildChannel gc)
return Task.CompletedTask;
2024-09-21 00:46:59 +12:00
_ = Task.Run(async () =>
{
if (IsChannelExcluded(gc))
return;
if (IsServerOrRoleExcluded(user))
2024-09-21 00:46:59 +12:00
return;
var xpConf = _xpConfig.Data;
var xp = 0;
if (arg.Attachments.Any(a => a.Height >= 128 && a.Width >= 128))
xp = xpConf.TextXpFromImage;
2024-09-21 00:46:59 +12:00
if (arg.Content.Contains(' ') || arg.Content.Length >= 5)
xp = Math.Max(xp, xpConf.TextXpPerMessage);
2024-09-21 00:46:59 +12:00
if (xp <= 0)
return;
if (!await TryAddUserGainedXpAsync(user.Id, xpConf.TextXpCooldown))
2024-09-21 00:46:59 +12:00
return;
2025-02-06 12:34:22 +13:00
_usersBatch.Add(user);
2024-09-21 00:46:59 +12:00
});
2024-09-21 00:46:59 +12:00
return Task.CompletedTask;
}
public async Task<int> AddXpToUsersAsync(ulong guildId, long amount, params ulong[] userIds)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<UserXpStats>()
2025-02-06 12:34:22 +13:00
.Where(x => x.GuildId == guildId && userIds.Contains(x.UserId))
.UpdateAsync(old => new()
{
Xp = old.Xp + amount
});
2024-09-21 00:46:59 +12:00
}
public async Task AddXpAsync(ulong userId, ulong guildId, int amount)
2024-09-21 00:46:59 +12:00
{
await using var uow = _db.GetDbContext();
2024-09-21 00:46:59 +12:00
var usr = uow.GetOrCreateUserXpStats(guildId, userId);
usr.Xp += amount;
2024-09-21 00:46:59 +12:00
await uow.SaveChangesAsync();
2024-09-21 00:46:59 +12:00
}
public bool IsServerExcluded(ulong id)
=> _excludedServers.Contains(id);
public IEnumerable<ulong> GetExcludedRoles(ulong id)
{
if (_excludedRoles.TryGetValue(id, out var val))
return val.ToArray();
return [];
2024-09-21 00:46:59 +12:00
}
public IEnumerable<ulong> GetExcludedChannels(ulong id)
{
if (_excludedChannels.TryGetValue(id, out var val))
return val.ToArray();
return [];
2024-09-21 00:46:59 +12:00
}
2025-02-24 13:22:34 +13:00
private Task<bool> TryAddUserGainedXpAsync(ulong userId, int cdInSeconds)
{
if (cdInSeconds <= 0)
2025-02-24 13:22:34 +13:00
return Task.FromResult(true);
if (_memCache.TryGetValue(userId, out _))
2025-02-24 13:22:34 +13:00
return Task.FromResult(false);
using var entry = _memCache.CreateEntry(userId);
entry.Value = true;
2024-09-21 00:46:59 +12:00
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(cdInSeconds);
2025-02-24 13:22:34 +13:00
return Task.FromResult(true);
}
2024-09-21 00:46:59 +12:00
public async Task<FullUserStats> GetUserStatsAsync(IGuildUser user)
{
await using var uow = _db.GetDbContext();
var du = uow.GetOrCreateUser(user, set => set.Include(x => x.Club));
var totalXp = du.TotalXp;
var globalRank = uow.Set<DiscordUser>().GetUserGlobalRank(user.Id);
var guildRank = await uow.Set<UserXpStats>().GetUserGuildRanking(user.Id, user.GuildId);
var stats = uow.GetOrCreateUserXpStats(user.GuildId, user.Id);
await uow.SaveChangesAsync();
return new(du,
stats,
new(totalXp),
new(stats.Xp),
2024-09-21 00:46:59 +12:00
globalRank,
guildRank);
}
public async Task<bool> ToggleExcludeServerAsync(ulong id)
2024-09-21 00:46:59 +12:00
{
await using var uow = _db.GetDbContext();
var xpSetting = await uow.XpSettingsFor(id);
2024-09-21 00:46:59 +12:00
if (_excludedServers.Add(id))
{
xpSetting.ServerExcluded = true;
await uow.SaveChangesAsync();
2024-09-21 00:46:59 +12:00
return true;
}
_excludedServers.TryRemove(id);
xpSetting.ServerExcluded = false;
await uow.SaveChangesAsync();
2024-09-21 00:46:59 +12:00
return false;
}
public async Task<bool> ToggleExcludeRoleAsync(ulong guildId, ulong rId)
2024-09-21 00:46:59 +12:00
{
var roles = _excludedRoles.GetOrAdd(guildId, _ => []);
await using var uow = _db.GetDbContext();
2025-02-06 12:51:47 +13:00
var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList));
2024-09-21 00:46:59 +12:00
var excludeObj = new ExcludedItem
{
ItemId = rId,
ItemType = ExcludedItemType.Role
};
if (roles.Add(rId))
{
if (xpSetting.ExclusionList.Add(excludeObj))
await uow.SaveChangesAsync();
2024-09-21 00:46:59 +12:00
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();
2024-09-21 00:46:59 +12:00
}
return false;
}
public async Task<bool> ToggleExcludeChannelAsync(ulong guildId, ulong chId)
2024-09-21 00:46:59 +12:00
{
var channels = _excludedChannels.GetOrAdd(guildId, _ => []);
await using var uow = _db.GetDbContext();
2025-02-06 12:51:47 +13:00
var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList));
2024-09-21 00:46:59 +12:00
var excludeObj = new ExcludedItem
{
ItemId = chId,
ItemType = ExcludedItemType.Channel
};
if (channels.Add(chId))
{
if (xpSetting.ExclusionList.Add(excludeObj))
await uow.SaveChangesAsync();
2024-09-21 00:46:59 +12:00
return true;
}
channels.TryRemove(chId);
if (xpSetting.ExclusionList.Remove(excludeObj))
await uow.SaveChangesAsync();
2024-09-21 00:46:59 +12:00
return false;
}
public async Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(IGuildUser user)
{
var stats = await GetUserStatsAsync(user);
return await GenerateXpImageAsync(stats);
}
public Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(FullUserStats stats)
=> Task.Run(async () =>
{
2025-02-08 16:35:53 +13:00
var template = _templateService.GetTemplate();
2024-09-21 00:46:59 +12:00
var bgBytes = await GetXpBackgroundAsync(stats.User.UserId);
if (bgBytes is null)
{
Log.Warning("Xp background image could not be loaded");
throw new ArgumentNullException(nameof(bgBytes));
}
var avatarUrl = stats.User.RealAvatarUrl();
byte[] avatarImageData = null;
if (avatarUrl is not null)
{
var result = await _c.GetImageDataAsync(avatarUrl);
if (!result.TryPickT0(out avatarImageData, out _))
{
using (var http = _httpFactory.CreateClient())
{
var avatarData = await http.GetByteArrayAsync(avatarUrl);
using (var tempDraw = Image.Load<Rgba32>(avatarData))
{
tempDraw.Mutate(x => x
2025-02-08 16:35:53 +13:00
.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);
}
}
2024-09-21 00:46:59 +12:00
var outlinePen = new SolidPen(Color.Black, 1f);
using var img = Image.Load<Rgba32>(bgBytes);
2025-02-08 16:35:53 +13:00
if (template.User.Name.Show)
2024-09-21 00:46:59 +12:00
{
2025-02-08 16:35:53 +13:00
var fontSize = (int)(template.User.Name.FontSize * 0.9);
2024-09-21 00:46:59 +12:00
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)
2025-02-08 16:35:53 +13:00
usernameFont = _fonts.NotoSans.CreateFont(template.User.Name.FontSize * scale, FontStyle.Bold);
2024-09-21 00:46:59 +12:00
img.Mutate(x =>
{
x.DrawText(new RichTextOptions(usernameFont)
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
FallbackFontFamilies = _fonts.FallBackFonts,
2025-02-08 16:35:53 +13:00
Origin = new(template.User.Name.Pos.X, template.User.Name.Pos.Y + 8)
},
2024-09-21 00:46:59 +12:00
"@" + username,
2025-02-08 16:35:53 +13:00
Brushes.Solid(template.User.Name.Color),
2024-09-21 00:46:59 +12:00
outlinePen);
//club name
2024-09-21 00:46:59 +12:00
2025-02-08 16:35:53 +13:00
if (template.Club.Name.Show)
{
var clubName = stats.User.Club?.ToString() ?? "-";
2024-09-21 00:46:59 +12:00
2025-02-08 16:35:53 +13:00
var clubFont = _fonts.NotoSans.CreateFont(template.Club.Name.FontSize, FontStyle.Regular);
2024-09-21 00:46:59 +12:00
x.DrawText(new RichTextOptions(clubFont)
{
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
FallbackFontFamilies = _fonts.FallBackFonts,
2025-02-08 16:35:53 +13:00
Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
},
clubName,
2025-02-08 16:35:53 +13:00
Brushes.Solid(template.Club.Name.Color),
outlinePen);
}
2024-09-21 00:46:59 +12:00
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);
2024-09-21 00:46:59 +12:00
return font;
}
2024-09-21 00:46:59 +12:00
2025-02-08 16:35:53 +13:00
if (template.User.GlobalLevel.Show)
{
// up to 83 width
2024-09-21 00:46:59 +12:00
var globalLevelFont = GetTruncatedFont(
_fonts.NotoSans,
2025-02-08 16:35:53 +13:00
template.User.GlobalLevel.FontSize,
FontStyle.Bold,
stats.Global.Level.ToString(),
75);
2024-09-21 00:46:59 +12:00
x.DrawText(stats.Global.Level.ToString(),
globalLevelFont,
2025-02-08 16:35:53 +13:00
template.User.GlobalLevel.Color,
new(template.User.GlobalLevel.Pos.X, template.User.GlobalLevel.Pos.Y)); //level
}
2024-09-21 00:46:59 +12:00
2025-02-08 16:35:53 +13:00
if (template.User.GuildLevel.Show)
{
var guildLevelFont = GetTruncatedFont(
_fonts.NotoSans,
2025-02-08 16:35:53 +13:00
template.User.GuildLevel.FontSize,
FontStyle.Bold,
stats.Guild.Level.ToString(),
75);
x.DrawText(stats.Guild.Level.ToString(),
guildLevelFont,
2025-02-08 16:35:53 +13:00
template.User.GuildLevel.Color,
new(template.User.GuildLevel.Pos.X, template.User.GuildLevel.Pos.Y));
}
2024-09-21 00:46:59 +12:00
var global = stats.Global;
var guild = stats.Guild;
2024-09-21 00:46:59 +12:00
//xp bar
2025-02-08 16:35:53 +13:00
if (template.User.Xp.Bar.Show)
2024-09-21 00:46:59 +12:00
{
var xpPercent = global.LevelXp / (float)global.RequiredXp;
2025-02-08 16:35:53 +13:00
DrawXpBar(xpPercent, template.User.Xp.Bar.Global, img);
xpPercent = guild.LevelXp / (float)guild.RequiredXp;
2025-02-08 16:35:53 +13:00
DrawXpBar(xpPercent, template.User.Xp.Bar.Guild, img);
}
2024-09-21 00:46:59 +12:00
2025-02-08 16:35:53 +13:00
if (template.User.Xp.Global.Show)
2024-09-21 00:46:59 +12:00
{
x.DrawText(
2025-02-08 16:35:53 +13:00
new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Global.FontSize,
FontStyle.Bold))
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
2025-02-08 16:35:53 +13:00
Origin = new(template.User.Xp.Global.Pos.X, template.User.Xp.Global.Pos.Y),
},
$"{global.LevelXp}/{global.RequiredXp}",
2025-02-08 16:35:53 +13:00
Brushes.Solid(template.User.Xp.Global.Color),
outlinePen);
}
2024-09-21 00:46:59 +12:00
2025-02-08 16:35:53 +13:00
if (template.User.Xp.Guild.Show)
{
x.DrawText(
2025-02-08 16:35:53 +13:00
new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Guild.FontSize,
FontStyle.Bold))
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
2025-02-08 16:35:53 +13:00
Origin = new(template.User.Xp.Guild.Pos.X, template.User.Xp.Guild.Pos.Y)
},
$"{guild.LevelXp}/{guild.RequiredXp}",
2025-02-08 16:35:53 +13:00
Brushes.Solid(template.User.Xp.Guild.Color),
outlinePen);
}
2024-09-21 00:46:59 +12:00
var rankPen = new SolidPen(Color.White, 1);
//ranking
2025-02-08 16:35:53 +13:00
if (template.User.GlobalRank.Show)
2024-09-21 00:46:59 +12:00
{
var globalRankStr = stats.GlobalRanking.ToString();
2024-09-21 00:46:59 +12:00
var globalRankFont = GetTruncatedFont(
_fonts.UniSans,
2025-02-08 16:35:53 +13:00
template.User.GlobalRank.FontSize,
FontStyle.Bold,
globalRankStr,
68);
2024-09-21 00:46:59 +12:00
x.DrawText(
new RichTextOptions(globalRankFont)
{
2025-02-08 16:35:53 +13:00
Origin = new(template.User.GlobalRank.Pos.X, template.User.GlobalRank.Pos.Y)
},
globalRankStr,
2025-02-08 16:35:53 +13:00
Brushes.Solid(template.User.GlobalRank.Color),
rankPen
);
}
2024-09-21 00:46:59 +12:00
2025-02-08 16:35:53 +13:00
if (template.User.GuildRank.Show)
2024-09-21 00:46:59 +12:00
{
var guildRankStr = stats.GuildRanking.ToString();
2024-09-21 00:46:59 +12:00
var guildRankFont = GetTruncatedFont(
_fonts.UniSans,
2025-02-08 16:35:53 +13:00
template.User.GuildRank.FontSize,
FontStyle.Bold,
guildRankStr,
43);
2024-09-21 00:46:59 +12:00
x.DrawText(
new RichTextOptions(guildRankFont)
2024-09-21 00:46:59 +12:00
{
2025-02-08 16:35:53 +13:00
Origin = new(template.User.GuildRank.Pos.X, template.User.GuildRank.Pos.Y)
},
guildRankStr,
2025-02-08 16:35:53 +13:00
Brushes.Solid(template.User.GuildRank.Color),
rankPen
);
}
2024-09-21 00:46:59 +12:00
2025-02-08 16:35:53 +13:00
if (template.User.Icon.Show)
{
try
{
using var toDraw = Image.Load(avatarImageData);
2025-02-08 16:35:53 +13:00
if (toDraw.Size != new Size(template.User.Icon.Size.X, template.User.Icon.Size.Y))
toDraw.Mutate(x
2025-02-08 16:35:53 +13:00
=> x.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y));
x.DrawImage(toDraw,
2025-02-08 16:35:53 +13:00
new Point(template.User.Icon.Pos.X, template.User.Icon.Pos.Y),
1);
}
catch (Exception ex)
{
Log.Warning(ex, "Error drawing avatar image");
2024-09-21 00:46:59 +12:00
}
}
});
2024-09-21 00:46:59 +12:00
}
//club image
2025-02-08 16:35:53 +13:00
if (template.Club.Icon.Show)
2025-02-08 16:43:01 +13:00
await DrawClubImage(template, img, stats);
2024-09-21 00:46:59 +12:00
await DrawFrame(img, stats.User.UserId);
2025-02-08 16:35:53 +13:00
var outputSize = template.OutputSize;
2024-09-21 00:46:59 +12:00
if (outputSize.X != img.Width || outputSize.Y != img.Height)
2025-02-08 16:35:53 +13:00
img.Mutate(x => x.Resize(template.OutputSize.X, template.OutputSize.Y));
2024-09-21 00:46:59 +12:00
var imageFormat = img.Metadata.DecodedImageFormat;
var output = ((Stream)await img.ToStreamAsync(imageFormat), imageFormat);
return output;
});
private async Task<byte[]?> GetXpBackgroundAsync(ulong userId)
{
var item = await GetItemInUse(userId, XpShopItemType.Background);
if (item is null)
{
return await _images.GetXpBackgroundImageAsync();
}
var url = _xpConfig.Data.Shop.GetItemUrl(XpShopItemType.Background, item.ItemKey);
if (!string.IsNullOrWhiteSpace(url))
{
var data = await _images.GetImageDataAsync(new Uri(url));
return data;
}
return await _images.GetXpBackgroundImageAsync();
}
private async Task DrawFrame(Image<Rgba32> img, ulong userId)
{
var patron = await _ps.GetPatronAsync(userId);
var item = await GetItemInUse(userId, XpShopItemType.Frame);
Image? frame = null;
if (item is null)
{
if (patron?.Tier == PatronTier.V)
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png"));
else if (patron?.Tier >= PatronTier.X || _creds.IsOwner(userId))
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_gold.png"));
}
else
{
var url = _xpConfig.Data.Shop.GetItemUrl(XpShopItemType.Frame, item.ItemKey);
if (!string.IsNullOrWhiteSpace(url))
{
var data = await _images.GetImageDataAsync(new Uri(url));
frame = Image.Load<Rgba32>(data);
}
}
if (frame is not null)
img.Mutate(x => x.DrawImage(frame, new Point(0, 0), new GraphicsOptions()));
}
private void DrawXpBar(float percent, XpBar info, Image<Rgba32> img)
{
var x1 = info.PointA.X;
var y1 = info.PointA.Y;
var x2 = info.PointB.X;
var y2 = info.PointB.Y;
var length = info.Length * percent;
float x3, x4, y3, y4;
var matrix = info.Direction switch
2024-09-21 00:46:59 +12:00
{
XpTemplateDirection.Down => new float[,] { { 0, 1 }, { 0, 1 } },
XpTemplateDirection.Up => new float[,] { { 0, -1 }, { 0, -1 } },
XpTemplateDirection.Left => new float[,] { { -1, 0 }, { -1, 0 } },
_ => new float[,] { { 1, 0 }, { 1, 0 } },
};
x3 = x1 + matrix[0, 0] * length;
x4 = x2 + matrix[1, 0] * length;
y3 = y1 + matrix[0, 1] * length;
y4 = y2 + matrix[1, 1] * length;
2024-09-21 00:46:59 +12:00
img.Mutate(x => x.FillPolygon(info.Color,
new PointF(x1, y1),
new PointF(x3, y3),
new PointF(x4, y4),
new PointF(x2, y2)));
}
2025-02-08 16:43:01 +13:00
private async Task DrawClubImage(XpTemplate template, Image<Rgba32> img, FullUserStats stats)
2024-09-21 00:46:59 +12:00
{
if (!string.IsNullOrWhiteSpace(stats.User.Club?.ImageUrl))
{
try
{
var imgUrl = new Uri(stats.User.Club.ImageUrl);
var result = await _c.GetImageDataAsync(imgUrl);
if (!result.TryPickT0(out var data, out _))
{
using (var http = _httpFactory.CreateClient())
using (var temp = await http.GetAsync(imgUrl, HttpCompletionOption.ResponseHeadersRead))
{
if (!temp.IsImage() || temp.GetContentLength() > 11 * 1024 * 1024)
return;
var imgData = await temp.Content.ReadAsByteArrayAsync();
using (var tempDraw = Image.Load<Rgba32>(imgData))
{
tempDraw.Mutate(x => x
2025-02-08 16:43:01 +13:00
.Resize(template.Club.Icon.Size.X, template.Club.Icon.Size.Y)
.ApplyRoundedCorners(Math.Max(template.Club.Icon.Size.X,
template.Club.Icon.Size.Y)
2025-02-06 12:34:22 +13:00
/ 2.0f));
2024-09-21 00:46:59 +12:00
await using (var tds = await tempDraw.ToStreamAsync())
{
data = tds.ToArray();
}
}
}
await _c.SetImageDataAsync(imgUrl, data);
}
using var toDraw = Image.Load(data);
2025-02-08 16:43:01 +13:00
if (toDraw.Size != new Size(template.Club.Icon.Size.X, template.Club.Icon.Size.Y))
toDraw.Mutate(x => x.Resize(template.Club.Icon.Size.X, template.Club.Icon.Size.Y));
2024-09-21 00:46:59 +12:00
img.Mutate(x => x.DrawImage(
toDraw,
2025-02-08 16:43:01 +13:00
new Point(template.Club.Icon.Pos.X, template.Club.Icon.Pos.Y),
2024-09-21 00:46:59 +12:00
1));
}
catch (Exception ex)
{
Log.Warning(ex, "Error drawing club image");
}
}
}
public async Task XpReset(ulong guildId, ulong userId)
2024-09-21 00:46:59 +12:00
{
await using var uow = _db.GetDbContext();
await uow.GetTable<UserXpStats>()
2025-02-06 12:34:22 +13:00
.DeleteAsync(x => x.UserId == userId && x.GuildId == guildId);
2024-09-21 00:46:59 +12:00
}
public void XpReset(ulong guildId)
{
using var uow = _db.GetDbContext();
uow.Set<UserXpStats>().ResetGuildXp(guildId);
uow.SaveChanges();
}
public async Task ResetXpRewards(ulong guildId)
{
await using var uow = _db.GetDbContext();
await uow.GetTable<XpSettings>()
2025-02-06 12:34:22 +13:00
.Where(x => x.GuildId == guildId)
.DeleteAsync();
2024-09-21 00:46:59 +12:00
}
public ValueTask<Dictionary<string, XpConfig.ShopItemInfo>?> GetShopBgs()
{
var data = _xpConfig.Data;
if (!data.Shop.IsEnabled)
return new(default(Dictionary<string, XpConfig.ShopItemInfo>));
return new(_xpConfig.Data.Shop.Bgs?.Where(x => x.Value.Price >= 0)
.ToDictionary(x => x.Key, x => x.Value));
2024-09-21 00:46:59 +12:00
}
public ValueTask<Dictionary<string, XpConfig.ShopItemInfo>?> GetShopFrames()
{
var data = _xpConfig.Data;
if (!data.Shop.IsEnabled)
return new(default(Dictionary<string, XpConfig.ShopItemInfo>));
return new(_xpConfig.Data.Shop.Frames?.Where(x => x.Value.Price >= 0)
.ToDictionary(x => x.Key, x => x.Value));
2024-09-21 00:46:59 +12:00
}
public async Task<BuyResult> BuyShopItemAsync(ulong userId, XpShopItemType type, string key)
{
var conf = _xpConfig.Data;
if (!conf.Shop.IsEnabled)
return BuyResult.XpShopDisabled;
var req = type == XpShopItemType.Background
? conf.Shop.BgsTierRequirement
: conf.Shop.FramesTierRequirement;
if (req != PatronTier.None && !_creds.IsOwner(userId))
{
var patron = await _ps.GetPatronAsync(userId);
if (patron is null || (int)patron.Value.Tier < (int)req)
return BuyResult.InsufficientPatronTier;
}
await using var ctx = _db.GetDbContext();
try
{
if (await ctx.GetTable<XpShopOwnedItem>()
2025-02-06 12:34:22 +13:00
.AnyAsyncLinqToDB(x => x.UserId == userId && x.ItemKey == key && x.ItemType == type))
2024-09-21 00:46:59 +12:00
return BuyResult.AlreadyOwned;
var item = GetShopItem(type, key);
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}")))
2024-09-21 00:46:59 +12:00
return BuyResult.InsufficientFunds;
await ctx.GetTable<XpShopOwnedItem>()
2025-02-06 12:34:22 +13:00
.InsertAsync(() => new XpShopOwnedItem()
{
UserId = userId,
IsUsing = false,
ItemKey = key,
ItemType = type,
DateAdded = DateTime.UtcNow,
});
2024-09-21 00:46:59 +12:00
return BuyResult.Success;
}
catch (Exception ex)
{
Log.Error(ex, "Error buying shop item: {ErrorMessage}", ex.Message);
return BuyResult.UnknownItem;
}
}
private XpConfig.ShopItemInfo? GetShopItem(XpShopItemType type, string key)
{
var data = _xpConfig.Data;
if (type == XpShopItemType.Background)
{
if (data.Shop.Bgs is { } bgs && bgs.TryGetValue(key, out var item))
return item;
return null;
}
if (type == XpShopItemType.Frame)
{
if (data.Shop.Frames is { } fs && fs.TryGetValue(key, out var item))
return item;
return null;
}
throw new ArgumentOutOfRangeException(nameof(type));
}
public async Task<bool> OwnsItemAsync(
ulong userId,
XpShopItemType itemType,
string key)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<XpShopOwnedItem>()
2025-02-06 12:34:22 +13:00
.AnyAsyncLinqToDB(x => x.UserId == userId
&& x.ItemType == itemType
&& x.ItemKey == key);
2024-09-21 00:46:59 +12:00
}
public async Task<XpShopOwnedItem?> GetUserItemAsync(
ulong userId,
XpShopItemType itemType,
string key)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<XpShopOwnedItem>()
2025-02-06 12:34:22 +13:00
.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId
&& x.ItemType == itemType
&& x.ItemKey == key);
2024-09-21 00:46:59 +12:00
}
public async Task<XpShopOwnedItem?> GetItemInUse(
ulong userId,
XpShopItemType itemType)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<XpShopOwnedItem>()
2025-02-06 12:34:22 +13:00
.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId
&& x.ItemType == itemType
&& x.IsUsing);
2024-09-21 00:46:59 +12:00
}
public async Task<bool> UseShopItemAsync(ulong userId, XpShopItemType itemType, string key)
{
var data = _xpConfig.Data;
XpConfig.ShopItemInfo? item = null;
if (itemType == XpShopItemType.Background)
{
data.Shop.Bgs?.TryGetValue(key, out item);
}
else
{
data.Shop.Frames?.TryGetValue(key, out item);
}
if (item is null)
return false;
await using var ctx = _db.GetDbContext();
if (await OwnsItemAsync(userId, itemType, key))
{
await ctx.GetTable<XpShopOwnedItem>()
2025-02-06 12:34:22 +13:00
.Where(x => x.UserId == userId && x.ItemType == itemType)
.UpdateAsync(old => new()
{
IsUsing = key == old.ItemKey
});
2024-09-21 00:46:59 +12:00
return true;
}
return false;
}
public PatronTier GetXpShopTierRequirement(Xp.XpShopInputType type)
=> type switch
{
Xp.XpShopInputType.F => _xpConfig.Data.Shop.FramesTierRequirement,
_ => _xpConfig.Data.Shop.BgsTierRequirement,
};
public bool IsShopEnabled()
=> _xpConfig.Data.Shop.IsEnabled;
public async Task<int> GetGuildXpUsersCountAsync(ulong requestGuildId, List<ulong>? guildUsers = null)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<UserXpStats>()
2025-02-06 12:34:22 +13:00
.Where(x => x.GuildId == requestGuildId
&& (guildUsers == null || guildUsers.Contains(x.UserId)))
.CountAsyncLinqToDB();
}
public async Task SetLevelAsync(ulong guildId, ulong userId, int level)
{
var lvlStats = LevelStats.CreateForLevel(level);
await using var ctx = _db.GetDbContext();
await ctx.GetTable<UserXpStats>()
2025-02-06 12:34:22 +13:00
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
UserId = userId,
Xp = lvlStats.TotalXp,
DateAdded = DateTime.UtcNow
},
(old) => new()
{
Xp = lvlStats.TotalXp
},
() => new()
{
GuildId = guildId,
UserId = userId
});
}
2025-02-06 12:34:22 +13:00
}
2025-02-08 16:43:01 +13:00
public sealed class XpTemplateService : IEService, IReadyExecutor
2025-02-06 12:34:22 +13:00
{
private const string XP_TEMPLATE_PATH = "./data/xp_template.json";
2025-02-06 12:51:47 +13:00
private readonly IPubSub _pubSub;
private XpTemplate _template = new();
private readonly TypedKey<bool> _xpTemplateReloadKey = new("xp.template.reload");
public XpTemplateService(IPubSub pubSub)
{
_pubSub = pubSub;
}
private void InternalReloadXpTemplate()
{
try
{
var settings = new JsonSerializerSettings
{
ContractResolver = new RequireObjectPropertiesContractResolver()
};
if (!File.Exists(XP_TEMPLATE_PATH))
{
var newTemp = new XpTemplate();
newTemp.Version = 2;
File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(newTemp, Formatting.Indented));
}
_template = JsonConvert.DeserializeObject<XpTemplate>(
File.ReadAllText(XP_TEMPLATE_PATH),
settings)!;
}
catch (Exception ex)
{
Log.Error(ex, "xp_template.json is invalid. Loaded default values");
_template = new();
}
}
public void ReloadXpTemplate()
=> _pubSub.Pub(_xpTemplateReloadKey, true);
public async Task OnReadyAsync()
{
InternalReloadXpTemplate();
await _pubSub.Sub(_xpTemplateReloadKey,
_ =>
{
InternalReloadXpTemplate();
return default;
});
}
public XpTemplate GetTemplate()
=> _template;
2024-09-21 00:46:59 +12:00
}