fix: xplb and xpglb pagination fixed, closes #430

fix: Page number when there is an unknown number of items while paginating is now correct
fix: .stm and .stma fixed and can now mention everyone as long as the user executing the command also can
dev: Cleaned up/improved some code
This commit is contained in:
Kwoth
2024-05-15 13:44:20 +00:00
parent 803fe5db2f
commit a52a246982
13 changed files with 183 additions and 134 deletions

View File

@@ -2,6 +2,15 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [5.0.7] - 15.05.2024
### Fixed
- `.streammessage` will once again be able to mention anyone (as long as the user setting the message has the permission to mention everyone)
- `.streammsgall` fixed
- `.xplb` and `.xpglb` pagination fixed
- Fixed page number when the total number of elements is unknown
## [5.0.6] - 14.05.2024 ## [5.0.6] - 14.05.2024
### Changed ### Changed

View File

@@ -20,48 +20,48 @@ public static class DiscordUserExtensions
string discrim, string discrim,
string avatarId) string avatarId)
=> ctx.GetTable<DiscordUser>() => ctx.GetTable<DiscordUser>()
.InsertOrUpdate( .InsertOrUpdate(
() => new() () => new()
{ {
UserId = userId, UserId = userId,
Username = username, Username = username,
Discriminator = discrim, Discriminator = discrim,
AvatarId = avatarId, AvatarId = avatarId,
TotalXp = 0, TotalXp = 0,
CurrencyAmount = 0 CurrencyAmount = 0
}, },
old => new() old => new()
{ {
Username = username, Username = username,
Discriminator = discrim, Discriminator = discrim,
AvatarId = avatarId AvatarId = avatarId
}, },
() => new() () => new()
{ {
UserId = userId UserId = userId
}); });
public static Task EnsureUserCreatedAsync( public static Task EnsureUserCreatedAsync(
this DbContext ctx, this DbContext ctx,
ulong userId) ulong userId)
=> ctx.GetTable<DiscordUser>() => ctx.GetTable<DiscordUser>()
.InsertOrUpdateAsync( .InsertOrUpdateAsync(
() => new() () => new()
{ {
UserId = userId, UserId = userId,
Username = "Unknown", Username = "Unknown",
Discriminator = "????", Discriminator = "????",
AvatarId = string.Empty, AvatarId = string.Empty,
TotalXp = 0, TotalXp = 0,
CurrencyAmount = 0 CurrencyAmount = 0
}, },
old => new() old => new()
{ {
}, },
() => new() () => new()
{ {
UserId = userId UserId = userId
}); });
//temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown //temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown
public static DiscordUser GetOrCreateUser( public static DiscordUser GetOrCreateUser(
@@ -83,25 +83,29 @@ public static class DiscordUserExtensions
public static int GetUserGlobalRank(this DbSet<DiscordUser> users, ulong id) public static int GetUserGlobalRank(this DbSet<DiscordUser> users, ulong id)
=> users.AsQueryable() => users.AsQueryable()
.Where(x => x.TotalXp .Where(x => x.TotalXp
> users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault()) > users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault())
.Count() .Count()
+ 1; + 1;
public static DiscordUser[] GetUsersXpLeaderboardFor(this DbSet<DiscordUser> users, int page, int perPage) public static async Task<IReadOnlyCollection<DiscordUser>> GetUsersXpLeaderboardFor(this DbSet<DiscordUser> users, int page, int perPage)
=> users.AsQueryable().OrderByDescending(x => x.TotalXp).Skip(page * perPage).Take(perPage).AsEnumerable() => await users.ToLinqToDBTable()
.ToArray(); .OrderByDescending(x => x.TotalXp)
.Skip(page * perPage)
.Take(perPage)
.ToArrayAsyncLinqToDB();
public static Task<List<DiscordUser>> GetTopRichest( public static Task<List<DiscordUser>> GetTopRichest(
this DbSet<DiscordUser> users, this DbSet<DiscordUser> users,
ulong botId, ulong botId,
int page = 0, int perPage = 9) int page = 0,
int perPage = 9)
=> users.AsQueryable() => users.AsQueryable()
.Where(c => c.CurrencyAmount > 0 && botId != c.UserId) .Where(c => c.CurrencyAmount > 0 && botId != c.UserId)
.OrderByDescending(c => c.CurrencyAmount) .OrderByDescending(c => c.CurrencyAmount)
.Skip(page * perPage) .Skip(page * perPage)
.Take(perPage) .Take(perPage)
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
public static async Task<long> GetUserCurrencyAsync(this DbSet<DiscordUser> users, ulong userId) public static async Task<long> GetUserCurrencyAsync(this DbSet<DiscordUser> users, ulong userId)
=> (await users.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId))?.CurrencyAmount ?? 0; => (await users.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId))?.CurrencyAmount ?? 0;
@@ -118,8 +122,8 @@ public static class DiscordUserExtensions
public static decimal GetTopOnePercentCurrency(this DbSet<DiscordUser> users, ulong botId) public static decimal GetTopOnePercentCurrency(this DbSet<DiscordUser> users, ulong botId)
=> users.AsQueryable() => users.AsQueryable()
.Where(x => x.UserId != botId) .Where(x => x.UserId != botId)
.OrderByDescending(x => x.CurrencyAmount) .OrderByDescending(x => x.CurrencyAmount)
.Take(users.Count() / 100 == 0 ? 1 : users.Count() / 100) .Take(users.Count() / 100 == 0 ? 1 : users.Count() / 100)
.Sum(x => x.CurrencyAmount); .Sum(x => x.CurrencyAmount);
} }

