xp almost fully reimplemented

This commit is contained in:
Toastie 2025-02-06 12:51:47 +13:00
parent 96c4ea0637
commit 5d9326b65e
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
5 changed files with 236 additions and 183 deletions
src/EllieBot

View file

@ -89,9 +89,9 @@ public sealed class EllieDbService : DbService
var applied = await ctx.Database.GetAppliedMigrationsAsync(); var applied = await ctx.Database.GetAppliedMigrationsAsync();
// get all .sql file names from the migrations folder // get all .sql file names from the migrations folder
var available = Directory.GetFiles("Migrations/Sqlite", "*_*.sql") var available = Directory.GetFiles("Migrations/" + GetMigrationDirectory(ctx.Database), "*_*.sql")
.Select(x => Path.GetFileNameWithoutExtension(x)) .Select(x => Path.GetFileNameWithoutExtension(x))
.OrderBy(x => x); .OrderBy(x => x);
var lastApplied = applied.Last(); var lastApplied = applied.Last();
Log.Information("Last applied migration: {LastApplied}", lastApplied); Log.Information("Last applied migration: {LastApplied}", lastApplied);
@ -112,12 +112,17 @@ public sealed class EllieDbService : DbService
} }
private static string GetMigrationPath(DatabaseFacade ctxDatabase, string runnable) private static string GetMigrationPath(DatabaseFacade ctxDatabase, string runnable)
{
return $"Migrations/{GetMigrationDirectory(ctxDatabase)}/{runnable}.sql";
}
private static string GetMigrationDirectory(DatabaseFacade ctxDatabase)
{ {
if (ctxDatabase.IsSqlite()) if (ctxDatabase.IsSqlite())
return $"Migrations/Sqlite/{runnable}.sql"; return "Sqlite";
if (ctxDatabase.IsNpgsql()) if (ctxDatabase.IsNpgsql())
return $"Migrations/PostgreSql/{runnable}.sql"; return "PostgreSql";
throw new NotSupportedException("This database type is not supported."); throw new NotSupportedException("This database type is not supported.");
} }

View file

