Compare commits

..

4 Commits
5.0.6 ... 5.0.7

Author SHA1 Message Date
Kwoth
a52a246982 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
2024-05-15 13:44:37 +00:00
Kwoth
803fe5db2f Merge branch 'v5' into 'v5'
Updating Dockerfile

See merge request Kwoth/nadekobot!322
2024-05-15 07:29:08 +00:00
Plarpoon
39980a1374 Updating Dockerfile 2024-05-15 07:29:08 +00:00
Kwoth
d2c4af273b dev: Version upped 2024-05-15 07:13:28 +00:00
14 changed files with 201 additions and 138 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
## [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

View File

@@ -1,16 +1,25 @@
# Use the .NET 8.0 SDK as the base image for the build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source
# Copy the .csproj files for each project
COPY src/Nadeko.Medusa/*.csproj src/Nadeko.Medusa/
COPY src/NadekoBot/*.csproj src/NadekoBot/
COPY src/NadekoBot.Coordinator/*.csproj src/NadekoBot.Coordinator/
COPY src/NadekoBot.Generators/*.csproj src/NadekoBot.Generators/
COPY src/NadekoBot.Voice/*.csproj src/NadekoBot.Voice/
COPY NuGet.Config ./
# Restore the dependencies for the NadekoBot project
RUN dotnet restore src/NadekoBot/
# Copy the rest of the source code
COPY . .
# Set the working directory to the NadekoBot project
WORKDIR /source/src/NadekoBot
# Build and publish the NadekoBot project, then clean up unnecessary files
RUN set -xe; \
dotnet --version; \
dotnet publish -c Release -o /app --no-restore; \
@@ -19,28 +28,33 @@ RUN set -xe; \
find /app -type f -exec chmod -x {} \; ;\
chmod +x /app/NadekoBot
# final stage/image
# Use the .NET 8.0 runtime as the base image for the final stage
FROM mcr.microsoft.com/dotnet/runtime:8.0
WORKDIR /app
# Create a new user, install dependencies, and set up sudoers file
RUN set -xe; \
useradd -m nadeko; \
apt-get update; \
apt-get install -y --no-install-recommends libopus0 libsodium23 libsqlite3-0 curl ffmpeg python3 sudo; \
update-alternatives --install /usr/local/bin/python python /usr/bin/python3.9 1; \
apt-get install -y --no-install-recommends libsqlite3-0 curl ffmpeg sudo python3; \
echo 'Defaults>nadeko env_keep+="ASPNETCORE_* DOTNET_* NadekoBot_* shard_id total_shards TZ"' > /etc/sudoers.d/nadeko; \
curl -Lo /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp; \
chmod a+rx /usr/local/bin/yt-dlp; \
apt-get autoremove -y; \
apt-get autoclean -y
# Copy the built application and the entrypoint script from the build stage
COPY --from=build /app ./
COPY docker-entrypoint.sh /usr/local/sbin
# Set environment variables
ENV shard_id=0
ENV total_shards=1
ENV NadekoBot__creds=/app/data/creds.yml
# Define the data directory as a volume
VOLUME [ "/app/data" ]
# Set the entrypoint and default command
ENTRYPOINT [ "/usr/local/sbin/docker-entrypoint.sh" ]
CMD dotnet NadekoBot.dll "$shard_id" "$total_shards"
CMD dotnet NadekoBot.dll "$shard_id" "$total_shards"

View File

@@ -20,48 +20,48 @@ public static class DiscordUserExtensions
string discrim,
string avatarId)
=> ctx.GetTable<DiscordUser>()
.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<DiscordUser>()
.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<DiscordUser> 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<DiscordUser> users, int page, int perPage)
=> users.AsQueryable().OrderByDescending(x => x.TotalXp).Skip(page * perPage).Take(perPage).AsEnumerable()
.ToArray();
public static async Task<IReadOnlyCollection<DiscordUser>> GetUsersXpLeaderboardFor(this DbSet<DiscordUser> users, int page, int perPage)
=> await users.ToLinqToDBTable()
.OrderByDescending(x => x.TotalXp)
.Skip(page * perPage)
.Take(perPage)
.ToArrayAsyncLinqToDB();
public static Task<List<DiscordUser>> GetTopRichest(
this DbSet<DiscordUser> 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<long> GetUserCurrencyAsync(this DbSet<DiscordUser> 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<DiscordUser> 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);
}

View File

@@ -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<UserXpStats> GetUsersFor(this DbSet<UserXpStats> 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<IReadOnlyCollection<UserXpStats>> GetUsersFor(
this DbSet<UserXpStats> 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<UserXpStats> GetTopUserXps(this DbSet<UserXpStats> 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<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> 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<UserXpStats> 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<int> GetUserGuildRanking(this DbSet<UserXpStats> 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<UserXpStats> xps, ulong userId, ulong guildId)
@@ -61,12 +60,11 @@ public static class UserXpExtensions
public static void ResetGuildXp(this DbSet<UserXpStats> xps, ulong guildId)
=> xps.Delete(x => x.GuildId == guildId);
public static async Task<LevelStats> GetLevelDataFor(this ITable<UserXpStats> 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);
}

View File

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

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)
{

View File

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

View File

@@ -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<FollowedStream>().ToList();
var all = uow.Set<FollowedStream>()
.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;
}

View File

@@ -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<XpService>
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<UserXpStats>(opts.Clean
? (curPage) => Task.FromResult<IEnumerable<UserXpStats>>(allCleanUsers.Skip(curPage * 9)
.Take(9)
.ToList())
: (curPage) => Task.FromResult<IEnumerable<UserXpStats>>(_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<XpService>
{
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]

View File

@@ -563,22 +563,23 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
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();
return uow.Set<UserXpStats>().GetUsersFor(guildId, page);
await using var uow = _db.GetDbContext();
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();
return uow.Set<UserXpStats>().GetTopUserXps(guildId, count);
await using var uow = _db.GetDbContext();
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();
return uow.Set<DiscordUser>().GetUsersXpLeaderboardFor(page, perPage);
return uow.Set<DiscordUser>()
.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<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);
await uow.SaveChangesAsync();

View File

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

View File

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

View File

@@ -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<T>(_builder)
.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)
.PageItems(items);
}
@@ -390,14 +390,14 @@ public sealed class SourcedPaginatedResponseBuilder<T> : PaginatedResponseBuilde
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 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<T> : 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<T>);
return this;
}
public SourcedPaginatedResponseBuilder<T> TotalElements(int i)
{
Elems = i;
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;
return this;
}

View File

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