View File

@@ -2,7 +2,6 @@
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
namespace NadekoBot.Db; namespace NadekoBot.Db;
@@ -27,33 +26,33 @@ public static class UserXpExtensions
return usr; return usr;
} }
public static List<UserXpStats> GetUsersFor(this DbSet<UserXpStats> xps, ulong guildId, int page) public static async Task<IReadOnlyCollection<UserXpStats>> GetUsersFor(
=> xps.AsQueryable() this DbSet<UserXpStats> xps,
.AsNoTracking() ulong guildId,
.Where(x => x.GuildId == guildId) int page)
.OrderByDescending(x => x.Xp + x.AwardedXp) => await xps.ToLinqToDBTable()
.Skip(page * 9) .Where(x => x.GuildId == guildId)
.Take(9) .OrderByDescending(x => x.Xp + x.AwardedXp)
.ToList(); .Skip(page * 9)
.Take(9)
.ToArrayAsyncLinqToDB();
public static List<UserXpStats> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count) public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
=> xps.AsQueryable() => await xps.ToLinqToDBTable()
.AsNoTracking() .Where(x => x.GuildId == guildId)
.Where(x => x.GuildId == guildId) .OrderByDescending(x => x.Xp + x.AwardedXp)
.OrderByDescending(x => x.Xp + x.AwardedXp) .Take(count)
.Take(count) .ToListAsyncLinqToDB();
.ToList();
public static int GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId) public static async Task<int> GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
=> xps.AsQueryable() => await xps.ToLinqToDBTable()
.AsNoTracking() .Where(x => x.GuildId == guildId
.Where(x => x.GuildId == guildId && x.Xp + x.AwardedXp
&& x.Xp + x.AwardedXp > xps.AsQueryable()
> xps.AsQueryable() .Where(y => y.UserId == userId && y.GuildId == guildId)
.Where(y => y.UserId == userId && y.GuildId == guildId) .Select(y => y.Xp + y.AwardedXp)
.Select(y => y.Xp + y.AwardedXp) .FirstOrDefault())
.FirstOrDefault()) .CountAsyncLinqToDB()
.Count()
+ 1; + 1;
public static void ResetGuildUserXp(this DbSet<UserXpStats> xps, ulong userId, ulong guildId) public static void ResetGuildUserXp(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
@@ -63,10 +62,9 @@ public static class UserXpExtensions
=> xps.Delete(x => x.GuildId == guildId); => xps.Delete(x => x.GuildId == guildId);
public static async Task<LevelStats> GetLevelDataFor(this ITable<UserXpStats> userXp, ulong guildId, ulong userId) public static async Task<LevelStats> GetLevelDataFor(this ITable<UserXpStats> userXp, ulong guildId, ulong userId)
=> await userXp => await userXp
.Where(x => x.GuildId == guildId && x.UserId == userId) .Where(x => x.GuildId == guildId && x.UserId == userId)
.FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs .FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs
? new(uxs.Xp + uxs.AwardedXp) ? new(uxs.Xp + uxs.AwardedXp)
: new(0); : new(0);
} }

View File