@ -33,8 +33,8 @@ public static class GuildConfigExtensions
public static async Task<StreamRoleSettings> GetOrCreateStreamRoleSettings(this DbContext ctx, ulong guildId) public static async Task<StreamRoleSettings> GetOrCreateStreamRoleSettings(this DbContext ctx, ulong guildId)
{ {
var srs = await ctx.GetTable<StreamRoleSettings>() var srs = await ctx.GetTable<StreamRoleSettings>()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.FirstOrDefaultAsyncEF(); .FirstOrDefaultAsyncEF();
if (srs is not null) if (srs is not null)
return srs; return srs;
@ -52,18 +52,18 @@ public static class GuildConfigExtensions
public static LogSetting LogSettingsFor(this DbContext ctx, ulong guildId) public static LogSetting LogSettingsFor(this DbContext ctx, ulong guildId)
{ {
var logSetting = ctx.Set<LogSetting>() var logSetting = ctx.Set<LogSetting>()
.AsQueryable() .AsQueryable()
.Include(x => x.LogIgnores) .Include(x => x.LogIgnores)
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.FirstOrDefault(); .FirstOrDefault();
if (logSetting is null) if (logSetting is null)
{ {
ctx.Set<LogSetting>() ctx.Set<LogSetting>()
.Add(logSetting = new() .Add(logSetting = new()
{ {
GuildId = guildId GuildId = guildId
}); });
ctx.SaveChanges(); ctx.SaveChanges();
} }
@ -71,7 +71,6 @@ public static class GuildConfigExtensions
} }
public static IEnumerable<GuildConfig> PermissionsForAll(this DbSet<GuildConfig> configs, List<ulong> include) public static IEnumerable<GuildConfig> PermissionsForAll(this DbSet<GuildConfig> configs, List<ulong> include)
{ {
var query = configs.AsQueryable().Where(x => include.Contains(x.GuildId)).Include(gc => gc.Permissions); var query = configs.AsQueryable().Where(x => include.Contains(x.GuildId)).Include(gc => gc.Permissions);
@ -82,19 +81,19 @@ public static class GuildConfigExtensions
public static GuildConfig GcWithPermissionsFor(this DbContext ctx, ulong guildId) public static GuildConfig GcWithPermissionsFor(this DbContext ctx, ulong guildId)
{ {
var config = ctx.Set<GuildConfig>() var config = ctx.Set<GuildConfig>()
.AsQueryable() .AsQueryable()
.Where(gc => gc.GuildId == guildId) .Where(gc => gc.GuildId == guildId)
.Include(gc => gc.Permissions) .Include(gc => gc.Permissions)
.FirstOrDefault(); .FirstOrDefault();
if (config is null) // if there is no guildconfig, create new one if (config is null) // if there is no guildconfig, create new one
{ {
ctx.Set<GuildConfig>() ctx.Set<GuildConfig>()
.Add(config = new() .Add(config = new()
{ {
GuildId = guildId, GuildId = guildId,
Permissions = Permissionv2.GetDefaultPermlist Permissions = Permissionv2.GetDefaultPermlist
}); });
ctx.SaveChanges(); ctx.SaveChanges();
} }
else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones
@ -106,20 +105,24 @@ public static class GuildConfigExtensions
return config; return config;
} }
public static async Task<XpSettings> XpSettingsFor(this DbContext ctx, ulong guildId) public static async Task<XpSettings> XpSettingsFor(this DbContext ctx, ulong guildId,
Func<IQueryable<XpSettings>, IQueryable<XpSettings>> includes = default)
{ {
var srs = await ctx.GetTable<XpSettings>() includes ??= static set => set;
.Where(x => x.GuildId == guildId)
.FirstOrDefaultAsyncLinqToDB(); var srs = await includes(ctx.GetTable<XpSettings>()
.Where(x => x.GuildId == guildId))
.FirstOrDefaultAsyncLinqToDB();
if (srs is not null) if (srs is not null)
return srs; return srs;
srs = await ctx.GetTable<XpSettings>() srs = await ctx.GetTable<XpSettings>()
.InsertWithOutputAsync(() => new() .InsertWithOutputAsync(() => new()
{ {
GuildId = guildId, GuildId = guildId,
}); ServerExcluded = false,
});
return srs; return srs;
} }

View file

@ -17,8 +17,8 @@ public partial class Xp
public async Task XpRewsReset() public async Task XpRewsReset()
{ {
var promptEmbed = CreateEmbed() var promptEmbed = CreateEmbed()
.WithPendingColor() .WithPendingColor()
.WithDescription(GetText(strs.xprewsreset_confirm)); .WithDescription(GetText(strs.xprewsreset_confirm));
var reply = await PromptUserConfirmAsync(promptEmbed); var reply = await PromptUserConfirmAsync(promptEmbed);
@ -38,57 +38,59 @@ public partial class Xp
if (page is < 0 or > 100) if (page is < 0 or > 100)
return; return;
var rews = await _service.GetRoleRewardsAsync(ctx.Guild.Id); var xpSettings = await _service.GetFullXpSettingsFor(ctx.Guild.Id);
var rews = xpSettings.RoleRewards;
var allRewards = rews.OrderBy(x => x.Level) var allRewards = rews.OrderBy(x => x.Level)
.Select(x => .Select(x =>
{ {
var sign = !x.Remove ? "✅ " : "❌ "; var sign = !x.Remove ? "✅ " : "❌ ";
var str = ctx.Guild.GetRole(x.RoleId)?.ToString(); var str = ctx.Guild.GetRole(x.RoleId)?.ToString();
if (str is null) if (str is null)
{ {
str = GetText(strs.role_not_found(Format.Code(x.RoleId.ToString()))); str = GetText(strs.role_not_found(Format.Code(x.RoleId.ToString())));
} }
else else
{ {
if (!x.Remove) if (!x.Remove)
str = GetText(strs.xp_receive_role(Format.Bold(str))); str = GetText(strs.xp_receive_role(Format.Bold(str)));
else else
str = GetText(strs.xp_lose_role(Format.Bold(str))); str = GetText(strs.xp_lose_role(Format.Bold(str)));
} }
return (x.Level, Text: sign + str); return (x.Level, Text: sign + str);
}) })
.Concat((await _service.GetCurrencyRewardsAsync(ctx.Guild.Id)) .Concat(xpSettings.CurrencyRewards
.OrderBy(x => x.Level) .OrderBy(x => x.Level)
.Select(x => (x.Level, .Select(x => (x.Level,
Format.Bold(x.Amount + _cp.GetCurrencySign())))) Format.Bold(x.Amount + _cp.GetCurrencySign()))))
.GroupBy(x => x.Level) .GroupBy(x => x.Level)
.OrderBy(x => x.Key) .OrderBy(x => x.Key)
.ToList(); .ToList();
await Response() await Response()
.Paginated() .Paginated()
.Items(allRewards) .Items(allRewards)
.PageSize(9) .PageSize(9)
.CurrentPage(page) .CurrentPage(page)
.Page((items, _) => .Page((items, _) =>
{ {
var embed = CreateEmbed().WithTitle(GetText(strs.level_up_rewards)).WithOkColor(); var embed = CreateEmbed().WithTitle(GetText(strs.level_up_rewards)).WithOkColor();
if (!items.Any()) if (!items.Any())
return embed.WithDescription(GetText(strs.no_level_up_rewards)); return embed.WithDescription(GetText(strs.no_level_up_rewards));
foreach (var reward in items) foreach (var reward in items)
{ {
embed.AddField(GetText(strs.level_x(reward.Key)), embed.AddField(GetText(strs.level_x(reward.Key)),
string.Join("\n", reward.Select(y => y.Item2))); string.Join("\n", reward.Select(y => y.Item2)));
} }
return embed; return embed;
}) })
.SendAsync(); .SendAsync();
} }
[Cmd] [Cmd]
@ -120,9 +122,9 @@ public partial class Xp
else else
{ {
await Response() await Response()
.Confirm(strs.xp_role_reward_remove_role(Format.Bold(level.ToString()), .Confirm(strs.xp_role_reward_remove_role(Format.Bold(level.ToString()),
Format.Bold(role.ToString()))) Format.Bold(role.ToString())))
.SendAsync(); .SendAsync();
} }
} }
@ -142,9 +144,9 @@ public partial class Xp
else else
{ {
await Response() await Response()
.Confirm(strs.cur_reward_added(level, .Confirm(strs.cur_reward_added(level,
Format.Bold(amount + _cp.GetCurrencySign()))) Format.Bold(amount + _cp.GetCurrencySign())))
.SendAsync(); .SendAsync();
} }
} }
} }

