diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f5606e6..e99ab90cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ 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 ### Changed diff --git a/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs b/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs index 5965d662b..c5db903c3 100644 --- a/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs +++ b/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs @@ -20,48 +20,48 @@ public static class DiscordUserExtensions string discrim, string avatarId) => ctx.GetTable() - .InsertOrUpdate( - () => new() - { - UserId = userId, - Username = username, - Discriminator = discrim, - AvatarId = avatarId, - TotalXp = 0, - CurrencyAmount = 0 - }, - old => new() - { - Username = username, - Discriminator = discrim, - AvatarId = avatarId - }, - () => new() - { - UserId = userId - }); + .InsertOrUpdate( + () => new() + { + UserId = userId, + Username = username, + Discriminator = discrim, + AvatarId = avatarId, + TotalXp = 0, + CurrencyAmount = 0 + }, + old => new() + { + Username = username, + Discriminator = discrim, + AvatarId = avatarId + }, + () => new() + { + UserId = userId + }); public static Task EnsureUserCreatedAsync( this DbContext ctx, ulong userId) => ctx.GetTable() - .InsertOrUpdateAsync( - () => new() - { - UserId = userId, - Username = "Unknown", - Discriminator = "????", - AvatarId = string.Empty, - TotalXp = 0, - CurrencyAmount = 0 - }, - old => new() - { - }, - () => new() - { - UserId = userId - }); + .InsertOrUpdateAsync( + () => new() + { + UserId = userId, + Username = "Unknown", + Discriminator = "????", + AvatarId = string.Empty, + TotalXp = 0, + CurrencyAmount = 0 + }, + old => new() + { + }, + () => new() + { + UserId = userId + }); //temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown public static DiscordUser GetOrCreateUser( @@ -83,25 +83,29 @@ public static class DiscordUserExtensions public static int GetUserGlobalRank(this DbSet users, ulong id) => users.AsQueryable() - .Where(x => x.TotalXp - > users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault()) - .Count() + .Where(x => x.TotalXp + > users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault()) + .Count() + 1; - public static DiscordUser[] GetUsersXpLeaderboardFor(this DbSet users, int page, int perPage) - => users.AsQueryable().OrderByDescending(x => x.TotalXp).Skip(page * perPage).Take(perPage).AsEnumerable() - .ToArray(); + public static async Task> GetUsersXpLeaderboardFor(this DbSet users, int page, int perPage) + => await users.ToLinqToDBTable() + .OrderByDescending(x => x.TotalXp) + .Skip(page * perPage) + .Take(perPage) + .ToArrayAsyncLinqToDB(); public static Task> GetTopRichest( this DbSet users, ulong botId, - int page = 0, int perPage = 9) + int page = 0, + int perPage = 9) => users.AsQueryable() - .Where(c => c.CurrencyAmount > 0 && botId != c.UserId) - .OrderByDescending(c => c.CurrencyAmount) - .Skip(page * perPage) - .Take(perPage) - .ToListAsyncLinqToDB(); + .Where(c => c.CurrencyAmount > 0 && botId != c.UserId) + .OrderByDescending(c => c.CurrencyAmount) + .Skip(page * perPage) + .Take(perPage) + .ToListAsyncLinqToDB(); public static async Task GetUserCurrencyAsync(this DbSet users, ulong userId) => (await users.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId))?.CurrencyAmount ?? 0; @@ -118,8 +122,8 @@ public static class DiscordUserExtensions public static decimal GetTopOnePercentCurrency(this DbSet users, ulong botId) => users.AsQueryable() - .Where(x => x.UserId != botId) - .OrderByDescending(x => x.CurrencyAmount) - .Take(users.Count() / 100 == 0 ? 1 : users.Count() / 100) - .Sum(x => x.CurrencyAmount); + .Where(x => x.UserId != botId) + .OrderByDescending(x => x.CurrencyAmount) + .Take(users.Count() / 100 == 0 ? 1 : users.Count() / 100) + .Sum(x => x.CurrencyAmount); } \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/UserXpExtensions.cs b/src/NadekoBot/Db/Extensions/UserXpExtensions.cs index 53e03117b..6fa8f77bb 100644 --- a/src/NadekoBot/Db/Extensions/UserXpExtensions.cs +++ b/src/NadekoBot/Db/Extensions/UserXpExtensions.cs @@ -2,7 +2,6 @@ using LinqToDB; using LinqToDB.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; - using NadekoBot.Db.Models; namespace NadekoBot.Db; @@ -27,33 +26,33 @@ public static class UserXpExtensions return usr; } - public static List GetUsersFor(this DbSet xps, ulong guildId, int page) - => xps.AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .OrderByDescending(x => x.Xp + x.AwardedXp) - .Skip(page * 9) - .Take(9) - .ToList(); + public static async Task> GetUsersFor( + this DbSet xps, + ulong guildId, + int page) + => await xps.ToLinqToDBTable() + .Where(x => x.GuildId == guildId) + .OrderByDescending(x => x.Xp + x.AwardedXp) + .Skip(page * 9) + .Take(9) + .ToArrayAsyncLinqToDB(); - public static List GetTopUserXps(this DbSet xps, ulong guildId, int count) - => xps.AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .OrderByDescending(x => x.Xp + x.AwardedXp) - .Take(count) - .ToList(); + public static async Task> GetTopUserXps(this DbSet xps, ulong guildId, int count) + => await xps.ToLinqToDBTable() + .Where(x => x.GuildId == guildId) + .OrderByDescending(x => x.Xp + x.AwardedXp) + .Take(count) + .ToListAsyncLinqToDB(); - public static int GetUserGuildRanking(this DbSet xps, ulong userId, ulong guildId) - => xps.AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId - && x.Xp + x.AwardedXp - > xps.AsQueryable() - .Where(y => y.UserId == userId && y.GuildId == guildId) - .Select(y => y.Xp + y.AwardedXp) - .FirstOrDefault()) - .Count() + public static async Task GetUserGuildRanking(this DbSet xps, ulong userId, ulong guildId) + => await xps.ToLinqToDBTable() + .Where(x => x.GuildId == guildId + && x.Xp + x.AwardedXp + > xps.AsQueryable() + .Where(y => y.UserId == userId && y.GuildId == guildId) + .Select(y => y.Xp + y.AwardedXp) + .FirstOrDefault()) + .CountAsyncLinqToDB() + 1; public static void ResetGuildUserXp(this DbSet xps, ulong userId, ulong guildId) @@ -61,12 +60,11 @@ public static class UserXpExtensions public static void ResetGuildXp(this DbSet xps, ulong guildId) => xps.Delete(x => x.GuildId == guildId); - + public static async Task GetLevelDataFor(this ITable userXp, ulong guildId, ulong userId) - => await userXp - .Where(x => x.GuildId == guildId && x.UserId == userId) - .FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs - ? new(uxs.Xp + uxs.AwardedXp) - : new(0); - + => await userXp + .Where(x => x.GuildId == guildId && x.UserId == userId) + .FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs + ? new(uxs.Xp + uxs.AwardedXp) + : new(0); } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/GuildConfig.cs b/src/NadekoBot/Db/Models/GuildConfig.cs index 27501870a..18b820a65 100644 --- a/src/NadekoBot/Db/Models/GuildConfig.cs +++ b/src/NadekoBot/Db/Models/GuildConfig.cs @@ -3,6 +3,7 @@ namespace NadekoBot.Db.Models; public class GuildConfig : DbEntity { + // public bool Keep { get; set; } public ulong GuildId { get; set; } public string Prefix { get; set; } diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index c70d6e141..62a1f254d 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -766,7 +766,7 @@ public partial class Gambling : GamblingModule } - async Task> GetTopRichest(int curPage) + async Task> GetTopRichest(int curPage) { if (opts.Clean) { diff --git a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs index 98c8e2ede..bed9daf10 100644 --- a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs @@ -143,6 +143,10 @@ public partial class Searches if (--index < 0) 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)) { await Response().Confirm(strs.stream_not_following).SendAsync(); @@ -160,6 +164,10 @@ public partial class Searches [UserPerm(GuildPerm.ManageMessages)] 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); if (count == 0) diff --git a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs index 9f8ac251e..30de4ea64 100644 --- a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs @@ -294,6 +294,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor var msg = await _sender.Response(textChannel) .Embed(GetEmbed(fs.GuildId, stream, false)) .Text(message) + .Sanitize(false) .SendAsync(); // only cache the ids of channel/message pairs @@ -615,7 +616,9 @@ public sealed class StreamNotificationService : INService, IReadyExecutor { using var uow = _db.GetDbContext(); - var all = uow.Set().ToList(); + var all = uow.Set() + .Where(x => x.GuildId == guildId) + .ToList(); if (all.Count == 0) return 0; @@ -623,6 +626,19 @@ public sealed class StreamNotificationService : INService, IReadyExecutor all.ForEach(x => x.Message = message); 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; } diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index 4dd1de798..07c74d0e4 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -1,7 +1,6 @@ #nullable disable warnings using NadekoBot.Modules.Xp.Services; using NadekoBot.Db.Models; -using NadekoBot.Db; using NadekoBot.Modules.Patronage; namespace NadekoBot.Modules.Xp; @@ -191,21 +190,22 @@ public partial class Xp : NadekoModule await ctx.Channel.TriggerTypingAsync(); await _tracker.EnsureUsersDownloadedAsync(ctx.Guild); - allCleanUsers = _service.GetTopUserXps(ctx.Guild.Id, 1000) - .Where(user => socketGuild.GetUser(user.UserId) is not null) - .ToList(); + allCleanUsers = (await _service.GetTopUserXps(ctx.Guild.Id, 1000)) + .Where(user => socketGuild.GetUser(user.UserId) is not null) + .ToList(); } - await Response() + var res = opts.Clean + ? Response() .Paginated() - .PageItems(opts.Clean - ? (curPage) => Task.FromResult>(allCleanUsers.Skip(curPage * 9) - .Take(9) - .ToList()) - : (curPage) => Task.FromResult>(_service.GetUserXps(ctx.Guild.Id, curPage))) + .Items(allCleanUsers) + : Response() + .Paginated() + .PageItems((curPage) => _service.GetUserXps(ctx.Guild.Id, curPage)); + + await res .PageSize(9) .CurrentPage(page) - .AddFooter(false) .Page((users, curPage) => { var embed = _sender.CreateEmbed().WithTitle(GetText(strs.server_leaderboard)).WithOkColor(); @@ -241,23 +241,33 @@ public partial class Xp : NadekoModule { if (--page < 0 || page > 99) 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()) - embed.WithDescription("-"); - else - { - 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"); - } - } + if (!users.Any()) + { + embed.WithDescription("-"); + return embed; + } - 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] diff --git a/src/NadekoBot/Modules/Xp/XpService.cs b/src/NadekoBot/Modules/Xp/XpService.cs index 61a9cbc82..ddee73c2e 100644 --- a/src/NadekoBot/Modules/Xp/XpService.cs +++ b/src/NadekoBot/Modules/Xp/XpService.cs @@ -563,22 +563,23 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand uow.SaveChanges(); } - public List GetUserXps(ulong guildId, int page) + public async Task> GetUserXps(ulong guildId, int page) { - using var uow = _db.GetDbContext(); - return uow.Set().GetUsersFor(guildId, page); + await using var uow = _db.GetDbContext(); + return await uow.Set().GetUsersFor(guildId, page); } - public List GetTopUserXps(ulong guildId, int count) + public async Task> GetTopUserXps(ulong guildId, int count) { - using var uow = _db.GetDbContext(); - return uow.Set().GetTopUserXps(guildId, count); + await using var uow = _db.GetDbContext(); + return await uow.Set().GetTopUserXps(guildId, count); } - public DiscordUser[] GetUserXps(int page, int perPage = 9) + public Task> GetUserXps(int page, int perPage = 9) { using var uow = _db.GetDbContext(); - return uow.Set().GetUsersXpLeaderboardFor(page, perPage); + return uow.Set() + .GetUsersXpLeaderboardFor(page, perPage); } 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 totalXp = du.TotalXp; var globalRank = uow.Set().GetUserGlobalRank(user.Id); - var guildRank = uow.Set().GetUserGuildRanking(user.Id, user.GuildId); + var guildRank = await uow.Set().GetUserGuildRanking(user.Id, user.GuildId); var stats = uow.GetOrCreateUserXpStats(user.GuildId, user.Id); await uow.SaveChangesAsync(); diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index c33194e66..8c8be7dff 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -5,7 +5,7 @@ enable true en - 5.0.6 + 5.0.7 $(MSBuildProjectDirectory) diff --git a/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs b/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs index e09151b66..91daf9845 100644 --- a/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs +++ b/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs @@ -58,7 +58,7 @@ public partial class ResponseBuilder cb.WithButton(new ButtonBuilder() .WithStyle(ButtonStyle.Primary) .WithCustomId(BUTTON_RIGHT) - .WithDisabled(lastPage == 0 || currentPage >= lastPage) + .WithDisabled(lastPage is not null && (lastPage == 0 || currentPage >= lastPage)) .WithEmote(InteractionHelpers.ArrowRight)); return cb; diff --git a/src/NadekoBot/_common/Sender/ResponseBuilder.cs b/src/NadekoBot/_common/Sender/ResponseBuilder.cs index 7eae8faf5..ed68d3ab1 100644 --- a/src/NadekoBot/_common/Sender/ResponseBuilder.cs +++ b/src/NadekoBot/_common/Sender/ResponseBuilder.cs @@ -1,5 +1,6 @@ using NadekoBot.Common.Configs; using NadekoBot.Db.Models; +using System.Collections.ObjectModel; namespace NadekoBot.Extensions; @@ -357,10 +358,9 @@ public sealed partial class ResponseBuilder fileName = name; return this; } - + public PaginatedResponseBuilder Paginated() => new(this); - } public class PaginatedResponseBuilder @@ -376,7 +376,7 @@ public class PaginatedResponseBuilder => new SourcedPaginatedResponseBuilder(_builder) .Items(items); - public SourcedPaginatedResponseBuilder PageItems(Func>> items) + public SourcedPaginatedResponseBuilder PageItems(Func>> items) => new SourcedPaginatedResponseBuilder(_builder) .PageItems(items); } @@ -390,14 +390,14 @@ public sealed class SourcedPaginatedResponseBuilder : PaginatedResponseBuilde return Task.FromResult(new()); }; - public Func>> ItemsFunc { get; set; } = static delegate + public Func>> ItemsFunc { get; set; } = static delegate { - return Task.FromResult(Enumerable.Empty()); + return Task.FromResult>(ReadOnlyCollection.Empty); }; public Func>? 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 bool AddPaginatedFooter { get; private set; } = true; public bool IsEphemeral { get; private set; } @@ -413,18 +413,19 @@ public sealed class SourcedPaginatedResponseBuilder : PaginatedResponseBuilde { items = col; 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); return this; } - + public SourcedPaginatedResponseBuilder TotalElements(int i) { Elems = i; return this; } - public SourcedPaginatedResponseBuilder PageItems(Func>> func) + public SourcedPaginatedResponseBuilder PageItems(Func>> func) { + Elems = null; ItemsFunc = func; return this; } diff --git a/src/NadekoBot/_common/_Extensions/Extensions.cs b/src/NadekoBot/_common/_Extensions/Extensions.cs index ed18aa1e6..cb519a48e 100644 --- a/src/NadekoBot/_common/_Extensions/Extensions.cs +++ b/src/NadekoBot/_common/_Extensions/Extensions.cs @@ -167,7 +167,8 @@ public static class Extensions { if (lastPage is not null) 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)