@@ -3,6 +3,7 @@ namespace NadekoBot.Db.Models;
public class GuildConfig : DbEntity public class GuildConfig : DbEntity
{ {
// public bool Keep { get; set; }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }
public string Prefix { get; set; } public string Prefix { get; set; }

View File

@@ -766,7 +766,7 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
async Task<IEnumerable<DiscordUser>> GetTopRichest(int curPage) async Task<IReadOnlyCollection<DiscordUser>> GetTopRichest(int curPage)
{ {
if (opts.Clean) if (opts.Clean)
{ {

View File

@@ -143,6 +143,10 @@ public partial class Searches
if (--index < 0) if (--index < 0)
return; return;
var canMentionEveryone = (ctx.User as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
if (!canMentionEveryone)
message = message?.SanitizeAllMentions();
if (!_service.SetStreamMessage(ctx.Guild.Id, index, message, out var fs)) if (!_service.SetStreamMessage(ctx.Guild.Id, index, message, out var fs))
{ {
await Response().Confirm(strs.stream_not_following).SendAsync(); await Response().Confirm(strs.stream_not_following).SendAsync();
@@ -160,6 +164,10 @@ public partial class Searches
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
public async Task StreamMessageAll([Leftover] string message) public async Task StreamMessageAll([Leftover] string message)
{ {
var canMentionEveryone = (ctx.User as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
if (!canMentionEveryone)
message = message?.SanitizeAllMentions();
var count = _service.SetStreamMessageForAll(ctx.Guild.Id, message); var count = _service.SetStreamMessageForAll(ctx.Guild.Id, message);
if (count == 0) if (count == 0)

View File

@@ -294,6 +294,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
var msg = await _sender.Response(textChannel) var msg = await _sender.Response(textChannel)
.Embed(GetEmbed(fs.GuildId, stream, false)) .Embed(GetEmbed(fs.GuildId, stream, false))
.Text(message) .Text(message)
.Sanitize(false)
.SendAsync(); .SendAsync();
// only cache the ids of channel/message pairs // only cache the ids of channel/message pairs
@@ -615,7 +616,9 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
{ {
using var uow = _db.GetDbContext(); using var uow = _db.GetDbContext();
var all = uow.Set<FollowedStream>().ToList(); var all = uow.Set<FollowedStream>()
.Where(x => x.GuildId == guildId)
.ToList();
if (all.Count == 0) if (all.Count == 0)
return 0; return 0;
@@ -624,6 +627,19 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
uow.SaveChanges(); uow.SaveChanges();
lock (_shardLock)
{
foreach (var fs in all)
{
var streams = GetLocalGuildStreams(fs.CreateKey(), guildId);
// message doesn't participate in equality checking
// removing and adding = update
streams.Remove(fs);
streams.Add(fs);
}
}
return all.Count; return all.Count;
} }

View File

@@ -1,7 +1,6 @@
#nullable disable warnings #nullable disable warnings
using NadekoBot.Modules.Xp.Services; using NadekoBot.Modules.Xp.Services;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
using NadekoBot.Db;
using NadekoBot.Modules.Patronage; using NadekoBot.Modules.Patronage;
namespace NadekoBot.Modules.Xp; namespace NadekoBot.Modules.Xp;
@@ -191,21 +190,22 @@ public partial class Xp : NadekoModule<XpService>
await ctx.Channel.TriggerTypingAsync(); await ctx.Channel.TriggerTypingAsync();
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild); await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
allCleanUsers = _service.GetTopUserXps(ctx.Guild.Id, 1000) allCleanUsers = (await _service.GetTopUserXps(ctx.Guild.Id, 1000))
.Where(user => socketGuild.GetUser(user.UserId) is not null) .Where(user => socketGuild.GetUser(user.UserId) is not null)
.ToList(); .ToList();
} }
await Response() var res = opts.Clean
? Response()
.Paginated() .Paginated()
.PageItems<UserXpStats>(opts.Clean .Items(allCleanUsers)
? (curPage) => Task.FromResult<IEnumerable<UserXpStats>>(allCleanUsers.Skip(curPage * 9) : Response()
.Take(9) .Paginated()
.ToList()) .PageItems((curPage) => _service.GetUserXps(ctx.Guild.Id, curPage));
: (curPage) => Task.FromResult<IEnumerable<UserXpStats>>(_service.GetUserXps(ctx.Guild.Id, curPage)))
await res
.PageSize(9) .PageSize(9)
.CurrentPage(page) .CurrentPage(page)
.AddFooter(false)
.Page((users, curPage) => .Page((users, curPage) =>
{ {
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.server_leaderboard)).WithOkColor(); var embed = _sender.CreateEmbed().WithTitle(GetText(strs.server_leaderboard)).WithOkColor();
@@ -241,23 +241,33 @@ public partial class Xp : NadekoModule<XpService>
{ {
if (--page < 0 || page > 99) if (--page < 0 || page > 99)
return; return;
var users = _service.GetUserXps(page);
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.global_leaderboard)).WithOkColor(); await Response()
.Paginated()
.PageItems(async curPage => await _service.GetUserXps(curPage))
.PageSize(9)
.Page((users, curPage) =>
{
var embed = _sender.CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.global_leaderboard));
if (!users.Any()) if (!users.Any())
embed.WithDescription("-"); {
else embed.WithDescription("-");
{ return embed;
for (var i = 0; i < users.Length; i++) }
{
var user = users[i];
embed.AddField($"#{i + 1 + (page * 9)} {user.ToString()}",
$"{GetText(strs.level_x(new LevelStats(users[i].TotalXp).Level))} - {users[i].TotalXp}xp");
}
}
await Response().Embed(embed).SendAsync(); for (var i = 0; i < users.Count; i++)
{
var user = users[i];
embed.AddField($"#{i + 1 + (curPage * 9)} {user}",
$"{GetText(strs.level_x(new LevelStats(users[i].TotalXp).Level))} - {users[i].TotalXp}xp");
}
return embed;
})
.SendAsync();
} }
[Cmd] [Cmd]

View File

@@ -563,22 +563,23 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
uow.SaveChanges(); uow.SaveChanges();
} }
public List<UserXpStats> GetUserXps(ulong guildId, int page) public async Task<IReadOnlyCollection<UserXpStats>> GetUserXps(ulong guildId, int page)
{ {
using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return uow.Set<UserXpStats>().GetUsersFor(guildId, page); return await uow.Set<UserXpStats>().GetUsersFor(guildId, page);
} }
public List<UserXpStats> GetTopUserXps(ulong guildId, int count) public async Task<IReadOnlyCollection<UserXpStats>> GetTopUserXps(ulong guildId, int count)
{ {
using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return uow.Set<UserXpStats>().GetTopUserXps(guildId, count); return await uow.Set<UserXpStats>().GetTopUserXps(guildId, count);
} }
public DiscordUser[] GetUserXps(int page, int perPage = 9) public Task<IReadOnlyCollection<DiscordUser>> GetUserXps(int page, int perPage = 9)
{ {
using var uow = _db.GetDbContext(); using var uow = _db.GetDbContext();
return uow.Set<DiscordUser>().GetUsersXpLeaderboardFor(page, perPage); return uow.Set<DiscordUser>()
.GetUsersXpLeaderboardFor(page, perPage);
} }
public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type) public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type)
@@ -884,7 +885,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
var du = uow.GetOrCreateUser(user, set => set.Include(x => x.Club)); var du = uow.GetOrCreateUser(user, set => set.Include(x => x.Club));
var totalXp = du.TotalXp; var totalXp = du.TotalXp;
var globalRank = uow.Set<DiscordUser>().GetUserGlobalRank(user.Id); var globalRank = uow.Set<DiscordUser>().GetUserGlobalRank(user.Id);
var guildRank = uow.Set<UserXpStats>().GetUserGuildRanking(user.Id, user.GuildId); var guildRank = await uow.Set<UserXpStats>().GetUserGuildRanking(user.Id, user.GuildId);
var stats = uow.GetOrCreateUserXpStats(user.GuildId, user.Id); var stats = uow.GetOrCreateUserXpStats(user.GuildId, user.Id);
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();

View File

@@ -5,7 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.0.6</Version> <Version>5.0.7</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View File

@@ -58,7 +58,7 @@ public partial class ResponseBuilder
cb.WithButton(new ButtonBuilder() cb.WithButton(new ButtonBuilder()
.WithStyle(ButtonStyle.Primary) .WithStyle(ButtonStyle.Primary)
.WithCustomId(BUTTON_RIGHT) .WithCustomId(BUTTON_RIGHT)
.WithDisabled(lastPage == 0 || currentPage >= lastPage) .WithDisabled(lastPage is not null && (lastPage == 0 || currentPage >= lastPage))
.WithEmote(InteractionHelpers.ArrowRight)); .WithEmote(InteractionHelpers.ArrowRight));
return cb; return cb;

View File

@@ -1,5 +1,6 @@
using NadekoBot.Common.Configs; using NadekoBot.Common.Configs;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
using System.Collections.ObjectModel;
namespace NadekoBot.Extensions; namespace NadekoBot.Extensions;
@@ -360,7 +361,6 @@ public sealed partial class ResponseBuilder
public PaginatedResponseBuilder Paginated() public PaginatedResponseBuilder Paginated()
=> new(this); => new(this);
} }
public class PaginatedResponseBuilder public class PaginatedResponseBuilder
@@ -376,7 +376,7 @@ public class PaginatedResponseBuilder
=> new SourcedPaginatedResponseBuilder<T>(_builder) => new SourcedPaginatedResponseBuilder<T>(_builder)
.Items(items); .Items(items);
public SourcedPaginatedResponseBuilder<T> PageItems<T>(Func<int, Task<IEnumerable<T>>> items) public SourcedPaginatedResponseBuilder<T> PageItems<T>(Func<int, Task<IReadOnlyCollection<T>>> items)
=> new SourcedPaginatedResponseBuilder<T>(_builder) => new SourcedPaginatedResponseBuilder<T>(_builder)
.PageItems(items); .PageItems(items);
} }
@@ -390,14 +390,14 @@ public sealed class SourcedPaginatedResponseBuilder<T> : PaginatedResponseBuilde
return Task.FromResult<EmbedBuilder>(new()); return Task.FromResult<EmbedBuilder>(new());
}; };
public Func<int, Task<IEnumerable<T>>> ItemsFunc { get; set; } = static delegate public Func<int, Task<IReadOnlyCollection<T>>> ItemsFunc { get; set; } = static delegate
{ {
return Task.FromResult(Enumerable.Empty<T>()); return Task.FromResult<IReadOnlyCollection<T>>(ReadOnlyCollection<T>.Empty);
}; };
public Func<int, Task<SimpleInteractionBase>>? InteractionFunc { get; private set; } public Func<int, Task<SimpleInteractionBase>>? InteractionFunc { get; private set; }
public int Elems { get; private set; } = 1; public int? Elems { get; private set; } = 1;
public int ItemsPerPage { get; private set; } = 9; public int ItemsPerPage { get; private set; } = 9;
public bool AddPaginatedFooter { get; private set; } = true; public bool AddPaginatedFooter { get; private set; } = true;
public bool IsEphemeral { get; private set; } public bool IsEphemeral { get; private set; }
@@ -413,7 +413,7 @@ public sealed class SourcedPaginatedResponseBuilder<T> : PaginatedResponseBuilde
{ {
items = col; items = col;
Elems = col.Count; Elems = col.Count;
ItemsFunc = (i) => Task.FromResult(items.Skip(i * ItemsPerPage).Take(ItemsPerPage)); ItemsFunc = (i) => Task.FromResult(items.Skip(i * ItemsPerPage).Take(ItemsPerPage).ToArray() as IReadOnlyCollection<T>);
return this; return this;
} }
@@ -423,8 +423,9 @@ public sealed class SourcedPaginatedResponseBuilder<T> : PaginatedResponseBuilde
return this; return this;
} }
public SourcedPaginatedResponseBuilder<T> PageItems(Func<int, Task<IEnumerable<T>>> func) public SourcedPaginatedResponseBuilder<T> PageItems(Func<int, Task<IReadOnlyCollection<T>>> func)
{ {
Elems = null;
ItemsFunc = func; ItemsFunc = func;
return this; return this;
} }

View File

@@ -167,7 +167,8 @@ public static class Extensions
{ {
if (lastPage is not null) if (lastPage is not null)
return embed.WithFooter($"{curPage + 1} / {lastPage + 1}"); return embed.WithFooter($"{curPage + 1} / {lastPage + 1}");
return embed.WithFooter(curPage.ToString());
return embed.WithFooter((curPage + 1).ToString());
} }
// public static EmbedBuilder WithOkColor(this EmbedBuilder eb) // public static EmbedBuilder WithOkColor(this EmbedBuilder eb)