View file

@ -46,11 +46,12 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
private readonly IPatronageService _ps; private readonly IPatronageService _ps;
private readonly IBotCache _c; private readonly IBotCache _c;
private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>(); private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>();
private readonly INotifySubscriber _notifySub; private readonly INotifySubscriber _notifySub;
private readonly ShardData _shardData; private readonly ShardData _shardData;
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 100);
public XpService( public XpService(
DiscordSocketClient client, DiscordSocketClient client,
DbService db, DbService db,
@ -109,7 +110,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
public async Task OnReadyAsync() public async Task OnReadyAsync()
{ {
// initialize ignored // initialize ignored
ArgumentOutOfRangeException.ThrowIfLessThan(_xpConfig.Data.MessageXpCooldown, 1, ArgumentOutOfRangeException.ThrowIfLessThan(_xpConfig.Data.MessageXpCooldown,
1,
nameof(_xpConfig.Data.MessageXpCooldown)); nameof(_xpConfig.Data.MessageXpCooldown));
await using (var ctx = _db.GetDbContext()) await using (var ctx = _db.GetDbContext())
@ -134,27 +136,25 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
} }
} }
await Task.WhenAll(UpdateTimer(), PeriodClearTimer()); await Task.WhenAll(UpdateTimer(), _levelUpQueue.RunAsync());
return; return;
async Task PeriodClearTimer()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(_xpConfig.Data.MessageXpCooldown));
while (true)
{
await timer.WaitForNextTickAsync();
_usersGainedInPeriod.Clear();
}
}
async Task UpdateTimer() async Task UpdateTimer()
{ {
// todo a bigger loop that runs once every XpTimer // todo a bigger loop that runs once every XpTimer
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3)); using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
while (await timer.WaitForNextTickAsync()) while (await timer.WaitForNextTickAsync())
{ {
await UpdateXp(); try
{
await UpdateXp();
}
catch (Exception ex)
{
Log.Error(ex, "Error updating xp");
await Task.Delay(30_000);
}
} }
} }
} }
@ -170,33 +170,80 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
var currentBatch = _usersBatch.ToArray(); var currentBatch = _usersBatch.ToArray();
_usersBatch.Clear(); _usersBatch.Clear();
if (currentBatch.Length == 0)
return;
var ids = currentBatch.Select(x => x.Id).ToArray();
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await using var lctx = ctx.CreateLinqToDBConnection(); await using var lctx = ctx.CreateLinqToDBConnection();
await using var batchTable = await lctx.CreateTempTableAsync<UserXpBatch>(); var tempTableName = "xp_batch_" + _shardData.ShardId;
await using var batchTable = await lctx.CreateTempTableAsync<UserXpBatch>(tempTableName);
await batchTable.BulkCopyAsync(currentBatch.Select(x => new UserXpBatch() await batchTable.BulkCopyAsync(currentBatch.Select(x => new UserXpBatch()
{ {
GuildId = x.GuildId, GuildId = x.GuildId,
UserId = x.Id, UserId = x.Id,
UserName = x.Username, Username = x.Username,
AvatarId = x.DisplayAvatarId, AvatarId = x.DisplayAvatarId
})); }));
await lctx.ExecuteAsync( await lctx.ExecuteAsync(
$""" $"""
INSERT INTO ${nameof(DiscordUser)} INSERT INTO UserXpStats (GuildId, UserId, Xp)
SELECT ${nameof(UserXpBatch.GuildId)}, ${nameof(UserXpBatch.UserId)}, ${nameof(UserXpBatch.UserName)}, ${nameof(UserXpBatch.AvatarId)}, ${xpAmount} SELECT "{tempTableName}"."GuildId", "{tempTableName}"."UserId", {xpAmount}
FROM ${nameof(UserXpBatch)} FROM {tempTableName}
ON CONFLICT(${nameof(DiscordUser.UserId)}, ${nameof(DiscordUser.UserId)}) DO UPDATE SET WHERE TRUE
${nameof(DiscordUser.Username)} = ${nameof(UserXpBatch)}.${nameof(UserXpBatch.UserName)} ON CONFLICT (GuildId, UserId) DO UPDATE
${nameof(DiscordUser.AvatarId)} = ${nameof(UserXpBatch)}.${nameof(UserXpBatch.AvatarId)} SET
${nameof(DiscordUser.TotalXp)} = ${nameof(DiscordUser.TotalXp)} + ${xpAmount} Xp = UserXpStats.Xp + EXCLUDED.Xp;
RETURNING *;
"""); """);
// todo send notifications await lctx.ExecuteAsync(
$"""
INSERT INTO DiscordUser (UserId, AvatarId, Username, TotalXp)
SELECT "{tempTableName}"."UserId", "{tempTableName}"."AvatarId", "{tempTableName}"."Username", {xpAmount}
FROM {tempTableName}
WHERE TRUE
ON CONFLICT (UserId) DO UPDATE
SET
Username = EXCLUDED.Username,
AvatarId = EXCLUDED.AvatarId,
TotalXp = DiscordUser.TotalXp + {xpAmount};
""");
foreach (var (guildId, users) in currentBatch.GroupBy(x => x.GuildId)
.ToDictionary(x => x.Key, x => x.AsEnumerable()))
{
var userIds = users.Select(x => x.Id).ToArray();
var dbStats = await ctx.GetTable<UserXpStats>()
.Where(x => x.GuildId == guildId && userIds.Contains(x.UserId))
.OrderByDescending(x => x.Xp)
.ToArrayAsyncLinqToDB();
for (var i = 0; i < dbStats.Length; i++)
{
var oldStats = new LevelStats(dbStats[i].Xp - xpAmount);
var newStats = new LevelStats(dbStats[i].Xp);
Log.Information("User {User} xp updated from {OldLevel} to {NewLevel}",
dbStats[i].UserId,
oldStats.TotalXp,
newStats.TotalXp);
if (oldStats.Level < newStats.Level)
{
await _levelUpQueue.EnqueueAsync(NotifyUser(guildId,
0,
dbStats[i].UserId,
true,
oldStats.Level,
newStats.Level));
}
}
}
} }
private Func<Task> NotifyUser( private Func<Task> NotifyUser(
@ -222,13 +269,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
long oldLevel, long oldLevel,
long newLevel) long newLevel)
{ {
List<XpRoleReward> rrews; var settings = await GetFullXpSettingsFor(guildId);
List<XpCurrencyReward> crews; var rrews = settings.RoleRewards;
await using (var ctx = _db.GetDbContext()) var crews = settings.CurrencyRewards;
{
rrews = await ctx.XpSettingsFor(guildId).Fmap(x => x.RoleRewards.ToList());
crews = await ctx.XpSettingsFor(guildId).Fmap(x => x.CurrencyRewards.ToList());
}
//loop through levels since last level up, so if a high amount of xp is gained, reward are still applied. //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++) for (var i = oldLevel + 1; i <= newLevel; i++)
@ -369,7 +412,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
public async Task SetCurrencyReward(ulong guildId, int level, int amount) public async Task SetCurrencyReward(ulong guildId, int level, int amount)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var settings = await uow.XpSettingsFor(guildId); var settings = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.CurrencyRewards));
if (amount <= 0) if (amount <= 0)
{ {
@ -399,22 +442,19 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
uow.SaveChanges(); uow.SaveChanges();
} }
public async Task<IEnumerable<XpCurrencyReward>> GetCurrencyRewardsAsync(ulong id) public async Task<XpSettings> GetFullXpSettingsFor(ulong guildId)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return (await uow.XpSettingsFor(id)).CurrencyRewards.ToArray(); return await uow.XpSettingsFor(guildId,
} set => set
.LoadWith(x => x.CurrencyRewards)
public async Task<IEnumerable<XpRoleReward>> GetRoleRewardsAsync(ulong id) .LoadWith(x => x.RoleRewards));
{
await using var uow = _db.GetDbContext();
return (await uow.XpSettingsFor(id)).RoleRewards.ToArray();
} }
public async Task ResetRoleRewardAsync(ulong guildId, int level) public async Task ResetRoleRewardAsync(ulong guildId, int level)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var settings = await uow.XpSettingsFor(guildId); var settings = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.RoleRewards));
var toRemove = settings.RoleRewards.FirstOrDefault(x => x.Level == level); var toRemove = settings.RoleRewards.FirstOrDefault(x => x.Level == level);
if (toRemove is not null) if (toRemove is not null)
@ -423,7 +463,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
settings.RoleRewards.Remove(toRemove); settings.RoleRewards.Remove(toRemove);
} }
uow.SaveChanges(); await uow.SaveChangesAsync();
} }
public async Task SetRoleRewardAsync( public async Task SetRoleRewardAsync(
@ -433,7 +473,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
bool remove) bool remove)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var settings = await uow.XpSettingsFor(guildId); var settings = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.RoleRewards));
var rew = settings.RoleRewards.FirstOrDefault(x => x.Level == level); var rew = settings.RoleRewards.FirstOrDefault(x => x.Level == level);
@ -806,7 +846,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
{ {
var roles = _excludedRoles.GetOrAdd(guildId, _ => new()); var roles = _excludedRoles.GetOrAdd(guildId, _ => new());
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var xpSetting = await uow.XpSettingsFor(guildId); var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList));
var excludeObj = new ExcludedItem var excludeObj = new ExcludedItem
{ {
ItemId = rId, ItemId = rId,
@ -837,7 +877,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
{ {
var channels = _excludedChannels.GetOrAdd(guildId, _ => new()); var channels = _excludedChannels.GetOrAdd(guildId, _ => new());
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var xpSetting = await uow.XpSettingsFor(guildId); var xpSetting = await uow.XpSettingsFor(guildId, set => set.LoadWith(x => x.ExclusionList));
var excludeObj = new ExcludedItem var excludeObj = new ExcludedItem
{ {
ItemId = chId, ItemId = chId,
@ -1510,8 +1550,10 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
public sealed class UserXpBatch public sealed class UserXpBatch
{ {
[Key] public ulong UserId { get; set; } [Key]
public ulong UserId { get; set; }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }
public string UserName { get; set; } = string.Empty; public string Username { get; set; } = string.Empty;
public string AvatarId { get; set; } = string.Empty; public string AvatarId { get; set; } = string.Empty;
} }

View file

@ -42,22 +42,23 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
var reply = new GetXpSettingsReply(); var reply = new GetXpSettingsReply();
reply.Exclusions.AddRange(excludedChannels reply.Exclusions.AddRange(excludedChannels
.Select(x => new ExclItemReply() .Select(x => new ExclItemReply()
{ {
Id = x, Id = x,
Type = "Channel", Type = "Channel",
Name = guild.GetChannel(x)?.Name ?? "????" Name = guild.GetChannel(x)?.Name ?? "????"
}) })
.Concat(excludedRoles .Concat(excludedRoles
.Select(x => new ExclItemReply() .Select(x => new ExclItemReply()
{ {
Id = x, Id = x,
Type = "Role", Type = "Role",
Name = guild.GetRole(x)?.Name ?? "????" Name = guild.GetRole(x)?.Name ?? "????"
}))); })));
var curRews = await _xp.GetCurrencyRewardsAsync(request.GuildId); var settings = await _xp.GetFullXpSettingsFor(request.GuildId);
var roleRews = await _xp.GetRoleRewardsAsync(request.GuildId); var curRews = settings.CurrencyRewards;
var roleRews = settings.RoleRewards;
var rews = curRews.Select(x => new RewItemReply() var rews = curRews.Select(x => new RewItemReply()
{ {
@ -72,7 +73,7 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
Type = x.Remove ? "RemoveRole" : "AddRole", Type = x.Remove ? "RemoveRole" : "AddRole",
Value = guild.GetRole(x.RoleId)?.ToString() ?? x.RoleId.ToString() Value = guild.GetRole(x.RoleId)?.ToString() ?? x.RoleId.ToString()
})) }))
.OrderBy(x => x.Level); .OrderBy(x => x.Level);
reply.Rewards.AddRange(rews); reply.Rewards.AddRange(rews);
@ -224,43 +225,43 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
}; };
var users = await data var users = await data
.Select(async x => .Select(async x =>
{ {
var user = guild.GetUser(x.UserId); var user = guild.GetUser(x.UserId);
if (user is null) if (user is null)
{ {
var du = await _duSvc.GetUserAsync(x.UserId); var du = await _duSvc.GetUserAsync(x.UserId);
if (du is null) if (du is null)
return new XpLbUserReply return new XpLbUserReply
{ {
UserId = x.UserId, UserId = x.UserId,
Avatar = string.Empty, Avatar = string.Empty,
Username = string.Empty, Username = string.Empty,
Xp = x.Xp, Xp = x.Xp,
Level = new LevelStats(x.Xp).Level Level = new LevelStats(x.Xp).Level
}; };
return new XpLbUserReply() return new XpLbUserReply()
{ {
UserId = x.UserId, UserId = x.UserId,
Avatar = du.RealAvatarUrl()?.ToString() ?? string.Empty, Avatar = du.RealAvatarUrl()?.ToString() ?? string.Empty,
Username = du.ToString() ?? string.Empty, Username = du.ToString() ?? string.Empty,
Xp = x.Xp, Xp = x.Xp,
Level = new LevelStats(x.Xp).Level Level = new LevelStats(x.Xp).Level
}; };
} }
return new XpLbUserReply return new XpLbUserReply
{ {
UserId = x.UserId, UserId = x.UserId,
Avatar = user?.GetAvatarUrl() ?? string.Empty, Avatar = user?.GetAvatarUrl() ?? string.Empty,
Username = user?.ToString() ?? string.Empty, Username = user?.ToString() ?? string.Empty,
Xp = x.Xp, Xp = x.Xp,
Level = new LevelStats(x.Xp).Level Level = new LevelStats(x.Xp).Level
}; };
}) })
.WhenAll(); .WhenAll();
reply.Users.AddRange(users); reply.Users.AddRange(users);