mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-03 16:24:27 -05:00 
			
		
		
		
	More restructuring
This commit is contained in:
		
							
								
								
									
										405
									
								
								src/Nadeko.Bot.Modules.Administration/Administration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										405
									
								
								src/Nadeko.Bot.Modules.Administration/Administration.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,405 @@
 | 
			
		||||
#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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										158
									
								
								src/Nadeko.Bot.Modules.Administration/AdministrationService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								src/Nadeko.Bot.Modules.Administration/AdministrationService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,158 @@
 | 
			
		||||
#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(
 | 
			
		||||
        IBot 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
#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")));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,159 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Threading.Channels;
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
 | 
			
		||||
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, IBot 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(',');
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								src/Nadeko.Bot.Modules.Administration/AutoPublishService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/Nadeko.Bot.Modules.Administration/AutoPublishService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
#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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,130 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -0,0 +1,172 @@
 | 
			
		||||
#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; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
#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)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,127 @@
 | 
			
		||||
#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, IBot 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/Nadeko.Bot.Modules.Administration/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/Nadeko.Bot.Modules.Administration/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
// // global using System.Collections.Concurrent;
 | 
			
		||||
global using NonBlocking;
 | 
			
		||||
//
 | 
			
		||||
// // packages
 | 
			
		||||
global using Serilog;
 | 
			
		||||
global using Humanizer;
 | 
			
		||||
global using Newtonsoft;
 | 
			
		||||
//
 | 
			
		||||
// // nadekobot
 | 
			
		||||
// global using NadekoBot;
 | 
			
		||||
global using NadekoBot.Services;
 | 
			
		||||
global using Nadeko.Common; // new project
 | 
			
		||||
global using NadekoBot.Common; // old + nadekobot specific things
 | 
			
		||||
global using NadekoBot.Common.Attributes;
 | 
			
		||||
global using NadekoBot.Extensions;
 | 
			
		||||
// global using Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
// discord
 | 
			
		||||
global using Discord;
 | 
			
		||||
global using Discord.Commands;
 | 
			
		||||
global using Discord.Net;
 | 
			
		||||
global using Discord.WebSocket;
 | 
			
		||||
 | 
			
		||||
// aliases
 | 
			
		||||
global using GuildPerm = Discord.GuildPermission;
 | 
			
		||||
global using ChannelPerm = Discord.ChannelPermission;
 | 
			
		||||
global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute;
 | 
			
		||||
global using LeftoverAttribute = Discord.Commands.RemainderAttribute;
 | 
			
		||||
global using TypeReaderResult = NadekoBot.Common.TypeReaders.TypeReaderResult;
 | 
			
		||||
 | 
			
		||||
// non-essential
 | 
			
		||||
