diff --git a/CHANGELOG.md b/CHANGELOG.md index 502fd7bea..a7e928b1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog. - type `.h .shopadd` for more info - Added `.stickyroles` Users leaving the server will have their roles saved to the database and reapplied if they rejoin within 30 days. - Giveaway commands - - `.ga start` starts the giveway with the specified duration and message (prize). You may have up to 5 giveaways on the server at once + - `.ga start ` starts the giveway with the specified duration and message (prize). You may have up to 5 giveaways on the server at once - `.ga end ` prematurely ends the giveaway and selects a winner - `.ga cancel ` cancels the giveaway and doesn't select the winner - `.ga list` lists active giveaways on the current server @@ -37,6 +37,8 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog. - You can now configure shop sale cut in `gambling.yml` - Added a page parameter to `.feedlist` - Added seconds/sec/s to .convert command +- Added `.prunecancel` to cancel an active prune +- Added progress reporting when using `.prune`. The bot will periodically update on how many messages have been deleted ### Changed @@ -46,6 +48,7 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog. - You can now target a different channel with .repeat, for example `.repeat #some-other 1h Hello every hour` - `.cmds `, `.cmds AllGuildConfigs { get; private set; } - private IKernel Services { get; set; } + private IContainer Services { get; set; } public bool IsReady { get; private set; } public int ShardId { get; set; } @@ -37,8 +35,7 @@ public sealed class Bot : IBot public Bot(int shardId, int? totalShards, string credPath = null) { - if (shardId < 0) - throw new ArgumentOutOfRangeException(nameof(shardId)); + ArgumentOutOfRangeException.ThrowIfLessThan(shardId, 0); ShardId = shardId; _credsProvider = new BotCredsProvider(totalShards, credPath); @@ -105,15 +102,17 @@ public sealed class Bot : IBot AllGuildConfigs = uow.Set().GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); } - var svcs = new StandardKernel(new NinjectSettings() - { - // ThrowOnGetServiceNotFound = true, - ActivationCacheDisabled = true, - }); + // var svcs = new StandardKernel(new NinjectSettings() + // { + // // ThrowOnGetServiceNotFound = true, + // ActivationCacheDisabled = true, + // }); + + var svcs = new Container(); // this is required in order for medusa unloading to work - svcs.Components.Remove(); - svcs.Components.Add(); + // svcs.Components.Remove(); + // svcs.Components.Add(); svcs.AddSingleton(_ => _credsProvider.GetCreds()); svcs.AddSingleton(_db); diff --git a/src/NadekoBot/Modules/Administration/Prune/PruneCommands.cs b/src/NadekoBot/Modules/Administration/Prune/PruneCommands.cs index 25bf2e9b8..46f65bc36 100644 --- a/src/NadekoBot/Modules/Administration/Prune/PruneCommands.cs +++ b/src/NadekoBot/Modules/Administration/Prune/PruneCommands.cs @@ -13,17 +13,25 @@ public partial class Administration public sealed class PruneOptions : INadekoCommandOptions { - [Option(shortName: 's', longName: "safe", Default = false, HelpText = "Whether pinned messages should be deleted.", Required = false)] + [Option(shortName: 's', + longName: "safe", + Default = false, + HelpText = "Whether pinned messages should be deleted.", + Required = false)] public bool Safe { get; set; } - - [Option(shortName: 'a', longName: "after", Default = null, HelpText = "Prune only messages after the specified message ID.", Required = false)] + + [Option(shortName: 'a', + longName: "after", + Default = null, + HelpText = "Prune only messages after the specified message ID.", + Required = false)] public ulong? After { get; set; } public void NormalizeOptions() { } } - + //deletes her own messages, no perm required [Cmd] [RequireContext(ContextType.Guild)] @@ -31,15 +39,27 @@ public partial class Administration public async Task Prune(params string[] args) { var (opts, _) = OptionsParser.ParseFrom(new PruneOptions(), args); - + var user = await ctx.Guild.GetCurrentUserAsync(); + var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync(); + var progress = GetProgressTracker(progressMsg); + if (opts.Safe) - await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id && !x.IsPinned, opts.After); + await _service.PruneWhere((ITextChannel)ctx.Channel, + 100, + x => x.Author.Id == user.Id && !x.IsPinned, + progress, + opts.After); else - await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id, opts.After); - + await _service.PruneWhere((ITextChannel)ctx.Channel, + 100, + x => x.Author.Id == user.Id, + progress, + opts.After); + ctx.Message.DeleteAfter(3); + await progressMsg.DeleteAsync(); } // prune x @@ -54,15 +74,52 @@ public partial class Administration count++; if (count < 1) return; + if (count > 1000) count = 1000; - + var (opts, _) = OptionsParser.ParseFrom(new PruneOptions(), args); + var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync(); + var progress = GetProgressTracker(progressMsg); + if (opts.Safe) - await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => !x.IsPinned, opts.After); + await _service.PruneWhere((ITextChannel)ctx.Channel, + count, + x => !x.IsPinned && x.Id != progressMsg.Id, + progress, + opts.After); else - await _service.PruneWhere((ITextChannel)ctx.Channel, count, _ => true, opts.After); + await _service.PruneWhere((ITextChannel)ctx.Channel, + count, + x => x.Id != progressMsg.Id, + progress, + opts.After); + + await progressMsg.DeleteAsync(); + } + + private IProgress<(int, int)> GetProgressTracker(IUserMessage progressMsg) + { + var progress = new Progress<(int, int)>(async (x) => + { + var (deleted, total) = x; + try + { + await progressMsg.ModifyAsync(props => + { + props.Embed = _sender.CreateEmbed() + .WithPendingColor() + .WithDescription(GetText(strs.prune_progress(deleted, total))) + .Build(); + }); + } + catch + { + } + }); + + return progress; } //prune @user [x] @@ -94,21 +151,48 @@ public partial class Administration count = 1000; var (opts, _) = OptionsParser.ParseFrom(new PruneOptions(), args); - + + var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync(); + var progress = GetProgressTracker(progressMsg); + if (opts.Safe) { await _service.PruneWhere((ITextChannel)ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned, - opts.After); + progress, + opts.After + ); } else { await _service.PruneWhere((ITextChannel)ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks, - opts.After); + progress, + opts.After + ); } + + await progressMsg.DeleteAsync(); + } + + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(ChannelPerm.ManageMessages)] + [BotPerm(ChannelPerm.ManageMessages)] + public async Task PruneCancel() + { + var ok = await _service.CancelAsync(ctx.Guild.Id); + + if (!ok) + { + await Response().Error(strs.prune_not_found).SendAsync(); + return; + } + + + await Response().Confirm(strs.prune_cancelled).SendAsync(); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Prune/PruneService.cs b/src/NadekoBot/Modules/Administration/Prune/PruneService.cs index 100e14374..9feeee53e 100644 --- a/src/NadekoBot/Modules/Administration/Prune/PruneService.cs +++ b/src/NadekoBot/Modules/Administration/Prune/PruneService.cs @@ -4,21 +4,29 @@ namespace NadekoBot.Modules.Administration.Services; public class PruneService : INService { //channelids where prunes are currently occuring - private readonly ConcurrentHashSet _pruningGuilds = new(); + private readonly ConcurrentDictionary _pruningGuilds = new(); private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14); private readonly ILogCommandService _logService; public PruneService(ILogCommandService logService) => _logService = logService; - public async Task PruneWhere(ITextChannel channel, int amount, Func predicate, ulong? after = null) + public async Task PruneWhere( + ITextChannel channel, + int amount, + Func predicate, + IProgress<(int deleted, int total)> progress, + ulong? after = null + ) { ArgumentNullException.ThrowIfNull(channel, nameof(channel)); + var originalAmount = amount; if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount)); - if (!_pruningGuilds.Add(channel.GuildId)) + using var cancelSource = new CancellationTokenSource(); + if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource)) return; try @@ -26,16 +34,22 @@ public class PruneService : INService var now = DateTime.UtcNow; IMessage[] msgs; IMessage lastMessage = null; - var dled = await channel.GetMessagesAsync(50).FlattenAsync(); - - msgs = dled - .Where(predicate) - .Where(x => after is ulong a ? x.Id > a : true) - .Take(amount) - .ToArray(); - - while (amount > 0 && msgs.Any()) + + while (amount > 0 && !cancelSource.IsCancellationRequested) { + var dled = lastMessage is null + ? await channel.GetMessagesAsync(50).FlattenAsync() + : await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync(); + + msgs = dled + .Where(predicate) + .Where(x => after is not ulong a || x.Id > a) + .Take(amount) + .ToArray(); + + if (!msgs.Any()) + return; + lastMessage = msgs[^1]; var bulkDeletable = new List(); @@ -53,27 +67,17 @@ public class PruneService : INService if (bulkDeletable.Count > 0) { await channel.DeleteMessagesAsync(bulkDeletable); - await Task.Delay(2000); + amount -= msgs.Length; + progress.Report((originalAmount - amount, originalAmount)); + await Task.Delay(2000, cancelSource.Token); } foreach (var group in singleDeletable.Chunk(5)) { await group.Select(x => x.DeleteAsync()).WhenAll(); - await Task.Delay(5000); - } - - //this isn't good, because this still work as if i want to remove only specific user's messages from the last - //100 messages, Maybe this needs to be reduced by msgs.Length instead of 100 - amount -= 50; - if (amount > 0) - { - dled = await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync(); - - msgs = dled - .Where(predicate) - .Where(x => after is ulong a ? x.Id > a : true) - .Take(amount) - .ToArray(); + amount -= 5; + progress.Report((originalAmount - amount, originalAmount)); + await Task.Delay(5000, cancelSource.Token); } } } @@ -83,7 +87,16 @@ public class PruneService : INService } finally { - _pruningGuilds.TryRemove(channel.GuildId); + _pruningGuilds.TryRemove(channel.GuildId, out _); } } + + public async Task CancelAsync(ulong guildId) + { + if (!_pruningGuilds.TryRemove(guildId, out var source)) + return false; + + await source.CancelAsync(); + return true; + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 354b86db2..be182bca0 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -110,10 +110,10 @@ public sealed partial class Music : NadekoModule try { var embed = _sender.CreateEmbed() - .WithOkColor() - .WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL) - .WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ") - .WithFooter(trackInfo.Platform.ToString()); + .WithOkColor() + .WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL) + .WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ") + .WithFooter(trackInfo.Platform.ToString()); if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail)) embed.WithThumbnailUrl(trackInfo.Thumbnail); @@ -315,11 +315,13 @@ public sealed partial class Music : NadekoModule desc = add + "\n" + desc; var embed = _sender.CreateEmbed() - .WithAuthor(GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)), - MUSIC_ICON_URL) - .WithDescription(desc) - .WithFooter($" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ") - .WithOkColor(); + .WithAuthor( + GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)), + MUSIC_ICON_URL) + .WithDescription(desc) + .WithFooter( + $" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ") + .WithOkColor(); return embed; } @@ -349,13 +351,21 @@ public sealed partial class Music : NadekoModule return; } - var resultsString = videos.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}").Join('\n'); - var msg = await Response().Confirm(resultsString).SendAsync(); + var embeds = videos.Select((x, i) => _sender.CreateEmbed() + .WithOkColor() + .WithThumbnailUrl(x.Thumbnail) + .WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}")) + .ToList(); + + var msg = await Response() + .Text(strs.queue_search_results) + .Embeds(embeds) + .SendAsync(); try { - var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id); + var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id, str => int.TryParse(str, out _)); if (input is null || !int.TryParse(input, out var index) || (index -= 1) < 0 || index >= videos.Count) { _logService.AddDeleteIgnore(msg.Id); @@ -415,10 +425,10 @@ public sealed partial class Music : NadekoModule } var embed = _sender.CreateEmbed() - .WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL) - .WithDescription(track.PrettyName()) - .WithFooter(track.PrettyInfo()) - .WithErrorColor(); + .WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL) + .WithDescription(track.PrettyName()) + .WithFooter(track.PrettyInfo()) + .WithErrorColor(); await _service.SendToOutputAsync(ctx.Guild.Id, embed); } @@ -583,11 +593,11 @@ public sealed partial class Music : NadekoModule } var embed = _sender.CreateEmbed() - .WithTitle(track.Title.TrimTo(65)) - .WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL) - .AddField(GetText(strs.from_position), $"#{from + 1}", true) - .AddField(GetText(strs.to_position), $"#{to + 1}", true) - .WithOkColor(); + .WithTitle(track.Title.TrimTo(65)) + .WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL) + .AddField(GetText(strs.from_position), $"#{from + 1}", true) + .AddField(GetText(strs.to_position), $"#{to + 1}", true) + .WithOkColor(); if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute)) embed.WithUrl(track.Url); @@ -642,12 +652,12 @@ public sealed partial class Music : NadekoModule return; var embed = _sender.CreateEmbed() - .WithOkColor() - .WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL) - .WithDescription(currentTrack.PrettyName()) - .WithThumbnailUrl(currentTrack.Thumbnail) - .WithFooter( - $"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}"); + .WithOkColor() + .WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL) + .WithDescription(currentTrack.PrettyName()) + .WithThumbnailUrl(currentTrack.Thumbnail) + .WithFooter( + $"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}"); await Response().Embed(embed).SendAsync(); } diff --git a/src/NadekoBot/Modules/Music/Services/IMusicService.cs b/src/NadekoBot/Modules/Music/Services/IMusicService.cs index 186bcfd6a..66c3405bd 100644 --- a/src/NadekoBot/Modules/Music/Services/IMusicService.cs +++ b/src/NadekoBot/Modules/Music/Services/IMusicService.cs @@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis; namespace NadekoBot.Modules.Music.Services; -public interface IMusicService : IPlaceholderProvider +public interface IMusicService { /// /// Leave voice channel in the specified guild if it's connected to one @@ -24,7 +24,7 @@ public interface IMusicService : IPlaceholderProvider Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer); Task SendToOutputAsync(ulong guildId, EmbedBuilder embed); Task PlayAsync(ulong guildId, ulong voiceChannelId); - Task> SearchVideosAsync(string query); + Task> SearchVideosAsync(string query); Task SetMusicChannelAsync(ulong guildId, ulong? channelId); Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType); Task SetVolumeAsync(ulong guildId, int value); diff --git a/src/NadekoBot/Modules/Music/Services/MusicService.cs b/src/NadekoBot/Modules/Music/Services/MusicService.cs index ad4ff5b9a..070636039 100644 --- a/src/NadekoBot/Modules/Music/Services/MusicService.cs +++ b/src/NadekoBot/Modules/Music/Services/MusicService.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; namespace NadekoBot.Modules.Music.Services; -public sealed class MusicService : IMusicService +public sealed class MusicService : IMusicService, IPlaceholderProvider { private readonly AyuVoiceStateService _voiceStateService; private readonly ITrackResolveProvider _trackResolveProvider; @@ -233,23 +233,23 @@ public sealed class MusicService : IMusicService return true; } - private async Task> SearchYtLoaderVideosAsync(string query) + private async Task> SearchYtLoaderVideosAsync(string query) { var result = await _ytLoader.LoadResultsAsync(query); - return result.Select(x => (x.Title, x.Url)).ToList(); + return result.Select(x => (x.Title, x.Url, x.Thumb)).ToList(); } - private async Task> SearchGoogleApiVideosAsync(string query) + private async Task> SearchGoogleApiVideosAsync(string query) { var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5); - return result.Select(x => (x.Name, x.Url)).ToList(); + return result.Select(x => (x.Name, x.Url, x.Thumbnail)).ToList(); } - public async Task> SearchVideosAsync(string query) + public async Task> SearchVideosAsync(string query) { try { - IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query); + IList<(string, string, string)> videos = await SearchYtLoaderVideosAsync(query); if (videos.Count > 0) return videos; } @@ -269,7 +269,7 @@ public sealed class MusicService : IMusicService ex.Message); } - return Array.Empty<(string, string)>(); + return Array.Empty<(string, string, string)>(); } private string GetText(ulong guildId, LocStr str) @@ -434,4 +434,4 @@ public sealed class MusicService : IMusicService } #endregion -} \ No newline at end of file +} diff --git a/src/NadekoBot/Modules/Music/Services/extractor/Misc.cs b/src/NadekoBot/Modules/Music/Services/extractor/Misc.cs index 2e8b270be..5c5aca5e4 100644 --- a/src/NadekoBot/Modules/Music/Services/extractor/Misc.cs +++ b/src/NadekoBot/Modules/Music/Services/extractor/Misc.cs @@ -47,6 +47,7 @@ public sealed partial class YtLoader { public abstract string Url { get; } public abstract string Title { get; } + public abstract string Thumb { get; } public abstract TimeSpan Duration { get; } } @@ -55,13 +56,15 @@ public sealed partial class YtLoader private const string BASE_YOUTUBE_URL = "https://youtube.com/watch?v="; public override string Url { get; } public override string Title { get; } + public override string Thumb { get; } public override TimeSpan Duration { get; } private readonly string _videoId; - public YtTrackInfo(string title, string videoId, TimeSpan duration) + public YtTrackInfo(string title, string videoId, string thumb, TimeSpan duration) { Title = title; + Thumb = thumb; Url = BASE_YOUTUBE_URL + videoId; Duration = duration; diff --git a/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs b/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs index 2e2b32334..7a581f383 100644 --- a/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs +++ b/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs @@ -5,7 +5,7 @@ using System.Text.Json; namespace NadekoBot.Modules.Music.Services; -public sealed partial class YtLoader +public sealed partial class YtLoader : INService { private static readonly byte[] _ytResultInitialData = Encoding.UTF8.GetBytes("var ytInitialData = "); private static readonly byte[] _ytResultJsonEnd = Encoding.UTF8.GetBytes(";<"); @@ -93,7 +93,7 @@ public sealed partial class YtLoader continue; var videoId = elem.GetProperty("videoId").GetString(); - // var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString(); + var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString(); var title = elem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString(); var durationString = elem.GetProperty("lengthText").GetProperty("simpleText").GetString(); @@ -106,7 +106,7 @@ public sealed partial class YtLoader continue; } - tracks.Add(new YtTrackInfo(title, videoId, duration)); + tracks.Add(new YtTrackInfo(title, videoId, thumb, duration)); if (tracks.Count >= 5) break; } diff --git a/src/NadekoBot/Modules/Utility/Todo/TodoService.cs b/src/NadekoBot/Modules/Utility/Todo/TodoService.cs index d2a99d065..57b3dd758 100644 --- a/src/NadekoBot/Modules/Utility/Todo/TodoService.cs +++ b/src/NadekoBot/Modules/Utility/Todo/TodoService.cs @@ -4,7 +4,7 @@ using NadekoBot.Db.Models; namespace NadekoBot.Modules.Utility; -public sealed class TodoService +public sealed class TodoService : INService { private const int ARCHIVE_MAX_COUNT = 9; private const int TODO_MAX_COUNT = 27; diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index e89ccb8cc..ffa79cb51 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -166,9 +166,10 @@ public partial class Utility : NadekoModule return _sender.CreateEmbed().WithOkColor().WithDescription(GetText(strs.no_user_on_this_page)); return _sender.CreateEmbed() - .WithOkColor() - .WithTitle(GetText(strs.inrole_list(Format.Bold(role?.Name ?? "No Role"), roleUsers.Length))) - .WithDescription(string.Join("\n", pageUsers)); + .WithOkColor() + .WithTitle(GetText(strs.inrole_list(Format.Bold(role?.Name ?? "No Role"), + roleUsers.Length))) + .WithDescription(string.Join("\n", pageUsers)); }) .SendAsync(); } @@ -183,9 +184,14 @@ public partial class Utility : NadekoModule [RequireContext(ContextType.Guild)] public async Task CheckPerms(MeOrBot who = MeOrBot.Me) { - var builder = new StringBuilder(); var user = who == MeOrBot.Me ? (IGuildUser)ctx.User : ((SocketGuild)ctx.Guild).CurrentUser; var perms = user.GetPermissions((ITextChannel)ctx.Channel); + await SendPerms(perms); + } + + private async Task SendPerms(ChannelPermissions perms) + { + var builder = new StringBuilder(); foreach (var p in perms.GetType() .GetProperties() .Where(static p => @@ -199,6 +205,16 @@ public partial class Utility : NadekoModule await Response().Confirm(builder.ToString()).SendAsync(); } + // [Cmd] + // [RequireContext(ContextType.Guild)] + // [RequireUserPermission(GuildPermission.ManageRoles)] + // public async Task CheckPerms(SocketRole role, string perm = null) + // { + // ChannelPermissions. + // var perms = ((ITextChannel)ctx.Channel); + // await SendPerms(perms) + // } + [Cmd] [RequireContext(ContextType.Guild)] public async Task UserId([Leftover] IGuildUser target = null) @@ -305,29 +321,29 @@ public partial class Utility : NadekoModule await Response() .Embed(_sender.CreateEmbed() - .WithOkColor() - .WithAuthor($"NadekoBot v{StatsService.BotVersion}", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/avatar.png", - "https://nadekobot.readthedocs.io/en/latest/") - .AddField(GetText(strs.author), _stats.Author, true) - .AddField(GetText(strs.botid), _client.CurrentUser.Id.ToString(), true) - .AddField(GetText(strs.shard), - $"#{_client.ShardId} / {_creds.TotalShards}", - true) - .AddField(GetText(strs.commands_ran), _stats.CommandsRan.ToString(), true) - .AddField(GetText(strs.messages), - $"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)", - true) - .AddField(GetText(strs.memory), - FormattableString.Invariant($"{_stats.GetPrivateMemoryMegabytes():F2} MB"), - true) - .AddField(GetText(strs.owner_ids), ownerIds, true) - .AddField(GetText(strs.uptime), _stats.GetUptimeString("\n"), true) - .AddField(GetText(strs.presence), - GetText(strs.presence_txt(_coord.GetGuildCount(), - _stats.TextChannels, - _stats.VoiceChannels)), - true)) + .WithOkColor() + .WithAuthor($"NadekoBot v{StatsService.BotVersion}", + "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/avatar.png", + "https://nadekobot.readthedocs.io/en/latest/") + .AddField(GetText(strs.author), _stats.Author, true) + .AddField(GetText(strs.botid), _client.CurrentUser.Id.ToString(), true) + .AddField(GetText(strs.shard), + $"#{_client.ShardId} / {_creds.TotalShards}", + true) + .AddField(GetText(strs.commands_ran), _stats.CommandsRan.ToString(), true) + .AddField(GetText(strs.messages), + $"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)", + true) + .AddField(GetText(strs.memory), + FormattableString.Invariant($"{_stats.GetPrivateMemoryMegabytes():F2} MB"), + true) + .AddField(GetText(strs.owner_ids), ownerIds, true) + .AddField(GetText(strs.uptime), _stats.GetUptimeString("\n"), true) + .AddField(GetText(strs.presence), + GetText(strs.presence_txt(_coord.GetGuildCount(), + _stats.TextChannels, + _stats.VoiceChannels)), + true)) .SendAsync(); } @@ -695,9 +711,9 @@ public partial class Utility : NadekoModule if (!string.IsNullOrWhiteSpace(output)) { var eb = _sender.CreateEmbed() - .WithOkColor() - .AddField("Code", scriptText) - .AddField("Output", output.TrimTo(512)!); + .WithOkColor() + .AddField("Code", scriptText) + .AddField("Output", output.TrimTo(512)!); _ = Response().Embed(eb).SendAsync(); } diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 262940530..9c5fb2f55 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -54,10 +54,11 @@ - - + + + diff --git a/src/NadekoBot/Services/Impl/GoogleApiService.cs b/src/NadekoBot/Services/Impl/GoogleApiService.cs index 29432f9c3..8aa8b0dd1 100644 --- a/src/NadekoBot/Services/Impl/GoogleApiService.cs +++ b/src/NadekoBot/Services/Impl/GoogleApiService.cs @@ -93,7 +93,7 @@ public sealed partial class GoogleApiService : IGoogleApiService, INService return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId); } - public async Task> GetVideoInfosByKeywordAsync( + public async Task> GetVideoInfosByKeywordAsync( string keywords, int count = 1) { @@ -108,7 +108,10 @@ public sealed partial class GoogleApiService : IGoogleApiService, INService query.Q = keywords; query.Type = "video"; return (await query.ExecuteAsync()).Items.Select(i - => (i.Snippet.Title.TrimTo(50), i.Id.VideoId, "https://www.youtube.com/watch?v=" + i.Id.VideoId)); + => (i.Snippet.Title.TrimTo(50), + i.Id.VideoId, + "https://www.youtube.com/watch?v=" + i.Id.VideoId, + i.Snippet.Thumbnails.High.Url)); } public Task ShortenUrl(Uri url) diff --git a/src/NadekoBot/_common/Abstractions/Extensions/StringExtensions.cs b/src/NadekoBot/_common/Abstractions/Extensions/StringExtensions.cs index 94be111fa..bc17d154e 100644 --- a/src/NadekoBot/_common/Abstractions/Extensions/StringExtensions.cs +++ b/src/NadekoBot/_common/Abstractions/Extensions/StringExtensions.cs @@ -32,7 +32,7 @@ public static class StringExtensions { if (hideDots) { - return str?.Substring(0, maxLength); + return str?.Substring(0, Math.Min(str?.Length ?? 0, maxLength)); } if (str is null || str.Length <= maxLength) diff --git a/src/NadekoBot/_common/Medusa/Common/MedusaLoaderService.cs b/src/NadekoBot/_common/Medusa/Common/MedusaLoaderService.cs index 9ffb4acd7..633b24197 100644 --- a/src/NadekoBot/_common/Medusa/Common/MedusaLoaderService.cs +++ b/src/NadekoBot/_common/Medusa/Common/MedusaLoaderService.cs @@ -1,4 +1,5 @@ using Discord.Commands.Builders; +using DryIoc; using Microsoft.Extensions.DependencyInjection; using Nadeko.Common.Medusa; using Nadeko.Medusa.Adapters; @@ -20,7 +21,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, private readonly IBehaviorHandler _behHandler; private readonly IPubSub _pubSub; private readonly IMedusaConfigService _medusaConfig; - private readonly IKernel _kernel; + private readonly IContainer _kernel; private readonly ConcurrentDictionary _resolved = new(); private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); @@ -34,7 +35,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, public MedusaLoaderService( CommandService cmdService, - IKernel kernel, + IContainer kernel, IBehaviorHandler behHandler, IPubSub pubSub, IMedusaConfigService medusaConfig) @@ -337,14 +338,17 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, // load services ninjectModule = new MedusaNinjectModule(a, safeName); - _kernel.Load(ninjectModule); + + // todo medusa won't work, uncomment + // _kernel.Load(ninjectModule); var sis = LoadSneksFromAssembly(safeName, a); typeReaders = LoadTypeReadersFromAssembly(a, strings); if (sis.Count == 0) { - _kernel.Unload(safeName); + // todo uncomment + // _kernel.Unload(safeName); return false; } @@ -604,7 +608,8 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, var km = lsi.KernelModule; lsi.KernelModule = null!; - _kernel.Unload(km.Name); + // todo uncomment + // _kernel.Unload(km.Name); if (km is IDisposable d) d.Dispose(); diff --git a/src/NadekoBot/_common/NadekoModule.cs b/src/NadekoBot/_common/NadekoModule.cs index 77a2cd4d3..8a3a22d81 100644 --- a/src/NadekoBot/_common/NadekoModule.cs +++ b/src/NadekoBot/_common/NadekoModule.cs @@ -61,7 +61,7 @@ public abstract class NadekoModule : ModuleBase } // TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); ? - public async Task GetUserInputAsync(ulong userId, ulong channelId) + public async Task GetUserInputAsync(ulong userId, ulong channelId, Func validate = null) { var userInputTask = new TaskCompletionSource(); var dsc = (DiscordSocketClient)ctx.Client; @@ -89,6 +89,9 @@ public abstract class NadekoModule : ModuleBase || userMsg.Channel.Id != channelId) return Task.CompletedTask; + if (validate is not null && !validate(arg.Content)) + return Task.CompletedTask; + if (userInputTask.TrySetResult(arg.Content)) userMsg.DeleteAfter(1); diff --git a/src/NadekoBot/_common/NinjectIKernelExtensions.cs b/src/NadekoBot/_common/NinjectIKernelExtensions.cs index fa26f243f..beaaa199b 100644 --- a/src/NadekoBot/_common/NinjectIKernelExtensions.cs +++ b/src/NadekoBot/_common/NinjectIKernelExtensions.cs @@ -1,43 +1,51 @@ -using Ninject; +using DryIoc; namespace NadekoBot.Extensions; -public static class NinjectIKernelExtensions +public static class DryIocExtensions { - public static IKernel AddSingleton(this IKernel kernel) + public static IContainer AddSingleton(this IContainer container) + where TImpl : TSvc { - kernel.Bind().ToSelf().InSingletonScope(); - return kernel; + container.Register(Reuse.Singleton); + + return container; + } + + public static IContainer AddSingleton(this IContainer container, TImpl obj) + where TImpl : TSvc + { + container.RegisterInstance(obj); + + return container; } - public static IKernel AddSingleton(this IKernel kernel) - where TImpl : TInterface + public static IContainer AddSingleton(this IContainer container, Func factory) + where TImpl : TSvc { - kernel.Bind().To().InSingletonScope(); - return kernel; + container.RegisterDelegate(factory, Reuse.Singleton); + + return container; } - public static IKernel AddSingleton(this IKernel kernel, TImpl obj) - => kernel.AddSingleton(obj); - - public static IKernel AddSingleton(this IKernel kernel, TImpl obj) - where TImpl : TInterface + public static IContainer AddSingleton(this IContainer container) { - kernel.Bind().ToConstant(obj).InSingletonScope(); - return kernel; + container.Register(Reuse.Singleton); + + return container; } - public static IKernel AddSingleton( - this IKernel kernel, - Func factory) - where TImpl : TInterface + public static IContainer AddSingleton(this IContainer container, TImpl obj) { - kernel.Bind().ToMethod(factory).InSingletonScope(); - return kernel; - } + container.RegisterInstance(obj); - public static IKernel AddSingleton( - this IKernel kernel, - Func factory) - => kernel.AddSingleton(factory); + return container; + } + + public static IContainer AddSingleton(this IContainer container, Func factory) + { + container.RegisterDelegate(factory); + + return container; + } } \ No newline at end of file diff --git a/src/NadekoBot/_common/ServiceCollectionExtensions.cs b/src/NadekoBot/_common/ServiceCollectionExtensions.cs index ab2e32d6a..f86704db8 100644 --- a/src/NadekoBot/_common/ServiceCollectionExtensions.cs +++ b/src/NadekoBot/_common/ServiceCollectionExtensions.cs @@ -1,9 +1,8 @@ +using DryIoc; using Microsoft.Extensions.DependencyInjection; using NadekoBot.Modules.Music; using NadekoBot.Modules.Music.Resolvers; using NadekoBot.Modules.Music.Services; -using Ninject; -using Ninject.Extensions.Conventions; using Ninject.Extensions.Conventions.Syntax; using StackExchange.Redis; using System.Net; @@ -15,92 +14,103 @@ namespace NadekoBot.Extensions; public static class ServiceCollectionExtensions { - public static IKernel AddBotStringsServices(this IKernel kernel, BotCacheImplemenation botCache) + public static IContainer AddBotStringsServices(this IContainer svcs, BotCacheImplemenation botCache) { if (botCache == BotCacheImplemenation.Memory) { - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); + svcs.AddSingleton(); + svcs.AddSingleton(); } else { - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); + svcs.AddSingleton(); + svcs.AddSingleton(); } - return kernel; + svcs.AddSingleton(); + + return svcs; } - public static IKernel AddConfigServices(this IKernel kernel, Assembly a) + public static IContainer AddConfigServices(this IContainer kernel, Assembly a) { - kernel.Bind(x => + // kernel.RegisterMany([typeof(ConfigServiceBase<>)]); + + foreach (var type in a.GetTypes() + .Where(x => !x.IsAbstract && x.IsAssignableToGenericType(typeof(ConfigServiceBase<>)))) { - var configs = x.From(a) - .SelectAllClasses() - .Where(f => f.IsAssignableToGenericType(typeof(ConfigServiceBase<>))); + kernel.RegisterMany([type], + getServiceTypes: type => type.GetImplementedTypes(ReflectionTools.AsImplementedType.SourceType), + getImplFactory: type => ReflectionFactory.Of(type, Reuse.Singleton)); + } - configs.BindToSelfWithInterfaces() - .Configure(c => c.InSingletonScope()); - }); + // + // kernel.Bind(x => + // { + // var configs = x.From(a) + // .SelectAllClasses() + // .Where(f => f.IsAssignableToGenericType(typeof(ConfigServiceBase<>))); + // + // configs.BindToSelfWithInterfaces() + // .Configure(c => c.InSingletonScope()); + // }); return kernel; } - public static IKernel AddConfigMigrators(this IKernel kernel, Assembly a) + public static IContainer AddConfigMigrators(this IContainer kernel, Assembly a) => kernel.AddSealedSubclassesOf(typeof(IConfigMigrator), a); - public static IKernel AddMusic(this IKernel kernel) + public static IContainer AddMusic(this IContainer kernel) { - kernel.Bind() - .To() - .InSingletonScope(); + kernel.RegisterMany(Reuse.Singleton); - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); - // kernel.Bind().ToSelf().InSingletonScope(); + kernel.AddSingleton(); + kernel.AddSingleton(); + kernel.AddSingleton(); + kernel.AddSingleton(); + kernel.AddSingleton(); return kernel; } - public static IKernel AddSealedSubclassesOf(this IKernel kernel, Type baseType, Assembly a) + public static IContainer AddSealedSubclassesOf(this IContainer cont, Type baseType, Assembly a) { - kernel.Bind(x => + var classes = a.GetExportedTypes() + .Where(x => x.IsClass && !x.IsAbstract && x.IsPublic) + .Where(x => x.IsNested && baseType.IsAssignableFrom(x)); + + foreach (var c in classes) { - var classes = x.From(a) - .SelectAllClasses() - .Where(c => c.IsPublic && c.IsNested && baseType.IsAssignableFrom(baseType)); + cont.RegisterMany([c], Reuse.Singleton); + // var inters = c.GetInterfaces(); - classes.BindToSelfWithInterfaces().Configure(x => x.InSingletonScope()); - }); + // cont.RegisterMany(inters, c); + } - return kernel; + return cont; } - public static IKernel AddCache(this IKernel kernel, IBotCredentials creds) + public static IContainer AddCache(this IContainer cont, IBotCredentials creds) { if (creds.BotCache == BotCacheImplemenation.Redis) { var conf = ConfigurationOptions.Parse(creds.RedisOptions); - kernel.Bind().ToConstant(ConnectionMultiplexer.Connect(conf)).InSingletonScope(); - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); + cont.AddSingleton(ConnectionMultiplexer.Connect(conf)); + cont.AddSingleton(); + cont.AddSingleton(); } else { - kernel.Bind().To().InSingletonScope(); - kernel.Bind().To().InSingletonScope(); + cont.AddSingleton(); + cont.AddSingleton(); } - return kernel + return cont .AddBotStringsServices(creds.BotCache); } - public static IKernel AddHttpClients(this IKernel kernel) + public static IContainer AddHttpClients(this IContainer kernel) { IServiceCollection svcs = new ServiceCollection(); svcs.AddHttpClient(); @@ -117,8 +127,8 @@ public static class ServiceCollectionExtensions }); var prov = svcs.BuildServiceProvider(); - kernel.Bind().ToMethod(_ => prov.GetRequiredService()); - kernel.Bind().ToMethod(_ => prov.GetRequiredService()); + kernel.RegisterDelegate(_ => prov.GetRequiredService()); + kernel.RegisterDelegate(_ => prov.GetRequiredService()); return kernel; } @@ -126,26 +136,39 @@ public static class ServiceCollectionExtensions public static IConfigureSyntax BindToSelfWithInterfaces(this IJoinExcludeIncludeBindSyntax matcher) => matcher.BindSelection((type, types) => types.Append(type)); - public static IKernel AddLifetimeServices(this IKernel kernel, Assembly a) + public static IContainer AddLifetimeServices(this IContainer kernel, Assembly a) { - kernel.Bind(scan => + Type[] types = + [ + typeof(IExecOnMessage), + typeof(IExecPreCommand), + typeof(IExecPostCommand), + typeof(IExecNoCommand), + typeof(IInputTransformer), + typeof(INService) + ]; + + foreach (var svc in a.GetTypes() + .Where(type => type.IsClass && types.Any(t => type.IsAssignableTo(t)) && !type.HasAttribute())) { - scan.From(a) - .SelectAllClasses() - .Where(c => (c.IsAssignableTo(typeof(INService)) - || c.IsAssignableTo(typeof(IExecOnMessage)) - || c.IsAssignableTo(typeof(IInputTransformer)) - || c.IsAssignableTo(typeof(IExecPreCommand)) - || c.IsAssignableTo(typeof(IExecPostCommand)) - || c.IsAssignableTo(typeof(IExecNoCommand))) - && !c.HasAttribute() -#if GLOBAL_NADEKO - && !c.HasAttribute() -#endif - ) - .BindToSelfWithInterfaces() - .Configure(c => c.InSingletonScope()); - }); + kernel.RegisterMany([svc], + getServiceTypes: type => type.GetImplementedTypes(ReflectionTools.AsImplementedType.SourceType), + getImplFactory: type => ReflectionFactory.Of(type, Reuse.Singleton)); + } +// +// kernel.RegisterMany( +// [a], +// #if GLOBAL_NADEKO +// && !c.HasAttribute() +// #endif +// ), +// reuse: +// Reuse.Singleton +// ); + + + // todo maybe self is missing + // todo maybe attribute doesn't work return kernel; } diff --git a/src/NadekoBot/_common/Services/IGoogleApiService.cs b/src/NadekoBot/_common/Services/IGoogleApiService.cs index 5d2592817..a02a56fe5 100644 --- a/src/NadekoBot/_common/Services/IGoogleApiService.cs +++ b/src/NadekoBot/_common/Services/IGoogleApiService.cs @@ -7,7 +7,7 @@ public interface IGoogleApiService IReadOnlyDictionary Languages { get; } Task> GetVideoLinksByKeywordAsync(string keywords, int count = 1); - Task> GetVideoInfosByKeywordAsync(string keywords, int count = 1); + Task> GetVideoInfosByKeywordAsync(string keywords, int count = 1); Task> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1); Task> GetRelatedVideosAsync(string id, int count = 1, string user = null); Task> GetPlaylistTracksAsync(string playlistId, int count = 50); diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index 2b1306b42..e36ce1ef4 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -185,6 +185,9 @@ threaddelete: prune: - prune - clear +prunecancel: + - prunecancel + - prunec die: - die setname: diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index b88cfd155..91b208176 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -385,6 +385,10 @@ prune: - "@Someone --safe" - "@Someone X" - "@Someone X -s" +prunecancel: + desc: "Cancels an active prune if there is any." + args: + - "" die: desc: "Shuts the bot down." args: diff --git a/src/NadekoBot/data/strings/responses/responses.en-US.json b/src/NadekoBot/data/strings/responses/responses.en-US.json index 47f5500ed..d58b08004 100644 --- a/src/NadekoBot/data/strings/responses/responses.en-US.json +++ b/src/NadekoBot/data/strings/responses/responses.en-US.json @@ -35,6 +35,9 @@ "banned_user": "User Banned", "ban_prune_disabled": "Banned user's messages will no longer be deleted.", "ban_prune": "Bot will prune up to {0} day(s) worth of messages from banned user.", + "prune_cancelled": "Pruning was cancelled.", + "prune_not_found": "No active prune was found on this server.", + "prune_progress": "Pruning... {0}/{1} messages deleted.", "timeoutdm": "You have been timed out in {0} server.\nReason: {1}", "timedout_user": "User Timed Out", "remove_roles_pl": "have had their roles removed", @@ -1089,5 +1092,7 @@ "todo_archive_empty": "You have no archived todos.", "todo_archive_list": "Archived Todo Lists", "todo_archive_not_found": "Archived todo list not found.", - "todo_archived_list": "Archived Todo List" + "todo_archived_list": "Archived Todo List", + "search_results": "Search results", + "queue_search_results": "Type the number of the search result to queue up that track." }