mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	More restructuring
This commit is contained in:
		@@ -1,51 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.TypeReaders;
 | 
			
		||||
 | 
			
		||||
public sealed class GuildDateTimeTypeReader : NadekoTypeReader<GuildDateTime>
 | 
			
		||||
{
 | 
			
		||||
    private readonly GuildTimezoneService _gts;
 | 
			
		||||
 | 
			
		||||
    public GuildDateTimeTypeReader(GuildTimezoneService gts)
 | 
			
		||||
        => _gts = gts;
 | 
			
		||||
 | 
			
		||||
    public override ValueTask<TypeReaderResult<GuildDateTime>> ReadAsync(ICommandContext context, string input)
 | 
			
		||||
    {
 | 
			
		||||
        var gdt = Parse(context.Guild.Id, input);
 | 
			
		||||
        if (gdt is null)
 | 
			
		||||
        {
 | 
			
		||||
            return new(TypeReaderResult.FromError<GuildDateTime>(CommandError.ParseFailed,
 | 
			
		||||
                "Input string is in an incorrect format."));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new(TypeReaderResult.FromSuccess(gdt));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private GuildDateTime Parse(ulong guildId, string input)
 | 
			
		||||
    {
 | 
			
		||||
        if (!DateTime.TryParse(input, out var dt))
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        var tz = _gts.GetTimeZoneOrUtc(guildId);
 | 
			
		||||
 | 
			
		||||
        return new(tz, dt);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class GuildDateTime
 | 
			
		||||
{
 | 
			
		||||
    public TimeZoneInfo Timezone { get; }
 | 
			
		||||
    public DateTime CurrentGuildTime { get; }
 | 
			
		||||
    public DateTime InputTime { get; }
 | 
			
		||||
    public DateTime InputTimeUtc { get; }
 | 
			
		||||
 | 
			
		||||
    public GuildDateTime(TimeZoneInfo guildTimezone, DateTime inputTime)
 | 
			
		||||
    {
 | 
			
		||||
        var now = DateTime.UtcNow;
 | 
			
		||||
        Timezone = guildTimezone;
 | 
			
		||||
        CurrentGuildTime = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.Utc, Timezone);
 | 
			
		||||
        InputTime = inputTime;
 | 
			
		||||
        InputTimeUtc = TimeZoneInfo.ConvertTime(inputTime, Timezone, TimeZoneInfo.Utc);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,405 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration : NadekoModule<AdministrationService>
 | 
			
		||||
{
 | 
			
		||||
    public enum Channel
 | 
			
		||||
    {
 | 
			
		||||
        Channel,
 | 
			
		||||
        Ch,
 | 
			
		||||
        Chnl,
 | 
			
		||||
        Chan
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public enum List
 | 
			
		||||
    {
 | 
			
		||||
        List = 0,
 | 
			
		||||
        Ls = 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public enum Server
 | 
			
		||||
    {
 | 
			
		||||
        Server
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public enum State
 | 
			
		||||
    {
 | 
			
		||||
        Enable,
 | 
			
		||||
        Disable,
 | 
			
		||||
        Inherit
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private readonly SomethingOnlyChannelService _somethingOnly;
 | 
			
		||||
    private readonly AutoPublishService _autoPubService;
 | 
			
		||||
 | 
			
		||||
    public Administration(SomethingOnlyChannelService somethingOnly, AutoPublishService autoPubService)
 | 
			
		||||
    {
 | 
			
		||||
        _somethingOnly = somethingOnly;
 | 
			
		||||
        _autoPubService = autoPubService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
    public async Task ImageOnlyChannel(StoopidTime time = null)
 | 
			
		||||
    {
 | 
			
		||||
        var newValue = await _somethingOnly.ToggleImageOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
        if (newValue)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.imageonly_enable);
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyPendingLocalizedAsync(strs.imageonly_disable);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
    public async Task LinkOnlyChannel(StoopidTime time = null)
 | 
			
		||||
    {
 | 
			
		||||
        var newValue = await _somethingOnly.ToggleLinkOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
        if (newValue)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.linkonly_enable);
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyPendingLocalizedAsync(strs.linkonly_disable);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(ChannelPerm.ManageChannels)]
 | 
			
		||||
    [BotPerm(ChannelPerm.ManageChannels)]
 | 
			
		||||
    public async Task Slowmode(StoopidTime time = null)
 | 
			
		||||
    {
 | 
			
		||||
        var seconds = (int?)time?.Time.TotalSeconds ?? 0;
 | 
			
		||||
        if (time is not null && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6)))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await ((ITextChannel)ctx.Channel).ModifyAsync(tcp =>
 | 
			
		||||
        {
 | 
			
		||||
            tcp.SlowModeInterval = seconds;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await ctx.OkAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
    [Priority(2)]
 | 
			
		||||
    public async Task Delmsgoncmd(List _)
 | 
			
		||||
    {
 | 
			
		||||
        var guild = (SocketGuild)ctx.Guild;
 | 
			
		||||
        var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
                       .WithOkColor()
 | 
			
		||||
                       .WithTitle(GetText(strs.server_delmsgoncmd))
 | 
			
		||||
                       .WithDescription(enabled ? "✅" : "❌");
 | 
			
		||||
 | 
			
		||||
        var str = string.Join("\n",
 | 
			
		||||
            channels.Select(x =>
 | 
			
		||||
            {
 | 
			
		||||
                var ch = guild.GetChannel(x.ChannelId)?.ToString() ?? x.ChannelId.ToString();
 | 
			
		||||
                var prefixSign = x.State ? "✅ " : "❌ ";
 | 
			
		||||
                return prefixSign + ch;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(str))
 | 
			
		||||
            str = "-";
 | 
			
		||||
 | 
			
		||||
        embed.AddField(GetText(strs.channel_delmsgoncmd), str);
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
    public async Task Delmsgoncmd(Server _ = Server.Server)
 | 
			
		||||
    {
 | 
			
		||||
        if (_service.ToggleDeleteMessageOnCommand(ctx.Guild.Id))
 | 
			
		||||
        {
 | 
			
		||||
            _service.DeleteMessagesOnCommand.Add(ctx.Guild.Id);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.delmsg_on);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            _service.DeleteMessagesOnCommand.TryRemove(ctx.Guild.Id);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.delmsg_off);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
    [Priority(0)]
 | 
			
		||||
    public Task Delmsgoncmd(Channel _, State s, ITextChannel ch)
 | 
			
		||||
        => Delmsgoncmd(_, s, ch.Id);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
    public async Task Delmsgoncmd(Channel _, State s, ulong? chId = null)
 | 
			
		||||
    {
 | 
			
		||||
        var actualChId = chId ?? ctx.Channel.Id;
 | 
			
		||||
        await _service.SetDelMsgOnCmdState(ctx.Guild.Id, actualChId, s);
 | 
			
		||||
 | 
			
		||||
        if (s == State.Disable)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.delmsg_channel_off);
 | 
			
		||||
        else if (s == State.Enable)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.delmsg_channel_on);
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.delmsg_channel_inherit);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.DeafenMembers)]
 | 
			
		||||
    [BotPerm(GuildPerm.DeafenMembers)]
 | 
			
		||||
    public async Task Deafen(params IGuildUser[] users)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.DeafenUsers(true, users);
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.deafen);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.DeafenMembers)]
 | 
			
		||||
    [BotPerm(GuildPerm.DeafenMembers)]
 | 
			
		||||
    public async Task UnDeafen(params IGuildUser[] users)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.DeafenUsers(false, users);
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.undeafen);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    public async Task DelVoiChanl([Leftover] IVoiceChannel voiceChannel)
 | 
			
		||||
    {
 | 
			
		||||
        await voiceChannel.DeleteAsync();
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.delvoich(Format.Bold(voiceChannel.Name)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    public async Task CreatVoiChanl([Leftover] string channelName)
 | 
			
		||||
    {
 | 
			
		||||
        var ch = await ctx.Guild.CreateVoiceChannelAsync(channelName);
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.createvoich(Format.Bold(ch.Name)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    public async Task DelTxtChanl([Leftover] ITextChannel toDelete)
 | 
			
		||||
    {
 | 
			
		||||
        await toDelete.DeleteAsync();
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.deltextchan(Format.Bold(toDelete.Name)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    public async Task CreaTxtChanl([Leftover] string channelName)
 | 
			
		||||
    {
 | 
			
		||||
        var txtCh = await ctx.Guild.CreateTextChannelAsync(channelName); 
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.createtextchan(Format.Bold(txtCh.Name)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    public async Task SetTopic([Leftover] string topic = null)
 | 
			
		||||
    {
 | 
			
		||||
        var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
        topic ??= "";
 | 
			
		||||
        await channel.ModifyAsync(c => c.Topic = topic);
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.set_topic);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    public async Task SetChanlName([Leftover] string name)
 | 
			
		||||
    {
 | 
			
		||||
        var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
        await channel.ModifyAsync(c => c.Name = name);
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.set_channel_name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
    public async Task NsfwToggle()
 | 
			
		||||
    {
 | 
			
		||||
        var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
        var isEnabled = channel.IsNsfw;
 | 
			
		||||
 | 
			
		||||
        await channel.ModifyAsync(c => c.IsNsfw = !isEnabled);
 | 
			
		||||
 | 
			
		||||
        if (isEnabled)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.nsfw_set_false);
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.nsfw_set_true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
    [Priority(0)]
 | 
			
		||||
    public Task Edit(ulong messageId, [Leftover] string text)
 | 
			
		||||
        => Edit((ITextChannel)ctx.Channel, messageId, text);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
    public async Task Edit(ITextChannel channel, ulong messageId, [Leftover] string text)
 | 
			
		||||
    {
 | 
			
		||||
        var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel);
 | 
			
		||||
        var botPerms = ((SocketGuild)ctx.Guild).CurrentUser.GetPermissions(channel);
 | 
			
		||||
        if (!userPerms.Has(ChannelPermission.ManageMessages))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.insuf_perms_u);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!botPerms.Has(ChannelPermission.ViewChannel))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.insuf_perms_i);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await _service.EditMessage(ctx, channel, messageId, text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
    [BotPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
    public Task Delete(ulong messageId, StoopidTime time = null)
 | 
			
		||||
        => Delete((ITextChannel)ctx.Channel, messageId, time);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task Delete(ITextChannel channel, ulong messageId, StoopidTime time = null)
 | 
			
		||||
        => await InternalMessageAction(channel, messageId, time, msg => msg.DeleteAsync());
 | 
			
		||||
 | 
			
		||||
    private async Task InternalMessageAction(
 | 
			
		||||
        ITextChannel channel,
 | 
			
		||||
        ulong messageId,
 | 
			
		||||
        StoopidTime time,
 | 
			
		||||
        Func<IMessage, Task> func)
 | 
			
		||||
    {
 | 
			
		||||
        var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel);
 | 
			
		||||
        var botPerms = ((SocketGuild)ctx.Guild).CurrentUser.GetPermissions(channel);
 | 
			
		||||
        if (!userPerms.Has(ChannelPermission.ManageMessages))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.insuf_perms_u);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!botPerms.Has(ChannelPermission.ManageMessages))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.insuf_perms_i);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var msg = await channel.GetMessageAsync(messageId);
 | 
			
		||||
        if (msg is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.msg_not_found);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (time is null)
 | 
			
		||||
            await msg.DeleteAsync();
 | 
			
		||||
        else if (time.Time <= TimeSpan.FromDays(7))
 | 
			
		||||
        {
 | 
			
		||||
            _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(time.Time);
 | 
			
		||||
                await msg.DeleteAsync();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.time_too_long);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ctx.OkAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [BotPerm(ChannelPermission.CreatePublicThreads)]
 | 
			
		||||
    [UserPerm(ChannelPermission.CreatePublicThreads)]
 | 
			
		||||
    public async Task ThreadCreate([Leftover] string name)
 | 
			
		||||
    {
 | 
			
		||||
        if (ctx.Channel is not SocketTextChannel stc)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        await stc.CreateThreadAsync(name, message: ctx.Message.ReferencedMessage);
 | 
			
		||||
        await ctx.OkAsync();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [BotPerm(ChannelPermission.ManageThreads)]
 | 
			
		||||
    [UserPerm(ChannelPermission.ManageThreads)]
 | 
			
		||||
    public async Task ThreadDelete([Leftover] string name)
 | 
			
		||||
    {
 | 
			
		||||
        if (ctx.Channel is not SocketTextChannel stc)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var t = stc.Threads.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.InvariantCultureIgnoreCase));
 | 
			
		||||
 | 
			
		||||
        if (t is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        await t.DeleteAsync();
 | 
			
		||||
        await ctx.OkAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
    public async Task AutoPublish()
 | 
			
		||||
    {
 | 
			
		||||
        if (ctx.Channel.GetChannelType() != ChannelType.News)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.req_announcement_channel);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var newState = await _autoPubService.ToggleAutoPublish(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
 | 
			
		||||
        if (newState)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.autopublish_enable);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.autopublish_disable);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,158 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class AdministrationService : INService
 | 
			
		||||
{
 | 
			
		||||
    public ConcurrentHashSet<ulong> DeleteMessagesOnCommand { get; }
 | 
			
		||||
    public ConcurrentDictionary<ulong, bool> DeleteMessagesOnCommandChannels { get; }
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly ILogCommandService _logService;
 | 
			
		||||
 | 
			
		||||
    public AdministrationService(
 | 
			
		||||
        Bot bot,
 | 
			
		||||
        CommandHandler cmdHandler,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        ILogCommandService logService)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _logService = logService;
 | 
			
		||||
 | 
			
		||||
        DeleteMessagesOnCommand = new(bot.AllGuildConfigs.Where(g => g.DeleteMessageOnCommand).Select(g => g.GuildId));
 | 
			
		||||
 | 
			
		||||
        DeleteMessagesOnCommandChannels = new(bot.AllGuildConfigs.SelectMany(x => x.DelMsgOnCmdChannels)
 | 
			
		||||
                                                 .ToDictionary(x => x.ChannelId, x => x.State)
 | 
			
		||||
                                                 .ToConcurrent());
 | 
			
		||||
 | 
			
		||||
        cmdHandler.CommandExecuted += DelMsgOnCmd_Handler;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public (bool DelMsgOnCmd, IEnumerable<DelMsgOnCmdChannel> channels) GetDelMsgOnCmdData(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.DelMsgOnCmdChannels));
 | 
			
		||||
 | 
			
		||||
        return (conf.DeleteMessageOnCommand, conf.DelMsgOnCmdChannels);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd)
 | 
			
		||||
    {
 | 
			
		||||
        if (msg.Channel is not ITextChannel channel)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            //wat ?!
 | 
			
		||||
            if (DeleteMessagesOnCommandChannels.TryGetValue(channel.Id, out var state))
 | 
			
		||||
            {
 | 
			
		||||
                if (state && cmd.Name != "prune" && cmd.Name != "pick")
 | 
			
		||||
                {
 | 
			
		||||
                    _logService.AddDeleteIgnore(msg.Id);
 | 
			
		||||
                    try { await msg.DeleteAsync(); }
 | 
			
		||||
                    catch { }
 | 
			
		||||
                }
 | 
			
		||||
                //if state is false, that means do not do it
 | 
			
		||||
            }
 | 
			
		||||
            else if (DeleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick")
 | 
			
		||||
            {
 | 
			
		||||
                _logService.AddDeleteIgnore(msg.Id);
 | 
			
		||||
                try { await msg.DeleteAsync(); }
 | 
			
		||||
                catch { }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool ToggleDeleteMessageOnCommand(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        bool enabled;
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        enabled = conf.DeleteMessageOnCommand = !conf.DeleteMessageOnCommand;
 | 
			
		||||
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return enabled;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetDelMsgOnCmdState(ulong guildId, ulong chId, Administration.State newState)
 | 
			
		||||
    {
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.DelMsgOnCmdChannels));
 | 
			
		||||
 | 
			
		||||
            var old = conf.DelMsgOnCmdChannels.FirstOrDefault(x => x.ChannelId == chId);
 | 
			
		||||
            if (newState == Administration.State.Inherit)
 | 
			
		||||
            {
 | 
			
		||||
                if (old is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    conf.DelMsgOnCmdChannels.Remove(old);
 | 
			
		||||
                    uow.Remove(old);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (old is null)
 | 
			
		||||
                {
 | 
			
		||||
                    old = new()
 | 
			
		||||
                    {
 | 
			
		||||
                        ChannelId = chId
 | 
			
		||||
                    };
 | 
			
		||||
                    conf.DelMsgOnCmdChannels.Add(old);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                old.State = newState == Administration.State.Enable;
 | 
			
		||||
                DeleteMessagesOnCommandChannels[chId] = newState == Administration.State.Enable;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (newState == Administration.State.Disable)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        else if (newState == Administration.State.Enable)
 | 
			
		||||
            DeleteMessagesOnCommandChannels[chId] = true;
 | 
			
		||||
        else
 | 
			
		||||
            DeleteMessagesOnCommandChannels.TryRemove(chId, out _);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task DeafenUsers(bool value, params IGuildUser[] users)
 | 
			
		||||
    {
 | 
			
		||||
        if (!users.Any())
 | 
			
		||||
            return;
 | 
			
		||||
        foreach (var u in users)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await u.ModifyAsync(usr => usr.Deaf = value);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task EditMessage(
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        ITextChannel chanl,
 | 
			
		||||
        ulong messageId,
 | 
			
		||||
        string input)
 | 
			
		||||
    {
 | 
			
		||||
        var msg = await chanl.GetMessageAsync(messageId);
 | 
			
		||||
 | 
			
		||||
        if (msg is not IUserMessage umsg || msg.Author.Id != context.Client.CurrentUser.Id)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var rep = new ReplacementBuilder().WithDefault(context).Build();
 | 
			
		||||
 | 
			
		||||
        var text = SmartText.CreateFrom(input);
 | 
			
		||||
        text = rep.Replace(text);
 | 
			
		||||
 | 
			
		||||
        await umsg.EditAsync(text);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class AutoAssignRoleCommands : NadekoModule<AutoAssignRoleService>
 | 
			
		||||
    {
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task AutoAssignRole([Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var guser = (IGuildUser)ctx.User;
 | 
			
		||||
            if (role.Id == ctx.Guild.EveryoneRole.Id)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            // the user can't aar the role which is higher or equal to his highest role
 | 
			
		||||
            if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.hierarchy);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id);
 | 
			
		||||
            if (roles.Count == 0)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.aar_disabled);
 | 
			
		||||
            else if (roles.Contains(role.Id))
 | 
			
		||||
                await AutoAssignRole();
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.aar_role_removed(Format.Bold(role.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task AutoAssignRole()
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.TryGetRoles(ctx.Guild.Id, out var roles))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.aar_none);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var existing = roles.Select(rid => ctx.Guild.GetRole(rid)).Where(r => r is not null).ToList();
 | 
			
		||||
 | 
			
		||||
            if (existing.Count != roles.Count)
 | 
			
		||||
                await _service.SetAarRolesAsync(ctx.Guild.Id, existing.Select(x => x.Id));
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.aar_roles(
 | 
			
		||||
                '\n' + existing.Select(x => Format.Bold(x.ToString())).Join(",\n")));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,160 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Threading.Channels;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class AutoAssignRoleService : INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    //guildid/roleid
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, IReadOnlyList<ulong>> _autoAssignableRoles;
 | 
			
		||||
 | 
			
		||||
    private readonly Channel<SocketGuildUser> _assignQueue = Channel.CreateBounded<SocketGuildUser>(
 | 
			
		||||
        new BoundedChannelOptions(100)
 | 
			
		||||
        {
 | 
			
		||||
            FullMode = BoundedChannelFullMode.DropOldest,
 | 
			
		||||
            SingleReader = true,
 | 
			
		||||
            SingleWriter = false
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    public AutoAssignRoleService(DiscordSocketClient client, Bot bot, DbService db)
 | 
			
		||||
    {
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _db = db;
 | 
			
		||||
 | 
			
		||||
        _autoAssignableRoles = bot.AllGuildConfigs.Where(x => !string.IsNullOrWhiteSpace(x.AutoAssignRoleIds))
 | 
			
		||||
                                  .ToDictionary<GuildConfig, ulong, IReadOnlyList<ulong>>(k => k.GuildId,
 | 
			
		||||
                                      v => v.GetAutoAssignableRoles())
 | 
			
		||||
                                  .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            while (true)
 | 
			
		||||
            {
 | 
			
		||||
                var user = await _assignQueue.Reader.ReadAsync();
 | 
			
		||||
                if (!_autoAssignableRoles.TryGetValue(user.Guild.Id, out var savedRoleIds))
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var roleIds = savedRoleIds.Select(roleId => user.Guild.GetRole(roleId))
 | 
			
		||||
                                              .Where(x => x is not null)
 | 
			
		||||
                                              .ToList();
 | 
			
		||||
 | 
			
		||||
                    if (roleIds.Any())
 | 
			
		||||
                    {
 | 
			
		||||
                        await user.AddRolesAsync(roleIds);
 | 
			
		||||
                        await Task.Delay(250);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(
 | 
			
		||||
                            "Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server the roles dont exist",
 | 
			
		||||
                            user.Guild.Name,
 | 
			
		||||
                            user.Guild.Id);
 | 
			
		||||
 | 
			
		||||
                        await DisableAarAsync(user.Guild.Id);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(
 | 
			
		||||
                        "Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server because I don't have role management permissions",
 | 
			
		||||
                        user.Guild.Name,
 | 
			
		||||
                        user.Guild.Id);
 | 
			
		||||
 | 
			
		||||
                    await DisableAarAsync(user.Guild.Id);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error in aar. Probably one of the roles doesn't exist");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        _client.UserJoined += OnClientOnUserJoined;
 | 
			
		||||
        _client.RoleDeleted += OnClientRoleDeleted;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task OnClientRoleDeleted(SocketRole role)
 | 
			
		||||
    {
 | 
			
		||||
        if (_autoAssignableRoles.TryGetValue(role.Guild.Id, out var roles) && roles.Contains(role.Id))
 | 
			
		||||
            await ToggleAarAsync(role.Guild.Id, role.Id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task OnClientOnUserJoined(SocketGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        if (_autoAssignableRoles.TryGetValue(user.Guild.Id, out _))
 | 
			
		||||
            await _assignQueue.Writer.WriteAsync(user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IReadOnlyList<ulong>> ToggleAarAsync(ulong guildId, ulong roleId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        var roles = gc.GetAutoAssignableRoles();
 | 
			
		||||
        if (!roles.Remove(roleId) && roles.Count < 3)
 | 
			
		||||
            roles.Add(roleId);
 | 
			
		||||
 | 
			
		||||
        gc.SetAutoAssignableRoles(roles);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        if (roles.Count > 0)
 | 
			
		||||
            _autoAssignableRoles[guildId] = roles;
 | 
			
		||||
        else
 | 
			
		||||
            _autoAssignableRoles.TryRemove(guildId, out _);
 | 
			
		||||
 | 
			
		||||
        return roles;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task DisableAarAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        await uow.GuildConfigs.AsNoTracking()
 | 
			
		||||
                 .Where(x => x.GuildId == guildId)
 | 
			
		||||
                 .UpdateAsync(_ => new()
 | 
			
		||||
                 {
 | 
			
		||||
                     AutoAssignRoleIds = null
 | 
			
		||||
                 });
 | 
			
		||||
 | 
			
		||||
        _autoAssignableRoles.TryRemove(guildId, out _);
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetAarRolesAsync(ulong guildId, IEnumerable<ulong> newRoles)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        gc.SetAutoAssignableRoles(newRoles);
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryGetRoles(ulong guildId, out IReadOnlyList<ulong> roles)
 | 
			
		||||
        => _autoAssignableRoles.TryGetValue(guildId, out roles);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public static class GuildConfigExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static List<ulong> GetAutoAssignableRoles(this GuildConfig gc)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(gc.AutoAssignRoleIds))
 | 
			
		||||
            return new();
 | 
			
		||||
 | 
			
		||||
        return gc.AutoAssignRoleIds.Split(',').Select(ulong.Parse).ToList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void SetAutoAssignableRoles(this GuildConfig gc, IEnumerable<ulong> roles)
 | 
			
		||||
        => gc.AutoAssignRoleIds = roles.Join(',');
 | 
			
		||||
}
 | 
			
		||||
@@ -1,87 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class AutoPublishService : IExecNoCommand, IReadyExecutor, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly IBotCredsProvider _creds;
 | 
			
		||||
    private ConcurrentDictionary<ulong, ulong> _enabled;
 | 
			
		||||
 | 
			
		||||
    public AutoPublishService(DbService db, DiscordSocketClient client, IBotCredsProvider creds)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
    } 
 | 
			
		||||
    
 | 
			
		||||
    public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is null)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        if (msg.Channel.GetChannelType() != ChannelType.News)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (!_enabled.TryGetValue(guild.Id, out var cid) || cid != msg.Channel.Id)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        await msg.CrosspostAsync(new RequestOptions()
 | 
			
		||||
        {
 | 
			
		||||
            RetryMode = RetryMode.AlwaysFail
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        var creds = _creds.GetCreds();
 | 
			
		||||
        
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        var items = await ctx.GetTable<AutoPublishChannel>()
 | 
			
		||||
            .Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, creds.TotalShards, _client.ShardId))
 | 
			
		||||
            .ToListAsyncLinqToDB();
 | 
			
		||||
 | 
			
		||||
        _enabled = items
 | 
			
		||||
            .ToDictionary(x => x.GuildId, x => x.ChannelId)
 | 
			
		||||
            .ToConcurrent();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<bool> ToggleAutoPublish(ulong guildId, ulong channelId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        var deleted = await ctx.GetTable<AutoPublishChannel>()
 | 
			
		||||
            .DeleteAsync(x => x.GuildId == guildId && x.ChannelId == channelId);
 | 
			
		||||
 | 
			
		||||
        if (deleted != 0)
 | 
			
		||||
        {
 | 
			
		||||
            _enabled.TryRemove(guildId, out _);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ctx.GetTable<AutoPublishChannel>()
 | 
			
		||||
            .InsertOrUpdateAsync(() => new()
 | 
			
		||||
                {
 | 
			
		||||
                    GuildId = guildId,
 | 
			
		||||
                    ChannelId = channelId,
 | 
			
		||||
                    DateAdded = DateTime.UtcNow,
 | 
			
		||||
                },
 | 
			
		||||
                old => new()
 | 
			
		||||
                {
 | 
			
		||||
                    ChannelId = channelId,
 | 
			
		||||
                    DateAdded = DateTime.UtcNow,
 | 
			
		||||
                },
 | 
			
		||||
                () => new()
 | 
			
		||||
                {
 | 
			
		||||
                    GuildId = guildId
 | 
			
		||||
                });
 | 
			
		||||
        
 | 
			
		||||
        _enabled[guildId] = channelId;
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,130 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
#if !GLOBAL_NADEKO
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public partial class DangerousCommands : NadekoModule<DangerousCommandsService>
 | 
			
		||||
        {
 | 
			
		||||
            private async Task ConfirmActionInternalAsync(string name, Func<Task> action)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var embed = _eb.Create()
 | 
			
		||||
                                   .WithTitle(GetText(strs.sql_confirm_exec))
 | 
			
		||||
                                   .WithDescription(name);
 | 
			
		||||
 | 
			
		||||
                    if (!await PromptUserConfirmAsync(embed))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    await action();
 | 
			
		||||
                    await ctx.OkAsync();
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    await SendErrorAsync(ex.ToString());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task SqlSelect([Leftover] string sql)
 | 
			
		||||
            {
 | 
			
		||||
                var result = _service.SelectSql(sql);
 | 
			
		||||
 | 
			
		||||
                return ctx.SendPaginatedConfirmAsync(0,
 | 
			
		||||
                    cur =>
 | 
			
		||||
                    {
 | 
			
		||||
                        var items = result.Results.Skip(cur * 20).Take(20).ToList();
 | 
			
		||||
 | 
			
		||||
                        if (!items.Any())
 | 
			
		||||
                            return _eb.Create().WithErrorColor().WithFooter(sql).WithDescription("-");
 | 
			
		||||
 | 
			
		||||
                        return _eb.Create()
 | 
			
		||||
                                  .WithOkColor()
 | 
			
		||||
                                  .WithFooter(sql)
 | 
			
		||||
                                  .WithTitle(string.Join(" ║ ", result.ColumnNames))
 | 
			
		||||
                                  .WithDescription(string.Join('\n', items.Select(x => string.Join(" ║ ", x))));
 | 
			
		||||
                    },
 | 
			
		||||
                    result.Results.Count,
 | 
			
		||||
                    20);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task SqlExec([Leftover] string sql)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var embed = _eb.Create()
 | 
			
		||||
                                   .WithTitle(GetText(strs.sql_confirm_exec))
 | 
			
		||||
                                   .WithDescription(Format.Code(sql));
 | 
			
		||||
 | 
			
		||||
                    if (!await PromptUserConfirmAsync(embed))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    var res = await _service.ExecuteSql(sql);
 | 
			
		||||
                    await SendConfirmAsync(res.ToString());
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    await SendErrorAsync(ex.ToString());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeleteWaifus()
 | 
			
		||||
                => ConfirmActionInternalAsync("Delete Waifus", () => _service.DeleteWaifus());
 | 
			
		||||
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task DeleteWaifu(IUser user)
 | 
			
		||||
                => await DeleteWaifu(user.Id);
 | 
			
		||||
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeleteWaifu(ulong userId)
 | 
			
		||||
                => ConfirmActionInternalAsync($"Delete Waifu {userId}", () => _service.DeleteWaifu(userId));
 | 
			
		||||
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeleteCurrency()
 | 
			
		||||
                => ConfirmActionInternalAsync("Delete Currency", () => _service.DeleteCurrency());
 | 
			
		||||
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeletePlaylists()
 | 
			
		||||
                => ConfirmActionInternalAsync("Delete Playlists", () => _service.DeletePlaylists());
 | 
			
		||||
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeleteXp()
 | 
			
		||||
                => ConfirmActionInternalAsync("Delete Xp", () => _service.DeleteXp());
 | 
			
		||||
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task PurgeUser(ulong userId)
 | 
			
		||||
            {
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithDescription(GetText(strs.purge_user_confirm(Format.Bold(userId.ToString()))));
 | 
			
		||||
 | 
			
		||||
                if (!await PromptUserConfirmAsync(embed))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await _service.PurgeUserAsync(userId);
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [Cmd]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task PurgeUser([Leftover] IUser user)
 | 
			
		||||
                => PurgeUser(user.Id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,172 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class DangerousCommandsService : INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    public DangerousCommandsService(DbService db)
 | 
			
		||||
        => _db = db;
 | 
			
		||||
 | 
			
		||||
    public async Task DeleteXp()
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        await ctx.DiscordUser.UpdateAsync(_ => new DiscordUser()
 | 
			
		||||
        {
 | 
			
		||||
            ClubId = null,
 | 
			
		||||
            // IsClubAdmin = false,
 | 
			
		||||
            TotalXp = 0
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await ctx.UserXpStats.DeleteAsync();
 | 
			
		||||
        await ctx.ClubApplicants.DeleteAsync();
 | 
			
		||||
        await ctx.ClubBans.DeleteAsync();
 | 
			
		||||
        await ctx.Clubs.DeleteAsync();
 | 
			
		||||
        await ctx.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task DeleteWaifus()
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        await ctx.WaifuUpdates.DeleteAsync();
 | 
			
		||||
        await ctx.WaifuItem.DeleteAsync();
 | 
			
		||||
        await ctx.WaifuInfo.DeleteAsync();
 | 
			
		||||
        await ctx.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task DeleteWaifu(ulong userId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        await ctx.WaifuUpdates
 | 
			
		||||
                 .Where(x => x.User.UserId == userId)
 | 
			
		||||
                 .DeleteAsync();
 | 
			
		||||
        await ctx.WaifuItem
 | 
			
		||||
                 .Where(x => x.WaifuInfo.Waifu.UserId == userId)
 | 
			
		||||
                 .DeleteAsync();
 | 
			
		||||
        await ctx.WaifuInfo
 | 
			
		||||
                 .Where(x => x.Claimer.UserId == userId)
 | 
			
		||||
                 .UpdateAsync(old => new WaifuInfo()
 | 
			
		||||
                 {
 | 
			
		||||
                     ClaimerId = null,
 | 
			
		||||
                 });
 | 
			
		||||
        await ctx.WaifuInfo
 | 
			
		||||
                 .Where(x => x.Waifu.UserId == userId)
 | 
			
		||||
                 .DeleteAsync();
 | 
			
		||||
        await ctx.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task DeletePlaylists()
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        await ctx.MusicPlaylists.DeleteAsync();
 | 
			
		||||
        await ctx.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task DeleteCurrency()
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        await ctx.DiscordUser.UpdateAsync(_ => new DiscordUser()
 | 
			
		||||
        {
 | 
			
		||||
            CurrencyAmount = 0
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await ctx.CurrencyTransactions.DeleteAsync();
 | 
			
		||||
        await ctx.PlantedCurrency.DeleteAsync();
 | 
			
		||||
        await ctx.BankUsers.DeleteAsync();
 | 
			
		||||
        await ctx.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<int> ExecuteSql(string sql)
 | 
			
		||||
    {
 | 
			
		||||
        int res;
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        res = await uow.Database.ExecuteSqlRawAsync(sql);
 | 
			
		||||
        return res;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public SelectResult SelectSql(string sql)
 | 
			
		||||
    {
 | 
			
		||||
        var result = new SelectResult
 | 
			
		||||
        {
 | 
			
		||||
            ColumnNames = new(),
 | 
			
		||||
            Results = new()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conn = uow.Database.GetDbConnection();
 | 
			
		||||
        conn.Open();
 | 
			
		||||
        using var cmd = conn.CreateCommand();
 | 
			
		||||
        cmd.CommandText = sql;
 | 
			
		||||
        using var reader = cmd.ExecuteReader();
 | 
			
		||||
        if (reader.HasRows)
 | 
			
		||||
        {
 | 
			
		||||
            for (var i = 0; i < reader.FieldCount; i++)
 | 
			
		||||
                result.ColumnNames.Add(reader.GetName(i));
 | 
			
		||||
            while (reader.Read())
 | 
			
		||||
            {
 | 
			
		||||
                var obj = new object[reader.FieldCount];
 | 
			
		||||
                reader.GetValues(obj);
 | 
			
		||||
                result.Results.Add(obj.Select(x => x.ToString()).ToArray());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task PurgeUserAsync(ulong userId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        // get waifu info
 | 
			
		||||
        var wi = await uow.Set<WaifuInfo>().FirstOrDefaultAsyncEF(x => x.Waifu.UserId == userId);
 | 
			
		||||
 | 
			
		||||
        // if it exists, delete waifu related things
 | 
			
		||||
        if (wi is not null)
 | 
			
		||||
        {
 | 
			
		||||
            // remove updates which have new or old as this waifu
 | 
			
		||||
            await uow.WaifuUpdates.DeleteAsync(wu => wu.New.UserId == userId || wu.Old.UserId == userId);
 | 
			
		||||
 | 
			
		||||
            // delete all items this waifu owns
 | 
			
		||||
            await uow.Set<WaifuItem>().DeleteAsync(x => x.WaifuInfoId == wi.Id);
 | 
			
		||||
 | 
			
		||||
            // all waifus this waifu claims are released
 | 
			
		||||
            await uow.Set<WaifuInfo>()
 | 
			
		||||
                     .AsQueryable()
 | 
			
		||||
                     .Where(x => x.Claimer.UserId == userId)
 | 
			
		||||
                     .UpdateAsync(x => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         ClaimerId = null
 | 
			
		||||
                     });
 | 
			
		||||
 | 
			
		||||
            // all affinities set to this waifu are reset
 | 
			
		||||
            await uow.Set<WaifuInfo>()
 | 
			
		||||
                     .AsQueryable()
 | 
			
		||||
                     .Where(x => x.Affinity.UserId == userId)
 | 
			
		||||
                     .UpdateAsync(x => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         AffinityId = null
 | 
			
		||||
                     });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // delete guild xp
 | 
			
		||||
        await uow.UserXpStats.DeleteAsync(x => x.UserId == userId);
 | 
			
		||||
 | 
			
		||||
        // delete currency transactions
 | 
			
		||||
        await uow.Set<CurrencyTransaction>().DeleteAsync(x => x.UserId == userId);
 | 
			
		||||
 | 
			
		||||
        // delete user, currency, and clubs go away with it
 | 
			
		||||
        await uow.DiscordUser.DeleteAsync(u => u.UserId == userId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class SelectResult
 | 
			
		||||
    {
 | 
			
		||||
        public List<string> ColumnNames { get; set; }
 | 
			
		||||
        public List<string[]> Results { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class GameVoiceChannelCommands : NadekoModule<GameVoiceChannelService>
 | 
			
		||||
    {
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [BotPerm(GuildPerm.MoveMembers)]
 | 
			
		||||
        public async Task GameVoiceChannel()
 | 
			
		||||
        {
 | 
			
		||||
            var vch = ((IGuildUser)ctx.User).VoiceChannel;
 | 
			
		||||
 | 
			
		||||
            if (vch is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.not_in_voice);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var id = _service.ToggleGameVoiceChannel(ctx.Guild.Id, vch.Id);
 | 
			
		||||
 | 
			
		||||
            if (id is null)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.gvc_disabled);
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _service.GameVoiceChannels.Add(vch.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.gvc_enabled(Format.Bold(vch.Name)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,127 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class GameVoiceChannelService : INService
 | 
			
		||||
{
 | 
			
		||||
    public ConcurrentHashSet<ulong> GameVoiceChannels { get; }
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
    public GameVoiceChannelService(DiscordSocketClient client, DbService db, Bot bot)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _client = client;
 | 
			
		||||
 | 
			
		||||
        GameVoiceChannels = new(bot.AllGuildConfigs
 | 
			
		||||
                                   .Where(gc => gc.GameVoiceChannel is not null)
 | 
			
		||||
                                   .Select(gc => gc.GameVoiceChannel!.Value));
 | 
			
		||||
 | 
			
		||||
        _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
 | 
			
		||||
        _client.PresenceUpdated += OnPresenceUpdate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task OnPresenceUpdate(SocketUser socketUser, SocketPresence before, SocketPresence after)
 | 
			
		||||
    {
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (socketUser is not SocketGuildUser newUser)
 | 
			
		||||
                    return;
 | 
			
		||||
                // if the user is in the voice channel and that voice channel is gvc
 | 
			
		||||
 | 
			
		||||
                if (newUser.VoiceChannel is not { } vc
 | 
			
		||||
                    || !GameVoiceChannels.Contains(vc.Id))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                //if the activity has changed, and is a playi1ng activity
 | 
			
		||||
                foreach (var activity in after.Activities)
 | 
			
		||||
                {
 | 
			
		||||
                    if (activity is { Type: ActivityType.Playing })
 | 
			
		||||
                        //trigger gvc
 | 
			
		||||
                    {
 | 
			
		||||
                        if (await TriggerGvc(newUser, activity.Name))
 | 
			
		||||
                            return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error running GuildMemberUpdated in gvc");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ulong? ToggleGameVoiceChannel(ulong guildId, ulong vchId)
 | 
			
		||||
    {
 | 
			
		||||
        ulong? id;
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
 | 
			
		||||
        if (gc.GameVoiceChannel == vchId)
 | 
			
		||||
        {
 | 
			
		||||
            GameVoiceChannels.TryRemove(vchId);
 | 
			
		||||
            id = gc.GameVoiceChannel = null;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            if (gc.GameVoiceChannel is not null)
 | 
			
		||||
                GameVoiceChannels.TryRemove(gc.GameVoiceChannel.Value);
 | 
			
		||||
            GameVoiceChannels.Add(vchId);
 | 
			
		||||
            id = gc.GameVoiceChannel = vchId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task OnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState)
 | 
			
		||||
    {
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (usr is not SocketGuildUser gUser)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (newState.VoiceChannel is null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (!GameVoiceChannels.Contains(newState.VoiceChannel.Id))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                foreach (var game in gUser.Activities.Select(x => x.Name))
 | 
			
		||||
                {
 | 
			
		||||
                    if (await TriggerGvc(gUser, game))
 | 
			
		||||
                        return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error running VoiceStateUpdate in gvc");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> TriggerGvc(SocketGuildUser gUser, string game)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(game))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        game = game.TrimTo(50)!.ToLowerInvariant();
 | 
			
		||||
        var vch = gUser.Guild.VoiceChannels.FirstOrDefault(x => x.Name.ToLowerInvariant() == game);
 | 
			
		||||
 | 
			
		||||
        if (vch is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        await Task.Delay(1000);
 | 
			
		||||
        await gUser.ModifyAsync(gu => gu.Channel = vch);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,229 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class GreetCommands : NadekoModule<GreetService>
 | 
			
		||||
    {
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task Boost()
 | 
			
		||||
        {
 | 
			
		||||
            var enabled = await _service.ToggleBoost(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.boost_on);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.boost_off);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task BoostDel(int timer = 30)
 | 
			
		||||
        {
 | 
			
		||||
            if (timer is < 0 or > 600)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await _service.SetBoostDel(ctx.Guild.Id, timer);
 | 
			
		||||
 | 
			
		||||
            if (timer > 0)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.boostdel_on(timer));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.boostdel_off);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task BoostMsg([Leftover] string? text = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(text))
 | 
			
		||||
            {
 | 
			
		||||
                var boostMessage = _service.GetBoostMessage(ctx.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.boostmsg_cur(boostMessage?.SanitizeMentions()));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var sendBoostEnabled = _service.SetBoostMessage(ctx.Guild.Id, ref text);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.boostmsg_new);
 | 
			
		||||
            if (!sendBoostEnabled)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.boostmsg_enable($"`{prefix}boost`"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task GreetDel(int timer = 30)
 | 
			
		||||
        {
 | 
			
		||||
            if (timer is < 0 or > 600)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await _service.SetGreetDel(ctx.Guild.Id, timer);
 | 
			
		||||
 | 
			
		||||
            if (timer > 0)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.greetdel_on(timer));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.greetdel_off);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task Greet()
 | 
			
		||||
        {
 | 
			
		||||
            var enabled = await _service.SetGreet(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.greet_on);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.greet_off);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task GreetMsg([Leftover] string? text = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(text))
 | 
			
		||||
            {
 | 
			
		||||
                var greetMsg = _service.GetGreetMsg(ctx.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.greetmsg_cur(greetMsg?.SanitizeMentions()));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var sendGreetEnabled = _service.SetGreetMessage(ctx.Guild.Id, ref text);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.greetmsg_new);
 | 
			
		||||
 | 
			
		||||
            if (!sendGreetEnabled)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{prefix}greet`"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task GreetDm()
 | 
			
		||||
        {
 | 
			
		||||
            var enabled = await _service.SetGreetDm(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.greetdm_on);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.greetdm_off);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task GreetDmMsg([Leftover] string? text = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(text))
 | 
			
		||||
            {
 | 
			
		||||
                var dmGreetMsg = _service.GetDmGreetMsg(ctx.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.greetdmmsg_cur(dmGreetMsg?.SanitizeMentions()));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var sendGreetEnabled = _service.SetGreetDmMessage(ctx.Guild.Id, ref text);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.greetdmmsg_new);
 | 
			
		||||
            if (!sendGreetEnabled)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{prefix}greetdm`"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task Bye()
 | 
			
		||||
        {
 | 
			
		||||
            var enabled = await _service.SetBye(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.bye_on);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.bye_off);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task ByeMsg([Leftover] string? text = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(text))
 | 
			
		||||
            {
 | 
			
		||||
                var byeMsg = _service.GetByeMessage(ctx.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.byemsg_cur(byeMsg?.SanitizeMentions()));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var sendByeEnabled = _service.SetByeMessage(ctx.Guild.Id, ref text);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.byemsg_new);
 | 
			
		||||
            if (!sendByeEnabled)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{prefix}bye`"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        public async Task ByeDel(int timer = 30)
 | 
			
		||||
        {
 | 
			
		||||
            await _service.SetByeDel(ctx.Guild.Id, timer);
 | 
			
		||||
 | 
			
		||||
            if (timer > 0)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.byedel_on(timer));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.byedel_off);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        [Ratelimit(5)]
 | 
			
		||||
        public async Task ByeTest([Leftover] IGuildUser? user = null)
 | 
			
		||||
        {
 | 
			
		||||
            user ??= (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
            await _service.ByeTest((ITextChannel)ctx.Channel, user);
 | 
			
		||||
            var enabled = _service.GetByeEnabled(ctx.Guild.Id);
 | 
			
		||||
            if (!enabled)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{prefix}bye`"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        [Ratelimit(5)]
 | 
			
		||||
        public async Task GreetTest([Leftover] IGuildUser? user = null)
 | 
			
		||||
        {
 | 
			
		||||
            user ??= (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
            await _service.GreetTest((ITextChannel)ctx.Channel, user);
 | 
			
		||||
            var enabled = _service.GetGreetEnabled(ctx.Guild.Id);
 | 
			
		||||
            if (!enabled)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{prefix}greet`"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
        [Ratelimit(5)]
 | 
			
		||||
        public async Task GreetDmTest([Leftover] IGuildUser? user = null)
 | 
			
		||||
        {
 | 
			
		||||
            user ??= (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
            var success = await _service.GreetDmTest(user);
 | 
			
		||||
            if (success)
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            else
 | 
			
		||||
                await ctx.WarningAsync();
 | 
			
		||||
            var enabled = _service.GetGreetDmEnabled(ctx.Guild.Id);
 | 
			
		||||
            if (!enabled)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{prefix}greetdm`"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public class GreetGrouper<T>
 | 
			
		||||
{
 | 
			
		||||
    private readonly Dictionary<ulong, HashSet<T>> _group;
 | 
			
		||||
    private readonly object _locker = new();
 | 
			
		||||
 | 
			
		||||
    public GreetGrouper()
 | 
			
		||||
        => _group = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Creates a group, if group already exists, adds the specified user
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId">Id of the server for which to create group for</param>
 | 
			
		||||
    /// <param name="toAddIfExists">User to add if group already exists</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public bool CreateOrAdd(ulong guildId, T toAddIfExists)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (_group.TryGetValue(guildId, out var list))
 | 
			
		||||
            {
 | 
			
		||||
                list.Add(toAddIfExists);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _group[guildId] = new();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Remove the specified amount of items from the group. If all items are removed, group will be removed.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId">Id of the group</param>
 | 
			
		||||
    /// <param name="count">Maximum number of items to retrieve</param>
 | 
			
		||||
    /// <param name="items">Items retrieved</param>
 | 
			
		||||
    /// <returns>Whether the group has no more items left and is deleted</returns>
 | 
			
		||||
    public bool ClearGroup(ulong guildId, int count, out IReadOnlyCollection<T> items)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (_group.TryGetValue(guildId, out var set))
 | 
			
		||||
            {
 | 
			
		||||
                // if we want more than there are, return everything
 | 
			
		||||
                if (count >= set.Count)
 | 
			
		||||
                {
 | 
			
		||||
                    items = set;
 | 
			
		||||
                    _group.Remove(guildId);
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // if there are more in the group than what's needed
 | 
			
		||||
                // take the requested number, remove them from the set
 | 
			
		||||
                // and return them
 | 
			
		||||
                var toReturn = set.TakeWhile(_ => count-- != 0).ToList();
 | 
			
		||||
                foreach (var item in toReturn)
 | 
			
		||||
                    set.Remove(item);
 | 
			
		||||
 | 
			
		||||
                items = toReturn;
 | 
			
		||||
                // returning falsemeans group is not yet deleted
 | 
			
		||||
                // because there are items left
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            items = Array.Empty<T>();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,619 +0,0 @@
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Threading.Channels;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public class GreetService : INService, IReadyExecutor
 | 
			
		||||
{
 | 
			
		||||
    public bool GroupGreets
 | 
			
		||||
        => _bss.Data.GroupGreets;
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, GreetSettings> _guildConfigsCache;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
    private readonly GreetGrouper<IGuildUser> _greets = new();
 | 
			
		||||
    private readonly GreetGrouper<IUser> _byes = new();
 | 
			
		||||
    private readonly BotConfigService _bss;
 | 
			
		||||
 | 
			
		||||
    public GreetService(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        Bot bot,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        BotConfigService bss)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _bss = bss;
 | 
			
		||||
 | 
			
		||||
        _guildConfigsCache = new(bot.AllGuildConfigs.ToDictionary(g => g.GuildId, GreetSettings.Create));
 | 
			
		||||
 | 
			
		||||
        _client.UserJoined += OnUserJoined;
 | 
			
		||||
        _client.UserLeft += OnUserLeft;
 | 
			
		||||
 | 
			
		||||
        bot.JoinedGuild += OnBotJoinedGuild;
 | 
			
		||||
        _client.LeftGuild += OnClientLeftGuild;
 | 
			
		||||
 | 
			
		||||
        _client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        while (true)
 | 
			
		||||
        {
 | 
			
		||||
            var (conf, user, compl) = await _greetDmQueue.Reader.ReadAsync();
 | 
			
		||||
            var res = await GreetDmUserInternal(conf, user);
 | 
			
		||||
            compl.TrySetResult(res);
 | 
			
		||||
            await Task.Delay(2000);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser)
 | 
			
		||||
    {
 | 
			
		||||
        // if user is a new booster
 | 
			
		||||
        // or boosted again the same server
 | 
			
		||||
        if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null })
 | 
			
		||||
            || (optOldUser.Value?.PremiumSince is { } oldDate
 | 
			
		||||
                && newUser.PremiumSince is { } newDate
 | 
			
		||||
                && newDate > oldDate))
 | 
			
		||||
        {
 | 
			
		||||
            var conf = GetOrAddSettingsForGuild(newUser.Guild.Id);
 | 
			
		||||
            if (!conf.SendBoostMessage)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            _ = Task.Run(TriggerBoostMessage(conf, newUser));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Func<Task> TriggerBoostMessage(GreetSettings conf, SocketGuildUser user)
 | 
			
		||||
        => async () =>
 | 
			
		||||
        {
 | 
			
		||||
            var channel = user.Guild.GetTextChannel(conf.BoostMessageChannelId);
 | 
			
		||||
            if (channel is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(conf.BoostMessage))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var toSend = SmartText.CreateFrom(conf.BoostMessage);
 | 
			
		||||
            var rep = new ReplacementBuilder().WithDefault(user, channel, user.Guild, _client).Build();
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var toDelete = await channel.SendAsync(rep.Replace(toSend));
 | 
			
		||||
                if (conf.BoostMessageDeleteAfter > 0)
 | 
			
		||||
                    toDelete.DeleteAfter(conf.BoostMessageDeleteAfter);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Error sending boost message");
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    private Task OnClientLeftGuild(SocketGuild arg)
 | 
			
		||||
    {
 | 
			
		||||
        _guildConfigsCache.TryRemove(arg.Id, out _);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task OnBotJoinedGuild(GuildConfig gc)
 | 
			
		||||
    {
 | 
			
		||||
        _guildConfigsCache[gc.GuildId] = GreetSettings.Create(gc);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task OnUserLeft(SocketGuild guild, SocketUser user)
 | 
			
		||||
    {
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var conf = GetOrAddSettingsForGuild(guild.Id);
 | 
			
		||||
 | 
			
		||||
                if (!conf.SendChannelByeMessage)
 | 
			
		||||
                    return;
 | 
			
		||||
                var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ByeMessageChannelId);
 | 
			
		||||
 | 
			
		||||
                if (channel is null) //maybe warn the server owner that the channel is missing
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (GroupGreets)
 | 
			
		||||
                {
 | 
			
		||||
                    // if group is newly created, greet that user right away,
 | 
			
		||||
                    // but any user which joins in the next 5 seconds will
 | 
			
		||||
                    // be greeted in a group greet
 | 
			
		||||
                    if (_byes.CreateOrAdd(guild.Id, user))
 | 
			
		||||
                    {
 | 
			
		||||
                        // greet single user
 | 
			
		||||
                        await ByeUsers(conf, channel, new[] { user });
 | 
			
		||||
                        var groupClear = false;
 | 
			
		||||
                        while (!groupClear)
 | 
			
		||||
                        {
 | 
			
		||||
                            await Task.Delay(5000);
 | 
			
		||||
                            groupClear = _byes.ClearGroup(guild.Id, 5, out var toBye);
 | 
			
		||||
                            await ByeUsers(conf, channel, toBye);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                    await ByeUsers(conf, channel, new[] { user });
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string? GetDmGreetMsg(ulong id)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.GuildConfigsForId(id, set => set).DmGreetMessageText;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string? GetGreetMsg(ulong gid)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.GuildConfigsForId(gid, set => set).ChannelGreetMessageText;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string? GetBoostMessage(ulong gid)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.GuildConfigsForId(gid, set => set).BoostMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task ByeUsers(GreetSettings conf, ITextChannel channel, IUser user)
 | 
			
		||||
        => ByeUsers(conf, channel, new[] { user });
 | 
			
		||||
 | 
			
		||||
    private async Task ByeUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection<IUser> users)
 | 
			
		||||
    {
 | 
			
		||||
        if (!users.Any())
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var rep = new ReplacementBuilder().WithChannel(channel)
 | 
			
		||||
                                          .WithClient(_client)
 | 
			
		||||
                                          .WithServer(_client, (SocketGuild)channel.Guild)
 | 
			
		||||
                                          .WithManyUsers(users)
 | 
			
		||||
                                          .Build();
 | 
			
		||||
 | 
			
		||||
        var text = SmartText.CreateFrom(conf.ChannelByeMessageText);
 | 
			
		||||
        text = rep.Replace(text);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var toDelete = await channel.SendAsync(text);
 | 
			
		||||
            if (conf.AutoDeleteByeMessagesTimer > 0)
 | 
			
		||||
                toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer);
 | 
			
		||||
        }
 | 
			
		||||
        catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions || ex.DiscordCode == DiscordErrorCode.UnknownChannel)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex, "Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}", channel.GuildId);
 | 
			
		||||
            await SetBye(channel.GuildId, channel.Id, false);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex, "Error embeding bye message");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task GreetUsers(GreetSettings conf, ITextChannel channel, IGuildUser user)
 | 
			
		||||
        => GreetUsers(conf, channel, new[] { user });
 | 
			
		||||
 | 
			
		||||
    private async Task GreetUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection<IGuildUser> users)
 | 
			
		||||
    {
 | 
			
		||||
        if (users.Count == 0)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var rep = new ReplacementBuilder().WithChannel(channel)
 | 
			
		||||
                                          .WithClient(_client)
 | 
			
		||||
                                          .WithServer(_client, (SocketGuild)channel.Guild)
 | 
			
		||||
                                          .WithManyUsers(users)
 | 
			
		||||
                                          .Build();
 | 
			
		||||
 | 
			
		||||
        var text = SmartText.CreateFrom(conf.ChannelGreetMessageText);
 | 
			
		||||
        text = rep.Replace(text);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var toDelete = await channel.SendAsync(text);
 | 
			
		||||
            if (conf.AutoDeleteGreetMessagesTimer > 0)
 | 
			
		||||
                toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer);
 | 
			
		||||
        }
 | 
			
		||||
        catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions || ex.DiscordCode == DiscordErrorCode.UnknownChannel)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex, "Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}", channel.GuildId);
 | 
			
		||||
            await SetGreet(channel.GuildId, channel.Id, false);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex, "Error embeding greet message");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private readonly Channel<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)> _greetDmQueue =
 | 
			
		||||
        Channel.CreateBounded<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)>(new BoundedChannelOptions(60)
 | 
			
		||||
        {
 | 
			
		||||
            // The limit of 60 users should be only hit when there's a raid. In that case 
 | 
			
		||||
            // probably the best thing to do is to drop newest (raiding) users
 | 
			
		||||
            FullMode = BoundedChannelFullMode.DropNewest
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> GreetDmUser(GreetSettings conf, IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
			
		||||
        await _greetDmQueue.Writer.WriteAsync((conf, user, completionSource));
 | 
			
		||||
        return await completionSource.Task;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> GreetDmUserInternal(GreetSettings conf, IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var rep = new ReplacementBuilder()
 | 
			
		||||
                      .WithUser(user)
 | 
			
		||||
                      .WithServer(_client, (SocketGuild)user.Guild)
 | 
			
		||||
                      .Build();
 | 
			
		||||
 | 
			
		||||
            var text = SmartText.CreateFrom(conf.DmGreetMessageText);
 | 
			
		||||
            text = rep.Replace(text);
 | 
			
		||||
 | 
			
		||||
            if (text is SmartPlainText pt)
 | 
			
		||||
            {
 | 
			
		||||
                text = new SmartEmbedText()
 | 
			
		||||
                {
 | 
			
		||||
                    Description = pt.Text
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (text is SmartEmbedText set)
 | 
			
		||||
            {
 | 
			
		||||
                text = set with
 | 
			
		||||
                {
 | 
			
		||||
                    Footer = CreateFooterSource(user)
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            else if (text is SmartEmbedTextArray seta)
 | 
			
		||||
            {
 | 
			
		||||
                // if the greet dm message is a text array
 | 
			
		||||
                var ebElem = seta.Embeds.LastOrDefault();
 | 
			
		||||
                if (ebElem is null)
 | 
			
		||||
                {
 | 
			
		||||
                    // if there are no embeds, add an embed with the footer
 | 
			
		||||
                    text = seta with
 | 
			
		||||
                    {
 | 
			
		||||
                        Embeds = new[]
 | 
			
		||||
                        {
 | 
			
		||||
                            new SmartEmbedArrayElementText()
 | 
			
		||||
                            {
 | 
			
		||||
                                Footer = CreateFooterSource(user)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // if the maximum amount of embeds is reached, edit the last embed
 | 
			
		||||
                    if (seta.Embeds.Length >= 10)
 | 
			
		||||
                    {
 | 
			
		||||
                        seta.Embeds[^1] = seta.Embeds[^1] with
 | 
			
		||||
                        {
 | 
			
		||||
                            Footer = CreateFooterSource(user)
 | 
			
		||||
                        };
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        // if there is less than 10 embeds, add an embed with footer only
 | 
			
		||||
                        seta.Embeds = seta.Embeds.Append(new SmartEmbedArrayElementText()
 | 
			
		||||
                        {
 | 
			
		||||
                            Footer = CreateFooterSource(user)
 | 
			
		||||
                        }).ToArray();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await user.SendAsync(text);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static SmartTextEmbedFooter CreateFooterSource(IGuildUser user)
 | 
			
		||||
        => new()
 | 
			
		||||
        {
 | 
			
		||||
            Text = $"This message was sent from {user.Guild} server.",
 | 
			
		||||
            IconUrl = user.Guild.IconUrl
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    private Task OnUserJoined(IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var conf = GetOrAddSettingsForGuild(user.GuildId);
 | 
			
		||||
 | 
			
		||||
                if (conf.SendChannelGreetMessage)
 | 
			
		||||
                {
 | 
			
		||||
                    var channel = await user.Guild.GetTextChannelAsync(conf.GreetMessageChannelId);
 | 
			
		||||
                    if (channel is not null)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (GroupGreets)
 | 
			
		||||
                        {
 | 
			
		||||
                            // if group is newly created, greet that user right away,
 | 
			
		||||
                            // but any user which joins in the next 5 seconds will
 | 
			
		||||
                            // be greeted in a group greet
 | 
			
		||||
                            if (_greets.CreateOrAdd(user.GuildId, user))
 | 
			
		||||
                            {
 | 
			
		||||
                                // greet single user
 | 
			
		||||
                                await GreetUsers(conf, channel, new[] { user });
 | 
			
		||||
                                var groupClear = false;
 | 
			
		||||
                                while (!groupClear)
 | 
			
		||||
                                {
 | 
			
		||||
                                    await Task.Delay(5000);
 | 
			
		||||
                                    groupClear = _greets.ClearGroup(user.GuildId, 5, out var toGreet);
 | 
			
		||||
                                    await GreetUsers(conf, channel, toGreet);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                            await GreetUsers(conf, channel, new[] { user });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (conf.SendDmGreetMessage)
 | 
			
		||||
                    await GreetDmUser(conf, user);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string? GetByeMessage(ulong gid)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.GuildConfigsForId(gid, set => set).ChannelByeMessageText;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public GreetSettings GetOrAddSettingsForGuild(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_guildConfigsCache.TryGetValue(guildId, out var settings))
 | 
			
		||||
            return settings;
 | 
			
		||||
 | 
			
		||||
        using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var gc = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
            settings = GreetSettings.Create(gc);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _guildConfigsCache.TryAdd(guildId, settings);
 | 
			
		||||
        return settings;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetGreet(ulong guildId, ulong channelId, bool? value = null)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        var enabled = conf.SendChannelGreetMessage = value ?? !conf.SendChannelGreetMessage;
 | 
			
		||||
        conf.GreetMessageChannelId = channelId;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return enabled;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool SetGreetMessage(ulong guildId, ref string message)
 | 
			
		||||
    {
 | 
			
		||||
        message = message.SanitizeMentions();
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(message))
 | 
			
		||||
            throw new ArgumentNullException(nameof(message));
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        conf.ChannelGreetMessageText = message;
 | 
			
		||||
        var greetMsgEnabled = conf.SendChannelGreetMessage;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache.AddOrUpdate(guildId, toAdd, (_, _) => toAdd);
 | 
			
		||||
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return greetMsgEnabled;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetGreetDm(ulong guildId, bool? value = null)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        var enabled = conf.SendDmGreetMessage = value ?? !conf.SendDmGreetMessage;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return enabled;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool SetGreetDmMessage(ulong guildId, ref string? message)
 | 
			
		||||
    {
 | 
			
		||||
        message = message?.SanitizeMentions();
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(message))
 | 
			
		||||
            throw new ArgumentNullException(nameof(message));
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        conf.DmGreetMessageText = message;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return conf.SendDmGreetMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetBye(ulong guildId, ulong channelId, bool? value = null)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        var enabled = conf.SendChannelByeMessage = value ?? !conf.SendChannelByeMessage;
 | 
			
		||||
        conf.ByeMessageChannelId = channelId;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return enabled;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool SetByeMessage(ulong guildId, ref string? message)
 | 
			
		||||
    {
 | 
			
		||||
        message = message?.SanitizeMentions();
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(message))
 | 
			
		||||
            throw new ArgumentNullException(nameof(message));
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        conf.ChannelByeMessageText = message;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return conf.SendChannelByeMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetByeDel(ulong guildId, int timer)
 | 
			
		||||
    {
 | 
			
		||||
        if (timer is < 0 or > 600)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        conf.AutoDeleteByeMessagesTimer = timer;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetGreetDel(ulong guildId, int timer)
 | 
			
		||||
    {
 | 
			
		||||
        if (timer is < 0 or > 600)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        conf.AutoDeleteGreetMessagesTimer = timer;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool SetBoostMessage(ulong guildId, ref string message)
 | 
			
		||||
    {
 | 
			
		||||
        message = message.SanitizeMentions();
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        conf.BoostMessage = message;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return conf.SendBoostMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetBoostDel(ulong guildId, int timer)
 | 
			
		||||
    {
 | 
			
		||||
        if (timer is < 0 or > 600)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(timer));
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        conf.BoostMessageDeleteAfter = timer;
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ToggleBoost(ulong guildId, ulong channelId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        conf.SendBoostMessage = !conf.SendBoostMessage;
 | 
			
		||||
        conf.BoostMessageChannelId = channelId;
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        var toAdd = GreetSettings.Create(conf);
 | 
			
		||||
        _guildConfigsCache[guildId] = toAdd;
 | 
			
		||||
        return conf.SendBoostMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region Get Enabled Status
 | 
			
		||||
 | 
			
		||||
    public bool GetGreetDmEnabled(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        return conf.SendDmGreetMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool GetGreetEnabled(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        return conf.SendChannelGreetMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool GetByeEnabled(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        return conf.SendChannelByeMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    #region Test Messages
 | 
			
		||||
 | 
			
		||||
    public Task ByeTest(ITextChannel channel, IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        var conf = GetOrAddSettingsForGuild(user.GuildId);
 | 
			
		||||
        return ByeUsers(conf, channel, user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task GreetTest(ITextChannel channel, IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        var conf = GetOrAddSettingsForGuild(user.GuildId);
 | 
			
		||||
        return GreetUsers(conf, channel, user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<bool> GreetDmTest(IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        var conf = GetOrAddSettingsForGuild(user.GuildId);
 | 
			
		||||
        return GreetDmUser(conf, user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
}
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public class GreetSettings
 | 
			
		||||
{
 | 
			
		||||
    public int AutoDeleteGreetMessagesTimer { get; set; }
 | 
			
		||||
    public int AutoDeleteByeMessagesTimer { get; set; }
 | 
			
		||||
 | 
			
		||||
    public ulong GreetMessageChannelId { get; set; }
 | 
			
		||||
    public ulong ByeMessageChannelId { get; set; }
 | 
			
		||||
 | 
			
		||||
    public bool SendDmGreetMessage { get; set; }
 | 
			
		||||
    public string? DmGreetMessageText { get; set; }
 | 
			
		||||
 | 
			
		||||
    public bool SendChannelGreetMessage { get; set; }
 | 
			
		||||
    public string? ChannelGreetMessageText { get; set; }
 | 
			
		||||
 | 
			
		||||
    public bool SendChannelByeMessage { get; set; }
 | 
			
		||||
    public string? ChannelByeMessageText { get; set; }
 | 
			
		||||
 | 
			
		||||
    public bool SendBoostMessage { get; set; }
 | 
			
		||||
    public string? BoostMessage { get; set; }
 | 
			
		||||
    public int BoostMessageDeleteAfter { get; set; }
 | 
			
		||||
    public ulong BoostMessageChannelId { get; set; }
 | 
			
		||||
 | 
			
		||||
    public static GreetSettings Create(GuildConfig g)
 | 
			
		||||
        => new()
 | 
			
		||||
        {
 | 
			
		||||
            AutoDeleteByeMessagesTimer = g.AutoDeleteByeMessagesTimer,
 | 
			
		||||
            AutoDeleteGreetMessagesTimer = g.AutoDeleteGreetMessagesTimer,
 | 
			
		||||
            GreetMessageChannelId = g.GreetMessageChannelId,
 | 
			
		||||
            ByeMessageChannelId = g.ByeMessageChannelId,
 | 
			
		||||
            SendDmGreetMessage = g.SendDmGreetMessage,
 | 
			
		||||
            DmGreetMessageText = g.DmGreetMessageText,
 | 
			
		||||
            SendChannelGreetMessage = g.SendChannelGreetMessage,
 | 
			
		||||
            ChannelGreetMessageText = g.ChannelGreetMessageText,
 | 
			
		||||
            SendChannelByeMessage = g.SendChannelByeMessage,
 | 
			
		||||
            ChannelByeMessageText = g.ChannelByeMessageText,
 | 
			
		||||
            SendBoostMessage = g.SendBoostMessage,
 | 
			
		||||
            BoostMessage = g.BoostMessage,
 | 
			
		||||
            BoostMessageDeleteAfter = g.BoostMessageDeleteAfter,
 | 
			
		||||
            BoostMessageChannelId = g.BoostMessageChannelId
 | 
			
		||||
        };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,235 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using Microsoft.Extensions.Caching.Memory;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Threading.Channels;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class SomethingOnlyChannelService : IExecOnMessage
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; } = 0;
 | 
			
		||||
    private readonly IMemoryCache _ticketCache;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _imageOnly;
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _linkOnly;
 | 
			
		||||
 | 
			
		||||
    private readonly Channel<IUserMessage> _deleteQueue = Channel.CreateBounded<IUserMessage>(
 | 
			
		||||
        new BoundedChannelOptions(100)
 | 
			
		||||
        {
 | 
			
		||||
            FullMode = BoundedChannelFullMode.DropOldest,
 | 
			
		||||
            SingleReader = true,
 | 
			
		||||
            SingleWriter = false
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public SomethingOnlyChannelService(IMemoryCache ticketCache, DiscordSocketClient client, DbService db)
 | 
			
		||||
    {
 | 
			
		||||
        _ticketCache = ticketCache;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _db = db;
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        _imageOnly = uow.ImageOnlyChannels
 | 
			
		||||
            .Where(x => x.Type == OnlyChannelType.Image)
 | 
			
		||||
            .ToList()
 | 
			
		||||
            .GroupBy(x => x.GuildId)
 | 
			
		||||
            .ToDictionary(x => x.Key, x => new ConcurrentHashSet<ulong>(x.Select(y => y.ChannelId)))
 | 
			
		||||
            .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
        _linkOnly = uow.ImageOnlyChannels
 | 
			
		||||
            .Where(x => x.Type == OnlyChannelType.Link)
 | 
			
		||||
            .ToList()
 | 
			
		||||
            .GroupBy(x => x.GuildId)
 | 
			
		||||
            .ToDictionary(x => x.Key, x => new ConcurrentHashSet<ulong>(x.Select(y => y.ChannelId)))
 | 
			
		||||
            .ToConcurrent();
 | 
			
		||||
        
 | 
			
		||||
        _ = Task.Run(DeleteQueueRunner);
 | 
			
		||||
 | 
			
		||||
        _client.ChannelDestroyed += ClientOnChannelDestroyed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task ClientOnChannelDestroyed(SocketChannel ch)
 | 
			
		||||
    {
 | 
			
		||||
        if (ch is not IGuildChannel gch)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (_imageOnly.TryGetValue(gch.GuildId, out var channels) && channels.TryRemove(ch.Id))
 | 
			
		||||
            await ToggleImageOnlyChannelAsync(gch.GuildId, ch.Id, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task DeleteQueueRunner()
 | 
			
		||||
    {
 | 
			
		||||
        while (true)
 | 
			
		||||
        {
 | 
			
		||||
            var toDelete = await _deleteQueue.Reader.ReadAsync();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await toDelete.DeleteAsync();
 | 
			
		||||
                await Task.Delay(1000);
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
 | 
			
		||||
            {
 | 
			
		||||
                // disable if bot can't delete messages in the channel
 | 
			
		||||
                await ToggleImageOnlyChannelAsync(((ITextChannel)toDelete.Channel).GuildId, toDelete.Channel.Id, true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ToggleImageOnlyChannelAsync(ulong guildId, ulong channelId, bool forceDisable = false)
 | 
			
		||||
    {
 | 
			
		||||
        var newState = false;
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        if (forceDisable || (_imageOnly.TryGetValue(guildId, out var channels) && channels.TryRemove(channelId)))
 | 
			
		||||
        {
 | 
			
		||||
            await uow.ImageOnlyChannels.DeleteAsync(x => x.ChannelId == channelId && x.Type == OnlyChannelType.Image);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await uow.ImageOnlyChannels.DeleteAsync(x => x.ChannelId == channelId);
 | 
			
		||||
            uow.ImageOnlyChannels.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                ChannelId = channelId,
 | 
			
		||||
                Type = OnlyChannelType.Image
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (_linkOnly.TryGetValue(guildId, out var chs))
 | 
			
		||||
                chs.TryRemove(channelId);
 | 
			
		||||
            
 | 
			
		||||
            channels = _imageOnly.GetOrAdd(guildId, new ConcurrentHashSet<ulong>());
 | 
			
		||||
            channels.Add(channelId);
 | 
			
		||||
            newState = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return newState;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<bool> ToggleLinkOnlyChannelAsync(ulong guildId, ulong channelId, bool forceDisable = false)
 | 
			
		||||
    {
 | 
			
		||||
        var newState = false;
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        if (forceDisable || (_linkOnly.TryGetValue(guildId, out var channels) && channels.TryRemove(channelId)))
 | 
			
		||||
        {
 | 
			
		||||
            await uow.ImageOnlyChannels.DeleteAsync(x => x.ChannelId == channelId && x.Type == OnlyChannelType.Link);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await uow.ImageOnlyChannels.DeleteAsync(x => x.ChannelId == channelId);
 | 
			
		||||
            uow.ImageOnlyChannels.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                ChannelId = channelId,
 | 
			
		||||
                Type = OnlyChannelType.Link
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (_imageOnly.TryGetValue(guildId, out var chs))
 | 
			
		||||
                chs.TryRemove(channelId);
 | 
			
		||||
            
 | 
			
		||||
            channels = _linkOnly.GetOrAdd(guildId, new ConcurrentHashSet<ulong>());
 | 
			
		||||
            channels.Add(channelId);
 | 
			
		||||
            newState = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return newState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (msg.Channel is not ITextChannel tch)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (_imageOnly.TryGetValue(tch.GuildId, out var chs) && chs.Contains(msg.Channel.Id))
 | 
			
		||||
            return await HandleOnlyChannel(tch, msg, OnlyChannelType.Image);
 | 
			
		||||
        
 | 
			
		||||
        if (_linkOnly.TryGetValue(tch.GuildId, out chs) && chs.Contains(msg.Channel.Id))
 | 
			
		||||
            return await HandleOnlyChannel(tch, msg, OnlyChannelType.Link);
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> HandleOnlyChannel(ITextChannel tch, IUserMessage msg, OnlyChannelType type)
 | 
			
		||||
    {
 | 
			
		||||
        if (type == OnlyChannelType.Image)
 | 
			
		||||
        {
 | 
			
		||||
            if (msg.Attachments.Any(x => x is { Height: > 0, Width: > 0 }))
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            if (msg.Content.TryGetUrlPath(out _))
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        var user = await tch.Guild.GetUserAsync(msg.Author.Id)
 | 
			
		||||
                   ?? await _client.Rest.GetGuildUserAsync(tch.GuildId, msg.Author.Id);
 | 
			
		||||
 | 
			
		||||
        if (user is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        // ignore owner and admin
 | 
			
		||||
        if (user.Id == tch.Guild.OwnerId || user.GuildPermissions.Administrator)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("{Type}-Only Channel: Ignoring owner od admin ({ChannelId})", type, msg.Channel.Id);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // ignore users higher in hierarchy
 | 
			
		||||
        var botUser = await tch.Guild.GetCurrentUserAsync();
 | 
			
		||||
        if (user.GetRoles().Max(x => x.Position) >= botUser.GetRoles().Max(x => x.Position))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (!botUser.GetPermissions(tch).ManageChannel)
 | 
			
		||||
        {
 | 
			
		||||
            if(type == OnlyChannelType.Image)
 | 
			
		||||
                await ToggleImageOnlyChannelAsync(tch.GuildId, tch.Id, true);
 | 
			
		||||
            else
 | 
			
		||||
                await ToggleImageOnlyChannelAsync(tch.GuildId, tch.Id, true);
 | 
			
		||||
            
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var shouldLock = AddUserTicket(tch.GuildId, msg.Author.Id);
 | 
			
		||||
        if (shouldLock)
 | 
			
		||||
        {
 | 
			
		||||
            await tch.AddPermissionOverwriteAsync(msg.Author, new(sendMessages: PermValue.Deny));
 | 
			
		||||
            Log.Warning("{Type}-Only Channel: User {User} [{UserId}] has been banned from typing in the channel [{ChannelId}]",
 | 
			
		||||
                type,
 | 
			
		||||
                msg.Author,
 | 
			
		||||
                msg.Author.Id,
 | 
			
		||||
                msg.Channel.Id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await _deleteQueue.Writer.WriteAsync(msg);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Error deleting message {MessageId} in image-only channel {ChannelId}", msg.Id, tch.Id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private bool AddUserTicket(ulong guildId, ulong userId)
 | 
			
		||||
    {
 | 
			
		||||
        var old = _ticketCache.GetOrCreate($"{guildId}_{userId}",
 | 
			
		||||
            entry =>
 | 
			
		||||
            {
 | 
			
		||||
                entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1);
 | 
			
		||||
                return 0;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        _ticketCache.Set($"{guildId}_{userId}", ++old);
 | 
			
		||||
 | 
			
		||||
        // if this is the third time that the user posts a
 | 
			
		||||
        // non image in an image-only channel on this server 
 | 
			
		||||
        return old > 2;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,250 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class LocalizationCommands : NadekoModule
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly IReadOnlyDictionary<string, string> _supportedLocales = new Dictionary<string, string>
 | 
			
		||||
        {
 | 
			
		||||
            { "ar", "العربية" },
 | 
			
		||||
            { "zh-TW", "繁體中文, 台灣" },
 | 
			
		||||
            { "zh-CN", "简体中文, 中华人民共和国" },
 | 
			
		||||
            { "nl-NL", "Nederlands, Nederland" },
 | 
			
		||||
            { "en-US", "English, United States" },
 | 
			
		||||
            { "fr-FR", "Français, France" },
 | 
			
		||||
            { "cs-CZ", "Čeština, Česká republika" },
 | 
			
		||||
            { "da-DK", "Dansk, Danmark" },
 | 
			
		||||
            { "de-DE", "Deutsch, Deutschland" },
 | 
			
		||||
            { "he-IL", "עברית, ישראל" },
 | 
			
		||||
            { "hu-HU", "Magyar, Magyarország" },
 | 
			
		||||
            { "id-ID", "Bahasa Indonesia, Indonesia" },
 | 
			
		||||
            { "it-IT", "Italiano, Italia" },
 | 
			
		||||
            { "ja-JP", "日本語, 日本" },
 | 
			
		||||
            { "ko-KR", "한국어, 대한민국" },
 | 
			
		||||
            { "nb-NO", "Norsk, Norge" },
 | 
			
		||||
            { "pl-PL", "Polski, Polska" },
 | 
			
		||||
            { "pt-BR", "Português Brasileiro, Brasil" },
 | 
			
		||||
            { "ro-RO", "Română, România" },
 | 
			
		||||
            { "ru-RU", "Русский, Россия" },
 | 
			
		||||
            { "sr-Cyrl-RS", "Српски, Србија" },
 | 
			
		||||
            { "es-ES", "Español, España" },
 | 
			
		||||
            { "sv-SE", "Svenska, Sverige" },
 | 
			
		||||
            { "tr-TR", "Türkçe, Türkiye" },
 | 
			
		||||
            { "ts-TS", "Tsundere, You Baka" },
 | 
			
		||||
            { "uk-UA", "Українська, Україна" }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task LanguageSet()
 | 
			
		||||
            => await ReplyConfirmLocalizedAsync(strs.lang_set_show(Format.Bold(Culture.ToString()),
 | 
			
		||||
                Format.Bold(Culture.NativeName)));
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task LanguageSet(string name)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                CultureInfo ci;
 | 
			
		||||
                if (name.Trim().ToLowerInvariant() == "default")
 | 
			
		||||
                {
 | 
			
		||||
                    _localization.RemoveGuildCulture(ctx.Guild);
 | 
			
		||||
                    ci = _localization.DefaultCultureInfo;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    ci = new(name);
 | 
			
		||||
                    _localization.SetGuildCulture(ctx.Guild, ci);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.lang_set(Format.Bold(ci.ToString()), Format.Bold(ci.NativeName)));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.lang_set_fail);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task LanguageSetDefault()
 | 
			
		||||
        {
 | 
			
		||||
            var cul = _localization.DefaultCultureInfo;
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.lang_set_bot_show(cul, cul.NativeName));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task LanguageSetDefault(string name)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                CultureInfo ci;
 | 
			
		||||
                if (name.Trim().ToLowerInvariant() == "default")
 | 
			
		||||
                {
 | 
			
		||||
                    _localization.ResetDefaultCulture();
 | 
			
		||||
                    ci = _localization.DefaultCultureInfo;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    ci = new(name);
 | 
			
		||||
                    _localization.SetDefaultCulture(ci);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.lang_set_bot(Format.Bold(ci.ToString()),
 | 
			
		||||
                    Format.Bold(ci.NativeName)));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.lang_set_fail);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task LanguagesList()
 | 
			
		||||
            => await ctx.Channel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                               .WithOkColor()
 | 
			
		||||
                                               .WithTitle(GetText(strs.lang_list))
 | 
			
		||||
                                               .WithDescription(string.Join("\n",
 | 
			
		||||
                                                   _supportedLocales.Select(
 | 
			
		||||
                                                       x => $"{Format.Code(x.Key),-10} => {x.Value}"))));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
/* list of language codes for reference. 
 | 
			
		||||
 * taken from https://github.com/dotnet/coreclr/blob/ee5862c6a257e60e263537d975ab6c513179d47f/src/mscorlib/src/System/Globalization/CultureData.cs#L192
 | 
			
		||||
            { "029", "en-029" },
 | 
			
		||||
            { "AE",  "ar-AE" },
 | 
			
		||||
            { "AF",  "prs-AF" },
 | 
			
		||||
            { "AL",  "sq-AL" },
 | 
			
		||||
            { "AM",  "hy-AM" },
 | 
			
		||||
            { "AR",  "es-AR" },
 | 
			
		||||
            { "AT",  "de-AT" },
 | 
			
		||||
            { "AU",  "en-AU" },
 | 
			
		||||
            { "AZ",  "az-Cyrl-AZ" },
 | 
			
		||||
            { "BA",  "bs-Latn-BA" },
 | 
			
		||||
            { "BD",  "bn-BD" },
 | 
			
		||||
            { "BE",  "nl-BE" },
 | 
			
		||||
            { "BG",  "bg-BG" },
 | 
			
		||||
            { "BH",  "ar-BH" },
 | 
			
		||||
            { "BN",  "ms-BN" },
 | 
			
		||||
            { "BO",  "es-BO" },
 | 
			
		||||
            { "BR",  "pt-BR" },
 | 
			
		||||
            { "BY",  "be-BY" },
 | 
			
		||||
            { "BZ",  "en-BZ" },
 | 
			
		||||
            { "CA",  "en-CA" },
 | 
			
		||||
            { "CH",  "it-CH" },
 | 
			
		||||
            { "CL",  "es-CL" },
 | 
			
		||||
            { "CN",  "zh-CN" },
 | 
			
		||||
            { "CO",  "es-CO" },
 | 
			
		||||
            { "CR",  "es-CR" },
 | 
			
		||||
            { "CS",  "sr-Cyrl-CS" },
 | 
			
		||||
            { "CZ",  "cs-CZ" },
 | 
			
		||||
            { "DE",  "de-DE" },
 | 
			
		||||
            { "DK",  "da-DK" },
 | 
			
		||||
            { "DO",  "es-DO" },
 | 
			
		||||
            { "DZ",  "ar-DZ" },
 | 
			
		||||
            { "EC",  "es-EC" },
 | 
			
		||||
            { "EE",  "et-EE" },
 | 
			
		||||
            { "EG",  "ar-EG" },
 | 
			
		||||
            { "ES",  "es-ES" },
 | 
			
		||||
            { "ET",  "am-ET" },
 | 
			
		||||
            { "FI",  "fi-FI" },
 | 
			
		||||
            { "FO",  "fo-FO" },
 | 
			
		||||
            { "FR",  "fr-FR" },
 | 
			
		||||
            { "GB",  "en-GB" },
 | 
			
		||||
            { "GE",  "ka-GE" },
 | 
			
		||||
            { "GL",  "kl-GL" },
 | 
			
		||||
            { "GR",  "el-GR" },
 | 
			
		||||
            { "GT",  "es-GT" },
 | 
			
		||||
            { "HK",  "zh-HK" },
 | 
			
		||||
            { "HN",  "es-HN" },
 | 
			
		||||
            { "HR",  "hr-HR" },
 | 
			
		||||
            { "HU",  "hu-HU" },
 | 
			
		||||
            { "ID",  "id-ID" },
 | 
			
		||||
            { "IE",  "en-IE" },
 | 
			
		||||
            { "IL",  "he-IL" },
 | 
			
		||||
            { "IN",  "hi-IN" },
 | 
			
		||||
            { "IQ",  "ar-IQ" },
 | 
			
		||||
            { "IR",  "fa-IR" },
 | 
			
		||||
            { "IS",  "is-IS" },
 | 
			
		||||
            { "IT",  "it-IT" },
 | 
			
		||||
            { "IV",  "" },
 | 
			
		||||
            { "JM",  "en-JM" },
 | 
			
		||||
            { "JO",  "ar-JO" },
 | 
			
		||||
            { "JP",  "ja-JP" },
 | 
			
		||||
            { "KE",  "sw-KE" },
 | 
			
		||||
            { "KG",  "ky-KG" },
 | 
			
		||||
            { "KH",  "km-KH" },
 | 
			
		||||
            { "KR",  "ko-KR" },
 | 
			
		||||
            { "KW",  "ar-KW" },
 | 
			
		||||
            { "KZ",  "kk-KZ" },
 | 
			
		||||
            { "LA",  "lo-LA" },
 | 
			
		||||
            { "LB",  "ar-LB" },
 | 
			
		||||
            { "LI",  "de-LI" },
 | 
			
		||||
            { "LK",  "si-LK" },
 | 
			
		||||
            { "LT",  "lt-LT" },
 | 
			
		||||
            { "LU",  "lb-LU" },
 | 
			
		||||
            { "LV",  "lv-LV" },
 | 
			
		||||
            { "LY",  "ar-LY" },
 | 
			
		||||
            { "MA",  "ar-MA" },
 | 
			
		||||
            { "MC",  "fr-MC" },
 | 
			
		||||
            { "ME",  "sr-Latn-ME" },
 | 
			
		||||
            { "MK",  "mk-MK" },
 | 
			
		||||
            { "MN",  "mn-MN" },
 | 
			
		||||
            { "MO",  "zh-MO" },
 | 
			
		||||
            { "MT",  "mt-MT" },
 | 
			
		||||
            { "MV",  "dv-MV" },
 | 
			
		||||
            { "MX",  "es-MX" },
 | 
			
		||||
            { "MY",  "ms-MY" },
 | 
			
		||||
            { "NG",  "ig-NG" },
 | 
			
		||||
            { "NI",  "es-NI" },
 | 
			
		||||
            { "NL",  "nl-NL" },
 | 
			
		||||
            { "NO",  "nn-NO" },
 | 
			
		||||
            { "NP",  "ne-NP" },
 | 
			
		||||
            { "NZ",  "en-NZ" },
 | 
			
		||||
            { "OM",  "ar-OM" },
 | 
			
		||||
            { "PA",  "es-PA" },
 | 
			
		||||
            { "PE",  "es-PE" },
 | 
			
		||||
            { "PH",  "en-PH" },
 | 
			
		||||
            { "PK",  "ur-PK" },
 | 
			
		||||
            { "PL",  "pl-PL" },
 | 
			
		||||
            { "PR",  "es-PR" },
 | 
			
		||||
            { "PT",  "pt-PT" },
 | 
			
		||||
            { "PY",  "es-PY" },
 | 
			
		||||
            { "QA",  "ar-QA" },
 | 
			
		||||
            { "RO",  "ro-RO" },
 | 
			
		||||
            { "RS",  "sr-Latn-RS" },
 | 
			
		||||
            { "RU",  "ru-RU" },
 | 
			
		||||
            { "RW",  "rw-RW" },
 | 
			
		||||
            { "SA",  "ar-SA" },
 | 
			
		||||
            { "SE",  "sv-SE" },
 | 
			
		||||
            { "SG",  "zh-SG" },
 | 
			
		||||
            { "SI",  "sl-SI" },
 | 
			
		||||
            { "SK",  "sk-SK" },
 | 
			
		||||
            { "SN",  "wo-SN" },
 | 
			
		||||
            { "SV",  "es-SV" },
 | 
			
		||||
            { "SY",  "ar-SY" },
 | 
			
		||||
            { "TH",  "th-TH" },
 | 
			
		||||
            { "TJ",  "tg-Cyrl-TJ" },
 | 
			
		||||
            { "TM",  "tk-TM" },
 | 
			
		||||
            { "TN",  "ar-TN" },
 | 
			
		||||
            { "TR",  "tr-TR" },
 | 
			
		||||
            { "TT",  "en-TT" },
 | 
			
		||||
            { "TW",  "zh-TW" },
 | 
			
		||||
            { "UA",  "uk-UA" },
 | 
			
		||||
            { "US",  "en-US" },
 | 
			
		||||
            { "UY",  "es-UY" },
 | 
			
		||||
            { "UZ",  "uz-Cyrl-UZ" },
 | 
			
		||||
            { "VE",  "es-VE" },
 | 
			
		||||
            { "VN",  "vi-VN" },
 | 
			
		||||
            { "YE",  "ar-YE" },
 | 
			
		||||
            { "ZA",  "af-ZA" },
 | 
			
		||||
            { "ZW",  "en-ZW" }
 | 
			
		||||
 */
 | 
			
		||||
@@ -1,231 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class MuteCommands : NadekoModule<MuteService>
 | 
			
		||||
    {
 | 
			
		||||
        private async Task<bool> VerifyMutePermissions(IGuildUser runnerUser, IGuildUser targetUser)
 | 
			
		||||
        {
 | 
			
		||||
            var runnerUserRoles = runnerUser.GetRoles();
 | 
			
		||||
            var targetUserRoles = targetUser.GetRoles();
 | 
			
		||||
            if (runnerUser.Id != ctx.Guild.OwnerId
 | 
			
		||||
                && runnerUserRoles.Max(x => x.Position) <= targetUserRoles.Max(x => x.Position))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_perms);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task MuteRole([Leftover] IRole role = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (role is null)
 | 
			
		||||
            {
 | 
			
		||||
                var muteRole = await _service.GetMuteRole(ctx.Guild);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.mute_role(Format.Code(muteRole.Name)));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ctx.User.Id != ctx.Guild.OwnerId
 | 
			
		||||
                && role.Position >= ((SocketGuildUser)ctx.User).Roles.Max(x => x.Position))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.insuf_perms_u);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _service.SetMuteRoleAsync(ctx.Guild.Id, role.Name);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.mute_role_set);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Mute(IGuildUser target, [Leftover] string reason = "")
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!await VerifyMutePermissions((IGuildUser)ctx.User, target))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await _service.MuteUser(target, ctx.User, reason: reason);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.user_muted(Format.Bold(target.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Exception in the mute command");
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task Mute(StoopidTime time, IGuildUser user, [Leftover] string reason = "")
 | 
			
		||||
        {
 | 
			
		||||
            if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49))
 | 
			
		||||
                return;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await _service.TimedMute(user, ctx.User, time.Time, reason: reason);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.user_muted_time(Format.Bold(user.ToString()),
 | 
			
		||||
                    (int)time.Time.TotalMinutes));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error in mute command");
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
 | 
			
		||||
        public async Task Unmute(IGuildUser user, [Leftover] string reason = "")
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, reason: reason);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.user_unmuted(Format.Bold(user.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task ChatMute(IGuildUser user, [Leftover] string reason = "")
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await _service.MuteUser(user, ctx.User, MuteType.Chat, reason);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.user_chat_mute(Format.Bold(user.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Exception in the chatmute command");
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task ChatMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "")
 | 
			
		||||
        {
 | 
			
		||||
            if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49))
 | 
			
		||||
                return;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await _service.TimedMute(user, ctx.User, time.Time, MuteType.Chat, reason);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.user_chat_mute_time(Format.Bold(user.ToString()),
 | 
			
		||||
                    (int)time.Time.TotalMinutes));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error in chatmute command");
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task ChatUnmute(IGuildUser user, [Leftover] string reason = "")
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _service.UnmuteUser(user.Guild.Id, user.Id, ctx.User, MuteType.Chat, reason);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.user_chat_unmute(Format.Bold(user.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.MuteMembers)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task VoiceMute(IGuildUser user, [Leftover] string reason = "")
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await _service.MuteUser(user, ctx.User, MuteType.Voice, reason);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.user_voice_mute(Format.Bold(user.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.MuteMembers)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task VoiceMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "")
 | 
			
		||||
        {
 | 
			
		||||
            if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49))
 | 
			
		||||
                return;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await _service.TimedMute(user, ctx.User, time.Time, MuteType.Voice, reason);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.user_voice_mute_time(Format.Bold(user.ToString()),
 | 
			
		||||
                    (int)time.Time.TotalMinutes));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.MuteMembers)]
 | 
			
		||||
        public async Task VoiceUnmute(IGuildUser user, [Leftover] string reason = "")
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, MuteType.Voice, reason);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.user_voice_unmute(Format.Bold(user.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.mute_error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,506 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public enum MuteType
 | 
			
		||||
{
 | 
			
		||||
    Voice,
 | 
			
		||||
    Chat,
 | 
			
		||||
    All
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class MuteService : INService
 | 
			
		||||
{
 | 
			
		||||
    public enum TimerType { Mute, Ban, AddRole }
 | 
			
		||||
 | 
			
		||||
    private static readonly OverwritePermissions _denyOverwrite = new(addReactions: PermValue.Deny,
 | 
			
		||||
        sendMessages: PermValue.Deny,
 | 
			
		||||
        sendMessagesInThreads: PermValue.Deny,
 | 
			
		||||
        attachFiles: PermValue.Deny);
 | 
			
		||||
 | 
			
		||||
    public event Action<IGuildUser, IUser, MuteType, string> UserMuted = delegate { };
 | 
			
		||||
    public event Action<IGuildUser, IUser, MuteType, string> UserUnmuted = delegate { };
 | 
			
		||||
 | 
			
		||||
    public ConcurrentDictionary<ulong, string> GuildMuteRoles { get; }
 | 
			
		||||
    public ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> MutedUsers { get; }
 | 
			
		||||
 | 
			
		||||
    public ConcurrentDictionary<ulong, ConcurrentDictionary<(ulong, TimerType), Timer>> UnTimers { get; } = new();
 | 
			
		||||
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
 | 
			
		||||
    public MuteService(DiscordSocketClient client, DbService db, IEmbedBuilderService eb)
 | 
			
		||||
    {
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _eb = eb;
 | 
			
		||||
 | 
			
		||||
        using (var uow = db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var guildIds = client.Guilds.Select(x => x.Id).ToList();
 | 
			
		||||
            var configs = uow.Set<GuildConfig>()
 | 
			
		||||
                             .AsNoTracking()
 | 
			
		||||
                             .AsSplitQuery()
 | 
			
		||||
                             .Include(x => x.MutedUsers)
 | 
			
		||||
                             .Include(x => x.UnbanTimer)
 | 
			
		||||
                             .Include(x => x.UnmuteTimers)
 | 
			
		||||
                             .Include(x => x.UnroleTimer)
 | 
			
		||||
                             .Where(x => guildIds.Contains(x.GuildId))
 | 
			
		||||
                             .ToList();
 | 
			
		||||
 | 
			
		||||
            GuildMuteRoles = configs.Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName))
 | 
			
		||||
                                    .ToDictionary(c => c.GuildId, c => c.MuteRoleName)
 | 
			
		||||
                                    .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
            MutedUsers = new(configs.ToDictionary(k => k.GuildId,
 | 
			
		||||
                v => new ConcurrentHashSet<ulong>(v.MutedUsers.Select(m => m.UserId))));
 | 
			
		||||
 | 
			
		||||
            var max = TimeSpan.FromDays(49);
 | 
			
		||||
 | 
			
		||||
            foreach (var conf in configs)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var x in conf.UnmuteTimers)
 | 
			
		||||
                {
 | 
			
		||||
                    TimeSpan after;
 | 
			
		||||
                    if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
 | 
			
		||||
                        after = TimeSpan.FromMinutes(2);
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        var unmute = x.UnmuteAt - DateTime.UtcNow;
 | 
			
		||||
                        after = unmute > max ? max : unmute;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Mute);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                foreach (var x in conf.UnbanTimer)
 | 
			
		||||
                {
 | 
			
		||||
                    TimeSpan after;
 | 
			
		||||
                    if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
 | 
			
		||||
                        after = TimeSpan.FromMinutes(2);
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        var unban = x.UnbanAt - DateTime.UtcNow;
 | 
			
		||||
                        after = unban > max ? max : unban;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Ban);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                foreach (var x in conf.UnroleTimer)
 | 
			
		||||
                {
 | 
			
		||||
                    TimeSpan after;
 | 
			
		||||
                    if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
 | 
			
		||||
                        after = TimeSpan.FromMinutes(2);
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        var unban = x.UnbanAt - DateTime.UtcNow;
 | 
			
		||||
                        after = unban > max ? max : unban;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.AddRole, x.RoleId);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _client.UserJoined += Client_UserJoined;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        UserMuted += OnUserMuted;
 | 
			
		||||
        UserUnmuted += OnUserUnmuted;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void OnUserMuted(
 | 
			
		||||
        IGuildUser user,
 | 
			
		||||
        IUser mod,
 | 
			
		||||
        MuteType type,
 | 
			
		||||
        string reason)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(reason))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create()
 | 
			
		||||
                                                           .WithDescription(
 | 
			
		||||
                                                               $"You've been muted in {user.Guild} server")
 | 
			
		||||
                                                           .AddField("Mute Type", type.ToString())
 | 
			
		||||
                                                           .AddField("Moderator", mod.ToString())
 | 
			
		||||
                                                           .AddField("Reason", reason)
 | 
			
		||||
                                                           .Build()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void OnUserUnmuted(
 | 
			
		||||
        IGuildUser user,
 | 
			
		||||
        IUser mod,
 | 
			
		||||
        MuteType type,
 | 
			
		||||
        string reason)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(reason))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create()
 | 
			
		||||
                                                           .WithDescription(
 | 
			
		||||
                                                               $"You've been unmuted in {user.Guild} server")
 | 
			
		||||
                                                           .AddField("Unmute Type", type.ToString())
 | 
			
		||||
                                                           .AddField("Moderator", mod.ToString())
 | 
			
		||||
                                                           .AddField("Reason", reason)
 | 
			
		||||
                                                           .Build()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task Client_UserJoined(IGuildUser usr)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            MutedUsers.TryGetValue(usr.Guild.Id, out var muted);
 | 
			
		||||
 | 
			
		||||
            if (muted is null || !muted.Contains(usr.Id))
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            _ = Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute"));
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex, "Error in MuteService UserJoined event");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetMuteRoleAsync(ulong guildId, string name)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var config = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        config.MuteRoleName = name;
 | 
			
		||||
        GuildMuteRoles.AddOrUpdate(guildId, name, (_, _) => name);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task MuteUser(
 | 
			
		||||
        IGuildUser usr,
 | 
			
		||||
        IUser mod,
 | 
			
		||||
        MuteType type = MuteType.All,
 | 
			
		||||
        string reason = "")
 | 
			
		||||
    {
 | 
			
		||||
        if (type == MuteType.All)
 | 
			
		||||
        {
 | 
			
		||||
            try { await usr.ModifyAsync(x => x.Mute = true); }
 | 
			
		||||
            catch { }
 | 
			
		||||
 | 
			
		||||
            var muteRole = await GetMuteRole(usr.Guild);
 | 
			
		||||
            if (!usr.RoleIds.Contains(muteRole.Id))
 | 
			
		||||
                await usr.AddRoleAsync(muteRole);
 | 
			
		||||
            StopTimer(usr.GuildId, usr.Id, TimerType.Mute);
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(usr.Guild.Id,
 | 
			
		||||
                    set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers));
 | 
			
		||||
                config.MutedUsers.Add(new()
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = usr.Id
 | 
			
		||||
                });
 | 
			
		||||
                if (MutedUsers.TryGetValue(usr.Guild.Id, out var muted))
 | 
			
		||||
                    muted.Add(usr.Id);
 | 
			
		||||
 | 
			
		||||
                config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id);
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            UserMuted(usr, mod, MuteType.All, reason);
 | 
			
		||||
        }
 | 
			
		||||
        else if (type == MuteType.Voice)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await usr.ModifyAsync(x => x.Mute = true);
 | 
			
		||||
                UserMuted(usr, mod, MuteType.Voice, reason);
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
        }
 | 
			
		||||
        else if (type == MuteType.Chat)
 | 
			
		||||
        {
 | 
			
		||||
            await usr.AddRoleAsync(await GetMuteRole(usr.Guild));
 | 
			
		||||
            UserMuted(usr, mod, MuteType.Chat, reason);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task UnmuteUser(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        ulong usrId,
 | 
			
		||||
        IUser mod,
 | 
			
		||||
        MuteType type = MuteType.All,
 | 
			
		||||
        string reason = "")
 | 
			
		||||
    {
 | 
			
		||||
        var usr = _client.GetGuild(guildId)?.GetUser(usrId);
 | 
			
		||||
        if (type == MuteType.All)
 | 
			
		||||
        {
 | 
			
		||||
            StopTimer(guildId, usrId, TimerType.Mute);
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(guildId,
 | 
			
		||||
                    set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers));
 | 
			
		||||
                var match = new MutedUserId
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = usrId
 | 
			
		||||
                };
 | 
			
		||||
                var toRemove = config.MutedUsers.FirstOrDefault(x => x.Equals(match));
 | 
			
		||||
                if (toRemove is not null)
 | 
			
		||||
                    uow.Remove(toRemove);
 | 
			
		||||
                if (MutedUsers.TryGetValue(guildId, out var muted))
 | 
			
		||||
                    muted.TryRemove(usrId);
 | 
			
		||||
 | 
			
		||||
                config.UnmuteTimers.RemoveWhere(x => x.UserId == usrId);
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (usr is not null)
 | 
			
		||||
            {
 | 
			
		||||
                try { await usr.ModifyAsync(x => x.Mute = false); }
 | 
			
		||||
                catch { }
 | 
			
		||||
 | 
			
		||||
                try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild)); }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    /*ignore*/
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                UserUnmuted(usr, mod, MuteType.All, reason);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (type == MuteType.Voice)
 | 
			
		||||
        {
 | 
			
		||||
            if (usr is null)
 | 
			
		||||
                return;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await usr.ModifyAsync(x => x.Mute = false);
 | 
			
		||||
                UserUnmuted(usr, mod, MuteType.Voice, reason);
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
        }
 | 
			
		||||
        else if (type == MuteType.Chat)
 | 
			
		||||
        {
 | 
			
		||||
            if (usr is null)
 | 
			
		||||
                return;
 | 
			
		||||
            await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild));
 | 
			
		||||
            UserUnmuted(usr, mod, MuteType.Chat, reason);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IRole> GetMuteRole(IGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is null)
 | 
			
		||||
            throw new ArgumentNullException(nameof(guild));
 | 
			
		||||
 | 
			
		||||
        const string defaultMuteRoleName = "nadeko-mute";
 | 
			
		||||
 | 
			
		||||
        var muteRoleName = GuildMuteRoles.GetOrAdd(guild.Id, defaultMuteRoleName);
 | 
			
		||||
 | 
			
		||||
        var muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName);
 | 
			
		||||
        if (muteRole is null)
 | 
			
		||||
            //if it doesn't exist, create it
 | 
			
		||||
        {
 | 
			
		||||
            try { muteRole = await guild.CreateRoleAsync(muteRoleName, isMentionable: false); }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                //if creations fails,  maybe the name is not correct, find default one, if doesn't work, create default one
 | 
			
		||||
                muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName)
 | 
			
		||||
                           ?? await guild.CreateRoleAsync(defaultMuteRoleName, isMentionable: false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var toOverwrite in await guild.GetTextChannelsAsync())
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id
 | 
			
		||||
                                                               && x.TargetType == PermissionTarget.Role))
 | 
			
		||||
                {
 | 
			
		||||
                    await toOverwrite.AddPermissionOverwriteAsync(muteRole, _denyOverwrite);
 | 
			
		||||
 | 
			
		||||
                    await Task.Delay(200);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return muteRole;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task TimedMute(
 | 
			
		||||
        IGuildUser user,
 | 
			
		||||
        IUser mod,
 | 
			
		||||
        TimeSpan after,
 | 
			
		||||
        MuteType muteType = MuteType.All,
 | 
			
		||||
        string reason = "")
 | 
			
		||||
    {
 | 
			
		||||
        await MuteUser(user, mod, muteType, reason); // mute the user. This will also remove any previous unmute timers
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnmuteTimers));
 | 
			
		||||
            config.UnmuteTimers.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                UserId = user.Id,
 | 
			
		||||
                UnmuteAt = DateTime.UtcNow + after
 | 
			
		||||
            }); // add teh unmute timer to the database
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        StartUn_Timer(user.GuildId, user.Id, after, TimerType.Mute); // start the timer
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task TimedBan(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        TimeSpan after,
 | 
			
		||||
        string reason,
 | 
			
		||||
        int pruneDays)
 | 
			
		||||
    {
 | 
			
		||||
        await guild.AddBanAsync(userId, pruneDays, reason);
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.UnbanTimer));
 | 
			
		||||
            config.UnbanTimer.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                UserId = userId,
 | 
			
		||||
                UnbanAt = DateTime.UtcNow + after
 | 
			
		||||
            }); // add teh unmute timer to the database
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        StartUn_Timer(guild.Id, userId, after, TimerType.Ban); // start the timer
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task TimedRole(
 | 
			
		||||
        IGuildUser user,
 | 
			
		||||
        TimeSpan after,
 | 
			
		||||
        string reason,
 | 
			
		||||
        IRole role)
 | 
			
		||||
    {
 | 
			
		||||
        await user.AddRoleAsync(role);
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnroleTimer));
 | 
			
		||||
            config.UnroleTimer.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                UserId = user.Id,
 | 
			
		||||
                UnbanAt = DateTime.UtcNow + after,
 | 
			
		||||
                RoleId = role.Id
 | 
			
		||||
            }); // add teh unmute timer to the database
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        StartUn_Timer(user.GuildId, user.Id, after, TimerType.AddRole, role.Id); // start the timer
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void StartUn_Timer(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        TimeSpan after,
 | 
			
		||||
        TimerType type,
 | 
			
		||||
        ulong? roleId = null)
 | 
			
		||||
    {
 | 
			
		||||
        //load the unmute timers for this guild
 | 
			
		||||
        var userUnTimers = UnTimers.GetOrAdd(guildId, new ConcurrentDictionary<(ulong, TimerType), Timer>());
 | 
			
		||||
 | 
			
		||||
        //unmute timer to be added
 | 
			
		||||
        var toAdd = new Timer(async _ =>
 | 
			
		||||
            {
 | 
			
		||||
                if (type == TimerType.Ban)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        RemoveTimerFromDb(guildId, userId, type);
 | 
			
		||||
                        StopTimer(guildId, userId, type);
 | 
			
		||||
                        var guild = _client.GetGuild(guildId); // load the guild
 | 
			
		||||
                        if (guild is not null)
 | 
			
		||||
                            await guild.RemoveBanAsync(userId);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex, "Couldn't unban user {UserId} in guild {GuildId}", userId, guildId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (type == TimerType.AddRole)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        if (roleId is null)
 | 
			
		||||
                            return;
 | 
			
		||||
 | 
			
		||||
                        RemoveTimerFromDb(guildId, userId, type);
 | 
			
		||||
                        StopTimer(guildId, userId, type);
 | 
			
		||||
                        var guild = _client.GetGuild(guildId);
 | 
			
		||||
                        var user = guild?.GetUser(userId);
 | 
			
		||||
                        var role = guild?.GetRole(roleId.Value);
 | 
			
		||||
                        if (guild is not null && user is not null && user.Roles.Contains(role))
 | 
			
		||||
                            await user.RemoveRoleAsync(role);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex, "Couldn't remove role from user {UserId} in guild {GuildId}", userId, guildId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        // unmute the user, this will also remove the timer from the db
 | 
			
		||||
                        await UnmuteUser(guildId, userId, _client.CurrentUser, reason: "Timed mute expired");
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        RemoveTimerFromDb(guildId, userId, type); // if unmute errored, just remove unmute from db
 | 
			
		||||
                        Log.Warning(ex, "Couldn't unmute user {UserId} in guild {GuildId}", userId, guildId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            null,
 | 
			
		||||
            after,
 | 
			
		||||
            Timeout.InfiniteTimeSpan);
 | 
			
		||||
 | 
			
		||||
        //add it, or stop the old one and add this one
 | 
			
		||||
        userUnTimers.AddOrUpdate((userId, type),
 | 
			
		||||
            _ => toAdd,
 | 
			
		||||
            (_, old) =>
 | 
			
		||||
            {
 | 
			
		||||
                old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                return toAdd;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void StopTimer(ulong guildId, ulong userId, TimerType type)
 | 
			
		||||
    {
 | 
			
		||||
        if (!UnTimers.TryGetValue(guildId, out var userTimer))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (userTimer.TryRemove((userId, type), out var removed))
 | 
			
		||||
            removed.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void RemoveTimerFromDb(ulong guildId, ulong userId, TimerType type)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        object toDelete;
 | 
			
		||||
        if (type == TimerType.Mute)
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnmuteTimers));
 | 
			
		||||
            toDelete = config.UnmuteTimers.FirstOrDefault(x => x.UserId == userId);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnbanTimer));
 | 
			
		||||
            toDelete = config.UnbanTimer.FirstOrDefault(x => x.UserId == userId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (toDelete is not null)
 | 
			
		||||
            uow.Remove(toDelete);
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,83 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using Nadeko.Bot.Db;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class DiscordPermOverrideCommands : NadekoModule<DiscordPermOverrideService>
 | 
			
		||||
    {
 | 
			
		||||
        // override stats, it should require that the user has managessages guild permission
 | 
			
		||||
        // .po 'stats' add user guild managemessages
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task DiscordPermOverride(CommandOrExprInfo cmd, params GuildPerm[] perms)
 | 
			
		||||
        {
 | 
			
		||||
            if (perms is null || perms.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await _service.RemoveOverride(ctx.Guild.Id, cmd.Name);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.perm_override_reset);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var aggregatePerms = perms.Aggregate((acc, seed) => seed | acc);
 | 
			
		||||
            await _service.AddOverride(ctx.Guild.Id, cmd.Name, aggregatePerms);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.perm_override(Format.Bold(aggregatePerms.ToString()),
 | 
			
		||||
                Format.Code(cmd.Name)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task DiscordPermOverrideReset()
 | 
			
		||||
        {
 | 
			
		||||
            var result = await PromptUserConfirmAsync(_eb.Create()
 | 
			
		||||
                                                         .WithOkColor()
 | 
			
		||||
                                                         .WithDescription(GetText(strs.perm_override_all_confirm)));
 | 
			
		||||
 | 
			
		||||
            if (!result)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await _service.ClearAllOverrides(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.perm_override_all);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task DiscordPermOverrideList(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var overrides = await _service.GetAllOverrides(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
                curPage =>
 | 
			
		||||
                {
 | 
			
		||||
                    var eb = _eb.Create().WithTitle(GetText(strs.perm_overrides)).WithOkColor();
 | 
			
		||||
 | 
			
		||||
                    var thisPageOverrides = overrides.Skip(9 * curPage).Take(9).ToList();
 | 
			
		||||
 | 
			
		||||
                    if (thisPageOverrides.Count == 0)
 | 
			
		||||
                        eb.WithDescription(GetText(strs.perm_override_page_none));
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        eb.WithDescription(thisPageOverrides.Select(ov => $"{ov.Command} => {ov.Perm.ToString()}")
 | 
			
		||||
                                                            .Join("\n"));
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return eb;
 | 
			
		||||
                },
 | 
			
		||||
                overrides.Count,
 | 
			
		||||
                9);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,136 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class DiscordPermOverrideService : INService, IExecPreCommand, IDiscordPermOverrideService
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; } = int.MaxValue;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly IServiceProvider _services;
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<(ulong, string), DiscordPermOverride> _overrides;
 | 
			
		||||
 | 
			
		||||
    public DiscordPermOverrideService(DbService db, IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _services = services;
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        _overrides = uow.DiscordPermOverrides.AsNoTracking()
 | 
			
		||||
                        .AsEnumerable()
 | 
			
		||||
                        .ToDictionary(o => (o.GuildId ?? 0, o.Command), o => o)
 | 
			
		||||
                        .ToConcurrent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryGetOverrides(ulong guildId, string commandName, out Nadeko.Bot.Db.GuildPerm? perm)
 | 
			
		||||
    {
 | 
			
		||||
        commandName = commandName.ToLowerInvariant();
 | 
			
		||||
        if (_overrides.TryGetValue((guildId, commandName), out var dpo))
 | 
			
		||||
        {
 | 
			
		||||
            perm = dpo.Perm;
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        perm = null;
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<PreconditionResult> ExecuteOverrides(
 | 
			
		||||
        ICommandContext ctx,
 | 
			
		||||
        CommandInfo command,
 | 
			
		||||
        GuildPerm perms,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        var rupa = new RequireUserPermissionAttribute(perms);
 | 
			
		||||
        return rupa.CheckPermissionsAsync(ctx, command, services);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task AddOverride(ulong guildId, string commandName, GuildPerm perm)
 | 
			
		||||
    {
 | 
			
		||||
        commandName = commandName.ToLowerInvariant();
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var over = await uow.Set<DiscordPermOverride>()
 | 
			
		||||
                            .AsQueryable()
 | 
			
		||||
                            .FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command);
 | 
			
		||||
 | 
			
		||||
        if (over is null)
 | 
			
		||||
        {
 | 
			
		||||
            uow.Set<DiscordPermOverride>()
 | 
			
		||||
               .Add(over = new()
 | 
			
		||||
               {
 | 
			
		||||
                   Command = commandName,
 | 
			
		||||
                   Perm = (Nadeko.Bot.Db.GuildPerm)perm,
 | 
			
		||||
                   GuildId = guildId
 | 
			
		||||
               });
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
            over.Perm = (Nadeko.Bot.Db.GuildPerm)perm;
 | 
			
		||||
 | 
			
		||||
        _overrides[(guildId, commandName)] = over;
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task ClearAllOverrides(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var overrides = await uow.Set<DiscordPermOverride>()
 | 
			
		||||
                                 .AsQueryable()
 | 
			
		||||
                                 .AsNoTracking()
 | 
			
		||||
                                 .Where(x => x.GuildId == guildId)
 | 
			
		||||
                                 .ToListAsync();
 | 
			
		||||
 | 
			
		||||
        uow.RemoveRange(overrides);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        foreach (var over in overrides)
 | 
			
		||||
            _overrides.TryRemove((guildId, over.Command), out _);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task RemoveOverride(ulong guildId, string commandName)
 | 
			
		||||
    {
 | 
			
		||||
        commandName = commandName.ToLowerInvariant();
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var over = await uow.Set<DiscordPermOverride>()
 | 
			
		||||
                            .AsQueryable()
 | 
			
		||||
                            .AsNoTracking()
 | 
			
		||||
                            .FirstOrDefaultAsync(x => x.GuildId == guildId && x.Command == commandName);
 | 
			
		||||
 | 
			
		||||
        if (over is null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        uow.Remove(over);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        _overrides.TryRemove((guildId, commandName), out _);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<DiscordPermOverride>> GetAllOverrides(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        return await uow.Set<DiscordPermOverride>()
 | 
			
		||||
                        .AsQueryable()
 | 
			
		||||
                        .AsNoTracking()
 | 
			
		||||
                        .Where(x => x.GuildId == guildId)
 | 
			
		||||
                        .ToListAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
 | 
			
		||||
    {
 | 
			
		||||
        if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && perm is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var result =
 | 
			
		||||
                await new RequireUserPermissionAttribute((GuildPermission)perm).CheckPermissionsAsync(context,
 | 
			
		||||
                    command,
 | 
			
		||||
                    _services);
 | 
			
		||||
 | 
			
		||||
            return !result.IsSuccess;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,60 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class PlayingRotateCommands : NadekoModule<PlayingRotateService>
 | 
			
		||||
    {
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task RotatePlaying()
 | 
			
		||||
        {
 | 
			
		||||
            if (_service.ToggleRotatePlaying())
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.ropl_enabled);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.ropl_disabled);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task AddPlaying(ActivityType t, [Leftover] string status)
 | 
			
		||||
        {
 | 
			
		||||
            await _service.AddPlaying(t, status);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.ropl_added);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task ListPlaying()
 | 
			
		||||
        {
 | 
			
		||||
            var statuses = _service.GetRotatingStatuses();
 | 
			
		||||
 | 
			
		||||
            if (!statuses.Any())
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.ropl_not_set);
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var i = 1;
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.ropl_list(string.Join("\n\t",
 | 
			
		||||
                    statuses.Select(rs => $"`{i++}.` *{rs.Type}* {rs.Status}"))));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task RemovePlaying(int index)
 | 
			
		||||
        {
 | 
			
		||||
            index -= 1;
 | 
			
		||||
 | 
			
		||||
            var msg = await _service.RemovePlayingAsync(index);
 | 
			
		||||
 | 
			
		||||
            if (msg is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.reprm(msg));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,109 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class PlayingRotateService : INService, IReadyExecutor
 | 
			
		||||
{
 | 
			
		||||
    private readonly BotConfigService _bss;
 | 
			
		||||
    private readonly SelfService _selfService;
 | 
			
		||||
    private readonly Replacer _rep;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
    public PlayingRotateService(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        BotConfigService bss,
 | 
			
		||||
        IEnumerable<IPlaceholderProvider> phProviders,
 | 
			
		||||
        SelfService selfService)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _bss = bss;
 | 
			
		||||
        _selfService = selfService;
 | 
			
		||||
        _client = client;
 | 
			
		||||
 | 
			
		||||
        if (client.ShardId == 0)
 | 
			
		||||
            _rep = new ReplacementBuilder().WithClient(client).WithProviders(phProviders).Build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (_client.ShardId != 0)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
 | 
			
		||||
        var index = 0;
 | 
			
		||||
        while (await timer.WaitForNextTickAsync())
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!_bss.Data.RotateStatuses)
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                IReadOnlyList<RotatingPlayingStatus> rotatingStatuses;
 | 
			
		||||
                await using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    rotatingStatuses = uow.RotatingStatus.AsNoTracking().OrderBy(x => x.Id).ToList();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (rotatingStatuses.Count == 0)
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                var playingStatus = index >= rotatingStatuses.Count
 | 
			
		||||
                    ? rotatingStatuses[index = 0]
 | 
			
		||||
                    : rotatingStatuses[index++];
 | 
			
		||||
 | 
			
		||||
                var statusText = _rep.Replace(playingStatus.Status);
 | 
			
		||||
                await _selfService.SetGameAsync(statusText, (ActivityType)playingStatus.Type);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<string> RemovePlayingAsync(int index)
 | 
			
		||||
    {
 | 
			
		||||
        if (index < 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(index));
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var toRemove = await uow.RotatingStatus.AsQueryable().AsNoTracking().Skip(index).FirstOrDefaultAsync();
 | 
			
		||||
 | 
			
		||||
        if (toRemove is null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        uow.Remove(toRemove);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return toRemove.Status;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task AddPlaying(ActivityType activityType, string status)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var toAdd = new RotatingPlayingStatus
 | 
			
		||||
        {
 | 
			
		||||
            Status = status,
 | 
			
		||||
            Type = (Nadeko.Bot.Db.ActivityType)activityType
 | 
			
		||||
        };
 | 
			
		||||
        uow.Add(toAdd);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool ToggleRotatePlaying()
 | 
			
		||||
    {
 | 
			
		||||
        var enabled = false;
 | 
			
		||||
        _bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; });
 | 
			
		||||
        return enabled;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses()
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.RotatingStatus.AsNoTracking().ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,57 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class PrefixCommands : NadekoModule
 | 
			
		||||
    {
 | 
			
		||||
        public enum Set
 | 
			
		||||
        {
 | 
			
		||||
            Set
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task Prefix()
 | 
			
		||||
            => await ReplyConfirmLocalizedAsync(strs.prefix_current(Format.Code(_cmdHandler.GetPrefix(ctx.Guild))));
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task Prefix(Set _, [Leftover] string newPrefix)
 | 
			
		||||
            => Prefix(newPrefix);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Prefix([Leftover] string toSet)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(prefix))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var oldPrefix = prefix;
 | 
			
		||||
            var newPrefix = _cmdHandler.SetPrefix(ctx.Guild, toSet);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.prefix_new(Format.Code(oldPrefix), Format.Code(newPrefix)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task DefPrefix([Leftover] string toSet = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(toSet))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.defprefix_current(_cmdHandler.GetPrefix()));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var oldPrefix = _cmdHandler.GetPrefix();
 | 
			
		||||
            var newPrefix = _cmdHandler.SetDefaultPrefix(toSet);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.defprefix_new(Format.Code(oldPrefix), Format.Code(newPrefix)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,288 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class ProtectionCommands : NadekoModule<ProtectionService>
 | 
			
		||||
    {
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task AntiAlt()
 | 
			
		||||
        {
 | 
			
		||||
            if (await _service.TryStopAntiAlt(ctx.Guild.Id))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Alt"));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.protection_not_running("Anti-Alt"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task AntiAlt(
 | 
			
		||||
            StoopidTime minAge,
 | 
			
		||||
            PunishmentAction action,
 | 
			
		||||
            [Leftover] StoopidTime punishTime = null)
 | 
			
		||||
        {
 | 
			
		||||
            var minAgeMinutes = (int)minAge.Time.TotalMinutes;
 | 
			
		||||
            var punishTimeMinutes = (int?)punishTime?.Time.TotalMinutes ?? 0;
 | 
			
		||||
 | 
			
		||||
            if (minAgeMinutes < 1 || punishTimeMinutes < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var minutes = (int?)punishTime?.Time.TotalMinutes ?? 0;
 | 
			
		||||
            if (action is PunishmentAction.TimeOut && minutes < 1)
 | 
			
		||||
                minutes = 1;
 | 
			
		||||
 | 
			
		||||
            await _service.StartAntiAltAsync(ctx.Guild.Id,
 | 
			
		||||
                minAgeMinutes,
 | 
			
		||||
                action,
 | 
			
		||||
                minutes);
 | 
			
		||||
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var minAgeMinutes = (int)minAge.Time.TotalMinutes;
 | 
			
		||||
 | 
			
		||||
            if (minAgeMinutes < 1)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (action == PunishmentAction.TimeOut)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await _service.StartAntiAltAsync(ctx.Guild.Id, minAgeMinutes, action, roleId: role.Id);
 | 
			
		||||
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public Task AntiRaid()
 | 
			
		||||
        {
 | 
			
		||||
            if (_service.TryStopAntiRaid(ctx.Guild.Id))
 | 
			
		||||
                return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Raid"));
 | 
			
		||||
            return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Raid"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task AntiRaid(
 | 
			
		||||
            int userThreshold,
 | 
			
		||||
            int seconds,
 | 
			
		||||
            PunishmentAction action,
 | 
			
		||||
            [Leftover] StoopidTime punishTime)
 | 
			
		||||
            => InternalAntiRaid(userThreshold, seconds, action, punishTime);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public Task AntiRaid(int userThreshold, int seconds, PunishmentAction action)
 | 
			
		||||
            => InternalAntiRaid(userThreshold, seconds, action);
 | 
			
		||||
 | 
			
		||||
        private async Task InternalAntiRaid(
 | 
			
		||||
            int userThreshold,
 | 
			
		||||
            int seconds = 10,
 | 
			
		||||
            PunishmentAction action = PunishmentAction.Mute,
 | 
			
		||||
            StoopidTime punishTime = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (action == PunishmentAction.AddRole)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.punishment_unsupported(action));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (userThreshold is < 2 or > 30)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.raid_cnt(2, 30));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (seconds is < 2 or > 300)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.raid_time(2, 300));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (punishTime is not null)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_service.IsDurationAllowed(action))
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.prot_cant_use_time);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var time = (int?)punishTime?.Time.TotalMinutes ?? 0;
 | 
			
		||||
            if (time is < 0 or > 60 * 24)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            if(action is PunishmentAction.TimeOut && time < 1)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var stats = await _service.StartAntiRaidAsync(ctx.Guild.Id, userThreshold, seconds, action, time);
 | 
			
		||||
 | 
			
		||||
            if (stats is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await SendConfirmAsync(GetText(strs.prot_enable("Anti-Raid")),
 | 
			
		||||
                $"{ctx.User.Mention} {GetAntiRaidString(stats)}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public Task AntiSpam()
 | 
			
		||||
        {
 | 
			
		||||
            if (_service.TryStopAntiSpam(ctx.Guild.Id))
 | 
			
		||||
                return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Spam"));
 | 
			
		||||
            return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Spam"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            if (action != PunishmentAction.AddRole)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            return InternalAntiSpam(messageCount, action, null, role);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime)
 | 
			
		||||
            => InternalAntiSpam(messageCount, action, punishTime);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public Task AntiSpam(int messageCount, PunishmentAction action)
 | 
			
		||||
            => InternalAntiSpam(messageCount, action);
 | 
			
		||||
 | 
			
		||||
        private async Task InternalAntiSpam(
 | 
			
		||||
            int messageCount,
 | 
			
		||||
            PunishmentAction action,
 | 
			
		||||
            StoopidTime timeData = null,
 | 
			
		||||
            IRole role = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (messageCount is < 2 or > 10)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (timeData is not null)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_service.IsDurationAllowed(action))
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.prot_cant_use_time);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var time = (int?)timeData?.Time.TotalMinutes ?? 0;
 | 
			
		||||
            if (time is < 0 or > 60 * 24)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (action is PunishmentAction.TimeOut && time < 1)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var stats = await _service.StartAntiSpamAsync(ctx.Guild.Id, messageCount, action, time, role?.Id);
 | 
			
		||||
 | 
			
		||||
            await SendConfirmAsync(GetText(strs.prot_enable("Anti-Spam")),
 | 
			
		||||
                $"{ctx.User.Mention} {GetAntiSpamString(stats)}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task AntispamIgnore()
 | 
			
		||||
        {
 | 
			
		||||
            var added = await _service.AntiSpamIgnoreAsync(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
 | 
			
		||||
            if (added is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.protection_not_running("Anti-Spam"));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (added.Value)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.spam_ignore("Anti-Spam"));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.spam_not_ignore("Anti-Spam"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task AntiList()
 | 
			
		||||
        {
 | 
			
		||||
            var (spam, raid, alt) = _service.GetAntiStats(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            if (spam is null && raid is null && alt is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.prot_none);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.prot_active));
 | 
			
		||||
 | 
			
		||||
            if (spam is not null)
 | 
			
		||||
                embed.AddField("Anti-Spam", GetAntiSpamString(spam).TrimTo(1024), true);
 | 
			
		||||
 | 
			
		||||
            if (raid is not null)
 | 
			
		||||
                embed.AddField("Anti-Raid", GetAntiRaidString(raid).TrimTo(1024), true);
 | 
			
		||||
 | 
			
		||||
            if (alt is not null)
 | 
			
		||||
                embed.AddField("Anti-Alt", GetAntiAltString(alt), true);
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetAntiAltString(AntiAltStats alt)
 | 
			
		||||
            => GetText(strs.anti_alt_status(Format.Bold(alt.MinAge.ToString(@"dd\d\ hh\h\ mm\m\ ")),
 | 
			
		||||
                Format.Bold(alt.Action.ToString()),
 | 
			
		||||
                Format.Bold(alt.Counter.ToString())));
 | 
			
		||||
 | 
			
		||||
        private string GetAntiSpamString(AntiSpamStats stats)
 | 
			
		||||
        {
 | 
			
		||||
            var settings = stats.AntiSpamSettings;
 | 
			
		||||
            var ignoredString = string.Join(", ", settings.IgnoredChannels.Select(c => $"<#{c.ChannelId}>"));
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(ignoredString))
 | 
			
		||||
                ignoredString = "none";
 | 
			
		||||
 | 
			
		||||
            var add = string.Empty;
 | 
			
		||||
            if (settings.MuteTime > 0)
 | 
			
		||||
                add = $" ({TimeSpan.FromMinutes(settings.MuteTime):hh\\hmm\\m})";
 | 
			
		||||
 | 
			
		||||
            return GetText(strs.spam_stats(Format.Bold(settings.MessageThreshold.ToString()),
 | 
			
		||||
                Format.Bold(settings.Action + add),
 | 
			
		||||
                ignoredString));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetAntiRaidString(AntiRaidStats stats)
 | 
			
		||||
        {
 | 
			
		||||
            var actionString = Format.Bold(stats.AntiRaidSettings.Action.ToString());
 | 
			
		||||
 | 
			
		||||
            if (stats.AntiRaidSettings.PunishDuration > 0)
 | 
			
		||||
                actionString += $" **({TimeSpan.FromMinutes(stats.AntiRaidSettings.PunishDuration):hh\\hmm\\m})**";
 | 
			
		||||
 | 
			
		||||
            return GetText(strs.raid_stats(Format.Bold(stats.AntiRaidSettings.UserThreshold.ToString()),
 | 
			
		||||
                Format.Bold(stats.AntiRaidSettings.Seconds.ToString()),
 | 
			
		||||
                actionString));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,499 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Threading.Channels;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class ProtectionService : INService
 | 
			
		||||
{
 | 
			
		||||
    public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = delegate
 | 
			
		||||
    {
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, AntiRaidStats> _antiRaidGuilds = new();
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, AntiSpamStats> _antiSpamGuilds = new();
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, AntiAltStats> _antiAltGuilds = new();
 | 
			
		||||
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly MuteService _mute;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly UserPunishService _punishService;
 | 
			
		||||
 | 
			
		||||
    private readonly Channel<PunishQueueItem> _punishUserQueue =
 | 
			
		||||
        Channel.CreateUnbounded<PunishQueueItem>(new()
 | 
			
		||||
        {
 | 
			
		||||
            SingleReader = true,
 | 
			
		||||
            SingleWriter = false
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    public ProtectionService(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        Bot bot,
 | 
			
		||||
        MuteService mute,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        UserPunishService punishService)
 | 
			
		||||
    {
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _mute = mute;
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _punishService = punishService;
 | 
			
		||||
 | 
			
		||||
        var ids = client.GetGuildIds();
 | 
			
		||||
        using (var uow = db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var configs = uow.Set<GuildConfig>()
 | 
			
		||||
                             .AsQueryable()
 | 
			
		||||
                             .Include(x => x.AntiRaidSetting)
 | 
			
		||||
                             .Include(x => x.AntiSpamSetting)
 | 
			
		||||
                             .ThenInclude(x => x.IgnoredChannels)
 | 
			
		||||
                             .Include(x => x.AntiAltSetting)
 | 
			
		||||
                             .Where(x => ids.Contains(x.GuildId))
 | 
			
		||||
                             .ToList();
 | 
			
		||||
 | 
			
		||||
            foreach (var gc in configs)
 | 
			
		||||
                Initialize(gc);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _client.MessageReceived += HandleAntiSpam;
 | 
			
		||||
        _client.UserJoined += HandleUserJoined;
 | 
			
		||||
 | 
			
		||||
        bot.JoinedGuild += _bot_JoinedGuild;
 | 
			
		||||
        _client.LeftGuild += _client_LeftGuild;
 | 
			
		||||
 | 
			
		||||
        _ = Task.Run(RunQueue);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task RunQueue()
 | 
			
		||||
    {
 | 
			
		||||
        while (true)
 | 
			
		||||
        {
 | 
			
		||||
            var item = await _punishUserQueue.Reader.ReadAsync();
 | 
			
		||||
 | 
			
		||||
            var muteTime = item.MuteTime;
 | 
			
		||||
            var gu = item.User;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _punishService.ApplyPunishment(gu.Guild,
 | 
			
		||||
                    gu,
 | 
			
		||||
                    _client.CurrentUser,
 | 
			
		||||
                    item.Action,
 | 
			
		||||
                    muteTime,
 | 
			
		||||
                    item.RoleId,
 | 
			
		||||
                    $"{item.Type} Protection");
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error in punish queue: {Message}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(1000);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task _client_LeftGuild(SocketGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            TryStopAntiRaid(guild.Id);
 | 
			
		||||
            TryStopAntiSpam(guild.Id);
 | 
			
		||||
            await TryStopAntiAlt(guild.Id);
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task _bot_JoinedGuild(GuildConfig gc)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var gcWithData = uow.GuildConfigsForId(gc.GuildId,
 | 
			
		||||
            set => set.Include(x => x.AntiRaidSetting)
 | 
			
		||||
                      .Include(x => x.AntiAltSetting)
 | 
			
		||||
                      .Include(x => x.AntiSpamSetting)
 | 
			
		||||
                      .ThenInclude(x => x.IgnoredChannels));
 | 
			
		||||
 | 
			
		||||
        Initialize(gcWithData);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void Initialize(GuildConfig gc)
 | 
			
		||||
    {
 | 
			
		||||
        var raid = gc.AntiRaidSetting;
 | 
			
		||||
        var spam = gc.AntiSpamSetting;
 | 
			
		||||
 | 
			
		||||
        if (raid is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var raidStats = new AntiRaidStats
 | 
			
		||||
            {
 | 
			
		||||
                AntiRaidSettings = raid
 | 
			
		||||
            };
 | 
			
		||||
            _antiRaidGuilds[gc.GuildId] = raidStats;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (spam is not null)
 | 
			
		||||
        {
 | 
			
		||||
            _antiSpamGuilds[gc.GuildId] = new()
 | 
			
		||||
            {
 | 
			
		||||
                AntiSpamSettings = spam
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var alt = gc.AntiAltSetting;
 | 
			
		||||
        if (alt is not null)
 | 
			
		||||
            _antiAltGuilds[gc.GuildId] = new(alt);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task HandleUserJoined(SocketGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        if (user.IsBot)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        _antiRaidGuilds.TryGetValue(user.Guild.Id, out var maybeStats);
 | 
			
		||||
        _antiAltGuilds.TryGetValue(user.Guild.Id, out var maybeAlts);
 | 
			
		||||
 | 
			
		||||
        if (maybeStats is null && maybeAlts is null)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            if (maybeAlts is { } alts)
 | 
			
		||||
            {
 | 
			
		||||
                if (user.CreatedAt != default)
 | 
			
		||||
                {
 | 
			
		||||
                    var diff = DateTime.UtcNow - user.CreatedAt.UtcDateTime;
 | 
			
		||||
                    if (diff < alts.MinAge)
 | 
			
		||||
                    {
 | 
			
		||||
                        alts.Increment();
 | 
			
		||||
 | 
			
		||||
                        await PunishUsers(alts.Action,
 | 
			
		||||
                            ProtectionType.Alting,
 | 
			
		||||
                            alts.ActionDurationMinutes,
 | 
			
		||||
                            alts.RoleId,
 | 
			
		||||
                            user);
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (maybeStats is not { } stats || !stats.RaidUsers.Add(user))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                ++stats.UsersCount;
 | 
			
		||||
 | 
			
		||||
                if (stats.UsersCount >= stats.AntiRaidSettings.UserThreshold)
 | 
			
		||||
                {
 | 
			
		||||
                    var users = stats.RaidUsers.ToArray();
 | 
			
		||||
                    stats.RaidUsers.Clear();
 | 
			
		||||
                    var settings = stats.AntiRaidSettings;
 | 
			
		||||
 | 
			
		||||
                    await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await Task.Delay(1000 * stats.AntiRaidSettings.Seconds);
 | 
			
		||||
 | 
			
		||||
                stats.RaidUsers.TryRemove(user);
 | 
			
		||||
                --stats.UsersCount;
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task HandleAntiSpam(SocketMessage arg)
 | 
			
		||||
    {
 | 
			
		||||
        if (arg is not SocketUserMessage msg || msg.Author.IsBot)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        if (msg.Channel is not ITextChannel channel)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings)
 | 
			
		||||
                    || spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new()
 | 
			
		||||
                    {
 | 
			
		||||
                        ChannelId = channel.Id
 | 
			
		||||
                    }))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id,
 | 
			
		||||
                    _ => new(msg),
 | 
			
		||||
                    (_, old) =>
 | 
			
		||||
                    {
 | 
			
		||||
                        old.ApplyNextMessage(msg);
 | 
			
		||||
                        return old;
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold)
 | 
			
		||||
                {
 | 
			
		||||
                    if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats))
 | 
			
		||||
                    {
 | 
			
		||||
                        var settings = spamSettings.AntiSpamSettings;
 | 
			
		||||
                        await PunishUsers(settings.Action,
 | 
			
		||||
                            ProtectionType.Spamming,
 | 
			
		||||
                            settings.MuteTime,
 | 
			
		||||
                            settings.RoleId,
 | 
			
		||||
                            (IGuildUser)msg.Author);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task PunishUsers(
 | 
			
		||||
        PunishmentAction action,
 | 
			
		||||
        ProtectionType pt,
 | 
			
		||||
        int muteTime,
 | 
			
		||||
        ulong? roleId,
 | 
			
		||||
        params IGuildUser[] gus)
 | 
			
		||||
    {
 | 
			
		||||
        Log.Information("[{PunishType}] - Punishing [{Count}] users with [{PunishAction}] in {GuildName} guild",
 | 
			
		||||
            pt,
 | 
			
		||||
            gus.Length,
 | 
			
		||||
            action,
 | 
			
		||||
            gus[0].Guild.Name);
 | 
			
		||||
 | 
			
		||||
        foreach (var gu in gus)
 | 
			
		||||
        {
 | 
			
		||||
            await _punishUserQueue.Writer.WriteAsync(new()
 | 
			
		||||
            {
 | 
			
		||||
                Action = action,
 | 
			
		||||
                Type = pt,
 | 
			
		||||
                User = gu,
 | 
			
		||||
                MuteTime = muteTime,
 | 
			
		||||
                RoleId = roleId
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ = OnAntiProtectionTriggered(action, pt, gus);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<AntiRaidStats> StartAntiRaidAsync(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        int userThreshold,
 | 
			
		||||
        int seconds,
 | 
			
		||||
        PunishmentAction action,
 | 
			
		||||
        int minutesDuration)
 | 
			
		||||
    {
 | 
			
		||||
        var g = _client.GetGuild(guildId);
 | 
			
		||||
        await _mute.GetMuteRole(g);
 | 
			
		||||
 | 
			
		||||
        if (action == PunishmentAction.AddRole)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        if (!IsDurationAllowed(action))
 | 
			
		||||
            minutesDuration = 0;
 | 
			
		||||
 | 
			
		||||
        var stats = new AntiRaidStats
 | 
			
		||||
        {
 | 
			
		||||
            AntiRaidSettings = new()
 | 
			
		||||
            {
 | 
			
		||||
                Action = action,
 | 
			
		||||
                Seconds = seconds,
 | 
			
		||||
                UserThreshold = userThreshold,
 | 
			
		||||
                PunishDuration = minutesDuration
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        _antiRaidGuilds.AddOrUpdate(guildId, stats, (_, _) => stats);
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiRaidSetting));
 | 
			
		||||
 | 
			
		||||
        gc.AntiRaidSetting = stats.AntiRaidSettings;
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        return stats;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryStopAntiRaid(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_antiRaidGuilds.TryRemove(guildId, out _))
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiRaidSetting));
 | 
			
		||||
 | 
			
		||||
            gc.AntiRaidSetting = null;
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryStopAntiSpam(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_antiSpamGuilds.TryRemove(guildId, out _))
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var gc = uow.GuildConfigsForId(guildId,
 | 
			
		||||
                set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels));
 | 
			
		||||
 | 
			
		||||
            gc.AntiSpamSetting = null;
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<AntiSpamStats> StartAntiSpamAsync(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        int messageCount,
 | 
			
		||||
        PunishmentAction action,
 | 
			
		||||
        int punishDurationMinutes,
 | 
			
		||||
        ulong? roleId)
 | 
			
		||||
    {
 | 
			
		||||
        var g = _client.GetGuild(guildId);
 | 
			
		||||
        await _mute.GetMuteRole(g);
 | 
			
		||||
 | 
			
		||||
        if (!IsDurationAllowed(action))
 | 
			
		||||
            punishDurationMinutes = 0;
 | 
			
		||||
 | 
			
		||||
        var stats = new AntiSpamStats
 | 
			
		||||
        {
 | 
			
		||||
            AntiSpamSettings = new()
 | 
			
		||||
            {
 | 
			
		||||
                Action = action,
 | 
			
		||||
                MessageThreshold = messageCount,
 | 
			
		||||
                MuteTime = punishDurationMinutes,
 | 
			
		||||
                RoleId = roleId
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        stats = _antiSpamGuilds.AddOrUpdate(guildId,
 | 
			
		||||
            stats,
 | 
			
		||||
            (_, old) =>
 | 
			
		||||
            {
 | 
			
		||||
                stats.AntiSpamSettings.IgnoredChannels = old.AntiSpamSettings.IgnoredChannels;
 | 
			
		||||
                return stats;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiSpamSetting));
 | 
			
		||||
 | 
			
		||||
        if (gc.AntiSpamSetting is not null)
 | 
			
		||||
        {
 | 
			
		||||
            gc.AntiSpamSetting.Action = stats.AntiSpamSettings.Action;
 | 
			
		||||
            gc.AntiSpamSetting.MessageThreshold = stats.AntiSpamSettings.MessageThreshold;
 | 
			
		||||
            gc.AntiSpamSetting.MuteTime = stats.AntiSpamSettings.MuteTime;
 | 
			
		||||
            gc.AntiSpamSetting.RoleId = stats.AntiSpamSettings.RoleId;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
            gc.AntiSpamSetting = stats.AntiSpamSettings;
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return stats;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool?> AntiSpamIgnoreAsync(ulong guildId, ulong channelId)
 | 
			
		||||
    {
 | 
			
		||||
        var obj = new AntiSpamIgnore
 | 
			
		||||
        {
 | 
			
		||||
            ChannelId = channelId
 | 
			
		||||
        };
 | 
			
		||||
        bool added;
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId,
 | 
			
		||||
            set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels));
 | 
			
		||||
        var spam = gc.AntiSpamSetting;
 | 
			
		||||
        if (spam is null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        if (spam.IgnoredChannels.Add(obj)) // if adding to db is successful
 | 
			
		||||
        {
 | 
			
		||||
            if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
 | 
			
		||||
                temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache
 | 
			
		||||
 | 
			
		||||
            added = true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var toRemove = spam.IgnoredChannels.First(x => x.ChannelId == channelId);
 | 
			
		||||
            uow.Set<AntiSpamIgnore>().Remove(toRemove); // remove from db
 | 
			
		||||
            if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
 | 
			
		||||
                temp.AntiSpamSettings.IgnoredChannels.Remove(toRemove); // remove from local cache
 | 
			
		||||
 | 
			
		||||
            added = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return added;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public (AntiSpamStats, AntiRaidStats, AntiAltStats) GetAntiStats(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        _antiRaidGuilds.TryGetValue(guildId, out var antiRaidStats);
 | 
			
		||||
        _antiSpamGuilds.TryGetValue(guildId, out var antiSpamStats);
 | 
			
		||||
        _antiAltGuilds.TryGetValue(guildId, out var antiAltStats);
 | 
			
		||||
 | 
			
		||||
        return (antiSpamStats, antiRaidStats, antiAltStats);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool IsDurationAllowed(PunishmentAction action)
 | 
			
		||||
    {
 | 
			
		||||
        switch (action)
 | 
			
		||||
        {
 | 
			
		||||
            case PunishmentAction.Ban:
 | 
			
		||||
            case PunishmentAction.Mute:
 | 
			
		||||
            case PunishmentAction.ChatMute:
 | 
			
		||||
            case PunishmentAction.VoiceMute:
 | 
			
		||||
            case PunishmentAction.AddRole:
 | 
			
		||||
            case PunishmentAction.TimeOut:
 | 
			
		||||
                return true;
 | 
			
		||||
            default:
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task StartAntiAltAsync(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        int minAgeMinutes,
 | 
			
		||||
        PunishmentAction action,
 | 
			
		||||
        int actionDurationMinutes = 0,
 | 
			
		||||
        ulong? roleId = null)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting));
 | 
			
		||||
        gc.AntiAltSetting = new()
 | 
			
		||||
        {
 | 
			
		||||
            Action = action,
 | 
			
		||||
            ActionDurationMinutes = actionDurationMinutes,
 | 
			
		||||
            MinAge = TimeSpan.FromMinutes(minAgeMinutes),
 | 
			
		||||
            RoleId = roleId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        _antiAltGuilds[guildId] = new(gc.AntiAltSetting);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> TryStopAntiAlt(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_antiAltGuilds.TryRemove(guildId, out _))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting));
 | 
			
		||||
        gc.AntiAltSetting = null;
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public enum ProtectionType
 | 
			
		||||
{
 | 
			
		||||
    Raiding,
 | 
			
		||||
    Spamming,
 | 
			
		||||
    Alting
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class AntiRaidStats
 | 
			
		||||
{
 | 
			
		||||
    public AntiRaidSetting AntiRaidSettings { get; set; }
 | 
			
		||||
    public int UsersCount { get; set; }
 | 
			
		||||
    public ConcurrentHashSet<IGuildUser> RaidUsers { get; set; } = new();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class AntiSpamStats
 | 
			
		||||
{
 | 
			
		||||
    public AntiSpamSetting AntiSpamSettings { get; set; }
 | 
			
		||||
    public ConcurrentDictionary<ulong, UserSpamStats> UserStats { get; set; } = new();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class AntiAltStats
 | 
			
		||||
{
 | 
			
		||||
    public PunishmentAction Action
 | 
			
		||||
        => _setting.Action;
 | 
			
		||||
 | 
			
		||||
    public int ActionDurationMinutes
 | 
			
		||||
        => _setting.ActionDurationMinutes;
 | 
			
		||||
 | 
			
		||||
    public ulong? RoleId
 | 
			
		||||
        => _setting.RoleId;
 | 
			
		||||
 | 
			
		||||
    public TimeSpan MinAge
 | 
			
		||||
        => _setting.MinAge;
 | 
			
		||||
 | 
			
		||||
    public int Counter
 | 
			
		||||
        => counter;
 | 
			
		||||
 | 
			
		||||
    private readonly AntiAltSetting _setting;
 | 
			
		||||
 | 
			
		||||
    private int counter;
 | 
			
		||||
 | 
			
		||||
    public AntiAltStats(AntiAltSetting setting)
 | 
			
		||||
        => _setting = setting;
 | 
			
		||||
 | 
			
		||||
    public void Increment()
 | 
			
		||||
        => Interlocked.Increment(ref counter);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public class PunishQueueItem
 | 
			
		||||
{
 | 
			
		||||
    public PunishmentAction Action { get; set; }
 | 
			
		||||
    public ProtectionType Type { get; set; }
 | 
			
		||||
    public int MuteTime { get; set; }
 | 
			
		||||
    public ulong? RoleId { get; set; }
 | 
			
		||||
    public IGuildUser User { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,64 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public sealed class UserSpamStats
 | 
			
		||||
{
 | 
			
		||||
    public int Count
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            lock (_applyLock)
 | 
			
		||||
            {
 | 
			
		||||
                Cleanup();
 | 
			
		||||
                return _messageTracker.Count;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string lastMessage;
 | 
			
		||||
 | 
			
		||||
    private readonly Queue<DateTime> _messageTracker;
 | 
			
		||||
 | 
			
		||||
    private readonly object _applyLock = new();
 | 
			
		||||
 | 
			
		||||
    private readonly TimeSpan _maxTime = TimeSpan.FromMinutes(30);
 | 
			
		||||
 | 
			
		||||
    public UserSpamStats(IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        lastMessage = msg.Content.ToUpperInvariant();
 | 
			
		||||
        _messageTracker = new();
 | 
			
		||||
 | 
			
		||||
        ApplyNextMessage(msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ApplyNextMessage(IUserMessage message)
 | 
			
		||||
    {
 | 
			
		||||
        var upperMsg = message.Content.ToUpperInvariant();
 | 
			
		||||
 | 
			
		||||
        lock (_applyLock)
 | 
			
		||||
        {
 | 
			
		||||
            if (upperMsg != lastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any()))
 | 
			
		||||
            {
 | 
			
		||||
                // if it's a new message, reset spam counter
 | 
			
		||||
                lastMessage = upperMsg;
 | 
			
		||||
                _messageTracker.Clear();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _messageTracker.Enqueue(DateTime.UtcNow);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void Cleanup()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_applyLock)
 | 
			
		||||
        {
 | 
			
		||||
            while (_messageTracker.TryPeek(out var dateTime))
 | 
			
		||||
            {
 | 
			
		||||
                if (DateTime.UtcNow - dateTime < _maxTime)
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                _messageTracker.Dequeue();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,114 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class PruneCommands : NadekoModule<PruneService>
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
 | 
			
		||||
 | 
			
		||||
        public sealed class PruneOptions : INadekoCommandOptions
 | 
			
		||||
        {
 | 
			
		||||
            [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)]
 | 
			
		||||
            public ulong? After { get; set; }
 | 
			
		||||
 | 
			
		||||
            public void NormalizeOptions()
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        //deletes her own messages, no perm required
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [NadekoOptions<PruneOptions>]
 | 
			
		||||
        public async Task Prune(params string[] args)
 | 
			
		||||
        {
 | 
			
		||||
            var (opts, _) = OptionsParser.ParseFrom(new PruneOptions(), args);
 | 
			
		||||
            
 | 
			
		||||
            var user = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
 | 
			
		||||
            if (opts.Safe)
 | 
			
		||||
                await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id && !x.IsPinned, opts.After);
 | 
			
		||||
            else
 | 
			
		||||
                await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id, opts.After);
 | 
			
		||||
            
 | 
			
		||||
            ctx.Message.DeleteAfter(3);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // prune x
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
        [BotPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
        [NadekoOptions<PruneOptions>]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task Prune(int count, params string[] args)
 | 
			
		||||
        {
 | 
			
		||||
            count++;
 | 
			
		||||
            if (count < 1)
 | 
			
		||||
                return;
 | 
			
		||||
            if (count > 1000)
 | 
			
		||||
                count = 1000;
 | 
			
		||||
            
 | 
			
		||||
            var (opts, _) = OptionsParser.ParseFrom<PruneOptions>(new PruneOptions(), args);
 | 
			
		||||
 | 
			
		||||
            if (opts.Safe)
 | 
			
		||||
                await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => !x.IsPinned, opts.After);
 | 
			
		||||
            else
 | 
			
		||||
                await _service.PruneWhere((ITextChannel)ctx.Channel, count, _ => true, opts.After);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //prune @user [x]
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
        [BotPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
        [NadekoOptions<PruneOptions>]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task Prune(IGuildUser user, int count = 100, params string[] args)
 | 
			
		||||
            => Prune(user.Id, count, args);
 | 
			
		||||
 | 
			
		||||
        //prune userid [x]
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
        [BotPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
        [NadekoOptions<PruneOptions>]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Prune(ulong userId, int count = 100, params string[] args)
 | 
			
		||||
        {
 | 
			
		||||
            if (userId == ctx.User.Id)
 | 
			
		||||
                count++;
 | 
			
		||||
 | 
			
		||||
            if (count < 1)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (count > 1000)
 | 
			
		||||
                count = 1000;
 | 
			
		||||
 | 
			
		||||
            var (opts, _) = OptionsParser.ParseFrom<PruneOptions>(new PruneOptions(), args);
 | 
			
		||||
            
 | 
			
		||||
            if (opts.Safe)
 | 
			
		||||
            {
 | 
			
		||||
                await _service.PruneWhere((ITextChannel)ctx.Channel,
 | 
			
		||||
                    count,
 | 
			
		||||
                    m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
 | 
			
		||||
                    opts.After);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await _service.PruneWhere((ITextChannel)ctx.Channel,
 | 
			
		||||
                    count,
 | 
			
		||||
                    m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
 | 
			
		||||
                    opts.After);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,89 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class PruneService : INService
 | 
			
		||||
{
 | 
			
		||||
    //channelids where prunes are currently occuring
 | 
			
		||||
    private readonly ConcurrentHashSet<ulong> _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<IMessage, bool> predicate, ulong? after = null)
 | 
			
		||||
    {
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(channel, nameof(channel));
 | 
			
		||||
 | 
			
		||||
        if (amount <= 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(amount));
 | 
			
		||||
 | 
			
		||||
        if (!_pruningGuilds.Add(channel.GuildId))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            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())
 | 
			
		||||
            {
 | 
			
		||||
                lastMessage = msgs[^1];
 | 
			
		||||
 | 
			
		||||
                var bulkDeletable = new List<IMessage>();
 | 
			
		||||
                var singleDeletable = new List<IMessage>();
 | 
			
		||||
                foreach (var x in msgs)
 | 
			
		||||
                {
 | 
			
		||||
                    _logService.AddDeleteIgnore(x.Id);
 | 
			
		||||
 | 
			
		||||
                    if (now - x.CreatedAt < _twoWeeks)
 | 
			
		||||
                        bulkDeletable.Add(x);
 | 
			
		||||
                    else
 | 
			
		||||
                        singleDeletable.Add(x);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (bulkDeletable.Count > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await channel.DeleteMessagesAsync(bulkDeletable);
 | 
			
		||||
                    await Task.Delay(2000);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                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();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            //ignore
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _pruningGuilds.TryRemove(channel.GuildId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Utility.Patronage;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using OneOf;
 | 
			
		||||
using OneOf.Types;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public interface IReactionRoleService
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Adds a single reaction role
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guild">Guild where to add a reaction role</param>
 | 
			
		||||
    /// <param name="msg">Message to which to add a reaction role</param>
 | 
			
		||||
    /// <param name="emote"></param>
 | 
			
		||||
    /// <param name="role"></param>
 | 
			
		||||
    /// <param name="group"></param>
 | 
			
		||||
    /// <param name="levelReq"></param>
 | 
			
		||||
    /// <returns>The result of the operation</returns>
 | 
			
		||||
    Task<OneOf<Success, FeatureLimit>> AddReactionRole(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        IMessage msg,
 | 
			
		||||
        string emote,
 | 
			
		||||
        IRole role,
 | 
			
		||||
        int group = 0,
 | 
			
		||||
        int levelReq = 0);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Get all reaction roles on the specified server
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Remove reaction roles on the specified message
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId"></param>
 | 
			
		||||
    /// <param name="messageId"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Remove all reaction roles in the specified server
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    Task<int> RemoveAllReactionRoles(ulong guildId);
 | 
			
		||||
 | 
			
		||||
    Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,171 +0,0 @@
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class ReactionRoleCommands : NadekoModule
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IReactionRoleService _rero;
 | 
			
		||||
 | 
			
		||||
        public ReactionRoleCommands(IReactionRoleService rero)
 | 
			
		||||
        {
 | 
			
		||||
            _rero = rero;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task ReactionRoleAdd(
 | 
			
		||||
            ulong messageId,
 | 
			
		||||
            string emoteStr,
 | 
			
		||||
            IRole role,
 | 
			
		||||
            int group = 0,
 | 
			
		||||
            int levelReq = 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (group < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (levelReq < 0)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            var msg = await ctx.Channel.GetMessageAsync(messageId);
 | 
			
		||||
            if (msg is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (ctx.User.Id != ctx.Guild.OwnerId && ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position) <= role.Position)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.hierarchy);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var emote = emoteStr.ToIEmote();
 | 
			
		||||
            await msg.AddReactionAsync(emote);
 | 
			
		||||
            var res = await _rero.AddReactionRole(ctx.Guild,
 | 
			
		||||
                msg,
 | 
			
		||||
                emoteStr,
 | 
			
		||||
                role,
 | 
			
		||||
                group,
 | 
			
		||||
                levelReq);
 | 
			
		||||
 | 
			
		||||
            await res.Match(
 | 
			
		||||
                _ => ctx.OkAsync(),
 | 
			
		||||
                fl =>
 | 
			
		||||
                {
 | 
			
		||||
                    _ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
 | 
			
		||||
                    return !fl.IsPatronLimit
 | 
			
		||||
                        ? ReplyErrorLocalizedAsync(strs.limit_reached(fl.Quota))
 | 
			
		||||
                        : ReplyPendingLocalizedAsync(strs.feature_limit_reached_owner(fl.Quota, fl.Name));
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task ReactionRolesList(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            var reros = await _rero.GetReactionRolesAsync(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page, curPage =>
 | 
			
		||||
            {
 | 
			
		||||
                var embed = _eb.Create(ctx)
 | 
			
		||||
                               .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                var content = string.Empty;
 | 
			
		||||
                foreach (var g in reros.OrderBy(x => x.Group)
 | 
			
		||||
                                       .Skip(curPage * 10)
 | 
			
		||||
                                       .GroupBy(x => x.MessageId)
 | 
			
		||||
                                       .OrderBy(x => x.Key))
 | 
			
		||||
                {
 | 
			
		||||
                    var messageId = g.Key;
 | 
			
		||||
                    content +=
 | 
			
		||||
                        $"[{messageId}](https://discord.com/channels/{ctx.Guild.Id}/{g.First().ChannelId}/{g.Key})\n";
 | 
			
		||||
 | 
			
		||||
                    var groupGroups = g.GroupBy(x => x.Group);
 | 
			
		||||
 | 
			
		||||
                    foreach (var ggs in groupGroups)
 | 
			
		||||
                    {
 | 
			
		||||
                        content += $"`< {(g.Key == 0 ? ("Not Exclusive (Group 0)") : ($"Group {ggs.Key}"))} >`\n";
 | 
			
		||||
 | 
			
		||||
                        foreach (var rero in ggs)
 | 
			
		||||
                        {
 | 
			
		||||
                            content +=
 | 
			
		||||
                                $"\t{rero.Emote} -> {(ctx.Guild.GetRole(rero.RoleId)?.Mention ?? "<missing role>")}";
 | 
			
		||||
                            if (rero.LevelReq > 0)
 | 
			
		||||
                                content += $" (lvl {rero.LevelReq}+)";
 | 
			
		||||
                            content += '\n';
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                embed.WithDescription(string.IsNullOrWhiteSpace(content)
 | 
			
		||||
                    ? "There are no reaction roles on this server"
 | 
			
		||||
                    : content);
 | 
			
		||||
 | 
			
		||||
                return embed;
 | 
			
		||||
            }, reros.Count, 10);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task ReactionRolesRemove(ulong messageId)
 | 
			
		||||
        {
 | 
			
		||||
            var succ = await _rero.RemoveReactionRoles(ctx.Guild.Id, messageId);
 | 
			
		||||
            if (succ)
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            else
 | 
			
		||||
                await ctx.ErrorAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task ReactionRolesDeleteAll()
 | 
			
		||||
        {
 | 
			
		||||
            await _rero.RemoveAllReactionRoles(ctx.Guild.Id);
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [Ratelimit(60)]
 | 
			
		||||
        public async Task ReactionRolesTransfer(ulong fromMessageId, ulong toMessageId)
 | 
			
		||||
        {
 | 
			
		||||
            var msg = await ctx.Channel.GetMessageAsync(toMessageId);
 | 
			
		||||
 | 
			
		||||
            if (msg is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.ErrorAsync();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var reactions = await _rero.TransferReactionRolesAsync(ctx.Guild.Id, fromMessageId, toMessageId);
 | 
			
		||||
 | 
			
		||||
            if (reactions.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.ErrorAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var r in reactions)
 | 
			
		||||
                {
 | 
			
		||||
                    await msg.AddReactionAsync(r);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,396 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Modules.Utility.Patronage;
 | 
			
		||||
using NadekoBot.Modules.Xp.Extensions;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using OneOf.Types;
 | 
			
		||||
using OneOf;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionRoleService
 | 
			
		||||
{
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
 | 
			
		||||
    private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
 | 
			
		||||
    private readonly object _cacheLock = new();
 | 
			
		||||
    private readonly SemaphoreSlim _assignementLock = new(1, 1);
 | 
			
		||||
    private readonly IPatronageService _ps;
 | 
			
		||||
 | 
			
		||||
    private static readonly FeatureLimitKey _reroFLKey = new()
 | 
			
		||||
    {
 | 
			
		||||
        Key = "rero:max_count",
 | 
			
		||||
        PrettyName = "Reaction Role"
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public ReactionRolesService(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        IBotCredentials creds,
 | 
			
		||||
        IPatronageService ps)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _ps = ps;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
        _cache = new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var reros = await uow.GetTable<ReactionRoleV2>()
 | 
			
		||||
                             .Where(
 | 
			
		||||
                                 x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
 | 
			
		||||
                             .ToListAsyncLinqToDB();
 | 
			
		||||
 | 
			
		||||
        foreach (var group in reros.GroupBy(x => x.MessageId))
 | 
			
		||||
        {
 | 
			
		||||
            _cache[group.Key] = group.ToList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _client.ReactionAdded += ClientOnReactionAdded;
 | 
			
		||||
        _client.ReactionRemoved += ClientOnReactionRemoved;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<(IGuildUser, IRole)> GetUserAndRoleAsync(
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        ReactionRoleV2 rero)
 | 
			
		||||
    {
 | 
			
		||||
        var guild = _client.GetGuild(rero.GuildId);
 | 
			
		||||
        var role = guild?.GetRole(rero.RoleId);
 | 
			
		||||
 | 
			
		||||
        if (role is null)
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        var user = guild.GetUser(userId) as IGuildUser
 | 
			
		||||
                   ?? await _client.Rest.GetGuildUserAsync(guild.Id, userId);
 | 
			
		||||
 | 
			
		||||
        if (user is null)
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        return (user, role);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task ClientOnReactionRemoved(
 | 
			
		||||
        Cacheable<IUserMessage, ulong> cmsg,
 | 
			
		||||
        Cacheable<IMessageChannel, ulong> ch,
 | 
			
		||||
        SocketReaction r)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_cache.TryGetValue(cmsg.Id, out var reros))
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            var emote = await GetFixedEmoteAsync(cmsg, r.Emote);
 | 
			
		||||
            
 | 
			
		||||
            var rero = reros.FirstOrDefault(x => x.Emote == emote.Name 
 | 
			
		||||
                                                 || x.Emote == emote.ToString());
 | 
			
		||||
            if (rero is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var (user, role) = await GetUserAndRoleAsync(r.UserId, rero);
 | 
			
		||||
 | 
			
		||||
            if (user.IsBot)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await _assignementLock.WaitAsync();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (user.RoleIds.Contains(role.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    await user.RemoveRoleAsync(role.Id);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _assignementLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // had to add this because for some reason, reactionremoved event's reaction doesn't have IsAnimated set,
 | 
			
		||||
    // causing the .ToString() to be wrong on animated custom emotes
 | 
			
		||||
    private async Task<IEmote> GetFixedEmoteAsync(
 | 
			
		||||
        Cacheable<IUserMessage, ulong> cmsg,
 | 
			
		||||
        IEmote inputEmote)
 | 
			
		||||
    {
 | 
			
		||||
        // this should only run for emote
 | 
			
		||||
        if (inputEmote is not Emote e)
 | 
			
		||||
            return inputEmote;
 | 
			
		||||
 | 
			
		||||
        // try to get the message and pull
 | 
			
		||||
        var msg = await cmsg.GetOrDownloadAsync();
 | 
			
		||||
 | 
			
		||||
        var emote = msg.Reactions.Keys.FirstOrDefault(x => e.Equals(x));
 | 
			
		||||
        return emote ?? inputEmote;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task ClientOnReactionAdded(
 | 
			
		||||
        Cacheable<IUserMessage, ulong> msg,
 | 
			
		||||
        Cacheable<IMessageChannel, ulong> ch,
 | 
			
		||||
        SocketReaction r)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_cache.TryGetValue(msg.Id, out var reros))
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            var rero = reros.FirstOrDefault(x => x.Emote == r.Emote.Name || x.Emote == r.Emote.ToString());
 | 
			
		||||
            if (rero is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var (user, role) = await GetUserAndRoleAsync(r.UserId, rero);
 | 
			
		||||
 | 
			
		||||
            if (user.IsBot)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await _assignementLock.WaitAsync();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!user.RoleIds.Contains(role.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    // first check if there is a level requirement
 | 
			
		||||
                    // and if there is, make sure user satisfies it
 | 
			
		||||
                    if (rero.LevelReq > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        await using var ctx = _db.GetDbContext();
 | 
			
		||||
                        var levelData = await ctx.GetTable<UserXpStats>()
 | 
			
		||||
                                                 .GetLevelDataFor(user.GuildId, user.Id);
 | 
			
		||||
 | 
			
		||||
                        if (levelData.Level < rero.LevelReq)
 | 
			
		||||
                            return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // remove all other roles from the same group from the user
 | 
			
		||||
                    // execept in group 0, which is a special, non-exclusive group
 | 
			
		||||
                    if (rero.Group != 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        var exclusive = reros
 | 
			
		||||
                                        .Where(x => x.Group == rero.Group && x.RoleId != role.Id)
 | 
			
		||||
                                        .Select(x => x.RoleId)
 | 
			
		||||
                                        .Distinct();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                        try { await user.RemoveRolesAsync(exclusive); }
 | 
			
		||||
                        catch { }
 | 
			
		||||
 | 
			
		||||
                        // remove user's previous reaction
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            var m = await msg.GetOrDownloadAsync();
 | 
			
		||||
                            if (m is not null)
 | 
			
		||||
                            {
 | 
			
		||||
                                var reactToRemove = m.Reactions
 | 
			
		||||
                                                     .FirstOrDefault(x => x.Key.ToString() != r.Emote.ToString())
 | 
			
		||||
                                                     .Key;
 | 
			
		||||
 | 
			
		||||
                                if (reactToRemove is not null)
 | 
			
		||||
                                {
 | 
			
		||||
                                    await m.RemoveReactionAsync(reactToRemove, user);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        catch
 | 
			
		||||
                        {
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await user.AddRoleAsync(role.Id);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _assignementLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Adds a single reaction role
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guild">Guild where to add a reaction role</param>
 | 
			
		||||
    /// <param name="msg">Message to which to add a reaction role</param>
 | 
			
		||||
    /// <param name="emote"></param>
 | 
			
		||||
    /// <param name="role"></param>
 | 
			
		||||
    /// <param name="group"></param>
 | 
			
		||||
    /// <param name="levelReq"></param>
 | 
			
		||||
    /// <returns>The result of the operation</returns>
 | 
			
		||||
    public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        IMessage msg,
 | 
			
		||||
        string emote,
 | 
			
		||||
        IRole role,
 | 
			
		||||
        int group = 0,
 | 
			
		||||
        int levelReq = 0)
 | 
			
		||||
    {
 | 
			
		||||
        if (group < 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(group));
 | 
			
		||||
 | 
			
		||||
        if (levelReq < 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(group));
 | 
			
		||||
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        await using var tran = await ctx.Database.BeginTransactionAsync();
 | 
			
		||||
        var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
                                           .Where(x => x.GuildId == guild.Id)
 | 
			
		||||
                                           .CountAsync();
 | 
			
		||||
        
 | 
			
		||||
        var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
 | 
			
		||||
        if (result.Quota != -1 && activeReactionRoles >= result.Quota)
 | 
			
		||||
            return result;
 | 
			
		||||
 | 
			
		||||
        await ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
                 .InsertOrUpdateAsync(() => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         GuildId = guild.Id,
 | 
			
		||||
                         ChannelId = msg.Channel.Id,
 | 
			
		||||
 | 
			
		||||
                         MessageId = msg.Id,
 | 
			
		||||
                         Emote = emote,
 | 
			
		||||
 | 
			
		||||
                         RoleId = role.Id,
 | 
			
		||||
                         Group = group,
 | 
			
		||||
                         LevelReq = levelReq
 | 
			
		||||
                     },
 | 
			
		||||
                     (old) => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         RoleId = role.Id,
 | 
			
		||||
                         Group = group,
 | 
			
		||||
                         LevelReq = levelReq
 | 
			
		||||
                     },
 | 
			
		||||
                     () => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         MessageId = msg.Id,
 | 
			
		||||
                         Emote = emote,
 | 
			
		||||
                     });
 | 
			
		||||
 | 
			
		||||
        await tran.CommitAsync();
 | 
			
		||||
 | 
			
		||||
        var obj = new ReactionRoleV2()
 | 
			
		||||
        {
 | 
			
		||||
            GuildId = guild.Id,
 | 
			
		||||
            MessageId = msg.Id,
 | 
			
		||||
            Emote = emote,
 | 
			
		||||
            RoleId = role.Id,
 | 
			
		||||
            Group = group,
 | 
			
		||||
            LevelReq = levelReq
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        lock (_cacheLock)
 | 
			
		||||
        {
 | 
			
		||||
            _cache.AddOrUpdate(msg.Id,
 | 
			
		||||
                _ => new()
 | 
			
		||||
                {
 | 
			
		||||
                    obj
 | 
			
		||||
                },
 | 
			
		||||
                (_, list) =>
 | 
			
		||||
                {
 | 
			
		||||
                    list.RemoveAll(x => x.Emote == emote);
 | 
			
		||||
                    list.Add(obj);
 | 
			
		||||
                    return list;
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new Success();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Get all reaction roles on the specified server
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public async Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        return await ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
                        .Where(x => x.GuildId == guildId)
 | 
			
		||||
                        .ToListAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Remove reaction roles on the specified message
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId"></param>
 | 
			
		||||
    /// <param name="messageId"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public async Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId)
 | 
			
		||||
    {
 | 
			
		||||
        // guildid is used for quick index lookup
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        var changed = await ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
                               .Where(x => x.GuildId == guildId && x.MessageId == messageId)
 | 
			
		||||
                               .DeleteAsync();
 | 
			
		||||
 | 
			
		||||
        _cache.TryRemove(messageId, out _);
 | 
			
		||||
 | 
			
		||||
        if (changed == 0)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Remove all reaction roles in the specified server
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public async Task<int> RemoveAllReactionRoles(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        var output = await ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
                              .Where(x => x.GuildId == guildId)
 | 
			
		||||
                              .DeleteWithOutputAsync(x => x.MessageId);
 | 
			
		||||
 | 
			
		||||
        lock (_cacheLock)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var o in output)
 | 
			
		||||
            {
 | 
			
		||||
                _cache.TryRemove(o, out _);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return output.Length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        ulong fromMessageId,
 | 
			
		||||
        ulong toMessageId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        var updated = ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
                         .Where(x => x.GuildId == guildId && x.MessageId == fromMessageId)
 | 
			
		||||
                         .UpdateWithOutput(old => new()
 | 
			
		||||
                             {
 | 
			
		||||
                                 MessageId = toMessageId
 | 
			
		||||
                             },
 | 
			
		||||
                             (old, neu) => neu);
 | 
			
		||||
        lock (_cacheLock)
 | 
			
		||||
        {
 | 
			
		||||
            if (_cache.TryRemove(fromMessageId, out var data))
 | 
			
		||||
            {
 | 
			
		||||
                if (_cache.TryGetValue(toMessageId, out var newData))
 | 
			
		||||
                {
 | 
			
		||||
                    newData.AddRange(data);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    _cache[toMessageId] = data;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return updated.Select(x => x.Emote.ToIEmote()).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,187 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using Color = SixLabors.ImageSharp.Color;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class RoleCommands : NadekoModule
 | 
			
		||||
    {
 | 
			
		||||
        public enum Exclude { Excl }
 | 
			
		||||
 | 
			
		||||
        private readonly IServiceProvider _services;
 | 
			
		||||
 | 
			
		||||
        public RoleCommands(IServiceProvider services)
 | 
			
		||||
        {
 | 
			
		||||
            _services = services;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task SetRole(IGuildUser targetUser, [Leftover] IRole roleToAdd)
 | 
			
		||||
        {
 | 
			
		||||
            var runnerUser = (IGuildUser)ctx.User;
 | 
			
		||||
            var runnerMaxRolePosition = runnerUser.GetRoles().Max(x => x.Position);
 | 
			
		||||
            if (ctx.User.Id != ctx.Guild.OwnerId && runnerMaxRolePosition <= roleToAdd.Position)
 | 
			
		||||
                return;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await targetUser.AddRoleAsync(roleToAdd);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.setrole(Format.Bold(roleToAdd.Name),
 | 
			
		||||
                    Format.Bold(targetUser.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error in setrole command");
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.setrole_err);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task RemoveRole(IGuildUser targetUser, [Leftover] IRole roleToRemove)
 | 
			
		||||
        {
 | 
			
		||||
            var runnerUser = (IGuildUser)ctx.User;
 | 
			
		||||
            if (ctx.User.Id != runnerUser.Guild.OwnerId
 | 
			
		||||
                && runnerUser.GetRoles().Max(x => x.Position) <= roleToRemove.Position)
 | 
			
		||||
                return;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await targetUser.RemoveRoleAsync(roleToRemove);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.remrole(Format.Bold(roleToRemove.Name),
 | 
			
		||||
                    Format.Bold(targetUser.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.remrole_err);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task RenameRole(IRole roleToEdit, [Leftover] string newname)
 | 
			
		||||
        {
 | 
			
		||||
            var guser = (IGuildUser)ctx.User;
 | 
			
		||||
            if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= roleToEdit.Position)
 | 
			
		||||
                return;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (roleToEdit.Position > (await ctx.Guild.GetCurrentUserAsync()).GetRoles().Max(r => r.Position))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.renrole_perms);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await roleToEdit.ModifyAsync(g => g.Name = newname);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.renrole);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.renrole_err);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task RemoveAllRoles([Leftover] IGuildUser user)
 | 
			
		||||
        {
 | 
			
		||||
            var guser = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
            var userRoles = user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole).ToList();
 | 
			
		||||
 | 
			
		||||
            if (user.Id == ctx.Guild.OwnerId
 | 
			
		||||
                || (ctx.User.Id != ctx.Guild.OwnerId
 | 
			
		||||
                    && guser.GetRoles().Max(x => x.Position) <= userRoles.Max(x => x.Position)))
 | 
			
		||||
                return;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await user.RemoveRolesAsync(userRoles);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.rar(Format.Bold(user.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.rar_err);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task CreateRole([Leftover] string roleName = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(roleName))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var r = await ctx.Guild.CreateRoleAsync(roleName, isMentionable: false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.cr(Format.Bold(r.Name)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task DeleteRole([Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var guser = (IGuildUser)ctx.User;
 | 
			
		||||
            if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await role.DeleteAsync();
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.dr(Format.Bold(role.Name)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task RoleHoist([Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var newHoisted = !role.IsHoisted;
 | 
			
		||||
            await role.ModifyAsync(r => r.Hoist = newHoisted);
 | 
			
		||||
            if (newHoisted)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.rolehoist_enabled(Format.Bold(role.Name)));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.rolehoist_disabled(Format.Bold(role.Name)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task RoleColor([Leftover] IRole role)
 | 
			
		||||
            => await SendConfirmAsync("Role Color", role.Color.RawValue.ToString("x6"));
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task RoleColor(Color color, [Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var rgba32 = color.ToPixel<Rgba32>();
 | 
			
		||||
                await role.ModifyAsync(r => r.Color = new Discord.Color(rgba32.R, rgba32.G, rgba32.B));
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.rc(Format.Bold(role.Name)));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.rc_perms);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,153 +0,0 @@
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Self;
 | 
			
		||||
 | 
			
		||||
public sealed class CheckForUpdatesService : INService, IReadyExecutor
 | 
			
		||||
{
 | 
			
		||||
    private readonly BotConfigService _bcs;
 | 
			
		||||
    private readonly IBotCredsProvider _bcp;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly IEmbedBuilderService _ebs;
 | 
			
		||||
 | 
			
		||||
    public CheckForUpdatesService(BotConfigService bcs, IBotCredsProvider bcp, IHttpClientFactory httpFactory,
 | 
			
		||||
        DiscordSocketClient client, IEmbedBuilderService ebs)
 | 
			
		||||
    {
 | 
			
		||||
        _bcs = bcs;
 | 
			
		||||
        _bcp = bcp;
 | 
			
		||||
        _httpFactory = httpFactory;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _ebs = ebs;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (_client.ShardId != 0)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
 | 
			
		||||
        while (await timer.WaitForNextTickAsync())
 | 
			
		||||
        {
 | 
			
		||||
            var conf = _bcs.Data;
 | 
			
		||||
 | 
			
		||||
            if (!conf.CheckForUpdates)
 | 
			
		||||
                continue;
 | 
			
		||||
            
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                const string URL = "https://cdn.nadeko.bot/cmds/versions.json";
 | 
			
		||||
                using var http = _httpFactory.CreateClient();
 | 
			
		||||
                var versions = await http.GetFromJsonAsync<string[]>(URL);
 | 
			
		||||
 | 
			
		||||
                if (versions is null)
 | 
			
		||||
                    continue;
 | 
			
		||||
                
 | 
			
		||||
                var latest = versions[0];
 | 
			
		||||
                var latestVersion = Version.Parse(latest);
 | 
			
		||||
                var lastKnownVersion = GetLastKnownVersion();
 | 
			
		||||
 | 
			
		||||
                if (lastKnownVersion is null)
 | 
			
		||||
                {
 | 
			
		||||
                    UpdateLastKnownVersion(latestVersion);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if (latestVersion > lastKnownVersion)
 | 
			
		||||
                {
 | 
			
		||||
                    UpdateLastKnownVersion(latestVersion);
 | 
			
		||||
                    
 | 
			
		||||
                    // pull changelog
 | 
			
		||||
                    var changelog = await http.GetStringAsync("https://gitlab.com/Kwoth/nadekobot/-/raw/v4/CHANGELOG.md");
 | 
			
		||||
 | 
			
		||||
                    var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog);
 | 
			
		||||
 | 
			
		||||
                    if (string.IsNullOrWhiteSpace(thisVersionChangelog))
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning("New version {BotVersion} was found but changelog is unavailable",
 | 
			
		||||
                            thisVersionChangelog);
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var creds = _bcp.GetCreds();
 | 
			
		||||
                    await creds.OwnerIds
 | 
			
		||||
                        .Select(async x =>
 | 
			
		||||
                        {
 | 
			
		||||
                            var user = await _client.GetUserAsync(x);
 | 
			
		||||
                            if (user is null)
 | 
			
		||||
                                return;
 | 
			
		||||
 | 
			
		||||
                            var eb = _ebs.Create()
 | 
			
		||||
                                .WithOkColor()
 | 
			
		||||
                                .WithAuthor($"NadekoBot v{latestVersion} Released!")
 | 
			
		||||
                                .WithTitle("Changelog")
 | 
			
		||||
                                .WithUrl("https://gitlab.com/Kwoth/nadekobot/-/blob/v4/CHANGELOG.md")
 | 
			
		||||
                                .WithDescription(thisVersionChangelog.TrimTo(4096))
 | 
			
		||||
                                .WithFooter("You may disable these messages by typing '.conf bot checkforupdates false'");
 | 
			
		||||
 | 
			
		||||
                            await user.EmbedAsync(eb);
 | 
			
		||||
                        }).WhenAll();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Error while checking for new bot release: {ErrorMessage}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string? GetVersionChangelog(Version latestVersion, string changelog)
 | 
			
		||||
    {
 | 
			
		||||
        var clSpan = changelog.AsSpan();
 | 
			
		||||
 | 
			
		||||
        var sb = new StringBuilder();
 | 
			
		||||
        var started = false;
 | 
			
		||||
        foreach (var line in clSpan.EnumerateLines())
 | 
			
		||||
        {
 | 
			
		||||
            // if we're at the current version, keep reading lines and adding to the output
 | 
			
		||||
            if (started)
 | 
			
		||||
            {
 | 
			
		||||
                // if we got to previous version, end
 | 
			
		||||
                if (line.StartsWith("## ["))
 | 
			
		||||
                    break;
 | 
			
		||||
                
 | 
			
		||||
                // if we're reading a new segment, reformat it to print it better to discord
 | 
			
		||||
                if (line.StartsWith("### "))
 | 
			
		||||
                {
 | 
			
		||||
                    sb.AppendLine(Format.Bold(line.ToString()));
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    sb.AppendLine(line.ToString());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (line.StartsWith($"## [{latestVersion.ToString()}]"))
 | 
			
		||||
            {
 | 
			
		||||
                started = true;
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return sb.ToString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private const string LAST_KNOWN_VERSION_PATH = "data/last_known_version.txt";
 | 
			
		||||
    private Version? GetLastKnownVersion()
 | 
			
		||||
    {
 | 
			
		||||
        if (!File.Exists(LAST_KNOWN_VERSION_PATH))
 | 
			
		||||
            return null;
 | 
			
		||||
        
 | 
			
		||||
        return Version.TryParse(File.ReadAllText(LAST_KNOWN_VERSION_PATH), out var ver)
 | 
			
		||||
            ? ver
 | 
			
		||||
            : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void UpdateLastKnownVersion(Version version)
 | 
			
		||||
    {
 | 
			
		||||
        File.WriteAllText("data/last_known_version.txt", version.ToString());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,572 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Medusa;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class SelfCommands : NadekoModule<SelfService>
 | 
			
		||||
    {
 | 
			
		||||
        public enum SettableUserStatus
 | 
			
		||||
        {
 | 
			
		||||
            Online,
 | 
			
		||||
            Invisible,
 | 
			
		||||
            Idle,
 | 
			
		||||
            Dnd
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly IBotStrings _strings;
 | 
			
		||||
        private readonly IMedusaLoaderService _medusaLoader;
 | 
			
		||||
        private readonly ICoordinator _coord;
 | 
			
		||||
 | 
			
		||||
        public SelfCommands(
 | 
			
		||||
            DiscordSocketClient client,
 | 
			
		||||
            IBotStrings strings,
 | 
			
		||||
            ICoordinator coord,
 | 
			
		||||
            IMedusaLoaderService medusaLoader)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _strings = strings;
 | 
			
		||||
            _coord = coord;
 | 
			
		||||
            _medusaLoader = medusaLoader;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task DoAs(IUser user, [Leftover] string message)
 | 
			
		||||
        {
 | 
			
		||||
            if (ctx.User is not IGuildUser { GuildPermissions.Administrator: true })
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (ctx.Guild is SocketGuild sg && ctx.Channel is ISocketMessageChannel ch
 | 
			
		||||
                && ctx.Message is SocketUserMessage msg)
 | 
			
		||||
            {
 | 
			
		||||
                var fakeMessage = new DoAsUserMessage(msg, user, message);
 | 
			
		||||
                
 | 
			
		||||
                await _cmdHandler.TryRunCommand(sg, ch, fakeMessage);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.error_occured);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task StartupCommandAdd([Leftover] string cmdText)
 | 
			
		||||
        {
 | 
			
		||||
            if (cmdText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var guser = (IGuildUser)ctx.User;
 | 
			
		||||
            var cmd = new AutoCommand
 | 
			
		||||
            {
 | 
			
		||||
                CommandText = cmdText,
 | 
			
		||||
                ChannelId = ctx.Channel.Id,
 | 
			
		||||
                ChannelName = ctx.Channel.Name,
 | 
			
		||||
                GuildId = ctx.Guild?.Id,
 | 
			
		||||
                GuildName = ctx.Guild?.Name,
 | 
			
		||||
                VoiceChannelId = guser.VoiceChannel?.Id,
 | 
			
		||||
                VoiceChannelName = guser.VoiceChannel?.Name,
 | 
			
		||||
                Interval = 0
 | 
			
		||||
            };
 | 
			
		||||
            _service.AddNewAutoCommand(cmd);
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                            .WithOkColor()
 | 
			
		||||
                                            .WithTitle(GetText(strs.scadd))
 | 
			
		||||
                                            .AddField(GetText(strs.server),
 | 
			
		||||
                                                cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}",
 | 
			
		||||
                                                true)
 | 
			
		||||
                                            .AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true)
 | 
			
		||||
                                            .AddField(GetText(strs.command_text), cmdText));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task AutoCommandAdd(int interval, [Leftover] string cmdText)
 | 
			
		||||
        {
 | 
			
		||||
            if (cmdText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (interval < 5)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var guser = (IGuildUser)ctx.User;
 | 
			
		||||
            var cmd = new AutoCommand
 | 
			
		||||
            {
 | 
			
		||||
                CommandText = cmdText,
 | 
			
		||||
                ChannelId = ctx.Channel.Id,
 | 
			
		||||
                ChannelName = ctx.Channel.Name,
 | 
			
		||||
                GuildId = ctx.Guild?.Id,
 | 
			
		||||
                GuildName = ctx.Guild?.Name,
 | 
			
		||||
                VoiceChannelId = guser.VoiceChannel?.Id,
 | 
			
		||||
                VoiceChannelName = guser.VoiceChannel?.Name,
 | 
			
		||||
                Interval = interval
 | 
			
		||||
            };
 | 
			
		||||
            _service.AddNewAutoCommand(cmd);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.autocmd_add(Format.Code(Format.Sanitize(cmdText)), cmd.Interval));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task StartupCommandsList(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (page-- < 1)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var scmds = _service.GetStartupCommands().Skip(page * 5).Take(5).ToList();
 | 
			
		||||
 | 
			
		||||
            if (scmds.Count == 0)
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.startcmdlist_none);
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var i = 0;
 | 
			
		||||
                await SendConfirmAsync(text: string.Join("\n",
 | 
			
		||||
                        scmds.Select(x => $@"```css
 | 
			
		||||
#{++i + (page * 5)}
 | 
			
		||||
[{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")}
 | 
			
		||||
[{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId}
 | 
			
		||||
[{GetText(strs.command_text)}]: {x.CommandText}```")),
 | 
			
		||||
                    title: string.Empty,
 | 
			
		||||
                    footer: GetText(strs.page(page + 1)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task AutoCommandsList(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (page-- < 1)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var scmds = _service.GetAutoCommands().Skip(page * 5).Take(5).ToList();
 | 
			
		||||
            if (!scmds.Any())
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.autocmdlist_none);
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var i = 0;
 | 
			
		||||
                await SendConfirmAsync(text: string.Join("\n",
 | 
			
		||||
                        scmds.Select(x => $@"```css
 | 
			
		||||
#{++i + (page * 5)}
 | 
			
		||||
[{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")}
 | 
			
		||||
[{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId}
 | 
			
		||||
{GetIntervalText(x.Interval)}
 | 
			
		||||
[{GetText(strs.command_text)}]: {x.CommandText}```")),
 | 
			
		||||
                    title: string.Empty,
 | 
			
		||||
                    footer: GetText(strs.page(page + 1)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetIntervalText(int interval)
 | 
			
		||||
            => $"[{GetText(strs.interval)}]: {interval}";
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task Wait(int miliseconds)
 | 
			
		||||
        {
 | 
			
		||||
            if (miliseconds <= 0)
 | 
			
		||||
                return;
 | 
			
		||||
            ctx.Message.DeleteAfter(0);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var msg = await SendConfirmAsync($"⏲ {miliseconds}ms");
 | 
			
		||||
                msg.DeleteAfter(miliseconds / 1000);
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
 | 
			
		||||
            await Task.Delay(miliseconds);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task AutoCommandRemove([Leftover] int index)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.RemoveAutoCommand(--index, out _))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.acrm_fail);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task StartupCommandRemove([Leftover] int index)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.RemoveStartupCommand(--index, out _))
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.scrm_fail);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.scrm);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task StartupCommandsClear()
 | 
			
		||||
        {
 | 
			
		||||
            _service.ClearStartupCommands();
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.startcmds_cleared);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task ForwardMessages()
 | 
			
		||||
        {
 | 
			
		||||
            var enabled = _service.ForwardMessages();
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.fwdm_start);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.fwdm_stop);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task ForwardToAll()
 | 
			
		||||
        {
 | 
			
		||||
            var enabled = _service.ForwardToAll();
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.fwall_start);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.fwall_stop);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task ForwardToChannel()
 | 
			
		||||
        {
 | 
			
		||||
            var enabled = _service.ForwardToChannel(ctx.Channel.Id);
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.fwch_start);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.fwch_stop);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task ShardStats(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var statuses = _coord.GetAllShardStatuses();
 | 
			
		||||
 | 
			
		||||
            var status = string.Join(" : ",
 | 
			
		||||
                statuses.Select(x => (ConnectionStateToEmoji(x), x))
 | 
			
		||||
                        .GroupBy(x => x.Item1)
 | 
			
		||||
                        .Select(x => $"`{x.Count()} {x.Key}`")
 | 
			
		||||
                        .ToArray());
 | 
			
		||||
 | 
			
		||||
            var allShardStrings = statuses.Select(st =>
 | 
			
		||||
                                          {
 | 
			
		||||
                                              var timeDiff = DateTime.UtcNow - st.LastUpdate;
 | 
			
		||||
                                              var stateStr = ConnectionStateToEmoji(st);
 | 
			
		||||
                                              var maxGuildCountLength =
 | 
			
		||||
                                                  statuses.Max(x => x.GuildCount).ToString().Length;
 | 
			
		||||
                                              return $"`{stateStr} "
 | 
			
		||||
                                                     + $"| #{st.ShardId.ToString().PadBoth(3)} "
 | 
			
		||||
                                                     + $"| {timeDiff:mm\\:ss} "
 | 
			
		||||
                                                     + $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `";
 | 
			
		||||
                                          })
 | 
			
		||||
                                          .ToArray();
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
                curPage =>
 | 
			
		||||
                {
 | 
			
		||||
                    var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25));
 | 
			
		||||
 | 
			
		||||
                    if (string.IsNullOrWhiteSpace(str))
 | 
			
		||||
                        str = GetText(strs.no_shards_on_page);
 | 
			
		||||
 | 
			
		||||
                    return _eb.Create().WithOkColor().WithDescription($"{status}\n\n{str}");
 | 
			
		||||
                },
 | 
			
		||||
                allShardStrings.Length,
 | 
			
		||||
                25);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string ConnectionStateToEmoji(ShardStatus status)
 | 
			
		||||
        {
 | 
			
		||||
            var timeDiff = DateTime.UtcNow - status.LastUpdate;
 | 
			
		||||
            return status.ConnectionState switch
 | 
			
		||||
            {
 | 
			
		||||
                ConnectionState.Disconnected => "🔻",
 | 
			
		||||
                _ when timeDiff > TimeSpan.FromSeconds(30) => " ❗ ",
 | 
			
		||||
                ConnectionState.Connected => "✅",
 | 
			
		||||
                _ => " ⏳"
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task RestartShard(int shardId)
 | 
			
		||||
        {
 | 
			
		||||
            var success = _coord.RestartShard(shardId);
 | 
			
		||||
            if (success)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.shard_reconnecting(Format.Bold("#" + shardId)));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.no_shard_id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public Task Leave([Leftover] string guildStr)
 | 
			
		||||
            => _service.LeaveGuild(guildStr);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task DeleteEmptyServers()
 | 
			
		||||
        {
 | 
			
		||||
            await ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
            var toLeave = _client.Guilds
 | 
			
		||||
                                 .Where(s => s.MemberCount == 1 && s.Users.Count == 1)
 | 
			
		||||
                                 .ToList();
 | 
			
		||||
 | 
			
		||||
            foreach (var server in toLeave)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await server.DeleteAsync();
 | 
			
		||||
                    Log.Information("Deleted server {ServerName} [{ServerId}]",
 | 
			
		||||
                        server.Name,
 | 
			
		||||
                        server.Id);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex,
 | 
			
		||||
                        "Error leaving server {ServerName} [{ServerId}]",
 | 
			
		||||
                        server.Name,
 | 
			
		||||
                        server.Id);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.deleted_x_servers(toLeave.Count));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task Die(bool graceful = false)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _client.SetStatusAsync(UserStatus.Invisible);
 | 
			
		||||
                _ =  _client.StopAsync();
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.shutting_down);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await Task.Delay(2000);
 | 
			
		||||
            _coord.Die(graceful);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task Restart()
 | 
			
		||||
        {
 | 
			
		||||
            var success = _coord.RestartBot();
 | 
			
		||||
            if (!success)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.restart_fail);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try { await ReplyConfirmLocalizedAsync(strs.restarting); }
 | 
			
		||||
            catch { }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task SetName([Leftover] string newName)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(newName))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _client.CurrentUser.ModifyAsync(u => u.Username = newName);
 | 
			
		||||
            }
 | 
			
		||||
            catch (RateLimitedException)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("You've been ratelimited. Wait 2 hours to change your name");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.bot_name(Format.Bold(newName)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageNicknames)]
 | 
			
		||||
        [BotPerm(GuildPerm.ChangeNickname)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task SetNick([Leftover] string newNick = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(newNick))
 | 
			
		||||
                return;
 | 
			
		||||
            var curUser = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
            await curUser.ModifyAsync(u => u.Nickname = newNick);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.bot_nick(Format.Bold(newNick) ?? "-"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageNicknames)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageNicknames)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task SetNick(IGuildUser gu, [Leftover] string newNick = null)
 | 
			
		||||
        {
 | 
			
		||||
            var sg = (SocketGuild)ctx.Guild;
 | 
			
		||||
            if (sg.OwnerId == gu.Id
 | 
			
		||||
                || gu.GetRoles().Max(r => r.Position) >= sg.CurrentUser.GetRoles().Max(r => r.Position))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.insuf_perms_i);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await gu.ModifyAsync(u => u.Nickname = newNick);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.user_nick(Format.Bold(gu.ToString()), Format.Bold(newNick) ?? "-"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task SetStatus([Leftover] SettableUserStatus status)
 | 
			
		||||
        {
 | 
			
		||||
            await _client.SetStatusAsync(SettableUserStatusToUserStatus(status));
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.bot_status(Format.Bold(status.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task SetAvatar([Leftover] string img = null)
 | 
			
		||||
        {
 | 
			
		||||
            var success = await _service.SetAvatar(img);
 | 
			
		||||
 | 
			
		||||
            if (success)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.set_avatar);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task SetGame(ActivityType type, [Leftover] string game = null)
 | 
			
		||||
        {
 | 
			
		||||
            var rep = new ReplacementBuilder().WithDefault(Context).Build();
 | 
			
		||||
 | 
			
		||||
            await _service.SetGameAsync(game is null ? game : rep.Replace(game), type);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.set_game);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task SetStream(string url, [Leftover] string name = null)
 | 
			
		||||
        {
 | 
			
		||||
            name ??= "";
 | 
			
		||||
 | 
			
		||||
            await _service.SetStreamAsync(name, url);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.set_stream);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task Send(string where, [Leftover] SmartText text = null)
 | 
			
		||||
        {
 | 
			
		||||
            var ids = where.Split('|');
 | 
			
		||||
            if (ids.Length != 2)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var sid = ulong.Parse(ids[0]);
 | 
			
		||||
            var server = _client.Guilds.FirstOrDefault(s => s.Id == sid);
 | 
			
		||||
 | 
			
		||||
            if (server is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var rep = new ReplacementBuilder().WithDefault(Context).Build();
 | 
			
		||||
 | 
			
		||||
            if (ids[1].ToUpperInvariant().StartsWith("C:", StringComparison.InvariantCulture))
 | 
			
		||||
            {
 | 
			
		||||
                var cid = ulong.Parse(ids[1][2..]);
 | 
			
		||||
                var ch = server.TextChannels.FirstOrDefault(c => c.Id == cid);
 | 
			
		||||
                if (ch is null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                text = rep.Replace(text);
 | 
			
		||||
                await ch.SendAsync(text);
 | 
			
		||||
            }
 | 
			
		||||
            else if (ids[1].ToUpperInvariant().StartsWith("U:", StringComparison.InvariantCulture))
 | 
			
		||||
            {
 | 
			
		||||
                var uid = ulong.Parse(ids[1][2..]);
 | 
			
		||||
                var user = server.Users.FirstOrDefault(u => u.Id == uid);
 | 
			
		||||
                if (user is null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var ch = await user.CreateDMChannelAsync();
 | 
			
		||||
                text = rep.Replace(text);
 | 
			
		||||
                await ch.SendAsync(text);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.invalid_format);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.message_sent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task StringsReload()
 | 
			
		||||
        {
 | 
			
		||||
            _strings.Reload();
 | 
			
		||||
            await _medusaLoader.ReloadStrings();
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.bot_strings_reloaded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task CoordReload()
 | 
			
		||||
        {
 | 
			
		||||
            await _coord.Reload();
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus)
 | 
			
		||||
        {
 | 
			
		||||
            switch (sus)
 | 
			
		||||
            {
 | 
			
		||||
                case SettableUserStatus.Online:
 | 
			
		||||
                    return UserStatus.Online;
 | 
			
		||||
                case SettableUserStatus.Invisible:
 | 
			
		||||
                    return UserStatus.Invisible;
 | 
			
		||||
                case SettableUserStatus.Idle:
 | 
			
		||||
                    return UserStatus.AFK;
 | 
			
		||||
                case SettableUserStatus.Dnd:
 | 
			
		||||
                    return UserStatus.DoNotDisturb;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return UserStatus.Online;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,400 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly CommandHandler _cmdHandler;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly IBotStrings _strings;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
 | 
			
		||||
    private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
 | 
			
		||||
        new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
 | 
			
		||||
 | 
			
		||||
    private ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>> autoCommands = new();
 | 
			
		||||
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly BotConfigService _bss;
 | 
			
		||||
    private readonly IPubSub _pubSub;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
 | 
			
		||||
    //keys
 | 
			
		||||
    private readonly TypedKey<ActivityPubData> _activitySetKey;
 | 
			
		||||
    private readonly TypedKey<string> _guildLeaveKey;
 | 
			
		||||
 | 
			
		||||
    public SelfService(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        CommandHandler cmdHandler,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        IBotCredentials creds,
 | 
			
		||||
        IHttpClientFactory factory,
 | 
			
		||||
        BotConfigService bss,
 | 
			
		||||
        IPubSub pubSub,
 | 
			
		||||
        IEmbedBuilderService eb)
 | 
			
		||||
    {
 | 
			
		||||
        _cmdHandler = cmdHandler;
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
        _httpFactory = factory;
 | 
			
		||||
        _bss = bss;
 | 
			
		||||
        _pubSub = pubSub;
 | 
			
		||||
        _eb = eb;
 | 
			
		||||
        _activitySetKey = new("activity.set");
 | 
			
		||||
        _guildLeaveKey = new("guild.leave");
 | 
			
		||||
 | 
			
		||||
        HandleStatusChanges();
 | 
			
		||||
 | 
			
		||||
        _pubSub.Sub(_guildLeaveKey,
 | 
			
		||||
            async input =>
 | 
			
		||||
            {
 | 
			
		||||
                var guildStr = input.ToString().Trim().ToUpperInvariant();
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(guildStr))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr
 | 
			
		||||
                                                                || g.Name.Trim().ToUpperInvariant() == guildStr);
 | 
			
		||||
                if (server is null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (server.OwnerId != _client.CurrentUser.Id)
 | 
			
		||||
                {
 | 
			
		||||
                    await server.LeaveAsync();
 | 
			
		||||
                    Log.Information("Left server {Name} [{Id}]", server.Name, server.Id);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await server.DeleteAsync();
 | 
			
		||||
                    Log.Information("Deleted server {Name} [{Id}]", server.Name, server.Id);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        autoCommands = uow.AutoCommands.AsNoTracking()
 | 
			
		||||
            .Where(x => x.Interval >= 5)
 | 
			
		||||
            .AsEnumerable()
 | 
			
		||||
            .GroupBy(x => x.GuildId)
 | 
			
		||||
            .ToDictionary(x => x.Key,
 | 
			
		||||
                y => y.ToDictionary(x => x.Id, TimerFromAutoCommand).ToConcurrent())
 | 
			
		||||
            .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
        var startupCommands = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0);
 | 
			
		||||
        foreach (var cmd in startupCommands)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await ExecuteCommand(cmd);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (_client.ShardId == 0)
 | 
			
		||||
            await LoadOwnerChannels();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Timer TimerFromAutoCommand(AutoCommand x)
 | 
			
		||||
        => new(async obj => await ExecuteCommand((AutoCommand)obj), x, x.Interval * 1000, x.Interval * 1000);
 | 
			
		||||
 | 
			
		||||
    private async Task ExecuteCommand(AutoCommand cmd)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (cmd.GuildId is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var guildShard = (int)((cmd.GuildId.Value >> 22) % (ulong)_creds.TotalShards);
 | 
			
		||||
            if (guildShard != _client.ShardId)
 | 
			
		||||
                return;
 | 
			
		||||
            var prefix = _cmdHandler.GetPrefix(cmd.GuildId);
 | 
			
		||||
            //if someone already has .die as their startup command, ignore it
 | 
			
		||||
            if (cmd.CommandText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
 | 
			
		||||
                return;
 | 
			
		||||
            await _cmdHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex, "Error in SelfService ExecuteCommand");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void AddNewAutoCommand(AutoCommand cmd)
 | 
			
		||||
    {
 | 
			
		||||
        using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            uow.AutoCommands.Add(cmd);
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cmd.Interval >= 5)
 | 
			
		||||
        {
 | 
			
		||||
            var autos = autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary<int, Timer>());
 | 
			
		||||
            autos.AddOrUpdate(cmd.Id,
 | 
			
		||||
                _ => TimerFromAutoCommand(cmd),
 | 
			
		||||
                (_, old) =>
 | 
			
		||||
                {
 | 
			
		||||
                    old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                    return TimerFromAutoCommand(cmd);
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<AutoCommand> GetStartupCommands()
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0).OrderBy(x => x.Id).ToList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<AutoCommand> GetAutoCommands()
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.AutoCommands.AsNoTracking().Where(x => x.Interval >= 5).OrderBy(x => x.Id).ToList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task LoadOwnerChannels()
 | 
			
		||||
    {
 | 
			
		||||
        var channels = await _creds.OwnerIds.Select(id =>
 | 
			
		||||
            {
 | 
			
		||||
                var user = _client.GetUser(id);
 | 
			
		||||
                if (user is null)
 | 
			
		||||
                    return Task.FromResult<IDMChannel>(null);
 | 
			
		||||
 | 
			
		||||
                return user.CreateDMChannelAsync();
 | 
			
		||||
            })
 | 
			
		||||
            .WhenAll();
 | 
			
		||||
 | 
			
		||||
        ownerChannels = channels.Where(x => x is not null)
 | 
			
		||||
            .ToDictionary(x => x.Recipient.Id, x => x)
 | 
			
		||||
            .ToImmutableDictionary();
 | 
			
		||||
 | 
			
		||||
        if (!ownerChannels.Any())
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(
 | 
			
		||||
                "No owner channels created! Make sure you've specified the correct OwnerId in the creds.yml file and invited the bot to a Discord server");
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("Created {OwnerChannelCount} out of {TotalOwnerChannelCount} owner message channels",
 | 
			
		||||
                ownerChannels.Count,
 | 
			
		||||
                _creds.OwnerIds.Count);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task LeaveGuild(string guildStr)
 | 
			
		||||
        => _pubSub.Pub(_guildLeaveKey, guildStr);
 | 
			
		||||
 | 
			
		||||
    // forwards dms
 | 
			
		||||
    public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        var bs = _bss.Data;
 | 
			
		||||
        if (msg.Channel is IDMChannel && bs.ForwardMessages && (ownerChannels.Any() || bs.ForwardToChannel is not null))
 | 
			
		||||
        {
 | 
			
		||||
            var title = _strings.GetText(strs.dm_from) + $" [{msg.Author}]({msg.Author.Id})";
 | 
			
		||||
 | 
			
		||||
            var attachamentsTxt = _strings.GetText(strs.attachments);
 | 
			
		||||
 | 
			
		||||
            var toSend = msg.Content;
 | 
			
		||||
 | 
			
		||||
            if (msg.Attachments.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                toSend += $"\n\n{Format.Code(attachamentsTxt)}:\n"
 | 
			
		||||
                          + string.Join("\n", msg.Attachments.Select(a => a.ProxyUrl));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (bs.ForwardToAllOwners)
 | 
			
		||||
            {
 | 
			
		||||
                var allOwnerChannels = ownerChannels.Values;
 | 
			
		||||
 | 
			
		||||
                foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await ownerCh.SendConfirmAsync(_eb, title, toSend);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning("Can't contact owner with id {OwnerId}", ownerCh.Recipient.Id);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else if (bs.ForwardToChannel is ulong cid)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (_client.GetChannel(cid) is ITextChannel ch)
 | 
			
		||||
                        await ch.SendConfirmAsync(_eb, title, toSend);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning("Error forwarding message to the channel");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var firstOwnerChannel = ownerChannels.Values.First();
 | 
			
		||||
                if (firstOwnerChannel.Recipient.Id != msg.Author.Id)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await firstOwnerChannel.SendConfirmAsync(_eb, title, toSend);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch
 | 
			
		||||
                    {
 | 
			
		||||
                        // ignored
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool RemoveStartupCommand(int index, out AutoCommand cmd)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        cmd = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0).Skip(index).FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
        if (cmd is not null)
 | 
			
		||||
        {
 | 
			
		||||
            uow.Remove(cmd);
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool RemoveAutoCommand(int index, out AutoCommand cmd)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        cmd = uow.AutoCommands.AsNoTracking().Where(x => x.Interval >= 5).Skip(index).FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
        if (cmd is not null)
 | 
			
		||||
        {
 | 
			
		||||
            uow.Remove(cmd);
 | 
			
		||||
            if (autoCommands.TryGetValue(cmd.GuildId, out var autos))
 | 
			
		||||
            {
 | 
			
		||||
                if (autos.TryRemove(cmd.Id, out var timer))
 | 
			
		||||
                    timer.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetAvatar(string img)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(img))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (!Uri.IsWellFormedUriString(img, UriKind.Absolute))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        var uri = new Uri(img);
 | 
			
		||||
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        using var sr = await http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
 | 
			
		||||
        if (!sr.IsImage())
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        // i can't just do ReadAsStreamAsync because dicord.net's image poops itself
 | 
			
		||||
        var imgData = await sr.Content.ReadAsByteArrayAsync();
 | 
			
		||||
        await using var imgStream = imgData.ToStream();
 | 
			
		||||
        await _client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream));
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ClearStartupCommands()
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var toRemove = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0);
 | 
			
		||||
 | 
			
		||||
        uow.AutoCommands.RemoveRange(toRemove);
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool ForwardMessages()
 | 
			
		||||
    {
 | 
			
		||||
        var isForwarding = false;
 | 
			
		||||
        _bss.ModifyConfig(config => { isForwarding = config.ForwardMessages = !config.ForwardMessages; });
 | 
			
		||||
 | 
			
		||||
        return isForwarding;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool ForwardToAll()
 | 
			
		||||
    {
 | 
			
		||||
        var isToAll = false;
 | 
			
		||||
        _bss.ModifyConfig(config => { isToAll = config.ForwardToAllOwners = !config.ForwardToAllOwners; });
 | 
			
		||||
        return isToAll;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool ForwardToChannel(ulong? channelId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        _bss.ModifyConfig(config =>
 | 
			
		||||
        {
 | 
			
		||||
            config.ForwardToChannel = channelId == config.ForwardToChannel
 | 
			
		||||
                ? null
 | 
			
		||||
                : channelId;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return channelId is not null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void HandleStatusChanges()
 | 
			
		||||
        => _pubSub.Sub(_activitySetKey,
 | 
			
		||||
            async data =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await _client.SetGameAsync(data.Name, data.Link, data.Type);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error setting activity");
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    public Task SetGameAsync(string game, ActivityType type)
 | 
			
		||||
        => _pubSub.Pub(_activitySetKey,
 | 
			
		||||
            new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = game,
 | 
			
		||||
                Link = null,
 | 
			
		||||
                Type = type
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    public Task SetStreamAsync(string name, string link)
 | 
			
		||||
        => _pubSub.Pub(_activitySetKey,
 | 
			
		||||
            new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = name,
 | 
			
		||||
                Link = link,
 | 
			
		||||
                Type = ActivityType.Streaming
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    private sealed class ActivityPubData
 | 
			
		||||
    {
 | 
			
		||||
        public string Name { get; init; }
 | 
			
		||||
        public string Link { get; init; }
 | 
			
		||||
        public ActivityType Type { get; init; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,234 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class SelfAssignedRolesCommands : NadekoModule<SelfAssignedRolesService>
 | 
			
		||||
    {
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
        public async Task AdSarm()
 | 
			
		||||
        {
 | 
			
		||||
            var newVal = _service.ToggleAdSarm(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            if (newVal)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.adsarm_enable(prefix));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.adsarm_disable(prefix));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task Asar([Leftover] IRole role)
 | 
			
		||||
            => Asar(0, role);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Asar(int group, [Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var guser = (IGuildUser)ctx.User;
 | 
			
		||||
            if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var succ = _service.AddNew(ctx.Guild.Id, role, group);
 | 
			
		||||
 | 
			
		||||
            if (succ)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.role_added(Format.Bold(role.Name),
 | 
			
		||||
                    Format.Bold(@group.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.role_in_list(Format.Bold(role.Name)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Sargn(int group, [Leftover] string name = null)
 | 
			
		||||
        {
 | 
			
		||||
            var set = await _service.SetNameAsync(ctx.Guild.Id, group, name);
 | 
			
		||||
 | 
			
		||||
            if (set)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(
 | 
			
		||||
                    strs.group_name_added(Format.Bold(@group.ToString()), Format.Bold(name)));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.group_name_removed(Format.Bold(group.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task Rsar([Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var guser = (IGuildUser)ctx.User;
 | 
			
		||||
            if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var success = _service.RemoveSar(role.Guild.Id, role.Id);
 | 
			
		||||
            if (!success)
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.self_assign_not);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.self_assign_rem(Format.Bold(role.Name)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Lsar(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var (exclusive, roles, groups) = _service.GetRoles(ctx.Guild);
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
                cur =>
 | 
			
		||||
                {
 | 
			
		||||
                    var rolesStr = new StringBuilder();
 | 
			
		||||
                    var roleGroups = roles.OrderBy(x => x.Model.Group)
 | 
			
		||||
                                          .Skip(cur * 20)
 | 
			
		||||
                                          .Take(20)
 | 
			
		||||
                                          .GroupBy(x => x.Model.Group)
 | 
			
		||||
                                          .OrderBy(x => x.Key);
 | 
			
		||||
 | 
			
		||||
                    foreach (var kvp in roleGroups)
 | 
			
		||||
                    {
 | 
			
		||||
                        string groupNameText;
 | 
			
		||||
                        if (!groups.TryGetValue(kvp.Key, out var name))
 | 
			
		||||
                            groupNameText = Format.Bold(GetText(strs.self_assign_group(kvp.Key)));
 | 
			
		||||
                        else
 | 
			
		||||
                            groupNameText = Format.Bold($"{kvp.Key} - {name.TrimTo(25, true)}");
 | 
			
		||||
 | 
			
		||||
                        rolesStr.AppendLine("\t\t\t\t ⟪" + groupNameText + "⟫");
 | 
			
		||||
                        foreach (var (model, role) in kvp.AsEnumerable())
 | 
			
		||||
                        {
 | 
			
		||||
                            if (role is null)
 | 
			
		||||
                            {
 | 
			
		||||
                            }
 | 
			
		||||
                            else
 | 
			
		||||
                            {
 | 
			
		||||
                                // first character is invisible space
 | 
			
		||||
                                if (model.LevelRequirement == 0)
 | 
			
		||||
                                    rolesStr.AppendLine("   " + role.Name);
 | 
			
		||||
                                else
 | 
			
		||||
                                    rolesStr.AppendLine("   " + role.Name + $" (lvl {model.LevelRequirement}+)");
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        rolesStr.AppendLine();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return _eb.Create()
 | 
			
		||||
                              .WithOkColor()
 | 
			
		||||
                              .WithTitle(Format.Bold(GetText(strs.self_assign_list(roles.Count()))))
 | 
			
		||||
                              .WithDescription(rolesStr.ToString())
 | 
			
		||||
                              .WithFooter(exclusive
 | 
			
		||||
                                  ? GetText(strs.self_assign_are_exclusive)
 | 
			
		||||
                                  : GetText(strs.self_assign_are_not_exclusive));
 | 
			
		||||
                },
 | 
			
		||||
                roles.Count(),
 | 
			
		||||
                20);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task Togglexclsar()
 | 
			
		||||
        {
 | 
			
		||||
            var areExclusive = _service.ToggleEsar(ctx.Guild.Id);
 | 
			
		||||
            if (areExclusive)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.self_assign_excl);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.self_assign_no_excl);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async Task RoleLevelReq(int level, [Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            if (level < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var succ = _service.SetLevelReq(ctx.Guild.Id, role, level);
 | 
			
		||||
 | 
			
		||||
            if (!succ)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.self_assign_not);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.self_assign_level_req(Format.Bold(role.Name),
 | 
			
		||||
                Format.Bold(level.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Iam([Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var guildUser = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
            var (result, autoDelete, extra) = await _service.Assign(guildUser, role);
 | 
			
		||||
 | 
			
		||||
            IUserMessage msg;
 | 
			
		||||
            if (result == SelfAssignedRolesService.AssignResult.ErrNotAssignable)
 | 
			
		||||
                msg = await ReplyErrorLocalizedAsync(strs.self_assign_not);
 | 
			
		||||
            else if (result == SelfAssignedRolesService.AssignResult.ErrLvlReq)
 | 
			
		||||
                msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_level(Format.Bold(extra.ToString())));
 | 
			
		||||
            else if (result == SelfAssignedRolesService.AssignResult.ErrAlreadyHave)
 | 
			
		||||
                msg = await ReplyErrorLocalizedAsync(strs.self_assign_already(Format.Bold(role.Name)));
 | 
			
		||||
            else if (result == SelfAssignedRolesService.AssignResult.ErrNotPerms)
 | 
			
		||||
                msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms);
 | 
			
		||||
            else
 | 
			
		||||
                msg = await ReplyConfirmLocalizedAsync(strs.self_assign_success(Format.Bold(role.Name)));
 | 
			
		||||
 | 
			
		||||
            if (autoDelete)
 | 
			
		||||
            {
 | 
			
		||||
                msg.DeleteAfter(3);
 | 
			
		||||
                ctx.Message.DeleteAfter(3);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Iamnot([Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var guildUser = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
            var (result, autoDelete) = await _service.Remove(guildUser, role);
 | 
			
		||||
 | 
			
		||||
            IUserMessage msg;
 | 
			
		||||
            if (result == SelfAssignedRolesService.RemoveResult.ErrNotAssignable)
 | 
			
		||||
                msg = await ReplyErrorLocalizedAsync(strs.self_assign_not);
 | 
			
		||||
            else if (result == SelfAssignedRolesService.RemoveResult.ErrNotHave)
 | 
			
		||||
                msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_have(Format.Bold(role.Name)));
 | 
			
		||||
            else if (result == SelfAssignedRolesService.RemoveResult.ErrNotPerms)
 | 
			
		||||
                msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms);
 | 
			
		||||
            else
 | 
			
		||||
                msg = await ReplyConfirmLocalizedAsync(strs.self_assign_remove(Format.Bold(role.Name)));
 | 
			
		||||
 | 
			
		||||
            if (autoDelete)
 | 
			
		||||
            {
 | 
			
		||||
                msg.DeleteAfter(3);
 | 
			
		||||
                ctx.Message.DeleteAfter(3);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,235 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Xp;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class SelfAssignedRolesService : INService
 | 
			
		||||
{
 | 
			
		||||
    public enum AssignResult
 | 
			
		||||
    {
 | 
			
		||||
        Assigned, // successfully removed
 | 
			
		||||
        ErrNotAssignable, // not assignable (error)
 | 
			
		||||
        ErrAlreadyHave, // you already have that role (error)
 | 
			
		||||
        ErrNotPerms, // bot doesn't have perms (error)
 | 
			
		||||
        ErrLvlReq // you are not required level (error)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public enum RemoveResult
 | 
			
		||||
    {
 | 
			
		||||
        Removed, // successfully removed
 | 
			
		||||
        ErrNotAssignable, // not assignable (error)
 | 
			
		||||
        ErrNotHave, // you don't have a role you want to remove (error)
 | 
			
		||||
        ErrNotPerms // bot doesn't have perms (error)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    public SelfAssignedRolesService(DbService db)
 | 
			
		||||
        => _db = db;
 | 
			
		||||
 | 
			
		||||
    public bool AddNew(ulong guildId, IRole role, int group)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var roles = uow.SelfAssignableRoles.GetFromGuild(guildId);
 | 
			
		||||
        if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        uow.SelfAssignableRoles.Add(new()
 | 
			
		||||
        {
 | 
			
		||||
            Group = group,
 | 
			
		||||
            RoleId = role.Id,
 | 
			
		||||
            GuildId = role.Guild.Id
 | 
			
		||||
        });
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool ToggleAdSarm(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        bool newval;
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var config = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages;
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return newval;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role)
 | 
			
		||||
    {
 | 
			
		||||
        LevelStats userLevelData;
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var stats = uow.GetOrCreateUserXpStats(guildUser.Guild.Id, guildUser.Id);
 | 
			
		||||
            userLevelData = new(stats.Xp + stats.AwardedXp);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id);
 | 
			
		||||
 | 
			
		||||
        var theRoleYouWant = roles.FirstOrDefault(r => r.RoleId == role.Id);
 | 
			
		||||
        if (theRoleYouWant is null)
 | 
			
		||||
            return (AssignResult.ErrNotAssignable, autoDelete, null);
 | 
			
		||||
        if (theRoleYouWant.LevelRequirement > userLevelData.Level)
 | 
			
		||||
            return (AssignResult.ErrLvlReq, autoDelete, theRoleYouWant.LevelRequirement);
 | 
			
		||||
        if (guildUser.RoleIds.Contains(role.Id))
 | 
			
		||||
            return (AssignResult.ErrAlreadyHave, autoDelete, null);
 | 
			
		||||
 | 
			
		||||
        var roleIds = roles.Where(x => x.Group == theRoleYouWant.Group).Select(x => x.RoleId).ToArray();
 | 
			
		||||
        if (exclusive)
 | 
			
		||||
        {
 | 
			
		||||
            var sameRoles = guildUser.RoleIds.Where(r => roleIds.Contains(r));
 | 
			
		||||
 | 
			
		||||
            foreach (var roleId in sameRoles)
 | 
			
		||||
            {
 | 
			
		||||
                var sameRole = guildUser.Guild.GetRole(roleId);
 | 
			
		||||
                if (sameRole is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await guildUser.RemoveRoleAsync(sameRole);
 | 
			
		||||
                        await Task.Delay(300);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch
 | 
			
		||||
                    {
 | 
			
		||||
                        // ignored
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await guildUser.AddRoleAsync(role);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            return (AssignResult.ErrNotPerms, autoDelete, ex);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (AssignResult.Assigned, autoDelete, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetNameAsync(ulong guildId, int group, string name)
 | 
			
		||||
    {
 | 
			
		||||
        var set = false;
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, y => y.Include(x => x.SelfAssignableRoleGroupNames));
 | 
			
		||||
        var toUpdate = gc.SelfAssignableRoleGroupNames.FirstOrDefault(x => x.Number == group);
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(name))
 | 
			
		||||
        {
 | 
			
		||||
            if (toUpdate is not null)
 | 
			
		||||
                gc.SelfAssignableRoleGroupNames.Remove(toUpdate);
 | 
			
		||||
        }
 | 
			
		||||
        else if (toUpdate is null)
 | 
			
		||||
        {
 | 
			
		||||
            gc.SelfAssignableRoleGroupNames.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = name,
 | 
			
		||||
                Number = group
 | 
			
		||||
            });
 | 
			
		||||
            set = true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            toUpdate.Name = name;
 | 
			
		||||
            set = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        return set;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<(RemoveResult Result, bool AutoDelete)> Remove(IGuildUser guildUser, IRole role)
 | 
			
		||||
    {
 | 
			
		||||
        var (autoDelete, _, roles) = GetAdAndRoles(guildUser.Guild.Id);
 | 
			
		||||
 | 
			
		||||
        if (roles.FirstOrDefault(r => r.RoleId == role.Id) is null)
 | 
			
		||||
            return (RemoveResult.ErrNotAssignable, autoDelete);
 | 
			
		||||
        if (!guildUser.RoleIds.Contains(role.Id))
 | 
			
		||||
            return (RemoveResult.ErrNotHave, autoDelete);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await guildUser.RemoveRoleAsync(role);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception)
 | 
			
		||||
        {
 | 
			
		||||
            return (RemoveResult.ErrNotPerms, autoDelete);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (RemoveResult.Removed, autoDelete);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool RemoveSar(ulong guildId, ulong roleId)
 | 
			
		||||
    {
 | 
			
		||||
        bool success;
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        success = uow.SelfAssignableRoles.DeleteByGuildAndRoleId(guildId, roleId);
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public (bool AutoDelete, bool Exclusive, IReadOnlyCollection<SelfAssignedRole>) GetAdAndRoles(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        var autoDelete = gc.AutoDeleteSelfAssignedRoleMessages;
 | 
			
		||||
        var exclusive = gc.ExclusiveSelfAssignedRoles;
 | 
			
		||||
        var roles = uow.SelfAssignableRoles.GetFromGuild(guildId);
 | 
			
		||||
 | 
			
		||||
        return (autoDelete, exclusive, roles);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool SetLevelReq(ulong guildId, IRole role, int level)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var roles = uow.SelfAssignableRoles.GetFromGuild(guildId);
 | 
			
		||||
        var sar = roles.FirstOrDefault(x => x.RoleId == role.Id);
 | 
			
		||||
        if (sar is not null)
 | 
			
		||||
        {
 | 
			
		||||
            sar.LevelRequirement = level;
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool ToggleEsar(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        bool areExclusive;
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var config = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
 | 
			
		||||
        areExclusive = config.ExclusiveSelfAssignedRoles = !config.ExclusiveSelfAssignedRoles;
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return areExclusive;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public (bool Exclusive, IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary<int, string>
 | 
			
		||||
        GroupNames
 | 
			
		||||
        ) GetRoles(IGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        var exclusive = false;
 | 
			
		||||
 | 
			
		||||
        IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> roles;
 | 
			
		||||
        IDictionary<int, string> groupNames;
 | 
			
		||||
        using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var gc = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.SelfAssignableRoleGroupNames));
 | 
			
		||||
            exclusive = gc.ExclusiveSelfAssignedRoles;
 | 
			
		||||
            groupNames = gc.SelfAssignableRoleGroupNames.ToDictionary(x => x.Number, x => x.Name);
 | 
			
		||||
            var roleModels = uow.SelfAssignableRoles.GetFromGuild(guild.Id);
 | 
			
		||||
            roles = roleModels.Select(x => (Model: x, Role: guild.GetRole(x.RoleId)))
 | 
			
		||||
                              .ToList();
 | 
			
		||||
            uow.SelfAssignableRoles.RemoveRange(roles.Where(x => x.Role is null).Select(x => x.Model).ToArray());
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (exclusive, roles.Where(x => x.Role is not null).ToList(), groupNames);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public sealed class DummyLogCommandService : ILogCommandService
 | 
			
		||||
#if GLOBAL_NADEKO
 | 
			
		||||
, INService
 | 
			
		||||
#endif
 | 
			
		||||
{
 | 
			
		||||
    public void AddDeleteIgnore(ulong xId)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task LogServer(ulong guildId, ulong channelId, bool actionValue)
 | 
			
		||||
        => Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
    public bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType)
 | 
			
		||||
        => false;
 | 
			
		||||
 | 
			
		||||
    public LogSetting? GetGuildLogSettings(ulong guildId)
 | 
			
		||||
        => default;
 | 
			
		||||
 | 
			
		||||
    public bool Log(ulong guildId, ulong? channelId, LogType type)
 | 
			
		||||
        => false;
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,165 +0,0 @@
 | 
			
		||||
using NadekoBot.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    [NoPublicBot]
 | 
			
		||||
    public partial class LogCommands : NadekoModule<ILogCommandService>
 | 
			
		||||
    {
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task LogServer(PermissionAction action)
 | 
			
		||||
        {
 | 
			
		||||
            await _service.LogServer(ctx.Guild.Id, ctx.Channel.Id, action.Value);
 | 
			
		||||
            if (action.Value)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.log_all);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.log_disabled);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task LogIgnore()
 | 
			
		||||
        {
 | 
			
		||||
            var settings = _service.GetGuildLogSettings(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            var chs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.Channel).ToList()
 | 
			
		||||
                      ?? new List<IgnoredLogItem>();
 | 
			
		||||
            var usrs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.User).ToList()
 | 
			
		||||
                       ?? new List<IgnoredLogItem>();
 | 
			
		||||
 | 
			
		||||
            var eb = _eb.Create(ctx)
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .AddField(GetText(strs.log_ignored_channels),
 | 
			
		||||
                            chs.Count == 0
 | 
			
		||||
                                ? "-"
 | 
			
		||||
                                : string.Join('\n', chs.Select(x => $"{x.LogItemId} | <#{x.LogItemId}>")))
 | 
			
		||||
                        .AddField(GetText(strs.log_ignored_users),
 | 
			
		||||
                            usrs.Count == 0
 | 
			
		||||
                                ? "-"
 | 
			
		||||
                                : string.Join('\n', usrs.Select(x => $"{x.LogItemId} | <@{x.LogItemId}>")));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(eb);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task LogIgnore([Leftover] ITextChannel target)
 | 
			
		||||
        {
 | 
			
		||||
            var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.Channel);
 | 
			
		||||
 | 
			
		||||
            if (!removed)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(
 | 
			
		||||
                    strs.log_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")")));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(
 | 
			
		||||
                    strs.log_not_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")")));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task LogIgnore([Leftover] IUser target)
 | 
			
		||||
        {
 | 
			
		||||
            var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.User);
 | 
			
		||||
 | 
			
		||||
            if (!removed)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(
 | 
			
		||||
                    strs.log_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")")));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(
 | 
			
		||||
                    strs.log_not_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")")));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task LogEvents()
 | 
			
		||||
        {
 | 
			
		||||
            var logSetting = _service.GetGuildLogSettings(ctx.Guild.Id);
 | 
			
		||||
            var str = string.Join("\n",
 | 
			
		||||
                Enum.GetNames<LogType>()
 | 
			
		||||
                    .Select(x =>
 | 
			
		||||
                    {
 | 
			
		||||
                        var val = logSetting is null ? null : GetLogProperty(logSetting, Enum.Parse<LogType>(x));
 | 
			
		||||
                        if (val is not null)
 | 
			
		||||
                            return $"{Format.Bold(x)} <#{val}>";
 | 
			
		||||
                        return Format.Bold(x);
 | 
			
		||||
                    }));
 | 
			
		||||
 | 
			
		||||
            await SendConfirmAsync(Format.Bold(GetText(strs.log_events)) + "\n" + str);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static ulong? GetLogProperty(LogSetting l, LogType type)
 | 
			
		||||
        {
 | 
			
		||||
            switch (type)
 | 
			
		||||
            {
 | 
			
		||||
                case LogType.Other:
 | 
			
		||||
                    return l.LogOtherId;
 | 
			
		||||
                case LogType.MessageUpdated:
 | 
			
		||||
                    return l.MessageUpdatedId;
 | 
			
		||||
                case LogType.MessageDeleted:
 | 
			
		||||
                    return l.MessageDeletedId;
 | 
			
		||||
                case LogType.UserJoined:
 | 
			
		||||
                    return l.UserJoinedId;
 | 
			
		||||
                case LogType.UserLeft:
 | 
			
		||||
                    return l.UserLeftId;
 | 
			
		||||
                case LogType.UserBanned:
 | 
			
		||||
                    return l.UserBannedId;
 | 
			
		||||
                case LogType.UserUnbanned:
 | 
			
		||||
                    return l.UserUnbannedId;
 | 
			
		||||
                case LogType.UserUpdated:
 | 
			
		||||
                    return l.UserUpdatedId;
 | 
			
		||||
                case LogType.ChannelCreated:
 | 
			
		||||
                    return l.ChannelCreatedId;
 | 
			
		||||
                case LogType.ChannelDestroyed:
 | 
			
		||||
                    return l.ChannelDestroyedId;
 | 
			
		||||
                case LogType.ChannelUpdated:
 | 
			
		||||
                    return l.ChannelUpdatedId;
 | 
			
		||||
                case LogType.UserPresence:
 | 
			
		||||
                    return l.LogUserPresenceId;
 | 
			
		||||
                case LogType.VoicePresence:
 | 
			
		||||
                    return l.LogVoicePresenceId;
 | 
			
		||||
                case LogType.VoicePresenceTts:
 | 
			
		||||
                    return l.LogVoicePresenceTTSId;
 | 
			
		||||
                case LogType.UserMuted:
 | 
			
		||||
                    return l.UserMutedId;
 | 
			
		||||
                default:
 | 
			
		||||
                    return null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task Log(LogType type)
 | 
			
		||||
        {
 | 
			
		||||
            var val = _service.Log(ctx.Guild.Id, ctx.Channel.Id, type);
 | 
			
		||||
 | 
			
		||||
            if (val)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.log(Format.Bold(type.ToString())));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.log_stop(Format.Bold(type.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class GuildTimezoneService : INService
 | 
			
		||||
{
 | 
			
		||||
    public static ConcurrentDictionary<ulong, GuildTimezoneService> AllServices { get; } = new();
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, TimeZoneInfo> _timezones;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    public GuildTimezoneService(DiscordSocketClient client, Bot bot, DbService db)
 | 
			
		||||
    {
 | 
			
		||||
        _timezones = bot.AllGuildConfigs.Select(GetTimzezoneTuple)
 | 
			
		||||
                        .Where(x => x.Timezone is not null)
 | 
			
		||||
                        .ToDictionary(x => x.GuildId, x => x.Timezone)
 | 
			
		||||
                        .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
        var curUser = client.CurrentUser;
 | 
			
		||||
        if (curUser is not null)
 | 
			
		||||
            AllServices.TryAdd(curUser.Id, this);
 | 
			
		||||
        _db = db;
 | 
			
		||||
 | 
			
		||||
        bot.JoinedGuild += Bot_JoinedGuild;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task Bot_JoinedGuild(GuildConfig arg)
 | 
			
		||||
    {
 | 
			
		||||
        var (guildId, tz) = GetTimzezoneTuple(arg);
 | 
			
		||||
        if (tz is not null)
 | 
			
		||||
            _timezones.TryAdd(guildId, tz);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static (ulong GuildId, TimeZoneInfo Timezone) GetTimzezoneTuple(GuildConfig x)
 | 
			
		||||
    {
 | 
			
		||||
        TimeZoneInfo tz;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (x.TimeZoneId is null)
 | 
			
		||||
                tz = null;
 | 
			
		||||
            else
 | 
			
		||||
                tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            tz = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (x.GuildId, Timezone: tz);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public TimeZoneInfo GetTimeZoneOrDefault(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_timezones.TryGetValue(guildId, out var tz))
 | 
			
		||||
            return tz;
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void SetTimeZone(ulong guildId, TimeZoneInfo tz)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
 | 
			
		||||
        gc.TimeZoneId = tz?.Id;
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
 | 
			
		||||
        if (tz is null)
 | 
			
		||||
            _timezones.TryRemove(guildId, out tz);
 | 
			
		||||
        else
 | 
			
		||||
            _timezones.AddOrUpdate(guildId, tz, (_, _) => tz);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public TimeZoneInfo GetTimeZoneOrUtc(ulong guildId)
 | 
			
		||||
        => GetTimeZoneOrDefault(guildId) ?? TimeZoneInfo.Utc;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,75 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class TimeZoneCommands : NadekoModule<GuildTimezoneService>
 | 
			
		||||
    {
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Timezones(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            page--;
 | 
			
		||||
 | 
			
		||||
            if (page is < 0 or > 20)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var timezones = TimeZoneInfo.GetSystemTimeZones().OrderBy(x => x.BaseUtcOffset).ToArray();
 | 
			
		||||
            var timezonesPerPage = 20;
 | 
			
		||||
 | 
			
		||||
            var curTime = DateTimeOffset.UtcNow;
 | 
			
		||||
 | 
			
		||||
            var i = 0;
 | 
			
		||||
            var timezoneStrings = timezones.Select(x => (x, ++i % 2 == 0))
 | 
			
		||||
                                           .Select(data =>
 | 
			
		||||
                                           {
 | 
			
		||||
                                               var (tzInfo, flip) = data;
 | 
			
		||||
                                               var nameStr = $"{tzInfo.Id,-30}";
 | 
			
		||||
                                               var offset = curTime.ToOffset(tzInfo.GetUtcOffset(curTime))
 | 
			
		||||
                                                                   .ToString("zzz");
 | 
			
		||||
                                               if (flip)
 | 
			
		||||
                                                   return $"{offset} {Format.Code(nameStr)}";
 | 
			
		||||
                                               return $"{Format.Code(offset)} {nameStr}";
 | 
			
		||||
                                           });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
                curPage => _eb.Create()
 | 
			
		||||
                              .WithOkColor()
 | 
			
		||||
                              .WithTitle(GetText(strs.timezones_available))
 | 
			
		||||
                              .WithDescription(string.Join("\n",
 | 
			
		||||
                                  timezoneStrings.Skip(curPage * timezonesPerPage).Take(timezonesPerPage))),
 | 
			
		||||
                timezones.Length,
 | 
			
		||||
                timezonesPerPage);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Timezone()
 | 
			
		||||
            => await ReplyConfirmLocalizedAsync(strs.timezone_guild(_service.GetTimeZoneOrUtc(ctx.Guild.Id)));
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task Timezone([Leftover] string id)
 | 
			
		||||
        {
 | 
			
		||||
            TimeZoneInfo tz;
 | 
			
		||||
            try { tz = TimeZoneInfo.FindSystemTimeZoneById(id); }
 | 
			
		||||
            catch { tz = null; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            if (tz is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.timezone_not_found);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _service.SetTimeZone(ctx.Guild.Id, tz);
 | 
			
		||||
 | 
			
		||||
            await SendConfirmAsync(tz.ToString());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,935 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using Humanizer.Localisation;
 | 
			
		||||
using NadekoBot.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class UserPunishCommands : NadekoModule<UserPunishService>
 | 
			
		||||
    {
 | 
			
		||||
        public enum AddRole
 | 
			
		||||
        {
 | 
			
		||||
            AddRole
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly MuteService _mute;
 | 
			
		||||
        private readonly BlacklistService _blacklistService;
 | 
			
		||||
 | 
			
		||||
        public UserPunishCommands(MuteService mute, BlacklistService blacklistService)
 | 
			
		||||
        {
 | 
			
		||||
            _mute = mute;
 | 
			
		||||
            _blacklistService = blacklistService;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<bool> CheckRoleHierarchy(IGuildUser target)
 | 
			
		||||
        {
 | 
			
		||||
            var curUser = ((SocketGuild)ctx.Guild).CurrentUser;
 | 
			
		||||
            var ownerId = ctx.Guild.OwnerId;
 | 
			
		||||
            var modMaxRole = ((IGuildUser)ctx.User).GetRoles().Max(r => r.Position);
 | 
			
		||||
            var targetMaxRole = target.GetRoles().Max(r => r.Position);
 | 
			
		||||
            var botMaxRole = curUser.GetRoles().Max(r => r.Position);
 | 
			
		||||
            // bot can't punish a user who is higher in the hierarchy. Discord will return 403
 | 
			
		||||
            // moderator can be owner, in which case role hierarchy doesn't matter
 | 
			
		||||
            // otherwise, moderator has to have a higher role
 | 
			
		||||
            if (botMaxRole <= targetMaxRole
 | 
			
		||||
                || (ctx.User.Id != ownerId && targetMaxRole >= modMaxRole)
 | 
			
		||||
                || target.Id == ownerId)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.hierarchy);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public Task Warn(IGuildUser user, [Leftover] string reason = null)
 | 
			
		||||
            => Warn(1, user, reason);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task Warn(int weight, IGuildUser user, [Leftover] string reason = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (weight <= 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (!await CheckRoleHierarchy(user))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var dmFailed = false;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await user.EmbedAsync(_eb.Create()
 | 
			
		||||
                                         .WithErrorColor()
 | 
			
		||||
                                         .WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
 | 
			
		||||
                                         .AddField(GetText(strs.moderator), ctx.User.ToString())
 | 
			
		||||
                                         .AddField(GetText(strs.reason), reason ?? "-"));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                dmFailed = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            WarningPunishment punishment;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, weight, reason);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Exception occured while warning a user");
 | 
			
		||||
                var errorEmbed = _eb.Create().WithErrorColor().WithDescription(GetText(strs.cant_apply_punishment));
 | 
			
		||||
 | 
			
		||||
                if (dmFailed)
 | 
			
		||||
                    errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(errorEmbed);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var embed = _eb.Create().WithOkColor();
 | 
			
		||||
            if (punishment is null)
 | 
			
		||||
                embed.WithDescription(GetText(strs.user_warned(Format.Bold(user.ToString()))));
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                embed.WithDescription(GetText(strs.user_warned_and_punished(Format.Bold(user.ToString()),
 | 
			
		||||
                    Format.Bold(punishment.Punishment.ToString()))));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                embed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [NadekoOptions<WarnExpireOptions>]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task WarnExpire()
 | 
			
		||||
        {
 | 
			
		||||
            var expireDays = await _service.GetWarnExpire(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            if (expireDays == 0)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.warns_dont_expire);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.warns_expire_in(expireDays));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [NadekoOptions<WarnExpireOptions>]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public async Task WarnExpire(int days, params string[] args)
 | 
			
		||||
        {
 | 
			
		||||
            if (days is < 0 or > 366)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var opts = OptionsParser.ParseFrom<WarnExpireOptions>(args);
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
            await _service.WarnExpireAsync(ctx.Guild.Id, days, opts.Delete);
 | 
			
		||||
            if (days == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.warn_expire_reset);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (opts.Delete)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.warn_expire_set_delete(Format.Bold(days.ToString())));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.warn_expire_set_clear(Format.Bold(days.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public Task Warnlog(int page, [Leftover] IGuildUser user = null)
 | 
			
		||||
        {
 | 
			
		||||
            user ??= (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
            return Warnlog(page, user.Id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(3)]
 | 
			
		||||
        public Task Warnlog(IGuildUser user = null)
 | 
			
		||||
        {
 | 
			
		||||
            user ??= (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
            return ctx.User.Id == user.Id || ((IGuildUser)ctx.User).GuildPermissions.BanMembers
 | 
			
		||||
                ? Warnlog(user.Id)
 | 
			
		||||
                : Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task Warnlog(int page, ulong userId)
 | 
			
		||||
            => InternalWarnlog(userId, page - 1);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task Warnlog(ulong userId)
 | 
			
		||||
            => InternalWarnlog(userId, 0);
 | 
			
		||||
 | 
			
		||||
        private async Task InternalWarnlog(ulong userId, int inputPage)
 | 
			
		||||
        {
 | 
			
		||||
            if (inputPage < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var allWarnings = _service.UserWarnings(ctx.Guild.Id, userId);
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(inputPage,
 | 
			
		||||
                page =>
 | 
			
		||||
                {
 | 
			
		||||
                    var warnings = allWarnings.Skip(page * 9).Take(9).ToArray();
 | 
			
		||||
 | 
			
		||||
                    var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString();
 | 
			
		||||
                    var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.warnlog_for(user)));
 | 
			
		||||
 | 
			
		||||
                    if (!warnings.Any())
 | 
			
		||||
                        embed.WithDescription(GetText(strs.warnings_none));
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        var descText = GetText(strs.warn_count(
 | 
			
		||||
                            Format.Bold(warnings.Where(x => !x.Forgiven).Sum(x => x.Weight).ToString()),
 | 
			
		||||
                            Format.Bold(warnings.Sum(x => x.Weight).ToString())));
 | 
			
		||||
 | 
			
		||||
                        embed.WithDescription(descText);
 | 
			
		||||
 | 
			
		||||
                        var i = page * 9;
 | 
			
		||||
                        foreach (var w in warnings)
 | 
			
		||||
                        {
 | 
			
		||||
                            i++;
 | 
			
		||||
                            var name = GetText(strs.warned_on_by(w.DateAdded?.ToString("dd.MM.yyy"),
 | 
			
		||||
                                w.DateAdded?.ToString("HH:mm"),
 | 
			
		||||
                                w.Moderator));
 | 
			
		||||
 | 
			
		||||
                            if (w.Forgiven)
 | 
			
		||||
                                name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                            embed.AddField($"#`{i}` " + name,
 | 
			
		||||
                                Format.Code(GetText(strs.warn_weight(w.Weight))) + '\n' + w.Reason.TrimTo(1000));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return embed;
 | 
			
		||||
                },
 | 
			
		||||
                allWarnings.Length,
 | 
			
		||||
                9);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task WarnlogAll(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
            var warnings = _service.WarnlogAll(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
                curPage =>
 | 
			
		||||
                {
 | 
			
		||||
                    var ws = warnings.Skip(curPage * 15)
 | 
			
		||||
                                     .Take(15)
 | 
			
		||||
                                     .ToArray()
 | 
			
		||||
                                     .Select(x =>
 | 
			
		||||
                                     {
 | 
			
		||||
                                         var all = x.Count();
 | 
			
		||||
                                         var forgiven = x.Count(y => y.Forgiven);
 | 
			
		||||
                                         var total = all - forgiven;
 | 
			
		||||
                                         var usr = ((SocketGuild)ctx.Guild).GetUser(x.Key);
 | 
			
		||||
                                         return (usr?.ToString() ?? x.Key.ToString())
 | 
			
		||||
                                                + $" | {total} ({all} - {forgiven})";
 | 
			
		||||
                                     });
 | 
			
		||||
 | 
			
		||||
                    return _eb.Create()
 | 
			
		||||
                              .WithOkColor()
 | 
			
		||||
                              .WithTitle(GetText(strs.warnings_list))
 | 
			
		||||
                              .WithDescription(string.Join("\n", ws));
 | 
			
		||||
                },
 | 
			
		||||
                warnings.Length,
 | 
			
		||||
                15);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public Task Warnclear(IGuildUser user, int index = 0)
 | 
			
		||||
            => Warnclear(user.Id, index);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task Warnclear(ulong userId, int index = 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (index < 0)
 | 
			
		||||
                return;
 | 
			
		||||
            var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString());
 | 
			
		||||
            var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString());
 | 
			
		||||
            if (index == 0)
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.warnings_cleared(userStr));
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (success)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync(strs.warning_cleared(Format.Bold(index.ToString()), userStr));
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.warning_clear_fail);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task WarnPunish(
 | 
			
		||||
            int number,
 | 
			
		||||
            AddRole _,
 | 
			
		||||
            IRole role,
 | 
			
		||||
            StoopidTime time = null)
 | 
			
		||||
        {
 | 
			
		||||
            var punish = PunishmentAction.AddRole;
 | 
			
		||||
 | 
			
		||||
            if (ctx.Guild.OwnerId != ctx.User.Id
 | 
			
		||||
                && role.Position >= ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.role_too_high);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time, role);
 | 
			
		||||
 | 
			
		||||
            if (!success)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (time is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.warn_punish_set(Format.Bold(punish.ToString()),
 | 
			
		||||
                    Format.Bold(number.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
 | 
			
		||||
                    Format.Bold(number.ToString()),
 | 
			
		||||
                    Format.Bold(time.Input)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task WarnPunish(int number, PunishmentAction punish, StoopidTime time = null)
 | 
			
		||||
        {
 | 
			
		||||
            // this should never happen. Addrole has its own method with higher priority
 | 
			
		||||
            // also disallow warn punishment for getting warned
 | 
			
		||||
            if (punish is PunishmentAction.AddRole or PunishmentAction.Warn)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            // you must specify the time for timeout
 | 
			
		||||
            if (punish is PunishmentAction.TimeOut && time is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time);
 | 
			
		||||
 | 
			
		||||
            if (!success)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (time is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.warn_punish_set(Format.Bold(punish.ToString()),
 | 
			
		||||
                    Format.Bold(number.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
 | 
			
		||||
                    Format.Bold(number.ToString()),
 | 
			
		||||
                    Format.Bold(time.Input)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task WarnPunish(int number)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.WarnPunishRemove(ctx.Guild.Id, number))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.warn_punish_rem(Format.Bold(number.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task WarnPunishList()
 | 
			
		||||
        {
 | 
			
		||||
            var ps = _service.WarnPunishList(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            string list;
 | 
			
		||||
            if (ps.Any())
 | 
			
		||||
            {
 | 
			
		||||
                list = string.Join("\n",
 | 
			
		||||
                    ps.Select(x
 | 
			
		||||
                        => $"{x.Count} -> {x.Punishment} {(x.Punishment == PunishmentAction.AddRole ? $"<@&{x.RoleId}>" : "")} {(x.Time <= 0 ? "" : x.Time + "m")} "));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
                list = GetText(strs.warnpl_none);
 | 
			
		||||
 | 
			
		||||
            await SendConfirmAsync(GetText(strs.warn_punish_list), list);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null)
 | 
			
		||||
            => Ban(time, user.Id, msg);
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Ban(StoopidTime time, ulong userId, [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (time.Time > TimeSpan.FromDays(49))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            if (guildUser is not null && !await CheckRoleHierarchy(guildUser))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var dmFailed = false;
 | 
			
		||||
 | 
			
		||||
            if (guildUser is not null)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
 | 
			
		||||
                    var embed = _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time);
 | 
			
		||||
                    if (embed is not null)
 | 
			
		||||
                        await guildUser.SendAsync(embed);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    dmFailed = true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var user = await ctx.Client.GetUserAsync(userId);
 | 
			
		||||
            var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
 | 
			
		||||
            await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
 | 
			
		||||
            var toSend = _eb.Create()
 | 
			
		||||
                            .WithOkColor()
 | 
			
		||||
                            .WithTitle("⛔️ " + GetText(strs.banned_user))
 | 
			
		||||
                            .AddField(GetText(strs.username),  user?.ToString() ?? userId.ToString(), true)
 | 
			
		||||
                            .AddField("ID", userId.ToString(), true)
 | 
			
		||||
                            .AddField(GetText(strs.duration),
 | 
			
		||||
                                time.Time.Humanize(3, minUnit: TimeUnit.Minute, culture: Culture),
 | 
			
		||||
                                true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Ban(ulong userId, [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
 | 
			
		||||
            if (user is null)
 | 
			
		||||
            {
 | 
			
		||||
                var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
 | 
			
		||||
                await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512));
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                                .WithOkColor()
 | 
			
		||||
                                                .WithTitle("⛔️ " + GetText(strs.banned_user))
 | 
			
		||||
                                                .AddField("ID", userId.ToString(), true));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
                await Ban(user, msg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public async Task Ban(IGuildUser user, [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckRoleHierarchy(user))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var dmFailed = false;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
 | 
			
		||||
                var embed = _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null);
 | 
			
		||||
                if (embed is not null)
 | 
			
		||||
                    await user.SendAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                dmFailed = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
 | 
			
		||||
            await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512));
 | 
			
		||||
 | 
			
		||||
            var toSend = _eb.Create()
 | 
			
		||||
                            .WithOkColor()
 | 
			
		||||
                            .WithTitle("⛔️ " + GetText(strs.banned_user))
 | 
			
		||||
                            .AddField(GetText(strs.username), user.ToString(), true)
 | 
			
		||||
                            .AddField("ID", user.Id.ToString(), true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task BanPrune(int days)
 | 
			
		||||
        {
 | 
			
		||||
            if (days < 0 || days > 7)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.invalid_input);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            await _service.SetBanPruneAsync(ctx.Guild.Id, days);
 | 
			
		||||
            
 | 
			
		||||
            if (days == 0)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.ban_prune_disabled);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.ban_prune(days));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task BanMessage([Leftover] string message = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (message is null)
 | 
			
		||||
            {
 | 
			
		||||
                var template = _service.GetBanTemplate(ctx.Guild.Id);
 | 
			
		||||
                if (template is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync(strs.banmsg_default);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await SendConfirmAsync(template);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _service.SetBanTemplate(ctx.Guild.Id, message);
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task BanMsgReset()
 | 
			
		||||
        {
 | 
			
		||||
            _service.SetBanTemplate(ctx.Guild.Id, null);
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task BanMessageTest([Leftover] string reason = null)
 | 
			
		||||
            => InternalBanMessageTest(reason, null);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task BanMessageTest(StoopidTime duration, [Leftover] string reason = null)
 | 
			
		||||
            => InternalBanMessageTest(reason, duration.Time);
 | 
			
		||||
 | 
			
		||||
        private async Task InternalBanMessageTest(string reason, TimeSpan? duration)
 | 
			
		||||
        {
 | 
			
		||||
            var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), reason));
 | 
			
		||||
            var embed = _service.GetBanUserDmEmbed(Context, (IGuildUser)ctx.User, defaultMessage, reason, duration);
 | 
			
		||||
 | 
			
		||||
            if (embed is null)
 | 
			
		||||
                await ConfirmLocalizedAsync(strs.banmsg_disabled);
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.User.SendAsync(embed);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.unable_to_dm_user);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task Unban([Leftover] string user)
 | 
			
		||||
        {
 | 
			
		||||
            var bans = await ctx.Guild.GetBansAsync().FlattenAsync();
 | 
			
		||||
 | 
			
		||||
            var bun = bans.FirstOrDefault(x => x.User.ToString()!.ToLowerInvariant() == user.ToLowerInvariant());
 | 
			
		||||
 | 
			
		||||
            if (bun is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.user_not_found);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await UnbanInternal(bun.User);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task Unban(ulong userId)
 | 
			
		||||
        {
 | 
			
		||||
            var bun = await ctx.Guild.GetBanAsync(userId);
 | 
			
		||||
 | 
			
		||||
            if (bun is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.user_not_found);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await UnbanInternal(bun.User);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task UnbanInternal(IUser user)
 | 
			
		||||
        {
 | 
			
		||||
            await ctx.Guild.RemoveBanAsync(user);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.unbanned_user(Format.Bold(user.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public Task Softban(IGuildUser user, [Leftover] string msg = null)
 | 
			
		||||
            => SoftbanInternal(user, msg);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async Task Softban(ulong userId, [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
 | 
			
		||||
            if (user is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await SoftbanInternal(user, msg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task SoftbanInternal(IGuildUser user, [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckRoleHierarchy(user))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var dmFailed = false;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await user.SendErrorAsync(_eb, GetText(strs.sbdm(Format.Bold(ctx.Guild.Name), msg)));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                dmFailed = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
 | 
			
		||||
            await ctx.Guild.AddBanAsync(user, banPrune, ("Softban | " + ctx.User + " | " + msg).TrimTo(512));
 | 
			
		||||
            try { await ctx.Guild.RemoveBanAsync(user); }
 | 
			
		||||
            catch { await ctx.Guild.RemoveBanAsync(user); }
 | 
			
		||||
 | 
			
		||||
            var toSend = _eb.Create()
 | 
			
		||||
                            .WithOkColor()
 | 
			
		||||
                            .WithTitle("☣ " + GetText(strs.sb_user))
 | 
			
		||||
                            .AddField(GetText(strs.username), user.ToString(), true)
 | 
			
		||||
                            .AddField("ID", user.Id.ToString(), true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.KickMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.KickMembers)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task Kick(IGuildUser user, [Leftover] string msg = null)
 | 
			
		||||
            => KickInternal(user, msg);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.KickMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.KickMembers)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Kick(ulong userId, [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
 | 
			
		||||
            if (user is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await KickInternal(user, msg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task KickInternal(IGuildUser user, string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckRoleHierarchy(user))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var dmFailed = false;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await user.SendErrorAsync(_eb, GetText(strs.kickdm(Format.Bold(ctx.Guild.Name), msg)));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                dmFailed = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
 | 
			
		||||
 | 
			
		||||
            var toSend = _eb.Create()
 | 
			
		||||
                            .WithOkColor()
 | 
			
		||||
                            .WithTitle(GetText(strs.kicked_user))
 | 
			
		||||
                            .AddField(GetText(strs.username), user.ToString(), true)
 | 
			
		||||
                            .AddField("ID", user.Id.ToString(), true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ModerateMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.ModerateMembers)]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public async Task Timeout(IUser globalUser, StoopidTime time,  [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            var user = await ctx.Guild.GetUserAsync(globalUser.Id);
 | 
			
		||||
 | 
			
		||||
            if (user is null)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            if (!await CheckRoleHierarchy(user))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var dmFailed = false;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg));
 | 
			
		||||
                await user.EmbedAsync(_eb.Create(ctx)
 | 
			
		||||
                        .WithPendingColor()
 | 
			
		||||
                        .WithDescription(dmMessage));
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                dmFailed = true;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            await user.SetTimeOutAsync(time.Time);
 | 
			
		||||
 | 
			
		||||
            var toSend = _eb.Create()
 | 
			
		||||
                .WithOkColor()
 | 
			
		||||
                .WithTitle("⏳ " + GetText(strs.timedout_user))
 | 
			
		||||
                .AddField(GetText(strs.username), user.ToString(), true)
 | 
			
		||||
                .AddField("ID", user.Id.ToString(), true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [Ratelimit(30)]
 | 
			
		||||
        public async Task MassBan(params string[] userStrings)
 | 
			
		||||
        {
 | 
			
		||||
            if (userStrings.Length == 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var missing = new List<string>();
 | 
			
		||||
            var banning = new HashSet<IUser>();
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
            foreach (var userStr in userStrings)
 | 
			
		||||
            {
 | 
			
		||||
                if (ulong.TryParse(userStr, out var userId))
 | 
			
		||||
                {
 | 
			
		||||
                    IUser user = await ctx.Guild.GetUserAsync(userId)
 | 
			
		||||
                                 ?? await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id,
 | 
			
		||||
                                     userId);
 | 
			
		||||
 | 
			
		||||
                    if (user is null)
 | 
			
		||||
                    {
 | 
			
		||||
                        // if IGuildUser is null, try to get IUser
 | 
			
		||||
                        user = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(userId);
 | 
			
		||||
 | 
			
		||||
                        // only add to missing if *still* null
 | 
			
		||||
                        if (user is null)
 | 
			
		||||
                        {
 | 
			
		||||
                            missing.Add(userStr);
 | 
			
		||||
                            continue;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    //Hierachy checks only if the user is in the guild
 | 
			
		||||
                    if (user is IGuildUser gu && !await CheckRoleHierarchy(gu))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    banning.Add(user);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                    missing.Add(userStr);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var missStr = string.Join("\n", missing);
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(missStr))
 | 
			
		||||
                missStr = "-";
 | 
			
		||||
 | 
			
		||||
            var toSend = _eb.Create(ctx)
 | 
			
		||||
                            .WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
 | 
			
		||||
                            .AddField(GetText(strs.invalid(missing.Count)), missStr)
 | 
			
		||||
                            .WithPendingColor();
 | 
			
		||||
 | 
			
		||||
            var banningMessage = await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
 | 
			
		||||
            var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
 | 
			
		||||
            foreach (var toBan in banning)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Guild.AddBanAsync(toBan.Id, banPrune, $"{ctx.User} | Massban");
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error banning {User} user in {GuildId} server", toBan.Id, ctx.Guild.Id);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await banningMessage.ModifyAsync(x => x.Embed = _eb.Create()
 | 
			
		||||
                                                               .WithDescription(
 | 
			
		||||
                                                                   GetText(strs.mass_ban_completed(banning.Count())))
 | 
			
		||||
                                                               .AddField(GetText(strs.invalid(missing.Count)), missStr)
 | 
			
		||||
                                                               .WithOkColor()
 | 
			
		||||
                                                               .Build());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task MassKill([Leftover] string people)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(people))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var (bans, missing) = _service.MassKill((SocketGuild)ctx.Guild, people);
 | 
			
		||||
 | 
			
		||||
            var missStr = string.Join("\n", missing);
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(missStr))
 | 
			
		||||
                missStr = "-";
 | 
			
		||||
 | 
			
		||||
            //send a message but don't wait for it
 | 
			
		||||
            var banningMessageTask = ctx.Channel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                                               .WithDescription(
 | 
			
		||||
                                                                   GetText(strs.mass_kill_in_progress(bans.Count())))
 | 
			
		||||
                                                               .AddField(GetText(strs.invalid(missing)), missStr)
 | 
			
		||||
                                                               .WithPendingColor());
 | 
			
		||||
 | 
			
		||||
            var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
 | 
			
		||||
            //do the banning
 | 
			
		||||
            await Task.WhenAll(bans.Where(x => x.Id.HasValue)
 | 
			
		||||
                                   .Select(x => ctx.Guild.AddBanAsync(x.Id.Value,
 | 
			
		||||
                                       banPrune,
 | 
			
		||||
                                       x.Reason,
 | 
			
		||||
                                       new()
 | 
			
		||||
                                       {
 | 
			
		||||
                                           RetryMode = RetryMode.AlwaysRetry
 | 
			
		||||
                                       })));
 | 
			
		||||
 | 
			
		||||
            //wait for the message and edit it
 | 
			
		||||
            var banningMessage = await banningMessageTask;
 | 
			
		||||
 | 
			
		||||
            await banningMessage.ModifyAsync(x => x.Embed = _eb.Create()
 | 
			
		||||
                                                               .WithDescription(
 | 
			
		||||
                                                                   GetText(strs.mass_kill_completed(bans.Count())))
 | 
			
		||||
                                                               .AddField(GetText(strs.invalid(missing)), missStr)
 | 
			
		||||
                                                               .WithOkColor()
 | 
			
		||||
                                                               .Build());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class WarnExpireOptions : INadekoCommandOptions
 | 
			
		||||
        {
 | 
			
		||||
            [Option('d', "delete", Default = false, HelpText = "Delete warnings instead of clearing them.")]
 | 
			
		||||
            public bool Delete { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
            public void NormalizeOptions()
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,595 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class UserPunishService : INService, IReadyExecutor
 | 
			
		||||
{
 | 
			
		||||
    private readonly MuteService _mute;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly BlacklistService _blacklistService;
 | 
			
		||||
    private readonly BotConfigService _bcs;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
    public event Func<Warning, Task> OnUserWarned = static delegate { return Task.CompletedTask; };
 | 
			
		||||
 | 
			
		||||
    public UserPunishService(
 | 
			
		||||
        MuteService mute,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        BlacklistService blacklistService,
 | 
			
		||||
        BotConfigService bcs,
 | 
			
		||||
        DiscordSocketClient client)
 | 
			
		||||
    {
 | 
			
		||||
        _mute = mute;
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _blacklistService = blacklistService;
 | 
			
		||||
        _bcs = bcs;
 | 
			
		||||
        _client = client;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (_client.ShardId != 0)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        using var expiryTimer = new PeriodicTimer(TimeSpan.FromHours(12));
 | 
			
		||||
        do
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await CheckAllWarnExpiresAsync();
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Unexpected error while checking for warn expiries: {ErrorMessage}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
        } while (await expiryTimer.WaitForNextTickAsync());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<WarningPunishment> Warn(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        IUser mod,
 | 
			
		||||
        long weight,
 | 
			
		||||
        string reason)
 | 
			
		||||
    {
 | 
			
		||||
        if (weight <= 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(weight));
 | 
			
		||||
 | 
			
		||||
        var modName = mod.ToString();
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(reason))
 | 
			
		||||
            reason = "-";
 | 
			
		||||
 | 
			
		||||
        var guildId = guild.Id;
 | 
			
		||||
 | 
			
		||||
        var warn = new Warning
 | 
			
		||||
        {
 | 
			
		||||
            UserId = userId,
 | 
			
		||||
            GuildId = guildId,
 | 
			
		||||
            Forgiven = false,
 | 
			
		||||
            Reason = reason,
 | 
			
		||||
            Moderator = modName,
 | 
			
		||||
            Weight = weight
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        long previousCount;
 | 
			
		||||
        List<WarningPunishment> ps;
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
 | 
			
		||||
 | 
			
		||||
            previousCount = uow.Warnings.ForId(guildId, userId)
 | 
			
		||||
                                .Where(w => !w.Forgiven && w.UserId == userId)
 | 
			
		||||
                                .Sum(x => x.Weight);
 | 
			
		||||
 | 
			
		||||
            uow.Warnings.Add(warn);
 | 
			
		||||
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ = OnUserWarned(warn);
 | 
			
		||||
 | 
			
		||||
        var totalCount = previousCount + weight;
 | 
			
		||||
        
 | 
			
		||||
        var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount)
 | 
			
		||||
                  .MaxBy(x => x.Count);
 | 
			
		||||
 | 
			
		||||
        if (p is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var user = await guild.GetUserAsync(userId);
 | 
			
		||||
            if (user is null)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            await ApplyPunishment(guild, user, mod, p.Punishment, p.Time, p.RoleId, "Warned too many times.");
 | 
			
		||||
            return p;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task ApplyPunishment(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        IGuildUser user,
 | 
			
		||||
        IUser mod,
 | 
			
		||||
        PunishmentAction p,
 | 
			
		||||
        int minutes,
 | 
			
		||||
        ulong? roleId,
 | 
			
		||||
        string reason)
 | 
			
		||||
    {
 | 
			
		||||
        if (!await CheckPermission(guild, p))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        int banPrune;
 | 
			
		||||
        switch (p)
 | 
			
		||||
        {
 | 
			
		||||
            case PunishmentAction.Mute:
 | 
			
		||||
                if (minutes == 0)
 | 
			
		||||
                    await _mute.MuteUser(user, mod, reason: reason);
 | 
			
		||||
                else
 | 
			
		||||
                    await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), reason: reason);
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.VoiceMute:
 | 
			
		||||
                if (minutes == 0)
 | 
			
		||||
                    await _mute.MuteUser(user, mod, MuteType.Voice, reason);
 | 
			
		||||
                else
 | 
			
		||||
                    await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Voice, reason);
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.ChatMute:
 | 
			
		||||
                if (minutes == 0)
 | 
			
		||||
                    await _mute.MuteUser(user, mod, MuteType.Chat, reason);
 | 
			
		||||
                else
 | 
			
		||||
                    await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Chat, reason);
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.Kick:
 | 
			
		||||
                await user.KickAsync(reason);
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.Ban:
 | 
			
		||||
                banPrune = await GetBanPruneAsync(user.GuildId) ?? 7;
 | 
			
		||||
                if (minutes == 0)
 | 
			
		||||
                    await guild.AddBanAsync(user, reason: reason, pruneDays: banPrune);
 | 
			
		||||
                else
 | 
			
		||||
                    await _mute.TimedBan(user.Guild, user.Id, TimeSpan.FromMinutes(minutes), reason, banPrune);
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.Softban:
 | 
			
		||||
                banPrune = await GetBanPruneAsync(user.GuildId) ?? 7;
 | 
			
		||||
                await guild.AddBanAsync(user, banPrune, $"Softban | {reason}");
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await guild.RemoveBanAsync(user);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    await guild.RemoveBanAsync(user);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.RemoveRoles:
 | 
			
		||||
                await user.RemoveRolesAsync(user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole));
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.AddRole:
 | 
			
		||||
                if (roleId is null)
 | 
			
		||||
                    return;
 | 
			
		||||
                var role = guild.GetRole(roleId.Value);
 | 
			
		||||
                if (role is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (minutes == 0)
 | 
			
		||||
                        await user.AddRoleAsync(role);
 | 
			
		||||
                    else
 | 
			
		||||
                        await _mute.TimedRole(user, TimeSpan.FromMinutes(minutes), reason, role);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning("Can't find role {RoleId} on server {GuildId} to apply punishment",
 | 
			
		||||
                        roleId.Value,
 | 
			
		||||
                        guild.Id);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.Warn:
 | 
			
		||||
                await Warn(guild, user.Id, mod, 1, reason);
 | 
			
		||||
                break;
 | 
			
		||||
            case PunishmentAction.TimeOut:
 | 
			
		||||
                await user.SetTimeOutAsync(TimeSpan.FromMinutes(minutes));
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Used to prevent the bot from hitting 403's when it needs to
 | 
			
		||||
    ///     apply punishments with insufficient permissions
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guild">Guild the punishment is applied in</param>
 | 
			
		||||
    /// <param name="punish">Punishment to apply</param>
 | 
			
		||||
    /// <returns>Whether the bot has sufficient permissions</returns>
 | 
			
		||||
    private async Task<bool> CheckPermission(IGuild guild, PunishmentAction punish)
 | 
			
		||||
    {
 | 
			
		||||
        var botUser = await guild.GetCurrentUserAsync();
 | 
			
		||||
        switch (punish)
 | 
			
		||||
        {
 | 
			
		||||
            case PunishmentAction.Mute:
 | 
			
		||||
                return botUser.GuildPermissions.MuteMembers && botUser.GuildPermissions.ManageRoles;
 | 
			
		||||
            case PunishmentAction.Kick:
 | 
			
		||||
                return botUser.GuildPermissions.KickMembers;
 | 
			
		||||
            case PunishmentAction.Ban:
 | 
			
		||||
                return botUser.GuildPermissions.BanMembers;
 | 
			
		||||
            case PunishmentAction.Softban:
 | 
			
		||||
                return botUser.GuildPermissions.BanMembers; // ban + unban
 | 
			
		||||
            case PunishmentAction.RemoveRoles:
 | 
			
		||||
                return botUser.GuildPermissions.ManageRoles;
 | 
			
		||||
            case PunishmentAction.ChatMute:
 | 
			
		||||
                return botUser.GuildPermissions.ManageRoles; // adds nadeko-mute role
 | 
			
		||||
            case PunishmentAction.VoiceMute:
 | 
			
		||||
                return botUser.GuildPermissions.MuteMembers;
 | 
			
		||||
            case PunishmentAction.AddRole:
 | 
			
		||||
                return botUser.GuildPermissions.ManageRoles;
 | 
			
		||||
            case PunishmentAction.TimeOut:
 | 
			
		||||
                return botUser.GuildPermissions.ModerateMembers;
 | 
			
		||||
            default:
 | 
			
		||||
                return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task CheckAllWarnExpiresAsync()
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var cleared = await uow.Warnings
 | 
			
		||||
                               .Where(x => uow.GuildConfigs
 | 
			
		||||
                                                 .Any(y => y.GuildId == x.GuildId
 | 
			
		||||
                                                           && y.WarnExpireHours > 0
 | 
			
		||||
                                                           && y.WarnExpireAction == WarnExpireAction.Clear)
 | 
			
		||||
                                           && x.Forgiven == false 
 | 
			
		||||
                                           && x.DateAdded
 | 
			
		||||
                                           < DateTime.UtcNow.AddHours(-uow.GuildConfigs
 | 
			
		||||
                                                                          .Where(y => x.GuildId == y.GuildId)
 | 
			
		||||
                                                                          .Select(y => y.WarnExpireHours)
 | 
			
		||||
                                                                          .First()))
 | 
			
		||||
                               .UpdateAsync(_ => new()
 | 
			
		||||
                               {
 | 
			
		||||
                                   Forgiven = true,
 | 
			
		||||
                                   ForgivenBy = "expiry"
 | 
			
		||||
                               });
 | 
			
		||||
 | 
			
		||||
        var deleted = await uow.Warnings
 | 
			
		||||
                               .Where(x => uow.GuildConfigs
 | 
			
		||||
                                              .Any(y => y.GuildId == x.GuildId
 | 
			
		||||
                                                        && y.WarnExpireHours > 0
 | 
			
		||||
                                                        && y.WarnExpireAction == WarnExpireAction.Delete)
 | 
			
		||||
                                           && x.DateAdded
 | 
			
		||||
                                           < DateTime.UtcNow.AddHours(-uow.GuildConfigs
 | 
			
		||||
                                                                          .Where(y => x.GuildId == y.GuildId)
 | 
			
		||||
                                                                          .Select(y => y.WarnExpireHours)
 | 
			
		||||
                                                                          .First()))
 | 
			
		||||
                               .DeleteAsync();
 | 
			
		||||
 | 
			
		||||
        if (cleared > 0 || deleted > 0)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("Cleared {ClearedWarnings} warnings and deleted {DeletedWarnings} warnings due to expiry",
 | 
			
		||||
                cleared,
 | 
			
		||||
                deleted);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task CheckWarnExpiresAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var config = uow.GuildConfigsForId(guildId, inc => inc);
 | 
			
		||||
 | 
			
		||||
        if (config.WarnExpireHours == 0)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (config.WarnExpireAction == WarnExpireAction.Clear)
 | 
			
		||||
        {
 | 
			
		||||
            await uow.Warnings
 | 
			
		||||
                     .Where(x => x.GuildId == guildId
 | 
			
		||||
                                 && x.Forgiven == false
 | 
			
		||||
                                 && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
 | 
			
		||||
                     .UpdateAsync(_ => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         Forgiven = true,
 | 
			
		||||
                         ForgivenBy = "expiry"
 | 
			
		||||
                     });
 | 
			
		||||
        }
 | 
			
		||||
        else if (config.WarnExpireAction == WarnExpireAction.Delete)
 | 
			
		||||
        {
 | 
			
		||||
            await uow.Warnings
 | 
			
		||||
                     .Where(x => x.GuildId == guildId
 | 
			
		||||
                                 && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
 | 
			
		||||
                     .DeleteAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<int> GetWarnExpire(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var config = uow.GuildConfigsForId(guildId, set => set);
 | 
			
		||||
        return Task.FromResult(config.WarnExpireHours / 24);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task WarnExpireAsync(ulong guildId, int days, bool delete)
 | 
			
		||||
    {
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GuildConfigsForId(guildId, inc => inc);
 | 
			
		||||
 | 
			
		||||
            config.WarnExpireHours = days * 24;
 | 
			
		||||
            config.WarnExpireAction = delete ? WarnExpireAction.Delete : WarnExpireAction.Clear;
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
            // no need to check for warn expires
 | 
			
		||||
            if (config.WarnExpireHours == 0)
 | 
			
		||||
                return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CheckWarnExpiresAsync(guildId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IGrouping<ulong, Warning>[] WarnlogAll(ulong gid)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.Warnings.GetForGuild(gid).GroupBy(x => x.UserId).ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Warning[] UserWarnings(ulong gid, ulong userId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.Warnings.ForId(gid, userId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> WarnClearAsync(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        int index,
 | 
			
		||||
        string moderator)
 | 
			
		||||
    {
 | 
			
		||||
        var toReturn = true;
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        if (index == 0)
 | 
			
		||||
            await uow.Warnings.ForgiveAll(guildId, userId, moderator);
 | 
			
		||||
        else
 | 
			
		||||
            toReturn = uow.Warnings.Forgive(guildId, userId, moderator, index - 1);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        return toReturn;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool WarnPunish(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        int number,
 | 
			
		||||
        PunishmentAction punish,
 | 
			
		||||
        StoopidTime time,
 | 
			
		||||
        IRole role = null)
 | 
			
		||||
    {
 | 
			
		||||
        // these 3 don't make sense with time
 | 
			
		||||
        if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles
 | 
			
		||||
            && time is not null)
 | 
			
		||||
            return false;
 | 
			
		||||
        if (number <= 0 || (time is not null && time.Time > TimeSpan.FromDays(49)))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (punish is PunishmentAction.AddRole && role is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (punish is PunishmentAction.TimeOut && time is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
 | 
			
		||||
        var toDelete = ps.Where(x => x.Count == number);
 | 
			
		||||
 | 
			
		||||
        uow.RemoveRange(toDelete);
 | 
			
		||||
 | 
			
		||||
        ps.Add(new()
 | 
			
		||||
        {
 | 
			
		||||
            Count = number,
 | 
			
		||||
            Punishment = punish,
 | 
			
		||||
            Time = (int?)time?.Time.TotalMinutes ?? 0,
 | 
			
		||||
            RoleId = punish == PunishmentAction.AddRole ? role!.Id : default(ulong?)
 | 
			
		||||
        });
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool WarnPunishRemove(ulong guildId, int number)
 | 
			
		||||
    {
 | 
			
		||||
        if (number <= 0)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
 | 
			
		||||
        var p = ps.FirstOrDefault(x => x.Count == number);
 | 
			
		||||
 | 
			
		||||
        if (p is not null)
 | 
			
		||||
        {
 | 
			
		||||
            uow.Remove(p);
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public WarningPunishment[] WarnPunishList(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments))
 | 
			
		||||
                  .WarnPunishments.OrderBy(x => x.Count)
 | 
			
		||||
                  .ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(
 | 
			
		||||
        SocketGuild guild,
 | 
			
		||||
        string people)
 | 
			
		||||
    {
 | 
			
		||||
        var gusers = guild.Users;
 | 
			
		||||
        //get user objects and reasons
 | 
			
		||||
        var bans = people.Split("\n")
 | 
			
		||||
                         .Select(x =>
 | 
			
		||||
                         {
 | 
			
		||||
                             var split = x.Trim().Split(" ");
 | 
			
		||||
 | 
			
		||||
                             var reason = string.Join(" ", split.Skip(1));
 | 
			
		||||
 | 
			
		||||
                             if (ulong.TryParse(split[0], out var id))
 | 
			
		||||
                                 return (Original: split[0], Id: id, Reason: reason);
 | 
			
		||||
 | 
			
		||||
                             return (Original: split[0],
 | 
			
		||||
                                 gusers.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)?.Id,
 | 
			
		||||
                                 Reason: reason);
 | 
			
		||||
                         })
 | 
			
		||||
                         .ToArray();
 | 
			
		||||
 | 
			
		||||
        //if user is null, means that person couldn't be found
 | 
			
		||||
        var missing = bans.Count(x => !x.Id.HasValue);
 | 
			
		||||
 | 
			
		||||
        //get only data for found users
 | 
			
		||||
        var found = bans.Where(x => x.Id.HasValue).Select(x => x.Id.Value).ToList();
 | 
			
		||||
 | 
			
		||||
        _blacklistService.BlacklistUsers(found);
 | 
			
		||||
 | 
			
		||||
        return (bans, missing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string GetBanTemplate(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var template = uow.BanTemplates.AsQueryable().FirstOrDefault(x => x.GuildId == guildId);
 | 
			
		||||
        return template?.Text;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void SetBanTemplate(ulong guildId, string text)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var template = uow.BanTemplates.AsQueryable().FirstOrDefault(x => x.GuildId == guildId);
 | 
			
		||||
 | 
			
		||||
        if (text is null)
 | 
			
		||||
        {
 | 
			
		||||
            if (template is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            uow.Remove(template);
 | 
			
		||||
        }
 | 
			
		||||
        else if (template is null)
 | 
			
		||||
        {
 | 
			
		||||
            uow.BanTemplates.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                Text = text
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
            template.Text = text;
 | 
			
		||||
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetBanPruneAsync(ulong guildId, int? pruneDays)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        await ctx.BanTemplates
 | 
			
		||||
            .ToLinqToDBTable()
 | 
			
		||||
            .InsertOrUpdateAsync(() => new()
 | 
			
		||||
                {
 | 
			
		||||
                    GuildId = guildId,
 | 
			
		||||
                    Text = null,
 | 
			
		||||
                    DateAdded = DateTime.UtcNow,
 | 
			
		||||
                    PruneDays = pruneDays
 | 
			
		||||
                },
 | 
			
		||||
                old => new()
 | 
			
		||||
                {
 | 
			
		||||
                    PruneDays = pruneDays
 | 
			
		||||
                },
 | 
			
		||||
                () => new()
 | 
			
		||||
                {
 | 
			
		||||
                    GuildId = guildId
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<int?> GetBanPruneAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        return await ctx.BanTemplates
 | 
			
		||||
            .Where(x => x.GuildId == guildId)
 | 
			
		||||
            .Select(x => x.PruneDays)
 | 
			
		||||
            .FirstOrDefaultAsyncLinqToDB();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public SmartText GetBanUserDmEmbed(
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        IGuildUser target,
 | 
			
		||||
        string defaultMessage,
 | 
			
		||||
        string banReason,
 | 
			
		||||
        TimeSpan? duration)
 | 
			
		||||
        => GetBanUserDmEmbed((DiscordSocketClient)context.Client,
 | 
			
		||||
            (SocketGuild)context.Guild,
 | 
			
		||||
            (IGuildUser)context.User,
 | 
			
		||||
            target,
 | 
			
		||||
            defaultMessage,
 | 
			
		||||
            banReason,
 | 
			
		||||
            duration);
 | 
			
		||||
 | 
			
		||||
    public SmartText GetBanUserDmEmbed(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        SocketGuild guild,
 | 
			
		||||
        IGuildUser moderator,
 | 
			
		||||
        IGuildUser target,
 | 
			
		||||
        string defaultMessage,
 | 
			
		||||
        string banReason,
 | 
			
		||||
        TimeSpan? duration)
 | 
			
		||||
    {
 | 
			
		||||
        var template = GetBanTemplate(guild.Id);
 | 
			
		||||
 | 
			
		||||
        banReason = string.IsNullOrWhiteSpace(banReason) ? "-" : banReason;
 | 
			
		||||
 | 
			
		||||
        var replacer = new ReplacementBuilder().WithServer(client, guild)
 | 
			
		||||
                                               .WithOverride("%ban.mod%", () => moderator.ToString())
 | 
			
		||||
                                               .WithOverride("%ban.mod.fullname%", () => moderator.ToString())
 | 
			
		||||
                                               .WithOverride("%ban.mod.name%", () => moderator.Username)
 | 
			
		||||
                                               .WithOverride("%ban.mod.discrim%", () => moderator.Discriminator)
 | 
			
		||||
                                               .WithOverride("%ban.user%", () => target.ToString())
 | 
			
		||||
                                               .WithOverride("%ban.user.fullname%", () => target.ToString())
 | 
			
		||||
                                               .WithOverride("%ban.user.name%", () => target.Username)
 | 
			
		||||
                                               .WithOverride("%ban.user.discrim%", () => target.Discriminator)
 | 
			
		||||
                                               .WithOverride("%reason%", () => banReason)
 | 
			
		||||
                                               .WithOverride("%ban.reason%", () => banReason)
 | 
			
		||||
                                               .WithOverride("%ban.duration%",
 | 
			
		||||
                                                   () => duration?.ToString(@"d\.hh\:mm") ?? "perma")
 | 
			
		||||
                                               .Build();
 | 
			
		||||
 | 
			
		||||
        // if template isn't set, use the old message style
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(template))
 | 
			
		||||
        {
 | 
			
		||||
            template = JsonConvert.SerializeObject(new
 | 
			
		||||
            {
 | 
			
		||||
                color = _bcs.Data.Color.Error.PackedValue >> 8,
 | 
			
		||||
                description = defaultMessage
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        // if template is set to "-" do not dm the user
 | 
			
		||||
        else if (template == "-")
 | 
			
		||||
            return default;
 | 
			
		||||
        // if template is an embed, send that embed with replacements
 | 
			
		||||
        // otherwise, treat template as a regular string with replacements
 | 
			
		||||
        else if (SmartText.CreateFrom(template) is not { IsEmbed: true } or { IsEmbedArray: true })
 | 
			
		||||
        {
 | 
			
		||||
            template = JsonConvert.SerializeObject(new
 | 
			
		||||
            {
 | 
			
		||||
                color = _bcs.Data.Color.Error.PackedValue >> 8,
 | 
			
		||||
                description = template
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var output = SmartText.CreateFrom(template);
 | 
			
		||||
        return replacer.Replace(output);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,77 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration;
 | 
			
		||||
 | 
			
		||||
public partial class Administration
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class VcRoleCommands : NadekoModule<VcRoleService>
 | 
			
		||||
    {
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task VcRoleRm(ulong vcId)
 | 
			
		||||
        {
 | 
			
		||||
            if (_service.RemoveVcRole(ctx.Guild.Id, vcId))
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vcId.ToString())));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.vcrole_not_found);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task VcRole([Leftover] IRole role = null)
 | 
			
		||||
        {
 | 
			
		||||
            var user = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
            var vc = user.VoiceChannel;
 | 
			
		||||
 | 
			
		||||
            if (vc is null || vc.GuildId != user.GuildId)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (role is null)
 | 
			
		||||
            {
 | 
			
		||||
                if (_service.RemoveVcRole(ctx.Guild.Id, vc.Id))
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vc.Name)));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _service.AddVcRole(ctx.Guild.Id, role, vc.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.vcrole_added(Format.Bold(vc.Name), Format.Bold(role.Name)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task VcRoleList()
 | 
			
		||||
        {
 | 
			
		||||
            var guild = (SocketGuild)ctx.Guild;
 | 
			
		||||
            string text;
 | 
			
		||||
            if (_service.VcRoles.TryGetValue(ctx.Guild.Id, out var roles))
 | 
			
		||||
            {
 | 
			
		||||
                if (!roles.Any())
 | 
			
		||||
                    text = GetText(strs.no_vcroles);
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    text = string.Join("\n",
 | 
			
		||||
                        roles.Select(x
 | 
			
		||||
                            => $"{Format.Bold(guild.GetVoiceChannel(x.Key)?.Name ?? x.Key.ToString())} => {x.Value}"));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
                text = GetText(strs.no_vcroles);
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                            .WithOkColor()
 | 
			
		||||
                                            .WithTitle(GetText(strs.vc_role_list))
 | 
			
		||||
                                            .WithDescription(text));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,210 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class VcRoleService : INService
 | 
			
		||||
{
 | 
			
		||||
    public ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IRole>> VcRoles { get; }
 | 
			
		||||
    public ConcurrentDictionary<ulong, System.Collections.Concurrent.ConcurrentQueue<(bool, IGuildUser, IRole)>> ToAssign { get; }
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
    public VcRoleService(DiscordSocketClient client, Bot bot, DbService db)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _client = client;
 | 
			
		||||
 | 
			
		||||
        _client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated;
 | 
			
		||||
        VcRoles = new();
 | 
			
		||||
        ToAssign = new();
 | 
			
		||||
 | 
			
		||||
        using (var uow = db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var guildIds = client.Guilds.Select(x => x.Id).ToList();
 | 
			
		||||
            uow.Set<GuildConfig>()
 | 
			
		||||
               .AsQueryable()
 | 
			
		||||
               .Include(x => x.VcRoleInfos)
 | 
			
		||||
               .Where(x => guildIds.Contains(x.GuildId))
 | 
			
		||||
               .AsEnumerable()
 | 
			
		||||
               .Select(InitializeVcRole)
 | 
			
		||||
               .WhenAll();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            while (true)
 | 
			
		||||
            {
 | 
			
		||||
                Task Selector(System.Collections.Concurrent.ConcurrentQueue<(bool, IGuildUser, IRole)> queue)
 | 
			
		||||
                {
 | 
			
		||||
                    return Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        while (queue.TryDequeue(out var item))
 | 
			
		||||
                        {
 | 
			
		||||
                            var (add, user, role) = item;
 | 
			
		||||
 | 
			
		||||
                            try
 | 
			
		||||
                            {
 | 
			
		||||
                                if (add)
 | 
			
		||||
                                {
 | 
			
		||||
                                    if (!user.RoleIds.Contains(role.Id))
 | 
			
		||||
                                        await user.AddRoleAsync(role);
 | 
			
		||||
                                }
 | 
			
		||||
                                else
 | 
			
		||||
                                {
 | 
			
		||||
                                    if (user.RoleIds.Contains(role.Id))
 | 
			
		||||
                                        await user.RemoveRoleAsync(role);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            catch
 | 
			
		||||
                            {
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            await Task.Delay(250);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ToAssign.Values.Select(Selector).Append(Task.Delay(1000)).WhenAll();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        _client.LeftGuild += _client_LeftGuild;
 | 
			
		||||
        bot.JoinedGuild += Bot_JoinedGuild;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task Bot_JoinedGuild(GuildConfig arg)
 | 
			
		||||
    {
 | 
			
		||||
        // includeall no longer loads vcrole
 | 
			
		||||
        // need to load new guildconfig with vc role included 
 | 
			
		||||
        using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var configWithVcRole = uow.GuildConfigsForId(arg.GuildId, set => set.Include(x => x.VcRoleInfos));
 | 
			
		||||
            _ = InitializeVcRole(configWithVcRole);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task _client_LeftGuild(SocketGuild arg)
 | 
			
		||||
    {
 | 
			
		||||
        VcRoles.TryRemove(arg.Id, out _);
 | 
			
		||||
        ToAssign.TryRemove(arg.Id, out _);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task InitializeVcRole(GuildConfig gconf)
 | 
			
		||||
    {
 | 
			
		||||
        var g = _client.GetGuild(gconf.GuildId);
 | 
			
		||||
        if (g is null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var infos = new ConcurrentDictionary<ulong, IRole>();
 | 
			
		||||
        var missingRoles = new List<VcRoleInfo>();
 | 
			
		||||
        VcRoles.AddOrUpdate(gconf.GuildId, infos, delegate { return infos; });
 | 
			
		||||
        foreach (var ri in gconf.VcRoleInfos)
 | 
			
		||||
        {
 | 
			
		||||
            var role = g.GetRole(ri.RoleId);
 | 
			
		||||
            if (role is null)
 | 
			
		||||
            {
 | 
			
		||||
                missingRoles.Add(ri);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            infos.TryAdd(ri.VoiceChannelId, role);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (missingRoles.Any())
 | 
			
		||||
        {
 | 
			
		||||
            await using var uow = _db.GetDbContext();
 | 
			
		||||
            uow.RemoveRange(missingRoles);
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            
 | 
			
		||||
            Log.Warning("Removed {MissingRoleCount} missing roles from {ServiceName}",
 | 
			
		||||
                missingRoles.Count,
 | 
			
		||||
                nameof(VcRoleService));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void AddVcRole(ulong guildId, IRole role, ulong vcId)
 | 
			
		||||
    {
 | 
			
		||||
        if (role is null)
 | 
			
		||||
            throw new ArgumentNullException(nameof(role));
 | 
			
		||||
 | 
			
		||||
        var guildVcRoles = VcRoles.GetOrAdd(guildId, new ConcurrentDictionary<ulong, IRole>());
 | 
			
		||||
 | 
			
		||||
        guildVcRoles.AddOrUpdate(vcId, role, (_, _) => role);
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos));
 | 
			
		||||
        var toDelete = conf.VcRoleInfos.FirstOrDefault(x => x.VoiceChannelId == vcId); // remove old one
 | 
			
		||||
        if (toDelete is not null)
 | 
			
		||||
            uow.Remove(toDelete);
 | 
			
		||||
        conf.VcRoleInfos.Add(new()
 | 
			
		||||
        {
 | 
			
		||||
            VoiceChannelId = vcId,
 | 
			
		||||
            RoleId = role.Id
 | 
			
		||||
        }); // add new one
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool RemoveVcRole(ulong guildId, ulong vcId)
 | 
			
		||||
    {
 | 
			
		||||
        if (!VcRoles.TryGetValue(guildId, out var guildVcRoles))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (!guildVcRoles.TryRemove(vcId, out _))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos));
 | 
			
		||||
        var toRemove = conf.VcRoleInfos.Where(x => x.VoiceChannelId == vcId).ToList();
 | 
			
		||||
        uow.RemoveRange(toRemove);
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task ClientOnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState)
 | 
			
		||||
    {
 | 
			
		||||
        if (usr is not SocketGuildUser gusr)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        var oldVc = oldState.VoiceChannel;
 | 
			
		||||
        var newVc = newState.VoiceChannel;
 | 
			
		||||
        _ = Task.Run(() =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (oldVc != newVc)
 | 
			
		||||
                {
 | 
			
		||||
                    ulong guildId;
 | 
			
		||||
                    guildId = newVc?.Guild.Id ?? oldVc.Guild.Id;
 | 
			
		||||
 | 
			
		||||
                    if (VcRoles.TryGetValue(guildId, out var guildVcRoles))
 | 
			
		||||
                    {
 | 
			
		||||
                        //remove old
 | 
			
		||||
                        if (oldVc is not null && guildVcRoles.TryGetValue(oldVc.Id, out var role))
 | 
			
		||||
                            Assign(false, gusr, role);
 | 
			
		||||
                        //add new
 | 
			
		||||
                        if (newVc is not null && guildVcRoles.TryGetValue(newVc.Id, out role))
 | 
			
		||||
                            Assign(true, gusr, role);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error in VcRoleService VoiceStateUpdate");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void Assign(bool v, SocketGuildUser gusr, IRole role)
 | 
			
		||||
    {
 | 
			
		||||
        var queue = ToAssign.GetOrAdd(gusr.Guild.Id, new System.Collections.Concurrent.ConcurrentQueue<(bool, IGuildUser, IRole)>());
 | 
			
		||||
        queue.Enqueue((v, gusr, role));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Help;
 | 
			
		||||
 | 
			
		||||
internal class CommandJsonObject
 | 
			
		||||
{
 | 
			
		||||
    public string[] Aliases { get; set; }
 | 
			
		||||
    public string Description { get; set; }
 | 
			
		||||
    public string[] Usage { get; set; }
 | 
			
		||||
    public string Submodule { get; set; }
 | 
			
		||||
    public string Module { get; set; }
 | 
			
		||||
    public List<string> Options { get; set; }
 | 
			
		||||
    public string[] Requirements { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using CommandLine;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Help.Common;
 | 
			
		||||
 | 
			
		||||
public class CommandsOptions : INadekoCommandOptions
 | 
			
		||||
{
 | 
			
		||||
    public enum ViewType
 | 
			
		||||
    {
 | 
			
		||||
        Hide,
 | 
			
		||||
        Cross,
 | 
			
		||||
        All
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Option('v',
 | 
			
		||||
        "view",
 | 
			
		||||
        Required = false,
 | 
			
		||||
        Default = ViewType.Hide,
 | 
			
		||||
        HelpText =
 | 
			
		||||
            "Specifies how to output the list of commands. 0 - Hide commands which you can't use, 1 - Cross out commands which you can't use, 2 - Show all.")]
 | 
			
		||||
    public ViewType View { get; set; } = ViewType.Hide;
 | 
			
		||||
 | 
			
		||||
    public void NormalizeOptions()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,565 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Amazon.S3;
 | 
			
		||||
using Nadeko.Medusa;
 | 
			
		||||
using NadekoBot.Modules.Help.Common;
 | 
			
		||||
using NadekoBot.Modules.Help.Services;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using JsonSerializer = System.Text.Json.JsonSerializer;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Help;
 | 
			
		||||
 | 
			
		||||
public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
{
 | 
			
		||||
    public const string PATREON_URL = "https://patreon.com/nadekobot";
 | 
			
		||||
    public const string PAYPAL_URL = "https://paypal.me/Kwoth";
 | 
			
		||||
 | 
			
		||||
    private readonly CommandService _cmds;
 | 
			
		||||
    private readonly BotConfigService _bss;
 | 
			
		||||
    private readonly GlobalPermissionService _perms;
 | 
			
		||||
    private readonly IServiceProvider _services;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly IBotStrings _strings;
 | 
			
		||||
 | 
			
		||||
    private readonly AsyncLazy<ulong> _lazyClientId;
 | 
			
		||||
    private readonly IMedusaLoaderService _medusae;
 | 
			
		||||
 | 
			
		||||
    public Help(
 | 
			
		||||
        GlobalPermissionService perms,
 | 
			
		||||
        CommandService cmds,
 | 
			
		||||
        BotConfigService bss,
 | 
			
		||||
        IServiceProvider services,
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        IMedusaLoaderService medusae)
 | 
			
		||||
    {
 | 
			
		||||
        _cmds = cmds;
 | 
			
		||||
        _bss = bss;
 | 
			
		||||
        _perms = perms;
 | 
			
		||||
        _services = services;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _medusae = medusae;
 | 
			
		||||
 | 
			
		||||
        _lazyClientId = new(async () => (await _client.GetApplicationInfoAsync()).Id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<SmartText> GetHelpString()
 | 
			
		||||
    {
 | 
			
		||||
        var botSettings = _bss.Data;
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(botSettings.HelpText) || botSettings.HelpText == "-")
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        var clientId = await _lazyClientId.Value;
 | 
			
		||||
        var r = new ReplacementBuilder().WithDefault(Context)
 | 
			
		||||
                                        .WithOverride("{0}", () => clientId.ToString())
 | 
			
		||||
                                        .WithOverride("{1}", () => prefix)
 | 
			
		||||
                                        .WithOverride("%prefix%", () => prefix)
 | 
			
		||||
                                        .WithOverride("%bot.prefix%", () => prefix)
 | 
			
		||||
                                        .Build();
 | 
			
		||||
 | 
			
		||||
        var text = SmartText.CreateFrom(botSettings.HelpText);
 | 
			
		||||
        return r.Replace(text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async Task Modules(int page = 1)
 | 
			
		||||
    {
 | 
			
		||||
        if (--page < 0)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var topLevelModules = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
 | 
			
		||||
                                   .Where(m => !_perms.BlockedModules.Contains(m.Key.Name.ToLowerInvariant()))
 | 
			
		||||
                                   .Select(x => x.Key)
 | 
			
		||||
                                   .ToList();
 | 
			
		||||
 | 
			
		||||
        await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
            cur =>
 | 
			
		||||
            {
 | 
			
		||||
                var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.list_of_modules));
 | 
			
		||||
 | 
			
		||||
                var localModules = topLevelModules.Skip(12 * cur).Take(12).ToList();
 | 
			
		||||
 | 
			
		||||
                if (!localModules.Any())
 | 
			
		||||
                {
 | 
			
		||||
                    embed = embed.WithOkColor().WithDescription(GetText(strs.module_page_empty));
 | 
			
		||||
                    return embed;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                localModules.OrderBy(module => module.Name)
 | 
			
		||||
                            .ToList()
 | 
			
		||||
                            .ForEach(module => embed.AddField($"{GetModuleEmoji(module.Name)} {module.Name}",
 | 
			
		||||
                                GetModuleDescription(module.Name)
 | 
			
		||||
                                + "\n"
 | 
			
		||||
                                + Format.Code(GetText(strs.module_footer(prefix, module.Name.ToLowerInvariant()))),
 | 
			
		||||
                                true));
 | 
			
		||||
 | 
			
		||||
                return embed;
 | 
			
		||||
            },
 | 
			
		||||
            topLevelModules.Count(),
 | 
			
		||||
            12,
 | 
			
		||||
            false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string GetModuleDescription(string moduleName)
 | 
			
		||||
    {
 | 
			
		||||
        var key = GetModuleLocStr(moduleName);
 | 
			
		||||
 | 
			
		||||
        if (key.Key == strs.module_description_missing.Key)
 | 
			
		||||
        {
 | 
			
		||||
            var desc = _medusae
 | 
			
		||||
                       .GetLoadedMedusae(Culture)
 | 
			
		||||
                       .FirstOrDefault(m => m.Sneks
 | 
			
		||||
                                             .Any(x => x.Name.Equals(moduleName,
 | 
			
		||||
                                                 StringComparison.InvariantCultureIgnoreCase)))
 | 
			
		||||
                       ?.Description;
 | 
			
		||||
 | 
			
		||||
            if (desc is not null)
 | 
			
		||||
                return desc;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return GetText(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private LocStr GetModuleLocStr(string moduleName)
 | 
			
		||||
    {
 | 
			
		||||
        switch (moduleName.ToLowerInvariant())
 | 
			
		||||
        {
 | 
			
		||||
            case "help":
 | 
			
		||||
                return strs.module_description_help;
 | 
			
		||||
            case "administration":
 | 
			
		||||
                return strs.module_description_administration;
 | 
			
		||||
            case "expressions":
 | 
			
		||||
                return strs.module_description_expressions;
 | 
			
		||||
            case "searches":
 | 
			
		||||
                return strs.module_description_searches;
 | 
			
		||||
            case "utility":
 | 
			
		||||
                return strs.module_description_utility;
 | 
			
		||||
            case "games":
 | 
			
		||||
                return strs.module_description_games;
 | 
			
		||||
            case "gambling":
 | 
			
		||||
                return strs.module_description_gambling;
 | 
			
		||||
            case "music":
 | 
			
		||||
                return strs.module_description_music;
 | 
			
		||||
            case "nsfw":
 | 
			
		||||
                return strs.module_description_nsfw;
 | 
			
		||||
            case "permissions":
 | 
			
		||||
                return strs.module_description_permissions;
 | 
			
		||||
            case "xp":
 | 
			
		||||
                return strs.module_description_xp;
 | 
			
		||||
            case "medusa":
 | 
			
		||||
                return strs.module_description_medusa;
 | 
			
		||||
            default:
 | 
			
		||||
                return strs.module_description_missing;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string GetModuleEmoji(string moduleName)
 | 
			
		||||
    {
 | 
			
		||||
        moduleName = moduleName.ToLowerInvariant();
 | 
			
		||||
        switch (moduleName)
 | 
			
		||||
        {
 | 
			
		||||
            case "help":
 | 
			
		||||
                return "❓";
 | 
			
		||||
            case "administration":
 | 
			
		||||
                return "🛠️";
 | 
			
		||||
            case "expressions":
 | 
			
		||||
                return "🗣️";
 | 
			
		||||
            case "searches":
 | 
			
		||||
                return "🔍";
 | 
			
		||||
            case "utility":
 | 
			
		||||
                return "🔧";
 | 
			
		||||
            case "games":
 | 
			
		||||
                return "🎲";
 | 
			
		||||
            case "gambling":
 | 
			
		||||
                return "💰";
 | 
			
		||||
            case "music":
 | 
			
		||||
                return "🎶";
 | 
			
		||||
            case "nsfw":
 | 
			
		||||
                return "😳";
 | 
			
		||||
            case "permissions":
 | 
			
		||||
                return "🚓";
 | 
			
		||||
            case "xp":
 | 
			
		||||
                return "📝";
 | 
			
		||||
            default:
 | 
			
		||||
                return "📖";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [NadekoOptions<CommandsOptions>]
 | 
			
		||||
    public async Task Commands(string module = null, params string[] args)
 | 
			
		||||
    {
 | 
			
		||||
        module = module?.Trim().ToUpperInvariant();
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(module))
 | 
			
		||||
        {
 | 
			
		||||
            await Modules();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var (opts, _) = OptionsParser.ParseFrom(new CommandsOptions(), args);
 | 
			
		||||
 | 
			
		||||
        // Find commands for that module
 | 
			
		||||
        // don't show commands which are blocked
 | 
			
		||||
        // order by name
 | 
			
		||||
        var cmds = _cmds.Commands
 | 
			
		||||
                        .Where(c => c.Module.GetTopLevelModule()
 | 
			
		||||
                                     .Name.ToUpperInvariant()
 | 
			
		||||
                                     .StartsWith(module, StringComparison.InvariantCulture))
 | 
			
		||||
                        .Where(c => !_perms.BlockedCommands.Contains(c.Aliases[0].ToLowerInvariant()))
 | 
			
		||||
                        .OrderBy(c => c.Aliases[0])
 | 
			
		||||
                        .DistinctBy(x => x.Aliases[0])
 | 
			
		||||
                        .ToList();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // check preconditions for all commands, but only if it's not 'all'
 | 
			
		||||
        // because all will show all commands anyway, no need to check
 | 
			
		||||
        var succ = new HashSet<CommandInfo>();
 | 
			
		||||
        if (opts.View != CommandsOptions.ViewType.All)
 | 
			
		||||
        {
 | 
			
		||||
            succ = new((await cmds.Select(async x =>
 | 
			
		||||
                                  {
 | 
			
		||||
                                      var pre = await x.CheckPreconditionsAsync(Context, _services);
 | 
			
		||||
                                      return (Cmd: x, Succ: pre.IsSuccess);
 | 
			
		||||
                                  })
 | 
			
		||||
                                  .WhenAll()).Where(x => x.Succ)
 | 
			
		||||
                                             .Select(x => x.Cmd));
 | 
			
		||||
 | 
			
		||||
            if (opts.View == CommandsOptions.ViewType.Hide)
 | 
			
		||||
                // if hidden is specified, completely remove these commands from the list
 | 
			
		||||
                cmds = cmds.Where(x => succ.Contains(x)).ToList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var cmdsWithGroup = cmds.GroupBy(c => c.Module.GetGroupName())
 | 
			
		||||
                                .OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count())
 | 
			
		||||
                                .ToList();
 | 
			
		||||
 | 
			
		||||
        if (cmdsWithGroup.Count == 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (opts.View != CommandsOptions.ViewType.Hide)
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.module_not_found);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.module_not_found_or_cant_exec);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var cnt = 0;
 | 
			
		||||
        var groups = cmdsWithGroup.GroupBy(_ => cnt++ / 48).ToArray();
 | 
			
		||||
        var embed = _eb.Create().WithOkColor();
 | 
			
		||||
        foreach (var g in groups)
 | 
			
		||||
        {
 | 
			
		||||
            var last = g.Count();
 | 
			
		||||
            for (var i = 0; i < last; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var transformed = g.ElementAt(i)
 | 
			
		||||
                                   .Select(x =>
 | 
			
		||||
                                   {
 | 
			
		||||
                                       //if cross is specified, and the command doesn't satisfy the requirements, cross it out
 | 
			
		||||
                                       if (opts.View == CommandsOptions.ViewType.Cross)
 | 
			
		||||
                                       {
 | 
			
		||||
                                           return
 | 
			
		||||
                                               $"{(succ.Contains(x) ? "✅" : "❌")}{prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}";
 | 
			
		||||
                                       }
 | 
			
		||||
 | 
			
		||||
                                       return
 | 
			
		||||
                                           $"{prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}";
 | 
			
		||||
                                   });
 | 
			
		||||
 | 
			
		||||
                if (i == last - 1 && (i + 1) % 2 != 0)
 | 
			
		||||
                {
 | 
			
		||||
                    transformed = transformed.Chunk(2)
 | 
			
		||||
                                             .Select(x =>
 | 
			
		||||
                                             {
 | 
			
		||||
                                                 if (x.Count() == 1)
 | 
			
		||||
                                                     return $"{x.First()}";
 | 
			
		||||
                                                 return string.Concat(x);
 | 
			
		||||
                                             });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                embed.AddField(g.ElementAt(i).Key, "```css\n" + string.Join("\n", transformed) + "\n```", true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        embed.WithFooter(GetText(strs.commands_instr(prefix)));
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task Group(ModuleInfo group)
 | 
			
		||||
    {
 | 
			
		||||
        var eb = _eb.Create(ctx)
 | 
			
		||||
                    .WithTitle(GetText(strs.cmd_group_commands(group.Name)))
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
 | 
			
		||||
        foreach (var cmd in group.Commands)
 | 
			
		||||
        {
 | 
			
		||||
            eb.AddField(prefix + cmd.Aliases.First(), cmd.RealSummary(_strings, _medusae, Culture, prefix));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(eb);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [Priority(0)]
 | 
			
		||||
    public async Task H([Leftover] string fail)
 | 
			
		||||
    {
 | 
			
		||||
        var prefixless =
 | 
			
		||||
            _cmds.Commands.FirstOrDefault(x => x.Aliases.Any(cmdName => cmdName.ToLowerInvariant() == fail));
 | 
			
		||||
        if (prefixless is not null)
 | 
			
		||||
        {
 | 
			
		||||
            await H(prefixless);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (fail.StartsWith(prefix))
 | 
			
		||||
            fail = fail.Substring(prefix.Length);
 | 
			
		||||
 | 
			
		||||
        var group = _cmds.Modules
 | 
			
		||||
                         .SelectMany(x => x.Submodules)
 | 
			
		||||
                         .Where(x => !string.IsNullOrWhiteSpace(x.Group))
 | 
			
		||||
                         .FirstOrDefault(x => x.Group.Equals(fail, StringComparison.InvariantCultureIgnoreCase));
 | 
			
		||||
 | 
			
		||||
        if (group is not null)
 | 
			
		||||
        {
 | 
			
		||||
            await Group(group);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ReplyErrorLocalizedAsync(strs.command_not_found);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
    public async Task H([Leftover] CommandInfo com = null)
 | 
			
		||||
    {
 | 
			
		||||
        var channel = ctx.Channel;
 | 
			
		||||
 | 
			
		||||
        if (com is null)
 | 
			
		||||
        {
 | 
			
		||||
            var ch = channel is ITextChannel ? await ctx.User.CreateDMChannelAsync() : channel;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var data = await GetHelpString();
 | 
			
		||||
                if (data == default)
 | 
			
		||||
                    return;
 | 
			
		||||
                await ch.SendAsync(data);
 | 
			
		||||
                try { await ctx.OkAsync(); }
 | 
			
		||||
                catch { } // ignore if bot can't react
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.cant_dm);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var embed = _service.GetCommandHelp(com, ctx.Guild);
 | 
			
		||||
        await channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [OwnerOnly]
 | 
			
		||||
    public async Task GenCmdList()
 | 
			
		||||
    {
 | 
			
		||||
        _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
        // order commands by top level module name
 | 
			
		||||
        // and make a dictionary of <ModuleName, Array<JsonCommandData>>
 | 
			
		||||
        var cmdData = _cmds.Commands.GroupBy(x => x.Module.GetTopLevelModule().Name)
 | 
			
		||||
                           .OrderBy(x => x.Key)
 | 
			
		||||
                           .ToDictionary(x => x.Key,
 | 
			
		||||
                               x => x.DistinctBy(c => c.Aliases.First())
 | 
			
		||||
                                     .Select(com =>
 | 
			
		||||
                                     {
 | 
			
		||||
                                         List<string> optHelpStr = null;
 | 
			
		||||
                                         
 | 
			
		||||
                                         var opt = HelpService.GetNadekoOptionType(com.Attributes);
 | 
			
		||||
                                         if (opt is not null)
 | 
			
		||||
                                             optHelpStr = HelpService.GetCommandOptionHelpList(opt);
 | 
			
		||||
 | 
			
		||||
                                         return new CommandJsonObject
 | 
			
		||||
                                         {
 | 
			
		||||
                                             Aliases = com.Aliases.Select(alias => prefix + alias).ToArray(),
 | 
			
		||||
                                             Description = com.RealSummary(_strings, _medusae, Culture, prefix),
 | 
			
		||||
                                             Usage = com.RealRemarksArr(_strings, _medusae, Culture, prefix),
 | 
			
		||||
                                             Submodule = com.Module.Name,
 | 
			
		||||
                                             Module = com.Module.GetTopLevelModule().Name,
 | 
			
		||||
                                             Options = optHelpStr,
 | 
			
		||||
                                             Requirements = HelpService.GetCommandRequirements(com)
 | 
			
		||||
                                         };
 | 
			
		||||
                                     })
 | 
			
		||||
                                     .ToList());
 | 
			
		||||
 | 
			
		||||
        var readableData = JsonConvert.SerializeObject(cmdData, Formatting.Indented);
 | 
			
		||||
        var uploadData = JsonConvert.SerializeObject(cmdData, Formatting.None);
 | 
			
		||||
 | 
			
		||||
        // for example https://nyc.digitaloceanspaces.com (without your space name)
 | 
			
		||||
        var serviceUrl = Environment.GetEnvironmentVariable("do_spaces_address");
 | 
			
		||||
 | 
			
		||||
        // generate spaces access key on https://cloud.digitalocean.com/account/api/tokens
 | 
			
		||||
        // you will get 2 keys, first, shorter one is id, longer one is secret
 | 
			
		||||
        var accessKey = Environment.GetEnvironmentVariable("do_access_key_id");
 | 
			
		||||
        var secretAcccessKey = Environment.GetEnvironmentVariable("do_access_key_secret");
 | 
			
		||||
 | 
			
		||||
        // if all env vars are set, upload the unindented file (to save space) there
 | 
			
		||||
        if (!(serviceUrl is null || accessKey is null || secretAcccessKey is null))
 | 
			
		||||
        {
 | 
			
		||||
            var config = new AmazonS3Config
 | 
			
		||||
            {
 | 
			
		||||
                ServiceURL = serviceUrl
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            using var dlClient = new AmazonS3Client(accessKey, secretAcccessKey, config);
 | 
			
		||||
 | 
			
		||||
            using (var client = new AmazonS3Client(accessKey, secretAcccessKey, config))
 | 
			
		||||
            {
 | 
			
		||||
                await client.PutObjectAsync(new()
 | 
			
		||||
                {
 | 
			
		||||
                    BucketName = "nadeko-pictures",
 | 
			
		||||
                    ContentType = "application/json",
 | 
			
		||||
                    ContentBody = uploadData,
 | 
			
		||||
                    // either use a path provided in the argument or the default one for public nadeko, other/cmds.json
 | 
			
		||||
                    Key = $"cmds/{StatsService.BOT_VERSION}.json",
 | 
			
		||||
                    CannedACL = S3CannedACL.PublicRead
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            var versionListString = "[]";
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using var oldVersionObject = await dlClient.GetObjectAsync(new()
 | 
			
		||||
                {
 | 
			
		||||
                    BucketName = "nadeko-pictures",
 | 
			
		||||
                    Key = "cmds/versions.json"
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                await using var ms = new MemoryStream();
 | 
			
		||||
                await oldVersionObject.ResponseStream.CopyToAsync(ms);
 | 
			
		||||
                versionListString = Encoding.UTF8.GetString(ms.ToArray());
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("No old version list found. Creating a new one");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var versionList = JsonSerializer.Deserialize<List<string>>(versionListString);
 | 
			
		||||
            if (versionList is not null && !versionList.Contains(StatsService.BOT_VERSION))
 | 
			
		||||
            {
 | 
			
		||||
                // save the file with new version added
 | 
			
		||||
                // versionList.Add(StatsService.BotVersion);
 | 
			
		||||
                versionListString = JsonSerializer.Serialize(versionList.Prepend(StatsService.BOT_VERSION),
 | 
			
		||||
                    new JsonSerializerOptions
 | 
			
		||||
                    {
 | 
			
		||||
                        WriteIndented = true
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                // upload the updated version list
 | 
			
		||||
                using var client = new AmazonS3Client(accessKey, secretAcccessKey, config);
 | 
			
		||||
                await client.PutObjectAsync(new()
 | 
			
		||||
                {
 | 
			
		||||
                    BucketName = "nadeko-pictures",
 | 
			
		||||
                    ContentType = "application/json",
 | 
			
		||||
                    ContentBody = versionListString,
 | 
			
		||||
                    // either use a path provided in the argument or the default one for public nadeko, other/cmds.json
 | 
			
		||||
                    Key = "cmds/versions.json",
 | 
			
		||||
                    CannedACL = S3CannedACL.PublicRead
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(
 | 
			
		||||
                    "Version {Version} already exists in the version file. " + "Did you forget to increment it?",
 | 
			
		||||
                    StatsService.BOT_VERSION);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // also send the file, but indented one, to chat
 | 
			
		||||
        await using var rDataStream = new MemoryStream(Encoding.ASCII.GetBytes(readableData));
 | 
			
		||||
        await ctx.Channel.SendFileAsync(rDataStream, "cmds.json", GetText(strs.commandlist_regen));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async Task Guide()
 | 
			
		||||
        => await ConfirmLocalizedAsync(strs.guide("https://nadeko.bot/commands",
 | 
			
		||||
            "https://nadekobot.readthedocs.io/en/latest/"));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private Task SelfhostAction(SocketMessageComponent smc, object _)
 | 
			
		||||
        => smc.RespondConfirmAsync(_eb,
 | 
			
		||||
            """
 | 
			
		||||
                - In case you don't want or cannot Donate to NadekoBot project, but you 
 | 
			
		||||
                - NadekoBot is a completely free and fully [open source](https://gitlab.com/kwoth/nadekobot) project which means you can run your own "selfhosted" instance on your computer or server for free.
 | 
			
		||||
                
 | 
			
		||||
                *Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer*
 | 
			
		||||
                
 | 
			
		||||
                - You can find the selfhosting guides by using the `.guide` command and clicking on the second link that pops up.
 | 
			
		||||
                - If you decide to selfhost the bot, still consider [supporting the project](https://patreon.com/join/nadekobot) to keep the development going :)
 | 
			
		||||
                """,
 | 
			
		||||
            true);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [OnlyPublicBot]
 | 
			
		||||
    public async Task Donate()
 | 
			
		||||
    {
 | 
			
		||||
        // => new NadekoInteractionData(new Emoji("🖥️"), "donate:selfhosting", "Selfhosting");
 | 
			
		||||
        var selfhostInter = _inter.Create(ctx.User.Id,
 | 
			
		||||
            new SimpleInteraction<object>(new ButtonBuilder(
 | 
			
		||||
                    emote: new Emoji("🖥️"),
 | 
			
		||||
                    customId: "donate:selfhosting",
 | 
			
		||||
                    label: "Selfhosting"),
 | 
			
		||||
                SelfhostAction));
 | 
			
		||||
 | 
			
		||||
        var eb = _eb.Create(ctx)
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("Thank you for considering to donate to the NadekoBot project!");
 | 
			
		||||
 | 
			
		||||
        eb
 | 
			
		||||
            .WithDescription("NadekoBot relies on donations to keep the servers, services and APIs running.\n"
 | 
			
		||||
                             + "Donating will give you access to some exclusive features. You can read about them on the [patreon page](https://patreon.com/join/nadekobot)")
 | 
			
		||||
            .AddField("Donation Instructions",
 | 
			
		||||
                $@"
 | 
			
		||||
🗒️ Before pledging it is recommended to open your DMs as Nadeko will send you a welcome message with instructions after you pledge has been processed and confirmed.
 | 
			
		||||
 | 
			
		||||
**Step 1:** ❤️ Pledge on Patreon ❤️
 | 
			
		||||
 | 
			
		||||
`1.` Go to <https://patreon.com/join/nadekobot> and choose a tier.
 | 
			
		||||
`2.` Make sure your payment is processed and accepted.
 | 
			
		||||
 | 
			
		||||
**Step 2** 🤝 Connect your Discord account 🤝
 | 
			
		||||
 | 
			
		||||
`1.` Go to your profile settings on Patreon and connect your Discord account to it.
 | 
			
		||||
*please make sure you're logged into the correct Discord account*
 | 
			
		||||
 | 
			
		||||
If you do not know how to do it, you may follow instructions in this link:
 | 
			
		||||
<https://support.patreon.com/hc/en-us/articles/212052266-How-do-I-connect-Discord-to-Patreon-Patron->
 | 
			
		||||
 | 
			
		||||
**Step 3** ⏰ Wait a short while (usually 1-3 minutes) ⏰
 | 
			
		||||
  
 | 
			
		||||
Nadeko will DM you the welcome instructions, and you may start using the patron-only commands and features!
 | 
			
		||||
🎉 **Enjoy!** 🎉
 | 
			
		||||
")
 | 
			
		||||
            .AddField("Troubleshooting",
 | 
			
		||||
                """
 | 
			
		||||
                    
 | 
			
		||||
                    *In case you didn't receive the rewards within 5 minutes:*
 | 
			
		||||
                    `1.` Make sure your DMs are open to everyone. Maybe your pledge was processed successfully but the bot was unable to DM you. Use the `.patron` command to check your status.
 | 
			
		||||
                    `2.` Make sure you've connected the CORRECT Discord account. Quite often users log in to different Discord accounts in their browser. You may also try disconnecting and reconnecting your account.
 | 
			
		||||
                    `3.` Make sure your payment has been processed and not declined by Patreon.
 | 
			
		||||
                    `4.` If any of the previous steps don't help, you can join the nadeko support server <https://discord.nadeko.bot> and ask for help in the #help channel
 | 
			
		||||
                    """);
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await (await ctx.User.CreateDMChannelAsync()).EmbedAsync(eb, inter: selfhostInter);
 | 
			
		||||
            _ = ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.cant_dm);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,203 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using Nadeko.Medusa;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Help.Services;
 | 
			
		||||
 | 
			
		||||
public class HelpService : IExecNoCommand, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly CommandHandler _ch;
 | 
			
		||||
    private readonly IBotStrings _strings;
 | 
			
		||||
    private readonly DiscordPermOverrideService _dpos;
 | 
			
		||||
    private readonly BotConfigService _bss;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
    private readonly ILocalization _loc;
 | 
			
		||||
    private readonly IMedusaLoaderService _medusae;
 | 
			
		||||
 | 
			
		||||
    public HelpService(
 | 
			
		||||
        CommandHandler ch,
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        DiscordPermOverrideService dpos,
 | 
			
		||||
        BotConfigService bss,
 | 
			
		||||
        IEmbedBuilderService eb,
 | 
			
		||||
        ILocalization loc,
 | 
			
		||||
        IMedusaLoaderService medusae)
 | 
			
		||||
    {
 | 
			
		||||
        _ch = ch;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _dpos = dpos;
 | 
			
		||||
        _bss = bss;
 | 
			
		||||
        _eb = eb;
 | 
			
		||||
        _loc = loc;
 | 
			
		||||
        _medusae = medusae;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        var settings = _bss.Data;
 | 
			
		||||
        if (guild is null)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(settings.DmHelpText) || settings.DmHelpText == "-")
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            // only send dm help text if it contains one of the keywords, if they're specified
 | 
			
		||||
            // if they're not, then reply to every DM
 | 
			
		||||
            if (settings.DmHelpTextKeywords is not null && !settings.DmHelpTextKeywords.Any(k => msg.Content.Contains(k)))
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            var rep = new ReplacementBuilder().WithOverride("%prefix%", () => _bss.Data.Prefix)
 | 
			
		||||
                                              .WithOverride("%bot.prefix%", () => _bss.Data.Prefix)
 | 
			
		||||
                                              .WithUser(msg.Author)
 | 
			
		||||
                                              .Build();
 | 
			
		||||
 | 
			
		||||
            var text = SmartText.CreateFrom(settings.DmHelpText);
 | 
			
		||||
            text = rep.Replace(text);
 | 
			
		||||
 | 
			
		||||
            return msg.Channel.SendAsync(text);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        var prefix = _ch.GetPrefix(guild);
 | 
			
		||||
        
 | 
			
		||||
        var str = $"**`{prefix + com.Aliases.First()}`**";
 | 
			
		||||
        var alias = com.Aliases.Skip(1).FirstOrDefault();
 | 
			
		||||
        if (alias is not null)
 | 
			
		||||
            str += $" **/ `{prefix + alias}`**";
 | 
			
		||||
 | 
			
		||||
        var culture = _loc.GetCultureInfo(guild);
 | 
			
		||||
        
 | 
			
		||||
        var em = _eb.Create()
 | 
			
		||||
                    .AddField(str, $"{com.RealSummary(_strings, _medusae, culture,  prefix)}", true);
 | 
			
		||||
 | 
			
		||||
        _dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides);
 | 
			
		||||
        var reqs = GetCommandRequirements(com, (GuildPermission?)overrides);
 | 
			
		||||
        if (reqs.Any())
 | 
			
		||||
            em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs));
 | 
			
		||||
 | 
			
		||||
        em.AddField(_strings.GetText(strs.usage),
 | 
			
		||||
              string.Join("\n", com.RealRemarksArr(_strings,_medusae, culture, prefix).Map(arg => Format.Code(arg))))
 | 
			
		||||
          .WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild))
 | 
			
		||||
          .WithOkColor();
 | 
			
		||||
 | 
			
		||||
        var opt = GetNadekoOptionType(com.Attributes);
 | 
			
		||||
        if (opt is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var hs = GetCommandOptionHelp(opt);
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(hs))
 | 
			
		||||
                em.AddField(GetText(strs.options, guild), hs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Type GetNadekoOptionType(IEnumerable<Attribute> attributes)
 | 
			
		||||
        => attributes
 | 
			
		||||
           .Select(a => a.GetType())
 | 
			
		||||
           .Where(a => a.IsGenericType
 | 
			
		||||
                       && a.GetGenericTypeDefinition() == typeof(NadekoOptionsAttribute<>))
 | 
			
		||||
           .Select(a => a.GenericTypeArguments[0])
 | 
			
		||||
           .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
    public static string GetCommandOptionHelp(Type opt)
 | 
			
		||||
    {
 | 
			
		||||
        var strs = GetCommandOptionHelpList(opt);
 | 
			
		||||
 | 
			
		||||
        return string.Join("\n", strs);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static List<string> GetCommandOptionHelpList(Type opt)
 | 
			
		||||
    {
 | 
			
		||||
        var strs = opt.GetProperties()
 | 
			
		||||
                      .Select(x => x.GetCustomAttributes(true).FirstOrDefault(a => a is OptionAttribute))
 | 
			
		||||
                      .Where(x => x is not null)
 | 
			
		||||
                      .Cast<OptionAttribute>()
 | 
			
		||||
                      .Select(x =>
 | 
			
		||||
                      {
 | 
			
		||||
                          var toReturn = $"`--{x.LongName}`";
 | 
			
		||||
 | 
			
		||||
                          if (!string.IsNullOrWhiteSpace(x.ShortName))
 | 
			
		||||
                              toReturn += $" (`-{x.ShortName}`)";
 | 
			
		||||
 | 
			
		||||
                          toReturn += $"   {x.HelpText}  ";
 | 
			
		||||
                          return toReturn;
 | 
			
		||||
                      })
 | 
			
		||||
                      .ToList();
 | 
			
		||||
 | 
			
		||||
        return strs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static string[] GetCommandRequirements(CommandInfo cmd, GuildPerm? overrides = null)
 | 
			
		||||
    {
 | 
			
		||||
        var toReturn = new List<string>();
 | 
			
		||||
 | 
			
		||||
        if (cmd.Preconditions.Any(x => x is OwnerOnlyAttribute))
 | 
			
		||||
            toReturn.Add("Bot Owner Only");
 | 
			
		||||
        
 | 
			
		||||
        if(cmd.Preconditions.Any(x => x is NoPublicBotAttribute)
 | 
			
		||||
           || cmd.Module
 | 
			
		||||
                  .Preconditions
 | 
			
		||||
                  .Any(x => x is NoPublicBotAttribute)
 | 
			
		||||
            || cmd.Module.GetTopLevelModule()
 | 
			
		||||
                  .Preconditions
 | 
			
		||||
                  .Any(x => x is NoPublicBotAttribute))
 | 
			
		||||
            toReturn.Add("No Public Bot");
 | 
			
		||||
 | 
			
		||||
        if (cmd.Preconditions
 | 
			
		||||
               .Any(x => x is OnlyPublicBotAttribute)
 | 
			
		||||
            || cmd.Module
 | 
			
		||||
                  .Preconditions
 | 
			
		||||
                  .Any(x => x is OnlyPublicBotAttribute)
 | 
			
		||||
            || cmd.Module.GetTopLevelModule()
 | 
			
		||||
                  .Preconditions
 | 
			
		||||
                  .Any(x => x is OnlyPublicBotAttribute))
 | 
			
		||||
            toReturn.Add("Only Public Bot");
 | 
			
		||||
 | 
			
		||||
        var userPermString = cmd.Preconditions
 | 
			
		||||
                                .Where(ca => ca is UserPermAttribute)
 | 
			
		||||
                                .Cast<UserPermAttribute>()
 | 
			
		||||
                                .Select(userPerm =>
 | 
			
		||||
                                {
 | 
			
		||||
                                    if (userPerm.ChannelPermission is { } cPerm)
 | 
			
		||||
                                        return GetPreconditionString(cPerm);
 | 
			
		||||
 | 
			
		||||
                                    if (userPerm.GuildPermission is { } gPerm)
 | 
			
		||||
                                        return GetPreconditionString(gPerm);
 | 
			
		||||
 | 
			
		||||
                                    return string.Empty;
 | 
			
		||||
                                })
 | 
			
		||||
                                .Where(x => !string.IsNullOrWhiteSpace(x))
 | 
			
		||||
                                .Join('\n');
 | 
			
		||||
        
 | 
			
		||||
        if (overrides is null)
 | 
			
		||||
        {
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(userPermString))
 | 
			
		||||
                toReturn.Add(userPermString);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(userPermString))
 | 
			
		||||
                toReturn.Add(Format.Strikethrough(userPermString));
 | 
			
		||||
 | 
			
		||||
            toReturn.Add(GetPreconditionString(overrides.Value));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return toReturn.ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static string GetPreconditionString(ChannelPerm perm)
 | 
			
		||||
        => (perm + " Channel Permission").Replace("Guild", "Server");
 | 
			
		||||
 | 
			
		||||
    public static string GetPreconditionString(GuildPerm perm)
 | 
			
		||||
        => (perm + " Server Permission").Replace("Guild", "Server");
 | 
			
		||||
 | 
			
		||||
    private string GetText(LocStr str, IGuild guild)
 | 
			
		||||
        => _strings.GetText(str, guild?.Id);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,758 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Music.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
[NoPublicBot]
 | 
			
		||||
public sealed partial class Music : NadekoModule<IMusicService>
 | 
			
		||||
{
 | 
			
		||||
    public enum All { All = -1 }
 | 
			
		||||
 | 
			
		||||
    public enum InputRepeatType
 | 
			
		||||
    {
 | 
			
		||||
        N = 0, No = 0, None = 0,
 | 
			
		||||
        T = 1, Track = 1, S = 1, Song = 1,
 | 
			
		||||
        Q = 2, Queue = 2, Playlist = 2, Pl = 2
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public const string MUSIC_ICON_URL = "http://i.imgur.com/nhKS3PT.png";
 | 
			
		||||
 | 
			
		||||
    private const int LQ_ITEMS_PER_PAGE = 9;
 | 
			
		||||
 | 
			
		||||
    private static readonly SemaphoreSlim _voiceChannelLock = new(1, 1);
 | 
			
		||||
    private readonly ILogCommandService _logService;
 | 
			
		||||
 | 
			
		||||
    public Music(ILogCommandService logService)
 | 
			
		||||
        => _logService = logService;
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> ValidateAsync()
 | 
			
		||||
    {
 | 
			
		||||
        var user = (IGuildUser)ctx.User;
 | 
			
		||||
        var userVoiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
 | 
			
		||||
        if (userVoiceChannelId is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var currentUser = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
        if (currentUser.VoiceChannel?.Id != userVoiceChannelId)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null)
 | 
			
		||||
    {
 | 
			
		||||
        botUser ??= await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
        await _voiceChannelLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _))
 | 
			
		||||
                await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _voiceChannelLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> QueuePreconditionInternalAsync()
 | 
			
		||||
    {
 | 
			
		||||
        var user = (IGuildUser)ctx.User;
 | 
			
		||||
        var voiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
 | 
			
		||||
        if (voiceChannelId is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
        var botUser = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
        await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
 | 
			
		||||
 | 
			
		||||
        if (botUser.VoiceChannel?.Id != voiceChannelId)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task QueueByQuery(string query, bool asNext = false, MusicPlatform? forcePlatform = null)
 | 
			
		||||
    {
 | 
			
		||||
        var succ = await QueuePreconditionInternalAsync();
 | 
			
		||||
        if (!succ)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
 | 
			
		||||
        if (mp is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var (trackInfo, index) = await mp.TryEnqueueTrackAsync(query, ctx.User.ToString(), asNext, forcePlatform);
 | 
			
		||||
        if (trackInfo is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.track_not_found);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var embed = _eb.Create()
 | 
			
		||||
                           .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);
 | 
			
		||||
 | 
			
		||||
            var queuedMessage = await _service.SendToOutputAsync(ctx.Guild.Id, embed);
 | 
			
		||||
            queuedMessage?.DeleteAfter(10, _logService);
 | 
			
		||||
            if (mp.IsStopped)
 | 
			
		||||
            {
 | 
			
		||||
                var msg = await ReplyPendingLocalizedAsync(strs.queue_stopped(Format.Code(prefix + "play")));
 | 
			
		||||
                msg.DeleteAfter(10, _logService);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            // ignored
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task MoveToIndex(int index)
 | 
			
		||||
    {
 | 
			
		||||
        if (--index < 0)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var succ = await QueuePreconditionInternalAsync();
 | 
			
		||||
        if (!succ)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
 | 
			
		||||
        if (mp is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mp.MoveTo(index);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // join vc
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task Join()
 | 
			
		||||
    {
 | 
			
		||||
        var user = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
        var voiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
 | 
			
		||||
        if (voiceChannelId is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await _service.JoinVoiceChannelAsync(user.GuildId, voiceChannelId.Value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // leave vc (destroy)
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task Destroy()
 | 
			
		||||
    {
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await _service.LeaveVoiceChannelAsync(ctx.Guild.Id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // play - no args = next
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [Priority(2)]
 | 
			
		||||
    public Task Play()
 | 
			
		||||
        => Next();
 | 
			
		||||
 | 
			
		||||
    // play - index = skip to that index
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
    public Task Play(int index)
 | 
			
		||||
        => MoveToIndex(index);
 | 
			
		||||
 | 
			
		||||
    // play - query = q(query)
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [Priority(0)]
 | 
			
		||||
    public Task Play([Leftover] string query)
 | 
			
		||||
        => QueueByQuery(query);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public Task Queue([Leftover] string query)
 | 
			
		||||
        => QueueByQuery(query);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public Task QueueNext([Leftover] string query)
 | 
			
		||||
        => QueueByQuery(query, true);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task Volume(int vol)
 | 
			
		||||
    {
 | 
			
		||||
        if (vol is < 0 or > 100)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.volume_input_invalid);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await _service.SetVolumeAsync(ctx.Guild.Id, vol);
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.volume_set(vol));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task Next()
 | 
			
		||||
    {
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var success = await _service.PlayAsync(ctx.Guild.Id, ((IGuildUser)ctx.User).VoiceChannel.Id);
 | 
			
		||||
        if (!success)
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // list queue, relevant page
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task ListQueue()
 | 
			
		||||
    {
 | 
			
		||||
        // show page with the current track
 | 
			
		||||
        if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ListQueue((mp.CurrentIndex / LQ_ITEMS_PER_PAGE) + 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // list queue, specify page
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task ListQueue(int page)
 | 
			
		||||
    {
 | 
			
		||||
        if (--page < 0)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        IReadOnlyCollection<IQueuedTrackInfo> tracks;
 | 
			
		||||
        if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || (tracks = mp.GetQueuedTracks()).Count == 0)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        IEmbedBuilder PrintAction(int curPage)
 | 
			
		||||
        {
 | 
			
		||||
            var desc = string.Empty;
 | 
			
		||||
            var current = mp.GetCurrentTrack(out var currentIndex);
 | 
			
		||||
            if (current is not null)
 | 
			
		||||
                desc = $"`🔊` {current.PrettyFullName()}\n\n" + desc;
 | 
			
		||||
 | 
			
		||||
            var repeatType = mp.Repeat;
 | 
			
		||||
            var add = string.Empty;
 | 
			
		||||
            if (mp.IsStopped)
 | 
			
		||||
                add += Format.Bold(GetText(strs.queue_stopped(Format.Code(prefix + "play")))) + "\n";
 | 
			
		||||
            // var mps = mp.MaxPlaytimeSeconds;
 | 
			
		||||
            // if (mps > 0)
 | 
			
		||||
            //     add += Format.Bold(GetText(strs.song_skips_after(TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss")))) + "\n";
 | 
			
		||||
            if (repeatType == PlayerRepeatType.Track)
 | 
			
		||||
                add += "🔂 " + GetText(strs.repeating_track) + "\n";
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (mp.AutoPlay)
 | 
			
		||||
                    add += "↪ " + GetText(strs.autoplaying) + "\n";
 | 
			
		||||
                // if (mp.FairPlay && !mp.Autoplay)
 | 
			
		||||
                //     add += " " + GetText(strs.fairplay) + "\n";
 | 
			
		||||
                if (repeatType == PlayerRepeatType.Queue)
 | 
			
		||||
                    add += "🔁 " + GetText(strs.repeating_queue) + "\n";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            desc += tracks.Skip(LQ_ITEMS_PER_PAGE * curPage)
 | 
			
		||||
                          .Take(LQ_ITEMS_PER_PAGE)
 | 
			
		||||
                          .Select((v, index) =>
 | 
			
		||||
                          {
 | 
			
		||||
                              index += LQ_ITEMS_PER_PAGE * curPage;
 | 
			
		||||
                              if (index == currentIndex)
 | 
			
		||||
                                  return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
 | 
			
		||||
 | 
			
		||||
                              return $"`{index + 1}.` {v.PrettyFullName()}";
 | 
			
		||||
                          })
 | 
			
		||||
                          .Join('\n');
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(add))
 | 
			
		||||
                desc = add + "\n" + desc;
 | 
			
		||||
 | 
			
		||||
            var embed = _eb.Create()
 | 
			
		||||
                           .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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ctx.SendPaginatedConfirmAsync(page, PrintAction, tracks.Count, LQ_ITEMS_PER_PAGE, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // search
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task QueueSearch([Leftover] string query)
 | 
			
		||||
    {
 | 
			
		||||
        _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
        var videos = await _service.SearchVideosAsync(query);
 | 
			
		||||
 | 
			
		||||
        if (videos.Count == 0)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.track_not_found);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var resultsString = videos.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}").Join('\n');
 | 
			
		||||
 | 
			
		||||
        var msg = await SendConfirmAsync(resultsString);
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
 | 
			
		||||
            if (input is null || !int.TryParse(input, out var index) || (index -= 1) < 0 || index >= videos.Count)
 | 
			
		||||
            {
 | 
			
		||||
                _logService.AddDeleteIgnore(msg.Id);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await msg.DeleteAsync();
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            query = videos[index].Url;
 | 
			
		||||
 | 
			
		||||
            await Play(query);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _logService.AddDeleteIgnore(msg.Id);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await msg.DeleteAsync();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
    public async Task TrackRemove(int index)
 | 
			
		||||
    {
 | 
			
		||||
        if (index < 1)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.removed_track_error);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!mp.TryRemoveTrackAt(index - 1, out var track))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.removed_track_error);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
                       .WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
 | 
			
		||||
                       .WithDescription(track.PrettyName())
 | 
			
		||||
                       .WithFooter(track.PrettyInfo())
 | 
			
		||||
                       .WithErrorColor();
 | 
			
		||||
 | 
			
		||||
        await _service.SendToOutputAsync(ctx.Guild.Id, embed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [Priority(0)]
 | 
			
		||||
    public async Task TrackRemove(All _ = All.All)
 | 
			
		||||
    {
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mp.Clear();
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.queue_cleared);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task Stop()
 | 
			
		||||
    {
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mp.Stop();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private PlayerRepeatType InputToDbType(InputRepeatType type)
 | 
			
		||||
        => type switch
 | 
			
		||||
        {
 | 
			
		||||
            InputRepeatType.None => PlayerRepeatType.None,
 | 
			
		||||
            InputRepeatType.Queue => PlayerRepeatType.Queue,
 | 
			
		||||
            InputRepeatType.Track => PlayerRepeatType.Track,
 | 
			
		||||
            _ => PlayerRepeatType.Queue
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task QueueRepeat(InputRepeatType type = InputRepeatType.Queue)
 | 
			
		||||
    {
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await _service.SetRepeatAsync(ctx.Guild.Id, InputToDbType(type));
 | 
			
		||||
 | 
			
		||||
        if (type == InputRepeatType.None)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.repeating_none);
 | 
			
		||||
        else if (type == InputRepeatType.Queue)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.repeating_queue);
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.repeating_track);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task Pause()
 | 
			
		||||
    {
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || mp.GetCurrentTrack(out _) is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mp.TogglePause();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public Task Radio(string radioLink)
 | 
			
		||||
        => QueueByQuery(radioLink, false, MusicPlatform.Radio);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [OwnerOnly]
 | 
			
		||||
    public Task Local([Leftover] string path)
 | 
			
		||||
        => QueueByQuery(path, false, MusicPlatform.Local);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [OwnerOnly]
 | 
			
		||||
    public async Task LocalPlaylist([Leftover] string dirPath)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(dirPath))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var user = (IGuildUser)ctx.User;
 | 
			
		||||
        var voiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
 | 
			
		||||
        if (voiceChannelId is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
        var botUser = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
        await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
 | 
			
		||||
 | 
			
		||||
        if (botUser.VoiceChannel?.Id != voiceChannelId)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
 | 
			
		||||
        if (mp is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await _service.EnqueueDirectoryAsync(mp, dirPath, ctx.User.ToString());
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.dir_queue_complete);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task TrackMove(int from, int to)
 | 
			
		||||
    {
 | 
			
		||||
        if (--from < 0 || --to < 0 || from == to)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.invalid_input);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
 | 
			
		||||
        if (mp is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var track = mp.MoveTrack(from, to);
 | 
			
		||||
        if (track is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.invalid_input);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
                       .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);
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public Task SoundCloudQueue([Leftover] string query)
 | 
			
		||||
        => QueueByQuery(query, false, MusicPlatform.SoundCloud);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task SoundCloudPl([Leftover] string playlist)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(playlist))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var succ = await QueuePreconditionInternalAsync();
 | 
			
		||||
        if (!succ)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
 | 
			
		||||
        if (mp is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
        await _service.EnqueueSoundcloudPlaylistAsync(mp, playlist, ctx.User.ToString());
 | 
			
		||||
 | 
			
		||||
        await ctx.OkAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task Playlist([Leftover] string playlistQuery)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(playlistQuery))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var succ = await QueuePreconditionInternalAsync();
 | 
			
		||||
        if (!succ)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
 | 
			
		||||
        if (mp is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var queuedCount = await _service.EnqueueYoutubePlaylistAsync(mp, playlistQuery, ctx.User.ToString());
 | 
			
		||||
        if (queuedCount == 0)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_search_results);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ctx.OkAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task NowPlaying()
 | 
			
		||||
    {
 | 
			
		||||
        var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
 | 
			
		||||
        if (mp is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var currentTrack = mp.GetCurrentTrack(out _);
 | 
			
		||||
        if (currentTrack is null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
                       .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 ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task PlaylistShuffle()
 | 
			
		||||
    {
 | 
			
		||||
        var valid = await ValidateAsync();
 | 
			
		||||
        if (!valid)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
 | 
			
		||||
        if (mp is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mp.ShuffleQueue();
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.queue_shuffled);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
    public async Task SetMusicChannel()
 | 
			
		||||
    {
 | 
			
		||||
        await _service.SetMusicChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.set_music_channel);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
    public async Task UnsetMusicChannel()
 | 
			
		||||
    {
 | 
			
		||||
        await _service.SetMusicChannelAsync(ctx.Guild.Id, null);
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.unset_music_channel);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task AutoDisconnect()
 | 
			
		||||
    {
 | 
			
		||||
        var newState = await _service.ToggleAutoDisconnectAsync(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
        if (newState)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.autodc_enable);
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.autodc_disable);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    public async Task MusicQuality()
 | 
			
		||||
    {
 | 
			
		||||
        var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id);
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.current_music_quality(Format.Bold(quality.ToString())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    public async Task MusicQuality(QualityPreset preset)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.SetMusicQualityAsync(ctx.Guild.Id, preset);
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.music_quality_set(Format.Bold(preset.ToString())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task QueueAutoPlay()
 | 
			
		||||
    {
 | 
			
		||||
        var newValue = await _service.ToggleQueueAutoPlayAsync(ctx.Guild.Id);
 | 
			
		||||
        if (newValue)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.music_autoplay_on);
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.music_autoplay_off);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,229 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Music.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed partial class Music
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public sealed partial class PlaylistCommands : NadekoModule<IMusicService>
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly SemaphoreSlim _playlistLock = new(1, 1);
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly IBotCredentials _creds;
 | 
			
		||||
 | 
			
		||||
        public PlaylistCommands(DbService db, IBotCredentials creds)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _creds = creds;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null)
 | 
			
		||||
        {
 | 
			
		||||
            botUser ??= await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
            await _voiceChannelLock.WaitAsync();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _))
 | 
			
		||||
                    await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _voiceChannelLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Playlists([Leftover] int num = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (num <= 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            List<MusicPlaylist> playlists;
 | 
			
		||||
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var embed = _eb.Create(ctx)
 | 
			
		||||
                           .WithAuthor(GetText(strs.playlists_page(num)), MUSIC_ICON_URL)
 | 
			
		||||
                           .WithDescription(string.Join("\n",
 | 
			
		||||
                               playlists.Select(r => GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count)))))
 | 
			
		||||
                           .WithOkColor();
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task DeletePlaylist([Leftover] int id)
 | 
			
		||||
        {
 | 
			
		||||
            var success = false;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await using var uow = _db.GetDbContext();
 | 
			
		||||
                var pl = uow.MusicPlaylists.FirstOrDefault(x => x.Id == id);
 | 
			
		||||
 | 
			
		||||
                if (pl is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_creds.IsOwner(ctx.User) || pl.AuthorId == ctx.User.Id)
 | 
			
		||||
                    {
 | 
			
		||||
                        uow.MusicPlaylists.Remove(pl);
 | 
			
		||||
                        await uow.SaveChangesAsync();
 | 
			
		||||
                        success = true;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error deleting playlist");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!success)
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.playlist_delete_fail);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.playlist_deleted);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task PlaylistShow(int id, int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (page-- < 1)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            MusicPlaylist mpl;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                mpl = uow.MusicPlaylists.GetWithSongs(id);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
                cur =>
 | 
			
		||||
                {
 | 
			
		||||
                    var i = 0;
 | 
			
		||||
                    var str = string.Join("\n",
 | 
			
		||||
                        mpl.Songs.Skip(cur * 20)
 | 
			
		||||
                           .Take(20)
 | 
			
		||||
                           .Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`"));
 | 
			
		||||
                    return _eb.Create().WithTitle($"\"{mpl.Name}\" by {mpl.Author}").WithOkColor().WithDescription(str);
 | 
			
		||||
                },
 | 
			
		||||
                mpl.Songs.Count,
 | 
			
		||||
                20);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Save([Leftover] string name)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var songs = mp.GetQueuedTracks()
 | 
			
		||||
                          .Select(s => new PlaylistSong
 | 
			
		||||
                          {
 | 
			
		||||
                              Provider = s.Platform.ToString(),
 | 
			
		||||
                              ProviderType = (MusicType)s.Platform,
 | 
			
		||||
                              Title = s.Title,
 | 
			
		||||
                              Query = s.Platform == MusicPlatform.Local ? s.GetStreamUrl().Result!.Trim('"') : s.Url
 | 
			
		||||
                          })
 | 
			
		||||
                          .ToList();
 | 
			
		||||
 | 
			
		||||
            MusicPlaylist playlist;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                playlist = new()
 | 
			
		||||
                {
 | 
			
		||||
                    Name = name,
 | 
			
		||||
                    Author = ctx.User.Username,
 | 
			
		||||
                    AuthorId = ctx.User.Id,
 | 
			
		||||
                    Songs = songs.ToList()
 | 
			
		||||
                };
 | 
			
		||||
                uow.MusicPlaylists.Add(playlist);
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                            .WithOkColor()
 | 
			
		||||
                                            .WithTitle(GetText(strs.playlist_saved))
 | 
			
		||||
                                            .AddField(GetText(strs.name), name)
 | 
			
		||||
                                            .AddField(GetText(strs.id), playlist.Id.ToString()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Load([Leftover] int id)
 | 
			
		||||
        {
 | 
			
		||||
            // expensive action, 1 at a time
 | 
			
		||||
            await _playlistLock.WaitAsync();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var user = (IGuildUser)ctx.User;
 | 
			
		||||
                var voiceChannelId = user.VoiceChannel?.Id;
 | 
			
		||||
 | 
			
		||||
                if (voiceChannelId is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
                var botUser = await ctx.Guild.GetCurrentUserAsync();
 | 
			
		||||
                await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
 | 
			
		||||
 | 
			
		||||
                if (botUser.VoiceChannel?.Id != voiceChannelId)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
 | 
			
		||||
                if (mp is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.no_player);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                MusicPlaylist mpl;
 | 
			
		||||
                await using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    mpl = uow.MusicPlaylists.GetWithSongs(id);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (mpl is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.playlist_id_not_found);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                IUserMessage msg = null;
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ctx.Channel.SendMessageAsync(
 | 
			
		||||
                        GetText(strs.attempting_to_queue(Format.Bold(mpl.Songs.Count.ToString()))));
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception)
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await mp.EnqueueManyAsync(mpl.Songs.Select(x => (x.Query, (MusicPlatform)x.ProviderType)),
 | 
			
		||||
                    ctx.User.ToString());
 | 
			
		||||
 | 
			
		||||
                if (msg is not null)
 | 
			
		||||
                    await msg.ModifyAsync(m => m.Content = GetText(strs.playlist_queue_complete));
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _playlistLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,218 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Ayu.Discord.Voice;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class AyuVoiceStateService : INService
 | 
			
		||||
{
 | 
			
		||||
    // public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy);
 | 
			
		||||
    // public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; };
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, IVoiceProxy> _voiceProxies = new();
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, SemaphoreSlim> _voiceGatewayLocks = new();
 | 
			
		||||
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly MethodInfo _sendVoiceStateUpdateMethodInfo;
 | 
			
		||||
    private readonly object _dnetApiClient;
 | 
			
		||||
    private readonly ulong _currentUserId;
 | 
			
		||||
 | 
			
		||||
    public AyuVoiceStateService(DiscordSocketClient client)
 | 
			
		||||
    {
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _currentUserId = _client.CurrentUser.Id;
 | 
			
		||||
 | 
			
		||||
        var prop = _client.GetType()
 | 
			
		||||
                          .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
 | 
			
		||||
                          .First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient");
 | 
			
		||||
        _dnetApiClient = prop.GetValue(_client, null);
 | 
			
		||||
        _sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType()
 | 
			
		||||
                                                        .GetMethod("SendVoiceStateUpdateAsync",
 | 
			
		||||
                                                            new[]
 | 
			
		||||
                                                            {
 | 
			
		||||
                                                                typeof(ulong), typeof(ulong?), typeof(bool),
 | 
			
		||||
                                                                typeof(bool), typeof(RequestOptions)
 | 
			
		||||
                                                            });
 | 
			
		||||
 | 
			
		||||
        _client.LeftGuild += ClientOnLeftGuild;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task ClientOnLeftGuild(SocketGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        if (_voiceProxies.TryRemove(guild.Id, out var proxy))
 | 
			
		||||
        {
 | 
			
		||||
            proxy.StopGateway();
 | 
			
		||||
            proxy.SetGateway(null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task InvokeSendVoiceStateUpdateAsync(
 | 
			
		||||
            ulong guildId,
 | 
			
		||||
            ulong? channelId = null,
 | 
			
		||||
            bool isDeafened = false,
 | 
			
		||||
            bool isMuted = false)
 | 
			
		||||
        // return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted);
 | 
			
		||||
        => (Task)_sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient,
 | 
			
		||||
            new object[] { guildId, channelId, isMuted, isDeafened, null });
 | 
			
		||||
 | 
			
		||||
    private Task SendLeaveVoiceChannelInternalAsync(ulong guildId)
 | 
			
		||||
        => InvokeSendVoiceStateUpdateAsync(guildId);
 | 
			
		||||
 | 
			
		||||
    private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId)
 | 
			
		||||
        => InvokeSendVoiceStateUpdateAsync(guildId, channelId);
 | 
			
		||||
 | 
			
		||||
    private SemaphoreSlim GetVoiceGatewayLock(ulong guildId)
 | 
			
		||||
        => _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1));
 | 
			
		||||
 | 
			
		||||
    private async Task LeaveVoiceChannelInternalAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var complete = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
			
		||||
 | 
			
		||||
        Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
 | 
			
		||||
        {
 | 
			
		||||
            if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId && newState.VoiceChannel?.Id is null)
 | 
			
		||||
                complete.TrySetResult(true);
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
 | 
			
		||||
 | 
			
		||||
            if (_voiceProxies.TryGetValue(guildId, out var proxy))
 | 
			
		||||
            {
 | 
			
		||||
                _ = proxy.StopGateway();
 | 
			
		||||
                proxy.SetGateway(null);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await SendLeaveVoiceChannelInternalAsync(guildId);
 | 
			
		||||
            await Task.WhenAny(Task.Delay(1500), complete.Task);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task LeaveVoiceChannel(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var gwLock = GetVoiceGatewayLock(guildId);
 | 
			
		||||
        await gwLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await LeaveVoiceChannelInternalAsync(guildId);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            gwLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<IVoiceProxy> InternalConnectToVcAsync(ulong guildId, ulong channelId)
 | 
			
		||||
    {
 | 
			
		||||
        var voiceStateUpdatedSource =
 | 
			
		||||
            new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
			
		||||
        var voiceServerUpdatedSource =
 | 
			
		||||
            new TaskCompletionSource<SocketVoiceServer>(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
			
		||||
 | 
			
		||||
        Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
 | 
			
		||||
        {
 | 
			
		||||
            if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId)
 | 
			
		||||
            {
 | 
			
		||||
                if (newState.VoiceChannel?.Id == channelId)
 | 
			
		||||
                    voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId);
 | 
			
		||||
 | 
			
		||||
                voiceStateUpdatedSource.TrySetResult(null);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Task OnVoiceServerUpdated(SocketVoiceServer data)
 | 
			
		||||
        {
 | 
			
		||||
            if (data.Guild.Id == guildId)
 | 
			
		||||
                voiceServerUpdatedSource.TrySetResult(data);
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            _client.VoiceServerUpdated += OnVoiceServerUpdated;
 | 
			
		||||
            _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
 | 
			
		||||
 | 
			
		||||
            await SendJoinVoiceChannelInternalAsync(guildId, channelId);
 | 
			
		||||
 | 
			
		||||
            // create a delay task, how much to wait for gateway response
 | 
			
		||||
            using var cts = new CancellationTokenSource();
 | 
			
		||||
            var delayTask = Task.Delay(2500, cts.Token);
 | 
			
		||||
 | 
			
		||||
            // either delay or successful voiceStateUpdate
 | 
			
		||||
            var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task);
 | 
			
		||||
            // either delay or successful voiceServerUpdate
 | 
			
		||||
            var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task);
 | 
			
		||||
 | 
			
		||||
            // wait for both to end (max 1s) and check if either of them is a delay task
 | 
			
		||||
            var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask);
 | 
			
		||||
            if (results[0] == delayTask || results[1] == delayTask)
 | 
			
		||||
                // if either is delay, return null - connection unsuccessful
 | 
			
		||||
                return null;
 | 
			
		||||
            else
 | 
			
		||||
                cts.Cancel();
 | 
			
		||||
 | 
			
		||||
            // if both are succesful, that means we can safely get
 | 
			
		||||
            // the values from  completion sources
 | 
			
		||||
 | 
			
		||||
            var session = await voiceStateUpdatedSource.Task;
 | 
			
		||||
 | 
			
		||||
            // session can be null. Means we disconnected, or connected to the wrong channel (?!)
 | 
			
		||||
            if (session is null)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            var voiceServerData = await voiceServerUpdatedSource.Task;
 | 
			
		||||
 | 
			
		||||
            VoiceGateway CreateVoiceGatewayLocal()
 | 
			
		||||
            {
 | 
			
		||||
                return new(guildId, _currentUserId, session, voiceServerData.Token, voiceServerData.Endpoint);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var current = _voiceProxies.AddOrUpdate(guildId,
 | 
			
		||||
                _ => new VoiceProxy(CreateVoiceGatewayLocal()),
 | 
			
		||||
                (gid, currentProxy) =>
 | 
			
		||||
                {
 | 
			
		||||
                    _ = currentProxy.StopGateway();
 | 
			
		||||
                    currentProxy.SetGateway(CreateVoiceGatewayLocal());
 | 
			
		||||
                    return currentProxy;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            _ = current.StartGateway(); // don't await, this blocks until gateway is closed
 | 
			
		||||
            return current;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _client.VoiceServerUpdated -= OnVoiceServerUpdated;
 | 
			
		||||
            _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IVoiceProxy> JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true)
 | 
			
		||||
    {
 | 
			
		||||
        var gwLock = GetVoiceGatewayLock(guildId);
 | 
			
		||||
        await gwLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await LeaveVoiceChannelInternalAsync(guildId);
 | 
			
		||||
            return await InternalConnectToVcAsync(guildId, channelId);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            gwLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy)
 | 
			
		||||
        => _voiceProxies.TryGetValue(guildId, out proxy);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services;
 | 
			
		||||
 | 
			
		||||
public interface IMusicService : IPlaceholderProvider
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Leave voice channel in the specified guild if it's connected to one
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId">Id of the guild</param>
 | 
			
		||||
    public Task LeaveVoiceChannelAsync(ulong guildId);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Joins the voice channel with the specified id
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId">Id of the guild where the voice channel is</param>
 | 
			
		||||
    /// <param name="voiceChannelId">Id of the voice channel</param>
 | 
			
		||||
    public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId);
 | 
			
		||||
 | 
			
		||||
    Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel);
 | 
			
		||||
    bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer);
 | 
			
		||||
    Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer);
 | 
			
		||||
    Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer);
 | 
			
		||||
    Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer);
 | 
			
		||||
    Task<IUserMessage?> SendToOutputAsync(ulong guildId, IEmbedBuilder embed);
 | 
			
		||||
    Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId);
 | 
			
		||||
    Task<IList<(string Title, string Url)>> SearchVideosAsync(string query);
 | 
			
		||||
    Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId);
 | 
			
		||||
    Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType);
 | 
			
		||||
    Task SetVolumeAsync(ulong guildId, int value);
 | 
			
		||||
    Task<bool> ToggleAutoDisconnectAsync(ulong guildId);
 | 
			
		||||
    Task<QualityPreset> GetMusicQualityAsync(ulong guildId);
 | 
			
		||||
    Task SetMusicQualityAsync(ulong guildId, QualityPreset preset);
 | 
			
		||||
    Task<bool> ToggleQueueAutoPlayAsync(ulong guildId);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,461 +0,0 @@
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class MusicService : IMusicService
 | 
			
		||||
{
 | 
			
		||||
    private readonly AyuVoiceStateService _voiceStateService;
 | 
			
		||||
    private readonly ITrackResolveProvider _trackResolveProvider;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly IYoutubeResolver _ytResolver;
 | 
			
		||||
    private readonly ILocalTrackResolver _localResolver;
 | 
			
		||||
    private readonly ISoundcloudResolver _scResolver;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly IBotStrings _strings;
 | 
			
		||||
    private readonly IGoogleApiService _googleApiService;
 | 
			
		||||
    private readonly YtLoader _ytLoader;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, IMusicPlayer> _players;
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, (ITextChannel Default, ITextChannel? Override)> _outputChannels;
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, MusicPlayerSettings> _settings;
 | 
			
		||||
 | 
			
		||||
    public MusicService(
 | 
			
		||||
        AyuVoiceStateService voiceStateService,
 | 
			
		||||
        ITrackResolveProvider trackResolveProvider,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        IYoutubeResolver ytResolver,
 | 
			
		||||
        ILocalTrackResolver localResolver,
 | 
			
		||||
        ISoundcloudResolver scResolver,
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        IGoogleApiService googleApiService,
 | 
			
		||||
        YtLoader ytLoader,
 | 
			
		||||
        IEmbedBuilderService eb)
 | 
			
		||||
    {
 | 
			
		||||
        _voiceStateService = voiceStateService;
 | 
			
		||||
        _trackResolveProvider = trackResolveProvider;
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _ytResolver = ytResolver;
 | 
			
		||||
        _localResolver = localResolver;
 | 
			
		||||
        _scResolver = scResolver;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _googleApiService = googleApiService;
 | 
			
		||||
        _ytLoader = ytLoader;
 | 
			
		||||
        _eb = eb;
 | 
			
		||||
 | 
			
		||||
        _players = new();
 | 
			
		||||
        _outputChannels = new ConcurrentDictionary<ulong, (ITextChannel, ITextChannel?)>();
 | 
			
		||||
        _settings = new();
 | 
			
		||||
 | 
			
		||||
        _client.LeftGuild += ClientOnLeftGuild;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void DisposeMusicPlayer(IMusicPlayer musicPlayer)
 | 
			
		||||
    {
 | 
			
		||||
        musicPlayer.Kill();
 | 
			
		||||
        _ = Task.Delay(10_000).ContinueWith(_ => musicPlayer.Dispose());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void RemoveMusicPlayer(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        _outputChannels.TryRemove(guildId, out _);
 | 
			
		||||
        if (_players.TryRemove(guildId, out var mp))
 | 
			
		||||
            DisposeMusicPlayer(mp);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task ClientOnLeftGuild(SocketGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        RemoveMusicPlayer(guild.Id);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task LeaveVoiceChannelAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        RemoveMusicPlayer(guildId);
 | 
			
		||||
        await _voiceStateService.LeaveVoiceChannel(guildId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId)
 | 
			
		||||
        => _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId);
 | 
			
		||||
 | 
			
		||||
    public async Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel)
 | 
			
		||||
    {
 | 
			
		||||
        var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel);
 | 
			
		||||
        if (newPLayer is null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        return _players.GetOrAdd(contextChannel.GuildId, newPLayer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer)
 | 
			
		||||
        => _players.TryGetValue(guildId, out musicPlayer);
 | 
			
		||||
 | 
			
		||||
    public async Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer)
 | 
			
		||||
    {
 | 
			
		||||
        var count = 0;
 | 
			
		||||
        await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query))
 | 
			
		||||
        {
 | 
			
		||||
            if (mp.IsKilled)
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            mp.EnqueueTrack(track, queuer);
 | 
			
		||||
            ++count;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return count;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer)
 | 
			
		||||
    {
 | 
			
		||||
        await foreach (var track in _localResolver.ResolveDirectoryAsync(dirPath))
 | 
			
		||||
        {
 | 
			
		||||
            if (mp.IsKilled)
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            mp.EnqueueTrack(track, queuer);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer)
 | 
			
		||||
    {
 | 
			
		||||
        var i = 0;
 | 
			
		||||
        await foreach (var track in _scResolver.ResolvePlaylistAsync(playlist))
 | 
			
		||||
        {
 | 
			
		||||
            if (mp.IsKilled)
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            mp.EnqueueTrack(track, queuer);
 | 
			
		||||
            ++i;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return i;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<IMusicPlayer?> CreateMusicPlayerInternalAsync(ulong guildId, ITextChannel defaultChannel)
 | 
			
		||||
    {
 | 
			
		||||
        var queue = new MusicQueue();
 | 
			
		||||
        var resolver = _trackResolveProvider;
 | 
			
		||||
 | 
			
		||||
        if (!_voiceStateService.TryGetProxy(guildId, out var proxy))
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        var settings = await GetSettingsInternalAsync(guildId);
 | 
			
		||||
 | 
			
		||||
        ITextChannel? overrideChannel = null;
 | 
			
		||||
        if (settings.MusicChannelId is { } channelId)
 | 
			
		||||
        {
 | 
			
		||||
            overrideChannel = _client.GetGuild(guildId)?.GetTextChannel(channelId);
 | 
			
		||||
 | 
			
		||||
            if (overrideChannel is null)
 | 
			
		||||
                Log.Warning("Saved music output channel doesn't exist, falling back to current channel");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _outputChannels[guildId] = (defaultChannel, overrideChannel);
 | 
			
		||||
 | 
			
		||||
        var mp = new MusicPlayer(queue,
 | 
			
		||||
            resolver,
 | 
			
		||||
            proxy,
 | 
			
		||||
            _googleApiService,
 | 
			
		||||
            settings.QualityPreset,
 | 
			
		||||
            settings.AutoPlay);
 | 
			
		||||
 | 
			
		||||
        mp.SetRepeat(settings.PlayerRepeat);
 | 
			
		||||
 | 
			
		||||
        if (settings.Volume is >= 0 and <= 100)
 | 
			
		||||
            mp.SetVolume(settings.Volume);
 | 
			
		||||
        else
 | 
			
		||||
            Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume);
 | 
			
		||||
 | 
			
		||||
        mp.OnCompleted += OnTrackCompleted(guildId);
 | 
			
		||||
        mp.OnStarted += OnTrackStarted(guildId);
 | 
			
		||||
        mp.OnQueueStopped += OnQueueStopped(guildId);
 | 
			
		||||
 | 
			
		||||
        return mp;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IUserMessage?> SendToOutputAsync(ulong guildId, IEmbedBuilder embed)
 | 
			
		||||
    {
 | 
			
		||||
        if (_outputChannels.TryGetValue(guildId, out var chan))
 | 
			
		||||
        {
 | 
			
		||||
            var msg = await (chan.Override ?? chan.Default).EmbedAsync(embed);
 | 
			
		||||
            return msg;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Func<IMusicPlayer, IQueuedTrackInfo, Task> OnTrackCompleted(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        IUserMessage? lastFinishedMessage = null;
 | 
			
		||||
        return async (mp, trackInfo) =>
 | 
			
		||||
        {
 | 
			
		||||
            _ = lastFinishedMessage?.DeleteAsync();
 | 
			
		||||
            var embed = _eb.Create()
 | 
			
		||||
                           .WithOkColor()
 | 
			
		||||
                           .WithAuthor(GetText(guildId, strs.finished_track), Music.MUSIC_ICON_URL)
 | 
			
		||||
                           .WithDescription(trackInfo.PrettyName())
 | 
			
		||||
                           .WithFooter(trackInfo.PrettyTotalTime());
 | 
			
		||||
 | 
			
		||||
            lastFinishedMessage = await SendToOutputAsync(guildId, embed);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Func<IMusicPlayer, IQueuedTrackInfo, int, Task> OnTrackStarted(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        IUserMessage? lastPlayingMessage = null;
 | 
			
		||||
        return async (mp, trackInfo, index) =>
 | 
			
		||||
        {
 | 
			
		||||
            _ = lastPlayingMessage?.DeleteAsync();
 | 
			
		||||
            var embed = _eb.Create()
 | 
			
		||||
                           .WithOkColor()
 | 
			
		||||
                           .WithAuthor(GetText(guildId, strs.playing_track(index + 1)), Music.MUSIC_ICON_URL)
 | 
			
		||||
                           .WithDescription(trackInfo.PrettyName())
 | 
			
		||||
                           .WithFooter($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}");
 | 
			
		||||
 | 
			
		||||
            lastPlayingMessage = await SendToOutputAsync(guildId, embed);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Func<IMusicPlayer, Task> OnQueueStopped(ulong guildId)
 | 
			
		||||
        => _ =>
 | 
			
		||||
        {
 | 
			
		||||
            if (_settings.TryGetValue(guildId, out var settings))
 | 
			
		||||
            {
 | 
			
		||||
                if (settings.AutoDisconnect)
 | 
			
		||||
                    return LeaveVoiceChannelAsync(guildId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    // this has to be done because dragging bot to another vc isn't supported yet
 | 
			
		||||
    public async Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId)
 | 
			
		||||
    {
 | 
			
		||||
        if (!TryGetMusicPlayer(guildId, out var mp))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (mp.IsStopped)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_voiceStateService.TryGetProxy(guildId, out var proxy)
 | 
			
		||||
                || proxy.State == VoiceProxy.VoiceProxyState.Stopped)
 | 
			
		||||
                await JoinVoiceChannelAsync(guildId, voiceChannelId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mp.Next();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<IList<(string Title, string Url)>> SearchYtLoaderVideosAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        var result = await _ytLoader.LoadResultsAsync(query);
 | 
			
		||||
        return result.Select(x => (x.Title, x.Url)).ToList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<IList<(string Title, string Url)>> SearchGoogleApiVideosAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5);
 | 
			
		||||
        return result.Select(x => (x.Name, x.Url)).ToList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IList<(string Title, string Url)>> SearchVideosAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query);
 | 
			
		||||
            if (videos.Count > 0)
 | 
			
		||||
                return videos;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Failed geting videos with YtLoader: {ErrorMessage}", ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return await SearchGoogleApiVideosAsync(query);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Failed getting video results with Google Api. "
 | 
			
		||||
                        + "Probably google api key missing: {ErrorMessage}",
 | 
			
		||||
                ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Array.Empty<(string, string)>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string GetText(ulong guildId, LocStr str)
 | 
			
		||||
        => _strings.GetText(str, guildId);
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders()
 | 
			
		||||
    {
 | 
			
		||||
        // random track that's playing
 | 
			
		||||
        yield return ("%music.playing%", () =>
 | 
			
		||||
        {
 | 
			
		||||
            var randomPlayingTrack = _players.Select(x => x.Value.GetCurrentTrack(out _))
 | 
			
		||||
                                             .Where(x => x is not null)
 | 
			
		||||
                                             .Shuffle()
 | 
			
		||||
                                             .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
            if (randomPlayingTrack is null)
 | 
			
		||||
                return "-";
 | 
			
		||||
 | 
			
		||||
            return randomPlayingTrack.Title;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // number of servers currently listening to music
 | 
			
		||||
        yield return ("%music.servers%", () =>
 | 
			
		||||
        {
 | 
			
		||||
            var count = _players.Select(x => x.Value.GetCurrentTrack(out _)).Count(x => x is not null);
 | 
			
		||||
 | 
			
		||||
            return count.ToString();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        yield return ("%music.queued%", () =>
 | 
			
		||||
        {
 | 
			
		||||
            var count = _players.Sum(x => x.Value.GetQueuedTracks().Count);
 | 
			
		||||
 | 
			
		||||
            return count.ToString();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region Settings
 | 
			
		||||
 | 
			
		||||
    private async Task<MusicPlayerSettings> GetSettingsInternalAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_settings.TryGetValue(guildId, out var settings))
 | 
			
		||||
            return settings;
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var toReturn = _settings[guildId] = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        return toReturn;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task ModifySettingsInternalAsync<TState>(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        Action<MusicPlayerSettings, TState> action,
 | 
			
		||||
        TState state)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var ms = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
 | 
			
		||||
        action(ms, state);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
        _settings[guildId] = ms;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId)
 | 
			
		||||
    {
 | 
			
		||||
        if (channelId is null)
 | 
			
		||||
        {
 | 
			
		||||
            await UnsetMusicChannelAsync(guildId);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var channel = _client.GetGuild(guildId)?.GetTextChannel(channelId.Value);
 | 
			
		||||
        if (channel is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        await ModifySettingsInternalAsync(guildId,
 | 
			
		||||
            (settings, chId) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.MusicChannelId = chId;
 | 
			
		||||
            },
 | 
			
		||||
            channelId);
 | 
			
		||||
 | 
			
		||||
        _outputChannels.AddOrUpdate(guildId, (channel, channel), (_, old) => (old.Default, channel));
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task UnsetMusicChannelAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await ModifySettingsInternalAsync(guildId,
 | 
			
		||||
            (settings, _) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.MusicChannelId = null;
 | 
			
		||||
            },
 | 
			
		||||
            (ulong?)null);
 | 
			
		||||
 | 
			
		||||
        if (_outputChannels.TryGetValue(guildId, out var old))
 | 
			
		||||
            _outputChannels[guildId] = (old.Default, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType)
 | 
			
		||||
    {
 | 
			
		||||
        await ModifySettingsInternalAsync(guildId,
 | 
			
		||||
            (settings, type) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.PlayerRepeat = type;
 | 
			
		||||
            },
 | 
			
		||||
            repeatType);
 | 
			
		||||
 | 
			
		||||
        if (TryGetMusicPlayer(guildId, out var mp))
 | 
			
		||||
            mp.SetRepeat(repeatType);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetVolumeAsync(ulong guildId, int value)
 | 
			
		||||
    {
 | 
			
		||||
        if (value is < 0 or > 100)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(value));
 | 
			
		||||
 | 
			
		||||
        await ModifySettingsInternalAsync(guildId,
 | 
			
		||||
            (settings, newValue) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.Volume = newValue;
 | 
			
		||||
            },
 | 
			
		||||
            value);
 | 
			
		||||
 | 
			
		||||
        if (TryGetMusicPlayer(guildId, out var mp))
 | 
			
		||||
            mp.SetVolume(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ToggleAutoDisconnectAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var newState = false;
 | 
			
		||||
        await ModifySettingsInternalAsync(guildId,
 | 
			
		||||
            (settings, _) =>
 | 
			
		||||
            {
 | 
			
		||||
                newState = settings.AutoDisconnect = !settings.AutoDisconnect;
 | 
			
		||||
            },
 | 
			
		||||
            default(object));
 | 
			
		||||
 | 
			
		||||
        return newState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<QualityPreset> GetMusicQualityAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var settings = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
 | 
			
		||||
        return settings.QualityPreset;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset)
 | 
			
		||||
        => ModifySettingsInternalAsync(guildId,
 | 
			
		||||
            (settings, _) =>
 | 
			
		||||
            {
 | 
			
		||||
                settings.QualityPreset = preset;
 | 
			
		||||
            },
 | 
			
		||||
            preset);
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ToggleQueueAutoPlayAsync(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var newValue = false;
 | 
			
		||||
        await ModifySettingsInternalAsync(guildId,
 | 
			
		||||
            (settings, _) => newValue = settings.AutoPlay = !settings.AutoPlay,
 | 
			
		||||
            false);
 | 
			
		||||
 | 
			
		||||
        if (TryGetMusicPlayer(guildId, out var mp))
 | 
			
		||||
            mp.AutoPlay = newValue;
 | 
			
		||||
        
 | 
			
		||||
        return newValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
}
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services;
 | 
			
		||||
 | 
			
		||||
public sealed partial class YtLoader
 | 
			
		||||
{
 | 
			
		||||
    public class InitRange
 | 
			
		||||
    {
 | 
			
		||||
        public string Start { get; set; }
 | 
			
		||||
        public string End { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class IndexRange
 | 
			
		||||
    {
 | 
			
		||||
        public string Start { get; set; }
 | 
			
		||||
        public string End { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class ColorInfo
 | 
			
		||||
    {
 | 
			
		||||
        public string Primaries { get; set; }
 | 
			
		||||
        public string TransferCharacteristics { get; set; }
 | 
			
		||||
        public string MatrixCoefficients { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class YtAdaptiveFormat
 | 
			
		||||
    {
 | 
			
		||||
        public int Itag { get; set; }
 | 
			
		||||
        public string MimeType { get; set; }
 | 
			
		||||
        public int Bitrate { get; set; }
 | 
			
		||||
        public int Width { get; set; }
 | 
			
		||||
        public int Height { get; set; }
 | 
			
		||||
        public InitRange InitRange { get; set; }
 | 
			
		||||
        public IndexRange IndexRange { get; set; }
 | 
			
		||||
        public string LastModified { get; set; }
 | 
			
		||||
        public string ContentLength { get; set; }
 | 
			
		||||
        public string Quality { get; set; }
 | 
			
		||||
        public int Fps { get; set; }
 | 
			
		||||
        public string QualityLabel { get; set; }
 | 
			
		||||
        public string ProjectionType { get; set; }
 | 
			
		||||
        public int AverageBitrate { get; set; }
 | 
			
		||||
        public ColorInfo ColorInfo { get; set; }
 | 
			
		||||
        public string ApproxDurationMs { get; set; }
 | 
			
		||||
        public string SignatureCipher { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public abstract class TrackInfo
 | 
			
		||||
    {
 | 
			
		||||
        public abstract string Url { get; }
 | 
			
		||||
        public abstract string Title { get; }
 | 
			
		||||
        public abstract TimeSpan Duration { get; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public sealed class YtTrackInfo : TrackInfo
 | 
			
		||||
    {
 | 
			
		||||
        private const string BASE_YOUTUBE_URL = "https://youtube.com/watch?v=";
 | 
			
		||||
        public override string Url { get; }
 | 
			
		||||
        public override string Title { get; }
 | 
			
		||||
        public override TimeSpan Duration { get; }
 | 
			
		||||
 | 
			
		||||
        private readonly string _videoId;
 | 
			
		||||
 | 
			
		||||
        public YtTrackInfo(string title, string videoId, TimeSpan duration)
 | 
			
		||||
        {
 | 
			
		||||
            Title = title;
 | 
			
		||||
            Url = BASE_YOUTUBE_URL + videoId;
 | 
			
		||||
            Duration = duration;
 | 
			
		||||
 | 
			
		||||
            _videoId = videoId;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,130 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Services;
 | 
			
		||||
 | 
			
		||||
public sealed partial class YtLoader
 | 
			
		||||
{
 | 
			
		||||
    private static readonly byte[] _ytResultInitialData = Encoding.UTF8.GetBytes("var ytInitialData = ");
 | 
			
		||||
    private static readonly byte[] _ytResultJsonEnd = Encoding.UTF8.GetBytes(";<");
 | 
			
		||||
 | 
			
		||||
    private static readonly string[] _durationFormats =
 | 
			
		||||
    {
 | 
			
		||||
        @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss"
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
 | 
			
		||||
    public YtLoader(IHttpClientFactory httpFactory)
 | 
			
		||||
        => _httpFactory = httpFactory;
 | 
			
		||||
 | 
			
		||||
    // public async Task<TrackInfo> LoadTrackByIdAsync(string videoId)
 | 
			
		||||
    // {
 | 
			
		||||
    //     using var http = new HttpClient();
 | 
			
		||||
    //     http.DefaultRequestHeaders.Add("X-YouTube-Client-Name", "1");
 | 
			
		||||
    //     http.DefaultRequestHeaders.Add("X-YouTube-Client-Version", "2.20210520.09.00");
 | 
			
		||||
    //     http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
 | 
			
		||||
    //
 | 
			
		||||
    //     var responseString = await http.GetStringAsync($"https://youtube.com?" +
 | 
			
		||||
    //                         $"pbj=1" +
 | 
			
		||||
    //                         $"&hl=en" +
 | 
			
		||||
    //                         $"&v=" + videoId);
 | 
			
		||||
    //
 | 
			
		||||
    //     var jsonDoc = JsonDocument.Parse(responseString).RootElement;
 | 
			
		||||
    //     var elem = jsonDoc.EnumerateArray()
 | 
			
		||||
    //         .FirstOrDefault(x => x.TryGetProperty("page", out var elem) && elem.GetString() == "watch");
 | 
			
		||||
    //
 | 
			
		||||
    //     var formatsJsonArray = elem.GetProperty("streamingdata")
 | 
			
		||||
    //         .GetProperty("formats")
 | 
			
		||||
    //         .GetRawText();
 | 
			
		||||
    //     
 | 
			
		||||
    //     var formats = JsonSerializer.Deserialize<List<YtAdaptiveFormat>>(formatsJsonArray);
 | 
			
		||||
    //     var result = formats
 | 
			
		||||
    //         .Where(x => x.MimeType.StartsWith("audio/"))
 | 
			
		||||
    //         .OrderByDescending(x => x.Bitrate)
 | 
			
		||||
    //         .FirstOrDefault();
 | 
			
		||||
    //
 | 
			
		||||
    //     if (result is null)
 | 
			
		||||
    //         return null;
 | 
			
		||||
    //
 | 
			
		||||
    //     return new YtTrackInfo("1", "2", TimeSpan.Zero);
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    public async Task<IList<TrackInfo>> LoadResultsAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        query = Uri.EscapeDataString(query);
 | 
			
		||||
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
 | 
			
		||||
 | 
			
		||||
        byte[] response;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            response = await http.GetByteArrayAsync($"https://youtube.com/results?hl=en&search_query={query}");
 | 
			
		||||
        }
 | 
			
		||||
        catch (HttpRequestException ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Unable to retrieve data with YtLoader: {ErrorMessage}", ex.Message);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // there is a lot of useless html above the script tag, however if html gets significantly reduced
 | 
			
		||||
        // this will result in the json being cut off
 | 
			
		||||
 | 
			
		||||
        var mem = GetScriptResponseSpan(response);
 | 
			
		||||
        var root = JsonDocument.Parse(mem).RootElement;
 | 
			
		||||
 | 
			
		||||
        using var tracksJsonItems = root
 | 
			
		||||
                              .GetProperty("contents")
 | 
			
		||||
                              .GetProperty("twoColumnSearchResultsRenderer")
 | 
			
		||||
                              .GetProperty("primaryContents")
 | 
			
		||||
                              .GetProperty("sectionListRenderer")
 | 
			
		||||
                              .GetProperty("contents")[0]
 | 
			
		||||
                              .GetProperty("itemSectionRenderer")
 | 
			
		||||
                              .GetProperty("contents")
 | 
			
		||||
                              .EnumerateArray();
 | 
			
		||||
 | 
			
		||||
        var tracks = new List<TrackInfo>();
 | 
			
		||||
        foreach (var track in tracksJsonItems)
 | 
			
		||||
        {
 | 
			
		||||
            if (!track.TryGetProperty("videoRenderer", out var elem))
 | 
			
		||||
                continue;
 | 
			
		||||
 | 
			
		||||
            var videoId = elem.GetProperty("videoId").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();
 | 
			
		||||
 | 
			
		||||
            if (!TimeSpan.TryParseExact(durationString,
 | 
			
		||||
                    _durationFormats,
 | 
			
		||||
                    CultureInfo.InvariantCulture,
 | 
			
		||||
                    out var duration))
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Cannot parse duration: {DurationString}", durationString);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            tracks.Add(new YtTrackInfo(title, videoId, duration));
 | 
			
		||||
            if (tracks.Count >= 5)
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return tracks;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Memory<byte> GetScriptResponseSpan(byte[] response)
 | 
			
		||||
    {
 | 
			
		||||
        var responseSpan = response.AsSpan()[140_000..];
 | 
			
		||||
        var startIndex = responseSpan.IndexOf(_ytResultInitialData);
 | 
			
		||||
        if (startIndex == -1)
 | 
			
		||||
            return null; // FUTURE try selecting html
 | 
			
		||||
        startIndex += _ytResultInitialData.Length;
 | 
			
		||||
 | 
			
		||||
        var endIndex =
 | 
			
		||||
            140_000 + startIndex + responseSpan[(startIndex + 20_000)..].IndexOf(_ytResultJsonEnd) + 20_000;
 | 
			
		||||
        startIndex += 140_000;
 | 
			
		||||
        return response.AsMemory(startIndex, endIndex - startIndex);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface ICachableTrackData
 | 
			
		||||
{
 | 
			
		||||
    string Id { get; set; }
 | 
			
		||||
    string Url { get; set; }
 | 
			
		||||
    string Thumbnail { get; set; }
 | 
			
		||||
    public TimeSpan Duration { get; }
 | 
			
		||||
    MusicPlatform Platform { get; set; }
 | 
			
		||||
    string Title { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface ILocalTrackResolver : IPlatformQueryResolver
 | 
			
		||||
{
 | 
			
		||||
    IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface IMusicPlayer : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    float Volume { get; }
 | 
			
		||||
    bool IsPaused { get; }
 | 
			
		||||
    bool IsStopped { get; }
 | 
			
		||||
    bool IsKilled { get; }
 | 
			
		||||
    int CurrentIndex { get; }
 | 
			
		||||
    public PlayerRepeatType Repeat { get; }
 | 
			
		||||
    bool AutoPlay { get; set; }
 | 
			
		||||
 | 
			
		||||
    void Stop();
 | 
			
		||||
    void Clear();
 | 
			
		||||
    IReadOnlyCollection<IQueuedTrackInfo> GetQueuedTracks();
 | 
			
		||||
    IQueuedTrackInfo? GetCurrentTrack(out int index);
 | 
			
		||||
    void Next();
 | 
			
		||||
    bool MoveTo(int index);
 | 
			
		||||
    void SetVolume(int newVolume);
 | 
			
		||||
 | 
			
		||||
    void Kill();
 | 
			
		||||
    bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(
 | 
			
		||||
        string query,
 | 
			
		||||
        string queuer,
 | 
			
		||||
        bool asNext,
 | 
			
		||||
        MusicPlatform? forcePlatform = null);
 | 
			
		||||
 | 
			
		||||
    Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer);
 | 
			
		||||
    bool TogglePause();
 | 
			
		||||
    IQueuedTrackInfo? MoveTrack(int from, int to);
 | 
			
		||||
    void EnqueueTrack(ITrackInfo track, string queuer);
 | 
			
		||||
    void EnqueueTracks(IEnumerable<ITrackInfo> tracks, string queuer);
 | 
			
		||||
    void SetRepeat(PlayerRepeatType type);
 | 
			
		||||
    void ShuffleQueue();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface IMusicQueue
 | 
			
		||||
{
 | 
			
		||||
    int Index { get; }
 | 
			
		||||
    int Count { get; }
 | 
			
		||||
    IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int index);
 | 
			
		||||
    IQueuedTrackInfo EnqueueNext(ITrackInfo song, string queuer, out int index);
 | 
			
		||||
 | 
			
		||||
    void EnqueueMany(IEnumerable<ITrackInfo> tracks, string queuer);
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyCollection<IQueuedTrackInfo> List();
 | 
			
		||||
    IQueuedTrackInfo? GetCurrent(out int index);
 | 
			
		||||
    void Advance();
 | 
			
		||||
    void Clear();
 | 
			
		||||
    bool SetIndex(int index);
 | 
			
		||||
    bool TryRemoveAt(int index, out IQueuedTrackInfo? trackInfo, out bool isCurrent);
 | 
			
		||||
    void RemoveCurrent();
 | 
			
		||||
    IQueuedTrackInfo? MoveTrack(int from, int to);
 | 
			
		||||
    void Shuffle(Random rng);
 | 
			
		||||
    bool IsLast();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface IPlatformQueryResolver
 | 
			
		||||
{
 | 
			
		||||
    Task<ITrackInfo?> ResolveByQueryAsync(string query);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface IQueuedTrackInfo : ITrackInfo
 | 
			
		||||
{
 | 
			
		||||
    public ITrackInfo TrackInfo { get; }
 | 
			
		||||
 | 
			
		||||
    public string Queuer { get; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface IRadioResolver : IPlatformQueryResolver
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface ISoundcloudResolver : IPlatformQueryResolver
 | 
			
		||||
{
 | 
			
		||||
    bool IsSoundCloudLink(string url);
 | 
			
		||||
    IAsyncEnumerable<ITrackInfo> ResolvePlaylistAsync(string playlist);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface ITrackCacher
 | 
			
		||||
{
 | 
			
		||||
    Task<string?> GetOrCreateStreamLink(
 | 
			
		||||
        string id,
 | 
			
		||||
        MusicPlatform platform,
 | 
			
		||||
        Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory);
 | 
			
		||||
 | 
			
		||||
    Task CacheTrackDataAsync(ICachableTrackData data);
 | 
			
		||||
    Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform);
 | 
			
		||||
    Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform);
 | 
			
		||||
    Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data);
 | 
			
		||||
 | 
			
		||||
    Task CacheStreamUrlAsync(
 | 
			
		||||
        string id,
 | 
			
		||||
        MusicPlatform platform,
 | 
			
		||||
        string url,
 | 
			
		||||
        TimeSpan expiry);
 | 
			
		||||
 | 
			
		||||
    Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform);
 | 
			
		||||
    Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids);
 | 
			
		||||
    Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId);
 | 
			
		||||
    Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface ITrackInfo
 | 
			
		||||
{
 | 
			
		||||
    public string Id => string.Empty;
 | 
			
		||||
    public string Title { get; }
 | 
			
		||||
    public string Url { get; }
 | 
			
		||||
    public string Thumbnail { get; }
 | 
			
		||||
    public TimeSpan Duration { get; }
 | 
			
		||||
    public MusicPlatform Platform { get; }
 | 
			
		||||
    public ValueTask<string?> GetStreamUrl();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface ITrackResolveProvider
 | 
			
		||||
{
 | 
			
		||||
    Task<ITrackInfo?> QuerySongAsync(string query, MusicPlatform? forcePlatform);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Ayu.Discord.Voice;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface IVoiceProxy
 | 
			
		||||
{
 | 
			
		||||
    VoiceProxy.VoiceProxyState State { get; }
 | 
			
		||||
    public bool SendPcmFrame(VoiceClient vc, Span<byte> data, int length);
 | 
			
		||||
    public void SetGateway(VoiceGateway gateway);
 | 
			
		||||
    Task StartSpeakingAsync();
 | 
			
		||||
    Task StopSpeakingAsync();
 | 
			
		||||
    public Task StartGateway();
 | 
			
		||||
    Task StopGateway();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public interface IYoutubeResolver : IPlatformQueryResolver
 | 
			
		||||
{
 | 
			
		||||
    public Regex YtVideoIdRegex { get; }
 | 
			
		||||
    public Task<ITrackInfo?> ResolveByIdAsync(string id);
 | 
			
		||||
    IAsyncEnumerable<ITrackInfo> ResolveTracksFromPlaylistAsync(string query);
 | 
			
		||||
    Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryExtractingId);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed class CachableTrackData : ICachableTrackData
 | 
			
		||||
{
 | 
			
		||||
    public string Title { get; set; } = string.Empty;
 | 
			
		||||
    public string Id { get; set; } = string.Empty;
 | 
			
		||||
    public string Url { get; set; } = string.Empty;
 | 
			
		||||
    public string Thumbnail { get; set; } = string.Empty;
 | 
			
		||||
    public double TotalDurationMs { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonIgnore]
 | 
			
		||||
    public TimeSpan Duration
 | 
			
		||||
        => TimeSpan.FromMilliseconds(TotalDurationMs);
 | 
			
		||||
 | 
			
		||||
    public MusicPlatform Platform { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,95 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Common;
 | 
			
		||||
 | 
			
		||||
public sealed class MultimediaTimer : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private LpTimeProcDelegate lpTimeProc;
 | 
			
		||||
    private readonly uint _eventId;
 | 
			
		||||
    private readonly Action<object> _callback;
 | 
			
		||||
    private readonly object _state;
 | 
			
		||||
 | 
			
		||||
    public MultimediaTimer(Action<object> callback, object state, int period)
 | 
			
		||||
    {
 | 
			
		||||
        if (period <= 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0");
 | 
			
		||||
 | 
			
		||||
        _callback = callback;
 | 
			
		||||
        _state = state;
 | 
			
		||||
 | 
			
		||||
        lpTimeProc = CallbackInternal;
 | 
			
		||||
        _eventId = timeSetEvent((uint)period, 1, lpTimeProc, 0, TimerMode.Periodic);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     The timeSetEvent function starts a specified timer event. The multimedia timer runs in its own thread.
 | 
			
		||||
    ///     After the event is activated, it calls the specified callback function or sets or pulses the specified
 | 
			
		||||
    ///     event object.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="uDelay">
 | 
			
		||||
    ///     Event delay, in milliseconds. If this value is not in the range of the minimum and
 | 
			
		||||
    ///     maximum event delays supported by the timer, the function returns an error.
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="uResolution">
 | 
			
		||||
    ///     Resolution of the timer event, in milliseconds. The resolution increases with
 | 
			
		||||
    ///     smaller values; a resolution of 0 indicates periodic events should occur with the greatest possible accuracy.
 | 
			
		||||
    ///     To reduce system overhead, however, you should use the maximum value appropriate for your application.
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="lpTimeProc">
 | 
			
		||||
    ///     Pointer to a callback function that is called once upon expiration of a single event or periodically upon
 | 
			
		||||
    ///     expiration of periodic events. If fuEvent specifies the TIME_CALLBACK_EVENT_SET or TIME_CALLBACK_EVENT_PULSE
 | 
			
		||||
    ///     flag, then the lpTimeProc parameter is interpreted as a handle to an event object. The event will be set or
 | 
			
		||||
    ///     pulsed upon completion of a single event or periodically upon completion of periodic events.
 | 
			
		||||
    ///     For any other value of fuEvent, the lpTimeProc parameter is a pointer to a callback function of type
 | 
			
		||||
    ///     LPTIMECALLBACK.
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="dwUser">User-supplied callback data.</param>
 | 
			
		||||
    /// <param name="fuEvent"></param>
 | 
			
		||||
    /// <returns>Timer event type. This parameter may include one of the following values.</returns>
 | 
			
		||||
    [DllImport("Winmm.dll")]
 | 
			
		||||
    private static extern uint timeSetEvent(
 | 
			
		||||
        uint uDelay,
 | 
			
		||||
        uint uResolution,
 | 
			
		||||
        LpTimeProcDelegate lpTimeProc,
 | 
			
		||||
        int dwUser,
 | 
			
		||||
        TimerMode fuEvent);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     The timeKillEvent function cancels a specified timer event.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="uTimerId">
 | 
			
		||||
    ///     Identifier of the timer event to cancel.
 | 
			
		||||
    ///     This identifier was returned by the timeSetEvent function when the timer event was set up.
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>Returns TIMERR_NOERROR if successful or MMSYSERR_INVALPARAM if the specified timer event does not exist.</returns>
 | 
			
		||||
    [DllImport("Winmm.dll")]
 | 
			
		||||
    private static extern int timeKillEvent(uint uTimerId);
 | 
			
		||||
 | 
			
		||||
    private void CallbackInternal(
 | 
			
		||||
        uint uTimerId,
 | 
			
		||||
        uint uMsg,
 | 
			
		||||
        int dwUser,
 | 
			
		||||
        int dw1,
 | 
			
		||||
        int dw2)
 | 
			
		||||
        => _callback(_state);
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        lpTimeProc = default;
 | 
			
		||||
        timeKillEvent(_eventId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private delegate void LpTimeProcDelegate(
 | 
			
		||||
        uint uTimerId,
 | 
			
		||||
        uint uMsg,
 | 
			
		||||
        int dwUser,
 | 
			
		||||
        int dw1,
 | 
			
		||||
        int dw2);
 | 
			
		||||
 | 
			
		||||
    private enum TimerMode
 | 
			
		||||
    {
 | 
			
		||||
        OneShot,
 | 
			
		||||
        Periodic
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,57 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public static class MusicExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static string PrettyTotalTime(this IMusicPlayer mp)
 | 
			
		||||
    {
 | 
			
		||||
        long sum = 0;
 | 
			
		||||
        foreach (var track in mp.GetQueuedTracks())
 | 
			
		||||
        {
 | 
			
		||||
            if (track.Duration == TimeSpan.MaxValue)
 | 
			
		||||
                return "∞";
 | 
			
		||||
 | 
			
		||||
            sum += track.Duration.Ticks;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var total = new TimeSpan(sum);
 | 
			
		||||
 | 
			
		||||
        return total.ToString(@"hh\:mm\:ss");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static string PrettyVolume(this IMusicPlayer mp)
 | 
			
		||||
        => $"🔉 {(int)(mp.Volume * 100)}%";
 | 
			
		||||
 | 
			
		||||
    public static string PrettyName(this ITrackInfo trackInfo)
 | 
			
		||||
        => $"**[{trackInfo.Title.TrimTo(60).Replace("[", "\\[").Replace("]", "\\]")}]({trackInfo.Url.TrimTo(50, true)})**";
 | 
			
		||||
 | 
			
		||||
    public static string PrettyInfo(this IQueuedTrackInfo trackInfo)
 | 
			
		||||
        => $"{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {trackInfo.Queuer}";
 | 
			
		||||
 | 
			
		||||
    public static string PrettyFullName(this IQueuedTrackInfo trackInfo)
 | 
			
		||||
        => $@"{trackInfo.PrettyName()}
 | 
			
		||||
		`{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {Format.Sanitize(trackInfo.Queuer.TrimTo(15))}`";
 | 
			
		||||
 | 
			
		||||
    public static string PrettyTotalTime(this ITrackInfo trackInfo)
 | 
			
		||||
    {
 | 
			
		||||
        if (trackInfo.Duration == TimeSpan.Zero)
 | 
			
		||||
            return "(?)";
 | 
			
		||||
        if (trackInfo.Duration == TimeSpan.MaxValue)
 | 
			
		||||
            return "∞";
 | 
			
		||||
        if (trackInfo.Duration.TotalHours >= 1)
 | 
			
		||||
            return trackInfo.Duration.ToString("""hh\:mm\:ss""");
 | 
			
		||||
 | 
			
		||||
        return trackInfo.Duration.ToString("""mm\:ss""");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ICachableTrackData ToCachedData(this ITrackInfo trackInfo, string id)
 | 
			
		||||
        => new CachableTrackData
 | 
			
		||||
        {
 | 
			
		||||
            TotalDurationMs = trackInfo.Duration.TotalMilliseconds,
 | 
			
		||||
            Id = id,
 | 
			
		||||
            Thumbnail = trackInfo.Thumbnail,
 | 
			
		||||
            Url = trackInfo.Url,
 | 
			
		||||
            Platform = trackInfo.Platform,
 | 
			
		||||
            Title = trackInfo.Title
 | 
			
		||||
        };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public enum MusicPlatform
 | 
			
		||||
{
 | 
			
		||||
    Radio,
 | 
			
		||||
    Youtube,
 | 
			
		||||
    Local,
 | 
			
		||||
    SoundCloud
 | 
			
		||||
}
 | 
			
		||||
@@ -1,529 +0,0 @@
 | 
			
		||||
using Ayu.Discord.Voice;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed class MusicPlayer : IMusicPlayer
 | 
			
		||||
{
 | 
			
		||||
    public event Func<IMusicPlayer, IQueuedTrackInfo, Task>? OnCompleted;
 | 
			
		||||
    public event Func<IMusicPlayer, IQueuedTrackInfo, int, Task>? OnStarted;
 | 
			
		||||
    public event Func<IMusicPlayer, Task>? OnQueueStopped;
 | 
			
		||||
    public bool IsKilled { get; private set; }
 | 
			
		||||
    public bool IsStopped { get; private set; }
 | 
			
		||||
    public bool IsPaused { get; private set; }
 | 
			
		||||
    public PlayerRepeatType Repeat { get; private set; }
 | 
			
		||||
 | 
			
		||||
    public int CurrentIndex
 | 
			
		||||
        => _queue.Index;
 | 
			
		||||
 | 
			
		||||
    public float Volume { get; private set; } = 1.0f;
 | 
			
		||||
 | 
			
		||||
    private readonly AdjustVolumeDelegate _adjustVolume;
 | 
			
		||||
    private readonly VoiceClient _vc;
 | 
			
		||||
 | 
			
		||||
    private readonly IMusicQueue _queue;
 | 
			
		||||
    private readonly ITrackResolveProvider _trackResolveProvider;
 | 
			
		||||
    private readonly IVoiceProxy _proxy;
 | 
			
		||||
    private readonly IGoogleApiService _googleApiService;
 | 
			
		||||
    private readonly ISongBuffer _songBuffer;
 | 
			
		||||
 | 
			
		||||
    private bool skipped;
 | 
			
		||||
    private int? forceIndex;
 | 
			
		||||
    private readonly Thread _thread;
 | 
			
		||||
    private readonly Random _rng;
 | 
			
		||||
 | 
			
		||||
    public bool AutoPlay { get; set; }
 | 
			
		||||
 | 
			
		||||
    public MusicPlayer(
 | 
			
		||||
        IMusicQueue queue,
 | 
			
		||||
        ITrackResolveProvider trackResolveProvider,
 | 
			
		||||
        IVoiceProxy proxy,
 | 
			
		||||
        IGoogleApiService googleApiService,
 | 
			
		||||
        QualityPreset qualityPreset,
 | 
			
		||||
        bool autoPlay)
 | 
			
		||||
    {
 | 
			
		||||
        _queue = queue;
 | 
			
		||||
        _trackResolveProvider = trackResolveProvider;
 | 
			
		||||
        _proxy = proxy;
 | 
			
		||||
        _googleApiService = googleApiService;
 | 
			
		||||
        AutoPlay = autoPlay;
 | 
			
		||||
        _rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
        _vc = GetVoiceClient(qualityPreset);
 | 
			
		||||
        if (_vc.BitDepth == 16)
 | 
			
		||||
            _adjustVolume = AdjustVolumeInt16;
 | 
			
		||||
        else
 | 
			
		||||
            _adjustVolume = AdjustVolumeFloat32;
 | 
			
		||||
 | 
			
		||||
        _songBuffer = new PoopyBufferImmortalized(_vc.InputLength);
 | 
			
		||||
 | 
			
		||||
        _thread = new(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            await PlayLoop();
 | 
			
		||||
        });
 | 
			
		||||
        _thread.Start();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static VoiceClient GetVoiceClient(QualityPreset qualityPreset)
 | 
			
		||||
        => qualityPreset switch
 | 
			
		||||
        {
 | 
			
		||||
            QualityPreset.Highest => new(),
 | 
			
		||||
            QualityPreset.High => new(SampleRate._48k, Bitrate._128k, Channels.Two, FrameDelay.Delay40),
 | 
			
		||||
            QualityPreset.Medium => new(SampleRate._48k,
 | 
			
		||||
                Bitrate._96k,
 | 
			
		||||
                Channels.Two,
 | 
			
		||||
                FrameDelay.Delay40,
 | 
			
		||||
                BitDepthEnum.UInt16),
 | 
			
		||||
            QualityPreset.Low => new(SampleRate._48k,
 | 
			
		||||
                Bitrate._64k,
 | 
			
		||||
                Channels.Two,
 | 
			
		||||
                FrameDelay.Delay40,
 | 
			
		||||
                BitDepthEnum.UInt16),
 | 
			
		||||
            _ => throw new ArgumentOutOfRangeException(nameof(qualityPreset), qualityPreset, null)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    private async Task PlayLoop()
 | 
			
		||||
    {
 | 
			
		||||
        var sw = new Stopwatch();
 | 
			
		||||
 | 
			
		||||
        while (!IsKilled)
 | 
			
		||||
        {
 | 
			
		||||
            // wait until a song is available in the queue
 | 
			
		||||
            // or until the queue is resumed
 | 
			
		||||
            var track = _queue.GetCurrent(out var index);
 | 
			
		||||
 | 
			
		||||
            if (track is null || IsStopped)
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(500);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (skipped)
 | 
			
		||||
            {
 | 
			
		||||
                skipped = false;
 | 
			
		||||
                _queue.Advance();
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using var cancellationTokenSource = new CancellationTokenSource();
 | 
			
		||||
            var token = cancellationTokenSource.Token;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                // light up green in vc
 | 
			
		||||
                _ = _proxy.StartSpeakingAsync();
 | 
			
		||||
 | 
			
		||||
                _ = OnStarted?.Invoke(this, track, index);
 | 
			
		||||
 | 
			
		||||
                // make sure song buffer is ready to be (re)used
 | 
			
		||||
                _songBuffer.Reset();
 | 
			
		||||
 | 
			
		||||
                var streamUrl = await track.GetStreamUrl();
 | 
			
		||||
                // start up the data source
 | 
			
		||||
                using var source = FfmpegTrackDataSource.CreateAsync(
 | 
			
		||||
                    _vc.BitDepth,
 | 
			
		||||
                    streamUrl,
 | 
			
		||||
                    track.Platform == MusicPlatform.Local);
 | 
			
		||||
 | 
			
		||||
                // start moving data from the source into the buffer
 | 
			
		||||
                // this method will return once the sufficient prebuffering is done
 | 
			
		||||
                await _songBuffer.BufferAsync(source, token);
 | 
			
		||||
 | 
			
		||||
                // // Implemenation with multimedia timer. Works but a hassle because no support for switching
 | 
			
		||||
                // // vcs, as any error in copying will cancel the song. Also no idea how to use this as an option
 | 
			
		||||
                // // for selfhosters.
 | 
			
		||||
                // if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 | 
			
		||||
                // {
 | 
			
		||||
                //     var cancelSource = new CancellationTokenSource();
 | 
			
		||||
                //     var cancelToken = cancelSource.Token;
 | 
			
		||||
                //     using var timer = new MultimediaTimer(_ =>
 | 
			
		||||
                //     {
 | 
			
		||||
                //         if (IsStopped || IsKilled)
 | 
			
		||||
                //         {
 | 
			
		||||
                //             cancelSource.Cancel();
 | 
			
		||||
                //             return;
 | 
			
		||||
                //         }
 | 
			
		||||
                //         
 | 
			
		||||
                //         if (_skipped)
 | 
			
		||||
                //         {
 | 
			
		||||
                //             _skipped = false;
 | 
			
		||||
                //             cancelSource.Cancel();
 | 
			
		||||
                //             return;
 | 
			
		||||
                //         }
 | 
			
		||||
                //
 | 
			
		||||
                //         if (IsPaused)
 | 
			
		||||
                //             return;
 | 
			
		||||
                //
 | 
			
		||||
                //         try
 | 
			
		||||
                //         {
 | 
			
		||||
                //             // this should tolerate certain number of errors
 | 
			
		||||
                //             var result = CopyChunkToOutput(_songBuffer, _vc);
 | 
			
		||||
                //             if (!result)
 | 
			
		||||
                //                 cancelSource.Cancel();
 | 
			
		||||
                //               
 | 
			
		||||
                //         }
 | 
			
		||||
                //         catch (Exception ex)
 | 
			
		||||
                //         {
 | 
			
		||||
                //             Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message);
 | 
			
		||||
                //             cancelSource.Cancel();
 | 
			
		||||
                //         }
 | 
			
		||||
                //
 | 
			
		||||
                //     }, null, 20);
 | 
			
		||||
                //     
 | 
			
		||||
                //     while(true)
 | 
			
		||||
                //         await Task.Delay(1000, cancelToken);
 | 
			
		||||
                // }
 | 
			
		||||
 | 
			
		||||
                // start sending data
 | 
			
		||||
                var ticksPerMs = 1000f / Stopwatch.Frequency;
 | 
			
		||||
                sw.Start();
 | 
			
		||||
                Thread.Sleep(2);
 | 
			
		||||
 | 
			
		||||
                var delay = sw.ElapsedTicks * ticksPerMs > 3f ? _vc.Delay - 16 : _vc.Delay - 3;
 | 
			
		||||
 | 
			
		||||
                var errorCount = 0;
 | 
			
		||||
                while (!IsStopped && !IsKilled)
 | 
			
		||||
                {
 | 
			
		||||
                    // doing the skip this way instead of in the condition
 | 
			
		||||
                    // ensures that a song will for sure be skipped
 | 
			
		||||
                    if (skipped)
 | 
			
		||||
                    {
 | 
			
		||||
                        skipped = false;
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (IsPaused)
 | 
			
		||||
                    {
 | 
			
		||||
                        await Task.Delay(200);
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    sw.Restart();
 | 
			
		||||
                    var ticks = sw.ElapsedTicks;
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        var result = CopyChunkToOutput(_songBuffer, _vc);
 | 
			
		||||
 | 
			
		||||
                        // if song is finished
 | 
			
		||||
                        if (result is null)
 | 
			
		||||
                            break;
 | 
			
		||||
 | 
			
		||||
                        if (result is true)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (errorCount > 0)
 | 
			
		||||
                            {
 | 
			
		||||
                                _ = _proxy.StartSpeakingAsync();
 | 
			
		||||
                                errorCount = 0;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // FUTURE windows multimedia api
 | 
			
		||||
 | 
			
		||||
                            // wait for slightly less than the latency
 | 
			
		||||
                            Thread.Sleep(delay);
 | 
			
		||||
 | 
			
		||||
                            // and then spin out the rest
 | 
			
		||||
                            while ((sw.ElapsedTicks - ticks) * ticksPerMs <= _vc.Delay - 0.1f)
 | 
			
		||||
                                Thread.SpinWait(100);
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            // result is false is either when the gateway is being swapped 
 | 
			
		||||
                            // or if the bot is reconnecting, or just disconnected for whatever reason
 | 
			
		||||
 | 
			
		||||
                            // tolerate up to 15x200ms of failures (3 seconds)
 | 
			
		||||
                            if (++errorCount <= 15)
 | 
			
		||||
                            {
 | 
			
		||||
                                await Task.Delay(200);
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            Log.Warning("Can't send data to voice channel");
 | 
			
		||||
 | 
			
		||||
                            IsStopped = true;
 | 
			
		||||
                            // if errors are happening for more than 3 seconds
 | 
			
		||||
                            // Stop the player
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Win32Exception)
 | 
			
		||||
            {
 | 
			
		||||
                IsStopped = true;
 | 
			
		||||
                Log.Error("Please install ffmpeg and make sure it's added to your "
 | 
			
		||||
                          + "PATH environment variable before trying again");
 | 
			
		||||
            }
 | 
			
		||||
            catch (OperationCanceledException)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("Song skipped");
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Unknown error in music loop: {ErrorMessage}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                cancellationTokenSource.Cancel();
 | 
			
		||||
                // turn off green in vc
 | 
			
		||||
 | 
			
		||||
                _ = OnCompleted?.Invoke(this, track);
 | 
			
		||||
                
 | 
			
		||||
                if (AutoPlay && track.Platform == MusicPlatform.Youtube)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        var relatedSongs = await _googleApiService.GetRelatedVideosAsync(track.TrackInfo.Id, 5);
 | 
			
		||||
                        var related = relatedSongs.Shuffle().FirstOrDefault();
 | 
			
		||||
                        if (related is not null)
 | 
			
		||||
                        {
 | 
			
		||||
                            var relatedTrack = await _trackResolveProvider.QuerySongAsync(related, MusicPlatform.Youtube);
 | 
			
		||||
                            if (relatedTrack is not null)
 | 
			
		||||
                                EnqueueTrack(relatedTrack, "Autoplay");
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex, "Failed queueing a related song via autoplay");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                HandleQueuePostTrack();
 | 
			
		||||
                skipped = false;
 | 
			
		||||
 | 
			
		||||
                _ = _proxy.StopSpeakingAsync();
 | 
			
		||||
 | 
			
		||||
                await Task.Delay(100);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private bool? CopyChunkToOutput(ISongBuffer sb, VoiceClient vc)
 | 
			
		||||
    {
 | 
			
		||||
        var data = sb.Read(vc.InputLength, out var length);
 | 
			
		||||
 | 
			
		||||
        // if nothing is read from the buffer, song is finished
 | 
			
		||||
        if (data.Length == 0)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        _adjustVolume(data, Volume);
 | 
			
		||||
        return _proxy.SendPcmFrame(vc, data, length);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void HandleQueuePostTrack()
 | 
			
		||||
    {
 | 
			
		||||
        if (forceIndex is { } index)
 | 
			
		||||
        {
 | 
			
		||||
            _queue.SetIndex(index);
 | 
			
		||||
            forceIndex = null;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var (repeat, isStopped) = (Repeat, IsStopped);
 | 
			
		||||
 | 
			
		||||
        if (repeat == PlayerRepeatType.Track || isStopped)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // if queue is being repeated, advance no matter what
 | 
			
		||||
        if (repeat == PlayerRepeatType.None)
 | 
			
		||||
        {
 | 
			
		||||
            // if this is the last song,
 | 
			
		||||
            // stop the queue
 | 
			
		||||
            if (_queue.IsLast())
 | 
			
		||||
            {
 | 
			
		||||
                IsStopped = true;
 | 
			
		||||
                OnQueueStopped?.Invoke(this);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _queue.Advance();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _queue.Advance();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    private static void AdjustVolumeInt16(Span<byte> audioSamples, float volume)
 | 
			
		||||
    {
 | 
			
		||||
        if (Math.Abs(volume - 1f) < 0.0001f)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var samples = MemoryMarshal.Cast<byte, short>(audioSamples);
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < samples.Length; i++)
 | 
			
		||||
        {
 | 
			
		||||
            ref var sample = ref samples[i];
 | 
			
		||||
            sample = (short)(sample * volume);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    private static void AdjustVolumeFloat32(Span<byte> audioSamples, float volume)
 | 
			
		||||
    {
 | 
			
		||||
        if (Math.Abs(volume - 1f) < 0.0001f)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var samples = MemoryMarshal.Cast<byte, float>(audioSamples);
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < samples.Length; i++)
 | 
			
		||||
        {
 | 
			
		||||
            ref var sample = ref samples[i];
 | 
			
		||||
            sample *= volume;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(
 | 
			
		||||
        string query,
 | 
			
		||||
        string queuer,
 | 
			
		||||
        bool asNext,
 | 
			
		||||
        MusicPlatform? forcePlatform = null)
 | 
			
		||||
    {
 | 
			
		||||
        var song = await _trackResolveProvider.QuerySongAsync(query, forcePlatform);
 | 
			
		||||
        if (song is null)
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        int index;
 | 
			
		||||
 | 
			
		||||
        if (asNext)
 | 
			
		||||
            return (_queue.EnqueueNext(song, queuer, out index), index);
 | 
			
		||||
 | 
			
		||||
        return (_queue.Enqueue(song, queuer, out index), index);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer)
 | 
			
		||||
    {
 | 
			
		||||
        var errorCount = 0;
 | 
			
		||||
        foreach (var chunk in queries.Chunk(5))
 | 
			
		||||
        {
 | 
			
		||||
            if (IsKilled)
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            await chunk.Select(async data =>
 | 
			
		||||
                       {
 | 
			
		||||
                           var (query, platform) = data;
 | 
			
		||||
                           try
 | 
			
		||||
                           {
 | 
			
		||||
                               await TryEnqueueTrackAsync(query, queuer, false, platform);
 | 
			
		||||
                               errorCount = 0;
 | 
			
		||||
                           }
 | 
			
		||||
                           catch (Exception ex)
 | 
			
		||||
                           {
 | 
			
		||||
                               Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query);
 | 
			
		||||
                               ++errorCount;
 | 
			
		||||
                           }
 | 
			
		||||
                       })
 | 
			
		||||
                       .WhenAll();
 | 
			
		||||
 | 
			
		||||
            await Task.Delay(1000);
 | 
			
		||||
 | 
			
		||||
            // > 10 errors in a row = kill
 | 
			
		||||
            if (errorCount > 10)
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void EnqueueTrack(ITrackInfo track, string queuer)
 | 
			
		||||
        => _queue.Enqueue(track, queuer, out _);
 | 
			
		||||
 | 
			
		||||
    public void EnqueueTracks(IEnumerable<ITrackInfo> tracks, string queuer)
 | 
			
		||||
        => _queue.EnqueueMany(tracks, queuer);
 | 
			
		||||
 | 
			
		||||
    public void SetRepeat(PlayerRepeatType type)
 | 
			
		||||
        => Repeat = type;
 | 
			
		||||
 | 
			
		||||
    public void ShuffleQueue()
 | 
			
		||||
        => _queue.Shuffle(_rng);
 | 
			
		||||
 | 
			
		||||
    public void Stop()
 | 
			
		||||
        => IsStopped = true;
 | 
			
		||||
 | 
			
		||||
    public void Clear()
 | 
			
		||||
    {
 | 
			
		||||
        _queue.Clear();
 | 
			
		||||
        skipped = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyCollection<IQueuedTrackInfo> GetQueuedTracks()
 | 
			
		||||
        => _queue.List();
 | 
			
		||||
 | 
			
		||||
    public IQueuedTrackInfo? GetCurrentTrack(out int index)
 | 
			
		||||
        => _queue.GetCurrent(out index);
 | 
			
		||||
 | 
			
		||||
    public void Next()
 | 
			
		||||
    {
 | 
			
		||||
        skipped = true;
 | 
			
		||||
        IsStopped = false;
 | 
			
		||||
        IsPaused = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool MoveTo(int index)
 | 
			
		||||
    {
 | 
			
		||||
        if (_queue.SetIndex(index))
 | 
			
		||||
        {
 | 
			
		||||
            forceIndex = index;
 | 
			
		||||
            skipped = true;
 | 
			
		||||
            IsStopped = false;
 | 
			
		||||
            IsPaused = false;
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void SetVolume(int newVolume)
 | 
			
		||||
    {
 | 
			
		||||
        var normalizedVolume = newVolume / 100f;
 | 
			
		||||
        if (normalizedVolume is < 0f or > 1f)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(newVolume), "Volume must be in range 0-100");
 | 
			
		||||
 | 
			
		||||
        Volume = normalizedVolume;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Kill()
 | 
			
		||||
    {
 | 
			
		||||
        IsKilled = true;
 | 
			
		||||
        IsStopped = true;
 | 
			
		||||
        IsPaused = false;
 | 
			
		||||
        skipped = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_queue.TryRemoveAt(index, out trackInfo, out var isCurrent))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (isCurrent)
 | 
			
		||||
            skipped = true;
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TogglePause()
 | 
			
		||||
        => IsPaused = !IsPaused;
 | 
			
		||||
 | 
			
		||||
    public IQueuedTrackInfo? MoveTrack(int from, int to)
 | 
			
		||||
        => _queue.MoveTrack(from, to);
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        IsKilled = true;
 | 
			
		||||
        OnCompleted = null;
 | 
			
		||||
        OnStarted = null;
 | 
			
		||||
        OnQueueStopped = null;
 | 
			
		||||
        _queue.Clear();
 | 
			
		||||
        _songBuffer.Dispose();
 | 
			
		||||
        _vc.Dispose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private delegate void AdjustVolumeDelegate(Span<byte> data, float volume);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,316 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed partial class MusicQueue
 | 
			
		||||
{
 | 
			
		||||
    private sealed class QueuedTrackInfo : IQueuedTrackInfo
 | 
			
		||||
    {
 | 
			
		||||
        public ITrackInfo TrackInfo { get; }
 | 
			
		||||
        public string Queuer { get; }
 | 
			
		||||
 | 
			
		||||
        public string Title
 | 
			
		||||
            => TrackInfo.Title;
 | 
			
		||||
 | 
			
		||||
        public string Url
 | 
			
		||||
            => TrackInfo.Url;
 | 
			
		||||
 | 
			
		||||
        public string Thumbnail
 | 
			
		||||
            => TrackInfo.Thumbnail;
 | 
			
		||||
 | 
			
		||||
        public TimeSpan Duration
 | 
			
		||||
            => TrackInfo.Duration;
 | 
			
		||||
 | 
			
		||||
        public MusicPlatform Platform
 | 
			
		||||
            => TrackInfo.Platform;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        public QueuedTrackInfo(ITrackInfo trackInfo, string queuer)
 | 
			
		||||
        {
 | 
			
		||||
            TrackInfo = trackInfo;
 | 
			
		||||
            Queuer = queuer;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public ValueTask<string?> GetStreamUrl()
 | 
			
		||||
            => TrackInfo.GetStreamUrl();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public sealed partial class MusicQueue : IMusicQueue
 | 
			
		||||
{
 | 
			
		||||
    public int Index
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            // just make sure the internal logic runs first
 | 
			
		||||
            // to make sure that some potential indermediate value is not returned
 | 
			
		||||
            lock (_locker)
 | 
			
		||||
            {
 | 
			
		||||
                return index;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int Count
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            lock (_locker)
 | 
			
		||||
            {
 | 
			
		||||
                return tracks.Count;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private LinkedList<QueuedTrackInfo> tracks;
 | 
			
		||||
 | 
			
		||||
    private int index;
 | 
			
		||||
 | 
			
		||||
    private readonly object _locker = new();
 | 
			
		||||
 | 
			
		||||
    public MusicQueue()
 | 
			
		||||
    {
 | 
			
		||||
        index = 0;
 | 
			
		||||
        tracks = new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int enqueuedAt)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            var added = new QueuedTrackInfo(trackInfo, queuer);
 | 
			
		||||
            enqueuedAt = tracks.Count;
 | 
			
		||||
            tracks.AddLast(added);
 | 
			
		||||
            return added;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IQueuedTrackInfo EnqueueNext(ITrackInfo trackInfo, string queuer, out int trackIndex)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (tracks.Count == 0)
 | 
			
		||||
                return Enqueue(trackInfo, queuer, out trackIndex);
 | 
			
		||||
 | 
			
		||||
            var currentNode = tracks.First!;
 | 
			
		||||
            int i;
 | 
			
		||||
            for (i = 1; i <= index; i++)
 | 
			
		||||
                currentNode = currentNode.Next!; // can't be null because index is always in range of the count
 | 
			
		||||
 | 
			
		||||
            var added = new QueuedTrackInfo(trackInfo, queuer);
 | 
			
		||||
            trackIndex = i;
 | 
			
		||||
 | 
			
		||||
            tracks.AddAfter(currentNode, added);
 | 
			
		||||
 | 
			
		||||
            return added;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void EnqueueMany(IEnumerable<ITrackInfo> toEnqueue, string queuer)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var track in toEnqueue)
 | 
			
		||||
            {
 | 
			
		||||
                var added = new QueuedTrackInfo(track, queuer);
 | 
			
		||||
                tracks.AddLast(added);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyCollection<IQueuedTrackInfo> List()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            return tracks.ToList();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IQueuedTrackInfo? GetCurrent(out int currentIndex)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            currentIndex = index;
 | 
			
		||||
            return tracks.ElementAtOrDefault(index);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Advance()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (++index >= tracks.Count)
 | 
			
		||||
                index = 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Clear()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            tracks.Clear();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool SetIndex(int newIndex)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (newIndex < 0 || newIndex >= tracks.Count)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            index = newIndex;
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void RemoveAtInternal(int remoteAtIndex, out IQueuedTrackInfo trackInfo)
 | 
			
		||||
    {
 | 
			
		||||
        var removedNode = tracks.First!;
 | 
			
		||||
        int i;
 | 
			
		||||
        for (i = 0; i < remoteAtIndex; i++)
 | 
			
		||||
            removedNode = removedNode.Next!;
 | 
			
		||||
 | 
			
		||||
        trackInfo = removedNode.Value;
 | 
			
		||||
        tracks.Remove(removedNode);
 | 
			
		||||
 | 
			
		||||
        if (i <= index)
 | 
			
		||||
            --index;
 | 
			
		||||
 | 
			
		||||
        if (index < 0)
 | 
			
		||||
            index = Count;
 | 
			
		||||
 | 
			
		||||
        // if it was the last song in the queue
 | 
			
		||||
        // // wrap back to start
 | 
			
		||||
        // if (_index == Count)
 | 
			
		||||
        //     _index = 0;
 | 
			
		||||
        // else if (i <= _index)
 | 
			
		||||
        //     if (_index == 0)
 | 
			
		||||
        //         _index = Count;
 | 
			
		||||
        //     else --_index;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void RemoveCurrent()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (index < tracks.Count)
 | 
			
		||||
                RemoveAtInternal(index, out _);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IQueuedTrackInfo? MoveTrack(int from, int to)
 | 
			
		||||
    {
 | 
			
		||||
        if (from < 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(from));
 | 
			
		||||
        if (to < 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(to));
 | 
			
		||||
        if (to == from)
 | 
			
		||||
            throw new ArgumentException($"{nameof(from)} and {nameof(to)} must be different");
 | 
			
		||||
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (from >= Count || to >= Count)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            // update current track index
 | 
			
		||||
            if (from == index)
 | 
			
		||||
            {
 | 
			
		||||
                // if the song being moved is the current track
 | 
			
		||||
                // it means that it will for sure end up on the destination
 | 
			
		||||
                index = to;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // moving a track from below the current track means 
 | 
			
		||||
                // means it will drop down
 | 
			
		||||
                if (from < index)
 | 
			
		||||
                    index--;
 | 
			
		||||
 | 
			
		||||
                // moving a track to below the current track
 | 
			
		||||
                // means it will rise up
 | 
			
		||||
                if (to <= index)
 | 
			
		||||
                    index++;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                // if both from and to are below _index - net change is + 1 - 1 = 0
 | 
			
		||||
                // if from is below and to is above - net change is -1 (as the track is taken and put above)
 | 
			
		||||
                // if from is above and to is below - net change is 1 (as the track is inserted under)
 | 
			
		||||
                // if from is above and to is above - net change is 0
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // get the node which needs to be moved
 | 
			
		||||
            var fromNode = tracks.First!;
 | 
			
		||||
            for (var i = 0; i < from; i++)
 | 
			
		||||
                fromNode = fromNode.Next!;
 | 
			
		||||
 | 
			
		||||
            // remove it from the queue
 | 
			
		||||
            tracks.Remove(fromNode);
 | 
			
		||||
 | 
			
		||||
            // if it needs to be added as a first node,
 | 
			
		||||
            // add it directly and return
 | 
			
		||||
            if (to == 0)
 | 
			
		||||
            {
 | 
			
		||||
                tracks.AddFirst(fromNode);
 | 
			
		||||
                return fromNode.Value;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // else find the node at the index before the specified target
 | 
			
		||||
            var addAfterNode = tracks.First!;
 | 
			
		||||
            for (var i = 1; i < to; i++)
 | 
			
		||||
                addAfterNode = addAfterNode.Next!;
 | 
			
		||||
 | 
			
		||||
            // and add after it
 | 
			
		||||
            tracks.AddAfter(addAfterNode, fromNode);
 | 
			
		||||
            return fromNode.Value;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Shuffle(Random rng)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            var list = tracks.ToList();
 | 
			
		||||
 | 
			
		||||
            for (var i = 0; i < list.Count; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var struck = rng.Next(i, list.Count);
 | 
			
		||||
                (list[struck], list[i]) = (list[i], list[struck]);
 | 
			
		||||
 | 
			
		||||
                // could preserving the index during shuffling be done better?
 | 
			
		||||
                if (i == index)
 | 
			
		||||
                    index = struck;
 | 
			
		||||
                else if (struck == index)
 | 
			
		||||
                    index = i;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            tracks = new(list);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool IsLast()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            return index == tracks.Count // if there are no tracks
 | 
			
		||||
                   || index == tracks.Count - 1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryRemoveAt(int remoteAt, out IQueuedTrackInfo? trackInfo, out bool isCurrent)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            isCurrent = false;
 | 
			
		||||
            trackInfo = null;
 | 
			
		||||
 | 
			
		||||
            if (remoteAt < 0 || remoteAt >= tracks.Count)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            if (remoteAt == index)
 | 
			
		||||
                isCurrent = true;
 | 
			
		||||
 | 
			
		||||
            RemoveAtInternal(remoteAt, out trackInfo);
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed record RemoteTrackInfo(
 | 
			
		||||
    string Id,
 | 
			
		||||
    string Title,
 | 
			
		||||
    string Url,
 | 
			
		||||
    string Thumbnail,
 | 
			
		||||
    TimeSpan Duration,
 | 
			
		||||
    MusicPlatform Platform,
 | 
			
		||||
    Func<Task<string?>> _streamFactory) : ITrackInfo
 | 
			
		||||
{
 | 
			
		||||
    private readonly Func<Task<string?>> _streamFactory = _streamFactory;
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<string?> GetStreamUrl()
 | 
			
		||||
        => await _streamFactory();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed class SimpleTrackInfo : ITrackInfo
 | 
			
		||||
{
 | 
			
		||||
    public string Title { get; }
 | 
			
		||||
    public string Url { get; }
 | 
			
		||||
    public string Thumbnail { get; }
 | 
			
		||||
    public TimeSpan Duration { get; }
 | 
			
		||||
    public MusicPlatform Platform { get; }
 | 
			
		||||
    public string? StreamUrl { get; }
 | 
			
		||||
 | 
			
		||||
    public SimpleTrackInfo(
 | 
			
		||||
        string title,
 | 
			
		||||
        string url,
 | 
			
		||||
        string thumbnail,
 | 
			
		||||
        TimeSpan duration,
 | 
			
		||||
        MusicPlatform platform,
 | 
			
		||||
        string streamUrl)
 | 
			
		||||
    {
 | 
			
		||||
        Title = title;
 | 
			
		||||
        Url = url;
 | 
			
		||||
        Thumbnail = thumbnail;
 | 
			
		||||
        Duration = duration;
 | 
			
		||||
        Platform = platform;
 | 
			
		||||
        StreamUrl = streamUrl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ValueTask<string?> GetStreamUrl()
 | 
			
		||||
        => new(StreamUrl);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,105 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed class TrackCacher : ITrackCacher
 | 
			
		||||
{
 | 
			
		||||
    private readonly IBotCache _cache;
 | 
			
		||||
 | 
			
		||||
    public TrackCacher(IBotCache cache)
 | 
			
		||||
        => _cache = cache;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private TypedKey<string> GetStreamLinkKey(MusicPlatform platform, string id)
 | 
			
		||||
        => new($"music:stream:{platform}:{id}");
 | 
			
		||||
    
 | 
			
		||||
    public async Task<string?> GetOrCreateStreamLink(
 | 
			
		||||
        string id,
 | 
			
		||||
        MusicPlatform platform,
 | 
			
		||||
        Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory)
 | 
			
		||||
    {
 | 
			
		||||
        var key = GetStreamLinkKey(platform, id);
 | 
			
		||||
 | 
			
		||||
        var streamUrl = await _cache.GetOrDefaultAsync(key);
 | 
			
		||||
        await _cache.RemoveAsync(key);
 | 
			
		||||
        
 | 
			
		||||
        if (streamUrl == default)
 | 
			
		||||
        {
 | 
			
		||||
            (streamUrl, _) = await streamUrlFactory();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // make a new one for later use
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            (streamUrl, var expiry) = await streamUrlFactory();
 | 
			
		||||
            await CacheStreamUrlAsync(id, platform, streamUrl, expiry);
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        return streamUrl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task CacheStreamUrlAsync(
 | 
			
		||||
        string id,
 | 
			
		||||
        MusicPlatform platform,
 | 
			
		||||
        string url,
 | 
			
		||||
        TimeSpan expiry)
 | 
			
		||||
        => await _cache.AddAsync(GetStreamLinkKey(platform, id), url, expiry);
 | 
			
		||||
 | 
			
		||||
    // track data by id
 | 
			
		||||
    private TypedKey<CachableTrackData> GetTrackDataKey(MusicPlatform platform, string id)
 | 
			
		||||
        => new($"music:track:{platform}:{id}");
 | 
			
		||||
    public async Task CacheTrackDataAsync(ICachableTrackData data)
 | 
			
		||||
        => await _cache.AddAsync(GetTrackDataKey(data.Platform, data.Id), ToCachableTrackData(data));
 | 
			
		||||
 | 
			
		||||
    private CachableTrackData ToCachableTrackData(ICachableTrackData data)
 | 
			
		||||
        => new CachableTrackData()
 | 
			
		||||
        {
 | 
			
		||||
            Id = data.Id,
 | 
			
		||||
            Platform = data.Platform,
 | 
			
		||||
            Thumbnail = data.Thumbnail,
 | 
			
		||||
            Title = data.Title,
 | 
			
		||||
            Url = data.Url,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    public async Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform)
 | 
			
		||||
        => await _cache.GetOrDefaultAsync(GetTrackDataKey(platform, id)); 
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    // track data by query
 | 
			
		||||
    private TypedKey<CachableTrackData> GetTrackDataQueryKey(MusicPlatform platform, string query)
 | 
			
		||||
        => new($"music:track:{platform}:q:{query}");
 | 
			
		||||
 | 
			
		||||
    public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data)
 | 
			
		||||
        => await Task.WhenAll(
 | 
			
		||||
            _cache.AddAsync(GetTrackDataQueryKey(data.Platform, query), ToCachableTrackData(data)).AsTask(),
 | 
			
		||||
            _cache.AddAsync(GetTrackDataKey(data.Platform, data.Id), ToCachableTrackData(data)).AsTask());
 | 
			
		||||
    
 | 
			
		||||
    public async Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform)
 | 
			
		||||
        => await _cache.GetOrDefaultAsync(GetTrackDataQueryKey(platform, query));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // playlist track ids by playlist id
 | 
			
		||||
    private TypedKey<List<string>> GetPlaylistTracksCacheKey(string playlist, MusicPlatform platform)
 | 
			
		||||
        => new($"music:playlist_tracks:{platform}:{playlist}");
 | 
			
		||||
 | 
			
		||||
    public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids)
 | 
			
		||||
        => await _cache.AddAsync(GetPlaylistTracksCacheKey(playlistId, platform), ids.ToList());
 | 
			
		||||
 | 
			
		||||
    public async Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform)
 | 
			
		||||
    {
 | 
			
		||||
        var result = await _cache.GetAsync(GetPlaylistTracksCacheKey(playlistId, platform));
 | 
			
		||||
        if (result.TryGetValue(out var val))
 | 
			
		||||
            return val;
 | 
			
		||||
 | 
			
		||||
        return Array.Empty<string>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // playlist id by query
 | 
			
		||||
    private TypedKey<string> GetPlaylistCacheKey(string query, MusicPlatform platform)
 | 
			
		||||
        => new($"music:playlist_id:{platform}:{query}");
 | 
			
		||||
    
 | 
			
		||||
    public async Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId)
 | 
			
		||||
        => await _cache.AddAsync(GetPlaylistCacheKey(query, platform), playlistId);
 | 
			
		||||
 | 
			
		||||
    public async Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform)
 | 
			
		||||
        => await _cache.GetOrDefaultAsync(GetPlaylistCacheKey(query, platform));
 | 
			
		||||
}
 | 
			
		||||
@@ -1,102 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Ayu.Discord.Voice;
 | 
			
		||||
using Ayu.Discord.Voice.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed class VoiceProxy : IVoiceProxy
 | 
			
		||||
{
 | 
			
		||||
    public enum VoiceProxyState
 | 
			
		||||
    {
 | 
			
		||||
        Created,
 | 
			
		||||
        Started,
 | 
			
		||||
        Stopped
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private const int MAX_ERROR_COUNT = 20;
 | 
			
		||||
    private const int DELAY_ON_ERROR_MILISECONDS = 200;
 | 
			
		||||
 | 
			
		||||
    public VoiceProxyState State
 | 
			
		||||
        => gateway switch
 | 
			
		||||
        {
 | 
			
		||||
            { Started: true, Stopped: false } => VoiceProxyState.Started,
 | 
			
		||||
            { Stopped: false } => VoiceProxyState.Created,
 | 
			
		||||
            _ => VoiceProxyState.Stopped
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private VoiceGateway gateway;
 | 
			
		||||
 | 
			
		||||
    public VoiceProxy(VoiceGateway initial)
 | 
			
		||||
        => gateway = initial;
 | 
			
		||||
 | 
			
		||||
    public bool SendPcmFrame(VoiceClient vc, Span<byte> data, int length)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var gw = gateway;
 | 
			
		||||
            if (gw is null || gw.Stopped || !gw.Started)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            vc.SendPcmFrame(gw, data, 0, length);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception)
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunGatewayAction(Func<VoiceGateway, Task> action)
 | 
			
		||||
    {
 | 
			
		||||
        var errorCount = 0;
 | 
			
		||||
        do
 | 
			
		||||
        {
 | 
			
		||||
            if (State == VoiceProxyState.Stopped)
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var gw = gateway;
 | 
			
		||||
                if (gw is null || !gw.ConnectingFinished.Task.IsCompleted)
 | 
			
		||||
                {
 | 
			
		||||
                    ++errorCount;
 | 
			
		||||
                    await Task.Delay(DELAY_ON_ERROR_MILISECONDS);
 | 
			
		||||
                    Log.Debug("Gateway is not ready");
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await action(gw);
 | 
			
		||||
                errorCount = 0;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                ++errorCount;
 | 
			
		||||
                await Task.Delay(DELAY_ON_ERROR_MILISECONDS);
 | 
			
		||||
                Log.Debug(ex, "Error performing proxy gateway action");
 | 
			
		||||
            }
 | 
			
		||||
        } while (errorCount is > 0 and <= MAX_ERROR_COUNT);
 | 
			
		||||
 | 
			
		||||
        return State != VoiceProxyState.Stopped && errorCount <= MAX_ERROR_COUNT;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void SetGateway(VoiceGateway newGateway)
 | 
			
		||||
        => gateway = newGateway;
 | 
			
		||||
 | 
			
		||||
    public Task StartSpeakingAsync()
 | 
			
		||||
        => RunGatewayAction(gw => gw.SendSpeakingAsync(VoiceSpeaking.State.Microphone));
 | 
			
		||||
 | 
			
		||||
    public Task StopSpeakingAsync()
 | 
			
		||||
        => RunGatewayAction(gw => gw.SendSpeakingAsync(VoiceSpeaking.State.None));
 | 
			
		||||
 | 
			
		||||
    public async Task StartGateway()
 | 
			
		||||
        => await gateway.Start();
 | 
			
		||||
 | 
			
		||||
    public Task StopGateway()
 | 
			
		||||
    {
 | 
			
		||||
        if (gateway is { } gw)
 | 
			
		||||
            return gw.StopAsync();
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,123 +0,0 @@
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Resolvers;
 | 
			
		||||
 | 
			
		||||
public sealed class LocalTrackResolver : ILocalTrackResolver
 | 
			
		||||
{
 | 
			
		||||
    private static readonly HashSet<string> _musicExtensions = new[]
 | 
			
		||||
    {
 | 
			
		||||
        ".MP4", ".MP3", ".FLAC", ".OGG", ".WAV", ".WMA", ".WMV", ".AAC", ".MKV", ".WEBM", ".M4A", ".AA", ".AAX",
 | 
			
		||||
        ".ALAC", ".AIFF", ".MOV", ".FLV", ".OGG", ".M4V"
 | 
			
		||||
    }.ToHashSet();
 | 
			
		||||
 | 
			
		||||
    public async Task<ITrackInfo?> ResolveByQueryAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        if (!File.Exists(query))
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        var trackDuration = await Ffprobe.GetTrackDurationAsync(query);
 | 
			
		||||
        return new SimpleTrackInfo(Path.GetFileNameWithoutExtension(query),
 | 
			
		||||
            $"https://google.com?q={Uri.EscapeDataString(Path.GetFileNameWithoutExtension(query))}",
 | 
			
		||||
            "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png",
 | 
			
		||||
            trackDuration,
 | 
			
		||||
            MusicPlatform.Local,
 | 
			
		||||
            $"\"{Path.GetFullPath(query)}\"");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath)
 | 
			
		||||
    {
 | 
			
		||||
        DirectoryInfo dir;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            dir = new(dirPath);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Specified directory {DirectoryPath} could not be opened", dirPath);
 | 
			
		||||
            yield break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var files = dir.EnumerateFiles()
 | 
			
		||||
                       .Where(x =>
 | 
			
		||||
                       {
 | 
			
		||||
                           if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)
 | 
			
		||||
                               && _musicExtensions.Contains(x.Extension.ToUpperInvariant()))
 | 
			
		||||
                               return true;
 | 
			
		||||
                           return false;
 | 
			
		||||
                       })
 | 
			
		||||
                       .ToList();
 | 
			
		||||
 | 
			
		||||
        var firstFile = files.FirstOrDefault()?.FullName;
 | 
			
		||||
        if (firstFile is null)
 | 
			
		||||
            yield break;
 | 
			
		||||
 | 
			
		||||
        var firstData = await ResolveByQueryAsync(firstFile);
 | 
			
		||||
        if (firstData is not null)
 | 
			
		||||
            yield return firstData;
 | 
			
		||||
 | 
			
		||||
        var fileChunks = files.Skip(1).Chunk(10);
 | 
			
		||||
        foreach (var chunk in fileChunks)
 | 
			
		||||
        {
 | 
			
		||||
            var part = await chunk.Select(x => ResolveByQueryAsync(x.FullName)).WhenAll();
 | 
			
		||||
 | 
			
		||||
            // nullable reference types being annoying
 | 
			
		||||
            foreach (var p in part)
 | 
			
		||||
            {
 | 
			
		||||
                if (p is null)
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                yield return p;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public static class Ffprobe
 | 
			
		||||
{
 | 
			
		||||
    public static async Task<TimeSpan> GetTrackDurationAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        query = query.Replace("\"", "");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var p = Process.Start(new ProcessStartInfo
 | 
			
		||||
            {
 | 
			
		||||
                FileName = "ffprobe",
 | 
			
		||||
                Arguments =
 | 
			
		||||
                    $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -- \"{query}\"",
 | 
			
		||||
                UseShellExecute = false,
 | 
			
		||||
                RedirectStandardOutput = true,
 | 
			
		||||
                RedirectStandardError = true,
 | 
			
		||||
                StandardOutputEncoding = Encoding.UTF8,
 | 
			
		||||
                StandardErrorEncoding = Encoding.UTF8,
 | 
			
		||||
                CreateNoWindow = true
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (p is null)
 | 
			
		||||
                return TimeSpan.Zero;
 | 
			
		||||
 | 
			
		||||
            var data = await p.StandardOutput.ReadToEndAsync();
 | 
			
		||||
            if (double.TryParse(data, out var seconds))
 | 
			
		||||
                return TimeSpan.FromSeconds(seconds);
 | 
			
		||||
 | 
			
		||||
            var errorData = await p.StandardError.ReadToEndAsync();
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(errorData))
 | 
			
		||||
                Log.Warning("Ffprobe warning for file {FileName}: {ErrorMessage}", query, errorData);
 | 
			
		||||
 | 
			
		||||
            return TimeSpan.Zero;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Win32Exception)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Ffprobe was likely not installed. Local song durations will show as (?)");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Unknown exception running ffprobe; {ErrorMessage}", ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return TimeSpan.Zero;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,106 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Resolvers;
 | 
			
		||||
 | 
			
		||||
public class RadioResolver : IRadioResolver
 | 
			
		||||
{
 | 
			
		||||
    private readonly Regex _plsRegex = new(@"File1=(?<url>.*?)\n", RegexOptions.Compiled);
 | 
			
		||||
    private readonly Regex _m3URegex = new(@"(?<url>^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline);
 | 
			
		||||
    private readonly Regex _asxRegex = new(@"<ref href=""(?<url>.*?)""", RegexOptions.Compiled);
 | 
			
		||||
    private readonly Regex _xspfRegex = new(@"<location>(?<url>.*?)</location>", RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
    public async Task<ITrackInfo> ResolveByQueryAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        if (IsRadioLink(query))
 | 
			
		||||
            query = await HandleStreamContainers(query);
 | 
			
		||||
 | 
			
		||||
        return new SimpleTrackInfo(query.TrimTo(50),
 | 
			
		||||
            query,
 | 
			
		||||
            "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png",
 | 
			
		||||
            TimeSpan.MaxValue,
 | 
			
		||||
            MusicPlatform.Radio,
 | 
			
		||||
            query);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static bool IsRadioLink(string query)
 | 
			
		||||
        => (query.StartsWith("http", StringComparison.InvariantCulture)
 | 
			
		||||
            || query.StartsWith("ww", StringComparison.InvariantCulture))
 | 
			
		||||
           && (query.Contains(".pls") || query.Contains(".m3u") || query.Contains(".asx") || query.Contains(".xspf"));
 | 
			
		||||
 | 
			
		||||
    private async Task<string> HandleStreamContainers(string query)
 | 
			
		||||
    {
 | 
			
		||||
        string file = null;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var http = new HttpClient();
 | 
			
		||||
            file = await http.GetStringAsync(query);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            return query;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (query.Contains(".pls"))
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var m = _plsRegex.Match(file);
 | 
			
		||||
                var res = m.Groups["url"]?.ToString();
 | 
			
		||||
                return res?.Trim();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Failed reading .pls:\n{PlsFile}", file);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (query.Contains(".m3u"))
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var m = _m3URegex.Match(file);
 | 
			
		||||
                var res = m.Groups["url"]?.ToString();
 | 
			
		||||
                return res?.Trim();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Failed reading .m3u:\n{M3uFile}", file);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (query.Contains(".asx"))
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var m = _asxRegex.Match(file);
 | 
			
		||||
                var res = m.Groups["url"]?.ToString();
 | 
			
		||||
                return res?.Trim();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Failed reading .asx:\n{AsxFile}", file);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (query.Contains(".xspf"))
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var m = _xspfRegex.Match(file);
 | 
			
		||||
                var res = m.Groups["url"]?.ToString();
 | 
			
		||||
                return res?.Trim();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Failed reading .xspf:\n{XspfFile}", file);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return query;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,85 +0,0 @@
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music.Resolvers;
 | 
			
		||||
 | 
			
		||||
public sealed class SoundcloudResolver : ISoundcloudResolver
 | 
			
		||||
{
 | 
			
		||||
    private readonly SoundCloudApiService _sc;
 | 
			
		||||
    private readonly ITrackCacher _trackCacher;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
 | 
			
		||||
    public SoundcloudResolver(SoundCloudApiService sc, ITrackCacher trackCacher, IHttpClientFactory httpFactory)
 | 
			
		||||
    {
 | 
			
		||||
        _sc = sc;
 | 
			
		||||
        _trackCacher = trackCacher;
 | 
			
		||||
        _httpFactory = httpFactory;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool IsSoundCloudLink(string url)
 | 
			
		||||
        => Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)");
 | 
			
		||||
 | 
			
		||||
    public async IAsyncEnumerable<ITrackInfo> ResolvePlaylistAsync(string playlist)
 | 
			
		||||
    {
 | 
			
		||||
        playlist = Uri.EscapeDataString(playlist);
 | 
			
		||||
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        var responseString = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={playlist}");
 | 
			
		||||
        var scvids = JObject.Parse(responseString)["tracks"]?.ToObject<SoundCloudVideo[]>();
 | 
			
		||||
        if (scvids is null)
 | 
			
		||||
            yield break;
 | 
			
		||||
 | 
			
		||||
        foreach (var videosChunk in scvids.Where(x => x.Streamable is true).Chunk(5))
 | 
			
		||||
        {
 | 
			
		||||
            var cachableTracks = videosChunk.Select(VideoModelToCachedData).ToList();
 | 
			
		||||
 | 
			
		||||
            await cachableTracks.Select(_trackCacher.CacheTrackDataAsync).WhenAll();
 | 
			
		||||
            foreach (var info in cachableTracks.Select(CachableDataToTrackInfo))
 | 
			
		||||
                yield return info;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ICachableTrackData VideoModelToCachedData(SoundCloudVideo svideo)
 | 
			
		||||
        => new CachableTrackData
 | 
			
		||||
        {
 | 
			
		||||
            Title = svideo.FullName,
 | 
			
		||||
            Url = svideo.TrackLink,
 | 
			
		||||
            Thumbnail = svideo.ArtworkUrl,
 | 
			
		||||
            TotalDurationMs = svideo.Duration,
 | 
			
		||||
            Id = svideo.Id.ToString(),
 | 
			
		||||
            Platform = MusicPlatform.SoundCloud
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    private ITrackInfo CachableDataToTrackInfo(ICachableTrackData trackData)
 | 
			
		||||
        => new SimpleTrackInfo(trackData.Title,
 | 
			
		||||
            trackData.Url,
 | 
			
		||||
            trackData.Thumbnail,
 | 
			
		||||
            trackData.Duration,
 | 
			
		||||
            trackData.Platform,
 | 
			
		||||
            GetStreamUrl(trackData.Id));
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    private string GetStreamUrl(string trackId)
 | 
			
		||||
        => $"https://api.soundcloud.com/tracks/{trackId}/stream?client_id=368b0c85751007cd588d869d3ae61ac0";
 | 
			
		||||
 | 
			
		||||
    public async Task<ITrackInfo?> ResolveByQueryAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        var cached = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.SoundCloud);
 | 
			
		||||
        if (cached is not null)
 | 
			
		||||
            return CachableDataToTrackInfo(cached);
 | 
			
		||||
 | 
			
		||||
        var svideo = !IsSoundCloudLink(query)
 | 
			
		||||
            ? await _sc.GetVideoByQueryAsync(query)
 | 
			
		||||
            : await _sc.ResolveVideoAsync(query);
 | 
			
		||||
 | 
			
		||||
        if (svideo is null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        var cachableData = VideoModelToCachedData(svideo);
 | 
			
		||||
        await _trackCacher.CacheTrackDataByQueryAsync(query, cachableData);
 | 
			
		||||
 | 
			
		||||
        return CachableDataToTrackInfo(cachableData);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,56 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed class TrackResolveProvider : ITrackResolveProvider
 | 
			
		||||
{
 | 
			
		||||
    private readonly IYoutubeResolver _ytResolver;
 | 
			
		||||
    private readonly ILocalTrackResolver _localResolver;
 | 
			
		||||
    private readonly ISoundcloudResolver _soundcloudResolver;
 | 
			
		||||
    private readonly IRadioResolver _radioResolver;
 | 
			
		||||
 | 
			
		||||
    public TrackResolveProvider(
 | 
			
		||||
        IYoutubeResolver ytResolver,
 | 
			
		||||
        ILocalTrackResolver localResolver,
 | 
			
		||||
        ISoundcloudResolver soundcloudResolver,
 | 
			
		||||
        IRadioResolver radioResolver)
 | 
			
		||||
    {
 | 
			
		||||
        _ytResolver = ytResolver;
 | 
			
		||||
        _localResolver = localResolver;
 | 
			
		||||
        _soundcloudResolver = soundcloudResolver;
 | 
			
		||||
        _radioResolver = radioResolver;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<ITrackInfo?> QuerySongAsync(string query, MusicPlatform? forcePlatform)
 | 
			
		||||
    {
 | 
			
		||||
        switch (forcePlatform)
 | 
			
		||||
        {
 | 
			
		||||
            case MusicPlatform.Radio:
 | 
			
		||||
                return _radioResolver.ResolveByQueryAsync(query);
 | 
			
		||||
            case MusicPlatform.Youtube:
 | 
			
		||||
                return _ytResolver.ResolveByQueryAsync(query);
 | 
			
		||||
            case MusicPlatform.Local:
 | 
			
		||||
                return _localResolver.ResolveByQueryAsync(query);
 | 
			
		||||
            case MusicPlatform.SoundCloud:
 | 
			
		||||
                return _soundcloudResolver.ResolveByQueryAsync(query);
 | 
			
		||||
            case null:
 | 
			
		||||
                var match = _ytResolver.YtVideoIdRegex.Match(query);
 | 
			
		||||
                if (match.Success)
 | 
			
		||||
                    return _ytResolver.ResolveByIdAsync(match.Groups["id"].Value);
 | 
			
		||||
                else if (_soundcloudResolver.IsSoundCloudLink(query))
 | 
			
		||||
                    return _soundcloudResolver.ResolveByQueryAsync(query);
 | 
			
		||||
                else if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile)
 | 
			
		||||
                    return _localResolver.ResolveByQueryAsync(uri.AbsolutePath);
 | 
			
		||||
                else if (IsRadioLink(query))
 | 
			
		||||
                    return _radioResolver.ResolveByQueryAsync(query);
 | 
			
		||||
                else
 | 
			
		||||
                    return _ytResolver.ResolveByQueryAsync(query, false);
 | 
			
		||||
            default:
 | 
			
		||||
                Log.Error("Unsupported platform: {MusicPlatform}", forcePlatform);
 | 
			
		||||
                return Task.FromResult<ITrackInfo?>(null);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static bool IsRadioLink(string query)
 | 
			
		||||
        => (query.StartsWith("http", StringComparison.InvariantCulture)
 | 
			
		||||
            || query.StartsWith("ww", StringComparison.InvariantCulture))
 | 
			
		||||
           && (query.Contains(".pls") || query.Contains(".m3u") || query.Contains(".asx") || query.Contains(".xspf"));
 | 
			
		||||
}
 | 
			
		||||
@@ -1,315 +0,0 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Music;
 | 
			
		||||
 | 
			
		||||
public sealed class YtdlYoutubeResolver : IYoutubeResolver
 | 
			
		||||
{
 | 
			
		||||
    private static readonly string[] _durationFormats =
 | 
			
		||||
    {
 | 
			
		||||
        "ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss"
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private static readonly Regex _expiryRegex = new(@"(?:[\?\&]expire\=(?<timestamp>\d+))");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private static readonly Regex _simplePlaylistRegex = new(@"&list=(?<id>[\w\-]{12,})", RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
    public Regex YtVideoIdRegex { get; } =
 | 
			
		||||
        new(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{6,11})",
 | 
			
		||||
            RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
    private readonly ITrackCacher _trackCacher;
 | 
			
		||||
 | 
			
		||||
    private readonly YtdlOperation _ytdlPlaylistOperation;
 | 
			
		||||
    private readonly YtdlOperation _ytdlIdOperation;
 | 
			
		||||
    private readonly YtdlOperation _ytdlSearchOperation;
 | 
			
		||||
 | 
			
		||||
    private readonly IGoogleApiService _google;
 | 
			
		||||
 | 
			
		||||
    public YtdlYoutubeResolver(ITrackCacher trackCacher, IGoogleApiService google, SearchesConfigService scs)
 | 
			
		||||
    {
 | 
			
		||||
        _trackCacher = trackCacher;
 | 
			
		||||
        _google = google;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        _ytdlPlaylistOperation = new("-4 "
 | 
			
		||||
                                     + "--geo-bypass "
 | 
			
		||||
                                     + "--encoding UTF8 "
 | 
			
		||||
                                     + "-f bestaudio "
 | 
			
		||||
                                     + "-e "
 | 
			
		||||
                                     + "--get-url "
 | 
			
		||||
                                     + "--get-id "
 | 
			
		||||
                                     + "--get-thumbnail "
 | 
			
		||||
                                     + "--get-duration "
 | 
			
		||||
                                     + "--no-check-certificate "
 | 
			
		||||
                                     + "-i "
 | 
			
		||||
                                     + "--yes-playlist "
 | 
			
		||||
                                     + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
 | 
			
		||||
 | 
			
		||||
        _ytdlIdOperation = new("-4 "
 | 
			
		||||
                               + "--geo-bypass "
 | 
			
		||||
                               + "--encoding UTF8 "
 | 
			
		||||
                               + "-f bestaudio "
 | 
			
		||||
                               + "-e "
 | 
			
		||||
                               + "--get-url "
 | 
			
		||||
                               + "--get-id "
 | 
			
		||||
                               + "--get-thumbnail "
 | 
			
		||||
                               + "--get-duration "
 | 
			
		||||
                               + "--no-check-certificate "
 | 
			
		||||
                               + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
 | 
			
		||||
 | 
			
		||||
        _ytdlSearchOperation = new("-4 "
 | 
			
		||||
                                   + "--geo-bypass "
 | 
			
		||||
                                   + "--encoding UTF8 "
 | 
			
		||||
                                   + "-f bestaudio "
 | 
			
		||||
                                   + "-e "
 | 
			
		||||
                                   + "--get-url "
 | 
			
		||||
                                   + "--get-id "
 | 
			
		||||
                                   + "--get-thumbnail "
 | 
			
		||||
                                   + "--get-duration "
 | 
			
		||||
                                   + "--no-check-certificate "
 | 
			
		||||
                                   + "--default-search "
 | 
			
		||||
                                   + "\"ytsearch:\" -- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private YtTrackData ResolveYtdlData(string ytdlOutputString)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(ytdlOutputString))
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        var dataArray = ytdlOutputString.Trim().Split('\n');
 | 
			
		||||
 | 
			
		||||
        if (dataArray.Length < 5)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("Not enough data received: {YtdlData}", ytdlOutputString);
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!TimeSpan.TryParseExact(dataArray[4], _durationFormats, CultureInfo.InvariantCulture, out var time))
 | 
			
		||||
            time = TimeSpan.Zero;
 | 
			
		||||
 | 
			
		||||
        var thumbnail = Uri.IsWellFormedUriString(dataArray[3], UriKind.Absolute) ? dataArray[3].Trim() : string.Empty;
 | 
			
		||||
 | 
			
		||||
        return new(dataArray[0], dataArray[1], thumbnail, dataArray[2], time);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ITrackInfo DataToInfo(in YtTrackData trackData)
 | 
			
		||||
        => new RemoteTrackInfo(
 | 
			
		||||
            trackData.Id,
 | 
			
		||||
            trackData.Title,
 | 
			
		||||
            $"https://youtube.com/watch?v={trackData.Id}",
 | 
			
		||||
            trackData.Thumbnail,
 | 
			
		||||
            trackData.Duration,
 | 
			
		||||
            MusicPlatform.Youtube,
 | 
			
		||||
            CreateCacherFactory(trackData.Id));
 | 
			
		||||
 | 
			
		||||
    private Func<Task<string?>> CreateCacherFactory(string id)
 | 
			
		||||
        => () => _trackCacher.GetOrCreateStreamLink(id,
 | 
			
		||||
            MusicPlatform.Youtube,
 | 
			
		||||
            async () => await ExtractNewStreamUrlAsync(id));
 | 
			
		||||
 | 
			
		||||
    private static TimeSpan GetExpiry(string streamUrl)
 | 
			
		||||
    {
 | 
			
		||||
        var match = _expiryRegex.Match(streamUrl);
 | 
			
		||||
        if (match.Success && double.TryParse(match.Groups["timestamp"].ToString(), out var timestamp))
 | 
			
		||||
        {
 | 
			
		||||
            var realExpiry = timestamp.ToUnixTimestamp() - DateTime.UtcNow;
 | 
			
		||||
            if (realExpiry > TimeSpan.FromMinutes(60))
 | 
			
		||||
                return realExpiry.Subtract(TimeSpan.FromMinutes(30));
 | 
			
		||||
 | 
			
		||||
            return realExpiry;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return TimeSpan.FromHours(1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<(string StreamUrl, TimeSpan Expiry)> ExtractNewStreamUrlAsync(string id)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await _ytdlIdOperation.GetDataAsync(id);
 | 
			
		||||
        var trackInfo = ResolveYtdlData(data);
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(trackInfo.StreamUrl))
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        return (trackInfo.StreamUrl!, GetExpiry(trackInfo.StreamUrl!));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<ITrackInfo?> ResolveByIdAsync(string id)
 | 
			
		||||
    {
 | 
			
		||||
        id = id.Trim();
 | 
			
		||||
 | 
			
		||||
        var cachedData = await _trackCacher.GetCachedDataByIdAsync(id, MusicPlatform.Youtube);
 | 
			
		||||
        if (cachedData is null)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("Resolving youtube track by Id: {YoutubeId}", id);
 | 
			
		||||
 | 
			
		||||
            var data = await _ytdlIdOperation.GetDataAsync(id);
 | 
			
		||||
 | 
			
		||||
            var trackInfo = ResolveYtdlData(data);
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(trackInfo.Title))
 | 
			
		||||
                return default;
 | 
			
		||||
 | 
			
		||||
            var toReturn = DataToInfo(in trackInfo);
 | 
			
		||||
 | 
			
		||||
            await Task.WhenAll(_trackCacher.CacheTrackDataAsync(toReturn.ToCachedData(id)),
 | 
			
		||||
                CacheStreamUrlAsync(trackInfo));
 | 
			
		||||
 | 
			
		||||
            return toReturn;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task CacheStreamUrlAsync(YtTrackData trackInfo)
 | 
			
		||||
        => _trackCacher.CacheStreamUrlAsync(trackInfo.Id,
 | 
			
		||||
            MusicPlatform.Youtube,
 | 
			
		||||
            trackInfo.StreamUrl!,
 | 
			
		||||
            GetExpiry(trackInfo.StreamUrl!));
 | 
			
		||||
 | 
			
		||||
    public async IAsyncEnumerable<ITrackInfo> ResolveTracksByPlaylistIdAsync(string playlistId)
 | 
			
		||||
    {
 | 
			
		||||
        Log.Information("Resolving youtube tracks from playlist: {PlaylistId}", playlistId);
 | 
			
		||||
        var count = 0;
 | 
			
		||||
 | 
			
		||||
        var ids = await _trackCacher.GetPlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube);
 | 
			
		||||
        if (ids.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var id in ids)
 | 
			
		||||
            {
 | 
			
		||||
                var trackInfo = await ResolveByIdAsync(id);
 | 
			
		||||
                if (trackInfo is null)
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                yield return trackInfo;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            yield break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var data = string.Empty;
 | 
			
		||||
        var trackIds = new List<string>();
 | 
			
		||||
        await foreach (var line in _ytdlPlaylistOperation.EnumerateDataAsync(playlistId))
 | 
			
		||||
        {
 | 
			
		||||
            data += line;
 | 
			
		||||
 | 
			
		||||
            if (++count == 5)
 | 
			
		||||
            {
 | 
			
		||||
                var trackData = ResolveYtdlData(data);
 | 
			
		||||
                data = string.Empty;
 | 
			
		||||
                count = 0;
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(trackData.Id))
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                var info = DataToInfo(in trackData);
 | 
			
		||||
                await Task.WhenAll(_trackCacher.CacheTrackDataAsync(info.ToCachedData(trackData.Id)),
 | 
			
		||||
                    CacheStreamUrlAsync(trackData));
 | 
			
		||||
 | 
			
		||||
                trackIds.Add(trackData.Id);
 | 
			
		||||
                yield return info;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
                data += Environment.NewLine;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await _trackCacher.CachePlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube, trackIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async IAsyncEnumerable<ITrackInfo> ResolveTracksFromPlaylistAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        string? playlistId;
 | 
			
		||||
        // try to match playlist id inside the query, if a playlist url has been queried
 | 
			
		||||
        var match = _simplePlaylistRegex.Match(query);
 | 
			
		||||
        if (match.Success)
 | 
			
		||||
        {
 | 
			
		||||
            // if it's a success, just return from that playlist using the id
 | 
			
		||||
            playlistId = match.Groups["id"].ToString();
 | 
			
		||||
            await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId))
 | 
			
		||||
                yield return track;
 | 
			
		||||
 | 
			
		||||
            yield break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if a query is a search term, try the cache
 | 
			
		||||
        playlistId = await _trackCacher.GetPlaylistIdByQueryAsync(query, MusicPlatform.Youtube);
 | 
			
		||||
        if (playlistId is null)
 | 
			
		||||
        {
 | 
			
		||||
            // if it's not in the cache
 | 
			
		||||
            // find playlist id by keyword using google api
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var playlistIds = await _google.GetPlaylistIdsByKeywordsAsync(query);
 | 
			
		||||
                playlistId = playlistIds.FirstOrDefault();
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error Getting playlist id via GoogleApi");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if query is not a playlist url
 | 
			
		||||
            // and query result is not in the cache
 | 
			
		||||
            // and api returns no values
 | 
			
		||||
            // it means invalid input has been used,
 | 
			
		||||
            // or google api key is not provided
 | 
			
		||||
            if (playlistId is null)
 | 
			
		||||
                yield break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // cache the query -> playlist id for fast future lookup
 | 
			
		||||
        await _trackCacher.CachePlaylistIdByQueryAsync(query, MusicPlatform.Youtube, playlistId);
 | 
			
		||||
        await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId))
 | 
			
		||||
            yield return track;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<ITrackInfo?> ResolveByQueryAsync(string query)
 | 
			
		||||
        => ResolveByQueryAsync(query, true);
 | 
			
		||||
 | 
			
		||||
    public async Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryResolving)
 | 
			
		||||
    {
 | 
			
		||||
        if (tryResolving)
 | 
			
		||||
        {
 | 
			
		||||
            var match = YtVideoIdRegex.Match(query);
 | 
			
		||||
            if (match.Success)
 | 
			
		||||
                return await ResolveByIdAsync(match.Groups["id"].Value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Log.Information("Resolving youtube song by search term: {YoutubeQuery}", query);
 | 
			
		||||
 | 
			
		||||
        var cachedData = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.Youtube);
 | 
			
		||||
        if (cachedData is null || string.IsNullOrWhiteSpace(cachedData.Title))
 | 
			
		||||
        {
 | 
			
		||||
            var stringData = await _ytdlSearchOperation.GetDataAsync(query);
 | 
			
		||||
            var trackData = ResolveYtdlData(stringData);
 | 
			
		||||
 | 
			
		||||
            var trackInfo = DataToInfo(trackData);
 | 
			
		||||
            await Task.WhenAll(_trackCacher.CacheTrackDataByQueryAsync(query, trackInfo.ToCachedData(trackData.Id)),
 | 
			
		||||
                CacheStreamUrlAsync(trackData));
 | 
			
		||||
            return trackInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private readonly struct YtTrackData
 | 
			
		||||
    {
 | 
			
		||||
        public readonly string Title;
 | 
			
		||||
        public readonly string Id;
 | 
			
		||||
        public readonly string Thumbnail;
 | 
			
		||||
        public readonly string? StreamUrl;
 | 
			
		||||
        public readonly TimeSpan Duration;
 | 
			
		||||
 | 
			
		||||
        public YtTrackData(
 | 
			
		||||
            string title,
 | 
			
		||||
            string id,
 | 
			
		||||
            string thumbnail,
 | 
			
		||||
            string? streamUrl,
 | 
			
		||||
            TimeSpan duration)
 | 
			
		||||
        {
 | 
			
		||||
            Title = title.Trim();
 | 
			
		||||
            Id = id.Trim();
 | 
			
		||||
            Thumbnail = thumbnail;
 | 
			
		||||
            StreamUrl = streamUrl;
 | 
			
		||||
            Duration = duration;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,144 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions;
 | 
			
		||||
 | 
			
		||||
public partial class Permissions
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class BlacklistCommands : NadekoModule<BlacklistService>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
        public BlacklistCommands(DiscordSocketClient client)
 | 
			
		||||
            => _client = client;
 | 
			
		||||
 | 
			
		||||
        private async Task ListBlacklistInternal(string title, BlacklistType type, int page = 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (page < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(page));
 | 
			
		||||
 | 
			
		||||
            var list = _service.GetBlacklist();
 | 
			
		||||
            var items = await list.Where(x => x.Type == type)
 | 
			
		||||
                                  .Select(async i =>
 | 
			
		||||
                                  {
 | 
			
		||||
                                      try
 | 
			
		||||
                                      {
 | 
			
		||||
                                          return i.Type switch
 | 
			
		||||
                                          {
 | 
			
		||||
                                              BlacklistType.Channel => Format.Code(i.ItemId.ToString())
 | 
			
		||||
                                                                       + " "
 | 
			
		||||
                                                                       + (_client.GetChannel(i.ItemId)?.ToString()
 | 
			
		||||
                                                                          ?? ""),
 | 
			
		||||
                                              BlacklistType.User => Format.Code(i.ItemId.ToString())
 | 
			
		||||
                                                                    + " "
 | 
			
		||||
                                                                    + ((await _client.Rest.GetUserAsync(i.ItemId))
 | 
			
		||||
                                                                       ?.ToString()
 | 
			
		||||
                                                                       ?? ""),
 | 
			
		||||
                                              BlacklistType.Server => Format.Code(i.ItemId.ToString())
 | 
			
		||||
                                                                      + " "
 | 
			
		||||
                                                                      + (_client.GetGuild(i.ItemId)?.ToString() ?? ""),
 | 
			
		||||
                                              _ => Format.Code(i.ItemId.ToString())
 | 
			
		||||
                                          };
 | 
			
		||||
                                      }
 | 
			
		||||
                                      catch
 | 
			
		||||
                                      {
 | 
			
		||||
                                          Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]",
 | 
			
		||||
                                              i.Type,
 | 
			
		||||
                                              i.ItemId);
 | 
			
		||||
                                          return Format.Code(i.ItemId.ToString());
 | 
			
		||||
                                      }
 | 
			
		||||
                                  })
 | 
			
		||||
                                  .WhenAll();
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
                curPage =>
 | 
			
		||||
                {
 | 
			
		||||
                    var pageItems = items.Skip(10 * curPage).Take(10).ToList();
 | 
			
		||||
 | 
			
		||||
                    if (pageItems.Count == 0)
 | 
			
		||||
                        return _eb.Create().WithOkColor().WithTitle(title).WithDescription(GetText(strs.empty_page));
 | 
			
		||||
 | 
			
		||||
                    return _eb.Create().WithTitle(title).WithDescription(pageItems.Join('\n')).WithOkColor();
 | 
			
		||||
                },
 | 
			
		||||
                items.Length,
 | 
			
		||||
                10);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public Task UserBlacklist(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            return ListBlacklistInternal(GetText(strs.blacklisted_users), BlacklistType.User, page);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public Task ChannelBlacklist(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            return ListBlacklistInternal(GetText(strs.blacklisted_channels), BlacklistType.Channel, page);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public Task ServerBlacklist(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            return ListBlacklistInternal(GetText(strs.blacklisted_servers), BlacklistType.Server, page);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public Task UserBlacklist(AddRemove action, ulong id)
 | 
			
		||||
            => Blacklist(action, id, BlacklistType.User);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public Task UserBlacklist(AddRemove action, IUser usr)
 | 
			
		||||
            => Blacklist(action, usr.Id, BlacklistType.User);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public Task ChannelBlacklist(AddRemove action, ulong id)
 | 
			
		||||
            => Blacklist(action, id, BlacklistType.Channel);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public Task ServerBlacklist(AddRemove action, ulong id)
 | 
			
		||||
            => Blacklist(action, id, BlacklistType.Server);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public Task ServerBlacklist(AddRemove action, IGuild guild)
 | 
			
		||||
            => Blacklist(action, guild.Id, BlacklistType.Server);
 | 
			
		||||
 | 
			
		||||
        private async Task Blacklist(AddRemove action, ulong id, BlacklistType type)
 | 
			
		||||
        {
 | 
			
		||||
            if (action == AddRemove.Add)
 | 
			
		||||
                _service.Blacklist(type, id);
 | 
			
		||||
            else
 | 
			
		||||
                _service.UnBlacklist(type, id);
 | 
			
		||||
 | 
			
		||||
            if (action == AddRemove.Add)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.blacklisted(Format.Code(type.ToString()),
 | 
			
		||||
                    Format.Code(id.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.unblacklisted(Format.Code(type.ToString()),
 | 
			
		||||
                    Format.Code(id.ToString())));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,131 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class BlacklistService : IExecOnMessage
 | 
			
		||||
{
 | 
			
		||||
    public int Priority
 | 
			
		||||
        => int.MaxValue;
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly IPubSub _pubSub;
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
    private IReadOnlyList<BlacklistEntry> blacklist;
 | 
			
		||||
 | 
			
		||||
    private readonly TypedKey<BlacklistEntry[]> _blPubKey = new("blacklist.reload");
 | 
			
		||||
 | 
			
		||||
    public BlacklistService(DbService db, IPubSub pubSub, IBotCredentials creds)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _pubSub = pubSub;
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
 | 
			
		||||
        Reload(false);
 | 
			
		||||
        _pubSub.Sub(_blPubKey, OnReload);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ValueTask OnReload(BlacklistEntry[] newBlacklist)
 | 
			
		||||
    {
 | 
			
		||||
        blacklist = newBlacklist;
 | 
			
		||||
        return default;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var bl in blacklist)
 | 
			
		||||
        {
 | 
			
		||||
            if (guild is not null && bl.Type == BlacklistType.Server && bl.ItemId == guild.Id)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", guild.Name, guild.Id);
 | 
			
		||||
 | 
			
		||||
                return Task.FromResult(true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]",
 | 
			
		||||
                    usrMsg.Channel.Name,
 | 
			
		||||
                    usrMsg.Channel.Id);
 | 
			
		||||
 | 
			
		||||
                return Task.FromResult(true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
 | 
			
		||||
                    usrMsg.Author.ToString(),
 | 
			
		||||
                    usrMsg.Author.Id);
 | 
			
		||||
 | 
			
		||||
                return Task.FromResult(true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.FromResult(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyList<BlacklistEntry> GetBlacklist()
 | 
			
		||||
        => blacklist;
 | 
			
		||||
 | 
			
		||||
    public void Reload(bool publish = true)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var toPublish = uow.Blacklist.AsNoTracking().ToArray();
 | 
			
		||||
        blacklist = toPublish;
 | 
			
		||||
        if (publish)
 | 
			
		||||
            _pubSub.Pub(_blPubKey, toPublish);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Blacklist(BlacklistType type, ulong id)
 | 
			
		||||
    {
 | 
			
		||||
        if (_creds.OwnerIds.Contains(id))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var item = new BlacklistEntry
 | 
			
		||||
        {
 | 
			
		||||
            ItemId = id,
 | 
			
		||||
            Type = type
 | 
			
		||||
        };
 | 
			
		||||
        uow.Blacklist.Add(item);
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
 | 
			
		||||
        Reload();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void UnBlacklist(BlacklistType type, ulong id)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var toRemove = uow.Blacklist.FirstOrDefault(bi => bi.ItemId == id && bi.Type == type);
 | 
			
		||||
 | 
			
		||||
        if (toRemove is not null)
 | 
			
		||||
            uow.Blacklist.Remove(toRemove);
 | 
			
		||||
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
 | 
			
		||||
        Reload();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist)
 | 
			
		||||
    {
 | 
			
		||||
        using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var bc = uow.Blacklist;
 | 
			
		||||
            //blacklist the users
 | 
			
		||||
            bc.AddRange(toBlacklist.Select(x => new BlacklistEntry
 | 
			
		||||
            {
 | 
			
		||||
                ItemId = x,
 | 
			
		||||
                Type = BlacklistType.User
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            //clear their currencies
 | 
			
		||||
            uow.DiscordUser.RemoveFromMany(toBlacklist);
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Reload();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using static NadekoBot.Common.TypeReaders.TypeReaderResult;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions;
 | 
			
		||||
 | 
			
		||||
public class CleverbotResponseCmdCdTypeReader : NadekoTypeReader<CleverBotResponseStr>
 | 
			
		||||
{
 | 
			
		||||
    public override ValueTask<TypeReaderResult<CleverBotResponseStr>> ReadAsync(
 | 
			
		||||
        ICommandContext ctx,
 | 
			
		||||
        string input)
 | 
			
		||||
        => input.ToLowerInvariant() == CleverBotResponseStr.CLEVERBOT_RESPONSE
 | 
			
		||||
            ? new(FromSuccess(new CleverBotResponseStr()))
 | 
			
		||||
            : new(FromError<CleverBotResponseStr>(CommandError.ParseFailed, "Not a valid cleverbot"));
 | 
			
		||||
}
 | 
			
		||||
@@ -1,143 +0,0 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<string, int>> _settings = new();
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<(ulong, string), ConcurrentDictionary<ulong, DateTime>> _activeCooldowns =
 | 
			
		||||
        new();
 | 
			
		||||
 | 
			
		||||
    public int Priority => 0;
 | 
			
		||||
 | 
			
		||||
    public CmdCdService(Bot bot, DbService db)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _settings = bot
 | 
			
		||||
            .AllGuildConfigs
 | 
			
		||||
            .ToDictionary(x => x.GuildId, x => x.CommandCooldowns
 | 
			
		||||
                .DistinctBy(x => x.CommandName.ToLowerInvariant())
 | 
			
		||||
                .ToDictionary(c => c.CommandName, c => c.Seconds)
 | 
			
		||||
                .ToConcurrent())
 | 
			
		||||
            .ToConcurrent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
 | 
			
		||||
        => TryBlock(context.Guild, context.User, command.Name.ToLowerInvariant());
 | 
			
		||||
 | 
			
		||||
    public Task<bool> TryBlock(IGuild? guild, IUser user, string commandName)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is null)
 | 
			
		||||
            return Task.FromResult(false);
 | 
			
		||||
        
 | 
			
		||||
        if (!_settings.TryGetValue(guild.Id, out var cooldownSettings))
 | 
			
		||||
            return Task.FromResult(false);
 | 
			
		||||
 | 
			
		||||
        if (!cooldownSettings.TryGetValue(commandName, out var cdSeconds))
 | 
			
		||||
            return Task.FromResult(false);
 | 
			
		||||
 | 
			
		||||
        var cooldowns = _activeCooldowns.GetOrAdd(
 | 
			
		||||
            (guild.Id, commandName),
 | 
			
		||||
            static _ => new());
 | 
			
		||||
 | 
			
		||||
        // if user is not already on cooldown, add 
 | 
			
		||||
        if (cooldowns.TryAdd(user.Id, DateTime.UtcNow))
 | 
			
		||||
        {
 | 
			
		||||
            return Task.FromResult(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if there is an entry, maybe it expired. Try to check if it expired and don't fail if it did
 | 
			
		||||
        // - just update
 | 
			
		||||
        if (cooldowns.TryGetValue(user.Id, out var oldValue))
 | 
			
		||||
        {
 | 
			
		||||
            var diff = DateTime.UtcNow - oldValue;
 | 
			
		||||
            if (diff.TotalSeconds > cdSeconds)
 | 
			
		||||
            {
 | 
			
		||||
                if (cooldowns.TryUpdate(user.Id, DateTime.UtcNow, oldValue))
 | 
			
		||||
                    return Task.FromResult(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.FromResult(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
 | 
			
		||||
 | 
			
		||||
        while (await timer.WaitForNextTickAsync())
 | 
			
		||||
        {
 | 
			
		||||
            // once per hour delete expired entries
 | 
			
		||||
            foreach (var ((guildId, commandName), dict) in _activeCooldowns)
 | 
			
		||||
            {
 | 
			
		||||
                // if this pair no longer has associated config, that means it has been removed.
 | 
			
		||||
                // remove all cooldowns
 | 
			
		||||
                if (!_settings.TryGetValue(guildId, out var inner)
 | 
			
		||||
                    || !inner.TryGetValue(commandName, out var cdSeconds))
 | 
			
		||||
                {
 | 
			
		||||
                    _activeCooldowns.Remove((guildId, commandName), out _);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Cleanup(dict, cdSeconds);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void Cleanup(ConcurrentDictionary<ulong, DateTime> dict, int cdSeconds)
 | 
			
		||||
    {
 | 
			
		||||
        var now = DateTime.UtcNow;
 | 
			
		||||
        foreach (var (key, _) in dict.Where(x => (now - x.Value).TotalSeconds > cdSeconds).ToArray())
 | 
			
		||||
        {
 | 
			
		||||
            dict.TryRemove(key, out _);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ClearCooldowns(ulong guildId, string cmdName)
 | 
			
		||||
    {
 | 
			
		||||
        if (_settings.TryGetValue(guildId, out var dict))
 | 
			
		||||
            dict.TryRemove(cmdName, out _);
 | 
			
		||||
 | 
			
		||||
        _activeCooldowns.TryRemove((guildId, cmdName), out _);
 | 
			
		||||
        
 | 
			
		||||
        using var ctx = _db.GetDbContext();
 | 
			
		||||
        var gc = ctx.GuildConfigsForId(guildId, x => x.Include(x => x.CommandCooldowns));
 | 
			
		||||
        gc.CommandCooldowns.RemoveWhere(x => x.CommandName == cmdName);
 | 
			
		||||
        ctx.SaveChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void AddCooldown(ulong guildId, string name, int secs)
 | 
			
		||||
    {
 | 
			
		||||
        if (secs <= 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(secs));
 | 
			
		||||
        
 | 
			
		||||
        var sett = _settings.GetOrAdd(guildId, static _ => new());
 | 
			
		||||
        sett[name] = secs;
 | 
			
		||||
 | 
			
		||||
        // force cleanup 
 | 
			
		||||
        if (_activeCooldowns.TryGetValue((guildId, name), out var dict))
 | 
			
		||||
            Cleanup(dict, secs);
 | 
			
		||||
        
 | 
			
		||||
        using var ctx = _db.GetDbContext();
 | 
			
		||||
        var gc = ctx.GuildConfigsForId(guildId, x => x.Include(x => x.CommandCooldowns));
 | 
			
		||||
        gc.CommandCooldowns.RemoveWhere(x => x.CommandName == name);
 | 
			
		||||
        gc.CommandCooldowns.Add(new()
 | 
			
		||||
        {
 | 
			
		||||
            Seconds = secs,
 | 
			
		||||
            CommandName = name
 | 
			
		||||
        });
 | 
			
		||||
        ctx.SaveChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyCollection<(string CommandName, int Seconds)> GetCommandCooldowns(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_settings.TryGetValue(guildId, out var dict))
 | 
			
		||||
            return Array.Empty<(string, int)>();
 | 
			
		||||
 | 
			
		||||
        return dict.Select(x => (x.Key, x.Value)).ToArray();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,105 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Humanizer.Localisation;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions;
 | 
			
		||||
 | 
			
		||||
public partial class Permissions
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class CmdCdsCommands : NadekoModule
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly CmdCdService _service;
 | 
			
		||||
 | 
			
		||||
        public CmdCdsCommands(CmdCdService service, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _service = service;
 | 
			
		||||
            _db = db;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task CmdCooldownInternal(string cmdName, int secs)
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
            if (secs is < 0 or > 3600)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.invalid_second_param_between(0, 3600));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var name = cmdName.ToLowerInvariant();
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns));
 | 
			
		||||
 | 
			
		||||
                var toDelete = config.CommandCooldowns.FirstOrDefault(cc => cc.CommandName == name);
 | 
			
		||||
                if (toDelete is not null)
 | 
			
		||||
                    uow.Set<CommandCooldown>().Remove(toDelete);
 | 
			
		||||
                if (secs != 0)
 | 
			
		||||
                {
 | 
			
		||||
                    var cc = new CommandCooldown
 | 
			
		||||
                    {
 | 
			
		||||
                        CommandName = name,
 | 
			
		||||
                        Seconds = secs
 | 
			
		||||
                    };
 | 
			
		||||
                    config.CommandCooldowns.Add(cc);
 | 
			
		||||
                    _service.AddCooldown(channel.Guild.Id, name, secs);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (secs == 0)
 | 
			
		||||
            {
 | 
			
		||||
                _service.ClearCooldowns(ctx.Guild.Id, cmdName);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.cmdcd_cleared(Format.Bold(name)));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.cmdcd_add(Format.Bold(name), Format.Bold(secs.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task CmdCooldown(CleverBotResponseStr command, int secs)
 | 
			
		||||
            => CmdCooldownInternal(CleverBotResponseStr.CLEVERBOT_RESPONSE, secs);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task CmdCooldown(CommandOrExprInfo command, int secs)
 | 
			
		||||
            => CmdCooldownInternal(command.Name, secs);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task AllCmdCooldowns(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
            var localSet = _service.GetCommandCooldowns(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            if (!localSet.Any())
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.cmdcd_none);
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.SendPaginatedConfirmAsync(page, curPage =>
 | 
			
		||||
                {
 | 
			
		||||
                    var items = localSet.Skip(curPage * 15)
 | 
			
		||||
                        .Take(15)
 | 
			
		||||
                        .Select(x => $"{Format.Code(x.CommandName)}: {x.Seconds.Seconds().Humanize(maxUnit: TimeUnit.Second, culture: Culture)}");
 | 
			
		||||
 | 
			
		||||
                    return _eb.Create(ctx)
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithDescription(items.Join("\n"));
 | 
			
		||||
 | 
			
		||||
                }, localSet.Count, 15);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,324 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions;
 | 
			
		||||
 | 
			
		||||
public partial class Permissions
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class FilterCommands : NadekoModule<FilterService>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public FilterCommands(DbService db)
 | 
			
		||||
            => _db = db;
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task FwClear()
 | 
			
		||||
        {
 | 
			
		||||
            _service.ClearFilteredWords(ctx.Guild.Id);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.fw_cleared);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task FilterList()
 | 
			
		||||
        {
 | 
			
		||||
            var embed = _eb.Create(ctx)
 | 
			
		||||
                .WithOkColor()
 | 
			
		||||
                .WithTitle("Server filter settings");
 | 
			
		||||
 | 
			
		||||
            var config = await _service.GetFilterSettings(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            string GetEnabledEmoji(bool value)
 | 
			
		||||
                => value ? "\\🟢" : "\\🔴";
 | 
			
		||||
 | 
			
		||||
            async Task<string> GetChannelListAsync(IReadOnlyCollection<ulong> channels)
 | 
			
		||||
            {
 | 
			
		||||
                var toReturn = (await channels
 | 
			
		||||
                        .Select(async cid =>
 | 
			
		||||
                        {
 | 
			
		||||
                            var ch = await ctx.Guild.GetChannelAsync(cid);
 | 
			
		||||
                            return ch is null
 | 
			
		||||
                                ? $"{cid} *missing*"
 | 
			
		||||
                                : $"<#{cid}>";
 | 
			
		||||
                        })
 | 
			
		||||
                        .WhenAll())
 | 
			
		||||
                    .Join('\n');
 | 
			
		||||
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(toReturn))
 | 
			
		||||
                    return GetText(strs.no_channel_found);
 | 
			
		||||
 | 
			
		||||
                return toReturn;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            embed.AddField($"{GetEnabledEmoji(config.FilterLinksEnabled)} Filter Links",
 | 
			
		||||
                await GetChannelListAsync(config.FilterLinksChannels));
 | 
			
		||||
 | 
			
		||||
            embed.AddField($"{GetEnabledEmoji(config.FilterInvitesEnabled)} Filter Invites",
 | 
			
		||||
                await GetChannelListAsync(config.FilterInvitesChannels));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task SrvrFilterInv()
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
            bool enabled;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(channel.Guild.Id, set => set);
 | 
			
		||||
                enabled = config.FilterInvites = !config.FilterInvites;
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
            {
 | 
			
		||||
                _service.InviteFilteringServers.Add(channel.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.invite_filter_server_on);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _service.InviteFilteringServers.TryRemove(channel.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.invite_filter_server_off);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task ChnlFilterInv()
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
            FilterChannelId removed;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(channel.Guild.Id,
 | 
			
		||||
                    set => set.Include(gc => gc.FilterInvitesChannelIds));
 | 
			
		||||
                var match = new FilterChannelId
 | 
			
		||||
                {
 | 
			
		||||
                    ChannelId = channel.Id
 | 
			
		||||
                };
 | 
			
		||||
                removed = config.FilterInvitesChannelIds.FirstOrDefault(fc => fc.Equals(match));
 | 
			
		||||
 | 
			
		||||
                if (removed is null)
 | 
			
		||||
                    config.FilterInvitesChannelIds.Add(match);
 | 
			
		||||
                else
 | 
			
		||||
                    uow.Remove(removed);
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (removed is null)
 | 
			
		||||
            {
 | 
			
		||||
                _service.InviteFilteringChannels.Add(channel.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_on);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _service.InviteFilteringChannels.TryRemove(channel.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_off);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task SrvrFilterLin()
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
            bool enabled;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(channel.Guild.Id, set => set);
 | 
			
		||||
                enabled = config.FilterLinks = !config.FilterLinks;
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
            {
 | 
			
		||||
                _service.LinkFilteringServers.Add(channel.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.link_filter_server_on);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _service.LinkFilteringServers.TryRemove(channel.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.link_filter_server_off);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task ChnlFilterLin()
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
            FilterLinksChannelId removed;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config =
 | 
			
		||||
                    uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterLinksChannelIds));
 | 
			
		||||
                var match = new FilterLinksChannelId
 | 
			
		||||
                {
 | 
			
		||||
                    ChannelId = channel.Id
 | 
			
		||||
                };
 | 
			
		||||
                removed = config.FilterLinksChannelIds.FirstOrDefault(fc => fc.Equals(match));
 | 
			
		||||
 | 
			
		||||
                if (removed is null)
 | 
			
		||||
                    config.FilterLinksChannelIds.Add(match);
 | 
			
		||||
                else
 | 
			
		||||
                    uow.Remove(removed);
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (removed is null)
 | 
			
		||||
            {
 | 
			
		||||
                _service.LinkFilteringChannels.Add(channel.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.link_filter_channel_on);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _service.LinkFilteringChannels.TryRemove(channel.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.link_filter_channel_off);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task SrvrFilterWords()
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
            bool enabled;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(channel.Guild.Id, set => set);
 | 
			
		||||
                enabled = config.FilterWords = !config.FilterWords;
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (enabled)
 | 
			
		||||
            {
 | 
			
		||||
                _service.WordFilteringServers.Add(channel.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.word_filter_server_on);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _service.WordFilteringServers.TryRemove(channel.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.word_filter_server_off);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task ChnlFilterWords()
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
            FilterWordsChannelId removed;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config =
 | 
			
		||||
                    uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterWordsChannelIds));
 | 
			
		||||
 | 
			
		||||
                var match = new FilterWordsChannelId
 | 
			
		||||
                {
 | 
			
		||||
                    ChannelId = channel.Id
 | 
			
		||||
                };
 | 
			
		||||
                removed = config.FilterWordsChannelIds.FirstOrDefault(fc => fc.Equals(match));
 | 
			
		||||
                if (removed is null)
 | 
			
		||||
                    config.FilterWordsChannelIds.Add(match);
 | 
			
		||||
                else
 | 
			
		||||
                    uow.Remove(removed);
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (removed is null)
 | 
			
		||||
            {
 | 
			
		||||
                _service.WordFilteringChannels.Add(channel.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.word_filter_channel_on);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _service.WordFilteringChannels.TryRemove(channel.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.word_filter_channel_off);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task FilterWord([Leftover] string word)
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
            word = word?.Trim().ToLowerInvariant();
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(word))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            FilteredWord removed;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilteredWords));
 | 
			
		||||
 | 
			
		||||
                removed = config.FilteredWords.FirstOrDefault(fw => fw.Word.Trim().ToLowerInvariant() == word);
 | 
			
		||||
 | 
			
		||||
                if (removed is null)
 | 
			
		||||
                {
 | 
			
		||||
                    config.FilteredWords.Add(new()
 | 
			
		||||
                    {
 | 
			
		||||
                        Word = word
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                    uow.Remove(removed);
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var filteredWords =
 | 
			
		||||
                _service.ServerFilteredWords.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet<string>());
 | 
			
		||||
 | 
			
		||||
            if (removed is null)
 | 
			
		||||
            {
 | 
			
		||||
                filteredWords.Add(word);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.filter_word_add(Format.Code(word)));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                filteredWords.TryRemove(word);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.filter_word_remove(Format.Code(word)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task LstFilterWords(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            page--;
 | 
			
		||||
            if (page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
            _service.ServerFilteredWords.TryGetValue(channel.Guild.Id, out var fwHash);
 | 
			
		||||
 | 
			
		||||
            var fws = fwHash.ToArray();
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
                curPage => _eb.Create()
 | 
			
		||||
                              .WithTitle(GetText(strs.filter_word_list))
 | 
			
		||||
                              .WithDescription(string.Join("\n", fws.Skip(curPage * 10).Take(10)))
 | 
			
		||||
                              .WithOkColor(),
 | 
			
		||||
                fws.Length,
 | 
			
		||||
                10);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,242 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using AngleSharp.Dom;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class FilterService : IExecOnMessage
 | 
			
		||||
{
 | 
			
		||||
    public ConcurrentHashSet<ulong> InviteFilteringChannels { get; }
 | 
			
		||||
    public ConcurrentHashSet<ulong> InviteFilteringServers { get; }
 | 
			
		||||
 | 
			
		||||
    //serverid, filteredwords
 | 
			
		||||
    public ConcurrentDictionary<ulong, ConcurrentHashSet<string>> ServerFilteredWords { get; }
 | 
			
		||||
 | 
			
		||||
    public ConcurrentHashSet<ulong> WordFilteringChannels { get; }
 | 
			
		||||
    public ConcurrentHashSet<ulong> WordFilteringServers { get; }
 | 
			
		||||
 | 
			
		||||
    public ConcurrentHashSet<ulong> LinkFilteringChannels { get; }
 | 
			
		||||
    public ConcurrentHashSet<ulong> LinkFilteringServers { get; }
 | 
			
		||||
 | 
			
		||||
    public int Priority
 | 
			
		||||
        => int.MaxValue - 1;
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    public FilterService(DiscordSocketClient client, DbService db)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
 | 
			
		||||
        using (var uow = db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var ids = client.GetGuildIds();
 | 
			
		||||
            var configs = uow.Set<GuildConfig>()
 | 
			
		||||
                             .AsQueryable()
 | 
			
		||||
                             .Include(x => x.FilteredWords)
 | 
			
		||||
                             .Include(x => x.FilterLinksChannelIds)
 | 
			
		||||
                             .Include(x => x.FilterWordsChannelIds)
 | 
			
		||||
                             .Include(x => x.FilterInvitesChannelIds)
 | 
			
		||||
                             .Where(gc => ids.Contains(gc.GuildId))
 | 
			
		||||
                             .ToList();
 | 
			
		||||
 | 
			
		||||
            InviteFilteringServers = new(configs.Where(gc => gc.FilterInvites).Select(gc => gc.GuildId));
 | 
			
		||||
            InviteFilteringChannels =
 | 
			
		||||
                new(configs.SelectMany(gc => gc.FilterInvitesChannelIds.Select(fci => fci.ChannelId)));
 | 
			
		||||
 | 
			
		||||
            LinkFilteringServers = new(configs.Where(gc => gc.FilterLinks).Select(gc => gc.GuildId));
 | 
			
		||||
            LinkFilteringChannels =
 | 
			
		||||
                new(configs.SelectMany(gc => gc.FilterLinksChannelIds.Select(fci => fci.ChannelId)));
 | 
			
		||||
 | 
			
		||||
            var dict = configs.ToDictionary(gc => gc.GuildId,
 | 
			
		||||
                gc => new ConcurrentHashSet<string>(gc.FilteredWords.Select(fw => fw.Word).Distinct()));
 | 
			
		||||
 | 
			
		||||
            ServerFilteredWords = new(dict);
 | 
			
		||||
 | 
			
		||||
            var serverFiltering = configs.Where(gc => gc.FilterWords);
 | 
			
		||||
            WordFilteringServers = new(serverFiltering.Select(gc => gc.GuildId));
 | 
			
		||||
            WordFilteringChannels =
 | 
			
		||||
                new(configs.SelectMany(gc => gc.FilterWordsChannelIds.Select(fwci => fwci.ChannelId)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        client.MessageUpdated += (oldData, newMsg, channel) =>
 | 
			
		||||
        {
 | 
			
		||||
            _ = Task.Run(() =>
 | 
			
		||||
            {
 | 
			
		||||
                var guild = (channel as ITextChannel)?.Guild;
 | 
			
		||||
 | 
			
		||||
                if (guild is null || newMsg is not IUserMessage usrMsg)
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
                return ExecOnMessageAsync(guild, usrMsg);
 | 
			
		||||
            });
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ConcurrentHashSet<string> FilteredWordsForChannel(ulong channelId, ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var words = new ConcurrentHashSet<string>();
 | 
			
		||||
        if (WordFilteringChannels.Contains(channelId))
 | 
			
		||||
            ServerFilteredWords.TryGetValue(guildId, out words);
 | 
			
		||||
        return words;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ClearFilteredWords(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId,
 | 
			
		||||
            set => set.Include(x => x.FilteredWords).Include(x => x.FilterWordsChannelIds));
 | 
			
		||||
 | 
			
		||||
        WordFilteringServers.TryRemove(guildId);
 | 
			
		||||
        ServerFilteredWords.TryRemove(guildId, out _);
 | 
			
		||||
 | 
			
		||||
        foreach (var c in gc.FilterWordsChannelIds)
 | 
			
		||||
            WordFilteringChannels.TryRemove(c.ChannelId);
 | 
			
		||||
 | 
			
		||||
        gc.FilterWords = false;
 | 
			
		||||
        gc.FilteredWords.Clear();
 | 
			
		||||
        gc.FilterWordsChannelIds.Clear();
 | 
			
		||||
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ConcurrentHashSet<string> FilteredWordsForServer(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var words = new ConcurrentHashSet<string>();
 | 
			
		||||
        if (WordFilteringServers.Contains(guildId))
 | 
			
		||||
            ServerFilteredWords.TryGetValue(guildId, out words);
 | 
			
		||||
        return words;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (msg.Author is not IGuildUser gu || gu.GuildPermissions.Administrator)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        var results = await Task.WhenAll(FilterInvites(guild, msg), FilterWords(guild, msg), FilterLinks(guild, msg));
 | 
			
		||||
 | 
			
		||||
        return results.Any(x => x);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> FilterWords(IGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is null)
 | 
			
		||||
            return false;
 | 
			
		||||
        if (usrMsg is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        var filteredChannelWords =
 | 
			
		||||
            FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet<string>();
 | 
			
		||||
        var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet<string>();
 | 
			
		||||
        var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' ');
 | 
			
		||||
        if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var word in wordsInMessage)
 | 
			
		||||
            {
 | 
			
		||||
                if (filteredChannelWords.Contains(word) || filteredServerWords.Contains(word))
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Information("User {UserName} [{UserId}] used a filtered word in {ChannelId} channel",
 | 
			
		||||
                        usrMsg.Author.ToString(),
 | 
			
		||||
                        usrMsg.Author.Id,
 | 
			
		||||
                        usrMsg.Channel.Id);
 | 
			
		||||
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await usrMsg.DeleteAsync();
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (HttpException ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex,
 | 
			
		||||
                            "I do not have permission to filter words in channel with id {Id}",
 | 
			
		||||
                            usrMsg.Channel.Id);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> FilterInvites(IGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is null)
 | 
			
		||||
            return false;
 | 
			
		||||
        if (usrMsg is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) || InviteFilteringServers.Contains(guild.Id))
 | 
			
		||||
            && usrMsg.Content.IsDiscordInvite())
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("User {UserName} [{UserId}] sent a filtered invite to {ChannelId} channel",
 | 
			
		||||
                usrMsg.Author.ToString(),
 | 
			
		||||
                usrMsg.Author.Id,
 | 
			
		||||
                usrMsg.Channel.Id);
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await usrMsg.DeleteAsync();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex,
 | 
			
		||||
                    "I do not have permission to filter invites in channel with id {Id}",
 | 
			
		||||
                    usrMsg.Channel.Id);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> FilterLinks(IGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is null)
 | 
			
		||||
            return false;
 | 
			
		||||
        if (usrMsg is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if ((LinkFilteringChannels.Contains(usrMsg.Channel.Id) || LinkFilteringServers.Contains(guild.Id))
 | 
			
		||||
            && usrMsg.Content.TryGetUrlPath(out _))
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("User {UserName} [{UserId}] sent a filtered link to {ChannelId} channel",
 | 
			
		||||
                usrMsg.Author.ToString(),
 | 
			
		||||
                usrMsg.Author.Id,
 | 
			
		||||
                usrMsg.Channel.Id);
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await usrMsg.DeleteAsync();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "I do not have permission to filter links in channel with id {Id}", usrMsg.Channel.Id);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<ServerFilterSettings> GetFilterSettings(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId, set => set
 | 
			
		||||
            .Include(x => x.FilterInvitesChannelIds)
 | 
			
		||||
            .Include(x => x.FilterLinksChannelIds));
 | 
			
		||||
 | 
			
		||||
        return new()
 | 
			
		||||
        {
 | 
			
		||||
            FilterInvitesChannels = gc.FilterInvitesChannelIds.Map(x => x.ChannelId),
 | 
			
		||||
            FilterLinksChannels = gc.FilterLinksChannelIds.Map(x => x.ChannelId),
 | 
			
		||||
            FilterInvitesEnabled = gc.FilterInvites,
 | 
			
		||||
            FilterLinksEnabled = gc.FilterLinks,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public readonly struct ServerFilterSettings
 | 
			
		||||
{
 | 
			
		||||
    public bool FilterInvitesEnabled { get; init; }
 | 
			
		||||
    public bool FilterLinksEnabled { get; init; }
 | 
			
		||||
    public IReadOnlyCollection<ulong> FilterInvitesChannels { get; init; }
 | 
			
		||||
    public IReadOnlyCollection<ulong> FilterLinksChannels { get; init; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,77 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions;
 | 
			
		||||
 | 
			
		||||
public partial class Permissions
 | 
			
		||||
{
 | 
			
		||||
    [Group]
 | 
			
		||||
    public partial class GlobalPermissionCommands : NadekoModule
 | 
			
		||||
    {
 | 
			
		||||
        private readonly GlobalPermissionService _service;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public GlobalPermissionCommands(GlobalPermissionService service, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _service = service;
 | 
			
		||||
            _db = db;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task GlobalPermList()
 | 
			
		||||
        {
 | 
			
		||||
            var blockedModule = _service.BlockedModules;
 | 
			
		||||
            var blockedCommands = _service.BlockedCommands;
 | 
			
		||||
            if (!blockedModule.Any() && !blockedCommands.Any())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.lgp_none);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var embed = _eb.Create().WithOkColor();
 | 
			
		||||
 | 
			
		||||
            if (blockedModule.Any())
 | 
			
		||||
                embed.AddField(GetText(strs.blocked_modules), string.Join("\n", _service.BlockedModules));
 | 
			
		||||
 | 
			
		||||
            if (blockedCommands.Any())
 | 
			
		||||
                embed.AddField(GetText(strs.blocked_commands), string.Join("\n", _service.BlockedCommands));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task GlobalModule(ModuleOrCrInfo module)
 | 
			
		||||
        {
 | 
			
		||||
            var moduleName = module.Name.ToLowerInvariant();
 | 
			
		||||
 | 
			
		||||
            var added = _service.ToggleModule(moduleName);
 | 
			
		||||
 | 
			
		||||
            if (added)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.gmod_add(Format.Bold(module.Name)));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.gmod_remove(Format.Bold(module.Name)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task GlobalCommand(CommandOrExprInfo cmd)
 | 
			
		||||
        {
 | 
			
		||||
            var commandName = cmd.Name.ToLowerInvariant();
 | 
			
		||||
            var added = _service.ToggleCommand(commandName);
 | 
			
		||||
 | 
			
		||||
            if (added)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.gcmd_add(Format.Bold(cmd.Name)));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.gcmd_remove(Format.Bold(cmd.Name)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,92 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public class GlobalPermissionService : IExecPreCommand, INService
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; } = 0;
 | 
			
		||||
 | 
			
		||||
    public HashSet<string> BlockedCommands
 | 
			
		||||
        => _bss.Data.Blocked.Commands;
 | 
			
		||||
 | 
			
		||||
    public HashSet<string> BlockedModules
 | 
			
		||||
        => _bss.Data.Blocked.Modules;
 | 
			
		||||
 | 
			
		||||
    private readonly BotConfigService _bss;
 | 
			
		||||
 | 
			
		||||
    public GlobalPermissionService(BotConfigService bss)
 | 
			
		||||
        => _bss = bss;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public Task<bool> ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command)
 | 
			
		||||
    {
 | 
			
		||||
        var settings = _bss.Data;
 | 
			
		||||
        var commandName = command.Name.ToLowerInvariant();
 | 
			
		||||
 | 
			
		||||
        if (commandName != "resetglobalperms"
 | 
			
		||||
            && (settings.Blocked.Commands.Contains(commandName)
 | 
			
		||||
                || settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant())))
 | 
			
		||||
            return Task.FromResult(true);
 | 
			
		||||
 | 
			
		||||
        return Task.FromResult(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Toggles module blacklist
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="moduleName">Lowercase module name</param>
 | 
			
		||||
    /// <returns>Whether the module is added</returns>
 | 
			
		||||
    public bool ToggleModule(string moduleName)
 | 
			
		||||
    {
 | 
			
		||||
        var added = false;
 | 
			
		||||
        _bss.ModifyConfig(bs =>
 | 
			
		||||
        {
 | 
			
		||||
            if (bs.Blocked.Modules.Add(moduleName))
 | 
			
		||||
                added = true;
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                bs.Blocked.Modules.Remove(moduleName);
 | 
			
		||||
                added = false;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return added;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Toggles command blacklist
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="commandName">Lowercase command name</param>
 | 
			
		||||
    /// <returns>Whether the command is added</returns>
 | 
			
		||||
    public bool ToggleCommand(string commandName)
 | 
			
		||||
    {
 | 
			
		||||
        var added = false;
 | 
			
		||||
        _bss.ModifyConfig(bs =>
 | 
			
		||||
        {
 | 
			
		||||
            if (bs.Blocked.Commands.Add(commandName))
 | 
			
		||||
                added = true;
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                bs.Blocked.Commands.Remove(commandName);
 | 
			
		||||
                added = false;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return added;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Resets all global permissions
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Task Reset()
 | 
			
		||||
    {
 | 
			
		||||
        _bss.ModifyConfig(bs =>
 | 
			
		||||
        {
 | 
			
		||||
            bs.Blocked.Commands.Clear();
 | 
			
		||||
            bs.Blocked.Modules.Clear();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Common;
 | 
			
		||||
 | 
			
		||||
public class PermissionCache
 | 
			
		||||
{
 | 
			
		||||
    public string PermRole { get; set; }
 | 
			
		||||
    public bool Verbose { get; set; } = true;
 | 
			
		||||
    public PermissionsCollection<Permissionv2> Permissions { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,132 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Common;
 | 
			
		||||
 | 
			
		||||
public static class PermissionExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static bool CheckPermissions(
 | 
			
		||||
        this IEnumerable<Permissionv2> permsEnumerable,
 | 
			
		||||
        IUser user,
 | 
			
		||||
        IMessageChannel message,
 | 
			
		||||
        string commandName,
 | 
			
		||||
        string moduleName,
 | 
			
		||||
        out int permIndex)
 | 
			
		||||
    {
 | 
			
		||||
        var perms = permsEnumerable as List<Permissionv2> ?? permsEnumerable.ToList();
 | 
			
		||||
 | 
			
		||||
        for (var i = perms.Count - 1; i >= 0; i--)
 | 
			
		||||
        {
 | 
			
		||||
            var perm = perms[i];
 | 
			
		||||
 | 
			
		||||
            var result = perm.CheckPermission(user, message, commandName, moduleName);
 | 
			
		||||
 | 
			
		||||
            if (result is null)
 | 
			
		||||
                continue;
 | 
			
		||||
            permIndex = i;
 | 
			
		||||
            return result.Value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        permIndex = -1; //defaut behaviour
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //null = not applicable
 | 
			
		||||
    //true = applicable, allowed
 | 
			
		||||
    //false = applicable, not allowed
 | 
			
		||||
    public static bool? CheckPermission(
 | 
			
		||||
        this Permissionv2 perm,
 | 
			
		||||
        IUser user,
 | 
			
		||||
        IMessageChannel channel,
 | 
			
		||||
        string commandName,
 | 
			
		||||
        string moduleName)
 | 
			
		||||
    {
 | 
			
		||||
        if (!((perm.SecondaryTarget == SecondaryPermissionType.Command
 | 
			
		||||
               && string.Equals(perm.SecondaryTargetName, commandName, StringComparison.InvariantCultureIgnoreCase))
 | 
			
		||||
              || (perm.SecondaryTarget == SecondaryPermissionType.Module
 | 
			
		||||
                  && string.Equals(perm.SecondaryTargetName, moduleName, StringComparison.InvariantCultureIgnoreCase))
 | 
			
		||||
              || perm.SecondaryTarget == SecondaryPermissionType.AllModules))
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        var guildUser = user as IGuildUser;
 | 
			
		||||
 | 
			
		||||
        switch (perm.PrimaryTarget)
 | 
			
		||||
        {
 | 
			
		||||
            case PrimaryPermissionType.User:
 | 
			
		||||
                if (perm.PrimaryTargetId == user.Id)
 | 
			
		||||
                    return perm.State;
 | 
			
		||||
                break;
 | 
			
		||||
            case PrimaryPermissionType.Channel:
 | 
			
		||||
                if (perm.PrimaryTargetId == channel.Id)
 | 
			
		||||
                    return perm.State;
 | 
			
		||||
                break;
 | 
			
		||||
            case PrimaryPermissionType.Role:
 | 
			
		||||
                if (guildUser is null)
 | 
			
		||||
                    break;
 | 
			
		||||
                if (guildUser.RoleIds.Contains(perm.PrimaryTargetId))
 | 
			
		||||
                    return perm.State;
 | 
			
		||||
                break;
 | 
			
		||||
            case PrimaryPermissionType.Server:
 | 
			
		||||
                if (guildUser is null)
 | 
			
		||||
                    break;
 | 
			
		||||
                return perm.State;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static string GetCommand(this Permissionv2 perm, string prefix, SocketGuild guild = null)
 | 
			
		||||
    {
 | 
			
		||||
        var com = string.Empty;
 | 
			
		||||
        switch (perm.PrimaryTarget)
 | 
			
		||||
        {
 | 
			
		||||
            case PrimaryPermissionType.User:
 | 
			
		||||
                com += "u";
 | 
			
		||||
                break;
 | 
			
		||||
            case PrimaryPermissionType.Channel:
 | 
			
		||||
                com += "c";
 | 
			
		||||
                break;
 | 
			
		||||
            case PrimaryPermissionType.Role:
 | 
			
		||||
                com += "r";
 | 
			
		||||
                break;
 | 
			
		||||
            case PrimaryPermissionType.Server:
 | 
			
		||||
                com += "s";
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch (perm.SecondaryTarget)
 | 
			
		||||
        {
 | 
			
		||||
            case SecondaryPermissionType.Module:
 | 
			
		||||
                com += "m";
 | 
			
		||||
                break;
 | 
			
		||||
            case SecondaryPermissionType.Command:
 | 
			
		||||
                com += "c";
 | 
			
		||||
                break;
 | 
			
		||||
            case SecondaryPermissionType.AllModules:
 | 
			
		||||
                com = "a" + com + "m";
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var secName = perm.SecondaryTarget == SecondaryPermissionType.Command && !perm.IsCustomCommand
 | 
			
		||||
            ? prefix + perm.SecondaryTargetName
 | 
			
		||||
            : perm.SecondaryTargetName;
 | 
			
		||||
        com += " " + (perm.SecondaryTargetName != "*" ? secName + " " : "") + (perm.State ? "enable" : "disable") + " ";
 | 
			
		||||
 | 
			
		||||
        switch (perm.PrimaryTarget)
 | 
			
		||||
        {
 | 
			
		||||
            case PrimaryPermissionType.User:
 | 
			
		||||
                com += guild?.GetUser(perm.PrimaryTargetId)?.ToString() ?? $"<@{perm.PrimaryTargetId}>";
 | 
			
		||||
                break;
 | 
			
		||||
            case PrimaryPermissionType.Channel:
 | 
			
		||||
                com += $"<#{perm.PrimaryTargetId}>";
 | 
			
		||||
                break;
 | 
			
		||||
            case PrimaryPermissionType.Role:
 | 
			
		||||
                com += guild?.GetRole(perm.PrimaryTargetId)?.ToString() ?? $"<@&{perm.PrimaryTargetId}>";
 | 
			
		||||
                break;
 | 
			
		||||
            case PrimaryPermissionType.Server:
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return prefix + com;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,516 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Common;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions;
 | 
			
		||||
 | 
			
		||||
public partial class Permissions : NadekoModule<PermissionService>
 | 
			
		||||
{
 | 
			
		||||
    public enum Reset { Reset }
 | 
			
		||||
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    public Permissions(DbService db)
 | 
			
		||||
        => _db = db;
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task Verbose(PermissionAction action = null)
 | 
			
		||||
    {
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
 | 
			
		||||
            if (action is null)
 | 
			
		||||
                action = new(!config.VerbosePermissions); // New behaviour, can toggle.
 | 
			
		||||
            config.VerbosePermissions = action.Value;
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            _service.UpdateCache(config);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.verbose_true);
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.verbose_false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    [Priority(0)]
 | 
			
		||||
    public async Task PermRole([Leftover] IRole role = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (role is not null && role == role.Guild.EveryoneRole)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (role is null)
 | 
			
		||||
        {
 | 
			
		||||
            var cache = _service.GetCacheFor(ctx.Guild.Id);
 | 
			
		||||
            if (!ulong.TryParse(cache.PermRole, out var roleId)
 | 
			
		||||
                || (role = ((SocketGuild)ctx.Guild).GetRole(roleId)) is null)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.permrole_not_set);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.permrole(Format.Bold(role.ToString())));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
 | 
			
		||||
            config.PermissionRole = role.Id.ToString();
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
            _service.UpdateCache(config);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.permrole_changed(Format.Bold(role.Name)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
    public async Task PermRole(Reset _)
 | 
			
		||||
    {
 | 
			
		||||
        await using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
 | 
			
		||||
            config.PermissionRole = null;
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            _service.UpdateCache(config);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.permrole_reset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task ListPerms(int page = 1)
 | 
			
		||||
    {
 | 
			
		||||
        if (page < 1)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        IList<Permissionv2> perms;
 | 
			
		||||
 | 
			
		||||
        if (_service.Cache.TryGetValue(ctx.Guild.Id, out var permCache))
 | 
			
		||||
            perms = permCache.Permissions.Source.ToList();
 | 
			
		||||
        else
 | 
			
		||||
            perms = Permissionv2.GetDefaultPermlist;
 | 
			
		||||
 | 
			
		||||
        var startPos = 20 * (page - 1);
 | 
			
		||||
        var toSend = Format.Bold(GetText(strs.page(page)))
 | 
			
		||||
                     + "\n\n"
 | 
			
		||||
                     + string.Join("\n",
 | 
			
		||||
                         perms.Reverse()
 | 
			
		||||
                              .Skip(startPos)
 | 
			
		||||
                              .Take(20)
 | 
			
		||||
                              .Select(p =>
 | 
			
		||||
                              {
 | 
			
		||||
                                  var str =
 | 
			
		||||
                                      $"`{p.Index + 1}.` {Format.Bold(p.GetCommand(prefix, (SocketGuild)ctx.Guild))}";
 | 
			
		||||
                                  if (p.Index == 0)
 | 
			
		||||
                                      str += $" [{GetText(strs.uneditable)}]";
 | 
			
		||||
                                  return str;
 | 
			
		||||
                              }));
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.SendMessageAsync(toSend);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task RemovePerm(int index)
 | 
			
		||||
    {
 | 
			
		||||
        index -= 1;
 | 
			
		||||
        if (index < 0)
 | 
			
		||||
            return;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            Permissionv2 p;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
 | 
			
		||||
                var permsCol = new PermissionsCollection<Permissionv2>(config.Permissions);
 | 
			
		||||
                p = permsCol[index];
 | 
			
		||||
                permsCol.RemoveAt(index);
 | 
			
		||||
                uow.Remove(p);
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
                _service.UpdateCache(config);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.removed(index + 1,
 | 
			
		||||
                Format.Code(p.GetCommand(prefix, (SocketGuild)ctx.Guild))));
 | 
			
		||||
        }
 | 
			
		||||
        catch (IndexOutOfRangeException)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.perm_out_of_range);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task MovePerm(int from, int to)
 | 
			
		||||
    {
 | 
			
		||||
        from -= 1;
 | 
			
		||||
        to -= 1;
 | 
			
		||||
        if (!(from == to || from < 0 || to < 0))
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                Permissionv2 fromPerm;
 | 
			
		||||
                await using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
 | 
			
		||||
                    var permsCol = new PermissionsCollection<Permissionv2>(config.Permissions);
 | 
			
		||||
 | 
			
		||||
                    var fromFound = @from < permsCol.Count;
 | 
			
		||||
                    var toFound = to < permsCol.Count;
 | 
			
		||||
 | 
			
		||||
                    if (!fromFound)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync(strs.perm_not_found(++@from));
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (!toFound)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync(strs.perm_not_found(++to));
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    fromPerm = permsCol[@from];
 | 
			
		||||
 | 
			
		||||
                    permsCol.RemoveAt(@from);
 | 
			
		||||
                    permsCol.Insert(to, fromPerm);
 | 
			
		||||
                    await uow.SaveChangesAsync();
 | 
			
		||||
                    _service.UpdateCache(config);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.moved_permission(
 | 
			
		||||
                    Format.Code(fromPerm.GetCommand(prefix, (SocketGuild)ctx.Guild)),
 | 
			
		||||
                    ++@from,
 | 
			
		||||
                    ++to));
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception e) when (e is ArgumentOutOfRangeException or IndexOutOfRangeException)
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.perm_out_of_range);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task SrvrCmd(CommandOrExprInfo command, PermissionAction action)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.Server,
 | 
			
		||||
                PrimaryTargetId = 0,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.Command,
 | 
			
		||||
                SecondaryTargetName = command.Name.ToLowerInvariant(),
 | 
			
		||||
                State = action.Value,
 | 
			
		||||
                IsCustomCommand = command.IsCustom
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.sx_enable(Format.Code(command.Name), GetText(strs.of_command)));
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.sx_disable(Format.Code(command.Name), GetText(strs.of_command)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task SrvrMdl(ModuleOrCrInfo module, PermissionAction action)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.Server,
 | 
			
		||||
                PrimaryTargetId = 0,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.Module,
 | 
			
		||||
                SecondaryTargetName = module.Name.ToLowerInvariant(),
 | 
			
		||||
                State = action.Value
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.sx_enable(Format.Code(module.Name), GetText(strs.of_module)));
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.sx_disable(Format.Code(module.Name), GetText(strs.of_module)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task UsrCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.User,
 | 
			
		||||
                PrimaryTargetId = user.Id,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.Command,
 | 
			
		||||
                SecondaryTargetName = command.Name.ToLowerInvariant(),
 | 
			
		||||
                State = action.Value,
 | 
			
		||||
                IsCustomCommand = command.IsCustom
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.ux_enable(Format.Code(command.Name),
 | 
			
		||||
                GetText(strs.of_command),
 | 
			
		||||
                Format.Code(user.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.ux_disable(Format.Code(command.Name),
 | 
			
		||||
                GetText(strs.of_command),
 | 
			
		||||
                Format.Code(user.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task UsrMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.User,
 | 
			
		||||
                PrimaryTargetId = user.Id,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.Module,
 | 
			
		||||
                SecondaryTargetName = module.Name.ToLowerInvariant(),
 | 
			
		||||
                State = action.Value
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.ux_enable(Format.Code(module.Name),
 | 
			
		||||
                GetText(strs.of_module),
 | 
			
		||||
                Format.Code(user.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.ux_disable(Format.Code(module.Name),
 | 
			
		||||
                GetText(strs.of_module),
 | 
			
		||||
                Format.Code(user.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task RoleCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] IRole role)
 | 
			
		||||
    {
 | 
			
		||||
        if (role == role.Guild.EveryoneRole)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.Role,
 | 
			
		||||
                PrimaryTargetId = role.Id,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.Command,
 | 
			
		||||
                SecondaryTargetName = command.Name.ToLowerInvariant(),
 | 
			
		||||
                State = action.Value,
 | 
			
		||||
                IsCustomCommand = command.IsCustom
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.rx_enable(Format.Code(command.Name),
 | 
			
		||||
                GetText(strs.of_command),
 | 
			
		||||
                Format.Code(role.Name)));
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.rx_disable(Format.Code(command.Name),
 | 
			
		||||
                GetText(strs.of_command),
 | 
			
		||||
                Format.Code(role.Name)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task RoleMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IRole role)
 | 
			
		||||
    {
 | 
			
		||||
        if (role == role.Guild.EveryoneRole)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.Role,
 | 
			
		||||
                PrimaryTargetId = role.Id,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.Module,
 | 
			
		||||
                SecondaryTargetName = module.Name.ToLowerInvariant(),
 | 
			
		||||
                State = action.Value
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.rx_enable(Format.Code(module.Name),
 | 
			
		||||
                GetText(strs.of_module),
 | 
			
		||||
                Format.Code(role.Name)));
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.rx_disable(Format.Code(module.Name),
 | 
			
		||||
                GetText(strs.of_module),
 | 
			
		||||
                Format.Code(role.Name)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task ChnlCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] ITextChannel chnl)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.Channel,
 | 
			
		||||
                PrimaryTargetId = chnl.Id,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.Command,
 | 
			
		||||
                SecondaryTargetName = command.Name.ToLowerInvariant(),
 | 
			
		||||
                State = action.Value,
 | 
			
		||||
                IsCustomCommand = command.IsCustom
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.cx_enable(Format.Code(command.Name),
 | 
			
		||||
                GetText(strs.of_command),
 | 
			
		||||
                Format.Code(chnl.Name)));
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.cx_disable(Format.Code(command.Name),
 | 
			
		||||
                GetText(strs.of_command),
 | 
			
		||||
                Format.Code(chnl.Name)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task ChnlMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] ITextChannel chnl)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.Channel,
 | 
			
		||||
                PrimaryTargetId = chnl.Id,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.Module,
 | 
			
		||||
                SecondaryTargetName = module.Name.ToLowerInvariant(),
 | 
			
		||||
                State = action.Value
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.cx_enable(Format.Code(module.Name),
 | 
			
		||||
                GetText(strs.of_module),
 | 
			
		||||
                Format.Code(chnl.Name)));
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.cx_disable(Format.Code(module.Name),
 | 
			
		||||
                GetText(strs.of_module),
 | 
			
		||||
                Format.Code(chnl.Name)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task AllChnlMdls(PermissionAction action, [Leftover] ITextChannel chnl)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.Channel,
 | 
			
		||||
                PrimaryTargetId = chnl.Id,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.AllModules,
 | 
			
		||||
                SecondaryTargetName = "*",
 | 
			
		||||
                State = action.Value
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.acm_enable(Format.Code(chnl.Name)));
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.acm_disable(Format.Code(chnl.Name)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task AllRoleMdls(PermissionAction action, [Leftover] IRole role)
 | 
			
		||||
    {
 | 
			
		||||
        if (role == role.Guild.EveryoneRole)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.Role,
 | 
			
		||||
                PrimaryTargetId = role.Id,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.AllModules,
 | 
			
		||||
                SecondaryTargetName = "*",
 | 
			
		||||
                State = action.Value
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.arm_enable(Format.Code(role.Name)));
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.arm_disable(Format.Code(role.Name)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task AllUsrMdls(PermissionAction action, [Leftover] IUser user)
 | 
			
		||||
    {
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id,
 | 
			
		||||
            new Permissionv2
 | 
			
		||||
            {
 | 
			
		||||
                PrimaryTarget = PrimaryPermissionType.User,
 | 
			
		||||
                PrimaryTargetId = user.Id,
 | 
			
		||||
                SecondaryTarget = SecondaryPermissionType.AllModules,
 | 
			
		||||
                SecondaryTargetName = "*",
 | 
			
		||||
                State = action.Value
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.aum_enable(Format.Code(user.ToString())));
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.aum_disable(Format.Code(user.ToString())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task AllSrvrMdls(PermissionAction action)
 | 
			
		||||
    {
 | 
			
		||||
        var newPerm = new Permissionv2
 | 
			
		||||
        {
 | 
			
		||||
            PrimaryTarget = PrimaryPermissionType.Server,
 | 
			
		||||
            PrimaryTargetId = 0,
 | 
			
		||||
            SecondaryTarget = SecondaryPermissionType.AllModules,
 | 
			
		||||
            SecondaryTargetName = "*",
 | 
			
		||||
            State = action.Value
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var allowUser = new Permissionv2
 | 
			
		||||
        {
 | 
			
		||||
            PrimaryTarget = PrimaryPermissionType.User,
 | 
			
		||||
            PrimaryTargetId = ctx.User.Id,
 | 
			
		||||
            SecondaryTarget = SecondaryPermissionType.AllModules,
 | 
			
		||||
            SecondaryTargetName = "*",
 | 
			
		||||
            State = true
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await _service.AddPermissions(ctx.Guild.Id, newPerm, allowUser);
 | 
			
		||||
 | 
			
		||||
        if (action.Value)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.asm_enable);
 | 
			
		||||
        else
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.asm_disable);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user