// global using JetBrains.Annotations;
 | 
			
		||||
							
								
								
									
										229
									
								
								src/Nadeko.Bot.Modules.Administration/GreetBye/GreetCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								src/Nadeko.Bot.Modules.Administration/GreetBye/GreetCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,229 @@
 | 
			
		||||
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`"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,71 @@
 | 
			
		||||
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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										619
									
								
								src/Nadeko.Bot.Modules.Administration/GreetBye/GreetService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										619
									
								
								src/Nadeko.Bot.Modules.Administration/GreetBye/GreetService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,619 @@
 | 
			
		||||
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,
 | 
			
		||||
        IBot 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
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
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
 | 
			
		||||
        };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										235
									
								
								src/Nadeko.Bot.Modules.Administration/ImageOnlyChannelService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								src/Nadeko.Bot.Modules.Administration/ImageOnlyChannelService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,235 @@
 | 
			
		||||
#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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										250
									
								
								src/Nadeko.Bot.Modules.Administration/LocalizationCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								src/Nadeko.Bot.Modules.Administration/LocalizationCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,250 @@
 | 
			
		||||
#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" }
 | 
			
		||||
 */
 | 
			
		||||
							
								
								
									
										231
									
								
								src/Nadeko.Bot.Modules.Administration/Mute/MuteCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/Nadeko.Bot.Modules.Administration/Mute/MuteCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,231 @@
 | 
			
		||||
#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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										506
									
								
								src/Nadeko.Bot.Modules.Administration/Mute/MuteService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								src/Nadeko.Bot.Modules.Administration/Mute/MuteService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,506 @@
 | 
			
		||||
#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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
			
		||||
 | 
			
		||||
    <PropertyGroup>
 | 
			
		||||
        <TargetFramework>net7.0</TargetFramework>
 | 
			
		||||
        <ImplicitUsings>enable</ImplicitUsings>
 | 
			
		||||
        <Nullable>enable</Nullable>
 | 
			
		||||
    </PropertyGroup>
 | 
			
		||||
    
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
        <PackageReference Include="Discord.Net" Version="3.104.0" />
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
    
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
      <ProjectReference Include="..\Nadeko.Bot.Common\Nadeko.Bot.Common.csproj" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
@@ -0,0 +1,83 @@
 | 
			
		||||
#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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,136 @@
 | 
			
		||||
#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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
#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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,109 @@
 | 
			
		||||
#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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
#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)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,288 @@
 | 
			
		||||
#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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,499 @@
 | 
			
		||||
#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,
 | 
			
		||||
        IBot 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
#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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,64 @@
 | 
			
		||||
#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();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										114
									
								
								src/Nadeko.Bot.Modules.Administration/Prune/PruneCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/Nadeko.Bot.Modules.Administration/Prune/PruneCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
#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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								src/Nadeko.Bot.Modules.Administration/Prune/PruneService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/Nadeko.Bot.Modules.Administration/Prune/PruneService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
#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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,171 @@
 | 
			
		||||
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);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,396 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Utility.Patronage;
 | 
			
		||||
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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										187
									
								
								src/Nadeko.Bot.Modules.Administration/Role/RoleCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								src/Nadeko.Bot.Modules.Administration/Role/RoleCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
#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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,153 @@
 | 
			
		||||
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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										572
									
								
								src/Nadeko.Bot.Modules.Administration/Self/SelfCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										572
									
								
								src/Nadeko.Bot.Modules.Administration/Self/SelfCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,572 @@
 | 
			
		||||
#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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										400
									
								
								src/Nadeko.Bot.Modules.Administration/Self/SelfService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										400
									
								
								src/Nadeko.Bot.Modules.Administration/Self/SelfService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,400 @@
 | 
			
		||||
#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; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,234 @@
 | 
			
		||||
#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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,234 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
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
											
										
									
								
							@@ -0,0 +1,165 @@
 | 
			
		||||
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())));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,78 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class GuildTimezoneService : ITimezoneService, INService
 | 
			
		||||
{
 | 
			
		||||
    public static ConcurrentDictionary<ulong, GuildTimezoneService> AllServices { get; } = new();
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, TimeZoneInfo> _timezones;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    public GuildTimezoneService(DiscordSocketClient client, IBot 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 (guildId is ulong gid && _timezones.TryGetValue(gid, 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;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,75 @@
 | 
			
		||||
#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());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,932 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using Humanizer.Localisation;
 | 
			
		||||
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 UserPunishCommands : NadekoModule<UserPunishService>
 | 
			
		||||
    {
 | 
			
		||||
        public enum AddRole
 | 
			
		||||
        {
 | 
			
		||||
            AddRole
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly MuteService _mute;
 | 
			
		||||
 | 
			
		||||
        public UserPunishCommands(MuteService mute)
 | 
			
		||||
        {
 | 
			
		||||
            _mute = mute;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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()
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,595 @@
 | 
			
		||||
#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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,77 @@
 | 
			
		||||
#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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										210
									
								
								src/Nadeko.Bot.Modules.Administration/VcRole/VcRoleService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/Nadeko.Bot.Modules.Administration/VcRole/VcRoleService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
			
		||||
#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, IBot 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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user