mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Restructured folders and project names, ci should be fixed
This commit is contained in:
		
							
								
								
									
										335
									
								
								src/NadekoBot/Modules/Administration/Administration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										335
									
								
								src/NadekoBot/Modules/Administration/Administration.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,335 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration : NadekoModule<AdministrationService>
 | 
			
		||||
    {
 | 
			
		||||
        public enum List
 | 
			
		||||
        {
 | 
			
		||||
            List = 0,
 | 
			
		||||
            Ls = 0
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(ChannelPerm.ManageChannel)]
 | 
			
		||||
        [BotPerm(ChannelPerm.ManageChannel)]
 | 
			
		||||
        public async Task Slowmode(StoopidTime time = null)
 | 
			
		||||
        {
 | 
			
		||||
            var seconds = (int?)time?.Time.TotalSeconds ?? 0;
 | 
			
		||||
            if (!(time is null) && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6)))
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            await ((ITextChannel) Context.Channel).ModifyAsync(tcp =>
 | 
			
		||||
            {
 | 
			
		||||
                tcp.SlowModeInterval = seconds;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            await Context.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [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 = new EmbedBuilder()
 | 
			
		||||
                .WithOkColor()
 | 
			
		||||
                .WithTitle(GetText("server_delmsgoncmd"))
 | 
			
		||||
                .WithDescription(enabled ? "✅" : "❌");
 | 
			
		||||
 | 
			
		||||
            var str = string.Join("\n", channels
 | 
			
		||||
                .Select(x =>
 | 
			
		||||
                {
 | 
			
		||||
                    var ch = guild.GetChannel(x.ChannelId)?.ToString()
 | 
			
		||||
                             ?? x.ChannelId.ToString();
 | 
			
		||||
                    var prefix = x.State ? "✅ " : "❌ ";
 | 
			
		||||
                    return prefix + ch;
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(str))
 | 
			
		||||
                str = "-";
 | 
			
		||||
 | 
			
		||||
            embed.AddField(GetText("channel_delmsgoncmd"), str);
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum Server
 | 
			
		||||
        {
 | 
			
		||||
            Server
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [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("delmsg_on").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _service.DeleteMessagesOnCommand.TryRemove(ctx.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("delmsg_off").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum Channel
 | 
			
		||||
        {
 | 
			
		||||
            Channel,
 | 
			
		||||
            Ch,
 | 
			
		||||
            Chnl,
 | 
			
		||||
            Chan
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum State
 | 
			
		||||
        {
 | 
			
		||||
            Enable,
 | 
			
		||||
            Disable,
 | 
			
		||||
            Inherit
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task Delmsgoncmd(Channel _, State s, ITextChannel ch)
 | 
			
		||||
            => Delmsgoncmd(_, s, ch.Id);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [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).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (s == State.Disable)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("delmsg_channel_off").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else if (s == State.Enable)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("delmsg_channel_on").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("delmsg_channel_inherit").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.DeafenMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.DeafenMembers)]
 | 
			
		||||
        public async Task Deafen(params IGuildUser[] users)
 | 
			
		||||
        {
 | 
			
		||||
            await _service.DeafenUsers(true, users).ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("deafen").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.DeafenMembers)]
 | 
			
		||||
        [BotPerm(GuildPerm.DeafenMembers)]
 | 
			
		||||
        public async Task UnDeafen(params IGuildUser[] users)
 | 
			
		||||
        {
 | 
			
		||||
            await _service.DeafenUsers(false, users).ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("undeafen").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        public async Task DelVoiChanl([Leftover] IVoiceChannel voiceChannel)
 | 
			
		||||
        {
 | 
			
		||||
            await voiceChannel.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("delvoich", Format.Bold(voiceChannel.Name)).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        public async Task CreatVoiChanl([Leftover] string channelName)
 | 
			
		||||
        {
 | 
			
		||||
            var ch = await ctx.Guild.CreateVoiceChannelAsync(channelName).ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("createvoich", Format.Bold(ch.Name)).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        public async Task DelTxtChanl([Leftover] ITextChannel toDelete)
 | 
			
		||||
        {
 | 
			
		||||
            await toDelete.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("deltextchan", Format.Bold(toDelete.Name)).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        public async Task CreaTxtChanl([Leftover] string channelName)
 | 
			
		||||
        {
 | 
			
		||||
            var txtCh = await ctx.Guild.CreateTextChannelAsync(channelName).ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("createtextchan", Format.Bold(txtCh.Name)).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageChannels)]
 | 
			
		||||
        public async Task SetTopic([Leftover] string topic = null)
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel) ctx.Channel;
 | 
			
		||||
            topic = topic ?? "";
 | 
			
		||||
            await channel.ModifyAsync(c => c.Topic = topic).ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("set_topic").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [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).ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("set_channel_name").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [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).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (isEnabled)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("nsfw_set_false").ConfigureAwait(false);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("nsfw_set_true").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task Edit(ulong messageId, [Leftover] string text)
 | 
			
		||||
            => Edit((ITextChannel) ctx.Channel, messageId, text);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [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("insuf_perms_u").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!botPerms.Has(ChannelPermission.ViewChannel))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("insuf_perms_i").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _service.EditMessage(ctx, channel, messageId, text);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
        [BotPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
        public Task Delete(ulong messageId, StoopidTime time = null)
 | 
			
		||||
            => Delete((ITextChannel) ctx.Channel, messageId, time);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [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("insuf_perms_u").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!botPerms.Has(ChannelPermission.ManageMessages))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("insuf_perms_i").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            var msg = await channel.GetMessageAsync(messageId).ConfigureAwait(false);
 | 
			
		||||
            if (msg == null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("msg_not_found").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (time == null)
 | 
			
		||||
            {
 | 
			
		||||
                await msg.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else if (time.Time <= TimeSpan.FromDays(7))
 | 
			
		||||
            {
 | 
			
		||||
                var _ = Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    await Task.Delay(time.Time).ConfigureAwait(false);
 | 
			
		||||
                    await msg.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("time_too_long").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,72 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class AutoAssignRoleCommands : NadekoSubmodule<AutoAssignRoleService>
 | 
			
		||||
        {
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(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("hierarchy");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id);
 | 
			
		||||
                if (roles.Count == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("aar_disabled");
 | 
			
		||||
                }
 | 
			
		||||
                else if (roles.Contains(role.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    await AutoAssignRole();
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("aar_role_removed", Format.Bold(role.ToString()));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            public async Task AutoAssignRole()
 | 
			
		||||
            {
 | 
			
		||||
                if (!_service.TryGetRoles(ctx.Guild.Id, out var roles))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("aar_none");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                var existing = roles.Select(rid => ctx.Guild.GetRole(rid)).Where(r => !(r is null))
 | 
			
		||||
                    .ToList();
 | 
			
		||||
 | 
			
		||||
                if (existing.Count != roles.Count)
 | 
			
		||||
                {
 | 
			
		||||
                    await _service.SetAarRolesAsync(ctx.Guild.Id, existing.Select(x => x.Id));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("aar_roles",
 | 
			
		||||
                    '\n' + existing.Select(x => Format.Bold(x.ToString()))
 | 
			
		||||
                        .JoinWith(",\n"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,50 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Common
 | 
			
		||||
{
 | 
			
		||||
    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 ConcurrentHashSet<IGuildUser>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class AntiSpamStats
 | 
			
		||||
    {
 | 
			
		||||
        public AntiSpamSetting AntiSpamSettings { get; set; }
 | 
			
		||||
        public ConcurrentDictionary<ulong, UserSpamStats> UserStats { get; set; }
 | 
			
		||||
            = new ConcurrentDictionary<ulong, UserSpamStats>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class AntiAltStats
 | 
			
		||||
    {
 | 
			
		||||
        private readonly AntiAltSetting _setting;
 | 
			
		||||
        public PunishmentAction Action => _setting.Action;
 | 
			
		||||
        public int ActionDurationMinutes => _setting.ActionDurationMinutes;
 | 
			
		||||
        public ulong? RoleId => _setting.RoleId;
 | 
			
		||||
        public TimeSpan MinAge => _setting.MinAge;
 | 
			
		||||
        
 | 
			
		||||
        private int _counter = 0;
 | 
			
		||||
        public int Counter => _counter;
 | 
			
		||||
 | 
			
		||||
        public AntiAltStats(AntiAltSetting setting)
 | 
			
		||||
        {
 | 
			
		||||
            _setting = setting;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public void Increment() => Interlocked.Increment(ref _counter);
 | 
			
		||||
        
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Common
 | 
			
		||||
{
 | 
			
		||||
    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; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								src/NadekoBot/Modules/Administration/Common/UserSpamStats.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/NadekoBot/Modules/Administration/Common/UserSpamStats.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using Discord;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Common
 | 
			
		||||
{
 | 
			
		||||
    public sealed class UserSpamStats : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        public int Count => timers.Count;
 | 
			
		||||
        public string LastMessage { get; set; }
 | 
			
		||||
 | 
			
		||||
        private ConcurrentQueue<Timer> timers { get; }
 | 
			
		||||
 | 
			
		||||
        public UserSpamStats(IUserMessage msg)
 | 
			
		||||
        {
 | 
			
		||||
            LastMessage = msg.Content.ToUpperInvariant();
 | 
			
		||||
            timers = new ConcurrentQueue<Timer>();
 | 
			
		||||
 | 
			
		||||
            ApplyNextMessage(msg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly object applyLock = new object();
 | 
			
		||||
        public void ApplyNextMessage(IUserMessage message)
 | 
			
		||||
        {
 | 
			
		||||
            lock (applyLock)
 | 
			
		||||
            {
 | 
			
		||||
                var upperMsg = message.Content.ToUpperInvariant();
 | 
			
		||||
                if (upperMsg != LastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any()))
 | 
			
		||||
                {
 | 
			
		||||
                    LastMessage = upperMsg;
 | 
			
		||||
                    while (timers.TryDequeue(out var old))
 | 
			
		||||
                        old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                }
 | 
			
		||||
                var t = new Timer((_) => {
 | 
			
		||||
                    if (timers.TryDequeue(out var old))
 | 
			
		||||
                        old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                }, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30));
 | 
			
		||||
                timers.Enqueue(t);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            while (timers.TryDequeue(out var old))
 | 
			
		||||
                old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										132
									
								
								src/NadekoBot/Modules/Administration/DangerousCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/NadekoBot/Modules/Administration/DangerousCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Core.Modules.Administration.Services;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
 | 
			
		||||
#if !GLOBAL_NADEKO
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public class DangerousCommands : NadekoSubmodule<DangerousCommandsService>
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            private async Task InternalExecSql(string sql, params object[] reps)
 | 
			
		||||
            {
 | 
			
		||||
                sql = string.Format(sql, reps);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var embed = new EmbedBuilder()
 | 
			
		||||
                        .WithTitle(GetText("sql_confirm_exec"))
 | 
			
		||||
                        .WithDescription(Format.Code(sql));
 | 
			
		||||
 | 
			
		||||
                    if (!await PromptUserConfirmAsync(embed).ConfigureAwait(false))
 | 
			
		||||
                    {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var res = await _service.ExecuteSql(sql).ConfigureAwait(false);
 | 
			
		||||
                    await ctx.Channel.SendConfirmAsync(res.ToString()).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendErrorAsync(ex.ToString()).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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);
 | 
			
		||||
 | 
			
		||||
                    if (!items.Any())
 | 
			
		||||
                    {
 | 
			
		||||
                        return new EmbedBuilder()
 | 
			
		||||
                            .WithErrorColor()
 | 
			
		||||
                            .WithFooter(sql)
 | 
			
		||||
                            .WithDescription("-");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return new EmbedBuilder()
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithFooter(sql)
 | 
			
		||||
                        .WithTitle(string.Join(" ║ ", result.ColumnNames))
 | 
			
		||||
                        .WithDescription(string.Join('\n', items.Select(x => string.Join(" ║ ", x))));
 | 
			
		||||
 | 
			
		||||
                }, result.Results.Count, 20);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task SqlExec([Leftover]string sql) =>
 | 
			
		||||
                InternalExecSql(sql);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeleteWaifus() =>
 | 
			
		||||
                SqlExec(DangerousCommandsService.WaifusDeleteSql);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeleteWaifu(IUser user) =>
 | 
			
		||||
                DeleteWaifu(user.Id);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeleteWaifu(ulong userId) =>
 | 
			
		||||
                InternalExecSql(DangerousCommandsService.WaifuDeleteSql, userId);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeleteCurrency() =>
 | 
			
		||||
                SqlExec(DangerousCommandsService.CurrencyDeleteSql);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeletePlaylists() =>
 | 
			
		||||
                SqlExec(DangerousCommandsService.MusicPlaylistDeleteSql);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task DeleteXp() =>
 | 
			
		||||
                SqlExec(DangerousCommandsService.XpDeleteSql);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task PurgeUser(ulong userId)
 | 
			
		||||
            {
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithDescription(GetText("purge_user_confirm", Format.Bold(userId.ToString())));
 | 
			
		||||
 | 
			
		||||
                if (!await PromptUserConfirmAsync(embed).ConfigureAwait(false))
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                await _service.PurgeUserAsync(userId);
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task PurgeUser([Leftover]IUser user)
 | 
			
		||||
                => PurgeUser(user.Id);
 | 
			
		||||
            //[NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            //[OwnerOnly]
 | 
			
		||||
            //public Task DeleteUnusedCrnQ() =>
 | 
			
		||||
            //    SqlExec(DangerousCommandsService.DeleteUnusedCustomReactionsAndQuotes);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -0,0 +1,87 @@
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class DiscordPermOverrideCommands : NadekoSubmodule<DiscordPermOverrideService>
 | 
			
		||||
        {
 | 
			
		||||
            // override stats, it should require that the user has managessages guild permission
 | 
			
		||||
            // .po 'stats' add user guild managemessages
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task DiscordPermOverride(CommandOrCrInfo cmd, params GuildPerm[] perms)
 | 
			
		||||
            {
 | 
			
		||||
                if (perms is null || perms.Length == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await _service.RemoveOverride(ctx.Guild.Id, cmd.Name);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("perm_override_reset");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var aggregatePerms = perms.Aggregate((acc, seed) => seed | acc);
 | 
			
		||||
                await _service.AddOverride(Context.Guild.Id, cmd.Name, aggregatePerms);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("perm_override",
 | 
			
		||||
                    Format.Bold(aggregatePerms.ToString()),
 | 
			
		||||
                    Format.Code(cmd.Name));
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task DiscordPermOverrideReset()
 | 
			
		||||
            {
 | 
			
		||||
                var result = await PromptUserConfirmAsync(new EmbedBuilder()
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithDescription(GetText("perm_override_all_confirm")));
 | 
			
		||||
                
 | 
			
		||||
                if (!result)
 | 
			
		||||
                    return;
 | 
			
		||||
                await _service.ClearAllOverrides(Context.Guild.Id);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("perm_override_all");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task DiscordPermOverrideList(int page = 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (--page < 0)
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
                var overrides = await _service.GetAllOverrides(Context.Guild.Id);
 | 
			
		||||
 | 
			
		||||
                await ctx.SendPaginatedConfirmAsync(page, curPage =>
 | 
			
		||||
                {
 | 
			
		||||
                    var eb = new EmbedBuilder()
 | 
			
		||||
                        .WithTitle(GetText("perm_overrides"))
 | 
			
		||||
                        .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                    var thisPageOverrides = overrides
 | 
			
		||||
                        .Skip(9 * curPage)
 | 
			
		||||
                        .Take(9)
 | 
			
		||||
                        .ToList();
 | 
			
		||||
 | 
			
		||||
                    if (thisPageOverrides.Count == 0)
 | 
			
		||||
                        eb.WithDescription(GetText("perm_override_page_none"));
 | 
			
		||||
                    else
 | 
			
		||||
                        eb.WithDescription(string.Join("\n",
 | 
			
		||||
                            thisPageOverrides.Select(ov => $"{ov.Command} => {ov.Perm.ToString()}")));
 | 
			
		||||
 | 
			
		||||
                    return eb;
 | 
			
		||||
                }, overrides.Count, 9, true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/NadekoBot/Modules/Administration/GameChannelCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/NadekoBot/Modules/Administration/GameChannelCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class GameChannelCommands : NadekoSubmodule<GameVoiceChannelService>
 | 
			
		||||
        {
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [BotPerm(GuildPerm.MoveMembers)]
 | 
			
		||||
            public async Task GameVoiceChannel()
 | 
			
		||||
            {
 | 
			
		||||
                var vch = ((IGuildUser)ctx.User).VoiceChannel;
 | 
			
		||||
 | 
			
		||||
                if (vch == null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("not_in_voice").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                var id = _service.ToggleGameVoiceChannel(ctx.Guild.Id, vch.Id);
 | 
			
		||||
 | 
			
		||||
                if (id == null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("gvc_disabled").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    _service.GameVoiceChannels.Add(vch.Id);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("gvc_enabled", Format.Bold(vch.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										263
									
								
								src/NadekoBot/Modules/Administration/LocalizationCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								src/NadekoBot/Modules/Administration/LocalizationCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,263 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class LocalizationCommands : NadekoSubmodule
 | 
			
		||||
        {
 | 
			
		||||
            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", "Українська, Україна"}
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public async Task LanguageSet()
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("lang_set_show", Format.Bold(_cultureInfo.ToString()),
 | 
			
		||||
                        Format.Bold(_cultureInfo.NativeName))
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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 CultureInfo(name);
 | 
			
		||||
                        Localization.SetGuildCulture(ctx.Guild, ci);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("lang_set", Format.Bold(ci.ToString()), Format.Bold(ci.NativeName))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("lang_set_fail").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task LanguageSetDefault()
 | 
			
		||||
            {
 | 
			
		||||
                var cul = Localization.DefaultCultureInfo;
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("lang_set_bot_show", cul, cul.NativeName).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task LanguageSetDefault(string name)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    CultureInfo ci;
 | 
			
		||||
                    if (name.Trim().ToLowerInvariant() == "default")
 | 
			
		||||
                    {
 | 
			
		||||
                        Localization.ResetDefaultCulture();
 | 
			
		||||
                        ci = Localization.DefaultCultureInfo;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        ci = new CultureInfo(name);
 | 
			
		||||
                        Localization.SetDefaultCulture(ci);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("lang_set_bot", Format.Bold(ci.ToString()),
 | 
			
		||||
                        Format.Bold(ci.NativeName)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("lang_set_fail").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task LanguagesList()
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("lang_list"))
 | 
			
		||||
                    .WithDescription(string.Join("\n",
 | 
			
		||||
                        supportedLocales.Select(x => $"{Format.Code(x.Key),-10} => {x.Value}")))).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
/* 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" }
 | 
			
		||||
 */
 | 
			
		||||
							
								
								
									
										134
									
								
								src/NadekoBot/Modules/Administration/LogCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/NadekoBot/Modules/Administration/LogCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
#if !GLOBAL_NADEKO
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using static NadekoBot.Modules.Administration.Services.LogCommandService;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        [NoPublicBot]
 | 
			
		||||
        public class LogCommands : NadekoSubmodule<LogCommandService>
 | 
			
		||||
        {
 | 
			
		||||
            public enum EnableDisable
 | 
			
		||||
            {
 | 
			
		||||
                Enable,
 | 
			
		||||
                Disable
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task LogServer(PermissionAction action)
 | 
			
		||||
            {
 | 
			
		||||
                await _service.LogServer(ctx.Guild.Id, ctx.Channel.Id, action.Value).ConfigureAwait(false);
 | 
			
		||||
                if (action.Value)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("log_all").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("log_disabled").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task LogIgnore()
 | 
			
		||||
            {
 | 
			
		||||
                var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
                var removed = _service.LogIgnore(ctx.Guild.Id, ctx.Channel.Id);
 | 
			
		||||
 | 
			
		||||
                if (!removed)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("log_ignore", Format.Bold(channel.Mention + "(" + channel.Id + ")")).ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("log_not_ignore", Format.Bold(channel.Mention + "(" + channel.Id + ")")).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task LogEvents()
 | 
			
		||||
            {
 | 
			
		||||
                _service.GuildLogSettings.TryGetValue(ctx.Guild.Id, out LogSetting l);
 | 
			
		||||
                var str = string.Join("\n", Enum.GetNames(typeof(LogType))
 | 
			
		||||
                    .Select(x =>
 | 
			
		||||
                    {
 | 
			
		||||
                        var val = l == null ? null : GetLogProperty(l, Enum.Parse<LogType>(x));
 | 
			
		||||
                        if (val != null)
 | 
			
		||||
                            return $"{Format.Bold(x)} <#{val}>";
 | 
			
		||||
                        return Format.Bold(x);
 | 
			
		||||
                    }));
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.SendConfirmAsync(Format.Bold(GetText("log_events")) + "\n" +
 | 
			
		||||
                    str)
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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("log", Format.Bold(type.ToString())).ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("log_stop", Format.Bold(type.ToString())).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										91
									
								
								src/NadekoBot/Modules/Administration/ModuleCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/NadekoBot/Modules/Administration/ModuleCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
//using Discord.Commands;
 | 
			
		||||
//using NadekoBot.Common.Attributes;
 | 
			
		||||
//using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
//using NadekoBot.Extensions;
 | 
			
		||||
//using System;
 | 
			
		||||
//using System.IO;
 | 
			
		||||
//using System.Reflection;
 | 
			
		||||
//using System.Text.RegularExpressions;
 | 
			
		||||
//using System.Threading.Tasks;
 | 
			
		||||
//using System.Linq;
 | 
			
		||||
 | 
			
		||||
//namespace NadekoBot.Modules.Administration
 | 
			
		||||
//{
 | 
			
		||||
//    public partial class Administration
 | 
			
		||||
//    {
 | 
			
		||||
//        [Group]
 | 
			
		||||
//        public class PackagesCommands : NadekoSubmodule<PackagesService>
 | 
			
		||||
//        {
 | 
			
		||||
//            private readonly NadekoBot _bot;
 | 
			
		||||
 | 
			
		||||
//            public PackagesCommands(NadekoBot bot)
 | 
			
		||||
//            {
 | 
			
		||||
//                _bot = bot;
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
//            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//            [RequireContext(ContextType.Guild)]
 | 
			
		||||
//            public async Task PackageList()
 | 
			
		||||
//            {
 | 
			
		||||
//                _service.ReloadAvailablePackages();
 | 
			
		||||
//                await Context.Channel.SendConfirmAsync(
 | 
			
		||||
//                    string.Join(
 | 
			
		||||
//                        "\n", 
 | 
			
		||||
//                        _service.Packages
 | 
			
		||||
//                            .Select(x => _bot.LoadedPackages.Contains(x)
 | 
			
		||||
//                                ? "【✘】" + x
 | 
			
		||||
//                                : "【  】" + x)));
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
//            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//            [RequireContext(ContextType.Guild)]
 | 
			
		||||
//            [OwnerOnly]
 | 
			
		||||
//            public async Task PackageUnload(string name)
 | 
			
		||||
//            {
 | 
			
		||||
//                if (name.Contains(":") || name.Contains(".") || name.Contains("\\") || name.Contains("/") || name.Contains("~"))
 | 
			
		||||
//                    return;
 | 
			
		||||
//                name = name.ToTitleCase();
 | 
			
		||||
//                var package = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory,
 | 
			
		||||
//                                                "modules",
 | 
			
		||||
//                                                $"NadekoBot.Modules.{name}",
 | 
			
		||||
//                                                $"NadekoBot.Modules.{name}.dll"));
 | 
			
		||||
                
 | 
			
		||||
//                await _bot.UnloadPackage(name).ConfigureAwait(false);
 | 
			
		||||
//                await ReplyAsync(":ok:");
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
//            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//            [RequireContext(ContextType.Guild)]
 | 
			
		||||
//            [OwnerOnly]
 | 
			
		||||
//            public async Task PackageLoad(string name)
 | 
			
		||||
//            {
 | 
			
		||||
//                if (name.Contains(".") || name.Contains("\\") || name.Contains("/") || name.Contains("~"))
 | 
			
		||||
//                    return;
 | 
			
		||||
//                name = name.ToTitleCase();
 | 
			
		||||
 | 
			
		||||
//                if (await _bot.LoadPackage(name))
 | 
			
		||||
//                    await ReplyAsync(":ok:");
 | 
			
		||||
//                else
 | 
			
		||||
//                    await ReplyAsync(":x:");
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
//            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
//            [RequireContext(ContextType.Guild)]
 | 
			
		||||
//            [OwnerOnly]
 | 
			
		||||
//            public async Task PackageReload(string name)
 | 
			
		||||
//            {
 | 
			
		||||
//                if (name.Contains(".") || name.Contains("\\") || name.Contains("/") || name.Contains("~"))
 | 
			
		||||
//                    return;
 | 
			
		||||
//                name = name.ToTitleCase();
 | 
			
		||||
 | 
			
		||||
//                if (await _bot.UnloadPackage(name))
 | 
			
		||||
//                {
 | 
			
		||||
//                    await _bot.LoadPackage(name);
 | 
			
		||||
//                    await ReplyAsync(":ok:");
 | 
			
		||||
//                }
 | 
			
		||||
//                else
 | 
			
		||||
//                    await ReplyAsync(":x:");
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//}
 | 
			
		||||
							
								
								
									
										237
									
								
								src/NadekoBot/Modules/Administration/MuteCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								src/NadekoBot/Modules/Administration/MuteCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,237 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class MuteCommands : NadekoSubmodule<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("mute_perms").ConfigureAwait(false);
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("mute_role", Format.Code(muteRole.Name)).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if (Context.User.Id != Context.Guild.OwnerId &&
 | 
			
		||||
                    role.Position >= ((SocketGuildUser) Context.User).Roles.Max(x => x.Position))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("insuf_perms_u").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                await _service.SetMuteRoleAsync(ctx.Guild.Id, role.Name).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("mute_role_set").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("user_muted", Format.Bold(target.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex.ToString());
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("mute_error").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("user_muted_time", Format.Bold(user.ToString()), (int)time.Time.TotalMinutes).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error in mute command");
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("mute_error").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("user_unmuted", Format.Bold(user.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("mute_error").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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: reason).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("user_chat_mute", Format.Bold(user.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex.ToString());
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("mute_error").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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: reason).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("user_chat_mute_time", Format.Bold(user.ToString()), (int)time.Time.TotalMinutes).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex.ToString());
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("mute_error").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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: reason).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("user_chat_unmute", Format.Bold(user.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("mute_error").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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: reason).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("user_voice_mute", Format.Bold(user.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("mute_error").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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: reason).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("user_voice_mute_time", Format.Bold(user.ToString()), (int)time.Time.TotalMinutes).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("mute_error").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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: reason).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("user_voice_unmute", Format.Bold(user.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("mute_error").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using Discord;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class PlayingRotateCommands : NadekoSubmodule<PlayingRotateService>
 | 
			
		||||
        {
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task RotatePlaying()
 | 
			
		||||
            {
 | 
			
		||||
                if (_service.ToggleRotatePlaying())
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("ropl_enabled").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("ropl_disabled").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task AddPlaying(ActivityType t, [Leftover] string status)
 | 
			
		||||
            {
 | 
			
		||||
                await _service.AddPlaying(t, status).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("ropl_added").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task ListPlaying()
 | 
			
		||||
            {
 | 
			
		||||
                var statuses = _service.GetRotatingStatuses();
 | 
			
		||||
 | 
			
		||||
                if (!statuses.Any())
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("ropl_not_set").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var i = 1;
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("ropl_list",
 | 
			
		||||
                            string.Join("\n\t", statuses.Select(rs => $"`{i++}.` *{rs.Type}* {rs.Status}")))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task RemovePlaying(int index)
 | 
			
		||||
            {
 | 
			
		||||
                index -= 1;
 | 
			
		||||
 | 
			
		||||
                var msg = await _service.RemovePlayingAsync(index).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (msg == null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("reprm", msg).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								src/NadekoBot/Modules/Administration/PrefixCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/NadekoBot/Modules/Administration/PrefixCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class PrefixCommands : NadekoSubmodule
 | 
			
		||||
        {
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task PrefixCommand()
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("prefix_current", Format.Code(CmdHandler.GetPrefix(ctx.Guild))).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public enum Set
 | 
			
		||||
            {
 | 
			
		||||
                Set
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public Task PrefixCommand(Set _, [Leftover] string prefix)
 | 
			
		||||
                => PrefixCommand(prefix);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public async Task PrefixCommand([Leftover]string prefix)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(prefix))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var oldPrefix = base.Prefix;
 | 
			
		||||
                var newPrefix = CmdHandler.SetPrefix(ctx.Guild, prefix);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("prefix_new", Format.Code(oldPrefix), Format.Code(newPrefix)).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task DefPrefix([Leftover]string prefix = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(prefix))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("defprefix_current", CmdHandler.GetPrefix()).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var oldPrefix = CmdHandler.GetPrefix();
 | 
			
		||||
                var newPrefix = CmdHandler.SetDefaultPrefix(prefix);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("defprefix_new", Format.Code(oldPrefix), Format.Code(newPrefix)).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										295
									
								
								src/NadekoBot/Modules/Administration/ProtectionCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								src/NadekoBot/Modules/Administration/ProtectionCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,295 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Administration.Common;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Common.TypeReaders.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class ProtectionCommands : NadekoSubmodule<ProtectionService>
 | 
			
		||||
        {
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task AntiAlt()
 | 
			
		||||
            {
 | 
			
		||||
                if (await _service.TryStopAntiAlt(ctx.Guild.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("prot_disable", "Anti-Alt");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyErrorLocalizedAsync("protection_not_running", "Anti-Alt");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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;
 | 
			
		||||
 | 
			
		||||
                await _service.StartAntiAltAsync(ctx.Guild.Id, minAgeMinutes, action, (int?)punishTime?.Time.TotalMinutes ?? 0);
 | 
			
		||||
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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;
 | 
			
		||||
 | 
			
		||||
                await _service.StartAntiAltAsync(ctx.Guild.Id, minAgeMinutes, action, roleId: role.Id);
 | 
			
		||||
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public Task AntiRaid()
 | 
			
		||||
            {
 | 
			
		||||
                if (_service.TryStopAntiRaid(ctx.Guild.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    return ReplyConfirmLocalizedAsync("prot_disable", "Anti-Raid");
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    return ReplyErrorLocalizedAsync("protection_not_running", "Anti-Raid");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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: punishTime);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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("punishment_unsupported", action);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if (userThreshold < 2 || userThreshold > 30)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("raid_cnt", 2, 30).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (seconds < 2 || seconds > 300)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("raid_time", 2, 300).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if (!(punishTime is null))
 | 
			
		||||
                {
 | 
			
		||||
                    if (!_service.IsDurationAllowed(action))
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("prot_cant_use_time");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                var time = (int?) punishTime?.Time.TotalMinutes ?? 0;
 | 
			
		||||
                if (time < 0 || time > 60 * 24)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var stats = await _service.StartAntiRaidAsync(ctx.Guild.Id, userThreshold, seconds,
 | 
			
		||||
                    action, time).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (stats == null)
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.SendConfirmAsync(GetText("prot_enable", "Anti-Raid"),
 | 
			
		||||
                        $"{ctx.User.Mention} {GetAntiRaidString(stats)}")
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public Task AntiSpam()
 | 
			
		||||
            {
 | 
			
		||||
                if (_service.TryStopAntiSpam(ctx.Guild.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    return ReplyConfirmLocalizedAsync("prot_disable", "Anti-Spam");
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    return ReplyErrorLocalizedAsync("protection_not_running", "Anti-Spam");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime)
 | 
			
		||||
                => InternalAntiSpam(messageCount, action, punishTime, null);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [Priority(2)]
 | 
			
		||||
            public Task AntiSpam(int messageCount, PunishmentAction action)
 | 
			
		||||
                => InternalAntiSpam(messageCount, action);
 | 
			
		||||
 | 
			
		||||
            public async Task InternalAntiSpam(int messageCount, PunishmentAction action,
 | 
			
		||||
                StoopidTime timeData = null, IRole role = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (messageCount < 2 || messageCount > 10)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (!(timeData is null))
 | 
			
		||||
                {
 | 
			
		||||
                    if (!_service.IsDurationAllowed(action))
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("prot_cant_use_time");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var time = (int?) timeData?.Time.TotalMinutes ?? 0;
 | 
			
		||||
                if (time < 0 || time > 60 * 24)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var stats = await _service.StartAntiSpamAsync(ctx.Guild.Id, messageCount, action, time, role?.Id).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.SendConfirmAsync(GetText("prot_enable", "Anti-Spam"),
 | 
			
		||||
                    $"{ctx.User.Mention} {GetAntiSpamString(stats)}").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task AntispamIgnore()
 | 
			
		||||
            {
 | 
			
		||||
                var added = await _service.AntiSpamIgnoreAsync(ctx.Guild.Id, ctx.Channel.Id).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if(added is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("protection_not_running", "Anti-Spam").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(added.Value ? "spam_ignore" : "spam_not_ignore", "Anti-Spam").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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("prot_none").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var embed = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("prot_active"));
 | 
			
		||||
 | 
			
		||||
                if (spam != null)
 | 
			
		||||
                    embed.AddField(efb => efb.WithName("Anti-Spam")
 | 
			
		||||
                        .WithValue(GetAntiSpamString(spam).TrimTo(1024))
 | 
			
		||||
                        .WithIsInline(true));
 | 
			
		||||
 | 
			
		||||
                if (raid != null)
 | 
			
		||||
                    embed.AddField(efb => efb.WithName("Anti-Raid")
 | 
			
		||||
                        .WithValue(GetAntiRaidString(raid).TrimTo(1024))
 | 
			
		||||
                        .WithIsInline(true));
 | 
			
		||||
 | 
			
		||||
                if (!(alt is null))
 | 
			
		||||
                    embed.AddField("Anti-Alt", GetAntiAltString(alt), true);
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private string GetAntiAltString(AntiAltStats alt) 
 | 
			
		||||
                => GetText("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";
 | 
			
		||||
 | 
			
		||||
                string add = "";
 | 
			
		||||
                if (settings.MuteTime > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    add = $" ({TimeSpan.FromMinutes(settings.MuteTime):hh\\hmm\\m})";
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return GetText("spam_stats",
 | 
			
		||||
                        Format.Bold(settings.MessageThreshold.ToString()),
 | 
			
		||||
                        Format.Bold(settings.Action.ToString() + 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("raid_stats",
 | 
			
		||||
                    Format.Bold(stats.AntiRaidSettings.UserThreshold.ToString()),
 | 
			
		||||
                    Format.Bold(stats.AntiRaidSettings.Seconds.ToString()),
 | 
			
		||||
                    actionString);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								src/NadekoBot/Modules/Administration/PruneCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/NadekoBot/Modules/Administration/PruneCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using ITextChannel = Discord.ITextChannel;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class PruneCommands : NadekoSubmodule<PruneService>
 | 
			
		||||
        {
 | 
			
		||||
            private static readonly TimeSpan twoWeeks = TimeSpan.FromDays(14);
 | 
			
		||||
 | 
			
		||||
            //delets her own messages, no perm required
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Prune(string parameter = null)
 | 
			
		||||
            {
 | 
			
		||||
                var user = await ctx.Guild.GetCurrentUserAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (parameter == "-s" || parameter == "--safe")
 | 
			
		||||
                    await _service.PruneWhere((ITextChannel)ctx.Channel, 100, (x) => x.Author.Id == user.Id && !x.IsPinned).ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await _service.PruneWhere((ITextChannel)ctx.Channel, 100, (x) => x.Author.Id == user.Id).ConfigureAwait(false);
 | 
			
		||||
                ctx.Message.DeleteAfter(3);
 | 
			
		||||
            }
 | 
			
		||||
            // prune x
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
            [BotPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task Prune(int count, string parameter = null)
 | 
			
		||||
            {
 | 
			
		||||
                count++;
 | 
			
		||||
                if (count < 1)
 | 
			
		||||
                    return;
 | 
			
		||||
                if (count > 1000)
 | 
			
		||||
                    count = 1000;
 | 
			
		||||
 | 
			
		||||
                if (parameter == "-s" || parameter == "--safe")
 | 
			
		||||
                    await _service.PruneWhere((ITextChannel)ctx.Channel, count, (x) => !x.IsPinned).ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => true).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //prune @user [x]
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
            [BotPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public Task Prune(IGuildUser user, int count = 100, string parameter = null)
 | 
			
		||||
                => Prune(user.Id, count, parameter);
 | 
			
		||||
 | 
			
		||||
            //prune userid [x]
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
            [BotPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public async Task Prune(ulong userId, int count = 100, string parameter = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (userId == ctx.User.Id)
 | 
			
		||||
                    count++;
 | 
			
		||||
 | 
			
		||||
                if (count < 1)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (count > 1000)
 | 
			
		||||
                    count = 1000;
 | 
			
		||||
 | 
			
		||||
                if (parameter == "-s" || parameter == "--safe")
 | 
			
		||||
                    await _service.PruneWhere((ITextChannel)ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < twoWeeks && !m.IsPinned).ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await _service.PruneWhere((ITextChannel)ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < twoWeeks).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										345
									
								
								src/NadekoBot/Modules/Administration/RoleCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										345
									
								
								src/NadekoBot/Modules/Administration/RoleCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,345 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        public class RoleCommands : NadekoSubmodule<RoleCommandsService>
 | 
			
		||||
        {
 | 
			
		||||
            private IServiceProvider _services;
 | 
			
		||||
            public enum Exclude { Excl }
 | 
			
		||||
 | 
			
		||||
            public RoleCommands(IServiceProvider services)
 | 
			
		||||
            {
 | 
			
		||||
                _services = services;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public async Task InternalReactionRoles(bool exclusive, params string[] input)
 | 
			
		||||
            {
 | 
			
		||||
                var msgs = await ((SocketTextChannel)ctx.Channel).GetMessagesAsync().FlattenAsync().ConfigureAwait(false);
 | 
			
		||||
                var prev = (IUserMessage)msgs.FirstOrDefault(x => x is IUserMessage && x.Id != ctx.Message.Id);
 | 
			
		||||
 | 
			
		||||
                if (prev == null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (input.Length % 2 != 0)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var grp = 0;
 | 
			
		||||
                var results = input
 | 
			
		||||
                    .GroupBy(x => grp++ / 2)
 | 
			
		||||
                    .Select(async x =>
 | 
			
		||||
                   {
 | 
			
		||||
                       var inputRoleStr = x.First();
 | 
			
		||||
                       var roleReader = new RoleTypeReader<SocketRole>();
 | 
			
		||||
                       var roleResult = await roleReader.ReadAsync(ctx, inputRoleStr, _services);
 | 
			
		||||
                       if (!roleResult.IsSuccess)
 | 
			
		||||
                       {
 | 
			
		||||
                           Log.Warning("Role {0} not found.", inputRoleStr);
 | 
			
		||||
                           return null;
 | 
			
		||||
                       }
 | 
			
		||||
                       var role = (IRole)roleResult.BestMatch;
 | 
			
		||||
                       if (role.Position > ((IGuildUser)ctx.User).GetRoles().Select(r => r.Position).Max()
 | 
			
		||||
                           && ctx.User.Id != ctx.Guild.OwnerId)
 | 
			
		||||
                           return null;
 | 
			
		||||
                       var emote = x.Last().ToIEmote();
 | 
			
		||||
                       return new { role, emote };
 | 
			
		||||
                   })
 | 
			
		||||
                    .Where(x => x != null);
 | 
			
		||||
 | 
			
		||||
                var all = await Task.WhenAll(results);
 | 
			
		||||
 | 
			
		||||
                if (!all.Any())
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                foreach (var x in all)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await prev.AddReactionAsync(x.emote, new RequestOptions()
 | 
			
		||||
                        {
 | 
			
		||||
                            RetryMode = RetryMode.Retry502 | RetryMode.RetryRatelimit
 | 
			
		||||
                        }).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Discord.Net.HttpException ex) when(ex.HttpCode == HttpStatusCode.BadRequest)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("reaction_cant_access", Format.Code(x.emote.ToString()));
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await Task.Delay(500).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (_service.Add(ctx.Guild.Id, new ReactionRoleMessage()
 | 
			
		||||
                {
 | 
			
		||||
                    Exclusive = exclusive,
 | 
			
		||||
                    MessageId = prev.Id,
 | 
			
		||||
                    ChannelId = prev.Channel.Id,
 | 
			
		||||
                    ReactionRoles = all.Select(x =>
 | 
			
		||||
                    {
 | 
			
		||||
                        return new ReactionRole()
 | 
			
		||||
                        {
 | 
			
		||||
                            EmoteName = x.emote.ToString(),
 | 
			
		||||
                            RoleId = x.role.Id,
 | 
			
		||||
                        };
 | 
			
		||||
                    }).ToList(),
 | 
			
		||||
                }))
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.OkAsync();
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("reaction_roles_full").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [NoPublicBot]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public Task ReactionRoles(params string[] input) =>
 | 
			
		||||
                InternalReactionRoles(false, input);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [NoPublicBot]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public Task ReactionRoles(Exclude _, params string[] input) =>
 | 
			
		||||
                InternalReactionRoles(true, input);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [NoPublicBot]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            public async Task ReactionRolesList()
 | 
			
		||||
            {
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
                if (!_service.Get(ctx.Guild.Id, out var rrs) ||
 | 
			
		||||
                    !rrs.Any())
 | 
			
		||||
                {
 | 
			
		||||
                    embed.WithDescription(GetText("no_reaction_roles"));
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var g = ((SocketGuild)ctx.Guild);
 | 
			
		||||
                    foreach (var rr in rrs)
 | 
			
		||||
                    {
 | 
			
		||||
                        var ch = g.GetTextChannel(rr.ChannelId);
 | 
			
		||||
                        IUserMessage msg = null;
 | 
			
		||||
                        if (!(ch is null))
 | 
			
		||||
                        {
 | 
			
		||||
                            msg = await ch.GetMessageAsync(rr.MessageId).ConfigureAwait(false) as IUserMessage;
 | 
			
		||||
                        }
 | 
			
		||||
                        var content = msg?.Content.TrimTo(30) ?? "DELETED!";
 | 
			
		||||
                        embed.AddField($"**{rr.Index + 1}.** {(ch?.Name ?? "DELETED!")}",
 | 
			
		||||
                            GetText("reaction_roles_message", rr.ReactionRoles?.Count ?? 0, content));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [NoPublicBot]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            public async Task ReactionRolesRemove(int index)
 | 
			
		||||
            {
 | 
			
		||||
                if (index < 1 ||
 | 
			
		||||
                    !_service.Get(ctx.Guild.Id, out var rrs) ||
 | 
			
		||||
                    !rrs.Any() || rrs.Count < index)
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                index--;
 | 
			
		||||
                var rr = rrs[index];
 | 
			
		||||
                _service.Remove(ctx.Guild.Id, index);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("reaction_role_removed", index + 1).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("setrole", Format.Bold(roleToAdd.Name), Format.Bold(targetUser.ToString()))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error in setrole command");
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("setrole_err").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("remrole", Format.Bold(roleToRemove.Name), Format.Bold(targetUser.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("remrole_err").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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().ConfigureAwait(false)).GetRoles().Max(r => r.Position))
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("renrole_perms").ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    await roleToEdit.ModifyAsync(g => g.Name = newname).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("renrole").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("renrole_err").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("rar", Format.Bold(user.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("rar_err").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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).ConfigureAwait(false);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("cr", Format.Bold(r.Name)).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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().ConfigureAwait(false);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("dr", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            public async Task RoleHoist(IRole role)
 | 
			
		||||
            {
 | 
			
		||||
                var newHoisted = !role.IsHoisted;
 | 
			
		||||
                await role.ModifyAsync(r => r.Hoist = newHoisted).ConfigureAwait(false);
 | 
			
		||||
                if (newHoisted)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("rolehoist_enabled", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("rolehoist_disabled", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task RoleColor([Leftover] IRole role)
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.Channel.SendConfirmAsync("Role Color", role.Color.RawValue.ToString("x6")).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public async Task RoleColor(SixLabors.ImageSharp.Color color, [Leftover]IRole role)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var rgba32 = color.ToPixel<Rgba32>();
 | 
			
		||||
                    await role.ModifyAsync(r => r.Color = new Color(rgba32.R, rgba32.G, rgba32.B)).ConfigureAwait(false);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("rc", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("rc_perms").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,272 @@
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class SelfAssignedRolesCommands : NadekoSubmodule<SelfAssignedRolesService>
 | 
			
		||||
        {
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
            public async Task AdSarm()
 | 
			
		||||
            {
 | 
			
		||||
                var newVal = _service.ToggleAdSarm(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
                if (newVal)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("adsarm_enable", Prefix).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("adsarm_disable", Prefix).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public Task Asar([Leftover] IRole role) =>
 | 
			
		||||
                Asar(0, role);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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("role_added", Format.Bold(role.Name), Format.Bold(group.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("role_in_list", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public async Task Sargn(int group, [Leftover] string name = null)
 | 
			
		||||
            {
 | 
			
		||||
                var guser = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
                var set = await _service.SetNameAsync(ctx.Guild.Id, group, name).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (set)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("group_name_added", Format.Bold(group.ToString()), Format.Bold(name.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("group_name_removed", Format.Bold(group.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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;
 | 
			
		||||
 | 
			
		||||
                bool success = _service.RemoveSar(role.Guild.Id, role.Id);
 | 
			
		||||
                if (!success)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("self_assign_not").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("self_assign_rem", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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)
 | 
			
		||||
                    {
 | 
			
		||||
                        var groupNameText = "";
 | 
			
		||||
                        if (!groups.TryGetValue(kvp.Key, out var name))
 | 
			
		||||
                        {
 | 
			
		||||
                            groupNameText = Format.Bold(GetText("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 == null)
 | 
			
		||||
                            {
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
                            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 new EmbedBuilder().WithOkColor()
 | 
			
		||||
                        .WithTitle(Format.Bold(GetText("self_assign_list", roles.Count())))
 | 
			
		||||
                        .WithDescription(rolesStr.ToString())
 | 
			
		||||
                        .WithFooter(exclusive
 | 
			
		||||
                            ? GetText("self_assign_are_exclusive")
 | 
			
		||||
                            : GetText("self_assign_are_not_exclusive"));
 | 
			
		||||
                }, roles.Count(), 20).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            public async Task Togglexclsar()
 | 
			
		||||
            {
 | 
			
		||||
                bool areExclusive = _service.ToggleEsar(ctx.Guild.Id);
 | 
			
		||||
                if (areExclusive)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("self_assign_excl").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("self_assign_no_excl").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            public async Task RoleLevelReq(int level, [Leftover] IRole role)
 | 
			
		||||
            {
 | 
			
		||||
                if (level < 0)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                bool succ = _service.SetLevelReq(ctx.Guild.Id, role, level);
 | 
			
		||||
 | 
			
		||||
                if (!succ)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("self_assign_not").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("self_assign_level_req",
 | 
			
		||||
                    Format.Bold(role.Name),
 | 
			
		||||
                    Format.Bold(level.ToString())).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Iam([Leftover] IRole role)
 | 
			
		||||
            {
 | 
			
		||||
                var guildUser = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
                var (result, autoDelete, extra) = await _service.Assign(guildUser, role).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                IUserMessage msg;
 | 
			
		||||
                if (result == SelfAssignedRolesService.AssignResult.Err_Not_Assignable)
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ReplyErrorLocalizedAsync("self_assign_not").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == SelfAssignedRolesService.AssignResult.Err_Lvl_Req)
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ReplyErrorLocalizedAsync("self_assign_not_level", Format.Bold(extra.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == SelfAssignedRolesService.AssignResult.Err_Already_Have)
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ReplyErrorLocalizedAsync("self_assign_already", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == SelfAssignedRolesService.AssignResult.Err_Not_Perms)
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ReplyErrorLocalizedAsync("self_assign_perms").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ReplyConfirmLocalizedAsync("self_assign_success", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (autoDelete)
 | 
			
		||||
                {
 | 
			
		||||
                    msg.DeleteAfter(3);
 | 
			
		||||
                    ctx.Message.DeleteAfter(3);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Iamnot([Leftover] IRole role)
 | 
			
		||||
            {
 | 
			
		||||
                var guildUser = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
                var (result, autoDelete) = await _service.Remove(guildUser, role).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                IUserMessage msg;
 | 
			
		||||
                if (result == SelfAssignedRolesService.RemoveResult.Err_Not_Assignable)
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ReplyErrorLocalizedAsync("self_assign_not").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == SelfAssignedRolesService.RemoveResult.Err_Not_Have)
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ReplyErrorLocalizedAsync("self_assign_not_have", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == SelfAssignedRolesService.RemoveResult.Err_Not_Perms)
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ReplyErrorLocalizedAsync("self_assign_perms").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    msg = await ReplyConfirmLocalizedAsync("self_assign_remove", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (autoDelete)
 | 
			
		||||
                {
 | 
			
		||||
                    msg.DeleteAfter(3);
 | 
			
		||||
                    ctx.Message.DeleteAfter(3);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										546
									
								
								src/NadekoBot/Modules/Administration/SelfCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										546
									
								
								src/NadekoBot/Modules/Administration/SelfCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,546 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.Net;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Common.Replacements;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class SelfCommands : NadekoSubmodule<SelfService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly DiscordSocketClient _client;
 | 
			
		||||
            private readonly NadekoBot _bot;
 | 
			
		||||
            private readonly IBotStrings _strings;
 | 
			
		||||
 | 
			
		||||
            public SelfCommands(DiscordSocketClient client, NadekoBot bot, IBotStrings strings)
 | 
			
		||||
            {
 | 
			
		||||
                _client = client;
 | 
			
		||||
                _bot = bot;
 | 
			
		||||
                _strings = strings;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("scadd"))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("server"))
 | 
			
		||||
                        .WithValue(cmd.GuildId == null ? $"-" : $"{cmd.GuildName}/{cmd.GuildId}").WithIsInline(true))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("channel"))
 | 
			
		||||
                        .WithValue($"{cmd.ChannelName}/{cmd.ChannelId}").WithIsInline(true))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("command_text"))
 | 
			
		||||
                        .WithValue(cmdText).WithIsInline(false))).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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("autocmd_add", Format.Code(Format.Sanitize(cmdText)), cmd.Interval).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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("startcmdlist_none").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var i = 0;
 | 
			
		||||
                    await ctx.Channel.SendConfirmAsync(
 | 
			
		||||
                        text: string.Join("\n", scmds
 | 
			
		||||
                        .Select(x => $@"```css
 | 
			
		||||
#{++i + page * 5}
 | 
			
		||||
[{GetText("server")}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")}
 | 
			
		||||
[{GetText("channel")}]: {x.ChannelName} #{x.ChannelId}
 | 
			
		||||
[{GetText("command_text")}]: {x.CommandText}```")),
 | 
			
		||||
                        title: string.Empty,
 | 
			
		||||
                        footer: GetText("page", page + 1))
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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("autocmdlist_none").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var i = 0;
 | 
			
		||||
                    await ctx.Channel.SendConfirmAsync(
 | 
			
		||||
                        text: string.Join("\n", scmds
 | 
			
		||||
                        .Select(x => $@"```css
 | 
			
		||||
#{++i + page * 5}
 | 
			
		||||
[{GetText("server")}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")}
 | 
			
		||||
[{GetText("channel")}]: {x.ChannelName} #{x.ChannelId}
 | 
			
		||||
{GetIntervalText(x.Interval)}
 | 
			
		||||
[{GetText("command_text")}]: {x.CommandText}```")),
 | 
			
		||||
                        title: string.Empty,
 | 
			
		||||
                        footer: GetText("page", page + 1))
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private string GetIntervalText(int interval)
 | 
			
		||||
            {
 | 
			
		||||
                return $"[{GetText("interval")}]: {interval}";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task Wait(int miliseconds)
 | 
			
		||||
            {
 | 
			
		||||
                if (miliseconds <= 0)
 | 
			
		||||
                    return;
 | 
			
		||||
                ctx.Message.DeleteAfter(0);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var msg = await ctx.Channel.SendConfirmAsync($"⏲ {miliseconds}ms")
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                    msg.DeleteAfter(miliseconds / 1000);
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
 | 
			
		||||
                await Task.Delay(miliseconds).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task AutoCommandRemove([Leftover] int index)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_service.RemoveAutoCommand(--index, out _))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("acrm_fail").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task StartupCommandRemove([Leftover] int index)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_service.RemoveStartupCommand(--index, out _))
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("scrm_fail").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("scrm").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task StartupCommandsClear()
 | 
			
		||||
            {
 | 
			
		||||
                _service.ClearStartupCommands();
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("startcmds_cleared").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task ForwardMessages()
 | 
			
		||||
            {
 | 
			
		||||
                var enabled = _service.ForwardMessages();
 | 
			
		||||
 | 
			
		||||
                if (enabled)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("fwdm_start").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("fwdm_stop").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task ForwardToAll()
 | 
			
		||||
            {
 | 
			
		||||
                var enabled = _service.ForwardToAll();
 | 
			
		||||
 | 
			
		||||
                if (enabled)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("fwall_start").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("fwall_stop").ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task ShardStats(int page = 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (--page < 0)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var statuses = _service.GetAllShardStatuses();
 | 
			
		||||
 | 
			
		||||
                var status = string.Join(", ", statuses
 | 
			
		||||
                    .GroupBy(x => x.ConnectionState)
 | 
			
		||||
                    .Select(x => $"{x.Count()} {x.Key}")
 | 
			
		||||
                    .ToArray());
 | 
			
		||||
 | 
			
		||||
                var allShardStrings = statuses
 | 
			
		||||
                    .Select(x =>
 | 
			
		||||
                    {
 | 
			
		||||
                        var timeDiff = DateTime.UtcNow - x.Time;
 | 
			
		||||
                        if (timeDiff >= TimeSpan.FromSeconds(30))
 | 
			
		||||
                            return $"Shard #{Format.Bold(x.ShardId.ToString())} **UNRESPONSIVE** for {timeDiff.ToString(@"hh\:mm\:ss")}";
 | 
			
		||||
                        return GetText("shard_stats_txt", x.ShardId.ToString(),
 | 
			
		||||
                            Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.ToString()), timeDiff.ToString(@"hh\:mm\:ss"));
 | 
			
		||||
                    })
 | 
			
		||||
                    .ToArray();
 | 
			
		||||
 | 
			
		||||
                await ctx.SendPaginatedConfirmAsync(page, (curPage) =>
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                    var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25));
 | 
			
		||||
 | 
			
		||||
                    if (string.IsNullOrWhiteSpace(str))
 | 
			
		||||
                        str = GetText("no_shards_on_page");
 | 
			
		||||
 | 
			
		||||
                    return new EmbedBuilder()
 | 
			
		||||
                        .WithAuthor(a => a.WithName(GetText("shard_stats")))
 | 
			
		||||
                        .WithTitle(status)
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithDescription(str);
 | 
			
		||||
                }, allShardStrings.Length, 25).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task RestartShard(int shardId)
 | 
			
		||||
            {
 | 
			
		||||
                var success = _service.RestartShard(shardId);
 | 
			
		||||
                if (success)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("shard_reconnecting", Format.Bold("#" + shardId)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("no_shard_id").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public Task Leave([Leftover] string guildStr)
 | 
			
		||||
            {
 | 
			
		||||
                return _service.LeaveGuild(guildStr);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task Die()
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("shutting_down").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    // ignored
 | 
			
		||||
                }
 | 
			
		||||
                await Task.Delay(2000).ConfigureAwait(false);
 | 
			
		||||
                _service.Die();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task Restart()
 | 
			
		||||
            {
 | 
			
		||||
                bool success = _service.RestartBot();
 | 
			
		||||
                if (!success)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("restart_fail").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                try { await ReplyConfirmLocalizedAsync("restarting").ConfigureAwait(false); } catch { }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task SetName([Leftover] string newName)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(newName))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await _client.CurrentUser.ModifyAsync(u => u.Username = newName).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (RateLimitedException)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning("You've been ratelimited. Wait 2 hours to change your name");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("bot_name", Format.Bold(newName)).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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().ConfigureAwait(false);
 | 
			
		||||
                await curUser.ModifyAsync(u => u.Nickname = newNick).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("bot_nick", Format.Bold(newNick) ?? "-").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageNicknames)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageNicknames)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task SetNick(IGuildUser gu, [Leftover] string newNick = null)
 | 
			
		||||
            {
 | 
			
		||||
                var sg = (SocketGuild) Context.Guild;
 | 
			
		||||
                if (sg.OwnerId == gu.Id ||
 | 
			
		||||
                    gu.GetRoles().Max(r => r.Position) >= sg.CurrentUser.GetRoles().Max(r => r.Position))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("insuf_perms_i");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                await gu.ModifyAsync(u => u.Nickname = newNick).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("user_nick", Format.Bold(gu.ToString()), Format.Bold(newNick) ?? "-").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task SetStatus([Leftover] SettableUserStatus status)
 | 
			
		||||
            {
 | 
			
		||||
                await _client.SetStatusAsync(SettableUserStatusToUserStatus(status)).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("bot_status", Format.Bold(status.ToString())).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task SetAvatar([Leftover] string img = null)
 | 
			
		||||
            {
 | 
			
		||||
                var success = await _service.SetAvatar(img);
 | 
			
		||||
 | 
			
		||||
                if (success)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("set_avatar").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task SetGame(ActivityType type, [Leftover] string game = null)
 | 
			
		||||
            {
 | 
			
		||||
                var rep = new ReplacementBuilder()
 | 
			
		||||
                    .WithDefault(Context)
 | 
			
		||||
                    .Build();
 | 
			
		||||
 | 
			
		||||
                await _bot.SetGameAsync(game == null ? game : rep.Replace(game), type).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("set_game").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task SetStream(string url, [Leftover] string name = null)
 | 
			
		||||
            {
 | 
			
		||||
                name = name ?? "";
 | 
			
		||||
 | 
			
		||||
                await _client.SetGameAsync(name, url, ActivityType.Streaming).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("set_stream").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task Send(string where, [Leftover] string msg = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(msg))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                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 == null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var rep = new ReplacementBuilder()
 | 
			
		||||
                    .WithDefault(Context)
 | 
			
		||||
                    .Build();
 | 
			
		||||
 | 
			
		||||
                if (ids[1].ToUpperInvariant().StartsWith("C:", StringComparison.InvariantCulture))
 | 
			
		||||
                {
 | 
			
		||||
                    var cid = ulong.Parse(ids[1].Substring(2));
 | 
			
		||||
                    var ch = server.TextChannels.FirstOrDefault(c => c.Id == cid);
 | 
			
		||||
                    if (ch == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (CREmbed.TryParse(msg, out var crembed))
 | 
			
		||||
                    {
 | 
			
		||||
                        rep.Replace(crembed);
 | 
			
		||||
                        await ch.EmbedAsync(crembed).ConfigureAwait(false);
 | 
			
		||||
                        await ReplyConfirmLocalizedAsync("message_sent").ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    await ch.SendMessageAsync(rep.Replace(msg).SanitizeMentions()).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else if (ids[1].ToUpperInvariant().StartsWith("U:", StringComparison.InvariantCulture))
 | 
			
		||||
                {
 | 
			
		||||
                    var uid = ulong.Parse(ids[1].Substring(2));
 | 
			
		||||
                    var user = server.Users.FirstOrDefault(u => u.Id == uid);
 | 
			
		||||
                    if (user == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (CREmbed.TryParse(msg, out var crembed))
 | 
			
		||||
                    {
 | 
			
		||||
                        rep.Replace(crembed);
 | 
			
		||||
                        await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).EmbedAsync(crembed)
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
                        await ReplyConfirmLocalizedAsync("message_sent").ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(rep.Replace(msg).SanitizeMentions()).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("invalid_format").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("message_sent").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task ImagesReload()
 | 
			
		||||
            {
 | 
			
		||||
                _service.ReloadImages();
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("images_loading", 0).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task StringsReload()
 | 
			
		||||
            {
 | 
			
		||||
                _strings.Reload();
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("bot_strings_reloaded").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public enum SettableUserStatus
 | 
			
		||||
            {
 | 
			
		||||
                Online,
 | 
			
		||||
                Invisible,
 | 
			
		||||
                Idle,
 | 
			
		||||
                Dnd
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										218
									
								
								src/NadekoBot/Modules/Administration/ServerGreetCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								src/NadekoBot/Modules/Administration/ServerGreetCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,218 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Common.Attributes;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class ServerGreetCommands : NadekoSubmodule<GreetSettingsService>
 | 
			
		||||
        {
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public async Task GreetDel(int timer = 30)
 | 
			
		||||
            {
 | 
			
		||||
                if (timer < 0 || timer > 600)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await _service.SetGreetDel(ctx.Guild.Id, timer).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (timer > 0)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greetdel_on", timer).ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greetdel_off").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public async Task Greet()
 | 
			
		||||
            {
 | 
			
		||||
                var enabled = await _service.SetGreet(ctx.Guild.Id, ctx.Channel.Id).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (enabled)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greet_on").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greet_off").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public Task GreetMsg()
 | 
			
		||||
            {
 | 
			
		||||
                string greetMsg = _service.GetGreetMsg(ctx.Guild.Id);
 | 
			
		||||
                return ReplyConfirmLocalizedAsync("greetmsg_cur", greetMsg?.SanitizeMentions());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public async Task GreetMsg([Leftover] string text)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(text))
 | 
			
		||||
                {
 | 
			
		||||
                    await GreetMsg().ConfigureAwait(false);
 | 
			
		||||
                    return;                    
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var sendGreetEnabled = _service.SetGreetMessage(ctx.Guild.Id, ref text);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("greetmsg_new").ConfigureAwait(false);
 | 
			
		||||
                if (!sendGreetEnabled)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greetmsg_enable", $"`{Prefix}greet`").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public async Task GreetDm()
 | 
			
		||||
            {
 | 
			
		||||
                var enabled = await _service.SetGreetDm(ctx.Guild.Id).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (enabled)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greetdm_on").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greetdm_off").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public Task GreetDmMsg()
 | 
			
		||||
            {
 | 
			
		||||
                var dmGreetMsg = _service.GetDmGreetMsg(ctx.Guild.Id);
 | 
			
		||||
                return ReplyConfirmLocalizedAsync("greetdmmsg_cur", dmGreetMsg?.SanitizeMentions());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public async Task GreetDmMsg([Leftover] string text = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(text))
 | 
			
		||||
                {
 | 
			
		||||
                    await GreetDmMsg().ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var sendGreetEnabled = _service.SetGreetDmMessage(ctx.Guild.Id, ref text);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("greetdmmsg_new").ConfigureAwait(false);
 | 
			
		||||
                if (!sendGreetEnabled)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greetdmmsg_enable", $"`{Prefix}greetdm`").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public async Task Bye()
 | 
			
		||||
            {
 | 
			
		||||
                var enabled = await _service.SetBye(ctx.Guild.Id, ctx.Channel.Id).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (enabled)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("bye_on").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("bye_off").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public Task ByeMsg()
 | 
			
		||||
            {
 | 
			
		||||
                var byeMsg = _service.GetByeMessage(ctx.Guild.Id);
 | 
			
		||||
                return ReplyConfirmLocalizedAsync("byemsg_cur", byeMsg?.SanitizeMentions());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public async Task ByeMsg([Leftover] string text)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(text))
 | 
			
		||||
                {
 | 
			
		||||
                    await ByeMsg().ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var sendByeEnabled = _service.SetByeMessage(ctx.Guild.Id, ref text);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("byemsg_new").ConfigureAwait(false);
 | 
			
		||||
                if (!sendByeEnabled)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("byemsg_enable", $"`{Prefix}bye`").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            public async Task ByeDel(int timer = 30)
 | 
			
		||||
            {
 | 
			
		||||
                await _service.SetByeDel(ctx.Guild.Id, timer).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (timer > 0)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("byedel_on", timer).ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("byedel_off").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            [Ratelimit(5)]
 | 
			
		||||
            public async Task ByeTest([Leftover] IGuildUser user = null)
 | 
			
		||||
            {
 | 
			
		||||
                user = user ?? (IGuildUser) Context.User;
 | 
			
		||||
                
 | 
			
		||||
                await _service.ByeTest((ITextChannel)Context.Channel, user);
 | 
			
		||||
                var enabled = _service.GetByeEnabled(Context.Guild.Id);
 | 
			
		||||
                if (!enabled)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("byemsg_enable", $"`{Prefix}bye`").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            [Ratelimit(5)]
 | 
			
		||||
            public async Task GreetTest([Leftover] IGuildUser user = null)
 | 
			
		||||
            {
 | 
			
		||||
                user = user ?? (IGuildUser) Context.User;
 | 
			
		||||
                
 | 
			
		||||
                await _service.GreetTest((ITextChannel)Context.Channel, user);
 | 
			
		||||
                var enabled = _service.GetGreetEnabled(Context.Guild.Id);
 | 
			
		||||
                if (!enabled)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greetmsg_enable", $"`{Prefix}greet`").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageGuild)]
 | 
			
		||||
            [Ratelimit(5)]
 | 
			
		||||
            public async Task GreetDmTest([Leftover] IGuildUser user = null)
 | 
			
		||||
            {
 | 
			
		||||
                user = user ?? (IGuildUser) Context.User;
 | 
			
		||||
                
 | 
			
		||||
                var channel = await user.GetOrCreateDMChannelAsync();
 | 
			
		||||
                var success = await _service.GreetDmTest(channel, user);
 | 
			
		||||
                if (success)
 | 
			
		||||
                    await Context.OkAsync();
 | 
			
		||||
                else
 | 
			
		||||
                    await Context.WarningAsync();
 | 
			
		||||
                var enabled = _service.GetGreetDmEnabled(Context.Guild.Id);
 | 
			
		||||
                if (!enabled)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("greetdmmsg_enable", $"`{Prefix}greetdm`").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,185 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Common.Replacements;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
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 LogCommandService _logService;
 | 
			
		||||
 | 
			
		||||
        public AdministrationService(NadekoBot bot, CommandHandler cmdHandler, DbService db,
 | 
			
		||||
            LogCommandService logService)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _logService = logService;
 | 
			
		||||
 | 
			
		||||
            DeleteMessagesOnCommand = new ConcurrentHashSet<ulong>(bot.AllGuildConfigs
 | 
			
		||||
                .Where(g => g.DeleteMessageOnCommand)
 | 
			
		||||
                .Select(g => g.GuildId));
 | 
			
		||||
 | 
			
		||||
            DeleteMessagesOnCommandChannels = new ConcurrentDictionary<ulong, bool>(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.GuildConfigs.ForId(guildId,
 | 
			
		||||
                    set => set.Include(x => x.DelMsgOnCmdChannels));
 | 
			
		||||
 | 
			
		||||
                return (conf.DeleteMessageOnCommand, conf.DelMsgOnCmdChannels);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                if (!(msg.Channel is SocketTextChannel channel))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                //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().ConfigureAwait(false); } 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().ConfigureAwait(false); } catch { }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool ToggleDeleteMessageOnCommand(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            bool enabled;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var conf = uow.GuildConfigs.ForId(guildId, set => set);
 | 
			
		||||
                enabled = conf.DeleteMessageOnCommand = !conf.DeleteMessageOnCommand;
 | 
			
		||||
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
            return enabled;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task SetDelMsgOnCmdState(ulong guildId, ulong chId, Administration.State newState)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var conf = uow.GuildConfigs.ForId(guildId,
 | 
			
		||||
                    set => set.Include(x => x.DelMsgOnCmdChannels));
 | 
			
		||||
 | 
			
		||||
                var old = conf.DelMsgOnCmdChannels.FirstOrDefault(x => x.ChannelId == chId);
 | 
			
		||||
                if (newState == Administration.State.Inherit)
 | 
			
		||||
                {
 | 
			
		||||
                    if (!(old is null))
 | 
			
		||||
                    {
 | 
			
		||||
                        conf.DelMsgOnCmdChannels.Remove(old);
 | 
			
		||||
                        uow._context.Remove(old);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (old is null)
 | 
			
		||||
                    {
 | 
			
		||||
                        old = new DelMsgOnCmdChannel { 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 var _);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    // ignored
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task EditMessage(ICommandContext context, ITextChannel chanl, ulong messageId, string text)
 | 
			
		||||
        {
 | 
			
		||||
            var msg = await chanl.GetMessageAsync(messageId);
 | 
			
		||||
 | 
			
		||||
            if (!(msg is IUserMessage umsg) || msg.Author.Id != context.Client.CurrentUser.Id)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var rep = new ReplacementBuilder()
 | 
			
		||||
                    .WithDefault(context)
 | 
			
		||||
                    .Build();
 | 
			
		||||
 | 
			
		||||
            if (CREmbed.TryParse(text, out var crembed))
 | 
			
		||||
            {
 | 
			
		||||
                rep.Replace(crembed);
 | 
			
		||||
                await umsg.ModifyAsync(x =>
 | 
			
		||||
                {
 | 
			
		||||
                    x.Embed = crembed.ToEmbed().Build();
 | 
			
		||||
                    x.Content = crembed.PlainText?.SanitizeMentions() ?? "";
 | 
			
		||||
                }).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await umsg.ModifyAsync(x =>
 | 
			
		||||
                {
 | 
			
		||||
                    x.Content = text.SanitizeMentions();
 | 
			
		||||
                    x.Embed = null;
 | 
			
		||||
                }).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,170 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Channels;
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
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 Channel<SocketGuildUser> _assignQueue = Channel.CreateBounded<SocketGuildUser>(
 | 
			
		||||
            new BoundedChannelOptions(100)
 | 
			
		||||
            {
 | 
			
		||||
                FullMode = BoundedChannelFullMode.DropOldest,
 | 
			
		||||
                SingleReader = true,
 | 
			
		||||
                SingleWriter = false,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        public AutoAssignRoleService(DiscordSocketClient client, NadekoBot 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 null))
 | 
			
		||||
                            .ToList();
 | 
			
		||||
                        
 | 
			
		||||
                        if (roleIds.Any())
 | 
			
		||||
                        {
 | 
			
		||||
                            await user.AddRolesAsync(roleIds).ConfigureAwait(false);
 | 
			
		||||
                            await Task.Delay(250).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                        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 (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.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)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var gc = uow.GuildConfigs.ForId(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)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            
 | 
			
		||||
            await uow._context
 | 
			
		||||
                .GuildConfigs
 | 
			
		||||
                .AsNoTracking()
 | 
			
		||||
                .Where(x => x.GuildId == guildId)
 | 
			
		||||
                .UpdateAsync(_ => new GuildConfig(){ AutoAssignRoleIds = null});
 | 
			
		||||
            
 | 
			
		||||
            _autoAssignableRoles.TryRemove(guildId, out _);
 | 
			
		||||
            
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task SetAarRolesAsync(ulong guildId, IEnumerable<ulong> newRoles)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            
 | 
			
		||||
            var gc = uow.GuildConfigs.ForId(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 List<ulong>();
 | 
			
		||||
 | 
			
		||||
            return gc.AutoAssignRoleIds.Split(',').Select(ulong.Parse).ToList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static void SetAutoAssignableRoles(this GuildConfig gc, IEnumerable<ulong> roles)
 | 
			
		||||
        {
 | 
			
		||||
            gc.AutoAssignRoleIds = roles.JoinWith(',');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,145 @@
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class DangerousCommandsService : INService
 | 
			
		||||
    {
 | 
			
		||||
        public const string WaifusDeleteSql = @"DELETE FROM WaifuUpdates;
 | 
			
		||||
DELETE FROM WaifuItem;
 | 
			
		||||
DELETE FROM WaifuInfo;";
 | 
			
		||||
        public const string WaifuDeleteSql = @"DELETE FROM WaifuUpdates WHERE UserId=(SELECT Id FROM DiscordUser WHERE UserId={0});
 | 
			
		||||
DELETE FROM WaifuItem WHERE WaifuInfoId=(SELECT Id FROM WaifuInfo WHERE WaifuId=(SELECT Id FROM DiscordUser WHERE UserId={0}));
 | 
			
		||||
UPDATE WaifuInfo SET ClaimerId=NULL WHERE ClaimerId=(SELECT Id FROM DiscordUser WHERE UserId={0});
 | 
			
		||||
DELETE FROM WaifuInfo WHERE WaifuId=(SELECT Id FROM DiscordUser WHERE UserId={0});";
 | 
			
		||||
        public const string CurrencyDeleteSql = "UPDATE DiscordUser SET CurrencyAmount=0; DELETE FROM CurrencyTransactions; DELETE FROM PlantedCurrency;";
 | 
			
		||||
        public const string MusicPlaylistDeleteSql = "DELETE FROM MusicPlaylists;";
 | 
			
		||||
        public const string XpDeleteSql = @"DELETE FROM UserXpStats;
 | 
			
		||||
UPDATE DiscordUser
 | 
			
		||||
SET ClubId=NULL,
 | 
			
		||||
    IsClubAdmin=0,
 | 
			
		||||
    TotalXp=0;
 | 
			
		||||
DELETE FROM ClubApplicants;
 | 
			
		||||
DELETE FROM ClubBans;
 | 
			
		||||
DELETE FROM Clubs;";
 | 
			
		||||
//        public const string DeleteUnusedCustomReactionsAndQuotes = @"DELETE FROM CustomReactions 
 | 
			
		||||
//WHERE UseCount=0 AND (DateAdded < date('now', '-7 day') OR DateAdded is null);
 | 
			
		||||
 | 
			
		||||
//DELETE FROM Quotes 
 | 
			
		||||
//WHERE UseCount=0 AND (DateAdded < date('now', '-7 day') OR DateAdded is null);";
 | 
			
		||||
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public DangerousCommandsService(DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<int> ExecuteSql(string sql)
 | 
			
		||||
        {
 | 
			
		||||
            int res;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                res = await uow._context.Database.ExecuteSqlRawAsync(sql);
 | 
			
		||||
            }
 | 
			
		||||
            return res;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class SelectResult
 | 
			
		||||
        {
 | 
			
		||||
            public List<string> ColumnNames { get; set; }
 | 
			
		||||
            public List<string[]> Results { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public SelectResult SelectSql(string sql)
 | 
			
		||||
        {
 | 
			
		||||
            var result = new SelectResult()
 | 
			
		||||
            {
 | 
			
		||||
                ColumnNames = new List<string>(),
 | 
			
		||||
                Results = new List<string[]>(),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var conn = uow._context.Database.GetDbConnection();
 | 
			
		||||
                using (var cmd = conn.CreateCommand())
 | 
			
		||||
                {
 | 
			
		||||
                    cmd.CommandText = sql;
 | 
			
		||||
                    using (var reader = cmd.ExecuteReader())
 | 
			
		||||
                    {
 | 
			
		||||
                        if (reader.HasRows)
 | 
			
		||||
                        {
 | 
			
		||||
                            for (int 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)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            
 | 
			
		||||
            // get waifu info
 | 
			
		||||
            var wi = await uow._context.Set<WaifuInfo>()
 | 
			
		||||
                .FirstOrDefaultAsyncEF(x => x.Waifu.UserId == userId);
 | 
			
		||||
 | 
			
		||||
            // if it exists, delete waifu related things
 | 
			
		||||
            if (!(wi is null))
 | 
			
		||||
            {
 | 
			
		||||
                // remove updates which have new or old as this waifu
 | 
			
		||||
                await uow._context
 | 
			
		||||
                    .WaifuUpdates
 | 
			
		||||
                    .DeleteAsync(wu => wu.New.UserId == userId || wu.Old.UserId == userId);
 | 
			
		||||
 | 
			
		||||
                // delete all items this waifu owns
 | 
			
		||||
                await uow._context
 | 
			
		||||
                    .Set<WaifuItem>()
 | 
			
		||||
                    .DeleteAsync(x => x.WaifuInfoId == wi.Id);
 | 
			
		||||
 | 
			
		||||
                // all waifus this waifu claims are released
 | 
			
		||||
                await uow._context
 | 
			
		||||
                    .Set<WaifuInfo>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Where(x => x.Claimer.UserId == userId)
 | 
			
		||||
                    .UpdateAsync(x => new WaifuInfo() {ClaimerId = null});
 | 
			
		||||
                
 | 
			
		||||
                // all affinities set to this waifu are reset
 | 
			
		||||
                await uow._context
 | 
			
		||||
                    .Set<WaifuInfo>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Where(x => x.Affinity.UserId == userId)
 | 
			
		||||
                    .UpdateAsync(x => new WaifuInfo() {AffinityId = null});
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // delete guild xp
 | 
			
		||||
            await uow._context
 | 
			
		||||
                .UserXpStats
 | 
			
		||||
                .DeleteAsync(x => x.UserId == userId);
 | 
			
		||||
            
 | 
			
		||||
            // delete currency transactions
 | 
			
		||||
            await uow._context.Set<CurrencyTransaction>()
 | 
			
		||||
                .DeleteAsync(x => x.UserId == userId);
 | 
			
		||||
            
 | 
			
		||||
            // delete user, currency, and clubs go away with it
 | 
			
		||||
            await uow._context.DiscordUser
 | 
			
		||||
                .DeleteAsync(u => u.UserId == userId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,159 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class DiscordPermOverrideService : INService, ILateBlocker
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly IServiceProvider _services;
 | 
			
		||||
 | 
			
		||||
        public int Priority { get; } = int.MaxValue;
 | 
			
		||||
 | 
			
		||||
        private readonly ConcurrentDictionary<(ulong, string), DiscordPermOverride> _overrides;
 | 
			
		||||
 | 
			
		||||
        public DiscordPermOverrideService(DbService db, IServiceProvider services)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _services = services;
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            _overrides = uow._context.DiscordPermOverrides
 | 
			
		||||
                .AsNoTracking()
 | 
			
		||||
                .AsEnumerable()
 | 
			
		||||
                .ToDictionary(o => (o.GuildId ?? 0, o.Command), o => o)
 | 
			
		||||
                .ToConcurrent();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public bool TryGetOverrides(ulong guildId, string commandName, out 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((GuildPermission) perms);
 | 
			
		||||
            return rupa.CheckPermissionsAsync(ctx, command, services);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task AddOverride(ulong guildId, string commandName, GuildPerm perm)
 | 
			
		||||
        {
 | 
			
		||||
            commandName = commandName.ToLowerInvariant();
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var over = await uow._context
 | 
			
		||||
                    .Set<DiscordPermOverride>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command);
 | 
			
		||||
 | 
			
		||||
                if (over is null)
 | 
			
		||||
                {
 | 
			
		||||
                    uow._context.Set<DiscordPermOverride>()
 | 
			
		||||
                        .Add(over = new DiscordPermOverride()
 | 
			
		||||
                        {
 | 
			
		||||
                            Command = commandName,
 | 
			
		||||
                            Perm = perm,
 | 
			
		||||
                            GuildId = guildId,
 | 
			
		||||
                        });
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    over.Perm = perm;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                _overrides[(guildId, commandName)] = over;
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task ClearAllOverrides(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var overrides = await uow._context
 | 
			
		||||
                    .Set<DiscordPermOverride>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .AsNoTracking()
 | 
			
		||||
                    .Where(x => x.GuildId == guildId)
 | 
			
		||||
                    .ToListAsync();
 | 
			
		||||
                
 | 
			
		||||
                uow._context.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();
 | 
			
		||||
            
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var over = await uow._context
 | 
			
		||||
                    .Set<DiscordPermOverride>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .AsNoTracking()
 | 
			
		||||
                    .FirstOrDefaultAsync(x => x.GuildId == guildId && x.Command == commandName);
 | 
			
		||||
 | 
			
		||||
                if (over is null)
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
                uow._context.Remove(over);
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
                _overrides.TryRemove((guildId, commandName), out _);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<List<DiscordPermOverride>> GetAllOverrides(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                return uow._context
 | 
			
		||||
                    .Set<DiscordPermOverride>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .AsNoTracking()
 | 
			
		||||
                    .Where(x => x.GuildId == guildId)
 | 
			
		||||
                    .ToListAsync();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> TryBlockLate(DiscordSocketClient client, ICommandContext context, string moduleName,
 | 
			
		||||
            CommandInfo command)
 | 
			
		||||
        {
 | 
			
		||||
            if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && !(perm is null))
 | 
			
		||||
            {
 | 
			
		||||
                var result = await new RequireUserPermissionAttribute((GuildPermission) perm)
 | 
			
		||||
                    .CheckPermissionsAsync(context, command, _services);
 | 
			
		||||
 | 
			
		||||
                return !result.IsSuccess;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,132 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class GameVoiceChannelService : INService
 | 
			
		||||
    {
 | 
			
		||||
        public ConcurrentHashSet<ulong> GameVoiceChannels { get; } = new ConcurrentHashSet<ulong>();
 | 
			
		||||
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
        public GameVoiceChannelService(DiscordSocketClient client, DbService db, NadekoBot bot)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _client = client;
 | 
			
		||||
 | 
			
		||||
            GameVoiceChannels = new ConcurrentHashSet<ulong>(
 | 
			
		||||
                bot.AllGuildConfigs.Where(gc => gc.GameVoiceChannel != null)
 | 
			
		||||
                                         .Select(gc => gc.GameVoiceChannel.Value));
 | 
			
		||||
 | 
			
		||||
            _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated;
 | 
			
		||||
            _client.GuildMemberUpdated += _client_GuildMemberUpdated;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task _client_GuildMemberUpdated(SocketGuildUser before, SocketGuildUser after)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    //if the user is in the voice channel and that voice channel is gvc
 | 
			
		||||
                    var vc = after.VoiceChannel;
 | 
			
		||||
                    if (vc == null || !GameVoiceChannels.Contains(vc.Id))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    //if the activity has changed, and is a playing activity
 | 
			
		||||
                    if (before.Activity != after.Activity
 | 
			
		||||
                        && after.Activity != null
 | 
			
		||||
                        && after.Activity.Type == Discord.ActivityType.Playing)
 | 
			
		||||
                    {
 | 
			
		||||
                        //trigger gvc
 | 
			
		||||
                        await TriggerGvc(after, after.Activity.Name);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
                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.GuildConfigs.ForId(guildId, set => set);
 | 
			
		||||
 | 
			
		||||
                if (gc.GameVoiceChannel == vchId)
 | 
			
		||||
                {
 | 
			
		||||
                    GameVoiceChannels.TryRemove(vchId);
 | 
			
		||||
                    id = gc.GameVoiceChannel = null;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (gc.GameVoiceChannel != null)
 | 
			
		||||
                        GameVoiceChannels.TryRemove(gc.GameVoiceChannel.Value);
 | 
			
		||||
                    GameVoiceChannels.Add(vchId);
 | 
			
		||||
                    id = gc.GameVoiceChannel = vchId;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
            return id;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task Client_UserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (!(usr is SocketGuildUser gUser))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    var game = gUser.Activity?.Name;
 | 
			
		||||
 | 
			
		||||
                    if (oldState.VoiceChannel == newState.VoiceChannel ||
 | 
			
		||||
                        newState.VoiceChannel == null)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    if (!GameVoiceChannels.Contains(newState.VoiceChannel.Id) ||
 | 
			
		||||
                        string.IsNullOrWhiteSpace(game))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    await TriggerGvc(gUser, game);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error running VoiceStateUpdate in gvc");
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task TriggerGvc(SocketGuildUser gUser, string game)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(game))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            game = game.TrimTo(50).ToLowerInvariant();
 | 
			
		||||
            var vch = gUser.Guild.VoiceChannels
 | 
			
		||||
                .FirstOrDefault(x => x.Name.ToLowerInvariant() == game);
 | 
			
		||||
 | 
			
		||||
            if (vch == null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await Task.Delay(1000).ConfigureAwait(false);
 | 
			
		||||
            await gUser.ModifyAsync(gu => gu.Channel = vch).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,85 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class GuildTimezoneService : INService
 | 
			
		||||
    {
 | 
			
		||||
        public static ConcurrentDictionary<ulong, GuildTimezoneService> AllServices { get; } = new ConcurrentDictionary<ulong, GuildTimezoneService>();
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, TimeZoneInfo> _timezones;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public GuildTimezoneService(DiscordSocketClient client, NadekoBot bot, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _timezones = bot.AllGuildConfigs
 | 
			
		||||
                .Select(GetTimzezoneTuple)
 | 
			
		||||
                .Where(x => x.Timezone != null)
 | 
			
		||||
                .ToDictionary(x => x.GuildId, x => x.Timezone)
 | 
			
		||||
                .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
            var curUser = client.CurrentUser;
 | 
			
		||||
            if (curUser != 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 != null)
 | 
			
		||||
                _timezones.TryAdd(guildId, tz);
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static (ulong GuildId, TimeZoneInfo Timezone) GetTimzezoneTuple(GuildConfig x)
 | 
			
		||||
        {
 | 
			
		||||
            TimeZoneInfo tz;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (x.TimeZoneId == null)
 | 
			
		||||
                    tz = null;
 | 
			
		||||
                else
 | 
			
		||||
                    tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                tz = null;
 | 
			
		||||
            }
 | 
			
		||||
            return (x.GuildId, Timezone: tz);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public TimeZoneInfo GetTimeZoneOrDefault(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            if (_timezones.TryGetValue(guildId, out var tz))
 | 
			
		||||
                return tz;
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void SetTimeZone(ulong guildId, TimeZoneInfo tz)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var gc = uow.GuildConfigs.ForId(guildId, set => set);
 | 
			
		||||
 | 
			
		||||
                gc.TimeZoneId = tz?.Id;
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
 | 
			
		||||
                if (tz == null)
 | 
			
		||||
                    _timezones.TryRemove(guildId, out tz);
 | 
			
		||||
                else
 | 
			
		||||
                    _timezones.AddOrUpdate(guildId, tz, (key, old) => tz);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public TimeZoneInfo GetTimeZoneOrUtc(ulong guildId)
 | 
			
		||||
            => GetTimeZoneOrDefault(guildId) ?? TimeZoneInfo.Utc;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1279
									
								
								src/NadekoBot/Modules/Administration/Services/LogCommandService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1279
									
								
								src/NadekoBot/Modules/Administration/Services/LogCommandService.cs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										477
									
								
								src/NadekoBot/Modules/Administration/Services/MuteService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										477
									
								
								src/NadekoBot/Modules/Administration/Services/MuteService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,477 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public enum MuteType
 | 
			
		||||
    {
 | 
			
		||||
        Voice,
 | 
			
		||||
        Chat,
 | 
			
		||||
        All
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class MuteService : INService
 | 
			
		||||
    {
 | 
			
		||||
        public ConcurrentDictionary<ulong, string> GuildMuteRoles { get; }
 | 
			
		||||
        public ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> MutedUsers { get; }
 | 
			
		||||
 | 
			
		||||
        public ConcurrentDictionary<ulong, ConcurrentDictionary<(ulong, TimerType), Timer>> Un_Timers { get; }
 | 
			
		||||
            = new ConcurrentDictionary<ulong, ConcurrentDictionary<(ulong, TimerType), Timer>>();
 | 
			
		||||
 | 
			
		||||
        public event Action<IGuildUser, IUser, MuteType, string> UserMuted = delegate { };
 | 
			
		||||
        public event Action<IGuildUser, IUser, MuteType, string> UserUnmuted = delegate { };
 | 
			
		||||
 | 
			
		||||
        private static readonly OverwritePermissions denyOverwrite =
 | 
			
		||||
            new OverwritePermissions(addReactions: PermValue.Deny, sendMessages: PermValue.Deny,
 | 
			
		||||
                attachFiles: PermValue.Deny);
 | 
			
		||||
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public MuteService(DiscordSocketClient client, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _db = db;
 | 
			
		||||
            
 | 
			
		||||
            using (var uow = db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var guildIds = client.Guilds.Select(x => x.Id).ToList();
 | 
			
		||||
                var configs = uow._context.Set<GuildConfig>().AsQueryable()
 | 
			
		||||
                    .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 ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>>(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;
 | 
			
		||||
            
 | 
			
		||||
            var _ = Task.Run(() => user.SendMessageAsync(embed: new EmbedBuilder()
 | 
			
		||||
                .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;
 | 
			
		||||
        
 | 
			
		||||
            var _ = Task.Run(() => user.SendMessageAsync(embed: new EmbedBuilder()
 | 
			
		||||
                .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 ConcurrentHashSet<ulong> muted);
 | 
			
		||||
 | 
			
		||||
                if (muted == null || !muted.Contains(usr.Id))
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
                var _ = Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute").ConfigureAwait(false));
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error in MuteService UserJoined event");
 | 
			
		||||
            }
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task SetMuteRoleAsync(ulong guildId, string name)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigs.ForId(guildId, set => set);
 | 
			
		||||
                config.MuteRoleName = name;
 | 
			
		||||
                GuildMuteRoles.AddOrUpdate(guildId, name, (id, old) => 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).ConfigureAwait(false); } catch { }
 | 
			
		||||
                var muteRole = await GetMuteRole(usr.Guild).ConfigureAwait(false);
 | 
			
		||||
                if (!usr.RoleIds.Contains(muteRole.Id))
 | 
			
		||||
                    await usr.AddRoleAsync(muteRole).ConfigureAwait(false);
 | 
			
		||||
                StopTimer(usr.GuildId, usr.Id, TimerType.Mute);
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var config = uow.GuildConfigs.ForId(usr.Guild.Id,
 | 
			
		||||
                        set => set.Include(gc => gc.MutedUsers)
 | 
			
		||||
                            .Include(gc => gc.UnmuteTimers));
 | 
			
		||||
                    config.MutedUsers.Add(new MutedUserId()
 | 
			
		||||
                    {
 | 
			
		||||
                        UserId = usr.Id
 | 
			
		||||
                    });
 | 
			
		||||
                    if (MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet<ulong> 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).ConfigureAwait(false);
 | 
			
		||||
                    UserMuted(usr, mod, MuteType.Voice, reason);
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
            }
 | 
			
		||||
            else if (type == MuteType.Chat)
 | 
			
		||||
            {
 | 
			
		||||
                await usr.AddRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false);
 | 
			
		||||
                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);
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var config = uow.GuildConfigs.ForId(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 != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        uow._context.Remove(toRemove);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (MutedUsers.TryGetValue(guildId, out ConcurrentHashSet<ulong> muted))
 | 
			
		||||
                        muted.TryRemove(usrId);
 | 
			
		||||
 | 
			
		||||
                    config.UnmuteTimers.RemoveWhere(x => x.UserId == usrId);
 | 
			
		||||
 | 
			
		||||
                    await uow.SaveChangesAsync();
 | 
			
		||||
                }
 | 
			
		||||
                if (usr != null)
 | 
			
		||||
                {
 | 
			
		||||
                    try { await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false); } catch { }
 | 
			
		||||
                    try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); } catch { /*ignore*/ }
 | 
			
		||||
                    UserUnmuted(usr, mod, MuteType.All, reason);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else if (type == MuteType.Voice)
 | 
			
		||||
            {
 | 
			
		||||
                if (usr == null)
 | 
			
		||||
                    return;
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false);
 | 
			
		||||
                    UserUnmuted(usr, mod, MuteType.Voice, reason);
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
            }
 | 
			
		||||
            else if (type == MuteType.Chat)
 | 
			
		||||
            {
 | 
			
		||||
                if (usr == null)
 | 
			
		||||
                    return;
 | 
			
		||||
                await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false);
 | 
			
		||||
                UserUnmuted(usr, mod, MuteType.Chat, reason);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<IRole> GetMuteRole(IGuild guild)
 | 
			
		||||
        {
 | 
			
		||||
            if (guild == 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 == null)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                //if it doesn't exist, create it
 | 
			
		||||
                try { muteRole = await guild.CreateRoleAsync(muteRoleName, isMentionable: false).ConfigureAwait(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).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var toOverwrite in (await guild.GetTextChannelsAsync().ConfigureAwait(false)))
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id
 | 
			
		||||
                        && x.TargetType == PermissionTarget.Role))
 | 
			
		||||
                    {
 | 
			
		||||
                        await toOverwrite.AddPermissionOverwriteAsync(muteRole, denyOverwrite)
 | 
			
		||||
                                .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                        await Task.Delay(200).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                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).ConfigureAwait(false); // mute the user. This will also remove any previous unmute timers
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigs.ForId(user.GuildId, set => set.Include(x => x.UnmuteTimers));
 | 
			
		||||
                config.UnmuteTimers.Add(new UnmuteTimer()
 | 
			
		||||
                {
 | 
			
		||||
                    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, IUser user, TimeSpan after, string reason)
 | 
			
		||||
        {
 | 
			
		||||
            await guild.AddBanAsync(user.Id, 0, reason).ConfigureAwait(false);
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigs.ForId(guild.Id, set => set.Include(x => x.UnbanTimer));
 | 
			
		||||
                config.UnbanTimer.Add(new UnbanTimer()
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = user.Id,
 | 
			
		||||
                    UnbanAt = DateTime.UtcNow + after,
 | 
			
		||||
                }); // add teh unmute timer to the database
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            StartUn_Timer(guild.Id, user.Id, after, TimerType.Ban); // start the timer
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task TimedRole(IGuildUser user, TimeSpan after, string reason, IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            await user.AddRoleAsync(role).ConfigureAwait(false);
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigs.ForId(user.GuildId, set => set.Include(x => x.UnroleTimer));
 | 
			
		||||
                config.UnroleTimer.Add(new UnroleTimer()
 | 
			
		||||
                {
 | 
			
		||||
                    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 enum TimerType { Mute, Ban, AddRole }
 | 
			
		||||
        public void StartUn_Timer(ulong guildId, ulong userId, TimeSpan after, TimerType type, ulong? roleId = null)
 | 
			
		||||
        {
 | 
			
		||||
            //load the unmute timers for this guild
 | 
			
		||||
            var userUnTimers = Un_Timers.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 != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            await guild.RemoveBanAsync(userId).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex, "Couldn't unban user {0} in guild {1}", userId, guildId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (type == TimerType.AddRole)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        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 != null && user != null && user.Roles.Contains(role))
 | 
			
		||||
                        {
 | 
			
		||||
                            await user.RemoveRoleAsync(role).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex, "Couldn't remove role from user {0} in guild {1}", 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").ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        RemoveTimerFromDb(guildId, userId, type); // if unmute errored, just remove unmute from db
 | 
			
		||||
                        Log.Warning(ex, "Couldn't unmute user {0} in guild {1}", userId, guildId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }, null, after, Timeout.InfiniteTimeSpan);
 | 
			
		||||
 | 
			
		||||
            //add it, or stop the old one and add this one
 | 
			
		||||
            userUnTimers.AddOrUpdate((userId, type), (key) => toAdd, (key, old) =>
 | 
			
		||||
            {
 | 
			
		||||
                old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                return toAdd;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void StopTimer(ulong guildId, ulong userId, TimerType type)
 | 
			
		||||
        {
 | 
			
		||||
            if (!Un_Timers.TryGetValue(guildId, out ConcurrentDictionary<(ulong, TimerType), Timer> userTimer))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (userTimer.TryRemove((userId, type), out Timer 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.GuildConfigs.ForId(guildId, set => set.Include(x => x.UnmuteTimers));
 | 
			
		||||
                    toDelete = config.UnmuteTimers.FirstOrDefault(x => x.UserId == userId);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var config = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.UnbanTimer));
 | 
			
		||||
                    toDelete = config.UnbanTimer.FirstOrDefault(x => x.UserId == userId);
 | 
			
		||||
                }
 | 
			
		||||
                if (toDelete != null)
 | 
			
		||||
                {
 | 
			
		||||
                    uow._context.Remove(toDelete);
 | 
			
		||||
                }
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
//using NadekoBot.Core.Services;
 | 
			
		||||
//using System;
 | 
			
		||||
//using System.Collections.Generic;
 | 
			
		||||
//using System.IO;
 | 
			
		||||
//using System.Linq;
 | 
			
		||||
//using System.Text.RegularExpressions;
 | 
			
		||||
 | 
			
		||||
//namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
//{
 | 
			
		||||
//    public class PackagesService : INService
 | 
			
		||||
//    {
 | 
			
		||||
//        public IEnumerable<string> Packages { get; private set; }
 | 
			
		||||
 | 
			
		||||
//        public PackagesService()
 | 
			
		||||
//        {
 | 
			
		||||
//            ReloadAvailablePackages();
 | 
			
		||||
//        }
 | 
			
		||||
 | 
			
		||||
//        public void ReloadAvailablePackages()
 | 
			
		||||
//        {
 | 
			
		||||
//            Packages = Directory.GetDirectories(Path.Combine(Appctx.BaseDirectory, "modules\\"), "NadekoBot.Modules.*", SearchOption.AllDirectories)
 | 
			
		||||
//                   .SelectMany(x => Directory.GetFiles(x, "NadekoBot.Modules.*.dll"))
 | 
			
		||||
//                   .Select(x => Path.GetFileNameWithoutExtension(x))
 | 
			
		||||
//                   .Select(x =>
 | 
			
		||||
//                   {
 | 
			
		||||
//                       var m = Regex.Match(x, @"NadekoBot\.Modules\.(?<name>.*)");
 | 
			
		||||
//                       return m.Groups["name"].Value;
 | 
			
		||||
//                   });
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//}
 | 
			
		||||
@@ -0,0 +1,122 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.Replacements;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using Discord;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public sealed class PlayingRotateService : INService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly Timer _t;
 | 
			
		||||
        private readonly BotConfigService _bss;
 | 
			
		||||
        private readonly Replacer _rep;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly NadekoBot _bot;
 | 
			
		||||
 | 
			
		||||
        private class TimerState
 | 
			
		||||
        {
 | 
			
		||||
            public int Index { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public PlayingRotateService(DiscordSocketClient client, DbService db, NadekoBot bot,
 | 
			
		||||
            BotConfigService bss, IEnumerable<IPlaceholderProvider> phProviders)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _bot = bot;
 | 
			
		||||
            _bss = bss;
 | 
			
		||||
 | 
			
		||||
            if (client.ShardId == 0)
 | 
			
		||||
            {
 | 
			
		||||
                _rep = new ReplacementBuilder()
 | 
			
		||||
                    .WithClient(client)
 | 
			
		||||
                    .WithProviders(phProviders)
 | 
			
		||||
                    .Build();
 | 
			
		||||
 | 
			
		||||
                _t = new Timer(RotatingStatuses, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async void RotatingStatuses(object objState)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var state = (TimerState) objState;
 | 
			
		||||
 | 
			
		||||
                if (!_bss.Data.RotateStatuses) return;
 | 
			
		||||
 | 
			
		||||
                IReadOnlyList<RotatingPlayingStatus> rotatingStatuses;
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    rotatingStatuses = uow._context.RotatingStatus
 | 
			
		||||
                        .AsNoTracking()
 | 
			
		||||
                        .OrderBy(x => x.Id)
 | 
			
		||||
                        .ToList();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (rotatingStatuses.Count == 0)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var playingStatus = state.Index >= rotatingStatuses.Count
 | 
			
		||||
                    ? rotatingStatuses[state.Index = 0]
 | 
			
		||||
                    : rotatingStatuses[state.Index++];
 | 
			
		||||
 | 
			
		||||
                var statusText = _rep.Replace(playingStatus.Status);
 | 
			
		||||
                await _bot.SetGameAsync(statusText, 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));
 | 
			
		||||
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var toRemove = await uow._context.RotatingStatus
 | 
			
		||||
                .AsQueryable()
 | 
			
		||||
                .AsNoTracking()
 | 
			
		||||
                .Skip(index)
 | 
			
		||||
                .FirstOrDefaultAsync();
 | 
			
		||||
 | 
			
		||||
            if (toRemove is null)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            uow._context.Remove(toRemove);
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            return toRemove.Status;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task AddPlaying(ActivityType t, string status)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var toAdd = new RotatingPlayingStatus {Status = status, Type = t};
 | 
			
		||||
            uow._context.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._context.RotatingStatus.AsNoTracking().ToList();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,487 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Channels;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Modules.Administration.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Common.TypeReaders.Models;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class ProtectionService : INService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, AntiRaidStats> _antiRaidGuilds
 | 
			
		||||
            = new ConcurrentDictionary<ulong, AntiRaidStats>();
 | 
			
		||||
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, AntiSpamStats> _antiSpamGuilds
 | 
			
		||||
            = new ConcurrentDictionary<ulong, AntiSpamStats>();
 | 
			
		||||
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, AntiAltStats> _antiAltGuilds
 | 
			
		||||
            = new ConcurrentDictionary<ulong, AntiAltStats>();
 | 
			
		||||
        
 | 
			
		||||
        public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered
 | 
			
		||||
            = delegate { return Task.CompletedTask; };
 | 
			
		||||
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly MuteService _mute;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly UserPunishService _punishService;
 | 
			
		||||
        
 | 
			
		||||
        private readonly Channel<PunishQueueItem> PunishUserQueue =
 | 
			
		||||
            System.Threading.Channels.Channel.CreateUnbounded<PunishQueueItem>(new UnboundedChannelOptions()
 | 
			
		||||
            {
 | 
			
		||||
                SingleReader = true,
 | 
			
		||||
                SingleWriter = false
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        public ProtectionService(DiscordSocketClient client, NadekoBot 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._context.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)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = 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.GuildConfigs.ForId(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 != null)
 | 
			
		||||
            {
 | 
			
		||||
                var raidStats = new AntiRaidStats() { AntiRaidSettings = raid };
 | 
			
		||||
                _antiRaidGuilds[gc.GuildId] = raidStats;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (spam != null)
 | 
			
		||||
                _antiSpamGuilds[gc.GuildId] = new AntiSpamStats() { AntiSpamSettings = spam };
 | 
			
		||||
 | 
			
		||||
            var alt = gc.AntiAltSetting;
 | 
			
		||||
            if (!(alt is null))
 | 
			
		||||
                _antiAltGuilds[gc.GuildId] = new AntiAltStats(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 AntiAltStats 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 AntiRaidStats 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).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    await Task.Delay(1000 * stats.AntiRaidSettings.Seconds).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    stats.RaidUsers.TryRemove(user);
 | 
			
		||||
                    --stats.UsersCount;
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    // ignored
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task HandleAntiSpam(SocketMessage arg)
 | 
			
		||||
        {
 | 
			
		||||
            if (!(arg is SocketUserMessage msg) || msg.Author.IsBot)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            if (!(msg.Channel is ITextChannel channel))
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings) ||
 | 
			
		||||
                        spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new AntiSpamIgnore()
 | 
			
		||||
                        {
 | 
			
		||||
                            ChannelId = channel.Id
 | 
			
		||||
                        }))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, (id) => new UserSpamStats(msg),
 | 
			
		||||
                        (id, old) =>
 | 
			
		||||
                        {
 | 
			
		||||
                            old.ApplyNextMessage(msg); return old;
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                    if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats))
 | 
			
		||||
                        {
 | 
			
		||||
                            stats.Dispose();
 | 
			
		||||
                            var settings = spamSettings.AntiSpamSettings;
 | 
			
		||||
                            await PunishUsers(settings.Action, ProtectionType.Spamming, settings.MuteTime,
 | 
			
		||||
                                    settings.RoleId, (IGuildUser)msg.Author)
 | 
			
		||||
                                .ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                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 PunishQueueItem()
 | 
			
		||||
                {
 | 
			
		||||
                    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).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (action == PunishmentAction.AddRole)
 | 
			
		||||
                return null;
 | 
			
		||||
            
 | 
			
		||||
            if (!IsDurationAllowed(action))
 | 
			
		||||
                minutesDuration = 0;
 | 
			
		||||
 | 
			
		||||
            var stats = new AntiRaidStats()
 | 
			
		||||
            {
 | 
			
		||||
                AntiRaidSettings = new AntiRaidSetting()
 | 
			
		||||
                {
 | 
			
		||||
                    Action = action,
 | 
			
		||||
                    Seconds = seconds,
 | 
			
		||||
                    UserThreshold = userThreshold,
 | 
			
		||||
                    PunishDuration = minutesDuration
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            _antiRaidGuilds.AddOrUpdate(guildId, stats, (key, old) => stats);
 | 
			
		||||
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var gc = uow.GuildConfigs.ForId(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.GuildConfigs.ForId(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 var removed))
 | 
			
		||||
            {
 | 
			
		||||
                removed.UserStats.ForEach(x => x.Value.Dispose());
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var gc = uow.GuildConfigs.ForId(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).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (!IsDurationAllowed(action))
 | 
			
		||||
                punishDurationMinutes = 0;
 | 
			
		||||
 | 
			
		||||
            var stats = new AntiSpamStats
 | 
			
		||||
            {
 | 
			
		||||
                AntiSpamSettings = new AntiSpamSetting()
 | 
			
		||||
                {
 | 
			
		||||
                    Action = action,
 | 
			
		||||
                    MessageThreshold = messageCount,
 | 
			
		||||
                    MuteTime = punishDurationMinutes,
 | 
			
		||||
                    RoleId = roleId,
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            stats = _antiSpamGuilds.AddOrUpdate(guildId, stats, (key, old) =>
 | 
			
		||||
            {
 | 
			
		||||
                stats.AntiSpamSettings.IgnoredChannels = old.AntiSpamSettings.IgnoredChannels;
 | 
			
		||||
                return stats;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiSpamSetting));
 | 
			
		||||
 | 
			
		||||
                if (gc.AntiSpamSetting != 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;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var gc = uow.GuildConfigs.ForId(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._context.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:
 | 
			
		||||
                    return true;
 | 
			
		||||
                default:
 | 
			
		||||
                    return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task StartAntiAltAsync(ulong guildId, int minAgeMinutes, PunishmentAction action,
 | 
			
		||||
            int actionDurationMinutes = 0, ulong? roleId = null)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiAltSetting));
 | 
			
		||||
            gc.AntiAltSetting = new AntiAltSetting()
 | 
			
		||||
            {
 | 
			
		||||
                Action = action,
 | 
			
		||||
                ActionDurationMinutes = actionDurationMinutes,
 | 
			
		||||
                MinAge = TimeSpan.FromMinutes(minAgeMinutes),
 | 
			
		||||
                RoleId = roleId,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            _antiAltGuilds[guildId] = new AntiAltStats(gc.AntiAltSetting);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> TryStopAntiAlt(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_antiAltGuilds.TryRemove(guildId, out _))
 | 
			
		||||
                return false;
 | 
			
		||||
            
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiAltSetting));
 | 
			
		||||
            gc.AntiAltSetting = null;
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,78 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class PruneService : INService
 | 
			
		||||
    {
 | 
			
		||||
        //channelids where prunes are currently occuring
 | 
			
		||||
        private ConcurrentHashSet<ulong> _pruningGuilds = new ConcurrentHashSet<ulong>();
 | 
			
		||||
        private readonly TimeSpan twoWeeks = TimeSpan.FromDays(14);
 | 
			
		||||
        private readonly LogCommandService _logService;
 | 
			
		||||
 | 
			
		||||
        public PruneService(LogCommandService logService)
 | 
			
		||||
        {
 | 
			
		||||
            this._logService = logService;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task PruneWhere(ITextChannel channel, int amount, Func<IMessage, bool> predicate)
 | 
			
		||||
        {
 | 
			
		||||
            channel.ThrowIfNull(nameof(channel));
 | 
			
		||||
            if (amount <= 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(amount));
 | 
			
		||||
 | 
			
		||||
            if (!_pruningGuilds.Add(channel.GuildId))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                IMessage[] msgs;
 | 
			
		||||
                IMessage lastMessage = null;
 | 
			
		||||
                msgs = (await channel.GetMessagesAsync(50).FlattenAsync().ConfigureAwait(false)).Where(predicate).Take(amount).ToArray();
 | 
			
		||||
                while (amount > 0 && msgs.Any())
 | 
			
		||||
                {
 | 
			
		||||
                    lastMessage = msgs[msgs.Length - 1];
 | 
			
		||||
 | 
			
		||||
                    var bulkDeletable = new List<IMessage>();
 | 
			
		||||
                    var singleDeletable = new List<IMessage>();
 | 
			
		||||
                    foreach (var x in msgs)
 | 
			
		||||
                    {
 | 
			
		||||
                        _logService.AddDeleteIgnore(x.Id);
 | 
			
		||||
 | 
			
		||||
                        if (DateTime.UtcNow - x.CreatedAt < twoWeeks)
 | 
			
		||||
                            bulkDeletable.Add(x);
 | 
			
		||||
                        else
 | 
			
		||||
                            singleDeletable.Add(x);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (bulkDeletable.Count > 0)
 | 
			
		||||
                        await Task.WhenAll(Task.Delay(1000), channel.DeleteMessagesAsync(bulkDeletable)).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    var i = 0;
 | 
			
		||||
                    foreach (var group in singleDeletable.GroupBy(x => ++i / (singleDeletable.Count / 5)))
 | 
			
		||||
                        await Task.WhenAll(Task.Delay(1000), Task.WhenAll(group.Select(x => x.DeleteAsync()))).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    //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)
 | 
			
		||||
                        msgs = (await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync().ConfigureAwait(false)).Where(predicate).Take(amount).ToArray();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                //ignore
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _pruningGuilds.TryRemove(channel.GuildId);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,190 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class RoleCommandsService : INService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
 | 
			
		||||
 | 
			
		||||
        public RoleCommandsService(DiscordSocketClient client, DbService db,
 | 
			
		||||
            NadekoBot bot)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _client = client;
 | 
			
		||||
#if !GLOBAL_NADEKO
 | 
			
		||||
            _models = bot.AllGuildConfigs.ToDictionary(x => x.GuildId,
 | 
			
		||||
                x => x.ReactionRoleMessages)
 | 
			
		||||
                .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
            _client.ReactionAdded += _client_ReactionAdded;
 | 
			
		||||
            _client.ReactionRemoved += _client_ReactionRemoved;
 | 
			
		||||
#endif
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task _client_ReactionAdded(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (!reaction.User.IsSpecified ||
 | 
			
		||||
                        reaction.User.Value.IsBot ||
 | 
			
		||||
                        !(reaction.User.Value is SocketGuildUser gusr))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    if (!(chan is SocketGuildChannel gch))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    if (!_models.TryGetValue(gch.Guild.Id, out var confs))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
 | 
			
		||||
 | 
			
		||||
                    if (conf == null)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    // compare emote names for backwards compatibility :facepalm:
 | 
			
		||||
                    var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
 | 
			
		||||
                    if (reactionRole != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (conf.Exclusive)
 | 
			
		||||
                        {
 | 
			
		||||
                            var roleIds = conf.ReactionRoles.Select(x => x.RoleId)
 | 
			
		||||
                                .Where(x => x != reactionRole.RoleId)
 | 
			
		||||
                                .Select(x => gusr.Guild.GetRole(x))
 | 
			
		||||
                                .Where(x => x != null);
 | 
			
		||||
 | 
			
		||||
                            var __ = Task.Run(async () =>
 | 
			
		||||
                            {
 | 
			
		||||
                                try
 | 
			
		||||
                                {
 | 
			
		||||
                                    //if the role is exclusive,
 | 
			
		||||
                                    // remove all other reactions user added to the message
 | 
			
		||||
                                    var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
 | 
			
		||||
                                    foreach (var r in dl.Reactions)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        if (r.Key.Name == reaction.Emote.Name)
 | 
			
		||||
                                            continue;
 | 
			
		||||
                                        try { await dl.RemoveReactionAsync(r.Key, gusr).ConfigureAwait(false); } catch { }
 | 
			
		||||
                                        await Task.Delay(100).ConfigureAwait(false);
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                                catch { }
 | 
			
		||||
                            });
 | 
			
		||||
                            await gusr.RemoveRolesAsync(roleIds).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        var toAdd = gusr.Guild.GetRole(reactionRole.RoleId);
 | 
			
		||||
                        if (toAdd != null && !gusr.Roles.Contains(toAdd))
 | 
			
		||||
                        {
 | 
			
		||||
                            await gusr.AddRolesAsync(new[] { toAdd }).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
 | 
			
		||||
                        await dl.RemoveReactionAsync(reaction.Emote, dl.Author,
 | 
			
		||||
                            new RequestOptions()
 | 
			
		||||
                            {
 | 
			
		||||
                                RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
 | 
			
		||||
                            }).ConfigureAwait(false);
 | 
			
		||||
                        Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task _client_ReactionRemoved(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (!reaction.User.IsSpecified ||
 | 
			
		||||
                        reaction.User.Value.IsBot ||
 | 
			
		||||
                        !(reaction.User.Value is SocketGuildUser gusr))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    if (!(chan is SocketGuildChannel gch))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    if (!_models.TryGetValue(gch.Guild.Id, out var confs))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
 | 
			
		||||
 | 
			
		||||
                    if (conf == null)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
 | 
			
		||||
 | 
			
		||||
                    if (reactionRole != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        var role = gusr.Guild.GetRole(reactionRole.RoleId);
 | 
			
		||||
                        if (role == null)
 | 
			
		||||
                            return;
 | 
			
		||||
                        await gusr.RemoveRoleAsync(role).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool Get(ulong id, out IndexedCollection<ReactionRoleMessage> rrs)
 | 
			
		||||
        {
 | 
			
		||||
            return _models.TryGetValue(id, out rrs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool Add(ulong id, ReactionRoleMessage rrm)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var gc = uow.GuildConfigs.ForId(id, set => set
 | 
			
		||||
                    .Include(x => x.ReactionRoleMessages)
 | 
			
		||||
                    .ThenInclude(x => x.ReactionRoles));
 | 
			
		||||
                if (gc.ReactionRoleMessages.Count >= 10)
 | 
			
		||||
                    return false;
 | 
			
		||||
                gc.ReactionRoleMessages.Add(rrm);
 | 
			
		||||
                _models.AddOrUpdate(id,
 | 
			
		||||
                    gc.ReactionRoleMessages,
 | 
			
		||||
                    delegate { return gc.ReactionRoleMessages; });
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Remove(ulong id, int index)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var gc = uow.GuildConfigs.ForId(id,
 | 
			
		||||
                    set => set.Include(x => x.ReactionRoleMessages)
 | 
			
		||||
                        .ThenInclude(x => x.ReactionRoles));
 | 
			
		||||
                uow._context.Set<ReactionRole>()
 | 
			
		||||
                    .RemoveRange(gc.ReactionRoleMessages[index].ReactionRoles);
 | 
			
		||||
                gc.ReactionRoleMessages.RemoveAt(index);
 | 
			
		||||
                _models.AddOrUpdate(id,
 | 
			
		||||
                    gc.ReactionRoleMessages,
 | 
			
		||||
                    delegate { return gc.ReactionRoleMessages; });
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,270 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Modules.Xp;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class SelfAssignedRolesService : INService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public enum RemoveResult
 | 
			
		||||
        {
 | 
			
		||||
            Removed, // successfully removed
 | 
			
		||||
            Err_Not_Assignable, // not assignable (error)
 | 
			
		||||
            Err_Not_Have, // you don't have a role you want to remove (error)
 | 
			
		||||
            Err_Not_Perms, // bot doesn't have perms (error)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum AssignResult
 | 
			
		||||
        {
 | 
			
		||||
            Assigned, // successfully removed
 | 
			
		||||
            Err_Not_Assignable, // not assignable (error)
 | 
			
		||||
            Err_Already_Have, // you already have that role (error)
 | 
			
		||||
            Err_Not_Perms, // bot doesn't have perms (error)
 | 
			
		||||
            Err_Lvl_Req, // you are not required level (error)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public SelfAssignedRolesService(DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool AddNew(ulong guildId, IRole role, int group)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var roles = uow.SelfAssignedRoles.GetFromGuild(guildId);
 | 
			
		||||
                if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                uow.SelfAssignedRoles.Add(new SelfAssignedRole
 | 
			
		||||
                {
 | 
			
		||||
                    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.GuildConfigs.ForId(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;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var stats = uow.Xp.GetOrCreateUser(guildUser.Guild.Id, guildUser.Id);
 | 
			
		||||
                userLevelData = new LevelStats(stats.Xp + stats.AwardedXp);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            var theRoleYouWant = roles.FirstOrDefault(r => r.RoleId == role.Id);
 | 
			
		||||
            if (theRoleYouWant == null)
 | 
			
		||||
            {
 | 
			
		||||
                return (AssignResult.Err_Not_Assignable, autoDelete, null);
 | 
			
		||||
            }
 | 
			
		||||
            else if (theRoleYouWant.LevelRequirement > userLevelData.Level)
 | 
			
		||||
            {
 | 
			
		||||
                return (AssignResult.Err_Lvl_Req, autoDelete, theRoleYouWant.LevelRequirement);
 | 
			
		||||
            }
 | 
			
		||||
            else if (guildUser.RoleIds.Contains(role.Id))
 | 
			
		||||
            {
 | 
			
		||||
                return (AssignResult.Err_Already_Have, 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 != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            await guildUser.RemoveRoleAsync(sameRole).ConfigureAwait(false);
 | 
			
		||||
                            await Task.Delay(300).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                        catch
 | 
			
		||||
                        {
 | 
			
		||||
                            // ignored
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await guildUser.AddRoleAsync(role).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                return (AssignResult.Err_Not_Perms, autoDelete, ex);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (AssignResult.Assigned, autoDelete, null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> SetNameAsync(ulong guildId, int group, string name)
 | 
			
		||||
        {
 | 
			
		||||
            bool set = false;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var gc = uow.GuildConfigs.ForId(guildId, y => y.Include(x => x.SelfAssignableRoleGroupNames));
 | 
			
		||||
                var toUpdate = gc.SelfAssignableRoleGroupNames.FirstOrDefault(x => x.Number == group);
 | 
			
		||||
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(name))
 | 
			
		||||
                {
 | 
			
		||||
                    if (toUpdate != null)
 | 
			
		||||
                        gc.SelfAssignableRoleGroupNames.Remove(toUpdate);
 | 
			
		||||
                }
 | 
			
		||||
                else if (toUpdate == null)
 | 
			
		||||
                {
 | 
			
		||||
                    gc.SelfAssignableRoleGroupNames.Add(new GroupName
 | 
			
		||||
                    {
 | 
			
		||||
                        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) == null)
 | 
			
		||||
            {
 | 
			
		||||
                return (RemoveResult.Err_Not_Assignable, autoDelete);
 | 
			
		||||
            }
 | 
			
		||||
            if (!guildUser.RoleIds.Contains(role.Id))
 | 
			
		||||
            {
 | 
			
		||||
                return (RemoveResult.Err_Not_Have, autoDelete);
 | 
			
		||||
            }
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await guildUser.RemoveRoleAsync(role).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                return (RemoveResult.Err_Not_Perms, autoDelete);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (RemoveResult.Removed, autoDelete);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool RemoveSar(ulong guildId, ulong roleId)
 | 
			
		||||
        {
 | 
			
		||||
            bool success;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                success = uow.SelfAssignedRoles.DeleteByGuildAndRoleId(guildId, roleId);
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
            return success;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public (bool AutoDelete, bool Exclusive, IEnumerable<SelfAssignedRole>) GetAdAndRoles(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var gc = uow.GuildConfigs.ForId(guildId, set => set);
 | 
			
		||||
                var autoDelete = gc.AutoDeleteSelfAssignedRoleMessages;
 | 
			
		||||
                var exclusive = gc.ExclusiveSelfAssignedRoles;
 | 
			
		||||
                var roles = uow.SelfAssignedRoles.GetFromGuild(guildId);
 | 
			
		||||
 | 
			
		||||
                return (autoDelete, exclusive, roles);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool SetLevelReq(ulong guildId, IRole role, int level)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var roles = uow.SelfAssignedRoles.GetFromGuild(guildId);
 | 
			
		||||
                var sar = roles.FirstOrDefault(x => x.RoleId == role.Id);
 | 
			
		||||
                if (sar != 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.GuildConfigs.ForId(guildId, set => set);
 | 
			
		||||
 | 
			
		||||
                areExclusive = config.ExclusiveSelfAssignedRoles = !config.ExclusiveSelfAssignedRoles;
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
            return areExclusive;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public (bool Exclusive, IEnumerable<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary<int, string> GroupNames) GetRoles(IGuild guild)
 | 
			
		||||
        {
 | 
			
		||||
            var exclusive = false;
 | 
			
		||||
 | 
			
		||||
            IEnumerable<(SelfAssignedRole Model, IRole Role)> roles;
 | 
			
		||||
            IDictionary<int, string> groupNames;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var gc = uow.GuildConfigs.ForId(guild.Id, set => set.Include(x => x.SelfAssignableRoleGroupNames));
 | 
			
		||||
                exclusive = gc.ExclusiveSelfAssignedRoles;
 | 
			
		||||
                groupNames = gc.SelfAssignableRoleGroupNames.ToDictionary(x => x.Number, x => x.Name);
 | 
			
		||||
                var roleModels = uow.SelfAssignedRoles.GetFromGuild(guild.Id);
 | 
			
		||||
                roles = roleModels
 | 
			
		||||
                    .Select(x => (Model: x, Role: guild.GetRole(x.RoleId)));
 | 
			
		||||
                uow.SelfAssignedRoles.RemoveRange(roles.Where(x => x.Role == null).Select(x => x.Model).ToArray());
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (exclusive, roles.Where(x => x.Role != null), groupNames);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										436
									
								
								src/NadekoBot/Modules/Administration/Services/SelfService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										436
									
								
								src/NadekoBot/Modules/Administration/Services/SelfService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,436 @@
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using StackExchange.Redis;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using NadekoBot.Common.ShardCom;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly ConnectionMultiplexer _redis;
 | 
			
		||||
        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 ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>>();
 | 
			
		||||
 | 
			
		||||
        private readonly IDataCache _cache;
 | 
			
		||||
        private readonly IImageCache _imgs;
 | 
			
		||||
        private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
        private readonly BotConfigService _bss;
 | 
			
		||||
 | 
			
		||||
        public SelfService(DiscordSocketClient client, CommandHandler cmdHandler, DbService db,
 | 
			
		||||
            IBotStrings strings, IBotCredentials creds, IDataCache cache, IHttpClientFactory factory,
 | 
			
		||||
            BotConfigService bss)
 | 
			
		||||
        {
 | 
			
		||||
            _redis = cache.Redis;
 | 
			
		||||
            _cmdHandler = cmdHandler;
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _strings = strings;
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _creds = creds;
 | 
			
		||||
            _cache = cache;
 | 
			
		||||
            _imgs = cache.LocalImages;
 | 
			
		||||
            _httpFactory = factory;
 | 
			
		||||
            _bss = bss;
 | 
			
		||||
 | 
			
		||||
            var sub = _redis.GetSubscriber();
 | 
			
		||||
            if (_client.ShardId == 0)
 | 
			
		||||
            {
 | 
			
		||||
                sub.Subscribe(_creds.RedisKey() + "_reload_images",
 | 
			
		||||
                    delegate { _imgs.Reload(); }, CommandFlags.FireAndForget);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            sub.Subscribe(_creds.RedisKey() + "_leave_guild", async (ch, v) =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var guildStr = v.ToString()?.Trim().ToUpperInvariant();
 | 
			
		||||
                    if (string.IsNullOrWhiteSpace(guildStr))
 | 
			
		||||
                        return;
 | 
			
		||||
                    var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr) ??
 | 
			
		||||
                                 _client.Guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == guildStr);
 | 
			
		||||
 | 
			
		||||
                    if (server == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (server.OwnerId != _client.CurrentUser.Id)
 | 
			
		||||
                    {
 | 
			
		||||
                        await server.LeaveAsync().ConfigureAwait(false);
 | 
			
		||||
                        Log.Information($"Left server {server.Name} [{server.Id}]");
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        await server.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
                        Log.Information($"Deleted server {server.Name} [{server.Id}]");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
            }, CommandFlags.FireAndForget);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task OnReadyAsync()
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
            _autoCommands = uow._context
 | 
			
		||||
                .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._context.AutoCommands.AsNoTracking().Where(x => x.Interval == 0);
 | 
			
		||||
            foreach (var cmd in startupCommands)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await ExecuteCommand(cmd).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (_client.ShardId == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await LoadOwnerChannels().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Timer TimerFromAutoCommand(AutoCommand x)
 | 
			
		||||
        {
 | 
			
		||||
            return new Timer(async (obj) => await ExecuteCommand((AutoCommand) obj).ConfigureAwait(false),
 | 
			
		||||
                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).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error in SelfService ExecuteCommand");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void AddNewAutoCommand(AutoCommand cmd)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                uow._context.AutoCommands.Add(cmd);
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (cmd.Interval >= 5)
 | 
			
		||||
            {
 | 
			
		||||
                var autos = _autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary<int, Timer>());
 | 
			
		||||
                autos.AddOrUpdate(cmd.Id, key => TimerFromAutoCommand(cmd), (key, old) =>
 | 
			
		||||
                {
 | 
			
		||||
                    old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                    return TimerFromAutoCommand(cmd);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<AutoCommand> GetStartupCommands()
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            return uow._context
 | 
			
		||||
                .AutoCommands
 | 
			
		||||
                .AsNoTracking()
 | 
			
		||||
                .Where(x => x.Interval == 0)
 | 
			
		||||
                .OrderBy(x => x.Id)
 | 
			
		||||
                .ToList();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public IEnumerable<AutoCommand> GetAutoCommands()
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            return uow._context
 | 
			
		||||
                .AutoCommands
 | 
			
		||||
                .AsNoTracking()
 | 
			
		||||
                .Where(x => x.Interval >= 5)
 | 
			
		||||
                .OrderBy(x => x.Id)
 | 
			
		||||
                .ToList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task LoadOwnerChannels()
 | 
			
		||||
        {
 | 
			
		||||
            var channels = await Task.WhenAll(_creds.OwnerIds.Select(id =>
 | 
			
		||||
            {
 | 
			
		||||
                var user = _client.GetUser(id);
 | 
			
		||||
                if (user == null)
 | 
			
		||||
                    return Task.FromResult<IDMChannel>(null);
 | 
			
		||||
 | 
			
		||||
                return user.GetOrCreateDMChannelAsync();
 | 
			
		||||
            })).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            ownerChannels = channels.Where(x => x != 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 credentials.json file and invited the bot to a Discord server.");
 | 
			
		||||
            else
 | 
			
		||||
                Log.Information($"Created {ownerChannels.Count} out of {_creds.OwnerIds.Length} owner message channels.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task LeaveGuild(string guildStr)
 | 
			
		||||
        {
 | 
			
		||||
            var sub = _cache.Redis.GetSubscriber();
 | 
			
		||||
            return sub.PublishAsync(_creds.RedisKey() + "_leave_guild", guildStr);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // forwards dms
 | 
			
		||||
        public async Task LateExecute(DiscordSocketClient client, IGuild guild, IUserMessage msg)
 | 
			
		||||
        {
 | 
			
		||||
            var bs = _bss.Data;
 | 
			
		||||
            if (msg.Channel is IDMChannel && _bss.Data.ForwardMessages && ownerChannels.Any())
 | 
			
		||||
            {
 | 
			
		||||
                var title = _strings.GetText("dm_from") +
 | 
			
		||||
                            $" [{msg.Author}]({msg.Author.Id})";
 | 
			
		||||
 | 
			
		||||
                var attachamentsTxt = _strings.GetText("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(title, toSend).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                        catch
 | 
			
		||||
                        {
 | 
			
		||||
                            Log.Warning("Can't contact owner with id {0}", ownerCh.Recipient.Id);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var firstOwnerChannel = ownerChannels.Values.First();
 | 
			
		||||
                    if (firstOwnerChannel.Recipient.Id != msg.Author.Id)
 | 
			
		||||
                    {
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            await firstOwnerChannel.SendConfirmAsync(title, toSend).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                        catch
 | 
			
		||||
                        {
 | 
			
		||||
                            // ignored
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool RestartBot()
 | 
			
		||||
        {
 | 
			
		||||
            var cmd = _creds.RestartCommand;
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(cmd?.Cmd))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Restart();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool RemoveStartupCommand(int index, out AutoCommand cmd)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                cmd = uow._context.AutoCommands
 | 
			
		||||
                    .AsNoTracking()
 | 
			
		||||
                    .Where(x => x.Interval == 0)
 | 
			
		||||
                    .Skip(index)
 | 
			
		||||
                    .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
                if (cmd != null)
 | 
			
		||||
                {
 | 
			
		||||
                    uow._context.Remove(cmd);
 | 
			
		||||
                    uow.SaveChanges();
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public bool RemoveAutoCommand(int index, out AutoCommand cmd)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                cmd = uow._context.AutoCommands
 | 
			
		||||
                    .AsNoTracking()
 | 
			
		||||
                    .Where(x => x.Interval >= 5)
 | 
			
		||||
                    .Skip(index)
 | 
			
		||||
                    .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
                if (cmd != null)
 | 
			
		||||
                {
 | 
			
		||||
                    uow._context.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).ConfigureAwait(false))
 | 
			
		||||
            {
 | 
			
		||||
                if (!sr.IsImage())
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                // i can't just do ReadAsStreamAsync because dicord.net's image poops itself
 | 
			
		||||
                var imgData = await sr.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
 | 
			
		||||
                using (var imgStream = imgData.ToStream())
 | 
			
		||||
                {
 | 
			
		||||
                    await _client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void ClearStartupCommands()
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var toRemove = uow._context
 | 
			
		||||
                    .AutoCommands
 | 
			
		||||
                    .AsNoTracking()
 | 
			
		||||
                    .Where(x => x.Interval == 0);
 | 
			
		||||
 | 
			
		||||
                uow._context.AutoCommands.RemoveRange(toRemove);
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void ReloadImages()
 | 
			
		||||
        {
 | 
			
		||||
            var sub = _cache.Redis.GetSubscriber();
 | 
			
		||||
            sub.Publish(_creds.RedisKey() + "_reload_images", "");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Die()
 | 
			
		||||
        {
 | 
			
		||||
            var sub = _cache.Redis.GetSubscriber();
 | 
			
		||||
            sub.Publish(_creds.RedisKey() + "_die", "", CommandFlags.FireAndForget);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Restart()
 | 
			
		||||
        {
 | 
			
		||||
            Process.Start(_creds.RestartCommand.Cmd, _creds.RestartCommand.Args);
 | 
			
		||||
            var sub = _cache.Redis.GetSubscriber();
 | 
			
		||||
            sub.Publish(_creds.RedisKey() + "_die", "", CommandFlags.FireAndForget);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool RestartShard(int shardId)
 | 
			
		||||
        {
 | 
			
		||||
            if (shardId < 0 || shardId >= _creds.TotalShards)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            var pub = _cache.Redis.GetSubscriber();
 | 
			
		||||
            pub.Publish(_creds.RedisKey() + "_shardcoord_stop",
 | 
			
		||||
                JsonConvert.SerializeObject(shardId),
 | 
			
		||||
                CommandFlags.FireAndForget);
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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 IEnumerable<ShardComMessage> GetAllShardStatuses()
 | 
			
		||||
        {
 | 
			
		||||
            var db = _cache.Redis.GetDatabase();
 | 
			
		||||
            return db.ListRange(_creds.RedisKey() + "_shardstats")
 | 
			
		||||
                .Select(x => JsonConvert.DeserializeObject<ShardComMessage>(x));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,488 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Replacements;
 | 
			
		||||
using NadekoBot.Core.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class UserPunishService : INService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly MuteService _mute;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly BlacklistService _blacklistService;
 | 
			
		||||
        private readonly Timer _warnExpiryTimer;
 | 
			
		||||
 | 
			
		||||
        public UserPunishService(MuteService mute, DbService db, BlacklistService blacklistService)
 | 
			
		||||
        {
 | 
			
		||||
            _mute = mute;
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _blacklistService = blacklistService;
 | 
			
		||||
            
 | 
			
		||||
            _warnExpiryTimer = new Timer(async _ =>
 | 
			
		||||
            {
 | 
			
		||||
                await CheckAllWarnExpiresAsync();
 | 
			
		||||
            }, null, TimeSpan.FromSeconds(0), TimeSpan.FromHours(12));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<WarningPunishment> Warn(IGuild guild, ulong userId, IUser mod, string reason)
 | 
			
		||||
        {
 | 
			
		||||
            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,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            int warnings = 1;
 | 
			
		||||
            List<WarningPunishment> ps;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                ps = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.WarnPunishments))
 | 
			
		||||
                    .WarnPunishments;
 | 
			
		||||
 | 
			
		||||
                warnings += uow.Warnings
 | 
			
		||||
                    .ForId(guildId, userId)
 | 
			
		||||
                    .Where(w => !w.Forgiven && w.UserId == userId)
 | 
			
		||||
                    .Count();
 | 
			
		||||
 | 
			
		||||
                uow.Warnings.Add(warn);
 | 
			
		||||
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var p = ps.FirstOrDefault(x => x.Count == warnings);
 | 
			
		||||
 | 
			
		||||
            if (p != null)
 | 
			
		||||
            {
 | 
			
		||||
                var user = await guild.GetUserAsync(userId).ConfigureAwait(false);
 | 
			
		||||
                if (user == 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)
 | 
			
		||||
        {
 | 
			
		||||
            switch (p)
 | 
			
		||||
            {
 | 
			
		||||
                case PunishmentAction.Mute:
 | 
			
		||||
                    if (minutes == 0)
 | 
			
		||||
                        await _mute.MuteUser(user, mod, reason: reason).ConfigureAwait(false);
 | 
			
		||||
                    else
 | 
			
		||||
                        await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), reason: reason)
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
                    break;
 | 
			
		||||
                case PunishmentAction.VoiceMute:
 | 
			
		||||
                    if (minutes == 0)
 | 
			
		||||
                        await _mute.MuteUser(user, mod, MuteType.Voice, reason).ConfigureAwait(false);
 | 
			
		||||
                    else
 | 
			
		||||
                        await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Voice, reason)
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
                    break;
 | 
			
		||||
                case PunishmentAction.ChatMute:
 | 
			
		||||
                    if (minutes == 0)
 | 
			
		||||
                        await _mute.MuteUser(user, mod, MuteType.Chat, reason).ConfigureAwait(false);
 | 
			
		||||
                    else
 | 
			
		||||
                        await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Chat, reason)
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
                    break;
 | 
			
		||||
                case PunishmentAction.Kick:
 | 
			
		||||
                    await user.KickAsync(reason).ConfigureAwait(false);
 | 
			
		||||
                    break;
 | 
			
		||||
                case PunishmentAction.Ban:
 | 
			
		||||
                    if (minutes == 0)
 | 
			
		||||
                        await guild.AddBanAsync(user, reason: reason).ConfigureAwait(false);
 | 
			
		||||
                    else
 | 
			
		||||
                        await _mute.TimedBan(user.Guild, user, TimeSpan.FromMinutes(minutes), reason)
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
                    break;
 | 
			
		||||
                case PunishmentAction.Softban:
 | 
			
		||||
                    await guild.AddBanAsync(user, 7, reason: $"Softban | {reason}").ConfigureAwait(false);
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await guild.RemoveBanAsync(user).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch
 | 
			
		||||
                    {
 | 
			
		||||
                        await guild.RemoveBanAsync(user).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
                case PunishmentAction.RemoveRoles:
 | 
			
		||||
                    await user.RemoveRolesAsync(user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                    break;
 | 
			
		||||
                case PunishmentAction.AddRole:
 | 
			
		||||
                    if (roleId is null)
 | 
			
		||||
                        return;
 | 
			
		||||
                    var role = guild.GetRole(roleId.Value);
 | 
			
		||||
                    if (!(role is null))
 | 
			
		||||
                    {
 | 
			
		||||
                        if (minutes == 0)
 | 
			
		||||
                            await user.AddRoleAsync(role).ConfigureAwait(false);
 | 
			
		||||
                        else
 | 
			
		||||
                            await _mute.TimedRole(user, TimeSpan.FromMinutes(minutes), reason, role)
 | 
			
		||||
                                .ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning($"Can't find role {roleId.Value} on server {guild.Id} to apply punishment.");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task CheckAllWarnExpiresAsync()
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var cleared = await uow._context.Database.ExecuteSqlRawAsync($@"UPDATE Warnings
 | 
			
		||||
SET Forgiven = 1,
 | 
			
		||||
    ForgivenBy = 'Expiry'
 | 
			
		||||
WHERE GuildId in (SELECT GuildId FROM GuildConfigs WHERE WarnExpireHours > 0 AND WarnExpireAction = 0)
 | 
			
		||||
	AND Forgiven = 0
 | 
			
		||||
	AND DateAdded < datetime('now', (SELECT '-' || WarnExpireHours || ' hours' FROM GuildConfigs as gc WHERE gc.GuildId = Warnings.GuildId));");
 | 
			
		||||
 | 
			
		||||
                var deleted = await uow._context.Database.ExecuteSqlRawAsync($@"DELETE FROM Warnings
 | 
			
		||||
WHERE GuildId in (SELECT GuildId FROM GuildConfigs WHERE WarnExpireHours > 0 AND WarnExpireAction = 1)
 | 
			
		||||
	AND DateAdded < datetime('now', (SELECT '-' || WarnExpireHours || ' hours' FROM GuildConfigs as gc WHERE gc.GuildId = Warnings.GuildId));");
 | 
			
		||||
 | 
			
		||||
                if(cleared > 0 || deleted > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Information($"Cleared {cleared} warnings and deleted {deleted} warnings due to expiry.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task CheckWarnExpiresAsync(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigs.ForId(guildId, inc => inc);
 | 
			
		||||
 | 
			
		||||
                if (config.WarnExpireHours == 0)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var hours = $"{-config.WarnExpireHours} hours";
 | 
			
		||||
                if (config.WarnExpireAction == WarnExpireAction.Clear)
 | 
			
		||||
                {
 | 
			
		||||
                    await uow._context.Database.ExecuteSqlInterpolatedAsync($@"UPDATE warnings
 | 
			
		||||
SET Forgiven = 1,
 | 
			
		||||
    ForgivenBy = 'Expiry'
 | 
			
		||||
WHERE GuildId={guildId}
 | 
			
		||||
    AND Forgiven = 0
 | 
			
		||||
    AND DateAdded < datetime('now', {hours})");
 | 
			
		||||
                }
 | 
			
		||||
                else if (config.WarnExpireAction == WarnExpireAction.Delete)
 | 
			
		||||
                {
 | 
			
		||||
                    await uow._context.Database.ExecuteSqlInterpolatedAsync($@"DELETE FROM warnings
 | 
			
		||||
WHERE GuildId={guildId}
 | 
			
		||||
    AND DateAdded < datetime('now', {hours})");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<int> GetWarnExpire(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var config = uow.GuildConfigs.ForId(guildId);
 | 
			
		||||
            return Task.FromResult(config.WarnExpireHours / 24);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public async Task WarnExpireAsync(ulong guildId, int days, bool delete)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigs.ForId(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)
 | 
			
		||||
        {
 | 
			
		||||
            bool toReturn = true;
 | 
			
		||||
            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);
 | 
			
		||||
                }
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
            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 == PunishmentAction.Softban || punish == PunishmentAction.Kick || punish == PunishmentAction.RemoveRoles) && time != null)
 | 
			
		||||
                return false;
 | 
			
		||||
            if (number <= 0 || (time != null && time.Time > TimeSpan.FromDays(49)))
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var ps = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
 | 
			
		||||
                var toDelete = ps.Where(x => x.Count == number);
 | 
			
		||||
 | 
			
		||||
                uow._context.RemoveRange(toDelete);
 | 
			
		||||
 | 
			
		||||
                ps.Add(new WarningPunishment()
 | 
			
		||||
                {
 | 
			
		||||
                    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.GuildConfigs.ForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
 | 
			
		||||
                var p = ps.FirstOrDefault(x => x.Count == number);
 | 
			
		||||
 | 
			
		||||
                if (p != null)
 | 
			
		||||
                {
 | 
			
		||||
                    uow._context.Remove(p);
 | 
			
		||||
                    uow.SaveChanges();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public WarningPunishment[] WarnPunishList(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                return uow.GuildConfigs.ForId(guildId, gc => gc.Include(x => x.WarnPunishments))
 | 
			
		||||
                    .WarnPunishments
 | 
			
		||||
                    .OrderBy(x => x.Count)
 | 
			
		||||
                    .ToArray();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public (IEnumerable<(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],
 | 
			
		||||
                        Id: 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._context.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._context.BanTemplates
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .FirstOrDefault(x => x.GuildId == guildId);
 | 
			
		||||
 | 
			
		||||
                if (text == null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (template is null)
 | 
			
		||||
                        return;
 | 
			
		||||
                    
 | 
			
		||||
                    uow._context.Remove(template);
 | 
			
		||||
                }
 | 
			
		||||
                else if (template == null)
 | 
			
		||||
                {
 | 
			
		||||
                    uow._context.BanTemplates.Add(new BanTemplate()
 | 
			
		||||
                    {
 | 
			
		||||
                        GuildId = guildId,
 | 
			
		||||
                        Text = text,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    template.Text = text;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public CREmbed GetBanUserDmEmbed(ICommandContext context, IGuildUser target, string defaultMessage,
 | 
			
		||||
            string banReason, TimeSpan? duration)
 | 
			
		||||
        {
 | 
			
		||||
            return GetBanUserDmEmbed(
 | 
			
		||||
                (DiscordSocketClient) context.Client,
 | 
			
		||||
                (SocketGuild) context.Guild,
 | 
			
		||||
                (IGuildUser) context.User,
 | 
			
		||||
                target,
 | 
			
		||||
                defaultMessage,
 | 
			
		||||
                banReason,
 | 
			
		||||
                duration);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public CREmbed 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();
 | 
			
		||||
 | 
			
		||||
            CREmbed crEmbed = null; 
 | 
			
		||||
            // if template isn't set, use the old message style
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(template))
 | 
			
		||||
            {
 | 
			
		||||
                template = JsonConvert.SerializeObject(new
 | 
			
		||||
                {
 | 
			
		||||
                    color = NadekoBot.ErrorColor.RawValue,
 | 
			
		||||
                    description = defaultMessage 
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
                CREmbed.TryParse(template, out crEmbed);
 | 
			
		||||
            }
 | 
			
		||||
            // 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
 | 
			
		||||
            else if (CREmbed.TryParse(template, out crEmbed))
 | 
			
		||||
            {
 | 
			
		||||
                replacer.Replace(crEmbed);
 | 
			
		||||
            }
 | 
			
		||||
            // otherwise, treat template as a regular string with replacements
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                template = JsonConvert.SerializeObject(new
 | 
			
		||||
                {
 | 
			
		||||
                    color = NadekoBot.ErrorColor.RawValue,
 | 
			
		||||
                    description = replacer.Replace(template) 
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                CREmbed.TryParse(template, out crEmbed);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            return crEmbed;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										230
									
								
								src/NadekoBot/Modules/Administration/Services/VcRoleService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								src/NadekoBot/Modules/Administration/Services/VcRoleService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,230 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
{
 | 
			
		||||
    public class VcRoleService : INService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
        public ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IRole>> VcRoles { get; }
 | 
			
		||||
        public ConcurrentDictionary<ulong, ConcurrentQueue<(bool, IGuildUser, IRole)>> ToAssign { get; }
 | 
			
		||||
 | 
			
		||||
        public VcRoleService(DiscordSocketClient client, NadekoBot bot, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _client = client;
 | 
			
		||||
 | 
			
		||||
            _client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated;
 | 
			
		||||
            VcRoles = new ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IRole>>();
 | 
			
		||||
            ToAssign = new ConcurrentDictionary<ulong, ConcurrentQueue<(bool, IGuildUser, IRole)>>();
 | 
			
		||||
            var missingRoles = new ConcurrentBag<VcRoleInfo>();
 | 
			
		||||
 | 
			
		||||
            using (var uow = db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var guildIds = client.Guilds.Select(x => x.Id).ToList();
 | 
			
		||||
                var configs = uow._context.Set<GuildConfig>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Include(x => x.VcRoleInfos)
 | 
			
		||||
                    .Where(x => guildIds.Contains(x.GuildId))
 | 
			
		||||
                    .ToList();
 | 
			
		||||
                
 | 
			
		||||
                Task.WhenAll(configs.Select(InitializeVcRole));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                while (true)
 | 
			
		||||
                {
 | 
			
		||||
                    var tasks = ToAssign.Values.Select(queue => Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        while (queue.TryDequeue(out var item))
 | 
			
		||||
                        {
 | 
			
		||||
                            var (add, user, role) = item;
 | 
			
		||||
                            if (add)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (!user.RoleIds.Contains(role.Id))
 | 
			
		||||
                                {
 | 
			
		||||
                                    try { await user.AddRoleAsync(role).ConfigureAwait(false); } catch { }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            else
 | 
			
		||||
                            {
 | 
			
		||||
                                if (user.RoleIds.Contains(role.Id))
 | 
			
		||||
                                {
 | 
			
		||||
                                    try { await user.RemoveRoleAsync(role).ConfigureAwait(false); } catch { }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            await Task.Delay(250).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }));
 | 
			
		||||
 | 
			
		||||
                    await Task.WhenAll(tasks.Append(Task.Delay(1000))).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            _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.GuildConfigs.ForId(
 | 
			
		||||
                    arg.GuildId,
 | 
			
		||||
                    set => set.Include(x => x.VcRoleInfos)
 | 
			
		||||
                );
 | 
			
		||||
                var _ = 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)
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Yield();
 | 
			
		||||
            var g = _client.GetGuild(gconf.GuildId);
 | 
			
		||||
            if (g == 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 == null)
 | 
			
		||||
                {
 | 
			
		||||
                    missingRoles.Add(ri);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                infos.TryAdd(ri.VoiceChannelId, role);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (missingRoles.Any())
 | 
			
		||||
            {
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning($"Removing {missingRoles.Count} missing roles from {nameof(VcRoleService)}");
 | 
			
		||||
                    uow._context.RemoveRange(missingRoles);
 | 
			
		||||
                    await uow.SaveChangesAsync();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void AddVcRole(ulong guildId, IRole role, ulong vcId)
 | 
			
		||||
        {
 | 
			
		||||
            if (role == null)
 | 
			
		||||
                throw new ArgumentNullException(nameof(role));
 | 
			
		||||
 | 
			
		||||
            var guildVcRoles = VcRoles.GetOrAdd(guildId, new ConcurrentDictionary<ulong, IRole>());
 | 
			
		||||
 | 
			
		||||
            guildVcRoles.AddOrUpdate(vcId, role, (key, old) => role);
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var conf = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.VcRoleInfos));
 | 
			
		||||
                var toDelete = conf.VcRoleInfos.FirstOrDefault(x => x.VoiceChannelId == vcId); // remove old one
 | 
			
		||||
                if(toDelete != null)
 | 
			
		||||
                {
 | 
			
		||||
                    uow._context.Remove(toDelete);
 | 
			
		||||
                }
 | 
			
		||||
                conf.VcRoleInfos.Add(new VcRoleInfo()
 | 
			
		||||
                {
 | 
			
		||||
                    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.GuildConfigs.ForId(guildId, set => set.Include(x => x.VcRoleInfos));
 | 
			
		||||
                var toRemove = conf.VcRoleInfos.Where(x => x.VoiceChannelId == vcId).ToList();
 | 
			
		||||
                uow._context.RemoveRange(toRemove);
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task ClientOnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState,
 | 
			
		||||
            SocketVoiceState newState)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            var gusr = usr as SocketGuildUser;
 | 
			
		||||
            if (gusr == null)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            var oldVc = oldState.VoiceChannel;
 | 
			
		||||
            var newVc = newState.VoiceChannel;
 | 
			
		||||
            var _ = Task.Run(() =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (oldVc != newVc)
 | 
			
		||||
                    {
 | 
			
		||||
                        ulong guildId;
 | 
			
		||||
                        guildId = newVc?.Guild.Id ?? oldVc.Guild.Id;
 | 
			
		||||
 | 
			
		||||
                        if (VcRoles.TryGetValue(guildId, out ConcurrentDictionary<ulong, IRole> guildVcRoles))
 | 
			
		||||
                        {
 | 
			
		||||
                            //remove old
 | 
			
		||||
                            if (oldVc != null && guildVcRoles.TryGetValue(oldVc.Id, out IRole role))
 | 
			
		||||
                            {
 | 
			
		||||
                                Assign(false, gusr, role);
 | 
			
		||||
                            }
 | 
			
		||||
                            //add new
 | 
			
		||||
                            if (newVc != 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 ConcurrentQueue<(bool, IGuildUser, IRole)>());
 | 
			
		||||
            queue.Enqueue((v, gusr, role));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								src/NadekoBot/Modules/Administration/TimeZoneCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/NadekoBot/Modules/Administration/TimeZoneCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class TimeZoneCommands : NadekoSubmodule<GuildTimezoneService>
 | 
			
		||||
        {
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Timezones(int page = 1)
 | 
			
		||||
            {
 | 
			
		||||
                page--;
 | 
			
		||||
 | 
			
		||||
                if (page < 0 || page > 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)}";
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            return $"{Format.Code(offset)} {nameStr}";
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                
 | 
			
		||||
                
 | 
			
		||||
                
 | 
			
		||||
                await ctx.SendPaginatedConfirmAsync(page,
 | 
			
		||||
                    (curPage) => new EmbedBuilder()
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithTitle(GetText("timezones_available"))
 | 
			
		||||
                        .WithDescription(string.Join("\n", timezoneStrings
 | 
			
		||||
                            .Skip(curPage * timezonesPerPage)
 | 
			
		||||
                            .Take(timezonesPerPage))),
 | 
			
		||||
                    timezones.Length, timezonesPerPage).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Timezone()
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("timezone_guild", _service.GetTimeZoneOrUtc(ctx.Guild.Id)).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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 == null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("timezone_not_found").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                _service.SetTimeZone(ctx.Guild.Id, tz);
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.SendConfirmAsync(tz.ToString()).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										802
									
								
								src/NadekoBot/Modules/Administration/UserPunishCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										802
									
								
								src/NadekoBot/Modules/Administration/UserPunishCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,802 @@
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Common.TypeReaders.Models;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class UserPunishCommands : NadekoSubmodule<UserPunishService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly MuteService _mute;
 | 
			
		||||
            private readonly BlacklistService _blacklistService;
 | 
			
		||||
 | 
			
		||||
            public UserPunishCommands(MuteService mute, BlacklistService blacklistService)
 | 
			
		||||
            {
 | 
			
		||||
                _mute = mute;
 | 
			
		||||
                _blacklistService = blacklistService;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task<bool> CheckRoleHierarchy(IGuildUser target)
 | 
			
		||||
            {
 | 
			
		||||
                var curUser = ((SocketGuild) ctx.Guild).CurrentUser;
 | 
			
		||||
                var ownerId = Context.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 || (Context.User.Id != ownerId && targetMaxRole >= modMaxRole)) || target.Id == ownerId)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("hierarchy");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            public async Task Warn(IGuildUser user, [Leftover] string reason = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await CheckRoleHierarchy(user))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var dmFailed = false;
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).EmbedAsync(new EmbedBuilder().WithErrorColor()
 | 
			
		||||
                                     .WithDescription(GetText("warned_on", ctx.Guild.ToString()))
 | 
			
		||||
                                     .AddField(efb => efb.WithName(GetText("moderator")).WithValue(ctx.User.ToString()))
 | 
			
		||||
                                     .AddField(efb => efb.WithName(GetText("reason")).WithValue(reason ?? "-")))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    dmFailed = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                WarningPunishment punishment;
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, reason).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex.Message);
 | 
			
		||||
                    var errorEmbed = new EmbedBuilder()
 | 
			
		||||
                        .WithErrorColor()
 | 
			
		||||
                        .WithDescription(GetText("cant_apply_punishment"));
 | 
			
		||||
                    
 | 
			
		||||
                    if (dmFailed)
 | 
			
		||||
                    {
 | 
			
		||||
                        errorEmbed.WithFooter("⚠️ " + GetText("unable_to_dm_user"));
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    await ctx.Channel.EmbedAsync(errorEmbed);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
                if (punishment == null)
 | 
			
		||||
                {
 | 
			
		||||
                    embed.WithDescription(GetText("user_warned",
 | 
			
		||||
                        Format.Bold(user.ToString())));
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    embed.WithDescription(GetText("user_warned_and_punished", Format.Bold(user.ToString()),
 | 
			
		||||
                        Format.Bold(punishment.Punishment.ToString())));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (dmFailed)
 | 
			
		||||
                {
 | 
			
		||||
                    embed.WithFooter("⚠️ " + GetText("unable_to_dm_user"));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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()
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [NadekoOptions(typeof(WarnExpireOptions))]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task WarnExpire()
 | 
			
		||||
            {
 | 
			
		||||
                var expireDays = await _service.GetWarnExpire(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
                if (expireDays == 0)
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("warns_dont_expire");
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("warns_expire_in", expireDays);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [NadekoOptions(typeof(WarnExpireOptions))]
 | 
			
		||||
            [Priority(2)]
 | 
			
		||||
            public async Task WarnExpire(int days, params string[] args)
 | 
			
		||||
            {
 | 
			
		||||
                if (days < 0 || days > 366)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var opts = OptionsParser.ParseFrom<WarnExpireOptions>(args);
 | 
			
		||||
 | 
			
		||||
                await Context.Channel.TriggerTypingAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await _service.WarnExpireAsync(ctx.Guild.Id, days, opts.Delete).ConfigureAwait(false);
 | 
			
		||||
                if(days == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("warn_expire_reset").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (opts.Delete)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("warn_expire_set_delete", Format.Bold(days.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("warn_expire_set_clear", Format.Bold(days.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public Task Warnlog(int page, ulong userId)
 | 
			
		||||
                => InternalWarnlog(userId, page - 1);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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 = new EmbedBuilder()
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithTitle(GetText("warnlog_for", user));
 | 
			
		||||
 | 
			
		||||
                    if (!warnings.Any())
 | 
			
		||||
                    {
 | 
			
		||||
                        embed.WithDescription(GetText("warnings_none"));
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        var i = page * 9;
 | 
			
		||||
                        foreach (var w in warnings)
 | 
			
		||||
                        {
 | 
			
		||||
                            i++;
 | 
			
		||||
                            var name = GetText("warned_on_by",
 | 
			
		||||
                                w.DateAdded.Value.ToString("dd.MM.yyy"),
 | 
			
		||||
                                w.DateAdded.Value.ToString("HH:mm"),
 | 
			
		||||
                                w.Moderator);
 | 
			
		||||
                            
 | 
			
		||||
                            if (w.Forgiven)
 | 
			
		||||
                                name = $"{Format.Strikethrough(name)} {GetText("warn_cleared_by", w.ForgivenBy)}";
 | 
			
		||||
 | 
			
		||||
                            embed.AddField($"#`{i}` " + name, w.Reason.TrimTo(1020));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return embed;
 | 
			
		||||
                }, allWarnings.Length, 9);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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 new EmbedBuilder().WithOkColor()
 | 
			
		||||
                        .WithTitle(GetText("warnings_list"))
 | 
			
		||||
                        .WithDescription(string.Join("\n", ws));
 | 
			
		||||
                }, warnings.Length, 15).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            public Task Warnclear(IGuildUser user, int index = 0)
 | 
			
		||||
                => Warnclear(user.Id, index);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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 ReplyConfirmLocalizedAsync("warnings_cleared", userStr).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (success)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyConfirmLocalizedAsync("warning_cleared", Format.Bold(index.ToString()), userStr)
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("warning_clear_fail").ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public enum AddRole
 | 
			
		||||
            {
 | 
			
		||||
                AddRole
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task WarnPunish(int number, AddRole _, IRole role, StoopidTime time = null)
 | 
			
		||||
            {
 | 
			
		||||
                var punish = PunishmentAction.AddRole;
 | 
			
		||||
                var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time, role);
 | 
			
		||||
 | 
			
		||||
                if (!success)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (time is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("warn_punish_set",
 | 
			
		||||
                        Format.Bold(punish.ToString()),
 | 
			
		||||
                        Format.Bold(number.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("warn_punish_set_timed",
 | 
			
		||||
                        Format.Bold(punish.ToString()),
 | 
			
		||||
                        Format.Bold(number.ToString()),
 | 
			
		||||
                        Format.Bold(time.Input)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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
 | 
			
		||||
                if (punish == PunishmentAction.AddRole)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time);
 | 
			
		||||
 | 
			
		||||
                if (!success)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (time is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("warn_punish_set",
 | 
			
		||||
                        Format.Bold(punish.ToString()),
 | 
			
		||||
                        Format.Bold(number.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("warn_punish_set_timed",
 | 
			
		||||
                        Format.Bold(punish.ToString()),
 | 
			
		||||
                        Format.Bold(number.ToString()),
 | 
			
		||||
                        Format.Bold(time.Input)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            public async Task WarnPunish(int number)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_service.WarnPunishRemove(ctx.Guild.Id, number))
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("warn_punish_rem",
 | 
			
		||||
                    Format.Bold(number.ToString())).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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.ToString() + "m")} "));
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    list = GetText("warnpl_none");
 | 
			
		||||
                }
 | 
			
		||||
                await ctx.Channel.SendConfirmAsync(
 | 
			
		||||
                    GetText("warn_punish_list"),
 | 
			
		||||
                    list).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (time.Time > TimeSpan.FromDays(49))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(Context.Guild.Id, user.Id);
 | 
			
		||||
 | 
			
		||||
                if (guildUser != null && !await CheckRoleHierarchy(guildUser))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var dmFailed = false;
 | 
			
		||||
 | 
			
		||||
                if (guildUser != null)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        var defaultMessage = GetText("bandm", Format.Bold(ctx.Guild.Name), msg);
 | 
			
		||||
                        var embed = _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time);
 | 
			
		||||
                        if (!(embed is null))
 | 
			
		||||
                        {
 | 
			
		||||
                            var userChannel = await guildUser.GetOrCreateDMChannelAsync();
 | 
			
		||||
                            await userChannel.EmbedAsync(embed);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch
 | 
			
		||||
                    {
 | 
			
		||||
                        dmFailed = true;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await _mute.TimedBan(Context.Guild, user, time.Time, ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
 | 
			
		||||
                var toSend = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle("⛔️ " + GetText("banned_user"))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("username")).WithValue(user.ToString()).WithIsInline(true))
 | 
			
		||||
                    .AddField(efb => efb.WithName("ID").WithValue(user.Id.ToString()).WithIsInline(true))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("duration")).WithValue($"{time.Time.Days}d {time.Time.Hours}h {time.Time.Minutes}m").WithIsInline(true));
 | 
			
		||||
 | 
			
		||||
                if (dmFailed)
 | 
			
		||||
                {
 | 
			
		||||
                    toSend.WithFooter("⚠️ " + GetText("unable_to_dm_user"));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(toSend)
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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(Context.Guild.Id, userId);
 | 
			
		||||
                if (user is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Guild.AddBanAsync(userId, 7, ctx.User.ToString() + " | " + msg);
 | 
			
		||||
                    
 | 
			
		||||
                    await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                            .WithTitle("⛔️ " + GetText("banned_user"))
 | 
			
		||||
                            .AddField(efb => efb.WithName("ID").WithValue(userId.ToString()).WithIsInline(true)))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await Ban(user, msg);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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("bandm", Format.Bold(ctx.Guild.Name), msg);
 | 
			
		||||
                    var embed = _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null);
 | 
			
		||||
                    if (!(embed is null))
 | 
			
		||||
                    {
 | 
			
		||||
                        var userChannel = await user.GetOrCreateDMChannelAsync();
 | 
			
		||||
                        await userChannel.EmbedAsync(embed);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    dmFailed = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Guild.AddBanAsync(user, 7, ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                var toSend = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle("⛔️ " + GetText("banned_user"))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("username")).WithValue(user.ToString()).WithIsInline(true))
 | 
			
		||||
                    .AddField(efb => efb.WithName("ID").WithValue(user.Id.ToString()).WithIsInline(true));
 | 
			
		||||
 | 
			
		||||
                if (dmFailed)
 | 
			
		||||
                {
 | 
			
		||||
                    toSend.WithFooter("⚠️ " + GetText("unable_to_dm_user"));
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                await ctx.Channel.EmbedAsync(toSend)
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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(Context.Guild.Id);
 | 
			
		||||
                    if (template is null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyConfirmLocalizedAsync("banmsg_default");
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await Context.Channel.SendConfirmAsync(template);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                _service.SetBanTemplate(Context.Guild.Id, message);
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            public async Task BanMsgReset()
 | 
			
		||||
            {
 | 
			
		||||
                _service.SetBanTemplate(Context.Guild.Id, null);
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public Task BanMessageTest([Leftover] string reason = null)
 | 
			
		||||
                => InternalBanMessageTest(reason, null);
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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 dmChannel = await ctx.User.GetOrCreateDMChannelAsync();
 | 
			
		||||
                var defaultMessage = GetText("bandm", Format.Bold(ctx.Guild.Name), reason);
 | 
			
		||||
                var crEmbed = _service.GetBanUserDmEmbed(Context,
 | 
			
		||||
                    (IGuildUser)Context.User,
 | 
			
		||||
                    defaultMessage,
 | 
			
		||||
                    reason,
 | 
			
		||||
                    duration);
 | 
			
		||||
 | 
			
		||||
                if (crEmbed is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ConfirmLocalizedAsync("bandm_disabled");
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await dmChannel.EmbedAsync(crEmbed);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("unable_to_dm_user");
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await Context.OkAsync();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            public async Task Unban([Leftover] string user)
 | 
			
		||||
            {
 | 
			
		||||
                var bans = await ctx.Guild.GetBansAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                var bun = bans.FirstOrDefault(x => x.User.ToString().ToLowerInvariant() == user.ToLowerInvariant());
 | 
			
		||||
 | 
			
		||||
                if (bun == null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("user_not_found").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await UnbanInternal(bun.User).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            public async Task Unban(ulong userId)
 | 
			
		||||
            {
 | 
			
		||||
                var bans = await ctx.Guild.GetBansAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                var bun = bans.FirstOrDefault(x => x.User.Id == userId);
 | 
			
		||||
 | 
			
		||||
                if (bun == null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("user_not_found").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await UnbanInternal(bun.User).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task UnbanInternal(IUser user)
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("unbanned_user", Format.Bold(user.ToString())).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)]
 | 
			
		||||
            [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
            public Task Softban(IGuildUser user, [Leftover] string msg = null)
 | 
			
		||||
                => SoftbanInternal(user, msg);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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(Context.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(GetText("sbdm", Format.Bold(ctx.Guild.Name), msg)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                    catch
 | 
			
		||||
                {
 | 
			
		||||
                    dmFailed = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Guild.AddBanAsync(user, 7, "Softban | " + ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
 | 
			
		||||
                try { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); }
 | 
			
		||||
                catch { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); }
 | 
			
		||||
 | 
			
		||||
                var toSend = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle("☣ " + GetText("sb_user"))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("username")).WithValue(user.ToString()).WithIsInline(true))
 | 
			
		||||
                    .AddField(efb => efb.WithName("ID").WithValue(user.Id.ToString()).WithIsInline(true));
 | 
			
		||||
                
 | 
			
		||||
                if (dmFailed)
 | 
			
		||||
                {
 | 
			
		||||
                    toSend.WithFooter("⚠️ " + GetText("unable_to_dm_user"));
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                await ctx.Channel.EmbedAsync(toSend)
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.KickMembers)]
 | 
			
		||||
            [BotPerm(GuildPerm.KickMembers)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public Task Kick(IGuildUser user, [Leftover] string msg = null)
 | 
			
		||||
                => KickInternal(user, msg);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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(Context.Guild.Id, userId);
 | 
			
		||||
                if (user is null)
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
                await KickInternal(user, msg);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public async Task KickInternal(IGuildUser user, string msg = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await CheckRoleHierarchy(user))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var dmFailed = false;
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await user.SendErrorAsync(GetText("kickdm", Format.Bold(ctx.Guild.Name), msg))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {                        
 | 
			
		||||
                    dmFailed = true;
 | 
			
		||||
                }
 | 
			
		||||
            
 | 
			
		||||
                await user.KickAsync(ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
 | 
			
		||||
                
 | 
			
		||||
                var toSend = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("kicked_user"))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("username")).WithValue(user.ToString()).WithIsInline(true))
 | 
			
		||||
                    .AddField(efb => efb.WithName("ID").WithValue(user.Id.ToString()).WithIsInline(true));
 | 
			
		||||
 | 
			
		||||
                if (dmFailed)
 | 
			
		||||
                {
 | 
			
		||||
                    toSend.WithFooter("⚠️ " + GetText("unable_to_dm_user"));
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                await ctx.Channel.EmbedAsync(toSend)
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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(new EmbedBuilder()
 | 
			
		||||
                    .WithDescription(GetText("mass_kill_in_progress", bans.Count()))
 | 
			
		||||
                    .AddField(GetText("invalid", missing), missStr)
 | 
			
		||||
                    .WithOkColor());
 | 
			
		||||
 | 
			
		||||
                //do the banning
 | 
			
		||||
                await Task.WhenAll(bans
 | 
			
		||||
                    .Where(x => x.Id.HasValue)
 | 
			
		||||
                    .Select(x => ctx.Guild.AddBanAsync(x.Id.Value, 7, x.Reason, new RequestOptions()
 | 
			
		||||
                    {
 | 
			
		||||
                        RetryMode = RetryMode.AlwaysRetry,
 | 
			
		||||
                    })))
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                //wait for the message and edit it
 | 
			
		||||
                var banningMessage = await banningMessageTask.ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await banningMessage.ModifyAsync(x => x.Embed = new EmbedBuilder()
 | 
			
		||||
                    .WithDescription(GetText("mass_kill_completed", bans.Count()))
 | 
			
		||||
                    .AddField(GetText("invalid", missing), missStr)
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .Build()).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										93
									
								
								src/NadekoBot/Modules/Administration/VcRoleCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/NadekoBot/Modules/Administration/VcRoleCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
{
 | 
			
		||||
    public partial class Administration
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class VcRoleCommands : NadekoSubmodule<VcRoleService>
 | 
			
		||||
        {
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task VcRoleRm(ulong vcId)
 | 
			
		||||
            {
 | 
			
		||||
                if (_service.RemoveVcRole(ctx.Guild.Id, vcId))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("vcrole_removed", Format.Bold(vcId.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("vcrole_not_found").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [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 == null || vc.GuildId != user.GuildId)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("must_be_in_voice").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (role == null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_service.RemoveVcRole(ctx.Guild.Id, vc.Id))
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyConfirmLocalizedAsync("vcrole_removed", Format.Bold(vc.Name)).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    _service.AddVcRole(ctx.Guild.Id, role, vc.Id);
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("vcrole_added", Format.Bold(vc.Name), Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task VcRoleList()
 | 
			
		||||
            {
 | 
			
		||||
                var guild = (SocketGuild)ctx.Guild;
 | 
			
		||||
                string text;
 | 
			
		||||
                if (_service.VcRoles.TryGetValue(ctx.Guild.Id, out ConcurrentDictionary<ulong, IRole> roles))
 | 
			
		||||
                {
 | 
			
		||||
                    if (!roles.Any())
 | 
			
		||||
                    {
 | 
			
		||||
                        text = GetText("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("no_vcroles");
 | 
			
		||||
                }
 | 
			
		||||
                await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                        .WithTitle(GetText("vc_role_list"))
 | 
			
		||||
                        .WithDescription(text))
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/NadekoBot/Modules/CustomReactions/Common/ExportedExpr.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/NadekoBot/Modules/CustomReactions/Common/ExportedExpr.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.CustomReactions
 | 
			
		||||
{
 | 
			
		||||
    public class ExportedExpr
 | 
			
		||||
    {
 | 
			
		||||
        public string Res { get; set; }
 | 
			
		||||
        public bool Ad { get; set; }
 | 
			
		||||
        public bool Dm { get; set; }
 | 
			
		||||
        public bool At { get; set; }
 | 
			
		||||
        public bool Ca { get; set; }
 | 
			
		||||
        public string[] React;
 | 
			
		||||
 | 
			
		||||
        public static ExportedExpr FromModel(CustomReaction cr)
 | 
			
		||||
            => new ExportedExpr()
 | 
			
		||||
            {
 | 
			
		||||
                Res = cr.Response,
 | 
			
		||||
                Ad = cr.AutoDeleteTrigger,
 | 
			
		||||
                At = cr.AllowTarget,
 | 
			
		||||
                Ca = cr.ContainsAnywhere,
 | 
			
		||||
                Dm = cr.DmResponse,
 | 
			
		||||
                React = string.IsNullOrWhiteSpace(cr.Reactions)
 | 
			
		||||
                    ? null
 | 
			
		||||
                    : cr.GetReactions(),
 | 
			
		||||
            };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										362
									
								
								src/NadekoBot/Modules/CustomReactions/CustomReactions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								src/NadekoBot/Modules/CustomReactions/CustomReactions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,362 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.CustomReactions.Services;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.CustomReactions
 | 
			
		||||
{
 | 
			
		||||
    public class CustomReactions : NadekoModule<CustomReactionsService>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IBotCredentials _creds;
 | 
			
		||||
        private readonly IHttpClientFactory _clientFactory;
 | 
			
		||||
 | 
			
		||||
        public CustomReactions(IBotCredentials creds, IHttpClientFactory clientFactory)
 | 
			
		||||
        {
 | 
			
		||||
            _creds = creds;
 | 
			
		||||
            _clientFactory = clientFactory;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool AdminInGuildOrOwnerInDm() => (ctx.Guild == null && _creds.IsOwner(ctx.User))
 | 
			
		||||
                                                  || (ctx.Guild != null && ((IGuildUser)ctx.User).GuildPermissions.Administrator);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task AddCustReact(string key, [Leftover] string message)
 | 
			
		||||
        {
 | 
			
		||||
            var channel = ctx.Channel as ITextChannel;
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (!AdminInGuildOrOwnerInDm())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var cr = await _service.AddAsync(ctx.Guild?.Id, key, message);
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                .WithTitle(GetText("new_cust_react"))
 | 
			
		||||
                .WithDescription($"#{(kwum)cr.Id}")
 | 
			
		||||
                .AddField(efb => efb.WithName(GetText("trigger")).WithValue(key))
 | 
			
		||||
                .AddField(efb => efb.WithName(GetText("response")).WithValue(message.Length > 1024 ? GetText("redacted_too_long") : message))
 | 
			
		||||
                ).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task EditCustReact(kwum id, [Leftover] string message)
 | 
			
		||||
        {
 | 
			
		||||
            var channel = ctx.Channel as ITextChannel;
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(message) || id < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if ((channel == null && !_creds.IsOwner(ctx.User)) || (channel != null && !((IGuildUser)ctx.User).GuildPermissions.Administrator))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var cr = await _service.EditAsync(ctx.Guild?.Id, (int)id, message).ConfigureAwait(false);
 | 
			
		||||
            if (cr != null)
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("edited_cust_react"))
 | 
			
		||||
                    .WithDescription($"#{id}")
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("trigger")).WithValue(cr.Trigger))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("response")).WithValue(message.Length > 1024 ? GetText("redacted_too_long") : message))
 | 
			
		||||
                    ).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("edit_fail").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task ListCustReact(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0 || page > 999)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var customReactions = _service.GetCustomReactionsFor(ctx.Guild?.Id);
 | 
			
		||||
 | 
			
		||||
            if (customReactions == null || !customReactions.Any())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_found").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(page, pageFunc: curPage =>
 | 
			
		||||
            {
 | 
			
		||||
                var desc = customReactions.OrderBy(cr => cr.Trigger)
 | 
			
		||||
                    .Skip(curPage * 20)
 | 
			
		||||
                    .Take(20)
 | 
			
		||||
                    .Select(cr => $"{(cr.ContainsAnywhere ? "🗯" : "◾")}" +
 | 
			
		||||
                                  $"{(cr.DmResponse ? "✉" : "◾")}" +
 | 
			
		||||
                                  $"{(cr.AutoDeleteTrigger ? "❌" : "◾")}" +
 | 
			
		||||
                                  $"`{(kwum) cr.Id}` {cr.Trigger}"
 | 
			
		||||
                                  + (string.IsNullOrWhiteSpace(cr.Reactions)
 | 
			
		||||
                                      ? string.Empty
 | 
			
		||||
                                      : " // " + string.Join(" ", cr.GetReactions())))
 | 
			
		||||
                    .JoinWith('\n');
 | 
			
		||||
 | 
			
		||||
                return new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("custom_reactions"))
 | 
			
		||||
                    .WithDescription(desc);
 | 
			
		||||
 | 
			
		||||
            }, customReactions.Length, 20);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum All
 | 
			
		||||
        {
 | 
			
		||||
            All
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task ListCustReact(All _)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyPendingLocalizedAsync("obsolete_use", Format.Code($"{Prefix}crsexport"));
 | 
			
		||||
            await CrsExport();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task ListCustReactG(int page = 1)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyPendingLocalizedAsync("obsolete_use", Format.Code($"{Prefix}crsexport"));
 | 
			
		||||
            await CrsExport();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task ShowCustReact(kwum id)
 | 
			
		||||
        {
 | 
			
		||||
            var found = _service.GetCustomReaction(ctx.Guild?.Id, (int)id);
 | 
			
		||||
 | 
			
		||||
            if (found == null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_found_id").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithDescription($"#{id}")
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("trigger")).WithValue(found.Trigger.TrimTo(1024)))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("response")).WithValue((found.Response + "\n```css\n" + found.Response).TrimTo(1020) + "```"))
 | 
			
		||||
                    ).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task DelCustReact(kwum id)
 | 
			
		||||
        {
 | 
			
		||||
            if (!AdminInGuildOrOwnerInDm())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var cr = await _service.DeleteAsync(ctx.Guild?.Id, (int)id);
 | 
			
		||||
 | 
			
		||||
            if (cr != null)
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("deleted"))
 | 
			
		||||
                    .WithDescription($"#{id}")
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("trigger")).WithValue(cr.Trigger.TrimTo(1024)))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("response")).WithValue(cr.Response.TrimTo(1024)))).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_found_id").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task CrReact(kwum id, params string[] emojiStrs)
 | 
			
		||||
        {
 | 
			
		||||
            if (!AdminInGuildOrOwnerInDm())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var cr = _service.GetCustomReaction(Context.Guild?.Id, id);
 | 
			
		||||
            if (cr is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_found").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (emojiStrs.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await _service.ResetCrReactions(ctx.Guild?.Id, id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("crr_reset", Format.Bold(id.ToString())).ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            List<string> succ = new List<string>();
 | 
			
		||||
            foreach (var emojiStr in emojiStrs)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                var emote = emojiStr.ToIEmote();
 | 
			
		||||
 | 
			
		||||
                // i should try adding these emojis right away to the message, to make sure the bot can react with these emojis. If it fails, skip that emoji
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await Context.Message.AddReactionAsync(emote).ConfigureAwait(false);
 | 
			
		||||
                    await Task.Delay(100).ConfigureAwait(false);
 | 
			
		||||
                    succ.Add(emojiStr);
 | 
			
		||||
 | 
			
		||||
                    if (succ.Count >= 3)
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if(succ.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("invalid_emojis").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _service.SetCrReactions(ctx.Guild?.Id, id, succ);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("crr_set", Format.Bold(id.ToString()), string.Join(", ", succ.Select(x => x.ToString()))).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public Task CrCa(kwum id)
 | 
			
		||||
            => InternalCrEdit(id, CustomReactionsService.CrField.ContainsAnywhere);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public Task CrDm(kwum id)
 | 
			
		||||
            => InternalCrEdit(id, CustomReactionsService.CrField.DmResponse);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public Task CrAd(kwum id)
 | 
			
		||||
            => InternalCrEdit(id, CustomReactionsService.CrField.AutoDelete);
 | 
			
		||||
        
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public Task CrAt(kwum id)
 | 
			
		||||
            => InternalCrEdit(id, CustomReactionsService.CrField.AllowTarget);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task CrsReload()
 | 
			
		||||
        {
 | 
			
		||||
            await _service.TriggerReloadCustomReactions();
 | 
			
		||||
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task InternalCrEdit(kwum id, CustomReactionsService.CrField option)
 | 
			
		||||
        {
 | 
			
		||||
            var cr = _service.GetCustomReaction(ctx.Guild?.Id, id);
 | 
			
		||||
            if (!AdminInGuildOrOwnerInDm())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            var (success, newVal) = await _service.ToggleCrOptionAsync(id, option).ConfigureAwait(false);
 | 
			
		||||
            if (!success)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("no_found_id").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (newVal)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("option_enabled", Format.Code(option.ToString()), Format.Code(id.ToString())).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("option_disabled", Format.Code(option.ToString()), Format.Code(id.ToString())).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async Task CrClear()
 | 
			
		||||
        {
 | 
			
		||||
            if (await PromptUserConfirmAsync(new EmbedBuilder()
 | 
			
		||||
                .WithTitle("Custom reaction clear")
 | 
			
		||||
                .WithDescription("This will delete all custom reactions on this server.")).ConfigureAwait(false))
 | 
			
		||||
            {
 | 
			
		||||
                var count = _service.DeleteAllCustomReactions(ctx.Guild.Id);
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("cleared", count).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task CrsExport()
 | 
			
		||||
        {
 | 
			
		||||
            if (!AdminInGuildOrOwnerInDm())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
            var serialized = _service.ExportCrs(ctx.Guild?.Id);
 | 
			
		||||
            using var stream = await serialized.ToStream();
 | 
			
		||||
            await ctx.Channel.SendFileAsync(stream, "crs-export.yml", text: null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
#if GLOBAL_NADEKO
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
#endif
 | 
			
		||||
        public async Task CrsImport([Leftover]string input = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (!AdminInGuildOrOwnerInDm())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("insuff_perms").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            input = input?.Trim();
 | 
			
		||||
 | 
			
		||||
            _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
            if (input is null)
 | 
			
		||||
            {
 | 
			
		||||
                var attachment = ctx.Message.Attachments.FirstOrDefault();
 | 
			
		||||
                if (attachment is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("expr_import_no_input");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                using var client = _clientFactory.CreateClient();
 | 
			
		||||
                input = await client.GetStringAsync(attachment.Url);
 | 
			
		||||
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(input))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("expr_import_no_input");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var succ = await _service.ImportCrsAsync(ctx.Guild?.Id, input);
 | 
			
		||||
            if (!succ)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("expr_import_invalid_data");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            await ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										167
									
								
								src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
			
		||||
using AngleSharp;
 | 
			
		||||
using AngleSharp.Html.Dom;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Replacements;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.CustomReactions.Extensions
 | 
			
		||||
{
 | 
			
		||||
    public static class Extensions
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly Regex imgRegex = new Regex("%(img|image):(?<tag>.*?)%", RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
        private static Dictionary<Regex, Func<Match, Task<string>>> regexPlaceholders { get; } = new Dictionary<Regex, Func<Match, Task<string>>>()
 | 
			
		||||
        {
 | 
			
		||||
            { imgRegex, async (match) => {
 | 
			
		||||
                var tag = match.Groups["tag"].ToString();
 | 
			
		||||
                if(string.IsNullOrWhiteSpace(tag))
 | 
			
		||||
                    return "";
 | 
			
		||||
 | 
			
		||||
                var fullQueryLink = $"http://imgur.com/search?q={ tag }";
 | 
			
		||||
                var config = Configuration.Default.WithDefaultLoader();
 | 
			
		||||
                using(var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink).ConfigureAwait(false))
 | 
			
		||||
                {
 | 
			
		||||
                    var elems = document.QuerySelectorAll("a.image-list-link").ToArray();
 | 
			
		||||
 | 
			
		||||
                    if (!elems.Any())
 | 
			
		||||
                        return "";
 | 
			
		||||
 | 
			
		||||
                    var img = (elems.ElementAtOrDefault(new NadekoRandom().Next(0, elems.Length))?.Children?.FirstOrDefault() as IHtmlImageElement);
 | 
			
		||||
 | 
			
		||||
                    if (img?.Source == null)
 | 
			
		||||
                        return "";
 | 
			
		||||
 | 
			
		||||
                    return " " + img.Source.Replace("b.", ".", StringComparison.InvariantCulture) + " ";
 | 
			
		||||
                }
 | 
			
		||||
            } }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        private static string ResolveTriggerString(this string str, IUserMessage ctx, DiscordSocketClient client)
 | 
			
		||||
            => str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal);
 | 
			
		||||
 | 
			
		||||
        private static async Task<string> ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger, bool containsAnywhere)
 | 
			
		||||
        {
 | 
			
		||||
            var substringIndex = resolvedTrigger.Length;
 | 
			
		||||
            if (containsAnywhere)
 | 
			
		||||
            {
 | 
			
		||||
                var pos = ctx.Content.AsSpan().GetWordPosition(resolvedTrigger);
 | 
			
		||||
                if (pos == WordPosition.Start)
 | 
			
		||||
                    substringIndex += 1;
 | 
			
		||||
                else if (pos == WordPosition.End)
 | 
			
		||||
                    substringIndex = ctx.Content.Length;
 | 
			
		||||
                else if (pos == WordPosition.Middle)
 | 
			
		||||
                    substringIndex += ctx.Content.IndexOf(resolvedTrigger, StringComparison.InvariantCulture);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
 | 
			
		||||
 | 
			
		||||
            var rep = new ReplacementBuilder()
 | 
			
		||||
                .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild as SocketGuild, client)
 | 
			
		||||
                .WithOverride("%target%", () =>
 | 
			
		||||
                    canMentionEveryone
 | 
			
		||||
                        ? ctx.Content.Substring(substringIndex).Trim()
 | 
			
		||||
                        : ctx.Content.Substring(substringIndex).Trim().SanitizeMentions(true))
 | 
			
		||||
                .Build();
 | 
			
		||||
 | 
			
		||||
            str = rep.Replace(str);
 | 
			
		||||
#if !GLOBAL_NADEKO
 | 
			
		||||
            foreach (var ph in regexPlaceholders)
 | 
			
		||||
            {
 | 
			
		||||
                str = await ph.Key.ReplaceAsync(str, ph.Value).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
#endif
 | 
			
		||||
            return str;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static Task<string> ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, bool containsAnywhere)
 | 
			
		||||
            => cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client), containsAnywhere);
 | 
			
		||||
 | 
			
		||||
        public static async Task<IUserMessage> Send(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, bool sanitize)
 | 
			
		||||
        {
 | 
			
		||||
            var channel = cr.DmResponse ? await ctx.Author.GetOrCreateDMChannelAsync().ConfigureAwait(false) : ctx.Channel;
 | 
			
		||||
 | 
			
		||||
            if (CREmbed.TryParse(cr.Response, out CREmbed crembed))
 | 
			
		||||
            {
 | 
			
		||||
                var trigger = cr.Trigger.ResolveTriggerString(ctx, client);
 | 
			
		||||
                var substringIndex = trigger.Length;
 | 
			
		||||
                if (cr.ContainsAnywhere)
 | 
			
		||||
                {
 | 
			
		||||
                    var pos = ctx.Content.AsSpan().GetWordPosition(trigger);
 | 
			
		||||
                    if (pos == WordPosition.Start)
 | 
			
		||||
                        substringIndex += 1;
 | 
			
		||||
                    else if (pos == WordPosition.End)
 | 
			
		||||
                        substringIndex = ctx.Content.Length;
 | 
			
		||||
                    else if (pos == WordPosition.Middle)
 | 
			
		||||
                        substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
 | 
			
		||||
                
 | 
			
		||||
                var rep = new ReplacementBuilder()
 | 
			
		||||
                    .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild as SocketGuild, client)
 | 
			
		||||
                    .WithOverride("%target%", () => canMentionEveryone
 | 
			
		||||
                        ? ctx.Content.Substring(substringIndex).Trim()
 | 
			
		||||
                        : ctx.Content.Substring(substringIndex).Trim().SanitizeMentions(true))
 | 
			
		||||
                    .Build();
 | 
			
		||||
 | 
			
		||||
                rep.Replace(crembed);
 | 
			
		||||
 | 
			
		||||
                return await channel.EmbedAsync(crembed, sanitize).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            return await channel.SendMessageAsync((await cr.ResponseWithContextAsync(ctx, client, cr.ContainsAnywhere).ConfigureAwait(false)).SanitizeMentions(sanitize)).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        public static WordPosition GetWordPosition(this ReadOnlySpan<char> str, in ReadOnlySpan<char> word)
 | 
			
		||||
        {
 | 
			
		||||
            var wordIndex = str.IndexOf(word, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
            if (wordIndex == -1)
 | 
			
		||||
                return WordPosition.None;
 | 
			
		||||
 | 
			
		||||
            if (wordIndex == 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (word.Length < str.Length && str.isValidWordDivider(word.Length))
 | 
			
		||||
                    return WordPosition.Start;
 | 
			
		||||
            }
 | 
			
		||||
            else if ((wordIndex + word.Length) == str.Length)
 | 
			
		||||
            {
 | 
			
		||||
                if (str.isValidWordDivider(wordIndex - 1))
 | 
			
		||||
                    return WordPosition.End;
 | 
			
		||||
            }
 | 
			
		||||
            else if (str.isValidWordDivider(wordIndex - 1) && str.isValidWordDivider(wordIndex + word.Length))
 | 
			
		||||
                return WordPosition.Middle;
 | 
			
		||||
 | 
			
		||||
            return WordPosition.None;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static bool isValidWordDivider(this in ReadOnlySpan<char> str, int index)
 | 
			
		||||
        {
 | 
			
		||||
            var ch = str[index];
 | 
			
		||||
            if (ch >= 'a' && ch <= 'z')
 | 
			
		||||
                return false;
 | 
			
		||||
            if (ch >= 'A' && ch <= 'Z')
 | 
			
		||||
                return false;
 | 
			
		||||
            if (ch >= '1' && ch <= '9')
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public enum WordPosition
 | 
			
		||||
    {
 | 
			
		||||
        None,
 | 
			
		||||
        Start,
 | 
			
		||||
        Middle,
 | 
			
		||||
        End,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,727 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.CustomReactions.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Common;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.Yml;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using Serilog;
 | 
			
		||||
using YamlDotNet.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.CustomReactions.Services
 | 
			
		||||
{
 | 
			
		||||
    public sealed class CustomReactionsService : IEarlyBehavior, INService, IReadyExecutor
 | 
			
		||||
    {
 | 
			
		||||
        public enum CrField
 | 
			
		||||
        {
 | 
			
		||||
            AutoDelete,
 | 
			
		||||
            DmResponse,
 | 
			
		||||
            AllowTarget,
 | 
			
		||||
            ContainsAnywhere,
 | 
			
		||||
            Message,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly object _gcrWriteLock = new object();
 | 
			
		||||
 | 
			
		||||
        private readonly TypedKey<CustomReaction> _gcrAddedKey = new TypedKey<CustomReaction>("gcr.added");
 | 
			
		||||
        private readonly TypedKey<int> _gcrDeletedkey = new TypedKey<int>("gcr.deleted");
 | 
			
		||||
        private readonly TypedKey<CustomReaction> _gcrEditedKey = new TypedKey<CustomReaction>("gcr.edited");
 | 
			
		||||
        private readonly TypedKey<bool> _crsReloadedKey = new TypedKey<bool>("crs.reloaded");
 | 
			
		||||
        private const string MentionPh = "%bot.mention%";
 | 
			
		||||
 | 
			
		||||
        // it is perfectly fine to have global customreactions as an array
 | 
			
		||||
        // 1. custom reactions are almost never added (compared to how many times they are being looped through)
 | 
			
		||||
        // 2. only need write locks for this as we'll rebuild+replace the array on every edit
 | 
			
		||||
        // 3. there's never many of them (at most a thousand, usually < 100)
 | 
			
		||||
        private CustomReaction[] _globalReactions;
 | 
			
		||||
        private ConcurrentDictionary<ulong, CustomReaction[]> _newGuildReactions;
 | 
			
		||||
 | 
			
		||||
        public int Priority => -1;
 | 
			
		||||
        public ModuleBehaviorType BehaviorType => ModuleBehaviorType.Executor;
 | 
			
		||||
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly PermissionService _perms;
 | 
			
		||||
        private readonly CommandHandler _cmd;
 | 
			
		||||
        private readonly IBotStrings _strings;
 | 
			
		||||
        private readonly NadekoBot _bot;
 | 
			
		||||
        private readonly GlobalPermissionService _gperm;
 | 
			
		||||
        private readonly CmdCdService _cmdCds;
 | 
			
		||||
        private readonly IPubSub _pubSub;
 | 
			
		||||
        private readonly Random _rng;
 | 
			
		||||
 | 
			
		||||
        public CustomReactionsService(PermissionService perms, DbService db, IBotStrings strings, NadekoBot bot,
 | 
			
		||||
            DiscordSocketClient client, CommandHandler cmd, GlobalPermissionService gperm, CmdCdService cmdCds,
 | 
			
		||||
            IPubSub pubSub)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _perms = perms;
 | 
			
		||||
            _cmd = cmd;
 | 
			
		||||
            _strings = strings;
 | 
			
		||||
            _bot = bot;
 | 
			
		||||
            _gperm = gperm;
 | 
			
		||||
            _cmdCds = cmdCds;
 | 
			
		||||
            _pubSub = pubSub;
 | 
			
		||||
            _rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
            _pubSub.Sub(_crsReloadedKey, OnCrsShouldReload);
 | 
			
		||||
            pubSub.Sub(_gcrAddedKey, OnGcrAdded);
 | 
			
		||||
            pubSub.Sub(_gcrDeletedkey, OnGcrDeleted);
 | 
			
		||||
            pubSub.Sub(_gcrEditedKey, OnGcrEdited);
 | 
			
		||||
 | 
			
		||||
            bot.JoinedGuild += OnJoinedGuild;
 | 
			
		||||
            _client.LeftGuild += OnLeftGuild;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task ReloadInternal(IReadOnlyList<ulong> allGuildIds)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var guildItems = await uow._context.CustomReactions
 | 
			
		||||
                .AsNoTracking()
 | 
			
		||||
                .Where(x => allGuildIds.Contains(x.GuildId.Value))
 | 
			
		||||
                .ToListAsync();
 | 
			
		||||
 | 
			
		||||
            _newGuildReactions = guildItems
 | 
			
		||||
                .GroupBy(k => k.GuildId!.Value)
 | 
			
		||||
                .ToDictionary(g => g.Key,
 | 
			
		||||
                    g => g.Select(x =>
 | 
			
		||||
                    {
 | 
			
		||||
                        x.Trigger = x.Trigger.Replace(MentionPh, _bot.Mention);
 | 
			
		||||
                        return x;
 | 
			
		||||
                    }).ToArray())
 | 
			
		||||
                .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
            lock (_gcrWriteLock)
 | 
			
		||||
            {
 | 
			
		||||
                var globalItems = uow._context
 | 
			
		||||
                    .CustomReactions
 | 
			
		||||
                    .AsNoTracking()
 | 
			
		||||
                    .Where(x => x.GuildId == null || x.GuildId == 0)
 | 
			
		||||
                    .AsEnumerable()
 | 
			
		||||
                    .Select(x =>
 | 
			
		||||
                    {
 | 
			
		||||
                        x.Trigger = x.Trigger.Replace(MentionPh, _bot.Mention);
 | 
			
		||||
                        return x;
 | 
			
		||||
                    })
 | 
			
		||||
                    .ToArray();
 | 
			
		||||
 | 
			
		||||
                _globalReactions = globalItems;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            ready = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #region Event Handlers
 | 
			
		||||
 | 
			
		||||
        public Task OnReadyAsync() 
 | 
			
		||||
            => ReloadInternal(_bot.GetCurrentGuildIds());
 | 
			
		||||
 | 
			
		||||
        private ValueTask OnCrsShouldReload(bool _)
 | 
			
		||||
            => new ValueTask(ReloadInternal(_bot.GetCurrentGuildIds()));
 | 
			
		||||
        
 | 
			
		||||
        private ValueTask OnGcrAdded(CustomReaction c)
 | 
			
		||||
        {
 | 
			
		||||
            lock (_gcrWriteLock)
 | 
			
		||||
            {
 | 
			
		||||
                var newGlobalReactions = new CustomReaction[_globalReactions.Length + 1];
 | 
			
		||||
                Array.Copy(_globalReactions, newGlobalReactions, _globalReactions.Length);
 | 
			
		||||
                newGlobalReactions[_globalReactions.Length] = c;
 | 
			
		||||
                _globalReactions = newGlobalReactions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private ValueTask OnGcrEdited(CustomReaction c)
 | 
			
		||||
        {
 | 
			
		||||
            lock (_gcrWriteLock)
 | 
			
		||||
            {
 | 
			
		||||
                for (var i = 0; i < _globalReactions.Length; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_globalReactions[i].Id == c.Id)
 | 
			
		||||
                    {
 | 
			
		||||
                        _globalReactions[i] = c;
 | 
			
		||||
                        return default;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // if edited cr is not found?!
 | 
			
		||||
                // add it
 | 
			
		||||
                OnGcrAdded(c);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private ValueTask OnGcrDeleted(int id)
 | 
			
		||||
        {
 | 
			
		||||
            lock (_gcrWriteLock)
 | 
			
		||||
            {
 | 
			
		||||
                var newGlobalReactions = DeleteInternal(_globalReactions, id, out _);
 | 
			
		||||
                _globalReactions = newGlobalReactions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task TriggerReloadCustomReactions()
 | 
			
		||||
            => _pubSub.Pub(_crsReloadedKey, true);
 | 
			
		||||
        
 | 
			
		||||
        #endregion
 | 
			
		||||
 | 
			
		||||
        #region Client Event Handlers
 | 
			
		||||
 | 
			
		||||
        private Task OnLeftGuild(SocketGuild arg)
 | 
			
		||||
        {
 | 
			
		||||
            _newGuildReactions.TryRemove(arg.Id, out _);
 | 
			
		||||
            
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task OnJoinedGuild(GuildConfig gc)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var crs = await uow._context
 | 
			
		||||
                .CustomReactions
 | 
			
		||||
                .AsNoTracking()
 | 
			
		||||
                .Where(x => x.GuildId == gc.GuildId)
 | 
			
		||||
                .ToArrayAsync();
 | 
			
		||||
 | 
			
		||||
            _newGuildReactions[gc.GuildId] = crs;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
        
 | 
			
		||||
        #region Basic Operations
 | 
			
		||||
 | 
			
		||||
        public async Task<CustomReaction> AddAsync(ulong? guildId, string key, string message)
 | 
			
		||||
        {
 | 
			
		||||
            key = key.ToLowerInvariant();
 | 
			
		||||
            var cr = new CustomReaction()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                Trigger = key,
 | 
			
		||||
                Response = message,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                cr.AllowTarget = true;
 | 
			
		||||
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                uow.CustomReactions.Add(cr);
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await AddInternalAsync(guildId, cr);
 | 
			
		||||
            
 | 
			
		||||
            return cr;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<CustomReaction> EditAsync(ulong? guildId, int id, string message)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var cr = uow.CustomReactions.GetById(id);
 | 
			
		||||
 | 
			
		||||
            if (cr == null || cr.GuildId != guildId)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            // disable allowtarget if message had target, but it was removed from it
 | 
			
		||||
            if (!message.Contains("%target%", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                && cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                cr.AllowTarget = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            cr.Response = message;
 | 
			
		||||
            
 | 
			
		||||
            // enable allow target if message is edited to contain target
 | 
			
		||||
            if (cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                cr.AllowTarget = true;
 | 
			
		||||
            
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            await UpdateInternalAsync(guildId, cr);
 | 
			
		||||
            
 | 
			
		||||
            return cr;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        public async Task<CustomReaction> DeleteAsync(ulong? guildId, int id)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var toDelete = uow.CustomReactions.GetById(id);
 | 
			
		||||
            
 | 
			
		||||
            if (toDelete is null)
 | 
			
		||||
                return null;
 | 
			
		||||
            
 | 
			
		||||
            if ((toDelete.IsGlobal() && guildId == null) || (guildId == toDelete.GuildId))
 | 
			
		||||
            {
 | 
			
		||||
                uow.CustomReactions.Remove(toDelete);
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
                await DeleteInternalAsync(guildId, id);
 | 
			
		||||
                return toDelete;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        public CustomReaction[] GetCustomReactionsFor(ulong? maybeGuildId)
 | 
			
		||||
        {
 | 
			
		||||
            if (maybeGuildId is ulong guildId)
 | 
			
		||||
            {
 | 
			
		||||
                return _newGuildReactions.TryGetValue(guildId, out var crs)
 | 
			
		||||
                    ? crs
 | 
			
		||||
                    : Array.Empty<CustomReaction>();
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            return _globalReactions;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 | 
			
		||||
        private bool ready;
 | 
			
		||||
 | 
			
		||||
        private CustomReaction TryGetCustomReaction(IUserMessage umsg)
 | 
			
		||||
        {
 | 
			
		||||
            if (!ready)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            if (!(umsg.Channel is SocketTextChannel channel))
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            var content = umsg.Content.Trim().ToLowerInvariant();
 | 
			
		||||
            
 | 
			
		||||
            if (_newGuildReactions.TryGetValue(channel.Guild.Id, out var reactions) && reactions.Length > 0)
 | 
			
		||||
            {
 | 
			
		||||
                var cr = MatchCustomReactions(content, reactions);
 | 
			
		||||
                if (!(cr is null))
 | 
			
		||||
                    return cr;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var localGrs = _globalReactions;
 | 
			
		||||
 | 
			
		||||
            return MatchCustomReactions(content, localGrs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
        private CustomReaction MatchCustomReactions(in ReadOnlySpan<char> content, CustomReaction[] crs)
 | 
			
		||||
        {
 | 
			
		||||
            var result = new List<CustomReaction>(1);
 | 
			
		||||
            for (var i = 0; i < crs.Length; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var cr = crs[i];
 | 
			
		||||
                var trigger = cr.Trigger;
 | 
			
		||||
                if (content.Length > trigger.Length)
 | 
			
		||||
                {
 | 
			
		||||
                    // if input is greater than the trigger, it can only work if:
 | 
			
		||||
                    // it has CA enabled
 | 
			
		||||
                    if (cr.ContainsAnywhere)
 | 
			
		||||
                    {
 | 
			
		||||
                        // if ca is enabled, we have to check if it is a word within the content
 | 
			
		||||
                        var wp = content.GetWordPosition(trigger);
 | 
			
		||||
 | 
			
		||||
                        // if it is, then that's valid
 | 
			
		||||
                        if (wp != WordPosition.None)
 | 
			
		||||
                        {
 | 
			
		||||
                            result.Add(cr);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // if it's not, then it cant' work under any circumstance,
 | 
			
		||||
                        // because content is greater than the trigger length
 | 
			
		||||
                        // so it can't be equal, and it's not contained as a word
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // if CA is disabled, and CR has AllowTarget, then the
 | 
			
		||||
                    // content has to start with the trigger followed by a space
 | 
			
		||||
                    if (cr.AllowTarget && content.StartsWith(trigger, StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                                       && content[trigger.Length] == ' ')
 | 
			
		||||
                    {
 | 
			
		||||
                        result.Add(cr);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (content.Length < cr.Trigger.Length)
 | 
			
		||||
                {
 | 
			
		||||
                    // if input length is less than trigger length, it means
 | 
			
		||||
                    // that the reaction can never be triggered
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // if input length is the same as trigger length
 | 
			
		||||
                    // reaction can only trigger if the strings are equal
 | 
			
		||||
                    if (content.SequenceEqual(cr.Trigger))
 | 
			
		||||
                    {
 | 
			
		||||
                        result.Add(cr);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (result.Count == 0)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            var cancelled = result.FirstOrDefault(x => x.Response == "-");
 | 
			
		||||
            if (!(cancelled is null))
 | 
			
		||||
                return cancelled;
 | 
			
		||||
            
 | 
			
		||||
            return result[_rng.Next(0, result.Count)];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> RunBehavior(DiscordSocketClient client, IGuild guild, IUserMessage msg)
 | 
			
		||||
        {
 | 
			
		||||
            // maybe this message is a custom reaction
 | 
			
		||||
            var cr = TryGetCustomReaction(msg);
 | 
			
		||||
 | 
			
		||||
            if (cr is null || cr.Response == "-")
 | 
			
		||||
                return false;
 | 
			
		||||
            
 | 
			
		||||
            if(await _cmdCds.TryBlock(guild, msg.Author, cr.Trigger))
 | 
			
		||||
                return false;
 | 
			
		||||
            
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (_gperm.BlockedModules.Contains("ActualCustomReactions"))
 | 
			
		||||
                {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (guild is SocketGuild sg)
 | 
			
		||||
                {
 | 
			
		||||
                    var pc = _perms.GetCacheFor(guild.Id);
 | 
			
		||||
                    if (!pc.Permissions.CheckPermissions(msg, cr.Trigger, "ActualCustomReactions",
 | 
			
		||||
                        out int index))
 | 
			
		||||
                    {
 | 
			
		||||
                        if (pc.Verbose)
 | 
			
		||||
                        {
 | 
			
		||||
                            var returnMsg = _strings.GetText("trigger", sg.Id,
 | 
			
		||||
                                index + 1,
 | 
			
		||||
                                Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg)));
 | 
			
		||||
                            try
 | 
			
		||||
                            {
 | 
			
		||||
                                await msg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false);
 | 
			
		||||
                            }
 | 
			
		||||
                            catch
 | 
			
		||||
                            {
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            Log.Information(returnMsg);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var sentMsg = await cr.Send(msg, _client, false).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                var reactions = cr.GetReactions();
 | 
			
		||||
                foreach (var reaction in reactions)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await sentMsg.AddReactionAsync(reaction.ToIEmote());
 | 
			
		||||
                    }
 | 
			
		||||
                    catch
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning("Unable to add reactions to message {Message} in server {GuildId}", sentMsg.Id,
 | 
			
		||||
                            cr.GuildId);
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await Task.Delay(1000);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (cr.AutoDeleteTrigger)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await msg.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch
 | 
			
		||||
                    {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task ResetCrReactions(ulong? maybeGuildId, int id)
 | 
			
		||||
        {
 | 
			
		||||
            CustomReaction cr;
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            cr = uow.CustomReactions.GetById(id);
 | 
			
		||||
            if (cr is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            cr.Reactions = string.Empty;
 | 
			
		||||
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task UpdateInternalAsync(ulong? maybeGuildId, CustomReaction cr)
 | 
			
		||||
        {
 | 
			
		||||
            if (maybeGuildId is ulong guildId)
 | 
			
		||||
                UpdateInternal(guildId, cr);
 | 
			
		||||
            else
 | 
			
		||||
                return _pubSub.Pub(_gcrEditedKey, cr);
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void UpdateInternal(ulong? maybeGuildId, CustomReaction cr)
 | 
			
		||||
        {
 | 
			
		||||
            if (maybeGuildId is ulong guildId)
 | 
			
		||||
            {
 | 
			
		||||
                _newGuildReactions.AddOrUpdate(guildId, new[] {cr},
 | 
			
		||||
                    (key, old) =>
 | 
			
		||||
                    {
 | 
			
		||||
                        var newArray = old.ToArray();
 | 
			
		||||
                        for (var i = 0; i < newArray.Length; i++)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (newArray[i].Id == cr.Id)
 | 
			
		||||
                                newArray[i] = cr;
 | 
			
		||||
                        }
 | 
			
		||||
                        return newArray;
 | 
			
		||||
                    });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                lock (_gcrWriteLock)
 | 
			
		||||
                {
 | 
			
		||||
                    var crs = _globalReactions;
 | 
			
		||||
                    for (var i = 0; i < crs.Length; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (crs[i].Id == cr.Id)
 | 
			
		||||
                            crs[i] = cr;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task AddInternalAsync(ulong? maybeGuildId, CustomReaction cr)
 | 
			
		||||
        {
 | 
			
		||||
            // only do this for perf purposes
 | 
			
		||||
            cr.Trigger = cr.Trigger.Replace(MentionPh, _client.CurrentUser.Mention);
 | 
			
		||||
 | 
			
		||||
            if (maybeGuildId is ulong guildId)
 | 
			
		||||
            {
 | 
			
		||||
                _newGuildReactions.AddOrUpdate(guildId,
 | 
			
		||||
                    new[] {cr},
 | 
			
		||||
                    (key, old) => old.With(cr));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return _pubSub.Pub(_gcrAddedKey, cr);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private Task DeleteInternalAsync(ulong? maybeGuildId, int id)
 | 
			
		||||
        {
 | 
			
		||||
            if (maybeGuildId is ulong guildId)
 | 
			
		||||
            {
 | 
			
		||||
                _newGuildReactions.AddOrUpdate(guildId,
 | 
			
		||||
                    Array.Empty<CustomReaction>(),
 | 
			
		||||
                    (key, old) => DeleteInternal(old, id, out _));
 | 
			
		||||
                
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            lock (_gcrWriteLock)
 | 
			
		||||
            {
 | 
			
		||||
                var cr = Array.Find(_globalReactions, item => item.Id == id);
 | 
			
		||||
                if (!(cr is null))
 | 
			
		||||
                {
 | 
			
		||||
                    return _pubSub.Pub(_gcrDeletedkey, cr.Id);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private CustomReaction[] DeleteInternal(IReadOnlyList<CustomReaction> crs, int id, out CustomReaction deleted)
 | 
			
		||||
        {
 | 
			
		||||
            deleted = null;
 | 
			
		||||
            if (crs is null || crs.Count == 0)
 | 
			
		||||
                return crs as CustomReaction[] ?? crs?.ToArray();
 | 
			
		||||
            
 | 
			
		||||
            var newCrs = new CustomReaction[crs.Count - 1];
 | 
			
		||||
            for (int i = 0, k = 0; i < crs.Count; i++, k++)
 | 
			
		||||
            {
 | 
			
		||||
                if (crs[i].Id == id)
 | 
			
		||||
                {
 | 
			
		||||
                    deleted = crs[i];
 | 
			
		||||
                    k--;
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                newCrs[k] = crs[i];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return newCrs;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task SetCrReactions(ulong? guildId, int id, IEnumerable<string> emojis)
 | 
			
		||||
        {
 | 
			
		||||
            CustomReaction cr;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                cr = uow.CustomReactions.GetById(id);
 | 
			
		||||
                if (cr is null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                cr.Reactions = string.Join("@@@", emojis);
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await UpdateInternalAsync(guildId, cr);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<(bool Sucess, bool NewValue)> ToggleCrOptionAsync(int id, CrField field)
 | 
			
		||||
        {
 | 
			
		||||
            var newVal = false;
 | 
			
		||||
            CustomReaction cr;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                cr = uow.CustomReactions.GetById(id);
 | 
			
		||||
                if (cr is null)
 | 
			
		||||
                    return (false, false);
 | 
			
		||||
                if (field == CrField.AutoDelete)
 | 
			
		||||
                    newVal = cr.AutoDeleteTrigger = !cr.AutoDeleteTrigger;
 | 
			
		||||
                else if (field == CrField.ContainsAnywhere)
 | 
			
		||||
                    newVal = cr.ContainsAnywhere = !cr.ContainsAnywhere;
 | 
			
		||||
                else if (field == CrField.DmResponse)
 | 
			
		||||
                    newVal = cr.DmResponse = !cr.DmResponse;
 | 
			
		||||
                else if (field == CrField.AllowTarget)
 | 
			
		||||
                    newVal = cr.AllowTarget = !cr.AllowTarget;
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await UpdateInternalAsync(cr.GuildId, cr);
 | 
			
		||||
 | 
			
		||||
            return (true, newVal);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public CustomReaction GetCustomReaction(ulong? guildId, int id)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var cr = uow.CustomReactions.GetById(id);
 | 
			
		||||
            if (cr == null || cr.GuildId != guildId)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            return cr;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public int DeleteAllCustomReactions(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var count = uow.CustomReactions.ClearFromGuild(guildId);
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
            
 | 
			
		||||
            _newGuildReactions.TryRemove(guildId, out _);
 | 
			
		||||
 | 
			
		||||
            return count;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool ReactionExists(ulong? guildId, string input)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var cr = uow.CustomReactions.GetByGuildIdAndInput(guildId, input);
 | 
			
		||||
            return cr != null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static readonly ISerializer _exportSerializer = new SerializerBuilder()
 | 
			
		||||
            .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
 | 
			
		||||
            .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
 | 
			
		||||
            .WithIndentedSequences()
 | 
			
		||||
            .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
 | 
			
		||||
            .DisableAliases()
 | 
			
		||||
            .Build();
 | 
			
		||||
 | 
			
		||||
        private const string _prependExport =
 | 
			
		||||
            @"# Keys are triggers, Each key has a LIST of custom reactions in the following format:
 | 
			
		||||
# - res: Response string
 | 
			
		||||
#   react: 
 | 
			
		||||
#     - <List
 | 
			
		||||
#     -  of
 | 
			
		||||
#     - reactions>
 | 
			
		||||
#   at: Whether custom reaction allows targets (see .h .crat) 
 | 
			
		||||
#   ca: Whether custom reaction expects trigger anywhere (see .h .crca) 
 | 
			
		||||
#   dm: Whether custom reaction DMs the response (see .h .crdm) 
 | 
			
		||||
#   ad: Whether custom reaction automatically deletes triggering message (see .h .crad) 
 | 
			
		||||
 | 
			
		||||
";
 | 
			
		||||
        public string ExportCrs(ulong? guildId)
 | 
			
		||||
        {
 | 
			
		||||
            var crs = GetCustomReactionsFor(guildId);
 | 
			
		||||
 | 
			
		||||
            var crsDict = crs
 | 
			
		||||
                .GroupBy(x => x.Trigger)
 | 
			
		||||
                .ToDictionary(x => x.Key, x => x.Select(ExportedExpr.FromModel));
 | 
			
		||||
            
 | 
			
		||||
            return _prependExport + _exportSerializer
 | 
			
		||||
                .Serialize(crsDict)
 | 
			
		||||
                .UnescapeUnicodeCodePoints();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> ImportCrsAsync(ulong? guildId, string input)
 | 
			
		||||
        {
 | 
			
		||||
            Dictionary<string, List<ExportedExpr>> data;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                data = Yaml.Deserializer.Deserialize<Dictionary<string, List<ExportedExpr>>>(input);
 | 
			
		||||
                if (data.Sum(x => x.Value.Count) == 0)
 | 
			
		||||
                    return false;
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            foreach (var entry in data)
 | 
			
		||||
            {
 | 
			
		||||
                var trigger = entry.Key;
 | 
			
		||||
                await uow._context.CustomReactions.AddRangeAsync(entry.Value
 | 
			
		||||
                    .Where(cr => !string.IsNullOrWhiteSpace(cr.Res))
 | 
			
		||||
                    .Select(cr => new CustomReaction()
 | 
			
		||||
                    {
 | 
			
		||||
                        GuildId = guildId,
 | 
			
		||||
                        Response = cr.Res,
 | 
			
		||||
                        Reactions = cr.React?.JoinWith("@@@"),
 | 
			
		||||
                        Trigger = trigger,
 | 
			
		||||
                        AllowTarget = cr.At,
 | 
			
		||||
                        ContainsAnywhere = cr.Ca,
 | 
			
		||||
                        DmResponse = cr.Dm,
 | 
			
		||||
                        AutoDeleteTrigger = cr.Ad,
 | 
			
		||||
                    }));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            await TriggerReloadCustomReactions();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										186
									
								
								src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,186 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common.AnimalRacing;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Modules.Games.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    // wth is this, needs full rewrite
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class AnimalRacingCommands : GamblingSubmodule<AnimalRaceService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly ICurrencyService _cs;
 | 
			
		||||
            private readonly DiscordSocketClient _client;
 | 
			
		||||
            private readonly GamesConfigService _gamesConf;
 | 
			
		||||
 | 
			
		||||
            public AnimalRacingCommands(ICurrencyService cs, DiscordSocketClient client,
 | 
			
		||||
                GamblingConfigService gamblingConf, GamesConfigService gamesConf) : base(gamblingConf) 
 | 
			
		||||
            {
 | 
			
		||||
                _cs = cs;
 | 
			
		||||
                _client = client;
 | 
			
		||||
                _gamesConf = gamesConf;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private IUserMessage raceMessage = null;
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [NadekoOptionsAttribute(typeof(RaceOptions))]
 | 
			
		||||
            public Task Race(params string[] args)
 | 
			
		||||
            {
 | 
			
		||||
                var (options, success) = OptionsParser.ParseFrom(new RaceOptions(), args);
 | 
			
		||||
 | 
			
		||||
                var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle());
 | 
			
		||||
                if (!_service.AnimalRaces.TryAdd(ctx.Guild.Id, ar))
 | 
			
		||||
                    return ctx.Channel.SendErrorAsync(GetText("animal_race"), GetText("animal_race_already_started"));
 | 
			
		||||
 | 
			
		||||
                ar.Initialize();
 | 
			
		||||
 | 
			
		||||
                var count = 0;
 | 
			
		||||
                Task _client_MessageReceived(SocketMessage arg)
 | 
			
		||||
                {
 | 
			
		||||
                    var _ = Task.Run(() =>
 | 
			
		||||
                    {
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            if (arg.Channel.Id == ctx.Channel.Id)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (ar.CurrentPhase == AnimalRace.Phase.Running && ++count % 9 == 0)
 | 
			
		||||
                                {
 | 
			
		||||
                                    raceMessage = null;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        catch { }
 | 
			
		||||
                    });
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Task Ar_OnEnded(AnimalRace race)
 | 
			
		||||
                {
 | 
			
		||||
                    _client.MessageReceived -= _client_MessageReceived;
 | 
			
		||||
                    _service.AnimalRaces.TryRemove(ctx.Guild.Id, out _);
 | 
			
		||||
                    var winner = race.FinishedUsers[0];
 | 
			
		||||
                    if (race.FinishedUsers[0].Bet > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        return ctx.Channel.SendConfirmAsync(GetText("animal_race"),
 | 
			
		||||
                                            GetText("animal_race_won_money", Format.Bold(winner.Username),
 | 
			
		||||
                                                winner.Animal.Icon, (race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign));
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        return ctx.Channel.SendConfirmAsync(GetText("animal_race"),
 | 
			
		||||
                            GetText("animal_race_won", Format.Bold(winner.Username), winner.Animal.Icon));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                ar.OnStartingFailed += Ar_OnStartingFailed;
 | 
			
		||||
                ar.OnStateUpdate += Ar_OnStateUpdate;
 | 
			
		||||
                ar.OnEnded += Ar_OnEnded;
 | 
			
		||||
                ar.OnStarted += Ar_OnStarted;
 | 
			
		||||
                _client.MessageReceived += _client_MessageReceived;
 | 
			
		||||
 | 
			
		||||
                return ctx.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting", options.StartTime),
 | 
			
		||||
                                    footer: GetText("animal_race_join_instr", Prefix));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private Task Ar_OnStarted(AnimalRace race)
 | 
			
		||||
            {
 | 
			
		||||
                if (race.Users.Count == race.MaxUsers)
 | 
			
		||||
                    return ctx.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_full"));
 | 
			
		||||
                else
 | 
			
		||||
                    return ctx.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting_with_x", race.Users.Count));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task Ar_OnStateUpdate(AnimalRace race)
 | 
			
		||||
            {
 | 
			
		||||
                var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|
 | 
			
		||||
{String.Join("\n", race.Users.Select(p =>
 | 
			
		||||
                {
 | 
			
		||||
                    var index = race.FinishedUsers.IndexOf(p);
 | 
			
		||||
                    var extra = (index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}");
 | 
			
		||||
                    return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}";
 | 
			
		||||
                }))}
 | 
			
		||||
|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|";
 | 
			
		||||
 | 
			
		||||
                var msg = raceMessage;
 | 
			
		||||
 | 
			
		||||
                if (msg == null)
 | 
			
		||||
                    raceMessage = await ctx.Channel.SendConfirmAsync(text)
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await msg.ModifyAsync(x => x.Embed = new EmbedBuilder()
 | 
			
		||||
                        .WithTitle(GetText("animal_race"))
 | 
			
		||||
                        .WithDescription(text)
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .Build())
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private Task Ar_OnStartingFailed(AnimalRace race)
 | 
			
		||||
            {
 | 
			
		||||
                _service.AnimalRaces.TryRemove(ctx.Guild.Id, out _);
 | 
			
		||||
                return ReplyErrorLocalizedAsync("animal_race_failed");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task JoinRace(ShmartNumber amount = default)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await CheckBetOptional(amount).ConfigureAwait(false))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (!_service.AnimalRaces.TryGetValue(ctx.Guild.Id, out var ar))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("race_not_exist").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var user = await ar.JoinRace(ctx.User.Id, ctx.User.ToString(), amount)
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                    if (amount > 0)
 | 
			
		||||
                        await ctx.Channel.SendConfirmAsync(GetText("animal_race_join_bet", ctx.User.Mention, user.Animal.Icon, amount + CurrencySign)).ConfigureAwait(false);
 | 
			
		||||
                    else
 | 
			
		||||
                        await ctx.Channel.SendConfirmAsync(GetText("animal_race_join", ctx.User.Mention, user.Animal.Icon)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (ArgumentOutOfRangeException)
 | 
			
		||||
                {
 | 
			
		||||
                    //ignore if user inputed an invalid amount
 | 
			
		||||
                }
 | 
			
		||||
                catch (AlreadyJoinedException)
 | 
			
		||||
                {
 | 
			
		||||
                    // just ignore this
 | 
			
		||||
                }
 | 
			
		||||
                catch (AlreadyStartedException)
 | 
			
		||||
                {
 | 
			
		||||
                    //ignore
 | 
			
		||||
                }
 | 
			
		||||
                catch (AnimalRaceFullException)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_full"))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (NotEnoughFundsException)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendErrorAsync(GetText("not_enough", CurrencySign)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										193
									
								
								src/NadekoBot/Modules/Gambling/BlackJackCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								src/NadekoBot/Modules/Gambling/BlackJackCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,193 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common.Blackjack;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        public class BlackJackCommands : GamblingSubmodule<BlackJackService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly ICurrencyService _cs;
 | 
			
		||||
            private readonly DbService _db;
 | 
			
		||||
            private IUserMessage _msg;
 | 
			
		||||
 | 
			
		||||
            public enum BjAction
 | 
			
		||||
            {
 | 
			
		||||
                Hit = int.MinValue,
 | 
			
		||||
                Stand,
 | 
			
		||||
                Double,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public BlackJackCommands(ICurrencyService cs, DbService db,
 | 
			
		||||
                GamblingConfigService gamblingConf) : base(gamblingConf) 
 | 
			
		||||
            {
 | 
			
		||||
                _cs = cs;
 | 
			
		||||
                _db = db;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task BlackJack(ShmartNumber amount)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await CheckBetMandatory(amount).ConfigureAwait(false))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var newBj = new Blackjack(_cs, _db);
 | 
			
		||||
                Blackjack bj;
 | 
			
		||||
                if (newBj == (bj = _service.Games.GetOrAdd(ctx.Channel.Id, newBj)))
 | 
			
		||||
                {
 | 
			
		||||
                    if (!await bj.Join(ctx.User, amount).ConfigureAwait(false))
 | 
			
		||||
                    {
 | 
			
		||||
                        _service.Games.TryRemove(ctx.Channel.Id, out _);
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    bj.StateUpdated += Bj_StateUpdated;
 | 
			
		||||
                    bj.GameEnded += Bj_GameEnded;
 | 
			
		||||
                    bj.Start();
 | 
			
		||||
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("bj_created").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (await bj.Join(ctx.User, amount).ConfigureAwait(false))
 | 
			
		||||
                        await ReplyConfirmLocalizedAsync("bj_joined").ConfigureAwait(false);
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Information($"{ctx.User} can't join a blackjack game as it's in " + bj.State.ToString() + " state already.");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Message.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private Task Bj_GameEnded(Blackjack arg)
 | 
			
		||||
            {
 | 
			
		||||
                _service.Games.TryRemove(ctx.Channel.Id, out _);
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task Bj_StateUpdated(Blackjack bj)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (_msg != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        var _ = _msg.DeleteAsync();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var c = bj.Dealer.Cards.Select(x => x.GetEmojiString());
 | 
			
		||||
                    var dealerIcon = "❔ ";
 | 
			
		||||
                    if (bj.State == Blackjack.GameState.Ended)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (bj.Dealer.GetHandValue() == 21)
 | 
			
		||||
                            dealerIcon = "💰 ";
 | 
			
		||||
                        else if (bj.Dealer.GetHandValue() > 21)
 | 
			
		||||
                            dealerIcon = "💥 ";
 | 
			
		||||
                        else
 | 
			
		||||
                            dealerIcon = "🏁 ";
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var cStr = string.Concat(c.Select(x => x.Substring(0, x.Length - 1) + " "));
 | 
			
		||||
                    cStr += "\n" + string.Concat(c.Select(x => x.Last() + " "));
 | 
			
		||||
                    var embed = new EmbedBuilder()
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithTitle("BlackJack")
 | 
			
		||||
                        .AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr);
 | 
			
		||||
 | 
			
		||||
                    if (bj.CurrentUser != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        embed.WithFooter($"Player to make a choice: {bj.CurrentUser.DiscordUser.ToString()}");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    foreach (var p in bj.Players)
 | 
			
		||||
                    {
 | 
			
		||||
                        c = p.Cards.Select(x => x.GetEmojiString());
 | 
			
		||||
                        cStr = "-\t" + string.Concat(c.Select(x => x.Substring(0, x.Length - 1) + " "));
 | 
			
		||||
                        cStr += "\n-\t" + string.Concat(c.Select(x => x.Last() + " "));
 | 
			
		||||
                        var full = $"{p.DiscordUser.ToString().TrimTo(20)} | Bet: {p.Bet} | Value: {p.GetHandValue()}";
 | 
			
		||||
                        if (bj.State == Blackjack.GameState.Ended)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (p.State == User.UserState.Lost)
 | 
			
		||||
                            {
 | 
			
		||||
                                full = "❌ " + full;
 | 
			
		||||
                            }
 | 
			
		||||
                            else
 | 
			
		||||
                            {
 | 
			
		||||
                                full = "✅ " + full;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else if (p == bj.CurrentUser)
 | 
			
		||||
                            full = "▶ " + full;
 | 
			
		||||
                        else if (p.State == User.UserState.Stand)
 | 
			
		||||
                            full = "⏹ " + full;
 | 
			
		||||
                        else if (p.State == User.UserState.Bust)
 | 
			
		||||
                            full = "💥 " + full;
 | 
			
		||||
                        else if (p.State == User.UserState.Blackjack)
 | 
			
		||||
                            full = "💰 " + full;
 | 
			
		||||
                        embed.AddField(full, cStr);
 | 
			
		||||
                    }
 | 
			
		||||
                    _msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private string UserToString(User x)
 | 
			
		||||
            {
 | 
			
		||||
                var playerName = x.State == User.UserState.Bust ?
 | 
			
		||||
                    Format.Strikethrough(x.DiscordUser.ToString().TrimTo(30)) :
 | 
			
		||||
                    x.DiscordUser.ToString();
 | 
			
		||||
 | 
			
		||||
                var hand = $"{string.Concat(x.Cards.Select(y => "〖" + y.GetEmojiString() + "〗"))}";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                return $"{playerName} | Bet: {x.Bet}\n";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public Task Hit() => InternalBlackJack(BjAction.Hit);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public Task Stand() => InternalBlackJack(BjAction.Stand);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public Task Double() => InternalBlackJack(BjAction.Double);
 | 
			
		||||
 | 
			
		||||
            public async Task InternalBlackJack(BjAction a)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_service.Games.TryGetValue(ctx.Channel.Id, out var bj))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (a == BjAction.Hit)
 | 
			
		||||
                    await bj.Hit(ctx.User).ConfigureAwait(false);
 | 
			
		||||
                else if (a == BjAction.Stand)
 | 
			
		||||
                    await bj.Stand(ctx.User).ConfigureAwait(false);
 | 
			
		||||
                else if (a == BjAction.Double)
 | 
			
		||||
                {
 | 
			
		||||
                    if (!await bj.Double(ctx.User).ConfigureAwait(false))
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Message.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										163
									
								
								src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common.AnimalRacing;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
 | 
			
		||||
using NadekoBot.Modules.Games.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing
 | 
			
		||||
{
 | 
			
		||||
    public sealed class AnimalRace : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        public enum Phase
 | 
			
		||||
        {
 | 
			
		||||
            WaitingForPlayers,
 | 
			
		||||
            Running,
 | 
			
		||||
            Ended,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Phase CurrentPhase { get; private set; } = Phase.WaitingForPlayers;
 | 
			
		||||
 | 
			
		||||
        public event Func<AnimalRace, Task> OnStarted = delegate { return Task.CompletedTask; };
 | 
			
		||||
        public event Func<AnimalRace, Task> OnStartingFailed = delegate { return Task.CompletedTask; };
 | 
			
		||||
        public event Func<AnimalRace, Task> OnStateUpdate = delegate { return Task.CompletedTask; };
 | 
			
		||||
        public event Func<AnimalRace, Task> OnEnded = delegate { return Task.CompletedTask; };
 | 
			
		||||
 | 
			
		||||
        public IReadOnlyCollection<AnimalRacingUser> Users => _users.ToList();
 | 
			
		||||
        public List<AnimalRacingUser> FinishedUsers { get; } = new List<AnimalRacingUser>();
 | 
			
		||||
 | 
			
		||||
        private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
 | 
			
		||||
        private readonly HashSet<AnimalRacingUser> _users = new HashSet<AnimalRacingUser>();
 | 
			
		||||
        private readonly ICurrencyService _currency;
 | 
			
		||||
        private readonly RaceOptions _options;
 | 
			
		||||
        private readonly Queue<RaceAnimal> _animalsQueue;
 | 
			
		||||
        public int MaxUsers { get; }
 | 
			
		||||
 | 
			
		||||
        public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable<RaceAnimal> availableAnimals)
 | 
			
		||||
        {
 | 
			
		||||
            this._currency = currency;
 | 
			
		||||
            this._options = options;
 | 
			
		||||
            this._animalsQueue = new Queue<RaceAnimal>(availableAnimals);
 | 
			
		||||
            this.MaxUsers = _animalsQueue.Count;
 | 
			
		||||
 | 
			
		||||
            if (this._animalsQueue.Count == 0)
 | 
			
		||||
                CurrentPhase = Phase.Ended;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Initialize() //lame name
 | 
			
		||||
        {
 | 
			
		||||
            var _t = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(_options.StartTime * 1000).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (CurrentPhase != Phase.WaitingForPlayers)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    await Start().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                finally { _locker.Release(); }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<AnimalRacingUser> JoinRace(ulong userId, string userName, long bet = 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (bet < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(bet));
 | 
			
		||||
 | 
			
		||||
            var user = new AnimalRacingUser(userName, userId, bet);
 | 
			
		||||
 | 
			
		||||
            await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (_users.Count == MaxUsers)
 | 
			
		||||
                    throw new AnimalRaceFullException();
 | 
			
		||||
 | 
			
		||||
                if (CurrentPhase != Phase.WaitingForPlayers)
 | 
			
		||||
                    throw new AlreadyStartedException();
 | 
			
		||||
 | 
			
		||||
                if (!await _currency.RemoveAsync(userId, "BetRace", bet).ConfigureAwait(false))
 | 
			
		||||
                    throw new NotEnoughFundsException();
 | 
			
		||||
 | 
			
		||||
                if (_users.Contains(user))
 | 
			
		||||
                    throw new AlreadyJoinedException();
 | 
			
		||||
 | 
			
		||||
                var animal = _animalsQueue.Dequeue();
 | 
			
		||||
                user.Animal = animal;
 | 
			
		||||
                _users.Add(user);
 | 
			
		||||
 | 
			
		||||
                if (_animalsQueue.Count == 0) //start if no more spots left
 | 
			
		||||
                    await Start().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                return user;
 | 
			
		||||
            }
 | 
			
		||||
            finally { _locker.Release(); }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task Start()
 | 
			
		||||
        {
 | 
			
		||||
            CurrentPhase = Phase.Running;
 | 
			
		||||
            if (_users.Count <= 1)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var user in _users)
 | 
			
		||||
                {
 | 
			
		||||
                    if (user.Bet > 0)
 | 
			
		||||
                        await _currency.AddAsync(user.UserId, "Race refund", user.Bet).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var _sf = OnStartingFailed?.Invoke(this);
 | 
			
		||||
                CurrentPhase = Phase.Ended;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var _ = OnStarted?.Invoke(this);
 | 
			
		||||
            var _t = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                var rng = new NadekoRandom();
 | 
			
		||||
                while (!_users.All(x => x.Progress >= 60))
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var user in _users)
 | 
			
		||||
                    {
 | 
			
		||||
                        user.Progress += rng.Next(1, 11);
 | 
			
		||||
                        if (user.Progress >= 60)
 | 
			
		||||
                            user.Progress = 60;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var finished = _users.Where(x => x.Progress >= 60 && !FinishedUsers.Contains(x))
 | 
			
		||||
                        .Shuffle();
 | 
			
		||||
 | 
			
		||||
                    FinishedUsers.AddRange(finished);
 | 
			
		||||
 | 
			
		||||
                    var _ignore = OnStateUpdate?.Invoke(this);
 | 
			
		||||
                    await Task.Delay(2500).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (FinishedUsers[0].Bet > 0)
 | 
			
		||||
                    await _currency.AddAsync(FinishedUsers[0].UserId, "Won a Race", FinishedUsers[0].Bet * (_users.Count - 1))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                var _ended = OnEnded?.Invoke(this);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            CurrentPhase = Phase.Ended;
 | 
			
		||||
            OnStarted = null;
 | 
			
		||||
            OnEnded = null;
 | 
			
		||||
            OnStartingFailed = null;
 | 
			
		||||
            OnStateUpdate = null;
 | 
			
		||||
            _locker.Dispose();
 | 
			
		||||
            _users.Clear();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
using NadekoBot.Modules.Games.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing
 | 
			
		||||
{
 | 
			
		||||
    public class AnimalRacingUser
 | 
			
		||||
    {
 | 
			
		||||
        public long Bet { get; }
 | 
			
		||||
        public string Username { get; }
 | 
			
		||||
        public ulong UserId { get; }
 | 
			
		||||
        public RaceAnimal Animal { get; set; }
 | 
			
		||||
        public int Progress { get; set; }
 | 
			
		||||
 | 
			
		||||
        public AnimalRacingUser(string username, ulong userId, long bet)
 | 
			
		||||
        {
 | 
			
		||||
            this.Bet = bet;
 | 
			
		||||
            this.Username = username;
 | 
			
		||||
            this.UserId = userId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override bool Equals(object obj)
 | 
			
		||||
        {
 | 
			
		||||
            return obj is AnimalRacingUser x
 | 
			
		||||
                ? x.UserId == this.UserId
 | 
			
		||||
                : false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override int GetHashCode()
 | 
			
		||||
        {
 | 
			
		||||
            return this.UserId.GetHashCode();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
 | 
			
		||||
{
 | 
			
		||||
    public class AlreadyJoinedException : Exception
 | 
			
		||||
    {
 | 
			
		||||
        public AlreadyJoinedException()
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AlreadyJoinedException(string message) : base(message)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AlreadyJoinedException(string message, Exception innerException) : base(message, innerException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
 | 
			
		||||
{
 | 
			
		||||
    public class AlreadyStartedException : Exception
 | 
			
		||||
    {
 | 
			
		||||
        public AlreadyStartedException()
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AlreadyStartedException(string message) : base(message)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AlreadyStartedException(string message, Exception innerException) : base(message, innerException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
 | 
			
		||||
{
 | 
			
		||||
    public class AnimalRaceFullException : Exception
 | 
			
		||||
    {
 | 
			
		||||
        public AnimalRaceFullException()
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AnimalRaceFullException(string message) : base(message)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AnimalRaceFullException(string message, Exception innerException) : base(message, innerException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
 | 
			
		||||
{
 | 
			
		||||
    public class NotEnoughFundsException : Exception
 | 
			
		||||
    {
 | 
			
		||||
        public NotEnoughFundsException()
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public NotEnoughFundsException(string message) : base(message)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public NotEnoughFundsException(string message, Exception innerException) : base(message, innerException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.AnimalRacing
 | 
			
		||||
{
 | 
			
		||||
    public class RaceOptions : INadekoCommandOptions
 | 
			
		||||
    {
 | 
			
		||||
        [Option('s', "start-time", Default = 20, Required = false)]
 | 
			
		||||
        public int StartTime { get; set; } = 20;
 | 
			
		||||
 | 
			
		||||
        public void NormalizeOptions()
 | 
			
		||||
        {
 | 
			
		||||
            if (this.StartTime < 10 || this.StartTime > 120)
 | 
			
		||||
                this.StartTime = 20;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								src/NadekoBot/Modules/Gambling/Common/BetRoll.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/NadekoBot/Modules/Gambling/Common/BetRoll.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common
 | 
			
		||||
{
 | 
			
		||||
    public class Betroll
 | 
			
		||||
    {
 | 
			
		||||
        public class Result
 | 
			
		||||
        {
 | 
			
		||||
            public int Roll { get; set; }
 | 
			
		||||
            public float Multiplier { get; set; }
 | 
			
		||||
            public int Threshold { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        private readonly IOrderedEnumerable<GamblingConfig.BetRollConfig.Pair> _thresholdPairs;
 | 
			
		||||
        private readonly Random _rng;
 | 
			
		||||
        
 | 
			
		||||
        public Betroll(GamblingConfig.BetRollConfig settings)
 | 
			
		||||
        {
 | 
			
		||||
            _thresholdPairs = settings.Pairs.OrderByDescending(x => x.WhenAbove);
 | 
			
		||||
            _rng = new Random();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Result Roll()
 | 
			
		||||
        {
 | 
			
		||||
            var roll = _rng.Next(0, 101);
 | 
			
		||||
 | 
			
		||||
            var pair = _thresholdPairs.FirstOrDefault(x => x.WhenAbove < roll);
 | 
			
		||||
            if (pair is null)
 | 
			
		||||
            {
 | 
			
		||||
                return new Result
 | 
			
		||||
                {
 | 
			
		||||
                    Multiplier = 0,
 | 
			
		||||
                    Roll = roll,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return new Result
 | 
			
		||||
            {
 | 
			
		||||
                Multiplier = pair.MultiplyBy,
 | 
			
		||||
                Roll = roll,
 | 
			
		||||
                Threshold = pair.WhenAbove,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										353
									
								
								src/NadekoBot/Modules/Gambling/Common/Blackjack/Blackjack.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										353
									
								
								src/NadekoBot/Modules/Gambling/Common/Blackjack/Blackjack.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,353 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Blackjack
 | 
			
		||||
{
 | 
			
		||||
    public class Blackjack
 | 
			
		||||
    {
 | 
			
		||||
        public enum GameState
 | 
			
		||||
        {
 | 
			
		||||
            Starting,
 | 
			
		||||
            Playing,
 | 
			
		||||
            Ended
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Deck Deck { get; set; } = new QuadDeck();
 | 
			
		||||
        public Dealer Dealer { get; set; }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        public List<User> Players { get; set; } = new List<User>();
 | 
			
		||||
        public GameState State { get; set; } = GameState.Starting;
 | 
			
		||||
        public User CurrentUser { get; private set; }
 | 
			
		||||
 | 
			
		||||
        private TaskCompletionSource<bool> _currentUserMove;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public event Func<Blackjack, Task> StateUpdated;
 | 
			
		||||
        public event Func<Blackjack, Task> GameEnded;
 | 
			
		||||
 | 
			
		||||
        private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
 | 
			
		||||
 | 
			
		||||
        public Blackjack(ICurrencyService cs, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
            _db = db;
 | 
			
		||||
            Dealer = new Dealer();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Start()
 | 
			
		||||
        {
 | 
			
		||||
            var _ = GameLoop();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task GameLoop()
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                //wait for players to join
 | 
			
		||||
                await Task.Delay(20000).ConfigureAwait(false);
 | 
			
		||||
                await locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    State = GameState.Playing;
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    locker.Release();
 | 
			
		||||
                }
 | 
			
		||||
                await PrintState().ConfigureAwait(false);
 | 
			
		||||
                //if no users joined the game, end it
 | 
			
		||||
                if (!Players.Any())
 | 
			
		||||
                {
 | 
			
		||||
                    State = GameState.Ended;
 | 
			
		||||
                    var end = GameEnded?.Invoke(this);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                //give 1 card to the dealer and 2 to each player
 | 
			
		||||
                Dealer.Cards.Add(Deck.Draw());
 | 
			
		||||
                foreach (var usr in Players)
 | 
			
		||||
                {
 | 
			
		||||
                    usr.Cards.Add(Deck.Draw());
 | 
			
		||||
                    usr.Cards.Add(Deck.Draw());
 | 
			
		||||
 | 
			
		||||
                    if (usr.GetHandValue() == 21)
 | 
			
		||||
                        usr.State = User.UserState.Blackjack;
 | 
			
		||||
                }
 | 
			
		||||
                //go through all users and ask them what they want to do
 | 
			
		||||
                foreach (var usr in Players.Where(x => !x.Done))
 | 
			
		||||
                {
 | 
			
		||||
                    while (!usr.Done)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Information($"Waiting for {usr.DiscordUser}'s move");
 | 
			
		||||
                        await PromptUserMove(usr).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                await PrintState().ConfigureAwait(false);
 | 
			
		||||
                State = GameState.Ended;
 | 
			
		||||
                await Task.Delay(2500).ConfigureAwait(false);
 | 
			
		||||
                Log.Information("Dealer moves");
 | 
			
		||||
                await DealerMoves().ConfigureAwait(false);
 | 
			
		||||
                await PrintState().ConfigureAwait(false);
 | 
			
		||||
                var _ = GameEnded?.Invoke(this);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "REPORT THE MESSAGE BELOW IN #NadekoLog SERVER PLEASE");
 | 
			
		||||
                State = GameState.Ended;
 | 
			
		||||
                var _ = GameEnded?.Invoke(this);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task PromptUserMove(User usr)
 | 
			
		||||
        {
 | 
			
		||||
            var pause = Task.Delay(20000); //10 seconds to decide
 | 
			
		||||
            CurrentUser = usr;
 | 
			
		||||
            _currentUserMove = new TaskCompletionSource<bool>();
 | 
			
		||||
            await PrintState().ConfigureAwait(false);
 | 
			
		||||
            // either wait for the user to make an action and
 | 
			
		||||
            // if he doesn't - stand
 | 
			
		||||
            var finished = await Task.WhenAny(pause, _currentUserMove.Task).ConfigureAwait(false);
 | 
			
		||||
            if (finished == pause)
 | 
			
		||||
            {
 | 
			
		||||
                await Stand(usr).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            CurrentUser = null;
 | 
			
		||||
            _currentUserMove = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> Join(IUser user, long bet)
 | 
			
		||||
        {
 | 
			
		||||
            await locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (State != GameState.Starting)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (Players.Count >= 5)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (!await _cs.RemoveAsync(user, "BlackJack-gamble", bet, gamble: true).ConfigureAwait(false))
 | 
			
		||||
                {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Players.Add(new User(user, bet));
 | 
			
		||||
                var _ = PrintState();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                locker.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> Stand(IUser u)
 | 
			
		||||
        {
 | 
			
		||||
            var cu = CurrentUser;
 | 
			
		||||
 | 
			
		||||
            if (cu != null && cu.DiscordUser == u)
 | 
			
		||||
                return await Stand(cu).ConfigureAwait(false);
 | 
			
		||||
            
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> Stand(User u)
 | 
			
		||||
        {
 | 
			
		||||
            await locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (State != GameState.Playing)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (CurrentUser != u)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                u.State = User.UserState.Stand;
 | 
			
		||||
                _currentUserMove.TrySetResult(true);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                locker.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task DealerMoves()
 | 
			
		||||
        {
 | 
			
		||||
            var hw = Dealer.GetHandValue();
 | 
			
		||||
            while (hw < 17
 | 
			
		||||
                || (hw == 17 && Dealer.Cards.Count(x => x.Number == 1) > (Dealer.GetRawHandValue() - 17) / 10))// hit on soft 17
 | 
			
		||||
            {
 | 
			
		||||
                /* Dealer has
 | 
			
		||||
                     A 6
 | 
			
		||||
                     That's 17, soft
 | 
			
		||||
                     hw == 17 => true
 | 
			
		||||
                     number of aces = 1
 | 
			
		||||
                     1 > 17-17 /10 => true
 | 
			
		||||
                    
 | 
			
		||||
                     AA 5
 | 
			
		||||
                     That's 17, again soft, since one ace is worth 11, even though another one is 1
 | 
			
		||||
                     hw == 17 => true
 | 
			
		||||
                     number of aces = 2
 | 
			
		||||
                     2 > 27 - 17 / 10 => true
 | 
			
		||||
 | 
			
		||||
                     AA Q 5
 | 
			
		||||
                     That's 17, but not soft, since both aces are worth 1
 | 
			
		||||
                     hw == 17 => true
 | 
			
		||||
                     number of aces = 2
 | 
			
		||||
                     2 > 37 - 17 / 10 => false
 | 
			
		||||
                 * */
 | 
			
		||||
                Dealer.Cards.Add(Deck.Draw());
 | 
			
		||||
                hw = Dealer.GetHandValue();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (hw > 21)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var usr in Players)
 | 
			
		||||
                {
 | 
			
		||||
                    if (usr.State == User.UserState.Stand || usr.State == User.UserState.Blackjack)
 | 
			
		||||
                        usr.State = User.UserState.Won;
 | 
			
		||||
                    else
 | 
			
		||||
                        usr.State = User.UserState.Lost;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var usr in Players)
 | 
			
		||||
                {
 | 
			
		||||
                    if (usr.State == User.UserState.Blackjack)
 | 
			
		||||
                        usr.State = User.UserState.Won;
 | 
			
		||||
                    else if (usr.State == User.UserState.Stand)
 | 
			
		||||
                        usr.State = hw < usr.GetHandValue()
 | 
			
		||||
                            ? User.UserState.Won
 | 
			
		||||
                            : User.UserState.Lost;
 | 
			
		||||
                    else
 | 
			
		||||
                        usr.State = User.UserState.Lost;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var usr in Players)
 | 
			
		||||
            {
 | 
			
		||||
                if (usr.State == User.UserState.Won || usr.State == User.UserState.Blackjack)
 | 
			
		||||
                {
 | 
			
		||||
                    await _cs.AddAsync(usr.DiscordUser.Id, "BlackJack-win", usr.Bet * 2, gamble: true).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> Double(IUser u)
 | 
			
		||||
        {
 | 
			
		||||
            var cu = CurrentUser;
 | 
			
		||||
 | 
			
		||||
            if (cu != null && cu.DiscordUser == u)
 | 
			
		||||
                return await Double(cu).ConfigureAwait(false);
 | 
			
		||||
            
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> Double(User u)
 | 
			
		||||
        {
 | 
			
		||||
            await locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (State != GameState.Playing)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (CurrentUser != u)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (!await _cs.RemoveAsync(u.DiscordUser.Id, "Blackjack-double", u.Bet).ConfigureAwait(false))
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                u.Bet *= 2;
 | 
			
		||||
 | 
			
		||||
                u.Cards.Add(Deck.Draw());
 | 
			
		||||
 | 
			
		||||
                if (u.GetHandValue() == 21)
 | 
			
		||||
                {
 | 
			
		||||
                    //blackjack
 | 
			
		||||
                    u.State = User.UserState.Blackjack;
 | 
			
		||||
                }
 | 
			
		||||
                else if (u.GetHandValue() > 21)
 | 
			
		||||
                {
 | 
			
		||||
                    // user busted
 | 
			
		||||
                    u.State = User.UserState.Bust;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    //with double you just get one card, and then you're done
 | 
			
		||||
                    u.State = User.UserState.Stand;
 | 
			
		||||
                }
 | 
			
		||||
                _currentUserMove.TrySetResult(true);
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                locker.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> Hit(IUser u)
 | 
			
		||||
        {
 | 
			
		||||
            var cu = CurrentUser;
 | 
			
		||||
 | 
			
		||||
            if (cu != null && cu.DiscordUser == u)
 | 
			
		||||
                return await Hit(cu).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> Hit(User u)
 | 
			
		||||
        {
 | 
			
		||||
            await locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (State != GameState.Playing)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (CurrentUser != u)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                u.Cards.Add(Deck.Draw());
 | 
			
		||||
 | 
			
		||||
                if (u.GetHandValue() == 21)
 | 
			
		||||
                {
 | 
			
		||||
                    //blackjack
 | 
			
		||||
                    u.State = User.UserState.Blackjack;
 | 
			
		||||
                }
 | 
			
		||||
                else if (u.GetHandValue() > 21)
 | 
			
		||||
                {
 | 
			
		||||
                    // user busted
 | 
			
		||||
                    u.State = User.UserState.Bust;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    //you can hit or stand again
 | 
			
		||||
                }
 | 
			
		||||
                _currentUserMove.TrySetResult(true);
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                locker.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task PrintState()
 | 
			
		||||
        {
 | 
			
		||||
            if (StateUpdated == null)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            return StateUpdated.Invoke(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								src/NadekoBot/Modules/Gambling/Common/Blackjack/Player.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/NadekoBot/Modules/Gambling/Common/Blackjack/Player.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Blackjack
 | 
			
		||||
{
 | 
			
		||||
    public abstract class Player
 | 
			
		||||
    {
 | 
			
		||||
        public List<Deck.Card> Cards { get; } = new List<Deck.Card>();
 | 
			
		||||
 | 
			
		||||
        public int GetHandValue()
 | 
			
		||||
        {
 | 
			
		||||
            var val = GetRawHandValue();
 | 
			
		||||
 | 
			
		||||
            // while the hand value is greater than 21, for each ace you have in the deck
 | 
			
		||||
            // reduce the value by 10 until it drops below 22
 | 
			
		||||
            // (emulating the fact that ace is either a 1 or a 11)
 | 
			
		||||
            var i = Cards.Count(x => x.Number == 1);
 | 
			
		||||
            while (val > 21 && i-- > 0)
 | 
			
		||||
            {
 | 
			
		||||
                val -= 10;
 | 
			
		||||
            }
 | 
			
		||||
            return val;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public int GetRawHandValue()
 | 
			
		||||
        {
 | 
			
		||||
            return Cards.Sum(x => x.Number == 1 ? 11 : x.Number >= 10 ? 10 : x.Number);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Dealer : Player
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class User : Player
 | 
			
		||||
    {
 | 
			
		||||
        public enum UserState
 | 
			
		||||
        {
 | 
			
		||||
            Waiting,
 | 
			
		||||
            Stand,
 | 
			
		||||
            Bust,
 | 
			
		||||
            Blackjack,
 | 
			
		||||
            Won,
 | 
			
		||||
            Lost
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public User(IUser user, long bet)
 | 
			
		||||
        {
 | 
			
		||||
            if (bet <= 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(bet));
 | 
			
		||||
 | 
			
		||||
            this.Bet = bet;
 | 
			
		||||
            this.DiscordUser = user;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public UserState State { get; set; } = UserState.Waiting;
 | 
			
		||||
        public long Bet { get; set; }
 | 
			
		||||
        public IUser DiscordUser { get; }
 | 
			
		||||
        public bool Done => State != UserState.Waiting;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								src/NadekoBot/Modules/Gambling/Common/CurrencyRaffleGame.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/NadekoBot/Modules/Gambling/Common/CurrencyRaffleGame.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common
 | 
			
		||||
{
 | 
			
		||||
    public class CurrencyRaffleGame
 | 
			
		||||
    {
 | 
			
		||||
        public enum Type {
 | 
			
		||||
            Mixed,
 | 
			
		||||
            Normal
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class User
 | 
			
		||||
        {
 | 
			
		||||
            public IUser DiscordUser { get; set; }
 | 
			
		||||
            public long Amount { get; set; }
 | 
			
		||||
 | 
			
		||||
            public override int GetHashCode()
 | 
			
		||||
            {
 | 
			
		||||
                return DiscordUser.GetHashCode();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public override bool Equals(object obj)
 | 
			
		||||
            {
 | 
			
		||||
                return obj is User u
 | 
			
		||||
                    ? u.DiscordUser == DiscordUser
 | 
			
		||||
                    : false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly HashSet<User> _users = new HashSet<User>();
 | 
			
		||||
        public IEnumerable<User> Users => _users;
 | 
			
		||||
        public Type GameType { get; }
 | 
			
		||||
 | 
			
		||||
        public CurrencyRaffleGame(Type type)
 | 
			
		||||
        {
 | 
			
		||||
            GameType = type;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool AddUser(IUser usr, long amount)
 | 
			
		||||
        {
 | 
			
		||||
            // if game type is normal, and someone already joined the game 
 | 
			
		||||
            // (that's the user who created it)
 | 
			
		||||
            if (GameType == Type.Normal && _users.Count > 0 &&
 | 
			
		||||
                _users.First().Amount != amount)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            if (!_users.Add(new User
 | 
			
		||||
            {
 | 
			
		||||
                DiscordUser = usr,
 | 
			
		||||
                Amount = amount,
 | 
			
		||||
            }))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public User GetWinner()
 | 
			
		||||
        {
 | 
			
		||||
            var rng = new NadekoRandom();
 | 
			
		||||
            if (GameType == Type.Mixed)
 | 
			
		||||
            {
 | 
			
		||||
                var num = rng.NextLong(0L, Users.Sum(x => x.Amount));
 | 
			
		||||
                var sum = 0L;
 | 
			
		||||
                foreach (var u in Users)
 | 
			
		||||
                {
 | 
			
		||||
                    sum += u.Amount;
 | 
			
		||||
                    if (sum > num)
 | 
			
		||||
                        return u;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var usrs = _users.ToArray();
 | 
			
		||||
            return usrs[rng.Next(0, usrs.Length)];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										312
									
								
								src/NadekoBot/Modules/Gambling/Common/Deck.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								src/NadekoBot/Modules/Gambling/Common/Deck.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,312 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common
 | 
			
		||||
{
 | 
			
		||||
    public class QuadDeck : Deck
 | 
			
		||||
    {
 | 
			
		||||
        protected override void RefillPool()
 | 
			
		||||
        {
 | 
			
		||||
            CardPool = new List<Card>(52 * 4);
 | 
			
		||||
            for (var j = 1; j < 14; j++)
 | 
			
		||||
            {
 | 
			
		||||
                for (var i = 1; i < 5; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    CardPool.Add(new Card((CardSuit)i, j));
 | 
			
		||||
                    CardPool.Add(new Card((CardSuit)i, j));
 | 
			
		||||
                    CardPool.Add(new Card((CardSuit)i, j));
 | 
			
		||||
                    CardPool.Add(new Card((CardSuit)i, j));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Deck
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly Dictionary<int, string> cardNames = new Dictionary<int, string>() {
 | 
			
		||||
            { 1, "Ace" },
 | 
			
		||||
            { 2, "Two" },
 | 
			
		||||
            { 3, "Three" },
 | 
			
		||||
            { 4, "Four" },
 | 
			
		||||
            { 5, "Five" },
 | 
			
		||||
            { 6, "Six" },
 | 
			
		||||
            { 7, "Seven" },
 | 
			
		||||
            { 8, "Eight" },
 | 
			
		||||
            { 9, "Nine" },
 | 
			
		||||
            { 10, "Ten" },
 | 
			
		||||
            { 11, "Jack" },
 | 
			
		||||
            { 12, "Queen" },
 | 
			
		||||
            { 13, "King" }
 | 
			
		||||
        };
 | 
			
		||||
        private static Dictionary<string, Func<List<Card>, bool>> handValues;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        public enum CardSuit
 | 
			
		||||
        {
 | 
			
		||||
            Spades = 1,
 | 
			
		||||
            Hearts = 2,
 | 
			
		||||
            Diamonds = 3,
 | 
			
		||||
            Clubs = 4
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class Card : IComparable
 | 
			
		||||
        {
 | 
			
		||||
            public CardSuit Suit { get; }
 | 
			
		||||
            public int Number { get; }
 | 
			
		||||
 | 
			
		||||
            public string FullName
 | 
			
		||||
            {
 | 
			
		||||
                get
 | 
			
		||||
                {
 | 
			
		||||
                    var str = "";
 | 
			
		||||
 | 
			
		||||
                    if (Number <= 10 && Number > 1)
 | 
			
		||||
                    {
 | 
			
		||||
                        str += "_" + Number;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        str += GetValueText().ToLowerInvariant();
 | 
			
		||||
                    }
 | 
			
		||||
                    return str + "_of_" + Suit.ToString().ToLowerInvariant();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public Card(CardSuit s, int cardNum)
 | 
			
		||||
            {
 | 
			
		||||
                this.Suit = s;
 | 
			
		||||
                this.Number = cardNum;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public string GetValueText() => cardNames[Number];
 | 
			
		||||
 | 
			
		||||
            public override string ToString() => cardNames[Number] + " Of " + Suit;
 | 
			
		||||
 | 
			
		||||
            public int CompareTo(object obj)
 | 
			
		||||
            {
 | 
			
		||||
                if (!(obj is Card)) return 0;
 | 
			
		||||
                var c = (Card)obj;
 | 
			
		||||
                return this.Number - c.Number;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public static Card Parse(string input)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(input))
 | 
			
		||||
                    throw new ArgumentNullException(nameof(input));
 | 
			
		||||
 | 
			
		||||
                if (input.Length != 2
 | 
			
		||||
                    || !_numberCharToNumber.TryGetValue(input[0], out var n)
 | 
			
		||||
                    || !_suitCharToSuit.TryGetValue(input[1].ToString(), out var s))
 | 
			
		||||
                {
 | 
			
		||||
                    throw new ArgumentException("Invalid input", nameof(input));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return new Card(s, n);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public string GetEmojiString()
 | 
			
		||||
            {
 | 
			
		||||
                var str = "";
 | 
			
		||||
 | 
			
		||||
                str += _regIndicators[this.Number - 1];
 | 
			
		||||
                str += _suitToSuitChar[this.Suit];
 | 
			
		||||
 | 
			
		||||
                return str;
 | 
			
		||||
            }
 | 
			
		||||
            private readonly string[] _regIndicators = new[]
 | 
			
		||||
            {
 | 
			
		||||
                "🇦",
 | 
			
		||||
                ":two:",
 | 
			
		||||
                ":three:",
 | 
			
		||||
                ":four:",
 | 
			
		||||
                ":five:",
 | 
			
		||||
                ":six:",
 | 
			
		||||
                ":seven:",
 | 
			
		||||
                ":eight:",
 | 
			
		||||
                ":nine:",
 | 
			
		||||
                ":keycap_ten:",
 | 
			
		||||
                "🇯",
 | 
			
		||||
                "🇶",
 | 
			
		||||
                "🇰"
 | 
			
		||||
            };
 | 
			
		||||
            private static readonly IReadOnlyDictionary<CardSuit, string> _suitToSuitChar = new Dictionary<CardSuit, string>
 | 
			
		||||
            {
 | 
			
		||||
                {CardSuit.Diamonds, "♦"},
 | 
			
		||||
                {CardSuit.Clubs, "♣"},
 | 
			
		||||
                {CardSuit.Spades, "♠"},
 | 
			
		||||
                {CardSuit.Hearts, "♥"},
 | 
			
		||||
            };
 | 
			
		||||
            private static IReadOnlyDictionary<string, CardSuit> _suitCharToSuit = new Dictionary<string, CardSuit>
 | 
			
		||||
            {
 | 
			
		||||
                {"♦", CardSuit.Diamonds },
 | 
			
		||||
                {"d", CardSuit.Diamonds },
 | 
			
		||||
                {"♣", CardSuit.Clubs },
 | 
			
		||||
                {"c", CardSuit.Clubs },
 | 
			
		||||
                {"♠", CardSuit.Spades },
 | 
			
		||||
                {"s", CardSuit.Spades },
 | 
			
		||||
                {"♥", CardSuit.Hearts },
 | 
			
		||||
                {"h", CardSuit.Hearts },
 | 
			
		||||
            };
 | 
			
		||||
            private static IReadOnlyDictionary<char, int> _numberCharToNumber = new Dictionary<char, int>()
 | 
			
		||||
            {
 | 
			
		||||
                {'a', 1 },
 | 
			
		||||
                {'2', 2 },
 | 
			
		||||
                {'3', 3 },
 | 
			
		||||
                {'4', 4 },
 | 
			
		||||
                {'5', 5 },
 | 
			
		||||
                {'6', 6 },
 | 
			
		||||
                {'7', 7 },
 | 
			
		||||
                {'8', 8 },
 | 
			
		||||
                {'9', 9 },
 | 
			
		||||
                {'t', 10 },
 | 
			
		||||
                {'j', 11 },
 | 
			
		||||
                {'q', 12 },
 | 
			
		||||
                {'k', 13 },
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public List<Card> CardPool { get; set; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Creates a new instance of the BlackJackGame, this allows you to create multiple games running at one time.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Deck()
 | 
			
		||||
        {
 | 
			
		||||
            RefillPool();
 | 
			
		||||
        }
 | 
			
		||||
        static Deck()
 | 
			
		||||
        {
 | 
			
		||||
            InitHandValues();
 | 
			
		||||
        }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Restart the game of blackjack. It will only refill the pool for now. Probably wont be used, unless you want to have only 1 bjg running at one time,
 | 
			
		||||
        /// then you will restart the same game every time.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public void Restart() => RefillPool();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Removes all cards from the pool and refills the pool with all of the possible cards. NOTE: I think this is too expensive.
 | 
			
		||||
        /// We should probably make it so it copies another premade list with all the cards, or something.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        protected virtual void RefillPool()
 | 
			
		||||
        {
 | 
			
		||||
            CardPool = new List<Card>(52);
 | 
			
		||||
            //foreach suit
 | 
			
		||||
            for (var j = 1; j < 14; j++)
 | 
			
		||||
            {
 | 
			
		||||
                // and number
 | 
			
		||||
                for (var i = 1; i < 5; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    //generate a card of that suit and number and add it to the pool
 | 
			
		||||
 | 
			
		||||
                    // the pool will go from ace of spades,hears,diamonds,clubs all the way to the king of spades. hearts, ...
 | 
			
		||||
                    CardPool.Add(new Card((CardSuit)i, j));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        private Random r = new NadekoRandom();
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Take a card from the pool, you either take it from the top if the deck is shuffled, or from a random place if the deck is in the default order.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>A card from the pool</returns>
 | 
			
		||||
        public Card Draw()
 | 
			
		||||
        {
 | 
			
		||||
            if (CardPool.Count == 0)
 | 
			
		||||
                Restart();
 | 
			
		||||
            //you can either do this if your deck is not shuffled
 | 
			
		||||
 | 
			
		||||
            var num = r.Next(0, CardPool.Count);
 | 
			
		||||
            var c = CardPool[num];
 | 
			
		||||
            CardPool.RemoveAt(num);
 | 
			
		||||
            return c;
 | 
			
		||||
 | 
			
		||||
            // if you want to shuffle when you fill, then take the first one
 | 
			
		||||
            /*
 | 
			
		||||
            Card c = cardPool[0];
 | 
			
		||||
            cardPool.RemoveAt(0);
 | 
			
		||||
            return c;
 | 
			
		||||
            */
 | 
			
		||||
        }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Shuffles the deck. Use this if you want to take cards from the top of the deck, instead of randomly. See DrawACard method.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private void Shuffle()
 | 
			
		||||
        {
 | 
			
		||||
            if (CardPool.Count <= 1) return;
 | 
			
		||||
            var orderedPool = CardPool.Shuffle();
 | 
			
		||||
            CardPool = CardPool as List<Card> ?? orderedPool.ToList();
 | 
			
		||||
        }
 | 
			
		||||
        public override string ToString() => string.Concat(CardPool.Select(c => c.ToString())) + Environment.NewLine;
 | 
			
		||||
 | 
			
		||||
        private static void InitHandValues()
 | 
			
		||||
        {
 | 
			
		||||
            bool hasPair(List<Card> cards) => cards.GroupBy(card => card.Number)
 | 
			
		||||
                                                .Count(group => group.Count() == 2) == 1;
 | 
			
		||||
            bool isPair(List<Card> cards) => cards.GroupBy(card => card.Number)
 | 
			
		||||
                                                .Count(group => group.Count() == 3) == 0
 | 
			
		||||
                                           && hasPair(cards);
 | 
			
		||||
 | 
			
		||||
            bool isTwoPair(List<Card> cards) => cards.GroupBy(card => card.Number)
 | 
			
		||||
                                                .Count(group => group.Count() == 2) == 2;
 | 
			
		||||
 | 
			
		||||
            bool isStraight(List<Card> cards)
 | 
			
		||||
            {
 | 
			
		||||
                if (cards.GroupBy(card => card.Number).Count() != cards.Count())
 | 
			
		||||
                    return false;
 | 
			
		||||
                var toReturn = (cards.Max(card => (int)card.Number)
 | 
			
		||||
                                    - cards.Min(card => (int)card.Number) == 4);
 | 
			
		||||
                if (toReturn || cards.All(c => c.Number != 1)) return toReturn;
 | 
			
		||||
 | 
			
		||||
                var newCards = cards.Select(c => c.Number == 1 ? new Card(c.Suit, 14) : c);
 | 
			
		||||
                return (newCards.Max(card => (int)card.Number)
 | 
			
		||||
                        - newCards.Min(card => (int)card.Number) == 4);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            bool hasThreeOfKind(List<Card> cards) => cards.GroupBy(card => card.Number)
 | 
			
		||||
                                                .Any(group => group.Count() == 3);
 | 
			
		||||
 | 
			
		||||
            bool isThreeOfKind(List<Card> cards) => hasThreeOfKind(cards) && !hasPair(cards);
 | 
			
		||||
 | 
			
		||||
            bool isFlush(List<Card> cards) => cards.GroupBy(card => card.Suit).Count() == 1;
 | 
			
		||||
 | 
			
		||||
            bool isFourOfKind(List<Card> cards) => cards.GroupBy(card => card.Number)
 | 
			
		||||
                                                .Any(group => group.Count() == 4);
 | 
			
		||||
 | 
			
		||||
            bool isFullHouse(List<Card> cards) => hasPair(cards) && hasThreeOfKind(cards);
 | 
			
		||||
 | 
			
		||||
            bool hasStraightFlush(List<Card> cards) => isFlush(cards) && isStraight(cards);
 | 
			
		||||
 | 
			
		||||
            bool isRoyalFlush(List<Card> cards) => cards.Min(card => card.Number) == 1 &&
 | 
			
		||||
                                           cards.Max(card => card.Number) == 13
 | 
			
		||||
                                           && hasStraightFlush(cards);
 | 
			
		||||
 | 
			
		||||
            bool isStraightFlush(List<Card> cards) => hasStraightFlush(cards) && !isRoyalFlush(cards);
 | 
			
		||||
 | 
			
		||||
            handValues = new Dictionary<string, Func<List<Card>, bool>>
 | 
			
		||||
            {
 | 
			
		||||
                { "Royal Flush", isRoyalFlush },
 | 
			
		||||
                { "Straight Flush", isStraightFlush },
 | 
			
		||||
                { "Four Of A Kind", isFourOfKind },
 | 
			
		||||
                { "Full House", isFullHouse },
 | 
			
		||||
                { "Flush", isFlush },
 | 
			
		||||
                { "Straight", isStraight },
 | 
			
		||||
                { "Three Of A Kind", isThreeOfKind },
 | 
			
		||||
                { "Two Pairs", isTwoPair },
 | 
			
		||||
                { "A Pair", isPair }
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static string GetHandValue(List<Card> cards)
 | 
			
		||||
        {
 | 
			
		||||
            if (handValues == null)
 | 
			
		||||
                InitHandValues();
 | 
			
		||||
            foreach (var kvp in handValues.Where(x => x.Value(cards)))
 | 
			
		||||
            {
 | 
			
		||||
                return kvp.Key;
 | 
			
		||||
            }
 | 
			
		||||
            return "High card " + (cards.FirstOrDefault(c => c.Number == 1)?.GetValueText() ?? cards.Max().GetValueText());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								src/NadekoBot/Modules/Gambling/Common/Events/EventOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/NadekoBot/Modules/Gambling/Common/Events/EventOptions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Events
 | 
			
		||||
{
 | 
			
		||||
    public class EventOptions : INadekoCommandOptions
 | 
			
		||||
    {
 | 
			
		||||
        [Option('a', "amount", Required = false, Default = 100, HelpText = "Amount of currency each user receives.")]
 | 
			
		||||
        public long Amount { get; set; } = 100;
 | 
			
		||||
        [Option('p', "pot-size", Required = false, Default = 0, HelpText = "The maximum amount of currency that can be rewarded. 0 means no limit.")]
 | 
			
		||||
        public long PotSize { get; set; } = 0;
 | 
			
		||||
        //[Option('t', "type", Required = false, Default = "reaction", HelpText = "Type of the event. reaction, gamestatus or joinserver.")]
 | 
			
		||||
        //public string TypeString { get; set; } = "reaction";
 | 
			
		||||
        [Option('d', "duration", Required = false, Default = 24, HelpText = "Number of hours the event should run for. Default 24.")]
 | 
			
		||||
        public int Hours { get; set; } = 24;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        public void NormalizeOptions()
 | 
			
		||||
        {
 | 
			
		||||
            if (Amount < 0)
 | 
			
		||||
                Amount = 100;
 | 
			
		||||
            if (PotSize < 0)
 | 
			
		||||
                PotSize = 0;
 | 
			
		||||
            if (Hours <= 0)
 | 
			
		||||
                Hours = 24;
 | 
			
		||||
            if (PotSize != 0 && PotSize < Amount)
 | 
			
		||||
                PotSize = 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										210
									
								
								src/NadekoBot/Modules/Gambling/Common/Events/GameStatusEvent.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/NadekoBot/Modules/Gambling/Common/Events/GameStatusEvent.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Events
 | 
			
		||||
{
 | 
			
		||||
    public class GameStatusEvent : ICurrencyEvent
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly IGuild _guild;
 | 
			
		||||
        private IUserMessage _msg;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly long _amount;
 | 
			
		||||
 | 
			
		||||
        private long PotSize { get; set; }
 | 
			
		||||
        public bool Stopped { get; private set; }
 | 
			
		||||
        public bool PotEmptied { get; private set; } = false;
 | 
			
		||||
 | 
			
		||||
        private readonly Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> _embedFunc;
 | 
			
		||||
        private readonly bool _isPotLimited;
 | 
			
		||||
        private readonly ITextChannel _channel;
 | 
			
		||||
        private readonly ConcurrentHashSet<ulong> _awardedUsers = new ConcurrentHashSet<ulong>();
 | 
			
		||||
        private readonly ConcurrentQueue<ulong> _toAward = new ConcurrentQueue<ulong>();
 | 
			
		||||
        private readonly Timer _t;
 | 
			
		||||
        private readonly Timer _timeout = null;
 | 
			
		||||
        private readonly EventOptions _opts;
 | 
			
		||||
 | 
			
		||||
        private readonly string _code;
 | 
			
		||||
 | 
			
		||||
        public event Func<ulong, Task> OnEnded;
 | 
			
		||||
 | 
			
		||||
        private readonly char[] _sneakyGameStatusChars = Enumerable.Range(48, 10)
 | 
			
		||||
            .Concat(Enumerable.Range(65, 26))
 | 
			
		||||
            .Concat(Enumerable.Range(97, 26))
 | 
			
		||||
            .Select(x => (char)x)
 | 
			
		||||
            .ToArray();
 | 
			
		||||
 | 
			
		||||
        public GameStatusEvent(DiscordSocketClient client, ICurrencyService cs,SocketGuild g, ITextChannel ch,
 | 
			
		||||
            EventOptions opt, Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> embedFunc)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _guild = g;
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
            _amount = opt.Amount;
 | 
			
		||||
            PotSize = opt.PotSize;
 | 
			
		||||
            _embedFunc = embedFunc;
 | 
			
		||||
            _isPotLimited = PotSize > 0;
 | 
			
		||||
            _channel = ch;
 | 
			
		||||
            _opts = opt;
 | 
			
		||||
            // generate code
 | 
			
		||||
            _code = new string(_sneakyGameStatusChars.Shuffle().Take(5).ToArray());
 | 
			
		||||
 | 
			
		||||
            _t = new Timer(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2));
 | 
			
		||||
            if (_opts.Hours > 0)
 | 
			
		||||
            {
 | 
			
		||||
                _timeout = new Timer(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void EventTimeout(object state)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = StopEvent();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async void OnTimerTick(object state)
 | 
			
		||||
        {
 | 
			
		||||
            var potEmpty = PotEmptied;
 | 
			
		||||
            List<ulong> toAward = new List<ulong>();
 | 
			
		||||
            while (_toAward.TryDequeue(out var x))
 | 
			
		||||
            {
 | 
			
		||||
                toAward.Add(x);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!toAward.Any())
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _cs.AddBulkAsync(toAward,
 | 
			
		||||
                    toAward.Select(x => "GameStatus Event"),
 | 
			
		||||
                    toAward.Select(x => _amount),
 | 
			
		||||
                    gamble: true).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (_isPotLimited)
 | 
			
		||||
                {
 | 
			
		||||
                    await _msg.ModifyAsync(m =>
 | 
			
		||||
                    {
 | 
			
		||||
                        m.Embed = GetEmbed(PotSize).Build();
 | 
			
		||||
                    }, new RequestOptions() { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Log.Information("Awarded {0} users {1} currency.{2}",
 | 
			
		||||
                    toAward.Count,
 | 
			
		||||
                    _amount,
 | 
			
		||||
                    _isPotLimited ? $" {PotSize} left." : "");
 | 
			
		||||
 | 
			
		||||
                if (potEmpty)
 | 
			
		||||
                {
 | 
			
		||||
                    var _ = StopEvent();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error in OnTimerTick in gamestatusevent");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task StartEvent()
 | 
			
		||||
        {
 | 
			
		||||
            _msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)).ConfigureAwait(false);
 | 
			
		||||
            await _client.SetGameAsync(_code).ConfigureAwait(false);
 | 
			
		||||
            _client.MessageDeleted += OnMessageDeleted;
 | 
			
		||||
            _client.MessageReceived += HandleMessage;
 | 
			
		||||
            _t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private EmbedBuilder GetEmbed(long pot)
 | 
			
		||||
        {
 | 
			
		||||
            return _embedFunc(CurrencyEvent.Type.GameStatus, _opts, pot);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task OnMessageDeleted(Cacheable<IMessage, ulong> msg, ISocketMessageChannel _)
 | 
			
		||||
        {
 | 
			
		||||
            if (msg.Id == _msg.Id)
 | 
			
		||||
            {
 | 
			
		||||
                await StopEvent().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly object stopLock = new object();
 | 
			
		||||
        public async Task StopEvent()
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Yield();
 | 
			
		||||
            lock (stopLock)
 | 
			
		||||
            {
 | 
			
		||||
                if (Stopped)
 | 
			
		||||
                    return;
 | 
			
		||||
                Stopped = true;
 | 
			
		||||
                _client.MessageDeleted -= OnMessageDeleted;
 | 
			
		||||
                _client.MessageReceived -= HandleMessage;
 | 
			
		||||
                _client.SetGameAsync(null);
 | 
			
		||||
                _t.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                _timeout?.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                try { var _ = _msg.DeleteAsync(); } catch { }
 | 
			
		||||
                var os = OnEnded(_guild.Id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task HandleMessage(SocketMessage msg)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                if (!(msg.Author is IGuildUser gu) // no unknown users, as they could be bots, or alts
 | 
			
		||||
                    || gu.IsBot // no bots
 | 
			
		||||
                    || msg.Content != _code // code has to be the same
 | 
			
		||||
                    || (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5) // no recently created accounts
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                // there has to be money left in the pot
 | 
			
		||||
                // and the user wasn't rewarded
 | 
			
		||||
                if (_awardedUsers.Add(msg.Author.Id) && TryTakeFromPot())
 | 
			
		||||
                {
 | 
			
		||||
                    _toAward.Enqueue(msg.Author.Id);
 | 
			
		||||
                    if (_isPotLimited && PotSize < _amount)
 | 
			
		||||
                        PotEmptied = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await msg.DeleteAsync(new RequestOptions()
 | 
			
		||||
                    {
 | 
			
		||||
                        RetryMode = RetryMode.AlwaysFail
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
            });
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly object potLock = new object();
 | 
			
		||||
        private bool TryTakeFromPot()
 | 
			
		||||
        {
 | 
			
		||||
            if (_isPotLimited)
 | 
			
		||||
            {
 | 
			
		||||
                lock (potLock)
 | 
			
		||||
                {
 | 
			
		||||
                    if (PotSize < _amount)
 | 
			
		||||
                        return false;
 | 
			
		||||
 | 
			
		||||
                    PotSize -= _amount;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common
 | 
			
		||||
{
 | 
			
		||||
    public interface ICurrencyEvent
 | 
			
		||||
    {
 | 
			
		||||
        event Func<ulong, Task> OnEnded;
 | 
			
		||||
        Task StopEvent();
 | 
			
		||||
        Task StartEvent();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										210
									
								
								src/NadekoBot/Modules/Gambling/Common/Events/ReactionEvent.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/NadekoBot/Modules/Gambling/Common/Events/ReactionEvent.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Events
 | 
			
		||||
{
 | 
			
		||||
    public class ReactionEvent : ICurrencyEvent
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly IGuild _guild;
 | 
			
		||||
        private IUserMessage _msg;
 | 
			
		||||
        private IEmote _emote;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly long _amount;
 | 
			
		||||
 | 
			
		||||
        private long PotSize { get; set; }
 | 
			
		||||
        public bool Stopped { get; private set; }
 | 
			
		||||
        public bool PotEmptied { get; private set; } = false;
 | 
			
		||||
 | 
			
		||||
        private readonly Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> _embedFunc;
 | 
			
		||||
        private readonly bool _isPotLimited;
 | 
			
		||||
        private readonly ITextChannel _channel;
 | 
			
		||||
        private readonly ConcurrentHashSet<ulong> _awardedUsers = new ConcurrentHashSet<ulong>();
 | 
			
		||||
        private readonly ConcurrentQueue<ulong> _toAward = new ConcurrentQueue<ulong>();
 | 
			
		||||
        private readonly Timer _t;
 | 
			
		||||
        private readonly Timer _timeout = null;
 | 
			
		||||
        private readonly bool _noRecentlyJoinedServer;
 | 
			
		||||
        private readonly EventOptions _opts;
 | 
			
		||||
        private readonly GamblingConfig _config;
 | 
			
		||||
 | 
			
		||||
        public event Func<ulong, Task> OnEnded;
 | 
			
		||||
 | 
			
		||||
        public ReactionEvent(DiscordSocketClient client, ICurrencyService cs,
 | 
			
		||||
            SocketGuild g, ITextChannel ch, EventOptions opt, GamblingConfig config,
 | 
			
		||||
            Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> embedFunc)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _guild = g;
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
            _amount = opt.Amount;
 | 
			
		||||
            PotSize = opt.PotSize;
 | 
			
		||||
            _embedFunc = embedFunc;
 | 
			
		||||
            _isPotLimited = PotSize > 0;
 | 
			
		||||
            _channel = ch;
 | 
			
		||||
            _noRecentlyJoinedServer = false;
 | 
			
		||||
            _opts = opt;
 | 
			
		||||
            _config = config;
 | 
			
		||||
 | 
			
		||||
            _t = new Timer(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2));
 | 
			
		||||
            if (_opts.Hours > 0)
 | 
			
		||||
            {
 | 
			
		||||
                _timeout = new Timer(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void EventTimeout(object state)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = StopEvent();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async void OnTimerTick(object state)
 | 
			
		||||
        {
 | 
			
		||||
            var potEmpty = PotEmptied;
 | 
			
		||||
            List<ulong> toAward = new List<ulong>();
 | 
			
		||||
            while (_toAward.TryDequeue(out var x))
 | 
			
		||||
            {
 | 
			
		||||
                toAward.Add(x);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!toAward.Any())
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _cs.AddBulkAsync(toAward,
 | 
			
		||||
                    toAward.Select(x => "Reaction Event"),
 | 
			
		||||
                    toAward.Select(x => _amount),
 | 
			
		||||
                    gamble: true).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (_isPotLimited)
 | 
			
		||||
                {
 | 
			
		||||
                    await _msg.ModifyAsync(m =>
 | 
			
		||||
                    {
 | 
			
		||||
                        m.Embed = GetEmbed(PotSize).Build();
 | 
			
		||||
                    }, new RequestOptions() { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Log.Information("Awarded {0} users {1} currency.{2}",
 | 
			
		||||
                    toAward.Count,
 | 
			
		||||
                    _amount,
 | 
			
		||||
                    _isPotLimited ? $" {PotSize} left." : "");
 | 
			
		||||
 | 
			
		||||
                if (potEmpty)
 | 
			
		||||
                {
 | 
			
		||||
                    var _ = StopEvent();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error adding bulk currency to users");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task StartEvent()
 | 
			
		||||
        {
 | 
			
		||||
            if (Emote.TryParse(_config.Currency.Sign, out var emote))
 | 
			
		||||
            {
 | 
			
		||||
                _emote = emote;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _emote = new Emoji(_config.Currency.Sign);
 | 
			
		||||
            }
 | 
			
		||||
            _msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)).ConfigureAwait(false);
 | 
			
		||||
            await _msg.AddReactionAsync(_emote).ConfigureAwait(false);
 | 
			
		||||
            _client.MessageDeleted += OnMessageDeleted;
 | 
			
		||||
            _client.ReactionAdded += HandleReaction;
 | 
			
		||||
            _t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private EmbedBuilder GetEmbed(long pot)
 | 
			
		||||
        {
 | 
			
		||||
            return _embedFunc(CurrencyEvent.Type.Reaction, _opts, pot);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task OnMessageDeleted(Cacheable<IMessage, ulong> msg, ISocketMessageChannel _)
 | 
			
		||||
        {
 | 
			
		||||
            if (msg.Id == _msg.Id)
 | 
			
		||||
            {
 | 
			
		||||
                await StopEvent().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly object stopLock = new object();
 | 
			
		||||
        public async Task StopEvent()
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Yield();
 | 
			
		||||
            lock (stopLock)
 | 
			
		||||
            {
 | 
			
		||||
                if (Stopped)
 | 
			
		||||
                    return;
 | 
			
		||||
                Stopped = true;
 | 
			
		||||
                _client.MessageDeleted -= OnMessageDeleted;
 | 
			
		||||
                _client.ReactionAdded -= HandleReaction;
 | 
			
		||||
                _t.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                _timeout?.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                try { var _ = _msg.DeleteAsync(); } catch { }
 | 
			
		||||
                var os = OnEnded(_guild.Id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task HandleReaction(Cacheable<IUserMessage, ulong> msg,
 | 
			
		||||
            ISocketMessageChannel ch, SocketReaction r)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = Task.Run(() =>
 | 
			
		||||
            {
 | 
			
		||||
                if (_emote.Name != r.Emote.Name)
 | 
			
		||||
                    return;
 | 
			
		||||
                var gu = (r.User.IsSpecified ? r.User.Value : null) as IGuildUser;
 | 
			
		||||
                if (gu == null // no unknown users, as they could be bots, or alts
 | 
			
		||||
                    || msg.Id != _msg.Id // same message
 | 
			
		||||
                    || gu.IsBot // no bots
 | 
			
		||||
                    || (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5 // no recently created accounts
 | 
			
		||||
                    || (_noRecentlyJoinedServer && // if specified, no users who joined the server in the last 24h
 | 
			
		||||
                        (gu.JoinedAt == null || (DateTime.UtcNow - gu.JoinedAt.Value).TotalDays < 1)))  // and no users for who we don't know when they joined
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                // there has to be money left in the pot
 | 
			
		||||
                // and the user wasn't rewarded
 | 
			
		||||
                if (_awardedUsers.Add(r.UserId) && TryTakeFromPot())
 | 
			
		||||
                {
 | 
			
		||||
                    _toAward.Enqueue(r.UserId);
 | 
			
		||||
                    if (_isPotLimited && PotSize < _amount)
 | 
			
		||||
                        PotEmptied = true;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly object potLock = new object();
 | 
			
		||||
        private bool TryTakeFromPot()
 | 
			
		||||
        {
 | 
			
		||||
            if (_isPotLimited)
 | 
			
		||||
            {
 | 
			
		||||
                lock (potLock)
 | 
			
		||||
                {
 | 
			
		||||
                    if (PotSize < _amount)
 | 
			
		||||
                        return false;
 | 
			
		||||
 | 
			
		||||
                    PotSize -= _amount;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										293
									
								
								src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,293 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using NadekoBot.Common.Yml;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common
 | 
			
		||||
{
 | 
			
		||||
    public sealed class GamblingConfig
 | 
			
		||||
    {
 | 
			
		||||
        public GamblingConfig()
 | 
			
		||||
        {
 | 
			
		||||
            BetRoll = new BetRollConfig();
 | 
			
		||||
            WheelOfFortune = new WheelOfFortuneSettings();
 | 
			
		||||
            Waifu = new WaifuConfig();
 | 
			
		||||
            Currency = new CurrencyConfig();
 | 
			
		||||
            BetFlip = new BetFlipConfig();
 | 
			
		||||
            Generation = new GenerationConfig();
 | 
			
		||||
            Timely = new TimelyConfig();
 | 
			
		||||
            Decay = new DecayConfig();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"DO NOT CHANGE")]
 | 
			
		||||
        public int Version { get; set; } = 1;
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Currency settings")]
 | 
			
		||||
        public CurrencyConfig Currency { get; set; }
 | 
			
		||||
 | 
			
		||||
        [Comment(@"Minimum amount users can bet (>=0)")]
 | 
			
		||||
        public int MinBet { get; set; } = 0;
 | 
			
		||||
 | 
			
		||||
        [Comment(@"Maximum amount users can bet
 | 
			
		||||
Set 0 for unlimited")]
 | 
			
		||||
        public int MaxBet { get; set; } = 0;
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Settings for betflip command")]
 | 
			
		||||
        public BetFlipConfig BetFlip { get; set; }
 | 
			
		||||
 | 
			
		||||
        [Comment(@"Settings for betroll command")]
 | 
			
		||||
        public BetRollConfig BetRoll { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Automatic currency generation settings.")]
 | 
			
		||||
        public GenerationConfig Generation { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Settings for timely command 
 | 
			
		||||
(letting people claim X amount of currency every Y hours)")]
 | 
			
		||||
        public TimelyConfig Timely { get; set; }
 | 
			
		||||
 | 
			
		||||
        [Comment(@"How much will each user's owned currency decay over time.")]
 | 
			
		||||
        public DecayConfig Decay { get; set; }
 | 
			
		||||
        [Comment(@"Settings for Wheel Of Fortune command.")]
 | 
			
		||||
        public WheelOfFortuneSettings WheelOfFortune { get; set; }
 | 
			
		||||
        [Comment(@"Settings related to waifus")]
 | 
			
		||||
        public WaifuConfig Waifu { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Amount of currency selfhosters will get PER pledged dollar CENT.
 | 
			
		||||
1 = 100 currency per $. Used almost exclusively on public nadeko.")]
 | 
			
		||||
        public decimal PatreonCurrencyPerCent { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
        public class CurrencyConfig
 | 
			
		||||
        {
 | 
			
		||||
            [Comment(@"What is the emoji/character which represents the currency")]
 | 
			
		||||
            public string Sign { get; set; } = "🌸";
 | 
			
		||||
            
 | 
			
		||||
            [Comment(@"What is the name of the currency")]
 | 
			
		||||
            public string Name { get; set; } = "Nadeko Flower";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class TimelyConfig
 | 
			
		||||
        {
 | 
			
		||||
            [Comment(@"How much currency will the users get every time they run .timely command
 | 
			
		||||
setting to 0 or less will disable this feature")]
 | 
			
		||||
            public int Amount { get; set; } = 0;
 | 
			
		||||
 | 
			
		||||
            [Comment(@"How often (in hours) can users claim currency with .timely command
 | 
			
		||||
setting to 0 or less will disable this feature")]
 | 
			
		||||
            public int Cooldown { get; set; } = 24;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class BetFlipConfig
 | 
			
		||||
        {
 | 
			
		||||
            [Comment(@"Bet multiplier if user guesses correctly")]
 | 
			
		||||
            public decimal Multiplier { get; set; } = 1.95M;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class BetRollConfig
 | 
			
		||||
        {
 | 
			
		||||
            [Comment(@"When betroll is played, user will roll a number 0-100.
 | 
			
		||||
This setting will describe which multiplier is used for when the roll is higher than the given number.
 | 
			
		||||
Doesn't have to be ordered.")]
 | 
			
		||||
            public Pair[] Pairs { get; set; } = Array.Empty<Pair>();
 | 
			
		||||
 | 
			
		||||
            public BetRollConfig()
 | 
			
		||||
            {
 | 
			
		||||
                Pairs = new BetRollConfig.Pair[]
 | 
			
		||||
                {
 | 
			
		||||
                    new BetRollConfig.Pair(99, 10),
 | 
			
		||||
                    new BetRollConfig.Pair(90, 4),
 | 
			
		||||
                    new BetRollConfig.Pair(66, 2)
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public class Pair
 | 
			
		||||
            {
 | 
			
		||||
                
 | 
			
		||||
                public int WhenAbove { get; set; }
 | 
			
		||||
                
 | 
			
		||||
                public float MultiplyBy { get; set; }
 | 
			
		||||
 | 
			
		||||
                public Pair()
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                public Pair(int threshold, int multiplier)
 | 
			
		||||
                {
 | 
			
		||||
                    WhenAbove = threshold;
 | 
			
		||||
                    MultiplyBy = multiplier;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public class GenerationConfig
 | 
			
		||||
        {
 | 
			
		||||
            [Comment(@"when currency is generated, should it also have a random password
 | 
			
		||||
associated with it which users have to type after the .pick command
 | 
			
		||||
in order to get it")]
 | 
			
		||||
            public bool HasPassword { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
            [Comment(@"Every message sent has a certain % chance to generate the currency
 | 
			
		||||
specify the percentage here (1 being 100%, 0 being 0% - for example
 | 
			
		||||
default is 0.02, which is 2%")]
 | 
			
		||||
            public decimal Chance { get; set; } = 0.02M;
 | 
			
		||||
 | 
			
		||||
            [Comment(@"How many seconds have to pass for the next message to have a chance to spawn currency")]
 | 
			
		||||
            public int GenCooldown { get; set; } = 10;
 | 
			
		||||
 | 
			
		||||
            [Comment(@"Minimum amount of currency that can spawn")]
 | 
			
		||||
            public int MinAmount { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
            [Comment(@"Maximum amount of currency that can spawn.
 | 
			
		||||
 Set to the same value as MinAmount to always spawn the same amount")]
 | 
			
		||||
            public int MaxAmount { get; set; } = 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class DecayConfig
 | 
			
		||||
        {
 | 
			
		||||
            [Comment(@"Percentage of user's current currency which will be deducted every 24h. 
 | 
			
		||||
0 - 1 (1 is 100%, 0.5 50%, 0 disabled)")]
 | 
			
		||||
            public decimal Percent { get; set; } = 0;
 | 
			
		||||
 | 
			
		||||
            [Comment(@"Maximum amount of user's currency that can decay at each interval. 0 for unlimited.")]
 | 
			
		||||
            public int MaxDecay { get; set; } = 0;
 | 
			
		||||
 | 
			
		||||
            [Comment(@"Only users who have more than this amount will have their currency decay.")]
 | 
			
		||||
            public int MinThreshold { get; set; } = 99;
 | 
			
		||||
 | 
			
		||||
            [Comment(@"How often, in hours, does the decay run. Default is 24 hours")]
 | 
			
		||||
            public int HourInterval { get; set; } = 24;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class WheelOfFortuneSettings
 | 
			
		||||
        {
 | 
			
		||||
            [Comment(@"Self-Explanatory. Has to have 8 values, otherwise the command won't work.")]
 | 
			
		||||
            public decimal[] Multipliers { get; set; }
 | 
			
		||||
 | 
			
		||||
            public WheelOfFortuneSettings()
 | 
			
		||||
            {
 | 
			
		||||
                Multipliers = new decimal[]
 | 
			
		||||
                {
 | 
			
		||||
                    1.7M,
 | 
			
		||||
                    1.5M,
 | 
			
		||||
                    0.2M,
 | 
			
		||||
                    0.1M,
 | 
			
		||||
                    0.3M,
 | 
			
		||||
                    0.5M,
 | 
			
		||||
                    1.2M,
 | 
			
		||||
                    2.4M,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class WaifuConfig
 | 
			
		||||
        {
 | 
			
		||||
            [Comment(@"Minimum price a waifu can have")]
 | 
			
		||||
            public int MinPrice { get; set; } = 50;
 | 
			
		||||
            public MultipliersData Multipliers { get; set; } = new MultipliersData();
 | 
			
		||||
 | 
			
		||||
            [Comment(@"List of items available for gifting.")]
 | 
			
		||||
            public List<WaifuItemModel> Items { get; set; } = new List<WaifuItemModel>();
 | 
			
		||||
 | 
			
		||||
            public WaifuConfig()
 | 
			
		||||
            {
 | 
			
		||||
                Items = new List<WaifuItemModel>()
 | 
			
		||||
                {
 | 
			
		||||
                    new WaifuItemModel("🥔", 5, "Potato"),
 | 
			
		||||
                    new WaifuItemModel("🍪", 10, "Cookie"),
 | 
			
		||||
                    new WaifuItemModel("🥖", 20, "Bread"),
 | 
			
		||||
                    new WaifuItemModel("🍭", 30, "Lollipop"),
 | 
			
		||||
                    new WaifuItemModel("🌹", 50, "Rose"),
 | 
			
		||||
                    new WaifuItemModel("🍺", 70, "Beer"),
 | 
			
		||||
                    new WaifuItemModel("🌮", 85, "Taco"),
 | 
			
		||||
                    new WaifuItemModel("💌", 100, "LoveLetter"),
 | 
			
		||||
                    new WaifuItemModel("🥛", 125, "Milk"),
 | 
			
		||||
                    new WaifuItemModel("🍕", 150, "Pizza"),
 | 
			
		||||
                    new WaifuItemModel("🍫", 200, "Chocolate"),
 | 
			
		||||
                    new WaifuItemModel("🍦", 250, "Icecream"),
 | 
			
		||||
                    new WaifuItemModel("🍣", 300, "Sushi"),
 | 
			
		||||
                    new WaifuItemModel("🍚", 400, "Rice"),
 | 
			
		||||
                    new WaifuItemModel("🍉", 500, "Watermelon"),
 | 
			
		||||
                    new WaifuItemModel("🍱", 600, "Bento"),
 | 
			
		||||
                    new WaifuItemModel("🎟", 800, "MovieTicket"),
 | 
			
		||||
                    new WaifuItemModel("🍰", 1000, "Cake"),
 | 
			
		||||
                    new WaifuItemModel("📔", 1500, "Book"),
 | 
			
		||||
                    new WaifuItemModel("🐱", 2000, "Cat"),
 | 
			
		||||
                    new WaifuItemModel("🐶", 2001, "Dog"),
 | 
			
		||||
                    new WaifuItemModel("🐼", 2500, "Panda"),
 | 
			
		||||
                    new WaifuItemModel("💄", 3000, "Lipstick"),
 | 
			
		||||
                    new WaifuItemModel("👛", 3500, "Purse"),
 | 
			
		||||
                    new WaifuItemModel("📱", 4000, "iPhone"),
 | 
			
		||||
                    new WaifuItemModel("👗", 4500, "Dress"),
 | 
			
		||||
                    new WaifuItemModel("💻", 5000, "Laptop"),
 | 
			
		||||
                    new WaifuItemModel("🎻", 7500, "Violin"),
 | 
			
		||||
                    new WaifuItemModel("🎹", 8000, "Piano"),
 | 
			
		||||
                    new WaifuItemModel("🚗", 9000, "Car"),
 | 
			
		||||
                    new WaifuItemModel("💍", 10000, "Ring"),
 | 
			
		||||
                    new WaifuItemModel("🛳", 12000, "Ship"),
 | 
			
		||||
                    new WaifuItemModel("🏠", 15000, "House"),
 | 
			
		||||
                    new WaifuItemModel("🚁", 20000, "Helicopter"),
 | 
			
		||||
                    new WaifuItemModel("🚀", 30000, "Spaceship"),
 | 
			
		||||
                    new WaifuItemModel("🌕", 50000, "Moon")
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            public class MultipliersData
 | 
			
		||||
            {
 | 
			
		||||
                [Comment(@"Multiplier for waifureset. Default 150.
 | 
			
		||||
Formula (at the time of writing this): 
 | 
			
		||||
price = (waifu_price * 1.25f) + ((number_of_divorces + changes_of_heart + 2) * WaifuReset) rounded up")]
 | 
			
		||||
                public int WaifuReset { get; set; } = 150;
 | 
			
		||||
 | 
			
		||||
                [Comment(@"The minimum amount of currency that you have to pay 
 | 
			
		||||
in order to buy a waifu who doesn't have a crush on you.
 | 
			
		||||
Default is 1.1
 | 
			
		||||
Example: If a waifu is worth 100, you will have to pay at least 100 * NormalClaim currency to claim her.
 | 
			
		||||
(100 * 1.1 = 110)")]
 | 
			
		||||
                public decimal NormalClaim { get; set; } = 1.1m;
 | 
			
		||||
 | 
			
		||||
                [Comment(@"The minimum amount of currency that you have to pay 
 | 
			
		||||
in order to buy a waifu that has a crush on you.
 | 
			
		||||
Default is 0.88
 | 
			
		||||
Example: If a waifu is worth 100, you will have to pay at least 100 * CrushClaim currency to claim her.
 | 
			
		||||
(100 * 0.88 = 88)")]
 | 
			
		||||
                public decimal CrushClaim { get; set; } = 0.88M;
 | 
			
		||||
 | 
			
		||||
                [Comment(@"When divorcing a waifu, her new value will be her current value multiplied by this number.
 | 
			
		||||
Default 0.75 (meaning will lose 25% of her value)")]
 | 
			
		||||
                public decimal DivorceNewValue { get; set; } = 0.75M;
 | 
			
		||||
 | 
			
		||||
                [Comment(@"All gift prices will be multiplied by this number.
 | 
			
		||||
Default 1 (meaning no effect)")]
 | 
			
		||||
                public decimal AllGiftPrices { get; set; } = 1.0M;
 | 
			
		||||
 | 
			
		||||
                [Comment(@"What percentage of the value of the gift will a waifu gain when she's gifted.
 | 
			
		||||
Default 0.95 (meaning 95%)
 | 
			
		||||
Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095)")]
 | 
			
		||||
                public decimal GiftEffect { get; set; } = 0.95M;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public class WaifuItemModel
 | 
			
		||||
    {
 | 
			
		||||
        public string ItemEmoji { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        public int Price { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        public WaifuItemModel()
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public WaifuItemModel(string itemEmoji, int price, string name)
 | 
			
		||||
        {
 | 
			
		||||
            ItemEmoji = itemEmoji;
 | 
			
		||||
            Price = price;
 | 
			
		||||
            Name = name;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override string ToString() => Name;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Modules;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common
 | 
			
		||||
{
 | 
			
		||||
    public abstract class GamblingModule<TService> : NadekoModule<TService>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly Lazy<GamblingConfig> _lazyConfig;
 | 
			
		||||
        protected GamblingConfig _config => _lazyConfig.Value;
 | 
			
		||||
        protected string CurrencySign => _config.Currency.Sign;
 | 
			
		||||
        protected string CurrencyName => _config.Currency.Name;
 | 
			
		||||
        
 | 
			
		||||
        protected GamblingModule(GamblingConfigService gambService)
 | 
			
		||||
        {
 | 
			
		||||
            _lazyConfig = new Lazy<GamblingConfig>(() => gambService.Data);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<bool> InternalCheckBet(long amount)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount < 1)
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            if (amount < _config.MinBet)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("min_bet_limit", 
 | 
			
		||||
                    Format.Bold(_config.MinBet.ToString()) + CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            if (_config.MaxBet > 0 && amount > _config.MaxBet)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("max_bet_limit", 
 | 
			
		||||
                    Format.Bold(_config.MaxBet.ToString()) + CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected Task<bool> CheckBetMandatory(long amount)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount < 1)
 | 
			
		||||
            {
 | 
			
		||||
                return Task.FromResult(false);
 | 
			
		||||
            }
 | 
			
		||||
            return InternalCheckBet(amount);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected Task<bool> CheckBetOptional(long amount)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount == 0)
 | 
			
		||||
            {
 | 
			
		||||
                return Task.FromResult(true);
 | 
			
		||||
            }
 | 
			
		||||
            return InternalCheckBet(amount);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public abstract class GamblingSubmodule<TService> : GamblingModule<TService>
 | 
			
		||||
    {
 | 
			
		||||
        protected GamblingSubmodule(GamblingConfigService gamblingConfService) : base(gamblingConfService)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/NadekoBot/Modules/Gambling/Common/Payout.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/NadekoBot/Modules/Gambling/Common/Payout.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common
 | 
			
		||||
{
 | 
			
		||||
    public class Payout
 | 
			
		||||
    {
 | 
			
		||||
        public string User { get; set; }
 | 
			
		||||
        public int Amount { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								src/NadekoBot/Modules/Gambling/Common/RollDuelGame.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/NadekoBot/Modules/Gambling/Common/RollDuelGame.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common
 | 
			
		||||
{
 | 
			
		||||
    public class RollDuelGame
 | 
			
		||||
    {
 | 
			
		||||
        public ulong P1 { get; }
 | 
			
		||||
        public ulong P2 { get; }
 | 
			
		||||
 | 
			
		||||
        private readonly ulong _botId;
 | 
			
		||||
 | 
			
		||||
        public long Amount { get; }
 | 
			
		||||
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
 | 
			
		||||
        public enum State
 | 
			
		||||
        {
 | 
			
		||||
            Waiting,
 | 
			
		||||
            Running,
 | 
			
		||||
            Ended,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum Reason
 | 
			
		||||
        {
 | 
			
		||||
            Normal,
 | 
			
		||||
            NoFunds,
 | 
			
		||||
            Timeout,
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private readonly Timer _timeoutTimer;
 | 
			
		||||
        private readonly NadekoRandom _rng = new NadekoRandom();
 | 
			
		||||
        private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
 | 
			
		||||
        
 | 
			
		||||
        public event Func<RollDuelGame, Task> OnGameTick;
 | 
			
		||||
        public event Func<RollDuelGame, Reason, Task> OnEnded;
 | 
			
		||||
 | 
			
		||||
        public List<(int, int)> Rolls { get; } = new List<(int, int)>();
 | 
			
		||||
        public State CurrentState { get; private set; }
 | 
			
		||||
        public ulong Winner { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public RollDuelGame(ICurrencyService cs, ulong botId, ulong p1, ulong p2, long amount)
 | 
			
		||||
        {
 | 
			
		||||
            this.P1 = p1;
 | 
			
		||||
            this.P2 = p2;
 | 
			
		||||
            this._botId = botId;
 | 
			
		||||
            this.Amount = amount;
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
 | 
			
		||||
            _timeoutTimer = new Timer(async delegate
 | 
			
		||||
            {
 | 
			
		||||
                await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (CurrentState != State.Waiting)
 | 
			
		||||
                        return;
 | 
			
		||||
                    CurrentState = State.Ended;
 | 
			
		||||
                    await (OnEnded?.Invoke(this, Reason.Timeout)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    _locker.Release();
 | 
			
		||||
                }
 | 
			
		||||
            }, null, TimeSpan.FromSeconds(15), TimeSpan.FromMilliseconds(-1));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task StartGame()
 | 
			
		||||
        {
 | 
			
		||||
            await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (CurrentState != State.Waiting)
 | 
			
		||||
                    return;
 | 
			
		||||
                _timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                CurrentState = State.Running;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _locker.Release();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if(!await _cs.RemoveAsync(P1, "Roll Duel", Amount).ConfigureAwait(false))
 | 
			
		||||
            {
 | 
			
		||||
                await (OnEnded?.Invoke(this, Reason.NoFunds)).ConfigureAwait(false);
 | 
			
		||||
                CurrentState = State.Ended;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if(!await _cs.RemoveAsync(P2, "Roll Duel", Amount).ConfigureAwait(false))
 | 
			
		||||
            {
 | 
			
		||||
                await _cs.AddAsync(P1, "Roll Duel - refund", Amount).ConfigureAwait(false);
 | 
			
		||||
                await (OnEnded?.Invoke(this, Reason.NoFunds)).ConfigureAwait(false);
 | 
			
		||||
                CurrentState = State.Ended;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            int n1, n2;
 | 
			
		||||
            do
 | 
			
		||||
            {
 | 
			
		||||
                n1 = _rng.Next(0, 5);
 | 
			
		||||
                n2 = _rng.Next(0, 5);
 | 
			
		||||
                Rolls.Add((n1, n2));
 | 
			
		||||
                if (n1 != n2)
 | 
			
		||||
                {
 | 
			
		||||
                    if (n1 > n2)
 | 
			
		||||
                    {
 | 
			
		||||
                        Winner = P1;                                                                                                                                                                                                                                                                                                
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        Winner = P2;
 | 
			
		||||
                    }
 | 
			
		||||
                    var won = (long)(Amount * 2 * 0.98f);
 | 
			
		||||
                    await _cs.AddAsync(Winner, "Roll Duel win", won)
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    await _cs.AddAsync(_botId, "Roll Duel fee", Amount * 2 - won)
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                try { await (OnGameTick?.Invoke(this)).ConfigureAwait(false); } catch { }
 | 
			
		||||
                await Task.Delay(2500).ConfigureAwait(false);
 | 
			
		||||
                if (n1 != n2)
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            while (true);
 | 
			
		||||
            CurrentState = State.Ended;
 | 
			
		||||
            await (OnEnded?.Invoke(this, Reason.Normal)).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public struct RollDuelChallenge
 | 
			
		||||
    {
 | 
			
		||||
        public ulong Player1 { get; set; }
 | 
			
		||||
        public ulong Player2 { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								src/NadekoBot/Modules/Gambling/Common/Waifu/AffinityTitle.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/NadekoBot/Modules/Gambling/Common/Waifu/AffinityTitle.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
 | 
			
		||||
{
 | 
			
		||||
    public enum AffinityTitle
 | 
			
		||||
    {
 | 
			
		||||
        Pure,
 | 
			
		||||
        Faithful,
 | 
			
		||||
        Playful,
 | 
			
		||||
        Cheater,
 | 
			
		||||
        Tainted,
 | 
			
		||||
        Corrupted,
 | 
			
		||||
        Lewd,
 | 
			
		||||
        Sloot,
 | 
			
		||||
        Depraved,
 | 
			
		||||
        Harlot
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/NadekoBot/Modules/Gambling/Common/Waifu/ClaimTitle.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/NadekoBot/Modules/Gambling/Common/Waifu/ClaimTitle.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
 | 
			
		||||
{
 | 
			
		||||
    public enum ClaimTitle
 | 
			
		||||
    {
 | 
			
		||||
        Lonely,
 | 
			
		||||
        Devoted,
 | 
			
		||||
        Rookie,
 | 
			
		||||
        Schemer,
 | 
			
		||||
        Dilettante,
 | 
			
		||||
        Intermediate,
 | 
			
		||||
        Seducer,
 | 
			
		||||
        Expert,
 | 
			
		||||
        Veteran,
 | 
			
		||||
        Incubis,
 | 
			
		||||
        Harem_King,
 | 
			
		||||
        Harem_God,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/NadekoBot/Modules/Gambling/Common/Waifu/DivorceResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/NadekoBot/Modules/Gambling/Common/Waifu/DivorceResult.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
 | 
			
		||||
{
 | 
			
		||||
    public enum DivorceResult
 | 
			
		||||
    {
 | 
			
		||||
        Success,
 | 
			
		||||
        SucessWithPenalty,
 | 
			
		||||
        NotYourWife,
 | 
			
		||||
        Cooldown
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
 | 
			
		||||
{
 | 
			
		||||
    public enum WaifuClaimResult
 | 
			
		||||
    {
 | 
			
		||||
        Success,
 | 
			
		||||
        NotEnoughFunds,
 | 
			
		||||
        InsufficientAmount
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
 | 
			
		||||
{
 | 
			
		||||
    public struct WaifuProfileTitle
 | 
			
		||||
    {
 | 
			
		||||
        public int Count { get; }
 | 
			
		||||
        public string Title { get; }
 | 
			
		||||
 | 
			
		||||
        public WaifuProfileTitle(int count, string title)
 | 
			
		||||
        {
 | 
			
		||||
            Count = count;
 | 
			
		||||
            Title = title;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common.WheelOfFortune
 | 
			
		||||
{
 | 
			
		||||
    public class WheelOfFortuneGame
 | 
			
		||||
    {
 | 
			
		||||
        public class Result
 | 
			
		||||
        {
 | 
			
		||||
            public int Index { get; set; }
 | 
			
		||||
            public long Amount { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly NadekoRandom _rng;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly long _bet;
 | 
			
		||||
        private readonly GamblingConfig _config;
 | 
			
		||||
        private readonly ulong _userId;
 | 
			
		||||
 | 
			
		||||
        public WheelOfFortuneGame(ulong userId, long bet, GamblingConfig config, ICurrencyService cs)
 | 
			
		||||
        {
 | 
			
		||||
            _rng = new NadekoRandom();
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
            _bet = bet;
 | 
			
		||||
            _config = config;
 | 
			
		||||
            _userId = userId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<Result> SpinAsync()
 | 
			
		||||
        {
 | 
			
		||||
            var result = _rng.Next(0, _config.WheelOfFortune.Multipliers.Length);
 | 
			
		||||
 | 
			
		||||
            var amount = (long)(_bet * _config.WheelOfFortune.Multipliers[result]);
 | 
			
		||||
 | 
			
		||||
            if (amount > 0)
 | 
			
		||||
                await _cs.AddAsync(_userId, "Wheel Of Fortune - won", amount, gamble: true).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            return new Result
 | 
			
		||||
            {
 | 
			
		||||
                Index = result,
 | 
			
		||||
                Amount = amount,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										401
									
								
								src/NadekoBot/Modules/Gambling/Connect4/Connect4.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										401
									
								
								src/NadekoBot/Modules/Gambling/Connect4/Connect4.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,401 @@
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common.Connect4
 | 
			
		||||
{
 | 
			
		||||
    public sealed class Connect4Game : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        public enum Phase
 | 
			
		||||
        {
 | 
			
		||||
            Joining, // waiting for second player to join
 | 
			
		||||
            P1Move,
 | 
			
		||||
            P2Move,
 | 
			
		||||
            Ended,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum Field //temporary most likely
 | 
			
		||||
        {
 | 
			
		||||
            Empty,
 | 
			
		||||
            P1,
 | 
			
		||||
            P2,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum Result
 | 
			
		||||
        {
 | 
			
		||||
            Draw,
 | 
			
		||||
            CurrentPlayerWon,
 | 
			
		||||
            OtherPlayerWon,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public const int NumberOfColumns = 7;
 | 
			
		||||
        public const int NumberOfRows = 6;
 | 
			
		||||
 | 
			
		||||
        public Phase CurrentPhase { get; private set; } = Phase.Joining;
 | 
			
		||||
 | 
			
		||||
        //state is bottom to top, left to right
 | 
			
		||||
        private readonly Field[] _gameState = new Field[NumberOfRows * NumberOfColumns];
 | 
			
		||||
        private readonly (ulong UserId, string Username)?[] _players = new(ulong, string)?[2];
 | 
			
		||||
 | 
			
		||||
        public ImmutableArray<Field> GameState => _gameState.ToImmutableArray();
 | 
			
		||||
        public ImmutableArray<(ulong UserId, string Username)?> Players => _players.ToImmutableArray();
 | 
			
		||||
 | 
			
		||||
        public (ulong UserId, string Username) CurrentPlayer => CurrentPhase == Phase.P1Move
 | 
			
		||||
            ? _players[0].Value
 | 
			
		||||
            : _players[1].Value;
 | 
			
		||||
 | 
			
		||||
        public (ulong UserId, string Username) OtherPlayer => CurrentPhase == Phase.P2Move
 | 
			
		||||
            ? _players[0].Value
 | 
			
		||||
            : _players[1].Value;
 | 
			
		||||
 | 
			
		||||
        //public event Func<Connect4Game, Task> OnGameStarted;
 | 
			
		||||
        public event Func<Connect4Game, Task> OnGameStateUpdated;
 | 
			
		||||
        public event Func<Connect4Game, Task> OnGameFailedToStart;
 | 
			
		||||
        public event Func<Connect4Game, Result, Task> OnGameEnded;
 | 
			
		||||
 | 
			
		||||
        private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
 | 
			
		||||
        private readonly Options _options;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly NadekoRandom _rng;
 | 
			
		||||
 | 
			
		||||
        private Timer _playerTimeoutTimer;
 | 
			
		||||
 | 
			
		||||
        /* [ ][ ][ ][ ][ ][ ]
 | 
			
		||||
         * [ ][ ][ ][ ][ ][ ]
 | 
			
		||||
         * [ ][ ][ ][ ][ ][ ]
 | 
			
		||||
         * [ ][ ][ ][ ][ ][ ]
 | 
			
		||||
         * [ ][ ][ ][ ][ ][ ]
 | 
			
		||||
         * [ ][ ][ ][ ][ ][ ]
 | 
			
		||||
         * [ ][ ][ ][ ][ ][ ]
 | 
			
		||||
         */
 | 
			
		||||
 | 
			
		||||
        public Connect4Game(ulong userId, string userName, Options options, ICurrencyService cs)
 | 
			
		||||
        {
 | 
			
		||||
            _players[0] = (userId, userName);
 | 
			
		||||
            _options = options;
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
 | 
			
		||||
            _rng = new NadekoRandom();
 | 
			
		||||
            for (int i = 0; i < NumberOfColumns * NumberOfRows; i++)
 | 
			
		||||
            {
 | 
			
		||||
                _gameState[i] = Field.Empty;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Initialize()
 | 
			
		||||
        {
 | 
			
		||||
            if (CurrentPhase != Phase.Joining)
 | 
			
		||||
                return;
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(15000).ConfigureAwait(false);
 | 
			
		||||
                await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (_players[1] == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        var __ = OnGameFailedToStart?.Invoke(this);
 | 
			
		||||
                        CurrentPhase = Phase.Ended;
 | 
			
		||||
                        await _cs.AddAsync(_players[0].Value.UserId, "Connect4-refund", _options.Bet, true).ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                finally { _locker.Release(); }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> Join(ulong userId, string userName, int bet)
 | 
			
		||||
        {
 | 
			
		||||
            await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (CurrentPhase != Phase.Joining) //can't join if its not a joining phase
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (_players[0].Value.UserId == userId) // same user can't join own game
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (bet != _options.Bet) // can't join if bet amount is not the same
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (!await _cs.RemoveAsync(userId, "Connect4-bet", bet, true).ConfigureAwait(false)) // user doesn't have enough money to gamble
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (_rng.Next(0, 2) == 0) //rolling from 0-1, if number is 0, join as first player
 | 
			
		||||
                {
 | 
			
		||||
                    _players[1] = _players[0];
 | 
			
		||||
                    _players[0] = (userId, userName);
 | 
			
		||||
                }
 | 
			
		||||
                else //else join as a second player
 | 
			
		||||
                    _players[1] = (userId, userName);
 | 
			
		||||
 | 
			
		||||
                CurrentPhase = Phase.P1Move; //start the game
 | 
			
		||||
                _playerTimeoutTimer = new Timer(async state =>
 | 
			
		||||
                {
 | 
			
		||||
                    await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        EndGame(Result.OtherPlayerWon, OtherPlayer.UserId);
 | 
			
		||||
                    }
 | 
			
		||||
                    finally { _locker.Release(); }
 | 
			
		||||
                }, null, TimeSpan.FromSeconds(_options.TurnTimer), TimeSpan.FromSeconds(_options.TurnTimer));
 | 
			
		||||
                var __ = OnGameStateUpdated?.Invoke(this);
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            finally { _locker.Release(); }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> Input(ulong userId, int inputCol)
 | 
			
		||||
        {
 | 
			
		||||
            await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                inputCol -= 1;
 | 
			
		||||
                if (CurrentPhase == Phase.Ended || CurrentPhase == Phase.Joining)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (!((_players[0].Value.UserId == userId && CurrentPhase == Phase.P1Move)
 | 
			
		||||
                    || (_players[1].Value.UserId == userId && CurrentPhase == Phase.P2Move)))
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (inputCol < 0 || inputCol > NumberOfColumns) //invalid input
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                if (IsColumnFull(inputCol)) //can't play there event?
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                var start = NumberOfRows * inputCol;
 | 
			
		||||
                for (int i = start; i < start + NumberOfRows; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_gameState[i] == Field.Empty)
 | 
			
		||||
                    {
 | 
			
		||||
                        _gameState[i] = GetPlayerPiece(userId);
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //check winnning condition
 | 
			
		||||
                // ok, i'll go from [0-2] in rows (and through all columns) and check upward if 4 are connected
 | 
			
		||||
 | 
			
		||||
                for (int i = 0; i < NumberOfRows - 3; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (CurrentPhase == Phase.Ended)
 | 
			
		||||
                        break;
 | 
			
		||||
 | 
			
		||||
                    for (int j = 0; j < NumberOfColumns; j++)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (CurrentPhase == Phase.Ended)
 | 
			
		||||
                            break;
 | 
			
		||||
 | 
			
		||||
                        var first = _gameState[i + j * NumberOfRows];
 | 
			
		||||
                        if (first != Field.Empty)
 | 
			
		||||
                        {
 | 
			
		||||
                            for (int k = 1; k < 4; k++)
 | 
			
		||||
                            {
 | 
			
		||||
                                var next = _gameState[i + k + j * NumberOfRows];
 | 
			
		||||
                                if (next == first)
 | 
			
		||||
                                {
 | 
			
		||||
                                    if (k == 3)
 | 
			
		||||
                                        EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
 | 
			
		||||
                                    else
 | 
			
		||||
                                        continue;
 | 
			
		||||
                                }
 | 
			
		||||
                                else break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // i'll go [0-1] in columns (and through all rows) and check to the right if 4 are connected
 | 
			
		||||
                for (int i = 0; i < NumberOfColumns - 3; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (CurrentPhase == Phase.Ended)
 | 
			
		||||
                        break;
 | 
			
		||||
 | 
			
		||||
                    for (int j = 0; j < NumberOfRows; j++)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (CurrentPhase == Phase.Ended)
 | 
			
		||||
                            break;
 | 
			
		||||
 | 
			
		||||
                        var first = _gameState[j + i * NumberOfRows];
 | 
			
		||||
                        if (first != Field.Empty)
 | 
			
		||||
                        {
 | 
			
		||||
                            for (int k = 1; k < 4; k++)
 | 
			
		||||
                            {
 | 
			
		||||
                                var next = _gameState[j + (i + k) * NumberOfRows];
 | 
			
		||||
                                if (next == first)
 | 
			
		||||
                                    if (k == 3)
 | 
			
		||||
                                        EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
 | 
			
		||||
                                    else
 | 
			
		||||
                                        continue;
 | 
			
		||||
                                else break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //need to check diagonal now
 | 
			
		||||
                for (int col = 0; col < NumberOfColumns; col++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (CurrentPhase == Phase.Ended)
 | 
			
		||||
                        break;
 | 
			
		||||
 | 
			
		||||
                    for (int row = 0; row < NumberOfRows; row++)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (CurrentPhase == Phase.Ended)
 | 
			
		||||
                            break;
 | 
			
		||||
 | 
			
		||||
                        var first = _gameState[row + col * NumberOfRows];
 | 
			
		||||
 | 
			
		||||
                        if (first != Field.Empty)
 | 
			
		||||
                        {
 | 
			
		||||
                            var same = 1;
 | 
			
		||||
 | 
			
		||||
                            //top left
 | 
			
		||||
                            for (int i = 1; i < 4; i++)
 | 
			
		||||
                            {
 | 
			
		||||
                                //while going top left, rows are increasing, columns are decreasing
 | 
			
		||||
                                var curRow = row + i;
 | 
			
		||||
                                var curCol = col - i;
 | 
			
		||||
 | 
			
		||||
                                //check if current values are in range
 | 
			
		||||
                                if (curRow >= NumberOfRows || curRow < 0)
 | 
			
		||||
                                    break;
 | 
			
		||||
                                if (curCol < 0 || curCol >= NumberOfColumns)
 | 
			
		||||
                                    break;
 | 
			
		||||
 | 
			
		||||
                                var cur = _gameState[curRow + curCol * NumberOfRows];
 | 
			
		||||
                                if (cur == first)
 | 
			
		||||
                                    same++;
 | 
			
		||||
                                else break;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (same == 4)
 | 
			
		||||
                            {
 | 
			
		||||
                                EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            same = 1;
 | 
			
		||||
 | 
			
		||||
                            //top right
 | 
			
		||||
                            for (int i = 1; i < 4; i++)
 | 
			
		||||
                            {
 | 
			
		||||
                                //while going top right, rows are increasing, columns are increasing
 | 
			
		||||
                                var curRow = row + i;
 | 
			
		||||
                                var curCol = col + i;
 | 
			
		||||
 | 
			
		||||
                                //check if current values are in range
 | 
			
		||||
                                if (curRow >= NumberOfRows || curRow < 0)
 | 
			
		||||
                                    break;
 | 
			
		||||
                                if (curCol < 0 || curCol >= NumberOfColumns)
 | 
			
		||||
                                    break;
 | 
			
		||||
 | 
			
		||||
                                var cur = _gameState[curRow + curCol * NumberOfRows];
 | 
			
		||||
                                if (cur == first)
 | 
			
		||||
                                    same++;
 | 
			
		||||
                                else break;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (same == 4)
 | 
			
		||||
                            {
 | 
			
		||||
                                EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //check draw? if it's even possible
 | 
			
		||||
                if (_gameState.All(x => x != Field.Empty))
 | 
			
		||||
                {
 | 
			
		||||
                    EndGame(Result.Draw, null);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (CurrentPhase != Phase.Ended)
 | 
			
		||||
                {
 | 
			
		||||
                    if (CurrentPhase == Phase.P1Move)
 | 
			
		||||
                        CurrentPhase = Phase.P2Move;
 | 
			
		||||
                    else
 | 
			
		||||
                        CurrentPhase = Phase.P1Move;
 | 
			
		||||
 | 
			
		||||
                    ResetTimer();
 | 
			
		||||
                }
 | 
			
		||||
                var _ = OnGameStateUpdated?.Invoke(this);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            finally { _locker.Release(); }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void ResetTimer()
 | 
			
		||||
        {
 | 
			
		||||
            _playerTimeoutTimer.Change(TimeSpan.FromSeconds(_options.TurnTimer), TimeSpan.FromSeconds(_options.TurnTimer));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void EndGame(Result result, ulong? winId)
 | 
			
		||||
        {
 | 
			
		||||
            if (CurrentPhase == Phase.Ended)
 | 
			
		||||
                return;
 | 
			
		||||
            var _ = OnGameEnded?.Invoke(this, result);
 | 
			
		||||
            CurrentPhase = Phase.Ended;
 | 
			
		||||
 | 
			
		||||
            if (result == Result.Draw)
 | 
			
		||||
            {
 | 
			
		||||
                _cs.AddAsync(CurrentPlayer.UserId, "Connect4-draw", this._options.Bet, true);
 | 
			
		||||
                _cs.AddAsync(OtherPlayer.UserId, "Connect4-draw", this._options.Bet, true);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (winId != null)
 | 
			
		||||
                _cs.AddAsync(winId.Value, "Connnect4-win", (long)(this._options.Bet * 1.98), true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Field GetPlayerPiece(ulong userId) => _players[0].Value.UserId == userId
 | 
			
		||||
            ? Field.P1
 | 
			
		||||
            : Field.P2;
 | 
			
		||||
 | 
			
		||||
        //column is full if there are no empty fields
 | 
			
		||||
        private bool IsColumnFull(int column)
 | 
			
		||||
        {
 | 
			
		||||
            var start = NumberOfRows * column;
 | 
			
		||||
            for (int i = start; i < start + NumberOfRows; i++)
 | 
			
		||||
            {
 | 
			
		||||
                if (_gameState[i] == Field.Empty)
 | 
			
		||||
                    return false;
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            OnGameFailedToStart = null;
 | 
			
		||||
            OnGameStateUpdated = null;
 | 
			
		||||
            OnGameEnded = null;
 | 
			
		||||
            _playerTimeoutTimer?.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        public class Options : INadekoCommandOptions
 | 
			
		||||
        {
 | 
			
		||||
            public void NormalizeOptions()
 | 
			
		||||
            {
 | 
			
		||||
                if (TurnTimer < 5 || TurnTimer > 60)
 | 
			
		||||
                    TurnTimer = 15;
 | 
			
		||||
 | 
			
		||||
                if (Bet < 0)
 | 
			
		||||
                    Bet = 0;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. It has to be between 5 and 60. Default 15.")]
 | 
			
		||||
            public int TurnTimer { get; set; } = 15;
 | 
			
		||||
            [Option('b', "bet", Required = false, Default = 0, HelpText = "Amount you bet. Default 0.")]
 | 
			
		||||
            public int Bet { get; set; } = 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										210
									
								
								src/NadekoBot/Modules/Gambling/Connect4Commands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/NadekoBot/Modules/Gambling/Connect4Commands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.Connect4;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class Connect4Commands : GamblingSubmodule<GamblingService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly DiscordSocketClient _client;
 | 
			
		||||
            private readonly ICurrencyService _cs;
 | 
			
		||||
            private static readonly string[] numbers = new string[] { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:" };
 | 
			
		||||
 | 
			
		||||
            public Connect4Commands(DiscordSocketClient client, ICurrencyService cs, GamblingConfigService gamb)
 | 
			
		||||
                : base(gamb)
 | 
			
		||||
            {
 | 
			
		||||
                _client = client;
 | 
			
		||||
                _cs = cs;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [NadekoOptionsAttribute(typeof(Connect4Game.Options))]
 | 
			
		||||
            public async Task Connect4(params string[] args)
 | 
			
		||||
            {
 | 
			
		||||
                var (options, _) = OptionsParser.ParseFrom(new Connect4Game.Options(), args);
 | 
			
		||||
                if (!await CheckBetOptional(options.Bet).ConfigureAwait(false))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var newGame = new Connect4Game(ctx.User.Id, ctx.User.ToString(), options, _cs);
 | 
			
		||||
                Connect4Game game;
 | 
			
		||||
                if ((game = _service.Connect4Games.GetOrAdd(ctx.Channel.Id, newGame)) != newGame)
 | 
			
		||||
                {
 | 
			
		||||
                    if (game.CurrentPhase != Connect4Game.Phase.Joining)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    newGame.Dispose();
 | 
			
		||||
                    //means game already exists, try to join
 | 
			
		||||
                    var joined = await game.Join(ctx.User.Id, ctx.User.ToString(), options.Bet).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (options.Bet > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    if (!await _cs.RemoveAsync(ctx.User.Id, "Connect4-bet", options.Bet, true).ConfigureAwait(false))
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                        _service.Connect4Games.TryRemove(ctx.Channel.Id, out _);
 | 
			
		||||
                        game.Dispose();
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                game.OnGameStateUpdated += Game_OnGameStateUpdated;
 | 
			
		||||
                game.OnGameFailedToStart += Game_OnGameFailedToStart;
 | 
			
		||||
                game.OnGameEnded += Game_OnGameEnded;
 | 
			
		||||
                _client.MessageReceived += _client_MessageReceived;
 | 
			
		||||
 | 
			
		||||
                game.Initialize();
 | 
			
		||||
                if (options.Bet == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("connect4_created").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("connect4_created_bet", options.Bet + CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Task _client_MessageReceived(SocketMessage arg)
 | 
			
		||||
                {
 | 
			
		||||
                    if (ctx.Channel.Id != arg.Channel.Id)
 | 
			
		||||
                        return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
                    var _ = Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        bool success = false;
 | 
			
		||||
                        if (int.TryParse(arg.Content, out var col))
 | 
			
		||||
                        {
 | 
			
		||||
                            success = await game.Input(arg.Author.Id, col).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (success)
 | 
			
		||||
                            try { await arg.DeleteAsync().ConfigureAwait(false); } catch { }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            if (game.CurrentPhase == Connect4Game.Phase.Joining
 | 
			
		||||
                                || game.CurrentPhase == Connect4Game.Phase.Ended)
 | 
			
		||||
                            {
 | 
			
		||||
                                return;
 | 
			
		||||
                            }
 | 
			
		||||
                            RepostCounter++;
 | 
			
		||||
                            if (RepostCounter == 0)
 | 
			
		||||
                                try { msg = await ctx.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()).ConfigureAwait(false); } catch { }
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Task Game_OnGameFailedToStart(Connect4Game arg)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose))
 | 
			
		||||
                    {
 | 
			
		||||
                        _client.MessageReceived -= _client_MessageReceived;
 | 
			
		||||
                        toDispose.Dispose();
 | 
			
		||||
                    }
 | 
			
		||||
                    return ErrorLocalizedAsync("connect4_failed_to_start");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Task Game_OnGameEnded(Connect4Game arg, Connect4Game.Result result)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose))
 | 
			
		||||
                    {
 | 
			
		||||
                        _client.MessageReceived -= _client_MessageReceived;
 | 
			
		||||
                        toDispose.Dispose();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    string title;
 | 
			
		||||
                    if (result == Connect4Game.Result.CurrentPlayerWon)
 | 
			
		||||
                    {
 | 
			
		||||
                        title = GetText("connect4_won", Format.Bold(arg.CurrentPlayer.Username), Format.Bold(arg.OtherPlayer.Username));
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (result == Connect4Game.Result.OtherPlayerWon)
 | 
			
		||||
                    {
 | 
			
		||||
                        title = GetText("connect4_won", Format.Bold(arg.OtherPlayer.Username), Format.Bold(arg.CurrentPlayer.Username));
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                        title = GetText("connect4_draw");
 | 
			
		||||
 | 
			
		||||
                    return msg.ModifyAsync(x => x.Embed = new EmbedBuilder()
 | 
			
		||||
                        .WithTitle(title)
 | 
			
		||||
                        .WithDescription(GetGameStateText(game))
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .Build());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private IUserMessage msg;
 | 
			
		||||
 | 
			
		||||
            private int _repostCounter = 0;
 | 
			
		||||
            private int RepostCounter
 | 
			
		||||
            {
 | 
			
		||||
                get => _repostCounter;
 | 
			
		||||
                set
 | 
			
		||||
                {
 | 
			
		||||
                    if (value < 0 || value > 7)
 | 
			
		||||
                        _repostCounter = 0;
 | 
			
		||||
                    else _repostCounter = value;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task Game_OnGameStateUpdated(Connect4Game game)
 | 
			
		||||
            {
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithTitle($"{game.CurrentPlayer.Username} vs {game.OtherPlayer.Username}")
 | 
			
		||||
                    .WithDescription(GetGameStateText(game))
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                if (msg == null)
 | 
			
		||||
                    msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await msg.ModifyAsync(x => x.Embed = embed.Build()).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private string GetGameStateText(Connect4Game game)
 | 
			
		||||
            {
 | 
			
		||||
                var sb = new StringBuilder();
 | 
			
		||||
 | 
			
		||||
                if (game.CurrentPhase == Connect4Game.Phase.P1Move ||
 | 
			
		||||
                    game.CurrentPhase == Connect4Game.Phase.P2Move)
 | 
			
		||||
                    sb.AppendLine(GetText("connect4_player_to_move", Format.Bold(game.CurrentPlayer.Username)));
 | 
			
		||||
 | 
			
		||||
                for (int i = Connect4Game.NumberOfRows; i > 0; i--)
 | 
			
		||||
                {
 | 
			
		||||
                    for (int j = 0; j < Connect4Game.NumberOfColumns; j++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var cur = game.GameState[i + (j * Connect4Game.NumberOfRows) - 1];
 | 
			
		||||
 | 
			
		||||
                        if (cur == Connect4Game.Field.Empty)
 | 
			
		||||
                            sb.Append("⚫"); //black circle
 | 
			
		||||
                        else if (cur == Connect4Game.Field.P1)
 | 
			
		||||
                            sb.Append("🔴"); //red circle
 | 
			
		||||
                        else
 | 
			
		||||
                            sb.Append("🔵"); //blue circle
 | 
			
		||||
                    }
 | 
			
		||||
                    sb.AppendLine();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                for (int i = 0; i < Connect4Game.NumberOfColumns; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    sb.Append(numbers[i]);
 | 
			
		||||
                }
 | 
			
		||||
                return sb.ToString();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										94
									
								
								src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common.Events;
 | 
			
		||||
using System;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class CurrencyEventsCommands : GamblingSubmodule<CurrencyEventsService>
 | 
			
		||||
        {
 | 
			
		||||
            public enum OtherEvent
 | 
			
		||||
            {
 | 
			
		||||
                BotListUpvoters
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public CurrencyEventsCommands(GamblingConfigService gamblingConf) : base(gamblingConf)
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [NadekoOptionsAttribute(typeof(EventOptions))]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task EventStart(CurrencyEvent.Type ev, params string[] options)
 | 
			
		||||
            {
 | 
			
		||||
                var (opts, _) = OptionsParser.ParseFrom(new EventOptions(), options);
 | 
			
		||||
                if (!await _service.TryCreateEventAsync(ctx.Guild.Id,
 | 
			
		||||
                    ctx.Channel.Id,
 | 
			
		||||
                    ev,
 | 
			
		||||
                    opts,
 | 
			
		||||
                    GetEmbed
 | 
			
		||||
                    ).ConfigureAwait(false))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("start_event_fail").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private EmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot)
 | 
			
		||||
            {
 | 
			
		||||
                switch (type)
 | 
			
		||||
                {
 | 
			
		||||
                    case CurrencyEvent.Type.Reaction:
 | 
			
		||||
                        return new EmbedBuilder()
 | 
			
		||||
                            .WithOkColor()
 | 
			
		||||
                            .WithTitle(GetText("event_title", type.ToString()))
 | 
			
		||||
                            .WithDescription(GetReactionDescription(opts.Amount, currentPot))
 | 
			
		||||
                            .WithFooter(GetText("event_duration_footer", opts.Hours));
 | 
			
		||||
                    case CurrencyEvent.Type.GameStatus:
 | 
			
		||||
                        return new EmbedBuilder()
 | 
			
		||||
                            .WithOkColor()
 | 
			
		||||
                            .WithTitle(GetText("event_title", type.ToString()))
 | 
			
		||||
                            .WithDescription(GetGameStatusDescription(opts.Amount, currentPot))
 | 
			
		||||
                            .WithFooter(GetText("event_duration_footer", opts.Hours));
 | 
			
		||||
                    default:
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(type));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private string GetReactionDescription(long amount, long potSize)
 | 
			
		||||
            {
 | 
			
		||||
                string potSizeStr = Format.Bold(potSize == 0
 | 
			
		||||
                    ? "∞" + CurrencySign
 | 
			
		||||
                    : potSize.ToString() + CurrencySign);
 | 
			
		||||
                return GetText("new_reaction_event",
 | 
			
		||||
                                   CurrencySign,
 | 
			
		||||
                                   Format.Bold(amount + CurrencySign),
 | 
			
		||||
                                   potSizeStr);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private string GetGameStatusDescription(long amount, long potSize)
 | 
			
		||||
            {
 | 
			
		||||
                string potSizeStr = Format.Bold(potSize == 0
 | 
			
		||||
                    ? "∞" + CurrencySign
 | 
			
		||||
                    : potSize.ToString() + CurrencySign);
 | 
			
		||||
                return GetText("new_gamestatus_event",
 | 
			
		||||
                                   CurrencySign,
 | 
			
		||||
                                   Format.Bold(amount + CurrencySign),
 | 
			
		||||
                                   potSizeStr);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								src/NadekoBot/Modules/Gambling/CurrencyRaffleCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/NadekoBot/Modules/Gambling/CurrencyRaffleCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        public class CurrencyRaffleCommands : GamblingSubmodule<CurrencyRaffleService>
 | 
			
		||||
        {
 | 
			
		||||
            public enum Mixed { Mixed }
 | 
			
		||||
 | 
			
		||||
            public CurrencyRaffleCommands(GamblingConfigService gamblingConfService) : base(gamblingConfService)
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public Task RaffleCur(Mixed _, ShmartNumber amount) =>
 | 
			
		||||
                RaffleCur(amount, true);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task RaffleCur(ShmartNumber amount, bool mixed = false)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await CheckBetMandatory(amount).ConfigureAwait(false))
 | 
			
		||||
                    return;
 | 
			
		||||
                async Task OnEnded(IUser arg, long won)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendConfirmAsync(GetText("rafflecur_ended", CurrencyName, Format.Bold(arg.ToString()), won + CurrencySign)).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                var res = await _service.JoinOrCreateGame(ctx.Channel.Id,
 | 
			
		||||
                    ctx.User, amount, mixed, OnEnded)
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                if (res.Item1 != null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendConfirmAsync(GetText("rafflecur", res.Item1.GameType.ToString()),
 | 
			
		||||
                        string.Join("\n", res.Item1.Users.Select(x => $"{x.DiscordUser} ({x.Amount})")),
 | 
			
		||||
                        footer: GetText("rafflecur_joined", ctx.User.ToString())).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (res.Item2 == CurrencyRaffleService.JoinErrorType.AlreadyJoinedOrInvalidAmount)
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("rafflecur_already_joined").ConfigureAwait(false);
 | 
			
		||||
                    else if (res.Item2 == CurrencyRaffleService.JoinErrorType.NotEnoughCurrency)
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										230
									
								
								src/NadekoBot/Modules/Gambling/DiceRollCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								src/NadekoBot/Modules/Gambling/DiceRollCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,230 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Image = SixLabors.ImageSharp.Image;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class DiceRollCommands : NadekoSubmodule
 | 
			
		||||
        {
 | 
			
		||||
            private static readonly Regex dndRegex = new Regex(@"^(?<n1>\d+)d(?<n2>\d+)(?:\+(?<add>\d+))?(?:\-(?<sub>\d+))?$", RegexOptions.Compiled);
 | 
			
		||||
            private static readonly Regex fudgeRegex = new Regex(@"^(?<n1>\d+)d(?:F|f)$", RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
            private static readonly char[] _fateRolls = { '-', ' ', '+' };
 | 
			
		||||
            private readonly IImageCache _images;
 | 
			
		||||
 | 
			
		||||
            public DiceRollCommands(IDataCache data)
 | 
			
		||||
            {
 | 
			
		||||
                _images = data.LocalImages;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task Roll()
 | 
			
		||||
            {
 | 
			
		||||
                var rng = new NadekoRandom();
 | 
			
		||||
                var gen = rng.Next(1, 101);
 | 
			
		||||
 | 
			
		||||
                var num1 = gen / 10;
 | 
			
		||||
                var num2 = gen % 10;
 | 
			
		||||
 | 
			
		||||
                using (var img1 = GetDice(num1))
 | 
			
		||||
                using (var img2 = GetDice(num2))
 | 
			
		||||
                using (var img = new[] { img1, img2 }.Merge(out var format))
 | 
			
		||||
                using (var ms = img.ToStream(format))
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendFileAsync(ms,
 | 
			
		||||
                        $"dice.{format.FileExtensions.First()}",
 | 
			
		||||
                        Format.Bold(ctx.User.ToString()) + " " + GetText("dice_rolled", Format.Code(gen.ToString()))).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task Roll(int num)
 | 
			
		||||
            {
 | 
			
		||||
                await InternalRoll(num, true).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task Rolluo(int num = 1)
 | 
			
		||||
            {
 | 
			
		||||
                await InternalRoll(num, false).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public async Task Roll(string arg)
 | 
			
		||||
            {
 | 
			
		||||
                await InternallDndRoll(arg, true).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public async Task Rolluo(string arg)
 | 
			
		||||
            {
 | 
			
		||||
                await InternallDndRoll(arg, false).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task InternalRoll(int num, bool ordered)
 | 
			
		||||
            {
 | 
			
		||||
                if (num < 1 || num > 30)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("dice_invalid_number", 1, 30).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
                var dice = new List<Image<Rgba32>>(num);
 | 
			
		||||
                var values = new List<int>(num);
 | 
			
		||||
                for (var i = 0; i < num; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    var randomNumber = rng.Next(1, 7);
 | 
			
		||||
                    var toInsert = dice.Count;
 | 
			
		||||
                    if (ordered)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (randomNumber == 6 || dice.Count == 0)
 | 
			
		||||
                            toInsert = 0;
 | 
			
		||||
                        else if (randomNumber != 1)
 | 
			
		||||
                            for (var j = 0; j < dice.Count; j++)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (values[j] < randomNumber)
 | 
			
		||||
                                {
 | 
			
		||||
                                    toInsert = j;
 | 
			
		||||
                                    break;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        toInsert = dice.Count;
 | 
			
		||||
                    }
 | 
			
		||||
                    dice.Insert(toInsert, GetDice(randomNumber));
 | 
			
		||||
                    values.Insert(toInsert, randomNumber);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                using (var bitmap = dice.Merge(out var format))
 | 
			
		||||
                using (var ms = bitmap.ToStream(format))
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var d in dice)
 | 
			
		||||
                    {
 | 
			
		||||
                        d.Dispose();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await ctx.Channel.SendFileAsync(ms, $"dice.{format.FileExtensions.First()}",
 | 
			
		||||
                        Format.Bold(ctx.User.ToString()) + " " +
 | 
			
		||||
                        GetText("dice_rolled_num", Format.Bold(values.Count.ToString())) +
 | 
			
		||||
                        " " + GetText("total_average",
 | 
			
		||||
                            Format.Bold(values.Sum().ToString()),
 | 
			
		||||
                            Format.Bold((values.Sum() / (1.0f * values.Count)).ToString("N2")))).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task InternallDndRoll(string arg, bool ordered)
 | 
			
		||||
            {
 | 
			
		||||
                Match match;
 | 
			
		||||
                if ((match = fudgeRegex.Match(arg)).Length != 0 &&
 | 
			
		||||
                    int.TryParse(match.Groups["n1"].ToString(), out int n1) &&
 | 
			
		||||
                    n1 > 0 && n1 < 500)
 | 
			
		||||
                {
 | 
			
		||||
                    var rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
                    var rolls = new List<char>();
 | 
			
		||||
 | 
			
		||||
                    for (int i = 0; i < n1; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]);
 | 
			
		||||
                    }
 | 
			
		||||
                    var embed = new EmbedBuilder().WithOkColor().WithDescription(ctx.User.Mention + " " + GetText("dice_rolled_num", Format.Bold(n1.ToString())))
 | 
			
		||||
                        .AddField(efb => efb.WithName(Format.Bold("Result"))
 | 
			
		||||
                            .WithValue(string.Join(" ", rolls.Select(c => Format.Code($"[{c}]")))));
 | 
			
		||||
                    await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else if ((match = dndRegex.Match(arg)).Length != 0)
 | 
			
		||||
                {
 | 
			
		||||
                    var rng = new NadekoRandom();
 | 
			
		||||
                    if (int.TryParse(match.Groups["n1"].ToString(), out n1) &&
 | 
			
		||||
                        int.TryParse(match.Groups["n2"].ToString(), out int n2) &&
 | 
			
		||||
                        n1 <= 50 && n2 <= 100000 && n1 > 0 && n2 > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!int.TryParse(match.Groups["add"].Value, out int add))
 | 
			
		||||
                            add = 0;
 | 
			
		||||
                        if (!int.TryParse(match.Groups["sub"].Value, out int sub))
 | 
			
		||||
                            sub = 0;
 | 
			
		||||
 | 
			
		||||
                        var arr = new int[n1];
 | 
			
		||||
                        for (int i = 0; i < n1; i++)
 | 
			
		||||
                        {
 | 
			
		||||
                            arr[i] = rng.Next(1, n2 + 1);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        var sum = arr.Sum();
 | 
			
		||||
                        var embed = new EmbedBuilder().WithOkColor().WithDescription(ctx.User.Mention + " " + GetText("dice_rolled_num", n1) + $"`1 - {n2}`")
 | 
			
		||||
                        .AddField(efb => efb.WithName(Format.Bold("Rolls"))
 | 
			
		||||
                            .WithValue(string.Join(" ", (ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x => Format.Code(x.ToString())))))
 | 
			
		||||
                        .AddField(efb => efb.WithName(Format.Bold("Sum"))
 | 
			
		||||
                            .WithValue(sum + " + " + add + " - " + sub + " = " + (sum + add - sub)));
 | 
			
		||||
                        await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task NRoll([Leftover] string range)
 | 
			
		||||
            {
 | 
			
		||||
                int rolled;
 | 
			
		||||
                if (range.Contains("-"))
 | 
			
		||||
                {
 | 
			
		||||
                    var arr = range.Split('-')
 | 
			
		||||
                        .Take(2)
 | 
			
		||||
                        .Select(int.Parse)
 | 
			
		||||
                        .ToArray();
 | 
			
		||||
                    if (arr[0] > arr[1])
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("second_larger_than_first").ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    rolled = new NadekoRandom().Next(arr[0], arr[1] + 1);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    rolled = new NadekoRandom().Next(0, int.Parse(range) + 1);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("dice_rolled", Format.Bold(rolled.ToString())).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private Image<Rgba32> GetDice(int num)
 | 
			
		||||
            {
 | 
			
		||||
                if (num < 0 || num > 10)
 | 
			
		||||
                    throw new ArgumentOutOfRangeException(nameof(num));
 | 
			
		||||
 | 
			
		||||
                if (num == 10)
 | 
			
		||||
                {
 | 
			
		||||
                    var images = _images.Dice;
 | 
			
		||||
                    using (var imgOne = Image.Load(images[1]))
 | 
			
		||||
                    using (var imgZero = Image.Load(images[0]))
 | 
			
		||||
                    {
 | 
			
		||||
                        return new[] { imgOne, imgZero }.Merge();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return Image.Load(_images.Dice[num]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								src/NadekoBot/Modules/Gambling/DrawCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/NadekoBot/Modules/Gambling/DrawCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using Image = SixLabors.ImageSharp.Image;
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class DrawCommands : NadekoSubmodule
 | 
			
		||||
        {
 | 
			
		||||
            private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new ConcurrentDictionary<IGuild, Deck>();
 | 
			
		||||
            private readonly IImageCache _images;
 | 
			
		||||
 | 
			
		||||
            public DrawCommands(IDataCache data)
 | 
			
		||||
            {
 | 
			
		||||
                _images = data.LocalImages;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task<(Stream ImageStream, string ToSend)> InternalDraw(int num, ulong? guildId = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (num < 1 || num > 10)
 | 
			
		||||
                    throw new ArgumentOutOfRangeException(nameof(num));
 | 
			
		||||
 | 
			
		||||
                Deck cards = guildId == null ? new Deck() : _allDecks.GetOrAdd(ctx.Guild, (s) => new Deck());
 | 
			
		||||
                var images = new List<Image<Rgba32>>();
 | 
			
		||||
                var cardObjects = new List<Deck.Card>();
 | 
			
		||||
                for (var i = 0; i < num; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (cards.CardPool.Count == 0 && i != 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            await ReplyErrorLocalizedAsync("no_more_cards").ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                        catch
 | 
			
		||||
                        {
 | 
			
		||||
                            // ignored
 | 
			
		||||
                        }
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                    var currentCard = cards.Draw();
 | 
			
		||||
                    cardObjects.Add(currentCard);
 | 
			
		||||
                    images.Add(Image.Load(_images.GetCard(currentCard.ToString().ToLowerInvariant().Replace(' ', '_'))));
 | 
			
		||||
                }
 | 
			
		||||
                using (var img = images.Merge())
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var i in images)
 | 
			
		||||
                    {
 | 
			
		||||
                        i.Dispose();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var toSend = $"{Format.Bold(ctx.User.ToString())}";
 | 
			
		||||
                    if (cardObjects.Count == 5)
 | 
			
		||||
                        toSend += $" drew `{Deck.GetHandValue(cardObjects)}`";
 | 
			
		||||
 | 
			
		||||
                    if (guildId != null)
 | 
			
		||||
                        toSend += "\n" + GetText("cards_left", Format.Bold(cards.CardPool.Count.ToString()));
 | 
			
		||||
 | 
			
		||||
                    return (img.ToStream(), toSend);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Draw(int num = 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (num < 1)
 | 
			
		||||
                    num = 1;
 | 
			
		||||
                if (num > 10)
 | 
			
		||||
                    num = 10;
 | 
			
		||||
 | 
			
		||||
                var (ImageStream, ToSend) = await InternalDraw(num, ctx.Guild.Id).ConfigureAwait(false);
 | 
			
		||||
                using (ImageStream)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendFileAsync(ImageStream, num + " cards.jpg", ToSend).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task DrawNew(int num = 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (num < 1)
 | 
			
		||||
                    num = 1;
 | 
			
		||||
                if (num > 10)
 | 
			
		||||
                    num = 10;
 | 
			
		||||
 | 
			
		||||
                var (ImageStream, ToSend) = await InternalDraw(num).ConfigureAwait(false);
 | 
			
		||||
                using (ImageStream)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendFileAsync(ImageStream, num + " cards.jpg", ToSend).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task DeckShuffle()
 | 
			
		||||
            {
 | 
			
		||||
                //var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
                _allDecks.AddOrUpdate(ctx.Guild,
 | 
			
		||||
                        (g) => new Deck(),
 | 
			
		||||
                        (g, c) =>
 | 
			
		||||
                        {
 | 
			
		||||
                            c.Restart();
 | 
			
		||||
                            return c;
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("deck_reshuffled").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										135
									
								
								src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using Image = SixLabors.ImageSharp.Image;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class FlipCoinCommands : GamblingSubmodule<GamblingService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly IImageCache _images;
 | 
			
		||||
            private readonly ICurrencyService _cs;
 | 
			
		||||
            private readonly DbService _db;
 | 
			
		||||
            private static readonly NadekoRandom rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
            public FlipCoinCommands(IDataCache data, ICurrencyService cs, DbService db,
 | 
			
		||||
                GamblingConfigService gss) : base(gss)
 | 
			
		||||
            {
 | 
			
		||||
                _images = data.LocalImages;
 | 
			
		||||
                _cs = cs;
 | 
			
		||||
                _db = db;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task Flip(int count = 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (count > 10 || count < 1)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("flip_invalid", 10).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                var headCount = 0;
 | 
			
		||||
                var tailCount = 0;
 | 
			
		||||
                var imgs = new Image<Rgba32>[count];
 | 
			
		||||
                for (var i = 0; i < count; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    var headsArr = _images.Heads[rng.Next(0, _images.Heads.Count)];
 | 
			
		||||
                    var tailsArr = _images.Tails[rng.Next(0, _images.Tails.Count)];
 | 
			
		||||
                    if (rng.Next(0, 10) < 5)
 | 
			
		||||
                    {
 | 
			
		||||
                        imgs[i] = Image.Load(headsArr);
 | 
			
		||||
                        headCount++;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        imgs[i] = Image.Load(tailsArr);
 | 
			
		||||
                        tailCount++;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                using (var img = imgs.Merge(out var format))
 | 
			
		||||
                using (var stream = img.ToStream(format))
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var i in imgs)
 | 
			
		||||
                    {
 | 
			
		||||
                        i.Dispose();
 | 
			
		||||
                    }
 | 
			
		||||
                    var msg = count != 1
 | 
			
		||||
                        ? Format.Bold(ctx.User.ToString()) + " " + GetText("flip_results", count, headCount, tailCount)
 | 
			
		||||
                        : Format.Bold(ctx.User.ToString()) + " " + GetText("flipped", headCount > 0
 | 
			
		||||
                            ? Format.Bold(GetText("heads"))
 | 
			
		||||
                            : Format.Bold(GetText("tails")));
 | 
			
		||||
                    await ctx.Channel.SendFileAsync(stream, $"{count} coins.{format.FileExtensions.First()}", msg).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public enum BetFlipGuess
 | 
			
		||||
            {
 | 
			
		||||
                H = 1,
 | 
			
		||||
                Head = 1,
 | 
			
		||||
                Heads = 1,
 | 
			
		||||
                T = 2,
 | 
			
		||||
                Tail = 2,
 | 
			
		||||
                Tails = 2
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task Betflip(ShmartNumber amount, BetFlipGuess guess)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await CheckBetMandatory(amount).ConfigureAwait(false) || amount == 1)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var removed = await _cs.RemoveAsync(ctx.User, "Betflip Gamble", amount, false, gamble: true).ConfigureAwait(false);
 | 
			
		||||
                if (!removed)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                BetFlipGuess result;
 | 
			
		||||
                Uri imageToSend;
 | 
			
		||||
                var coins = _images.ImageUrls.Coins;
 | 
			
		||||
                if (rng.Next(0, 1000) <= 499)
 | 
			
		||||
                {
 | 
			
		||||
                    imageToSend = coins.Heads[rng.Next(0, coins.Heads.Length)];
 | 
			
		||||
                    result = BetFlipGuess.Heads;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    imageToSend = coins.Tails[rng.Next(0, coins.Tails.Length)];
 | 
			
		||||
                    result = BetFlipGuess.Tails;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                string str;
 | 
			
		||||
                if (guess == result)
 | 
			
		||||
                {
 | 
			
		||||
                    var toWin = (long)(amount * _config.BetFlip.Multiplier);
 | 
			
		||||
                    str = Format.Bold(ctx.User.ToString()) + " " + GetText("flip_guess", toWin + CurrencySign);
 | 
			
		||||
                    await _cs.AddAsync(ctx.User, "Betflip Gamble", toWin, false, gamble: true).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    str = ctx.User.ToString() + " " + GetText("better_luck");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(new EmbedBuilder()
 | 
			
		||||
                    .WithDescription(str)
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithImageUrl(imageToSend.ToString())).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										669
									
								
								src/NadekoBot/Modules/Gambling/Gambling.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										669
									
								
								src/NadekoBot/Modules/Gambling/Gambling.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,669 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Numerics;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly IDataCache _cache;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly NumberFormatInfo _enUsCulture;
 | 
			
		||||
        private readonly DownloadTracker _tracker;
 | 
			
		||||
        private readonly GamblingConfigService _configService;
 | 
			
		||||
 | 
			
		||||
        public Gambling(DbService db, ICurrencyService currency,
 | 
			
		||||
            IDataCache cache, DiscordSocketClient client,
 | 
			
		||||
            DownloadTracker tracker, GamblingConfigService configService) : base(configService)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _cs = currency;
 | 
			
		||||
            _cache = cache;
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _enUsCulture = new CultureInfo("en-US", false).NumberFormat;
 | 
			
		||||
            _enUsCulture.NumberDecimalDigits = 0;
 | 
			
		||||
            _enUsCulture.NumberGroupSeparator = " ";
 | 
			
		||||
            _tracker = tracker;
 | 
			
		||||
            _configService = configService;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string n(long cur) => cur.ToString("N", _enUsCulture);
 | 
			
		||||
 | 
			
		||||
        public string GetCurrency(ulong id)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                return n(uow.DiscordUsers.GetUserCurrency(id));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task Economy()
 | 
			
		||||
        {
 | 
			
		||||
            var ec = _service.GetEconomy();
 | 
			
		||||
            decimal onePercent = 0;
 | 
			
		||||
            if (ec.Cash > 0)
 | 
			
		||||
            {
 | 
			
		||||
                onePercent = ec.OnePercent / (ec.Cash-ec.Bot); // This stops the top 1% from owning more than 100% of the money
 | 
			
		||||
                // [21:03] Bob Page: Kinda remids me of US economy
 | 
			
		||||
            }
 | 
			
		||||
            var embed = new EmbedBuilder()
 | 
			
		||||
                .WithTitle(GetText("economy_state"))
 | 
			
		||||
                .AddField(GetText("currency_owned"), ((BigInteger)(ec.Cash - ec.Bot)) + CurrencySign)
 | 
			
		||||
                .AddField(GetText("currency_one_percent"), (onePercent * 100).ToString("F2") + "%")
 | 
			
		||||
                .AddField(GetText("currency_planted"), ((BigInteger)ec.Planted) + CurrencySign)
 | 
			
		||||
                .AddField(GetText("owned_waifus_total"), ((BigInteger)ec.Waifus) + CurrencySign)
 | 
			
		||||
                .AddField(GetText("bot_currency"), ec.Bot + CurrencySign)
 | 
			
		||||
                .AddField(GetText("total"), ((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", _enUsCulture) + CurrencySign)
 | 
			
		||||
                .WithOkColor();
 | 
			
		||||
                // ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task Timely()
 | 
			
		||||
        {
 | 
			
		||||
            var val = _config.Timely.Amount;
 | 
			
		||||
            var period = _config.Timely.Cooldown;
 | 
			
		||||
            if (val <= 0 || period <= 0)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("timely_none").ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            TimeSpan? rem;
 | 
			
		||||
            if ((rem = _cache.AddTimelyClaim(ctx.User.Id, period)) != null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("timely_already_claimed", rem?.ToString(@"dd\d\ hh\h\ mm\m\ ss\s")).ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _cs.AddAsync(ctx.User.Id, "Timely claim", val).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("timely", n(val) + CurrencySign, period).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task TimelyReset()
 | 
			
		||||
        {
 | 
			
		||||
            _cache.RemoveAllTimelyClaims();
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("timely_reset").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task TimelySet(int amount, int period = 24)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount < 0 || period < 0)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            _configService.ModifyConfig(gs =>
 | 
			
		||||
            {
 | 
			
		||||
                gs.Timely.Amount = amount;
 | 
			
		||||
                gs.Timely.Cooldown = period;
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            if (amount == 0)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("timely_set_none").ConfigureAwait(false);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("timely_set", Format.Bold(n(amount) + CurrencySign), Format.Bold(period.ToString())).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Raffle([Leftover] IRole role = null)
 | 
			
		||||
        {
 | 
			
		||||
            role = role ?? ctx.Guild.EveryoneRole;
 | 
			
		||||
 | 
			
		||||
            var members = (await role.GetMembersAsync().ConfigureAwait(false)).Where(u => u.Status != UserStatus.Offline);
 | 
			
		||||
            var membersArray = members as IUser[] ?? members.ToArray();
 | 
			
		||||
            if (membersArray.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)];
 | 
			
		||||
            await ctx.Channel.SendConfirmAsync("🎟 " + GetText("raffled_user"), $"**{usr.Username}#{usr.Discriminator}**", footer: $"ID: {usr.Id}").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task RaffleAny([Leftover] IRole role = null)
 | 
			
		||||
        {
 | 
			
		||||
            role = role ?? ctx.Guild.EveryoneRole;
 | 
			
		||||
 | 
			
		||||
            var members = (await role.GetMembersAsync().ConfigureAwait(false));
 | 
			
		||||
            var membersArray = members as IUser[] ?? members.ToArray();
 | 
			
		||||
            if (membersArray.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)];
 | 
			
		||||
            await ctx.Channel.SendConfirmAsync("🎟 " + GetText("raffled_user"), $"**{usr.Username}#{usr.Discriminator}**", footer: $"ID: {usr.Id}").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task Cash([Leftover] IUser user = null)
 | 
			
		||||
        {
 | 
			
		||||
            user = user ?? ctx.User;
 | 
			
		||||
            await ConfirmLocalizedAsync("has", Format.Bold(user.ToString()), $"{GetCurrency(user.Id)} {CurrencySign}").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public Task CurrencyTransactions(int page = 1) =>
 | 
			
		||||
            InternalCurrencyTransactions(ctx.User.Id, page);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task CurrencyTransactions([Leftover] IUser usr) =>
 | 
			
		||||
            InternalCurrencyTransactions(usr.Id, 1);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task CurrencyTransactions(IUser usr, int page) =>
 | 
			
		||||
            InternalCurrencyTransactions(usr.Id, page);
 | 
			
		||||
 | 
			
		||||
        private async Task InternalCurrencyTransactions(ulong userId, int page)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var trs = new List<CurrencyTransaction>();
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                trs = uow._context.CurrencyTransactions.GetPageFor(userId, page);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var embed = new EmbedBuilder()
 | 
			
		||||
                .WithTitle(GetText("transactions",
 | 
			
		||||
                    ((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() ?? $"{userId}"))
 | 
			
		||||
                .WithOkColor();
 | 
			
		||||
 | 
			
		||||
            var desc = "";
 | 
			
		||||
            foreach (var tr in trs)
 | 
			
		||||
            {
 | 
			
		||||
                var type = tr.Amount > 0 ? "🔵" : "🔴";
 | 
			
		||||
                var date = Format.Code($"〖{tr.DateAdded:HH:mm yyyy-MM-dd}〗");
 | 
			
		||||
                desc += $"\\{type} {date} {Format.Bold(n(tr.Amount))}\n\t{tr.Reason?.Trim()}\n";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            embed.WithDescription(desc);
 | 
			
		||||
            embed.WithFooter(GetText("page", page + 1));
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Cash(ulong userId)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("has", Format.Code(userId.ToString()), $"{GetCurrency(userId)} {CurrencySign}").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Give(ShmartNumber amount, IGuildUser receiver, [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount <= 0 || ctx.User.Id == receiver.Id || receiver.IsBot)
 | 
			
		||||
                return;
 | 
			
		||||
            var success = await _cs.RemoveAsync((IGuildUser)ctx.User, $"Gift to {receiver.Username} ({receiver.Id}).", amount, false).ConfigureAwait(false);
 | 
			
		||||
            if (!success)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            await _cs.AddAsync(receiver, $"Gift from {ctx.User.Username} ({ctx.User.Id}) - {msg}.", amount, true).ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("gifted", n(amount) + CurrencySign, Format.Bold(receiver.ToString()), msg)
 | 
			
		||||
                .ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task Give(ShmartNumber amount, [Leftover] IGuildUser receiver)
 | 
			
		||||
            => Give(amount, receiver, null);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task Award(ShmartNumber amount, IGuildUser usr, [Leftover] string msg) =>
 | 
			
		||||
            Award(amount, usr.Id, msg);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public Task Award(ShmartNumber amount, [Leftover] IGuildUser usr) =>
 | 
			
		||||
            Award(amount, usr.Id);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public async Task Award(ShmartNumber amount, ulong usrId, [Leftover] string msg = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount <= 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await _cs.AddAsync(usrId,
 | 
			
		||||
                $"Awarded by bot owner. ({ctx.User.Username}/{ctx.User.Id}) {(msg ?? "")}",
 | 
			
		||||
                amount,
 | 
			
		||||
                gamble: (ctx.Client.CurrentUser.Id != usrId)).ConfigureAwait(false);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("awarded", n(amount) + CurrencySign, $"<@{usrId}>").ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        [Priority(2)]
 | 
			
		||||
        public async Task Award(ShmartNumber amount, [Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var users = (await ctx.Guild.GetUsersAsync().ConfigureAwait(false))
 | 
			
		||||
                               .Where(u => u.GetRoles().Contains(role))
 | 
			
		||||
                               .ToList();
 | 
			
		||||
 | 
			
		||||
            await _cs.AddBulkAsync(users.Select(x => x.Id),
 | 
			
		||||
                users.Select(x => $"Awarded by bot owner to **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
 | 
			
		||||
                users.Select(x => amount.Value),
 | 
			
		||||
                gamble: true)
 | 
			
		||||
                .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("mass_award",
 | 
			
		||||
                n(amount) + CurrencySign,
 | 
			
		||||
                Format.Bold(users.Count.ToString()),
 | 
			
		||||
                Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task Take(ShmartNumber amount, [Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var users = (await role.GetMembersAsync()).ToList();
 | 
			
		||||
 | 
			
		||||
            await _cs.RemoveBulkAsync(users.Select(x => x.Id),
 | 
			
		||||
                    users.Select(x => $"Taken by bot owner from **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
 | 
			
		||||
                    users.Select(x => amount.Value),
 | 
			
		||||
                    gamble: true)
 | 
			
		||||
                .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync("mass_take",
 | 
			
		||||
                n(amount) + CurrencySign,
 | 
			
		||||
                Format.Bold(users.Count.ToString()),
 | 
			
		||||
                Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task Take(ShmartNumber amount, [Leftover] IGuildUser user)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount <= 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (await _cs.RemoveAsync(user, $"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})", amount,
 | 
			
		||||
                gamble: (ctx.Client.CurrentUser.Id != user.Id)).ConfigureAwait(false))
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("take", n(amount) + CurrencySign, Format.Bold(user.ToString())).ConfigureAwait(false);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyErrorLocalizedAsync("take_fail", n(amount) + CurrencySign, Format.Bold(user.ToString()), CurrencySign).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
        public async Task Take(ShmartNumber amount, [Leftover] ulong usrId)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount <= 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (await _cs.RemoveAsync(usrId, $"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})", amount,
 | 
			
		||||
                gamble: (ctx.Client.CurrentUser.Id != usrId)).ConfigureAwait(false))
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("take", amount + CurrencySign, $"<@{usrId}>").ConfigureAwait(false);
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyErrorLocalizedAsync("take_fail", amount + CurrencySign, Format.Code(usrId.ToString()), CurrencySign).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private IUserMessage rdMsg = null;
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task RollDuel(IUser u)
 | 
			
		||||
        {
 | 
			
		||||
            if (ctx.User.Id == u.Id)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            //since the challenge is created by another user, we need to reverse the ids
 | 
			
		||||
            //if it gets removed, means challenge is accepted
 | 
			
		||||
            if (_service.Duels.TryRemove((ctx.User.Id, u.Id), out var game))
 | 
			
		||||
            {
 | 
			
		||||
                await game.StartGame().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task RollDuel(ShmartNumber amount, IUser u)
 | 
			
		||||
        {
 | 
			
		||||
            if (ctx.User.Id == u.Id)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (amount <= 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("roll_duel"));
 | 
			
		||||
 | 
			
		||||
            var game = new RollDuelGame(_cs, _client.CurrentUser.Id, ctx.User.Id, u.Id, amount);
 | 
			
		||||
            //means challenge is just created
 | 
			
		||||
            if (_service.Duels.TryGetValue((ctx.User.Id, u.Id), out var other))
 | 
			
		||||
            {
 | 
			
		||||
                if (other.Amount != amount)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("roll_duel_already_challenged").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await RollDuel(u).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (_service.Duels.TryAdd((u.Id, ctx.User.Id), game))
 | 
			
		||||
            {
 | 
			
		||||
                game.OnGameTick += Game_OnGameTick;
 | 
			
		||||
                game.OnEnded += Game_OnEnded;
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("roll_duel_challenge",
 | 
			
		||||
                    Format.Bold(ctx.User.ToString()),
 | 
			
		||||
                    Format.Bold(u.ToString()),
 | 
			
		||||
                    Format.Bold(amount + CurrencySign))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            async Task Game_OnGameTick(RollDuelGame arg)
 | 
			
		||||
            {
 | 
			
		||||
                var rolls = arg.Rolls.Last();
 | 
			
		||||
                embed.Description += $@"{Format.Bold(ctx.User.ToString())} rolled **{rolls.Item1}**
 | 
			
		||||
{Format.Bold(u.ToString())} rolled **{rolls.Item2}**
 | 
			
		||||
--
 | 
			
		||||
";
 | 
			
		||||
 | 
			
		||||
                if (rdMsg == null)
 | 
			
		||||
                {
 | 
			
		||||
                    rdMsg = await ctx.Channel.EmbedAsync(embed)
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await rdMsg.ModifyAsync(x =>
 | 
			
		||||
                    {
 | 
			
		||||
                        x.Embed = embed.Build();
 | 
			
		||||
                    }).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            async Task Game_OnEnded(RollDuelGame rdGame, RollDuelGame.Reason reason)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (reason == RollDuelGame.Reason.Normal)
 | 
			
		||||
                    {
 | 
			
		||||
                        var winner = rdGame.Winner == rdGame.P1
 | 
			
		||||
                            ? ctx.User
 | 
			
		||||
                            : u;
 | 
			
		||||
                        embed.Description += $"\n**{winner}** Won {n(((long)(rdGame.Amount * 2 * 0.98))) + CurrencySign}";
 | 
			
		||||
                        await rdMsg.ModifyAsync(x => x.Embed = embed.Build())
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (reason == RollDuelGame.Reason.Timeout)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("roll_duel_timeout").ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (reason == RollDuelGame.Reason.NoFunds)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("roll_duel_no_funds").ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    _service.Duels.TryRemove((u.Id, ctx.User.Id), out var _);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task InternallBetroll(long amount)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckBetMandatory(amount).ConfigureAwait(false))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (!await _cs.RemoveAsync(ctx.User, "Betroll Gamble", amount, false, gamble: true).ConfigureAwait(false))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var br = new Betroll(base._config.BetRoll);
 | 
			
		||||
 | 
			
		||||
            var result = br.Roll();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            var str = Format.Bold(ctx.User.ToString()) + Format.Code(GetText("roll", result.Roll));
 | 
			
		||||
            if (result.Multiplier > 0)
 | 
			
		||||
            {
 | 
			
		||||
                var win = (long)(amount * result.Multiplier);
 | 
			
		||||
                str += GetText("br_win",
 | 
			
		||||
                    n(win) + CurrencySign,
 | 
			
		||||
                    result.Threshold + (result.Roll == 100 ? " 👑" : ""));
 | 
			
		||||
                await _cs.AddAsync(ctx.User, "Betroll Gamble",
 | 
			
		||||
                    win, false, gamble: true).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                str += GetText("better_luck");
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            await ctx.Channel.SendConfirmAsync(str).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public Task BetRoll(ShmartNumber amount)
 | 
			
		||||
            => InternallBetroll(amount);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [NadekoOptions(typeof(LbOpts))]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task Leaderboard(params string[] args)
 | 
			
		||||
            => Leaderboard(1, args);
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        [NadekoOptions(typeof(LbOpts))]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task Leaderboard(int page = 1, params string[] args)
 | 
			
		||||
        {
 | 
			
		||||
            if (--page < 0)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
 | 
			
		||||
 | 
			
		||||
            List<DiscordUser> cleanRichest = new List<DiscordUser>();
 | 
			
		||||
            // it's pointless to have clean on dm context
 | 
			
		||||
            if (Context.Guild is null)
 | 
			
		||||
            {
 | 
			
		||||
                opts.Clean = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (opts.Clean)
 | 
			
		||||
            {
 | 
			
		||||
                var now = DateTime.UtcNow;
 | 
			
		||||
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    cleanRichest = uow.DiscordUsers.GetTopRichest(_client.CurrentUser.Id, 10_000);
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                await Context.Channel.TriggerTypingAsync().ConfigureAwait(false);
 | 
			
		||||
                await _tracker.EnsureUsersDownloadedAsync(ctx.Guild).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                var sg = (SocketGuild)Context.Guild;
 | 
			
		||||
                cleanRichest = cleanRichest.Where(x => sg.GetUser(x.UserId) != null)
 | 
			
		||||
                    .ToList();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    cleanRichest = uow.DiscordUsers.GetTopRichest(_client.CurrentUser.Id, 9, page).ToList();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await Context.SendPaginatedConfirmAsync(page, curPage =>
 | 
			
		||||
            {
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                   .WithOkColor()
 | 
			
		||||
                   .WithTitle(CurrencySign + " " + GetText("leaderboard"));
 | 
			
		||||
 | 
			
		||||
                List<DiscordUser> toSend;
 | 
			
		||||
                if (!opts.Clean)
 | 
			
		||||
                {
 | 
			
		||||
                    using (var uow = _db.GetDbContext())
 | 
			
		||||
                    {
 | 
			
		||||
                        toSend = uow.DiscordUsers.GetTopRichest(_client.CurrentUser.Id, 9, curPage);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    toSend = cleanRichest.Skip(curPage * 9).Take(9).ToList();
 | 
			
		||||
                }
 | 
			
		||||
                 if (!toSend.Any())
 | 
			
		||||
                 {
 | 
			
		||||
                     embed.WithDescription(GetText("no_user_on_this_page"));
 | 
			
		||||
                     return embed;
 | 
			
		||||
                 }
 | 
			
		||||
 | 
			
		||||
                 for (var i = 0; i < toSend.Count; i++)
 | 
			
		||||
                 {
 | 
			
		||||
                     var x = toSend[i];
 | 
			
		||||
                     var usrStr = x.ToString().TrimTo(20, true);
 | 
			
		||||
 | 
			
		||||
                     var j = i;
 | 
			
		||||
                     embed.AddField(efb => efb.WithName("#" + (9 * curPage + j + 1) + " " + usrStr)
 | 
			
		||||
                                              .WithValue(n(x.CurrencyAmount) + " " + CurrencySign)
 | 
			
		||||
                                              .WithIsInline(true));
 | 
			
		||||
                 }
 | 
			
		||||
 | 
			
		||||
                 return embed;
 | 
			
		||||
             }, opts.Clean ? cleanRichest.Count() : 9000, 9, opts.Clean);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        public enum RpsPick
 | 
			
		||||
        {
 | 
			
		||||
            R = 0,
 | 
			
		||||
            Rock = 0,
 | 
			
		||||
            Rocket = 0,
 | 
			
		||||
            P = 1,
 | 
			
		||||
            Paper = 1,
 | 
			
		||||
            Paperclip = 1,
 | 
			
		||||
            S = 2,
 | 
			
		||||
            Scissors = 2
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum RpsResult
 | 
			
		||||
        {
 | 
			
		||||
            Win,
 | 
			
		||||
            Loss,
 | 
			
		||||
            Draw,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
        public async Task Rps(RpsPick pick, ShmartNumber amount = default)
 | 
			
		||||
        {
 | 
			
		||||
            long oldAmount = amount;
 | 
			
		||||
            if (!await CheckBetOptional(amount).ConfigureAwait(false) || (amount == 1))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            string getRpsPick(RpsPick p)
 | 
			
		||||
            {
 | 
			
		||||
                switch (p)
 | 
			
		||||
                {
 | 
			
		||||
                    case RpsPick.R:
 | 
			
		||||
                        return "🚀";
 | 
			
		||||
                    case RpsPick.P:
 | 
			
		||||
                        return "📎";
 | 
			
		||||
                    default:
 | 
			
		||||
                        return "✂️";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            var embed = new EmbedBuilder();
 | 
			
		||||
 | 
			
		||||
            var nadekoPick = (RpsPick)new NadekoRandom().Next(0, 3);
 | 
			
		||||
 | 
			
		||||
            if (amount > 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await _cs.RemoveAsync(ctx.User.Id,
 | 
			
		||||
                    "Rps-bet", amount, gamble: true).ConfigureAwait(false))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            string msg;
 | 
			
		||||
            if (pick == nadekoPick)
 | 
			
		||||
            {
 | 
			
		||||
                await _cs.AddAsync(ctx.User.Id,
 | 
			
		||||
                    "Rps-draw", amount, gamble: true).ConfigureAwait(false);
 | 
			
		||||
                embed.WithOkColor();
 | 
			
		||||
                msg = GetText("rps_draw", getRpsPick(pick));
 | 
			
		||||
            }
 | 
			
		||||
            else if ((pick == RpsPick.Paper && nadekoPick == RpsPick.Rock) ||
 | 
			
		||||
                     (pick == RpsPick.Rock && nadekoPick == RpsPick.Scissors) ||
 | 
			
		||||
                     (pick == RpsPick.Scissors && nadekoPick == RpsPick.Paper))
 | 
			
		||||
            {
 | 
			
		||||
                amount = (long)(amount * base._config.BetFlip.Multiplier);
 | 
			
		||||
                await _cs.AddAsync(ctx.User.Id,
 | 
			
		||||
                    "Rps-win", amount, gamble: true).ConfigureAwait(false);
 | 
			
		||||
                embed.WithOkColor();
 | 
			
		||||
                embed.AddField(GetText("won"), n(amount));
 | 
			
		||||
                msg = GetText("rps_win", ctx.User.Mention,
 | 
			
		||||
                    getRpsPick(pick), getRpsPick(nadekoPick));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                embed.WithErrorColor();
 | 
			
		||||
                amount = 0;
 | 
			
		||||
                msg = GetText("rps_win", ctx.Client.CurrentUser.Mention, getRpsPick(nadekoPick),
 | 
			
		||||
                    getRpsPick(pick));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            embed
 | 
			
		||||
                .WithDescription(msg);
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/NadekoBot/Modules/Gambling/Services/AnimalRaceService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/NadekoBot/Modules/Gambling/Services/AnimalRaceService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public class AnimalRaceService : INService, IUnloadableService
 | 
			
		||||
    {
 | 
			
		||||
        public ConcurrentDictionary<ulong, AnimalRace> AnimalRaces { get; } = new ConcurrentDictionary<ulong, AnimalRace>();
 | 
			
		||||
 | 
			
		||||
        public Task Unload()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var kvp in AnimalRaces)
 | 
			
		||||
            {
 | 
			
		||||
                try { kvp.Value.Dispose(); } catch { }
 | 
			
		||||
            }
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/NadekoBot/Modules/Gambling/Services/BlackJackService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/NadekoBot/Modules/Gambling/Services/BlackJackService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common.Blackjack;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public class BlackJackService : INService
 | 
			
		||||
    {
 | 
			
		||||
        public ConcurrentDictionary<ulong, Blackjack> Games { get; } = new ConcurrentDictionary<ulong, Blackjack>();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										137
									
								
								src/NadekoBot/Modules/Gambling/Services/CurrencyEventsService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/NadekoBot/Modules/Gambling/Services/CurrencyEventsService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common.Events;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using System;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public class CurrencyEventsService : INService
 | 
			
		||||
    {
 | 
			
		||||
        public class VoteModel
 | 
			
		||||
        {
 | 
			
		||||
            public ulong User { get; set; }
 | 
			
		||||
            public long Date { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly IBotCredentials _creds;
 | 
			
		||||
        private readonly IHttpClientFactory _http;
 | 
			
		||||
        private readonly GamblingConfigService _configService;
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events =
 | 
			
		||||
            new ConcurrentDictionary<ulong, ICurrencyEvent>();
 | 
			
		||||
 | 
			
		||||
        public CurrencyEventsService(DiscordSocketClient client,
 | 
			
		||||
            IBotCredentials creds, ICurrencyService cs,
 | 
			
		||||
            IHttpClientFactory http, GamblingConfigService configService)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
            _creds = creds;
 | 
			
		||||
            _http = http;
 | 
			
		||||
            _configService = configService;
 | 
			
		||||
            
 | 
			
		||||
            if (_client.ShardId == 0)
 | 
			
		||||
            {
 | 
			
		||||
                Task t = BotlistUpvoteLoop();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task BotlistUpvoteLoop()
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(_creds.VotesUrl))
 | 
			
		||||
                return;
 | 
			
		||||
            while (true)
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(TimeSpan.FromHours(1)).ConfigureAwait(false);
 | 
			
		||||
                await TriggerVoteCheck().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task TriggerVoteCheck()
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using (var req = new HttpRequestMessage(HttpMethod.Get, _creds.VotesUrl))
 | 
			
		||||
                {
 | 
			
		||||
                    if (!string.IsNullOrWhiteSpace(_creds.VotesToken))
 | 
			
		||||
                        req.Headers.Add("Authorization", _creds.VotesToken);
 | 
			
		||||
                    using (var http = _http.CreateClient())
 | 
			
		||||
                    using (var res = await http.SendAsync(req).ConfigureAwait(false))
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!res.IsSuccessStatusCode)
 | 
			
		||||
                        {
 | 
			
		||||
                            Log.Warning("Botlist API not reached.");
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                        var resStr = await res.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
                        var ids = JsonConvert.DeserializeObject<VoteModel[]>(resStr)
 | 
			
		||||
                            .Select(x => x.User)
 | 
			
		||||
                            .Distinct();
 | 
			
		||||
                        await _cs.AddBulkAsync(ids, ids.Select(x => "Voted - <https://discordbots.org/bot/nadeko/vote>"), ids.Select(x => 10L), true).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error in TriggerVoteCheck");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> TryCreateEventAsync(ulong guildId, ulong channelId, CurrencyEvent.Type type,
 | 
			
		||||
            EventOptions opts, Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> embed)
 | 
			
		||||
        {
 | 
			
		||||
            SocketGuild g = _client.GetGuild(guildId);
 | 
			
		||||
            SocketTextChannel ch = g?.GetChannel(channelId) as SocketTextChannel;
 | 
			
		||||
            if (ch == null)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            ICurrencyEvent ce;
 | 
			
		||||
 | 
			
		||||
            if (type == CurrencyEvent.Type.Reaction)
 | 
			
		||||
            {
 | 
			
		||||
                ce = new ReactionEvent(_client, _cs, g, ch, opts, _configService.Data, embed);
 | 
			
		||||
            }
 | 
			
		||||
            else if (type == CurrencyEvent.Type.GameStatus)
 | 
			
		||||
            {
 | 
			
		||||
                ce = new GameStatusEvent(_client, _cs, g, ch, opts, embed);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var added = _events.TryAdd(guildId, ce);
 | 
			
		||||
            if (added)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    ce.OnEnded += OnEventEnded;
 | 
			
		||||
                    await ce.StartEvent().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error starting event");
 | 
			
		||||
                    _events.TryRemove(guildId, out ce);
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return added;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task OnEventEnded(ulong gid)
 | 
			
		||||
        {
 | 
			
		||||
            _events.TryRemove(gid, out _);
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,88 @@
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using Discord;
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public class CurrencyRaffleService : INService
 | 
			
		||||
    {
 | 
			
		||||
        public enum JoinErrorType
 | 
			
		||||
        {
 | 
			
		||||
            NotEnoughCurrency,
 | 
			
		||||
            AlreadyJoinedOrInvalidAmount
 | 
			
		||||
        }
 | 
			
		||||
        private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
 | 
			
		||||
        public Dictionary<ulong, CurrencyRaffleGame> Games { get; } = new Dictionary<ulong, CurrencyRaffleGame>();
 | 
			
		||||
 | 
			
		||||
        public CurrencyRaffleService(DbService db, ICurrencyService cs)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<(CurrencyRaffleGame, JoinErrorType?)> JoinOrCreateGame(ulong channelId, IUser user, long amount, bool mixed, Func<IUser, long, Task> onEnded)
 | 
			
		||||
        {
 | 
			
		||||
            await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var newGame = false;
 | 
			
		||||
                if (!Games.TryGetValue(channelId, out var crg))
 | 
			
		||||
                {
 | 
			
		||||
                    newGame = true;
 | 
			
		||||
                    crg = new CurrencyRaffleGame(mixed
 | 
			
		||||
                        ? CurrencyRaffleGame.Type.Mixed
 | 
			
		||||
                        : CurrencyRaffleGame.Type.Normal);
 | 
			
		||||
                    Games.Add(channelId, crg);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //remove money, and stop the game if this 
 | 
			
		||||
                // user created it and doesn't have the money
 | 
			
		||||
                if (!await _cs.RemoveAsync(user.Id, "Currency Raffle Join", amount).ConfigureAwait(false))
 | 
			
		||||
                {
 | 
			
		||||
                    if (newGame)
 | 
			
		||||
                        Games.Remove(channelId);
 | 
			
		||||
                    return (null, JoinErrorType.NotEnoughCurrency);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!crg.AddUser(user, amount))
 | 
			
		||||
                {
 | 
			
		||||
                    await _cs.AddAsync(user.Id, "Curency Raffle Refund", amount).ConfigureAwait(false);
 | 
			
		||||
                    return (null, JoinErrorType.AlreadyJoinedOrInvalidAmount);
 | 
			
		||||
                }
 | 
			
		||||
                if (newGame)
 | 
			
		||||
                {
 | 
			
		||||
                    var _t = Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        await Task.Delay(60000).ConfigureAwait(false);
 | 
			
		||||
                        await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            var winner = crg.GetWinner();
 | 
			
		||||
                            var won = crg.Users.Sum(x => x.Amount);
 | 
			
		||||
 | 
			
		||||
                            await _cs.AddAsync(winner.DiscordUser.Id, "Currency Raffle Win",
 | 
			
		||||
                                won).ConfigureAwait(false);
 | 
			
		||||
                            Games.Remove(channelId, out _);
 | 
			
		||||
                            var oe = onEnded(winner.DiscordUser, won);
 | 
			
		||||
                        }
 | 
			
		||||
                        catch { }
 | 
			
		||||
                        finally { _locker.Release(); }
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                return (crg, null);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _locker.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,42 @@
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Common.Configs;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
 | 
			
		||||
    {
 | 
			
		||||
        public override string Name { get; } = "gambling";
 | 
			
		||||
        private const string FilePath = "data/gambling.yml";
 | 
			
		||||
        private static TypedKey<GamblingConfig> changeKey = new TypedKey<GamblingConfig>("config.gambling.updated");
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        public GamblingConfigService(IConfigSeria serializer, IPubSub pubSub)
 | 
			
		||||
            : base(FilePath, serializer, pubSub, changeKey)
 | 
			
		||||
        {
 | 
			
		||||
            AddParsedProp("currency.name", gs => gs.Currency.Name, ConfigParsers.String, ConfigPrinters.ToString);
 | 
			
		||||
            AddParsedProp("currency.sign", gs => gs.Currency.Sign, ConfigParsers.String, ConfigPrinters.ToString);
 | 
			
		||||
            
 | 
			
		||||
            AddParsedProp("minbet", gs => gs.MinBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
 | 
			
		||||
            AddParsedProp("maxbet", gs => gs.MaxBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
 | 
			
		||||
            
 | 
			
		||||
            AddParsedProp("gen.min", gs => gs.Generation.MinAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1);
 | 
			
		||||
            AddParsedProp("gen.max", gs => gs.Generation.MaxAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1);
 | 
			
		||||
            AddParsedProp("gen.cd", gs => gs.Generation.GenCooldown, int.TryParse, ConfigPrinters.ToString, val => val > 0);
 | 
			
		||||
            AddParsedProp("gen.chance", gs => gs.Generation.Chance, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1);
 | 
			
		||||
            AddParsedProp("gen.has_pw", gs => gs.Generation.HasPassword, bool.TryParse, ConfigPrinters.ToString);
 | 
			
		||||
            AddParsedProp("bf.multi", gs => gs.BetFlip.Multiplier, decimal.TryParse, ConfigPrinters.ToString, val => val >= 1);
 | 
			
		||||
            AddParsedProp("waifu.min_price", gs => gs.Waifu.MinPrice, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
 | 
			
		||||
            AddParsedProp("waifu.multi.reset", gs => gs.Waifu.Multipliers.WaifuReset, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
 | 
			
		||||
            AddParsedProp("waifu.multi.crush_claim", gs => gs.Waifu.Multipliers.CrushClaim, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
 | 
			
		||||
            AddParsedProp("waifu.multi.normal_claim", gs => gs.Waifu.Multipliers.NormalClaim, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
 | 
			
		||||
            AddParsedProp("waifu.multi.divorce_value", gs => gs.Waifu.Multipliers.DivorceNewValue, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
 | 
			
		||||
            AddParsedProp("waifu.multi.all_gifts", gs => gs.Waifu.Multipliers.AllGiftPrices, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
 | 
			
		||||
            AddParsedProp("waifu.multi.gift_effect", gs => gs.Waifu.Multipliers.GiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
 | 
			
		||||
            AddParsedProp("decay.percent", gs => gs.Decay.Percent, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1);
 | 
			
		||||
            AddParsedProp("decay.maxdecay", gs => gs.Decay.MaxDecay, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
 | 
			
		||||
            AddParsedProp("decay.threshold", gs => gs.Decay.MinThreshold, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										162
									
								
								src/NadekoBot/Modules/Gambling/Services/GamblingService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								src/NadekoBot/Modules/Gambling/Services/GamblingService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,162 @@
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.Connect4;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public class GamblingService : INService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly NadekoBot _bot;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly IDataCache _cache;
 | 
			
		||||
        private readonly GamblingConfigService _gss;
 | 
			
		||||
 | 
			
		||||
        public ConcurrentDictionary<(ulong, ulong), RollDuelGame> Duels { get; } = new ConcurrentDictionary<(ulong, ulong), RollDuelGame>();
 | 
			
		||||
        public ConcurrentDictionary<ulong, Connect4Game> Connect4Games { get; } = new ConcurrentDictionary<ulong, Connect4Game>();
 | 
			
		||||
 | 
			
		||||
        private readonly Timer _decayTimer;
 | 
			
		||||
 | 
			
		||||
        public GamblingService(DbService db, NadekoBot bot, ICurrencyService cs,
 | 
			
		||||
            DiscordSocketClient client, IDataCache cache, GamblingConfigService gss)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
            _bot = bot;
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _cache = cache;
 | 
			
		||||
            _gss = gss;
 | 
			
		||||
            
 | 
			
		||||
            if (_bot.Client.ShardId == 0)
 | 
			
		||||
            {
 | 
			
		||||
                _decayTimer = new Timer(_ =>
 | 
			
		||||
                {
 | 
			
		||||
                    var config = _gss.Data;
 | 
			
		||||
                    var maxDecay = config.Decay.MaxDecay;
 | 
			
		||||
                    if (config.Decay.Percent <= 0 || config.Decay.Percent > 1 || maxDecay < 0)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    using (var uow = _db.GetDbContext())
 | 
			
		||||
                    {
 | 
			
		||||
                        var lastCurrencyDecay = _cache.GetLastCurrencyDecay();
 | 
			
		||||
                        
 | 
			
		||||
                        if (DateTime.UtcNow - lastCurrencyDecay < TimeSpan.FromHours(config.Decay.HourInterval))
 | 
			
		||||
                           return;
 | 
			
		||||
                        
 | 
			
		||||
                         Log.Information($"Decaying users' currency - decay: {config.Decay.Percent * 100}% " +
 | 
			
		||||
                                   $"| max: {maxDecay} " +
 | 
			
		||||
                                   $"| threshold: {config.Decay.MinThreshold}");
 | 
			
		||||
                         
 | 
			
		||||
                         if (maxDecay == 0)
 | 
			
		||||
                             maxDecay = int.MaxValue;
 | 
			
		||||
                         
 | 
			
		||||
                        uow._context.Database.ExecuteSqlInterpolated($@"
 | 
			
		||||
UPDATE DiscordUser
 | 
			
		||||
SET CurrencyAmount=
 | 
			
		||||
    CASE WHEN
 | 
			
		||||
    {maxDecay} > ROUND(CurrencyAmount * {config.Decay.Percent} - 0.5)
 | 
			
		||||
    THEN
 | 
			
		||||
    CurrencyAmount - ROUND(CurrencyAmount * {config.Decay.Percent} - 0.5)
 | 
			
		||||
    ELSE
 | 
			
		||||
    CurrencyAmount - {maxDecay}
 | 
			
		||||
    END
 | 
			
		||||
WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentUser.Id};");
 | 
			
		||||
 | 
			
		||||
                        _cache.SetLastCurrencyDecay();
 | 
			
		||||
                        uow.SaveChanges();
 | 
			
		||||
                    }
 | 
			
		||||
                }, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //using (var uow = _db.UnitOfWork)
 | 
			
		||||
            //{
 | 
			
		||||
            //    //refund all of the currency users had at stake in gambling games
 | 
			
		||||
            //    //at the time bot was restarted
 | 
			
		||||
 | 
			
		||||
            //    var stakes = uow._context.Set<Stake>()
 | 
			
		||||
            //        .ToArray();
 | 
			
		||||
 | 
			
		||||
            //    var userIds = stakes.Select(x => x.UserId).ToArray();
 | 
			
		||||
            //    var reasons = stakes.Select(x => "Stake-" + x.Source).ToArray();
 | 
			
		||||
            //    var amounts = stakes.Select(x => x.Amount).ToArray();
 | 
			
		||||
 | 
			
		||||
            //    _cs.AddBulkAsync(userIds, reasons, amounts, gamble: true).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            //    foreach (var s in stakes)
 | 
			
		||||
            //    {
 | 
			
		||||
            //        _cs.AddAsync(s.UserId, "Stake-" + s.Source, s.Amount, gamble: true)
 | 
			
		||||
            //            .GetAwaiter()
 | 
			
		||||
            //            .GetResult();
 | 
			
		||||
            //    }
 | 
			
		||||
 | 
			
		||||
            //    uow._context.Set<Stake>().RemoveRange(stakes);
 | 
			
		||||
            //    uow.Complete();
 | 
			
		||||
            //    Log.Information("Refunded {0} users' stakes.", stakes.Length);
 | 
			
		||||
            //}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public struct EconomyResult
 | 
			
		||||
        {
 | 
			
		||||
            public decimal Cash { get; set; }
 | 
			
		||||
            public decimal Planted { get; set; }
 | 
			
		||||
            public decimal Waifus { get; set; }
 | 
			
		||||
            public decimal OnePercent { get; set; }
 | 
			
		||||
            public long Bot { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public EconomyResult GetEconomy()
 | 
			
		||||
        {
 | 
			
		||||
            if (_cache.TryGetEconomy(out var data))
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    return JsonConvert.DeserializeObject<EconomyResult>(data);
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            decimal cash;
 | 
			
		||||
            decimal onePercent;
 | 
			
		||||
            decimal planted;
 | 
			
		||||
            decimal waifus;
 | 
			
		||||
            long bot;
 | 
			
		||||
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                cash = uow.DiscordUsers.GetTotalCurrency();
 | 
			
		||||
                onePercent = uow.DiscordUsers.GetTopOnePercentCurrency(_client.CurrentUser.Id);
 | 
			
		||||
                planted = uow.PlantedCurrency.GetTotalPlanted();
 | 
			
		||||
                waifus = uow.Waifus.GetTotalValue();
 | 
			
		||||
                bot = uow.DiscordUsers.GetUserCurrency(_client.CurrentUser.Id);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = new EconomyResult
 | 
			
		||||
            {
 | 
			
		||||
                Cash = cash,
 | 
			
		||||
                Planted = planted,
 | 
			
		||||
                Bot = bot,
 | 
			
		||||
                Waifus = waifus,
 | 
			
		||||
                OnePercent = onePercent,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            _cache.SetEconomy(JsonConvert.SerializeObject(result));
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<WheelOfFortuneGame.Result> WheelOfFortuneSpinAsync(ulong userId, long bet)
 | 
			
		||||
        {
 | 
			
		||||
            return new WheelOfFortuneGame(userId, bet, _gss.Data, _cs).SpinAsync();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										43
									
								
								src/NadekoBot/Modules/Gambling/Services/IShopService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/NadekoBot/Modules/Gambling/Services/IShopService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IShopService
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Changes the price of a shop item
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="guildId">Id of the guild in which the shop is</param>
 | 
			
		||||
        /// <param name="index">Index of the item</param>
 | 
			
		||||
        /// <param name="newPrice">New item price</param>
 | 
			
		||||
        /// <returns>Success status</returns>
 | 
			
		||||
        Task<bool> ChangeEntryPriceAsync(ulong guildId, int index, int newPrice);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Changes the name of a shop item
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="guildId">Id of the guild in which the shop is</param>
 | 
			
		||||
        /// <param name="index">Index of the item</param>
 | 
			
		||||
        /// <param name="newName">New item name</param>
 | 
			
		||||
        /// <returns>Success status</returns>
 | 
			
		||||
        Task<bool> ChangeEntryNameAsync(ulong guildId, int index, string newName);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Swaps indexes of 2 items in the shop
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="guildId">Id of the guild in which the shop is</param>
 | 
			
		||||
        /// <param name="index1">First entry's index</param>
 | 
			
		||||
        /// <param name="index2">Second entry's index</param>
 | 
			
		||||
        /// <returns>Whether swap was successful</returns>
 | 
			
		||||
        Task<bool> SwapEntriesAsync(ulong guildId, int index1, int index2);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Swaps indexes of 2 items in the shop
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="guildId">Id of the guild in which the shop is</param>
 | 
			
		||||
        /// <param name="fromIndex">Current index of the entry to move</param>
 | 
			
		||||
        /// <param name="toIndex">Destination index of the entry</param>
 | 
			
		||||
        /// <returns>Whether swap was successful</returns>
 | 
			
		||||
        Task<bool> MoveEntryAsync(ulong guildId, int fromIndex, int toIndex);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										106
									
								
								src/NadekoBot/Modules/Gambling/Services/Impl/ShopService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/NadekoBot/Modules/Gambling/Services/Impl/ShopService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public class ShopService : IShopService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public ShopService(DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private IndexedCollection<ShopEntry> GetEntriesInternal(IUnitOfWork uow, ulong guildId) =>
 | 
			
		||||
            uow.GuildConfigs.ForId(
 | 
			
		||||
                    guildId,
 | 
			
		||||
                    set => set.Include(x => x.ShopEntries).ThenInclude(x => x.Items)
 | 
			
		||||
                )
 | 
			
		||||
                .ShopEntries
 | 
			
		||||
                .ToIndexed();
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> ChangeEntryPriceAsync(ulong guildId, int index, int newPrice)
 | 
			
		||||
        {
 | 
			
		||||
            if (index < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(index));
 | 
			
		||||
            if (newPrice <= 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(newPrice));
 | 
			
		||||
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var entries = GetEntriesInternal(uow, guildId);
 | 
			
		||||
 | 
			
		||||
            if (index >= entries.Count)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            entries[index].Price = newPrice;
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> ChangeEntryNameAsync(ulong guildId, int index, string newName)
 | 
			
		||||
        {
 | 
			
		||||
            if (index < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(index));
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(newName))
 | 
			
		||||
                throw new ArgumentNullException(nameof(newName));
 | 
			
		||||
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var entries = GetEntriesInternal(uow, guildId);
 | 
			
		||||
 | 
			
		||||
            if (index >= entries.Count)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            entries[index].Name = newName.TrimTo(100);
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> SwapEntriesAsync(ulong guildId, int index1, int index2)
 | 
			
		||||
        {
 | 
			
		||||
            if (index1 < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(index1));
 | 
			
		||||
            if (index2 < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(index2));
 | 
			
		||||
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var entries = GetEntriesInternal(uow, guildId);
 | 
			
		||||
 | 
			
		||||
            if (index1 >= entries.Count || index2 >= entries.Count || index1 == index2)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            entries[index1].Index = index2;
 | 
			
		||||
            entries[index2].Index = index1;
 | 
			
		||||
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> MoveEntryAsync(ulong guildId, int fromIndex, int toIndex)
 | 
			
		||||
        {
 | 
			
		||||
            if (fromIndex < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(fromIndex));
 | 
			
		||||
            if (toIndex < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(toIndex));
 | 
			
		||||
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            var entries = GetEntriesInternal(uow, guildId);
 | 
			
		||||
 | 
			
		||||
            if (fromIndex >= entries.Count || toIndex >= entries.Count || fromIndex == toIndex)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            var entry = entries[fromIndex];
 | 
			
		||||
            entries.RemoveAt(fromIndex);
 | 
			
		||||
            entries.Insert(toIndex, entry);
 | 
			
		||||
 | 
			
		||||
            await uow.SaveChangesAsync();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										376
									
								
								src/NadekoBot/Modules/Gambling/Services/PlantPickService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										376
									
								
								src/NadekoBot/Modules/Gambling/Services/PlantPickService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,376 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Repositories;
 | 
			
		||||
using NadekoBot.Core.Services.Impl;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using SixLabors.Fonts;
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
using SixLabors.ImageSharp.Drawing.Processing;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using SixLabors.ImageSharp.Processing;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using Image = SixLabors.ImageSharp.Image;
 | 
			
		||||
using Color = SixLabors.ImageSharp.Color;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public class PlantPickService : INService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly IBotStrings _strings;
 | 
			
		||||
        private readonly IImageCache _images;
 | 
			
		||||
        private readonly FontProvider _fonts;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly CommandHandler _cmdHandler;
 | 
			
		||||
        private readonly NadekoRandom _rng;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly GamblingConfigService _gss;
 | 
			
		||||
 | 
			
		||||
        public readonly ConcurrentHashSet<ulong> _generationChannels = new ConcurrentHashSet<ulong>();
 | 
			
		||||
        //channelId/last generation
 | 
			
		||||
        public ConcurrentDictionary<ulong, DateTime> LastGenerations { get; } = new ConcurrentDictionary<ulong, DateTime>();
 | 
			
		||||
        private readonly SemaphoreSlim pickLock = new SemaphoreSlim(1, 1);
 | 
			
		||||
 | 
			
		||||
        public PlantPickService(DbService db, CommandHandler cmd, IBotStrings strings,
 | 
			
		||||
            IDataCache cache, FontProvider fonts, ICurrencyService cs,
 | 
			
		||||
            CommandHandler cmdHandler, DiscordSocketClient client, GamblingConfigService gss)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _strings = strings;
 | 
			
		||||
            _images = cache.LocalImages;
 | 
			
		||||
            _fonts = fonts;
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
            _cmdHandler = cmdHandler;
 | 
			
		||||
            _rng = new NadekoRandom();
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _gss = gss;
 | 
			
		||||
 | 
			
		||||
            cmd.OnMessageNoTrigger += PotentialFlowerGeneration;
 | 
			
		||||
            using (var uow = db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var guildIds = client.Guilds.Select(x => x.Id).ToList();
 | 
			
		||||
                var configs = uow._context.Set<GuildConfig>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Include(x => x.GenerateCurrencyChannelIds)
 | 
			
		||||
                    .Where(x => guildIds.Contains(x.GuildId))
 | 
			
		||||
                    .ToList();
 | 
			
		||||
                
 | 
			
		||||
                _generationChannels = new ConcurrentHashSet<ulong>(configs
 | 
			
		||||
                    .SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetText(ulong gid, string key, params object[] rep)
 | 
			
		||||
            => _strings.GetText(key, gid, rep);
 | 
			
		||||
 | 
			
		||||
        public bool ToggleCurrencyGeneration(ulong gid, ulong cid)
 | 
			
		||||
        {
 | 
			
		||||
            bool enabled;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var guildConfig = uow.GuildConfigs.ForId(gid, set => set.Include(gc => gc.GenerateCurrencyChannelIds));
 | 
			
		||||
 | 
			
		||||
                var toAdd = new GCChannelId() { ChannelId = cid };
 | 
			
		||||
                if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd))
 | 
			
		||||
                {
 | 
			
		||||
                    guildConfig.GenerateCurrencyChannelIds.Add(toAdd);
 | 
			
		||||
                    _generationChannels.Add(cid);
 | 
			
		||||
                    enabled = true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd));
 | 
			
		||||
                    if (toDelete != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        uow._context.Remove(toDelete);
 | 
			
		||||
                    }
 | 
			
		||||
                    _generationChannels.TryRemove(cid);
 | 
			
		||||
                    enabled = false;
 | 
			
		||||
                }
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
            return enabled;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<GeneratingChannel> GetAllGeneratingChannels()
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var chs = uow.GuildConfigs.GetGeneratingChannels();
 | 
			
		||||
                return chs;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Get a random currency image stream, with an optional password sticked onto it.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="pass">Optional password to add to top left corner.</param>
 | 
			
		||||
        /// <returns>Stream of the currency image</returns>
 | 
			
		||||
        public Stream GetRandomCurrencyImage(string pass, out string extension)
 | 
			
		||||
        {
 | 
			
		||||
            // get a random currency image bytes
 | 
			
		||||
            var rng = new NadekoRandom();
 | 
			
		||||
            var curImg = _images.Currency[rng.Next(0, _images.Currency.Count)];
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(pass))
 | 
			
		||||
            {
 | 
			
		||||
                // determine the extension
 | 
			
		||||
                using (var img = Image.Load(curImg, out var format))
 | 
			
		||||
                {
 | 
			
		||||
                    extension = format.FileExtensions.FirstOrDefault() ?? "png";
 | 
			
		||||
                }
 | 
			
		||||
                // return the image
 | 
			
		||||
                return curImg.ToStream();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // get the image stream and extension
 | 
			
		||||
            var (s, ext) = AddPassword(curImg, pass);
 | 
			
		||||
            // set the out extension parameter to the extension we've got
 | 
			
		||||
            extension = ext;
 | 
			
		||||
            // return the image
 | 
			
		||||
            return s;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Add a password to the image.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="curImg">Image to add password to.</param>
 | 
			
		||||
        /// <param name="pass">Password to add to top left corner.</param>
 | 
			
		||||
        /// <returns>Image with the password in the top left corner.</returns>
 | 
			
		||||
        private (Stream, string) AddPassword(byte[] curImg, string pass)
 | 
			
		||||
        {
 | 
			
		||||
            // draw lower, it looks better
 | 
			
		||||
            pass = pass.TrimTo(10, true).ToLowerInvariant();
 | 
			
		||||
            using (var img = Image.Load<Rgba32>(curImg, out var format))
 | 
			
		||||
            {
 | 
			
		||||
                // choose font size based on the image height, so that it's visible
 | 
			
		||||
                var font = _fonts.NotoSans.CreateFont(img.Height / 12, FontStyle.Bold);
 | 
			
		||||
                img.Mutate(x =>
 | 
			
		||||
                {
 | 
			
		||||
                    // measure the size of the text to be drawing
 | 
			
		||||
                    var size = TextMeasurer.Measure(pass, new RendererOptions(font, new PointF(0, 0)));
 | 
			
		||||
 | 
			
		||||
                    // fill the background with black, add 5 pixels on each side to make it look better
 | 
			
		||||
                    x.FillPolygon(Color.ParseHex("00000080"),
 | 
			
		||||
                        new PointF(0, 0),
 | 
			
		||||
                        new PointF(size.Width + 5, 0),
 | 
			
		||||
                        new PointF(size.Width + 5, size.Height + 10),
 | 
			
		||||
                        new PointF(0, size.Height + 10));
 | 
			
		||||
 | 
			
		||||
                    // draw the password over the background
 | 
			
		||||
                    x.DrawText(pass,
 | 
			
		||||
                        font,
 | 
			
		||||
                        SixLabors.ImageSharp.Color.White,
 | 
			
		||||
                        new PointF(0, 0));
 | 
			
		||||
                });
 | 
			
		||||
                // return image as a stream for easy sending
 | 
			
		||||
                return (img.ToStream(format), format.FileExtensions.FirstOrDefault() ?? "png");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task PotentialFlowerGeneration(IUserMessage imsg)
 | 
			
		||||
        {
 | 
			
		||||
            var msg = imsg as SocketUserMessage;
 | 
			
		||||
            if (msg == null || msg.Author.IsBot)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            if (!(imsg.Channel is ITextChannel channel))
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            if (!_generationChannels.Contains(channel.Id))
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var config = _gss.Data;
 | 
			
		||||
                    var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue);
 | 
			
		||||
                    var rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
                    if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown) < lastGeneration) //recently generated in this channel, don't generate again
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    var num = rng.Next(1, 101) + config.Generation.Chance * 100;
 | 
			
		||||
                    if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration))
 | 
			
		||||
                    {
 | 
			
		||||
                        var dropAmount = config.Generation.MinAmount;
 | 
			
		||||
                        var dropAmountMax = config.Generation.MaxAmount;
 | 
			
		||||
 | 
			
		||||
                        if (dropAmountMax > dropAmount)
 | 
			
		||||
                            dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax + 1);
 | 
			
		||||
 | 
			
		||||
                        if (dropAmount > 0)
 | 
			
		||||
                        {
 | 
			
		||||
                            var prefix = _cmdHandler.GetPrefix(channel.Guild.Id);
 | 
			
		||||
                            var toSend = dropAmount == 1
 | 
			
		||||
                                ? GetText(channel.GuildId, "curgen_sn", config.Currency.Sign)
 | 
			
		||||
                                    + " " + GetText(channel.GuildId, "pick_sn", prefix)
 | 
			
		||||
                                : GetText(channel.GuildId, "curgen_pl", dropAmount, config.Currency.Sign)
 | 
			
		||||
                                    + " " + GetText(channel.GuildId, "pick_pl", prefix);
 | 
			
		||||
 | 
			
		||||
                            var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null;
 | 
			
		||||
 | 
			
		||||
                            IUserMessage sent;
 | 
			
		||||
                            using (var stream = GetRandomCurrencyImage(pw, out var ext))
 | 
			
		||||
                            {
 | 
			
		||||
                                sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend).ConfigureAwait(false);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            await AddPlantToDatabase(channel.GuildId,
 | 
			
		||||
                                channel.Id,
 | 
			
		||||
                                _client.CurrentUser.Id,
 | 
			
		||||
                                sent.Id,
 | 
			
		||||
                                dropAmount,
 | 
			
		||||
                                pw).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Generate a hexadecimal string from 1000 to ffff.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>A hexadecimal string from 1000 to ffff</returns>
 | 
			
		||||
        private string GenerateCurrencyPassword()
 | 
			
		||||
        {
 | 
			
		||||
            // generate a number from 1000 to ffff
 | 
			
		||||
            var num = _rng.Next(4096, 65536);
 | 
			
		||||
            // convert it to hexadecimal
 | 
			
		||||
            return num.ToString("x4");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<long> PickAsync(ulong gid, ITextChannel ch, ulong uid, string pass)
 | 
			
		||||
        {
 | 
			
		||||
            await pickLock.WaitAsync();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                long amount;
 | 
			
		||||
                ulong[] ids;
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    // this method will sum all plants with that password,
 | 
			
		||||
                    // remove them, and get messageids of the removed plants
 | 
			
		||||
 | 
			
		||||
                    (amount, ids) = uow.PlantedCurrency.RemoveSumAndGetMessageIdsFor(ch.Id, pass);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    if (amount > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        // give the picked currency to the user
 | 
			
		||||
                        await _cs.AddAsync(uid, "Picked currency", amount, gamble: false);
 | 
			
		||||
                    }
 | 
			
		||||
                    uow.SaveChanges();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    // delete all of the plant messages which have just been picked
 | 
			
		||||
                    var _ = ch.DeleteMessagesAsync(ids);
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
 | 
			
		||||
                // return the amount of currency the user picked
 | 
			
		||||
                return amount;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                pickLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<ulong?> SendPlantMessageAsync(ulong gid, IMessageChannel ch, string user, long amount, string pass)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                // get the text
 | 
			
		||||
                var prefix = _cmdHandler.GetPrefix(gid);
 | 
			
		||||
                var msgToSend = GetText(gid,
 | 
			
		||||
                    "planted",
 | 
			
		||||
                    Format.Bold(user),
 | 
			
		||||
                    amount + _gss.Data.Currency.Sign,
 | 
			
		||||
                    prefix);
 | 
			
		||||
 | 
			
		||||
                if (amount > 1)
 | 
			
		||||
                    msgToSend += " " + GetText(gid, "pick_pl", prefix);
 | 
			
		||||
                else
 | 
			
		||||
                    msgToSend += " " + GetText(gid, "pick_sn", prefix);
 | 
			
		||||
 | 
			
		||||
                //get the image
 | 
			
		||||
                using (var stream = GetRandomCurrencyImage(pass, out var ext))
 | 
			
		||||
                {
 | 
			
		||||
                    // send it
 | 
			
		||||
                    var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend).ConfigureAwait(false);
 | 
			
		||||
                    // return sent message's id (in order to be able to delete it when it's picked)
 | 
			
		||||
                    return msg.Id;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // if sending fails, return null as message id
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> PlantAsync(ulong gid, IMessageChannel ch, ulong uid, string user, long amount, string pass)
 | 
			
		||||
        {
 | 
			
		||||
            // normalize it - no more than 10 chars, uppercase
 | 
			
		||||
            pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant();
 | 
			
		||||
            // has to be either null or alphanumeric
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric())
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            // remove currency from the user who's planting
 | 
			
		||||
            if (await _cs.RemoveAsync(uid, "Planted currency", amount, gamble: false))
 | 
			
		||||
            {
 | 
			
		||||
                // try to send the message with the currency image
 | 
			
		||||
                var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass).ConfigureAwait(false);
 | 
			
		||||
                if (msgId == null)
 | 
			
		||||
                {
 | 
			
		||||
                    // if it fails it will return null, if it returns null, refund
 | 
			
		||||
                    await _cs.AddAsync(uid, "Planted currency refund", amount, gamble: false);
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                // if it doesn't fail, put the plant in the database for other people to pick
 | 
			
		||||
                await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass).ConfigureAwait(false);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            // if user doesn't have enough currency, fail
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task AddPlantToDatabase(ulong gid, ulong cid, ulong uid, ulong mid, long amount, string pass)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                uow.PlantedCurrency.Add(new PlantedCurrency
 | 
			
		||||
                {
 | 
			
		||||
                    Amount = amount,
 | 
			
		||||
                    GuildId = gid,
 | 
			
		||||
                    ChannelId = cid,
 | 
			
		||||
                    Password = pass,
 | 
			
		||||
                    UserId = uid,
 | 
			
		||||
                    MessageId = mid,
 | 
			
		||||
                });
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										519
									
								
								src/NadekoBot/Modules/Gambling/Services/WaifuService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										519
									
								
								src/NadekoBot/Modules/Gambling/Services/WaifuService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,519 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common.Waifu;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Repositories;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Services
 | 
			
		||||
{
 | 
			
		||||
    public class WaifuService : INService
 | 
			
		||||
    {
 | 
			
		||||
        public class FullWaifuInfo
 | 
			
		||||
        {
 | 
			
		||||
            public WaifuInfo Waifu { get; set; }
 | 
			
		||||
            public IEnumerable<string> Claims { get; set; }
 | 
			
		||||
            public int Divorces { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly IDataCache _cache;
 | 
			
		||||
        private readonly GamblingConfigService _gss;
 | 
			
		||||
 | 
			
		||||
        public WaifuService(DbService db, ICurrencyService cs, IDataCache cache,
 | 
			
		||||
            GamblingConfigService gss)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
            _cache = cache;
 | 
			
		||||
            _gss = gss;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> WaifuTransfer(IUser owner, ulong waifuId, IUser newOwner)
 | 
			
		||||
        {
 | 
			
		||||
            if (owner.Id == newOwner.Id || waifuId == newOwner.Id)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            var settings = _gss.Data;
 | 
			
		||||
            
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var waifu = uow.Waifus.ByWaifuUserId(waifuId);
 | 
			
		||||
                var ownerUser = uow.DiscordUsers.GetOrCreate(owner);
 | 
			
		||||
 | 
			
		||||
                // owner has to be the owner of the waifu
 | 
			
		||||
                if (waifu == null || waifu.ClaimerId != ownerUser.Id)
 | 
			
		||||
                    return false;
 | 
			
		||||
                
 | 
			
		||||
                // if waifu likes the person, gotta pay the penalty
 | 
			
		||||
                if (waifu.AffinityId == ownerUser.Id)
 | 
			
		||||
                {
 | 
			
		||||
                    if (!await _cs.RemoveAsync(owner.Id,
 | 
			
		||||
                        "Waifu Transfer - affinity penalty", 
 | 
			
		||||
                        (int)(waifu.Price * 0.6),
 | 
			
		||||
                        true))
 | 
			
		||||
                    {
 | 
			
		||||
                        // unable to pay 60% penalty
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    waifu.Price = (int)(waifu.Price * 0.7); // half of 60% = 30% price reduction
 | 
			
		||||
                    if (waifu.Price < settings.Waifu.MinPrice)
 | 
			
		||||
                        waifu.Price = settings.Waifu.MinPrice;
 | 
			
		||||
                }
 | 
			
		||||
                else // if not, pay 10% fee
 | 
			
		||||
                {
 | 
			
		||||
                    if (!await _cs.RemoveAsync(owner.Id, "Waifu Transfer", waifu.Price / 10, gamble: true))
 | 
			
		||||
                    {
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    waifu.Price = (int) (waifu.Price * 0.95); // half of 10% = 5% price reduction
 | 
			
		||||
                    if (waifu.Price < settings.Waifu.MinPrice)
 | 
			
		||||
                        waifu.Price = settings.Waifu.MinPrice;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //new claimerId is the id of the new owner
 | 
			
		||||
                var newOwnerUser = uow.DiscordUsers.GetOrCreate(newOwner);
 | 
			
		||||
                waifu.ClaimerId = newOwnerUser.Id;
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public int GetResetPrice(IUser user)
 | 
			
		||||
        {
 | 
			
		||||
            var settings = _gss.Data;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var waifu = uow.Waifus.ByWaifuUserId(user.Id);
 | 
			
		||||
 | 
			
		||||
                if (waifu == null)
 | 
			
		||||
                    return settings.Waifu.MinPrice;
 | 
			
		||||
 | 
			
		||||
                var divorces = uow._context.WaifuUpdates.Count(x => x.Old != null &&
 | 
			
		||||
                                                                    x.Old.UserId == user.Id &&
 | 
			
		||||
                                                                    x.UpdateType == WaifuUpdateType.Claimed &&
 | 
			
		||||
                                                                    x.New == null);
 | 
			
		||||
                var affs = uow._context.WaifuUpdates
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Where(w => w.User.UserId == user.Id && w.UpdateType == WaifuUpdateType.AffinityChanged &&
 | 
			
		||||
                                w.New != null)
 | 
			
		||||
                    .ToList()
 | 
			
		||||
                    .GroupBy(x => x.New)
 | 
			
		||||
                    .Count();
 | 
			
		||||
 | 
			
		||||
                return (int) Math.Ceiling(waifu.Price * 1.25f) +
 | 
			
		||||
                       ((divorces + affs + 2) * settings.Waifu.Multipliers.WaifuReset);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> TryReset(IUser user)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var price = GetResetPrice(user);
 | 
			
		||||
                if (!await _cs.RemoveAsync(user.Id, "Waifu Reset", price, gamble: true))
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                var affs = uow._context.WaifuUpdates
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Where(w => w.User.UserId == user.Id
 | 
			
		||||
                                && w.UpdateType == WaifuUpdateType.AffinityChanged
 | 
			
		||||
                                && w.New != null);
 | 
			
		||||
 | 
			
		||||
                var divorces = uow._context.WaifuUpdates
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Where(x => x.Old != null &&
 | 
			
		||||
                                x.Old.UserId == user.Id &&
 | 
			
		||||
                                x.UpdateType == WaifuUpdateType.Claimed &&
 | 
			
		||||
                                x.New == null);
 | 
			
		||||
 | 
			
		||||
                //reset changes of heart to 0
 | 
			
		||||
                uow._context.WaifuUpdates.RemoveRange(affs);
 | 
			
		||||
                //reset divorces to 0
 | 
			
		||||
                uow._context.WaifuUpdates.RemoveRange(divorces);
 | 
			
		||||
                var waifu = uow.Waifus.ByWaifuUserId(user.Id);
 | 
			
		||||
                //reset price, remove items
 | 
			
		||||
                //remove owner, remove affinity
 | 
			
		||||
                waifu.Price = 50;
 | 
			
		||||
                waifu.Items.Clear();
 | 
			
		||||
                waifu.ClaimerId = null;
 | 
			
		||||
                waifu.AffinityId = null;
 | 
			
		||||
 | 
			
		||||
                //wives stay though
 | 
			
		||||
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<(WaifuInfo, bool, WaifuClaimResult)> ClaimWaifuAsync(IUser user, IUser target, int amount)
 | 
			
		||||
        {
 | 
			
		||||
            var settings = _gss.Data;
 | 
			
		||||
            WaifuClaimResult result;
 | 
			
		||||
            WaifuInfo w;
 | 
			
		||||
            bool isAffinity;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                w = uow.Waifus.ByWaifuUserId(target.Id);
 | 
			
		||||
                isAffinity = (w?.Affinity?.UserId == user.Id);
 | 
			
		||||
                if (w == null)
 | 
			
		||||
                {
 | 
			
		||||
                    var claimer = uow.DiscordUsers.GetOrCreate(user);
 | 
			
		||||
                    var waifu = uow.DiscordUsers.GetOrCreate(target);
 | 
			
		||||
                    if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = WaifuClaimResult.NotEnoughFunds;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        uow.Waifus.Add(w = new WaifuInfo()
 | 
			
		||||
                        {
 | 
			
		||||
                            Waifu = waifu,
 | 
			
		||||
                            Claimer = claimer,
 | 
			
		||||
                            Affinity = null,
 | 
			
		||||
                            Price = amount
 | 
			
		||||
                        });
 | 
			
		||||
                        uow._context.WaifuUpdates.Add(new WaifuUpdate()
 | 
			
		||||
                        {
 | 
			
		||||
                            User = waifu,
 | 
			
		||||
                            Old = null,
 | 
			
		||||
                            New = claimer,
 | 
			
		||||
                            UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                        });
 | 
			
		||||
                        result = WaifuClaimResult.Success;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (isAffinity && amount > w.Price * settings.Waifu.Multipliers.CrushClaim)
 | 
			
		||||
                {
 | 
			
		||||
                    if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = WaifuClaimResult.NotEnoughFunds;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        var oldClaimer = w.Claimer;
 | 
			
		||||
                        w.Claimer = uow.DiscordUsers.GetOrCreate(user);
 | 
			
		||||
                        w.Price = amount + (amount / 4);
 | 
			
		||||
                        result = WaifuClaimResult.Success;
 | 
			
		||||
 | 
			
		||||
                        uow._context.WaifuUpdates.Add(new WaifuUpdate()
 | 
			
		||||
                        {
 | 
			
		||||
                            User = w.Waifu,
 | 
			
		||||
                            Old = oldClaimer,
 | 
			
		||||
                            New = w.Claimer,
 | 
			
		||||
                            UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity
 | 
			
		||||
                {
 | 
			
		||||
                    if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = WaifuClaimResult.NotEnoughFunds;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        var oldClaimer = w.Claimer;
 | 
			
		||||
                        w.Claimer = uow.DiscordUsers.GetOrCreate(user);
 | 
			
		||||
                        w.Price = amount;
 | 
			
		||||
                        result = WaifuClaimResult.Success;
 | 
			
		||||
 | 
			
		||||
                        uow._context.WaifuUpdates.Add(new WaifuUpdate()
 | 
			
		||||
                        {
 | 
			
		||||
                            User = w.Waifu,
 | 
			
		||||
                            Old = oldClaimer,
 | 
			
		||||
                            New = w.Claimer,
 | 
			
		||||
                            UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                    result = WaifuClaimResult.InsufficientAmount;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (w, isAffinity, result);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<(DiscordUser, bool, TimeSpan?)> ChangeAffinityAsync(IUser user, IGuildUser target)
 | 
			
		||||
        {
 | 
			
		||||
            DiscordUser oldAff = null;
 | 
			
		||||
            var success = false;
 | 
			
		||||
            TimeSpan? remaining = null;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var w = uow.Waifus.ByWaifuUserId(user.Id);
 | 
			
		||||
                var newAff = target == null ? null : uow.DiscordUsers.GetOrCreate(target);
 | 
			
		||||
                if (w?.Affinity?.UserId == target?.Id)
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
                else if (!_cache.TryAddAffinityCooldown(user.Id, out remaining))
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
                else if (w == null)
 | 
			
		||||
                {
 | 
			
		||||
                    var thisUser = uow.DiscordUsers.GetOrCreate(user);
 | 
			
		||||
                    uow.Waifus.Add(new WaifuInfo()
 | 
			
		||||
                    {
 | 
			
		||||
                        Affinity = newAff,
 | 
			
		||||
                        Waifu = thisUser,
 | 
			
		||||
                        Price = 1,
 | 
			
		||||
                        Claimer = null
 | 
			
		||||
                    });
 | 
			
		||||
                    success = true;
 | 
			
		||||
 | 
			
		||||
                    uow._context.WaifuUpdates.Add(new WaifuUpdate()
 | 
			
		||||
                    {
 | 
			
		||||
                        User = thisUser,
 | 
			
		||||
                        Old = null,
 | 
			
		||||
                        New = newAff,
 | 
			
		||||
                        UpdateType = WaifuUpdateType.AffinityChanged
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (w.Affinity != null)
 | 
			
		||||
                        oldAff = w.Affinity;
 | 
			
		||||
                    w.Affinity = newAff;
 | 
			
		||||
                    success = true;
 | 
			
		||||
 | 
			
		||||
                    uow._context.WaifuUpdates.Add(new WaifuUpdate()
 | 
			
		||||
                    {
 | 
			
		||||
                        User = w.Waifu,
 | 
			
		||||
                        Old = oldAff,
 | 
			
		||||
                        New = newAff,
 | 
			
		||||
                        UpdateType = WaifuUpdateType.AffinityChanged
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (oldAff, success, remaining);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<WaifuLbResult> GetTopWaifusAtPage(int page)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                return uow.Waifus.GetTop(9, page * 9);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public ulong GetWaifuUserId(ulong ownerId, string name)
 | 
			
		||||
        {
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            return uow.Waifus.GetWaifuUserId(ownerId, name);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
 | 
			
		||||
        {
 | 
			
		||||
            DivorceResult result;
 | 
			
		||||
            TimeSpan? remaining = null;
 | 
			
		||||
            long amount = 0;
 | 
			
		||||
            WaifuInfo w = null;
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                w = uow.Waifus.ByWaifuUserId(targetId);
 | 
			
		||||
                var now = DateTime.UtcNow;
 | 
			
		||||
                if (w?.Claimer == null || w.Claimer.UserId != user.Id)
 | 
			
		||||
                    result = DivorceResult.NotYourWife;
 | 
			
		||||
                else if (!_cache.TryAddDivorceCooldown(user.Id, out remaining))
 | 
			
		||||
                {
 | 
			
		||||
                    result = DivorceResult.Cooldown;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    amount = w.Price / 2;
 | 
			
		||||
 | 
			
		||||
                    if (w.Affinity?.UserId == user.Id)
 | 
			
		||||
                    {
 | 
			
		||||
                        await _cs.AddAsync(w.Waifu.UserId, "Waifu Compensation", amount, gamble: true);
 | 
			
		||||
                        w.Price = (int) Math.Floor(w.Price * _gss.Data.Waifu.Multipliers.DivorceNewValue);
 | 
			
		||||
                        result = DivorceResult.SucessWithPenalty;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        await _cs.AddAsync(user.Id, "Waifu Refund", amount, gamble: true);
 | 
			
		||||
 | 
			
		||||
                        result = DivorceResult.Success;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var oldClaimer = w.Claimer;
 | 
			
		||||
                    w.Claimer = null;
 | 
			
		||||
 | 
			
		||||
                    uow._context.WaifuUpdates.Add(new WaifuUpdate()
 | 
			
		||||
                    {
 | 
			
		||||
                        User = w.Waifu,
 | 
			
		||||
                        Old = oldClaimer,
 | 
			
		||||
                        New = null,
 | 
			
		||||
                        UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (w, result, amount, remaining);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await _cs.RemoveAsync(from, "Bought waifu item", itemObj.Price, gamble: true))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var w = uow.Waifus.ByWaifuUserId(giftedWaifu.Id, 
 | 
			
		||||
                    set => set.Include(x => x.Items)
 | 
			
		||||
                        .Include(x => x.Claimer));
 | 
			
		||||
                if (w == null)
 | 
			
		||||
                {
 | 
			
		||||
                    uow.Waifus.Add(w = new WaifuInfo()
 | 
			
		||||
                    {
 | 
			
		||||
                        Affinity = null,
 | 
			
		||||
                        Claimer = null,
 | 
			
		||||
                        Price = 1,
 | 
			
		||||
                        Waifu = uow.DiscordUsers.GetOrCreate(giftedWaifu),
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                w.Items.Add(new WaifuItem()
 | 
			
		||||
                {
 | 
			
		||||
                    Name = itemObj.Name.ToLowerInvariant(),
 | 
			
		||||
                    ItemEmoji = itemObj.ItemEmoji,
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
                if (w.Claimer?.UserId == from.Id)
 | 
			
		||||
                {
 | 
			
		||||
                    w.Price += (int) (itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    w.Price += itemObj.Price / 2;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await uow.SaveChangesAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public WaifuInfoStats GetFullWaifuInfoAsync(ulong targetId)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var wi = uow.Waifus.GetWaifuInfo(targetId);
 | 
			
		||||
                if (wi is null)
 | 
			
		||||
                {
 | 
			
		||||
                    wi = new WaifuInfoStats
 | 
			
		||||
                    {
 | 
			
		||||
                        AffinityCount = 0,
 | 
			
		||||
                        AffinityName = null,
 | 
			
		||||
                        ClaimCount = 0,
 | 
			
		||||
                        ClaimerName = null,
 | 
			
		||||
                        Claims = new List<string>(),
 | 
			
		||||
                        Fans = new List<string>(),
 | 
			
		||||
                        DivorceCount = 0,
 | 
			
		||||
                        FullName = null,
 | 
			
		||||
                        Items = new List<WaifuItem>(),
 | 
			
		||||
                        Price = 1
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return wi;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        public WaifuInfoStats GetFullWaifuInfoAsync(IGuildUser target)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var du = uow.DiscordUsers.GetOrCreate(target);
 | 
			
		||||
                
 | 
			
		||||
                return GetFullWaifuInfoAsync(target.Id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string GetClaimTitle(int count)
 | 
			
		||||
        {
 | 
			
		||||
            ClaimTitle title;
 | 
			
		||||
            if (count == 0)
 | 
			
		||||
                title = ClaimTitle.Lonely;
 | 
			
		||||
            else if (count == 1)
 | 
			
		||||
                title = ClaimTitle.Devoted;
 | 
			
		||||
            else if (count < 3)
 | 
			
		||||
                title = ClaimTitle.Rookie;
 | 
			
		||||
            else if (count < 6)
 | 
			
		||||
                title = ClaimTitle.Schemer;
 | 
			
		||||
            else if (count < 10)
 | 
			
		||||
                title = ClaimTitle.Dilettante;
 | 
			
		||||
            else if (count < 17)
 | 
			
		||||
                title = ClaimTitle.Intermediate;
 | 
			
		||||
            else if (count < 25)
 | 
			
		||||
                title = ClaimTitle.Seducer;
 | 
			
		||||
            else if (count < 35)
 | 
			
		||||
                title = ClaimTitle.Expert;
 | 
			
		||||
            else if (count < 50)
 | 
			
		||||
                title = ClaimTitle.Veteran;
 | 
			
		||||
            else if (count < 75)
 | 
			
		||||
                title = ClaimTitle.Incubis;
 | 
			
		||||
            else if (count < 100)
 | 
			
		||||
                title = ClaimTitle.Harem_King;
 | 
			
		||||
            else
 | 
			
		||||
                title = ClaimTitle.Harem_God;
 | 
			
		||||
 | 
			
		||||
            return title.ToString().Replace('_', ' ');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string GetAffinityTitle(int count)
 | 
			
		||||
        {
 | 
			
		||||
            AffinityTitle title;
 | 
			
		||||
            if (count < 1)
 | 
			
		||||
                title = AffinityTitle.Pure;
 | 
			
		||||
            else if (count < 2)
 | 
			
		||||
                title = AffinityTitle.Faithful;
 | 
			
		||||
            else if (count < 4)
 | 
			
		||||
                title = AffinityTitle.Playful;
 | 
			
		||||
            else if (count < 8)
 | 
			
		||||
                title = AffinityTitle.Cheater;
 | 
			
		||||
            else if (count < 11)
 | 
			
		||||
                title = AffinityTitle.Tainted;
 | 
			
		||||
            else if (count < 15)
 | 
			
		||||
                title = AffinityTitle.Corrupted;
 | 
			
		||||
            else if (count < 20)
 | 
			
		||||
                title = AffinityTitle.Lewd;
 | 
			
		||||
            else if (count < 25)
 | 
			
		||||
                title = AffinityTitle.Sloot;
 | 
			
		||||
            else if (count < 35)
 | 
			
		||||
                title = AffinityTitle.Depraved;
 | 
			
		||||
            else
 | 
			
		||||
                title = AffinityTitle.Harlot;
 | 
			
		||||
 | 
			
		||||
            return title.ToString().Replace('_', ' ');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IReadOnlyList<WaifuItemModel> GetWaifuItems()
 | 
			
		||||
        {
 | 
			
		||||
            var conf = _gss.Data;
 | 
			
		||||
            return _gss.Data.Waifu.Items
 | 
			
		||||
                .Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name))
 | 
			
		||||
                .ToList();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										466
									
								
								src/NadekoBot/Modules/Gambling/ShopCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										466
									
								
								src/NadekoBot/Modules/Gambling/ShopCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,466 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class ShopCommands : GamblingSubmodule<IShopService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly DbService _db;
 | 
			
		||||
            private readonly ICurrencyService _cs;
 | 
			
		||||
 | 
			
		||||
            public enum Role
 | 
			
		||||
            {
 | 
			
		||||
                Role
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public enum List
 | 
			
		||||
            {
 | 
			
		||||
                List
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public ShopCommands(DbService db, ICurrencyService cs, GamblingConfigService gamblingConf)
 | 
			
		||||
                : base(gamblingConf) 
 | 
			
		||||
            {
 | 
			
		||||
                _db = db;
 | 
			
		||||
                _cs = cs;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            private Task ShopInternalAsync(int page = 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (page < 0)
 | 
			
		||||
                    throw new ArgumentOutOfRangeException(nameof(page));
 | 
			
		||||
                
 | 
			
		||||
                using var uow = _db.GetDbContext();
 | 
			
		||||
                var entries = uow.GuildConfigs.ForId(ctx.Guild.Id,
 | 
			
		||||
                        set => set.Include(x => x.ShopEntries)
 | 
			
		||||
                            .ThenInclude(x => x.Items)).ShopEntries
 | 
			
		||||
                        .ToIndexed();
 | 
			
		||||
                return ctx.SendPaginatedConfirmAsync(page, (curPage) =>
 | 
			
		||||
                {
 | 
			
		||||
                    var theseEntries = entries.Skip(curPage * 9).Take(9).ToArray();
 | 
			
		||||
 | 
			
		||||
                    if (!theseEntries.Any())
 | 
			
		||||
                        return new EmbedBuilder().WithErrorColor()
 | 
			
		||||
                            .WithDescription(GetText("shop_none"));
 | 
			
		||||
                    var embed = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                        .WithTitle(GetText("shop", CurrencySign));
 | 
			
		||||
 | 
			
		||||
                    for (int i = 0; i < theseEntries.Length; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var entry = theseEntries[i];
 | 
			
		||||
                        embed.AddField(
 | 
			
		||||
                            $"#{curPage * 9 + i + 1} - {entry.Price}{CurrencySign}",
 | 
			
		||||
                            EntryToString(entry),
 | 
			
		||||
                            true);
 | 
			
		||||
                    }
 | 
			
		||||
                    return embed;
 | 
			
		||||
                }, entries.Count, 9, true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public Task Shop(int page = 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (--page < 0)
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
                
 | 
			
		||||
                return ShopInternalAsync(page);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Buy(int index)
 | 
			
		||||
            {
 | 
			
		||||
                index -= 1;
 | 
			
		||||
                if (index < 0)
 | 
			
		||||
                    return;
 | 
			
		||||
                ShopEntry entry;
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var config = uow.GuildConfigs.ForId(ctx.Guild.Id, set => set
 | 
			
		||||
                        .Include(x => x.ShopEntries)
 | 
			
		||||
                        .ThenInclude(x => x.Items));
 | 
			
		||||
                    var entries = new IndexedCollection<ShopEntry>(config.ShopEntries);
 | 
			
		||||
                    entry = entries.ElementAtOrDefault(index);
 | 
			
		||||
                    uow.SaveChanges();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (entry == null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("shop_item_not_found").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (entry.Type == ShopEntryType.Role)
 | 
			
		||||
                {
 | 
			
		||||
                    var guser = (IGuildUser)ctx.User;
 | 
			
		||||
                    var role = ctx.Guild.GetRole(entry.RoleId);
 | 
			
		||||
 | 
			
		||||
                    if (role == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("shop_role_not_found").ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    if (guser.RoleIds.Any(id => id == role.Id))
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("shop_role_already_bought").ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price).ConfigureAwait(false))
 | 
			
		||||
                    {
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            await guser.AddRoleAsync(role).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                        catch (Exception ex)
 | 
			
		||||
                        {
 | 
			
		||||
                            Log.Warning(ex, "Error adding shop role");
 | 
			
		||||
                            await _cs.AddAsync(ctx.User.Id, $"Shop error refund", entry.Price).ConfigureAwait(false);
 | 
			
		||||
                            await ReplyErrorLocalizedAsync("shop_role_purchase_error").ConfigureAwait(false);
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                        var profit = GetProfitAmount(entry.Price);
 | 
			
		||||
                        await _cs.AddAsync(entry.AuthorId, $"Shop sell item - {entry.Type}", profit).ConfigureAwait(false);
 | 
			
		||||
                        await _cs.AddAsync(ctx.Client.CurrentUser.Id, $"Shop sell item - cut", entry.Price - profit).ConfigureAwait(false);
 | 
			
		||||
                        await ReplyConfirmLocalizedAsync("shop_role_purchase", Format.Bold(role.Name)).ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (entry.Type == ShopEntryType.List)
 | 
			
		||||
                {
 | 
			
		||||
                    if (entry.Items.Count == 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("out_of_stock").ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var item = entry.Items.ToArray()[new NadekoRandom().Next(0, entry.Items.Count)];
 | 
			
		||||
 | 
			
		||||
                    if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price).ConfigureAwait(false))
 | 
			
		||||
                    {
 | 
			
		||||
                        using (var uow = _db.GetDbContext())
 | 
			
		||||
                        {
 | 
			
		||||
                            var x = uow._context.Set<ShopEntryItem>().Remove(item);
 | 
			
		||||
                            uow.SaveChanges();
 | 
			
		||||
                        }
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            await (await ctx.User.GetOrCreateDMChannelAsync().ConfigureAwait(false))
 | 
			
		||||
                                .EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                                .WithTitle(GetText("shop_purchase", ctx.Guild.Name))
 | 
			
		||||
                                .AddField(efb => efb.WithName(GetText("item")).WithValue(item.Text).WithIsInline(false))
 | 
			
		||||
                                .AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
 | 
			
		||||
                                .AddField(efb => efb.WithName(GetText("name")).WithValue(entry.Name).WithIsInline(true)))
 | 
			
		||||
                                .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                            await _cs.AddAsync(entry.AuthorId,
 | 
			
		||||
                                    $"Shop sell item - {entry.Name}",
 | 
			
		||||
                                    GetProfitAmount(entry.Price)).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                        catch
 | 
			
		||||
                        {
 | 
			
		||||
                            await _cs.AddAsync(ctx.User.Id,
 | 
			
		||||
                                $"Shop error refund - {entry.Name}",
 | 
			
		||||
                                entry.Price).ConfigureAwait(false);
 | 
			
		||||
                            using (var uow = _db.GetDbContext())
 | 
			
		||||
                            {
 | 
			
		||||
                                var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.ForId(ctx.Guild.Id,
 | 
			
		||||
                                    set => set.Include(x => x.ShopEntries)
 | 
			
		||||
                                              .ThenInclude(x => x.Items)).ShopEntries);
 | 
			
		||||
                                entry = entries.ElementAtOrDefault(index);
 | 
			
		||||
                                if (entry != null)
 | 
			
		||||
                                {
 | 
			
		||||
                                    if (entry.Items.Add(item))
 | 
			
		||||
                                    {
 | 
			
		||||
                                        uow.SaveChanges();
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            await ReplyErrorLocalizedAsync("shop_buy_error").ConfigureAwait(false);
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                        await ReplyConfirmLocalizedAsync("shop_item_purchase").ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private static long GetProfitAmount(int price) =>
 | 
			
		||||
                (int)(Math.Ceiling(0.90 * price));
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
            public async Task ShopAdd(Role _, int price, [Leftover] IRole role)
 | 
			
		||||
            {
 | 
			
		||||
                var entry = new ShopEntry()
 | 
			
		||||
                {
 | 
			
		||||
                    Name = "-",
 | 
			
		||||
                    Price = price,
 | 
			
		||||
                    Type = ShopEntryType.Role,
 | 
			
		||||
                    AuthorId = ctx.User.Id,
 | 
			
		||||
                    RoleId = role.Id,
 | 
			
		||||
                    RoleName = role.Name
 | 
			
		||||
                };
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.ForId(ctx.Guild.Id,
 | 
			
		||||
                        set => set.Include(x => x.ShopEntries)
 | 
			
		||||
                                  .ThenInclude(x => x.Items)).ShopEntries)
 | 
			
		||||
                    {
 | 
			
		||||
                        entry
 | 
			
		||||
                    };
 | 
			
		||||
                    uow.GuildConfigs.ForId(ctx.Guild.Id, set => set).ShopEntries = entries;
 | 
			
		||||
                    uow.SaveChanges();
 | 
			
		||||
                }
 | 
			
		||||
                await ctx.Channel.EmbedAsync(EntryToEmbed(entry)
 | 
			
		||||
                    .WithTitle(GetText("shop_item_add"))).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task ShopAdd(List _, int price, [Leftover]string name)
 | 
			
		||||
            {
 | 
			
		||||
                var entry = new ShopEntry()
 | 
			
		||||
                {
 | 
			
		||||
                    Name = name.TrimTo(100),
 | 
			
		||||
                    Price = price,
 | 
			
		||||
                    Type = ShopEntryType.List,
 | 
			
		||||
                    AuthorId = ctx.User.Id,
 | 
			
		||||
                    Items = new HashSet<ShopEntryItem>(),
 | 
			
		||||
                };
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.ForId(ctx.Guild.Id,
 | 
			
		||||
                        set => set.Include(x => x.ShopEntries)
 | 
			
		||||
                                  .ThenInclude(x => x.Items)).ShopEntries)
 | 
			
		||||
                    {
 | 
			
		||||
                        entry
 | 
			
		||||
                    };
 | 
			
		||||
                    uow.GuildConfigs.ForId(ctx.Guild.Id, set => set).ShopEntries = entries;
 | 
			
		||||
                    uow.SaveChanges();
 | 
			
		||||
                }
 | 
			
		||||
                await ctx.Channel.EmbedAsync(EntryToEmbed(entry)
 | 
			
		||||
                    .WithTitle(GetText("shop_item_add"))).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task ShopListAdd(int index, [Leftover] string itemText)
 | 
			
		||||
            {
 | 
			
		||||
                index -= 1;
 | 
			
		||||
                if (index < 0)
 | 
			
		||||
                    return;
 | 
			
		||||
                var item = new ShopEntryItem()
 | 
			
		||||
                {
 | 
			
		||||
                    Text = itemText
 | 
			
		||||
                };
 | 
			
		||||
                ShopEntry entry;
 | 
			
		||||
                bool rightType = false;
 | 
			
		||||
                bool added = false;
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.ForId(ctx.Guild.Id,
 | 
			
		||||
                        set => set.Include(x => x.ShopEntries)
 | 
			
		||||
                                  .ThenInclude(x => x.Items)).ShopEntries);
 | 
			
		||||
                    entry = entries.ElementAtOrDefault(index);
 | 
			
		||||
                    if (entry != null && (rightType = (entry.Type == ShopEntryType.List)))
 | 
			
		||||
                    {
 | 
			
		||||
                        if (added = entry.Items.Add(item))
 | 
			
		||||
                        {
 | 
			
		||||
                            uow.SaveChanges();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (entry == null)
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("shop_item_not_found").ConfigureAwait(false);
 | 
			
		||||
                else if (!rightType)
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("shop_item_wrong_type").ConfigureAwait(false);
 | 
			
		||||
                else if (added == false)
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("shop_list_item_not_unique").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("shop_list_item_added").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task ShopRemove(int index)
 | 
			
		||||
            {
 | 
			
		||||
                index -= 1;
 | 
			
		||||
                if (index < 0)
 | 
			
		||||
                    return;
 | 
			
		||||
                ShopEntry removed;
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var config = uow.GuildConfigs.ForId(ctx.Guild.Id, set => set
 | 
			
		||||
                        .Include(x => x.ShopEntries)
 | 
			
		||||
                        .ThenInclude(x => x.Items));
 | 
			
		||||
 | 
			
		||||
                    var entries = new IndexedCollection<ShopEntry>(config.ShopEntries);
 | 
			
		||||
                    removed = entries.ElementAtOrDefault(index);
 | 
			
		||||
                    if (removed != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        uow._context.RemoveRange(removed.Items);
 | 
			
		||||
                        uow._context.Remove(removed);
 | 
			
		||||
                        uow.SaveChanges();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (removed == null)
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("shop_item_not_found").ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ctx.Channel.EmbedAsync(EntryToEmbed(removed)
 | 
			
		||||
                        .WithTitle(GetText("shop_item_rm"))).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task ShopChangePrice(int index, int price)
 | 
			
		||||
            {
 | 
			
		||||
                if (--index < 0 || price <= 0)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var succ = await _service.ChangeEntryPriceAsync(Context.Guild.Id, index, price);
 | 
			
		||||
                if (succ)
 | 
			
		||||
                {
 | 
			
		||||
                    await ShopInternalAsync(index / 9);
 | 
			
		||||
                    await ctx.OkAsync();
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.ErrorAsync();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task ShopChangeName(int index, [Leftover] string newName)
 | 
			
		||||
            {
 | 
			
		||||
                if (--index < 0 || string.IsNullOrWhiteSpace(newName))
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
                var succ = await _service.ChangeEntryNameAsync(Context.Guild.Id, index, newName);
 | 
			
		||||
                if (succ)
 | 
			
		||||
                {
 | 
			
		||||
                    await ShopInternalAsync(index / 9);
 | 
			
		||||
                    await ctx.OkAsync();
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.ErrorAsync();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task ShopSwap(int index1, int index2)
 | 
			
		||||
            {
 | 
			
		||||
                if (--index1 < 0 || --index2 < 0 || index1 == index2)
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
                var succ = await _service.SwapEntriesAsync(Context.Guild.Id, index1, index2);
 | 
			
		||||
                if (succ)
 | 
			
		||||
                {
 | 
			
		||||
                    await ShopInternalAsync(index1 / 9);
 | 
			
		||||
                    await ctx.OkAsync();
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.ErrorAsync();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
            public async Task ShopMove(int fromIndex, int toIndex)
 | 
			
		||||
            {
 | 
			
		||||
                if (--fromIndex < 0 || --toIndex < 0 || fromIndex == toIndex)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var succ = await _service.MoveEntryAsync(Context.Guild.Id, fromIndex, toIndex);
 | 
			
		||||
                if (succ)
 | 
			
		||||
                {
 | 
			
		||||
                    await ShopInternalAsync(toIndex / 9);
 | 
			
		||||
                    await ctx.OkAsync();
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.ErrorAsync();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            public EmbedBuilder EntryToEmbed(ShopEntry entry)
 | 
			
		||||
            {
 | 
			
		||||
                var embed = new EmbedBuilder().WithOkColor();
 | 
			
		||||
 | 
			
		||||
                if (entry.Type == ShopEntryType.Role)
 | 
			
		||||
                    return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"))).WithIsInline(true))
 | 
			
		||||
                            .AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
 | 
			
		||||
                            .AddField(efb => efb.WithName(GetText("type")).WithValue(entry.Type.ToString()).WithIsInline(true));
 | 
			
		||||
                else if (entry.Type == ShopEntryType.List)
 | 
			
		||||
                    return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(entry.Name).WithIsInline(true))
 | 
			
		||||
                            .AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
 | 
			
		||||
                            .AddField(efb => efb.WithName(GetText("type")).WithValue(GetText("random_unique_item")).WithIsInline(true));
 | 
			
		||||
                //else if (entry.Type == ShopEntryType.Infinite_List)
 | 
			
		||||
                //    return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(entry.RoleName))).WithIsInline(true))
 | 
			
		||||
                //            .AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
 | 
			
		||||
                //            .AddField(efb => efb.WithName(GetText("type")).WithValue(entry.Type.ToString()).WithIsInline(true));
 | 
			
		||||
                else return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public string EntryToString(ShopEntry entry)
 | 
			
		||||
            {
 | 
			
		||||
                if (entry.Type == ShopEntryType.Role)
 | 
			
		||||
                {
 | 
			
		||||
                    return GetText("shop_role", Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"));
 | 
			
		||||
                }
 | 
			
		||||
                else if (entry.Type == ShopEntryType.List)
 | 
			
		||||
                {
 | 
			
		||||
                    return GetText("unique_items_left", entry.Items.Count) + "\n" + entry.Name;
 | 
			
		||||
                }
 | 
			
		||||
                //else if (entry.Type == ShopEntryType.Infinite_List)
 | 
			
		||||
                //{
 | 
			
		||||
 | 
			
		||||
                //}
 | 
			
		||||
                return "";
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										234
									
								
								src/NadekoBot/Modules/Gambling/SlotCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								src/NadekoBot/Modules/Gambling/SlotCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,234 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using Image = SixLabors.ImageSharp.Image;
 | 
			
		||||
using SixLabors.ImageSharp.Processing;
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class SlotCommands : GamblingSubmodule<GamblingService>
 | 
			
		||||
        {
 | 
			
		||||
            private static long _totalBet;
 | 
			
		||||
            private static long _totalPaidOut;
 | 
			
		||||
 | 
			
		||||
            private static readonly HashSet<ulong> _runningUsers = new HashSet<ulong>();
 | 
			
		||||
 | 
			
		||||
            //here is a payout chart
 | 
			
		||||
            //https://lh6.googleusercontent.com/-i1hjAJy_kN4/UswKxmhrbPI/AAAAAAAAB1U/82wq_4ZZc-Y/DE6B0895-6FC1-48BE-AC4F-14D1B91AB75B.jpg
 | 
			
		||||
            //thanks to judge for helping me with this
 | 
			
		||||
 | 
			
		||||
            private readonly IImageCache _images;
 | 
			
		||||
            private readonly ICurrencyService _cs;
 | 
			
		||||
 | 
			
		||||
            public SlotCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gamb) : base(gamb)
 | 
			
		||||
            {
 | 
			
		||||
                _images = data.LocalImages;
 | 
			
		||||
                _cs = cs;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public sealed class SlotMachine
 | 
			
		||||
            {
 | 
			
		||||
                public const int MaxValue = 5;
 | 
			
		||||
 | 
			
		||||
                static readonly List<Func<int[], int>> _winningCombos = new List<Func<int[], int>>()
 | 
			
		||||
                {
 | 
			
		||||
                    //three flowers
 | 
			
		||||
                    (arr) => arr.All(a=>a==MaxValue) ? 30 : 0,
 | 
			
		||||
                    //three of the same
 | 
			
		||||
                    (arr) => !arr.Any(a => a != arr[0]) ? 10 : 0,
 | 
			
		||||
                    //two flowers
 | 
			
		||||
                    (arr) => arr.Count(a => a == MaxValue) == 2 ? 4 : 0,
 | 
			
		||||
                    //one flower
 | 
			
		||||
                    (arr) => arr.Any(a => a == MaxValue) ? 1 : 0,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                public static SlotResult Pull()
 | 
			
		||||
                {
 | 
			
		||||
                    var numbers = new int[3];
 | 
			
		||||
                    for (var i = 0; i < numbers.Length; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        numbers[i] = new NadekoRandom().Next(0, MaxValue + 1);
 | 
			
		||||
                    }
 | 
			
		||||
                    var multi = 0;
 | 
			
		||||
                    foreach (var t in _winningCombos)
 | 
			
		||||
                    {
 | 
			
		||||
                        multi = t(numbers);
 | 
			
		||||
                        if (multi != 0)
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return new SlotResult(numbers, multi);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                public struct SlotResult
 | 
			
		||||
                {
 | 
			
		||||
                    public int[] Numbers { get; }
 | 
			
		||||
                    public int Multiplier { get; }
 | 
			
		||||
                    public SlotResult(int[] nums, int multi)
 | 
			
		||||
                    {
 | 
			
		||||
                        Numbers = nums;
 | 
			
		||||
                        Multiplier = multi;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task SlotStats()
 | 
			
		||||
            {
 | 
			
		||||
                //i remembered to not be a moron
 | 
			
		||||
                var paid = _totalPaidOut;
 | 
			
		||||
                var bet = _totalBet;
 | 
			
		||||
 | 
			
		||||
                if (bet <= 0)
 | 
			
		||||
                    bet = 1;
 | 
			
		||||
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("Slot Stats")
 | 
			
		||||
                    .AddField(efb => efb.WithName("Total Bet").WithValue(bet.ToString()).WithIsInline(true))
 | 
			
		||||
                    .AddField(efb => efb.WithName("Paid Out").WithValue(paid.ToString()).WithIsInline(true))
 | 
			
		||||
                    .WithFooter(efb => efb.WithText($"Payout Rate: {paid * 1.0 / bet * 100:f4}%"));
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task SlotTest(int tests = 1000)
 | 
			
		||||
            {
 | 
			
		||||
                if (tests <= 0)
 | 
			
		||||
                    return;
 | 
			
		||||
                //multi vs how many times it occured
 | 
			
		||||
                var dict = new Dictionary<int, int>();
 | 
			
		||||
                for (int i = 0; i < tests; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    var res = SlotMachine.Pull();
 | 
			
		||||
                    if (dict.ContainsKey(res.Multiplier))
 | 
			
		||||
                        dict[res.Multiplier] += 1;
 | 
			
		||||
                    else
 | 
			
		||||
                        dict.Add(res.Multiplier, 1);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var sb = new StringBuilder();
 | 
			
		||||
                const int bet = 1;
 | 
			
		||||
                int payout = 0;
 | 
			
		||||
                foreach (var key in dict.Keys.OrderByDescending(x => x))
 | 
			
		||||
                {
 | 
			
		||||
                    sb.AppendLine($"x{key} occured {dict[key]} times. {dict[key] * 1.0f / tests * 100}%");
 | 
			
		||||
                    payout += key * dict[key];
 | 
			
		||||
                }
 | 
			
		||||
                await ctx.Channel.SendConfirmAsync("Slot Test Results", sb.ToString(),
 | 
			
		||||
                    footer: $"Total Bet: {tests * bet} | Payout: {payout * bet} | {payout * 1.0f / tests * 100}%").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task Slot(ShmartNumber amount)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_runningUsers.Add(ctx.User.Id))
 | 
			
		||||
                    return;
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (!await CheckBetMandatory(amount).ConfigureAwait(false))
 | 
			
		||||
                        return;
 | 
			
		||||
                    const int maxAmount = 9999;
 | 
			
		||||
                    if (amount > maxAmount)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("max_bet_limit", maxAmount + CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (!await _cs.RemoveAsync(ctx.User, "Slot Machine", amount, false, gamble: true).ConfigureAwait(false))
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    Interlocked.Add(ref _totalBet, amount.Value);
 | 
			
		||||
                    using (var bgImage = Image.Load(_images.SlotBackground))
 | 
			
		||||
                    {
 | 
			
		||||
                        var result = SlotMachine.Pull();
 | 
			
		||||
                        int[] numbers = result.Numbers;
 | 
			
		||||
 | 
			
		||||
                        for (int i = 0; i < 3; i++)
 | 
			
		||||
                        {
 | 
			
		||||
                            using (var randomImage = Image.Load(_images.SlotEmojis[numbers[i]]))
 | 
			
		||||
                            {
 | 
			
		||||
                                bgImage.Mutate(x => x.DrawImage(randomImage, new Point(95 + 142 * i, 330), new GraphicsOptions()));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        var won = amount * result.Multiplier;
 | 
			
		||||
                        var printWon = won;
 | 
			
		||||
                        var n = 0;
 | 
			
		||||
                        do
 | 
			
		||||
                        {
 | 
			
		||||
                            var digit = (int)(printWon % 10);
 | 
			
		||||
                            using (var img = Image.Load(_images.SlotNumbers[digit]))
 | 
			
		||||
                            {
 | 
			
		||||
                                bgImage.Mutate(x => x.DrawImage(img, new Point(230 - n * 16, 462), new GraphicsOptions()));
 | 
			
		||||
                            }
 | 
			
		||||
                            n++;
 | 
			
		||||
                        } while ((printWon /= 10) != 0);
 | 
			
		||||
 | 
			
		||||
                        var printAmount = amount;
 | 
			
		||||
                        n = 0;
 | 
			
		||||
                        do
 | 
			
		||||
                        {
 | 
			
		||||
                            var digit = (int)(printAmount % 10);
 | 
			
		||||
                            using (var img = Image.Load(_images.SlotNumbers[digit]))
 | 
			
		||||
                            {
 | 
			
		||||
                                bgImage.Mutate(x => x.DrawImage(img, new Point(395 - n * 16, 462), new GraphicsOptions()));
 | 
			
		||||
                            }
 | 
			
		||||
                            n++;
 | 
			
		||||
                        } while ((printAmount /= 10) != 0);
 | 
			
		||||
 | 
			
		||||
                        var msg = GetText("better_luck");
 | 
			
		||||
                        if (result.Multiplier != 0)
 | 
			
		||||
                        {
 | 
			
		||||
                            await _cs.AddAsync(ctx.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false, gamble: true).ConfigureAwait(false);
 | 
			
		||||
                            Interlocked.Add(ref _totalPaidOut, amount * result.Multiplier);
 | 
			
		||||
                            if (result.Multiplier == 1)
 | 
			
		||||
                                msg = GetText("slot_single", CurrencySign, 1);
 | 
			
		||||
                            else if (result.Multiplier == 4)
 | 
			
		||||
                                msg = GetText("slot_two", CurrencySign, 4);
 | 
			
		||||
                            else if (result.Multiplier == 10)
 | 
			
		||||
                                msg = GetText("slot_three", 10);
 | 
			
		||||
                            else if (result.Multiplier == 30)
 | 
			
		||||
                                msg = GetText("slot_jackpot", 30);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        using (var imgStream = bgImage.ToStream())
 | 
			
		||||
                        {
 | 
			
		||||
                            await ctx.Channel.SendFileAsync(imgStream, "result.png", ctx.User.Mention + " " + msg + $"\n`{GetText("slot_bet")}:`{amount} `{GetText("won")}:` {amount * result.Multiplier}{CurrencySign}").ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    var _ = Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        await Task.Delay(1000).ConfigureAwait(false);
 | 
			
		||||
                        _runningUsers.Remove(ctx.User.Id);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										362
									
								
								src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,362 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common.Waifu;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Internal;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class WaifuClaimCommands : GamblingSubmodule<WaifuService>
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            public WaifuClaimCommands(GamblingConfigService gamblingConfService) : base(gamblingConfService)
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task WaifuReset()
 | 
			
		||||
            {
 | 
			
		||||
                var price = _service.GetResetPrice(ctx.User);
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                        .WithTitle(GetText("waifu_reset_confirm"))
 | 
			
		||||
                        .WithDescription(GetText("waifu_reset_price", Format.Bold(price + CurrencySign)));
 | 
			
		||||
 | 
			
		||||
                if (!await PromptUserConfirmAsync(embed))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (await _service.TryReset(ctx.User))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("waifu_reset");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                await ReplyErrorLocalizedAsync("waifu_reset_fail");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task WaifuClaim(int amount, [Leftover]IUser target)
 | 
			
		||||
            {
 | 
			
		||||
                if (amount < _config.Waifu.MinPrice)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("waifu_isnt_cheap", _config.Waifu.MinPrice + CurrencySign);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (target.Id == ctx.User.Id)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("waifu_not_yourself");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var (w, isAffinity, result) = await _service.ClaimWaifuAsync(ctx.User, target, amount);
 | 
			
		||||
 | 
			
		||||
                if (result == WaifuClaimResult.InsufficientAmount)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("waifu_not_enough", Math.Ceiling(w.Price * (isAffinity ? 0.88f : 1.1f)));
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if (result == WaifuClaimResult.NotEnoughFunds)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("not_enough", CurrencySign);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                var msg = GetText("waifu_claimed",
 | 
			
		||||
                    Format.Bold(target.ToString()),
 | 
			
		||||
                    amount + CurrencySign);
 | 
			
		||||
                if (w.Affinity?.UserId == ctx.User.Id)
 | 
			
		||||
                    msg += "\n" + GetText("waifu_fulfilled", target, w.Price + CurrencySign);
 | 
			
		||||
                else
 | 
			
		||||
                    msg = " " + msg;
 | 
			
		||||
                await ctx.Channel.SendConfirmAsync(ctx.User.Mention + msg);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public async Task WaifuTransfer(ulong waifuId, IUser newOwner)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await _service.WaifuTransfer(ctx.User, waifuId, newOwner)
 | 
			
		||||
                )
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("waifu_transfer_fail");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("waifu_transfer_success",
 | 
			
		||||
                    Format.Bold(waifuId.ToString()),
 | 
			
		||||
                    Format.Bold(ctx.User.ToString()),
 | 
			
		||||
                    Format.Bold(newOwner.ToString()));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task WaifuTransfer(IUser waifu, IUser newOwner)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await _service.WaifuTransfer(ctx.User, waifu.Id, newOwner))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("waifu_transfer_fail");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("waifu_transfer_success",
 | 
			
		||||
                    Format.Bold(waifu.ToString()),
 | 
			
		||||
                    Format.Bold(ctx.User.ToString()),
 | 
			
		||||
                    Format.Bold(newOwner.ToString()));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(-1)]
 | 
			
		||||
            public Task Divorce([Leftover] string target)
 | 
			
		||||
            {
 | 
			
		||||
                var waifuUserId = _service.GetWaifuUserId(ctx.User.Id, target);
 | 
			
		||||
                if (waifuUserId == default)
 | 
			
		||||
                {
 | 
			
		||||
                    return ReplyErrorLocalizedAsync("waifu_not_yours");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return Divorce(waifuUserId);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public Task Divorce([Leftover]IGuildUser target) 
 | 
			
		||||
                => Divorce(target.Id);
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task Divorce([Leftover]ulong targetId)
 | 
			
		||||
            {
 | 
			
		||||
                if (targetId == ctx.User.Id)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var (w, result, amount, remaining) = await _service.DivorceWaifuAsync(ctx.User, targetId);
 | 
			
		||||
 | 
			
		||||
                if (result == DivorceResult.SucessWithPenalty)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("waifu_divorced_like", Format.Bold(w.Waifu.ToString()), amount + CurrencySign);
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == DivorceResult.Success)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("waifu_divorced_notlike", amount + CurrencySign);
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == DivorceResult.NotYourWife)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("waifu_not_yours");
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("waifu_recent_divorce",
 | 
			
		||||
                        Format.Bold(((int)remaining?.TotalHours).ToString()),
 | 
			
		||||
                        Format.Bold(remaining?.Minutes.ToString()));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task Affinity([Leftover]IGuildUser u = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (u?.Id == ctx.User.Id)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("waifu_egomaniac");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                var (oldAff, sucess, remaining) = await _service.ChangeAffinityAsync(ctx.User, u);
 | 
			
		||||
                if (!sucess)
 | 
			
		||||
                {
 | 
			
		||||
                    if (remaining != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("waifu_affinity_cooldown",
 | 
			
		||||
                            Format.Bold(((int)remaining?.TotalHours).ToString()),
 | 
			
		||||
                            Format.Bold(remaining?.Minutes.ToString()));
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        await ReplyErrorLocalizedAsync("waifu_affinity_already");
 | 
			
		||||
                    }
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if (u == null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("waifu_affinity_reset");
 | 
			
		||||
                }
 | 
			
		||||
                else if (oldAff == null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("waifu_affinity_set", Format.Bold(u.ToString()));
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("waifu_affinity_changed", Format.Bold(oldAff.ToString()), Format.Bold(u.ToString()));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task WaifuLb(int page = 1)
 | 
			
		||||
            {
 | 
			
		||||
                page--;
 | 
			
		||||
 | 
			
		||||
                if (page < 0)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (page > 100)
 | 
			
		||||
                    page = 100;
 | 
			
		||||
 | 
			
		||||
                var waifus = _service.GetTopWaifusAtPage(page);
 | 
			
		||||
 | 
			
		||||
                if (waifus.Count() == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("waifus_none");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithTitle(GetText("waifus_top_waifus"))
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                var i = 0;
 | 
			
		||||
                foreach (var w in waifus)
 | 
			
		||||
                {
 | 
			
		||||
                    var j = i++;
 | 
			
		||||
                    embed.AddField(efb => efb.WithName("#" + ((page * 9) + j + 1) + " - " + w.Price + CurrencySign).WithValue(w.ToString()).WithIsInline(false));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public Task WaifuInfo([Leftover]IUser target = null)
 | 
			
		||||
            {
 | 
			
		||||
                if (target == null)
 | 
			
		||||
                    target = ctx.User;
 | 
			
		||||
 | 
			
		||||
                return InternalWaifuInfo(target.Id, target.ToString());
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public Task WaifuInfo(ulong targetId)
 | 
			
		||||
                => InternalWaifuInfo(targetId);
 | 
			
		||||
 | 
			
		||||
            private Task InternalWaifuInfo(ulong targetId, string name = null)
 | 
			
		||||
            {
 | 
			
		||||
                var wi = _service.GetFullWaifuInfoAsync(targetId);
 | 
			
		||||
                var affInfo = _service.GetAffinityTitle(wi.AffinityCount);
 | 
			
		||||
 | 
			
		||||
                var waifuItems = _service.GetWaifuItems()
 | 
			
		||||
                    .ToDictionary(x => x.ItemEmoji, x => x);
 | 
			
		||||
                
 | 
			
		||||
                
 | 
			
		||||
                var nobody = GetText("nobody");
 | 
			
		||||
                var itemsStr = !wi.Items.Any()
 | 
			
		||||
                    ? "-"
 | 
			
		||||
                    : string.Join("\n", wi.Items
 | 
			
		||||
                        .Where(x => waifuItems.TryGetValue(x.ItemEmoji, out _))
 | 
			
		||||
                        .OrderBy(x => waifuItems[x.ItemEmoji].Price)
 | 
			
		||||
                        .GroupBy(x => x.ItemEmoji)
 | 
			
		||||
                        .Select(x => $"{x.Key} x{x.Count(),-3}")
 | 
			
		||||
                        .Chunk(2)
 | 
			
		||||
                        .Select(x => string.Join(" ", x)));
 | 
			
		||||
 | 
			
		||||
                var fansStr = wi
 | 
			
		||||
                    .Fans
 | 
			
		||||
                    .Shuffle()
 | 
			
		||||
                    .Take(30)
 | 
			
		||||
                    .Select(x => wi.Claims.Contains(x) ? $"{x} 💞" : x)
 | 
			
		||||
                    .JoinWith('\n');
 | 
			
		||||
                
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(fansStr))
 | 
			
		||||
                    fansStr = "-";
 | 
			
		||||
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("waifu") + " " + (wi.FullName ?? name ?? targetId.ToString()) + " - \"the " +
 | 
			
		||||
                               _service.GetClaimTitle(wi.ClaimCount) + "\"")
 | 
			
		||||
                    .AddField(GetText("price"), wi.Price.ToString(), true)
 | 
			
		||||
                    .AddField(GetText("claimed_by"), wi.ClaimerName ?? nobody, true)
 | 
			
		||||
                    .AddField(GetText("likes"), wi.AffinityName ?? nobody, true)
 | 
			
		||||
                    .AddField(GetText("changes_of_heart"), $"{wi.AffinityCount} - \"the {affInfo}\"", true)
 | 
			
		||||
                    .AddField(GetText("divorces"), wi.DivorceCount.ToString(), true)
 | 
			
		||||
                    .AddField("\u200B", "\u200B", true)
 | 
			
		||||
                    .AddField(GetText("fans", wi.Fans.Count), fansStr, true)
 | 
			
		||||
                    .AddField($"Waifus ({wi.ClaimCount})", wi.ClaimCount == 0 
 | 
			
		||||
                        ? nobody 
 | 
			
		||||
                        : string.Join("\n", wi.Claims.Shuffle().Take(30)), true)
 | 
			
		||||
                    .AddField(GetText("gifts"), itemsStr, true);
 | 
			
		||||
 | 
			
		||||
                return ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(1)]
 | 
			
		||||
            public async Task WaifuGift(int page = 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (--page < 0 || page > 3)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var waifuItems = _service.GetWaifuItems();
 | 
			
		||||
                await ctx.SendPaginatedConfirmAsync(page, (cur) =>
 | 
			
		||||
                {
 | 
			
		||||
                    var embed = new EmbedBuilder()
 | 
			
		||||
                        .WithTitle(GetText("waifu_gift_shop"))
 | 
			
		||||
                        .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                    waifuItems
 | 
			
		||||
                        .OrderBy(x => x.Price)
 | 
			
		||||
                        .Skip(9 * cur)
 | 
			
		||||
                        .Take(9)
 | 
			
		||||
                        .ForEach(x => embed.AddField($"{x.ItemEmoji} {x.Name}", x.Price, true));
 | 
			
		||||
 | 
			
		||||
                    return embed;
 | 
			
		||||
                }, waifuItems.Count, 9);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [Priority(0)]
 | 
			
		||||
            public async Task WaifuGift(string itemName, [Leftover] IUser waifu)
 | 
			
		||||
            {
 | 
			
		||||
                if (waifu.Id == ctx.User.Id)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var allItems = _service.GetWaifuItems();
 | 
			
		||||
                var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName.ToLowerInvariant());
 | 
			
		||||
                if (item is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("waifu_gift_not_exist");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, item);
 | 
			
		||||
 | 
			
		||||
                if (sucess)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("waifu_gift", 
 | 
			
		||||
                        Format.Bold(item.ToString() + " " + item.ItemEmoji),
 | 
			
		||||
                        Format.Bold(waifu.ToString()));
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("not_enough", CurrencySign);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Wof = NadekoBot.Modules.Gambling.Common.WheelOfFortune.WheelOfFortuneGame;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using NadekoBot.Core.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class Gambling
 | 
			
		||||
    {
 | 
			
		||||
        public class WheelOfFortuneCommands : GamblingSubmodule<GamblingService>
 | 
			
		||||
        {
 | 
			
		||||
            private static readonly ImmutableArray<string> _emojis = new string[] {
 | 
			
		||||
            "⬆",
 | 
			
		||||
            "↖",
 | 
			
		||||
            "⬅",
 | 
			
		||||
            "↙",
 | 
			
		||||
            "⬇",
 | 
			
		||||
            "↘",
 | 
			
		||||
            "➡",
 | 
			
		||||
            "↗" }.ToImmutableArray();
 | 
			
		||||
 | 
			
		||||
            private readonly ICurrencyService _cs;
 | 
			
		||||
            private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
            public WheelOfFortuneCommands(ICurrencyService cs, DbService db, GamblingConfigService gamblingConfService)
 | 
			
		||||
                : base(gamblingConfService)
 | 
			
		||||
            {
 | 
			
		||||
                _cs = cs;
 | 
			
		||||
                _db = db;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            public async Task WheelOfFortune(ShmartNumber amount)
 | 
			
		||||
            {
 | 
			
		||||
                if (!await CheckBetMandatory(amount).ConfigureAwait(false))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (!await _cs.RemoveAsync(ctx.User.Id, "Wheel Of Fortune - bet", amount, gamble: true).ConfigureAwait(false))
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var result = await _service.WheelOfFortuneSpinAsync(ctx.User.Id, amount).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                var wofMultipliers = _config.WheelOfFortune.Multipliers;
 | 
			
		||||
                await ctx.Channel.SendConfirmAsync(
 | 
			
		||||
Format.Bold($@"{ctx.User.ToString()} won: {result.Amount + CurrencySign}
 | 
			
		||||
 | 
			
		||||
   『{wofMultipliers[1]}』   『{wofMultipliers[0]}』   『{wofMultipliers[7]}』
 | 
			
		||||
 | 
			
		||||
『{wofMultipliers[2]}』      {_emojis[result.Index]}      『{wofMultipliers[6]}』
 | 
			
		||||
 | 
			
		||||
     『{wofMultipliers[3]}』   『{wofMultipliers[4]}』   『{wofMultipliers[5]}』")).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										149
									
								
								src/NadekoBot/Modules/Games/AcropobiaCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								src/NadekoBot/Modules/Games/AcropobiaCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,149 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Games.Common.Acrophobia;
 | 
			
		||||
using NadekoBot.Modules.Games.Services;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games
 | 
			
		||||
{
 | 
			
		||||
    public partial class Games
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class AcropobiaCommands : NadekoSubmodule<GamesService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
            public AcropobiaCommands(DiscordSocketClient client)
 | 
			
		||||
            {
 | 
			
		||||
                _client = client;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [NadekoOptions(typeof(AcrophobiaGame.Options))]
 | 
			
		||||
            public async Task Acrophobia(params string[] args)
 | 
			
		||||
            {
 | 
			
		||||
                var (options, _) = OptionsParser.ParseFrom(new AcrophobiaGame.Options(), args);
 | 
			
		||||
                var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
                var game = new AcrophobiaGame(options);
 | 
			
		||||
                if (_service.AcrophobiaGames.TryAdd(channel.Id, game))
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        game.OnStarted += Game_OnStarted;
 | 
			
		||||
                        game.OnEnded += Game_OnEnded;
 | 
			
		||||
                        game.OnVotingStarted += Game_OnVotingStarted;
 | 
			
		||||
                        game.OnUserVoted += Game_OnUserVoted;
 | 
			
		||||
                        _client.MessageReceived += _client_MessageReceived;
 | 
			
		||||
                        await game.Run().ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    finally
 | 
			
		||||
                    {
 | 
			
		||||
                        _client.MessageReceived -= _client_MessageReceived;
 | 
			
		||||
                        _service.AcrophobiaGames.TryRemove(channel.Id, out game);
 | 
			
		||||
                        game.Dispose();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("acro_running").ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Task _client_MessageReceived(SocketMessage msg)
 | 
			
		||||
                {
 | 
			
		||||
                    if (msg.Channel.Id != ctx.Channel.Id)
 | 
			
		||||
                        return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
                    var _ = Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            var success = await game.UserInput(msg.Author.Id, msg.Author.ToString(), msg.Content)
 | 
			
		||||
                                .ConfigureAwait(false);
 | 
			
		||||
                            if (success)
 | 
			
		||||
                                await msg.DeleteAsync().ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                        catch { }
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private Task Game_OnStarted(AcrophobiaGame game)
 | 
			
		||||
            {
 | 
			
		||||
                var embed = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                        .WithTitle(GetText("acrophobia"))
 | 
			
		||||
                        .WithDescription(GetText("acro_started", Format.Bold(string.Join(".", game.StartingLetters))))
 | 
			
		||||
                        .WithFooter(efb => efb.WithText(GetText("acro_started_footer", game.Opts.SubmissionTime)));
 | 
			
		||||
 | 
			
		||||
                return ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private Task Game_OnUserVoted(string user)
 | 
			
		||||
            {
 | 
			
		||||
                return ctx.Channel.SendConfirmAsync(
 | 
			
		||||
                    GetText("acrophobia"),
 | 
			
		||||
                    GetText("acro_vote_cast", Format.Bold(user)));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task Game_OnVotingStarted(AcrophobiaGame game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> submissions)
 | 
			
		||||
            {
 | 
			
		||||
                if (submissions.Length == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_ended_no_sub")).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if (submissions.Length == 1)
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
 | 
			
		||||
                            .WithDescription(
 | 
			
		||||
                                GetText("acro_winner_only",
 | 
			
		||||
                                    Format.Bold(submissions.First().Key.UserName)))
 | 
			
		||||
                            .WithFooter(efb => efb.WithText(submissions.First().Key.Input)))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                var i = 0;
 | 
			
		||||
                var embed = new EmbedBuilder()
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithTitle(GetText("acrophobia") + " - " + GetText("submissions_closed"))
 | 
			
		||||
                        .WithDescription(GetText("acro_nym_was", Format.Bold(string.Join(".", game.StartingLetters)) + "\n" +
 | 
			
		||||
$@"--
 | 
			
		||||
{submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.Input}**\n")}
 | 
			
		||||
--"))
 | 
			
		||||
                        .WithFooter(efb => efb.WithText(GetText("acro_vote")));
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task Game_OnEnded(AcrophobiaGame game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> votes)
 | 
			
		||||
            {
 | 
			
		||||
                if (!votes.Any() || votes.All(x => x.Value == 0))
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_no_votes_cast")).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                var table = votes.OrderByDescending(v => v.Value);
 | 
			
		||||
                var winner = table.First();
 | 
			
		||||
                var embed = new EmbedBuilder().WithOkColor()
 | 
			
		||||
                    .WithTitle(GetText("acrophobia"))
 | 
			
		||||
                    .WithDescription(GetText("acro_winner", Format.Bold(winner.Key.UserName),
 | 
			
		||||
                        Format.Bold(winner.Value.ToString())))
 | 
			
		||||
                    .WithFooter(efb => efb.WithText(winner.Key.Input));
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								src/NadekoBot/Modules/Games/CleverBotCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/NadekoBot/Modules/Games/CleverBotCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Games.Services;
 | 
			
		||||
using NadekoBot.Modules.Games.Common.ChatterBot;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games
 | 
			
		||||
{
 | 
			
		||||
    public partial class Games
 | 
			
		||||
    {
 | 
			
		||||
        [Group]
 | 
			
		||||
        public class ChatterBotCommands : NadekoSubmodule<ChatterBotService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
            public ChatterBotCommands(DbService db)
 | 
			
		||||
            {
 | 
			
		||||
                _db = db;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
            public async Task Cleverbot()
 | 
			
		||||
            {
 | 
			
		||||
                var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
 | 
			
		||||
                if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _))
 | 
			
		||||
                {
 | 
			
		||||
                    using (var uow = _db.GetDbContext())
 | 
			
		||||
                    {
 | 
			
		||||
                        uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, false);
 | 
			
		||||
                        await uow.SaveChangesAsync();
 | 
			
		||||
                    }
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("cleverbot_disabled").ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                _service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy<IChatterBotSession>(() => _service.CreateSession(), true));
 | 
			
		||||
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, true);
 | 
			
		||||
                    await uow.SaveChangesAsync();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await ReplyConfirmLocalizedAsync("cleverbot_enabled").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
       
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										190
									
								
								src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,190 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.Acrophobia
 | 
			
		||||
{
 | 
			
		||||
    public sealed class AcrophobiaGame : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        public class Options : INadekoCommandOptions
 | 
			
		||||
        {
 | 
			
		||||
            [Option('s', "submission-time", Required = false, Default = 60, HelpText = "Time after which the submissions are closed and voting starts.")]
 | 
			
		||||
            public int SubmissionTime { get; set; } = 60;
 | 
			
		||||
 | 
			
		||||
            [Option('v', "vote-time", Required = false, Default = 60, HelpText = "Time after which the voting is closed and the winner is declared.")]
 | 
			
		||||
            public int VoteTime { get; set; } = 30;
 | 
			
		||||
 | 
			
		||||
            public void NormalizeOptions()
 | 
			
		||||
            {
 | 
			
		||||
                if (SubmissionTime < 15 || SubmissionTime > 300)
 | 
			
		||||
                    SubmissionTime = 60;
 | 
			
		||||
                if (VoteTime < 15 || VoteTime > 120)
 | 
			
		||||
                    VoteTime = 30;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum Phase
 | 
			
		||||
        {
 | 
			
		||||
            Submission,
 | 
			
		||||
            Voting,
 | 
			
		||||
            Ended
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public enum UserInputResult
 | 
			
		||||
        {
 | 
			
		||||
            Submitted,
 | 
			
		||||
            SubmissionFailed,
 | 
			
		||||
            Voted,
 | 
			
		||||
            VotingFailed,
 | 
			
		||||
            Failed
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Phase CurrentPhase { get; private set; } = Phase.Submission;
 | 
			
		||||
        public ImmutableArray<char> StartingLetters { get; private set; }
 | 
			
		||||
 | 
			
		||||
        private readonly Dictionary<AcrophobiaUser, int> submissions = new Dictionary<AcrophobiaUser, int>();
 | 
			
		||||
        private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
 | 
			
		||||
        public Options Opts { get; }
 | 
			
		||||
        private readonly NadekoRandom _rng;
 | 
			
		||||
 | 
			
		||||
        public event Func<AcrophobiaGame, Task> OnStarted = delegate { return Task.CompletedTask; };
 | 
			
		||||
        public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnVotingStarted = delegate { return Task.CompletedTask; };
 | 
			
		||||
        public event Func<string, Task> OnUserVoted = delegate { return Task.CompletedTask; };
 | 
			
		||||
        public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnEnded = delegate { return Task.CompletedTask; };
 | 
			
		||||
 | 
			
		||||
        private readonly HashSet<ulong> _usersWhoVoted = new HashSet<ulong>();
 | 
			
		||||
 | 
			
		||||
        public AcrophobiaGame(Options options)
 | 
			
		||||
        {
 | 
			
		||||
            Opts = options;
 | 
			
		||||
            _rng = new NadekoRandom();
 | 
			
		||||
            InitializeStartingLetters();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task Run()
 | 
			
		||||
        {
 | 
			
		||||
            await OnStarted(this).ConfigureAwait(false);
 | 
			
		||||
            await Task.Delay(Opts.SubmissionTime * 1000).ConfigureAwait(false);
 | 
			
		||||
            await locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (submissions.Count == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    CurrentPhase = Phase.Ended;
 | 
			
		||||
                    await OnVotingStarted(this, ImmutableArray.Create<KeyValuePair<AcrophobiaUser, int>>()).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if (submissions.Count == 1)
 | 
			
		||||
                {
 | 
			
		||||
                    CurrentPhase = Phase.Ended;
 | 
			
		||||
                    await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                CurrentPhase = Phase.Voting;
 | 
			
		||||
 | 
			
		||||
                await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            finally { locker.Release(); }
 | 
			
		||||
 | 
			
		||||
            await Task.Delay(Opts.VoteTime * 1000).ConfigureAwait(false);
 | 
			
		||||
            await locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                CurrentPhase = Phase.Ended;
 | 
			
		||||
                await OnEnded(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            finally { locker.Release(); }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void InitializeStartingLetters()
 | 
			
		||||
        {
 | 
			
		||||
            var wordCount = _rng.Next(3, 6);
 | 
			
		||||
 | 
			
		||||
            var lettersArr = new char[wordCount];
 | 
			
		||||
 | 
			
		||||
            for (int i = 0; i < wordCount; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var randChar = (char)_rng.Next(65, 91);
 | 
			
		||||
                lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar;
 | 
			
		||||
            }
 | 
			
		||||
            StartingLetters = lettersArr.ToImmutableArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> UserInput(ulong userId, string userName, string input)
 | 
			
		||||
        {
 | 
			
		||||
            var user = new AcrophobiaUser(userId, userName, input.ToLowerInvariant().ToTitleCase());
 | 
			
		||||
 | 
			
		||||
            await locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                switch (CurrentPhase)
 | 
			
		||||
                {
 | 
			
		||||
                    case Phase.Submission:
 | 
			
		||||
                        if (submissions.ContainsKey(user) || !IsValidAnswer(input))
 | 
			
		||||
                            break;
 | 
			
		||||
 | 
			
		||||
                        submissions.Add(user, 0);
 | 
			
		||||
                        return true;
 | 
			
		||||
                    case Phase.Voting:
 | 
			
		||||
                        AcrophobiaUser toVoteFor;
 | 
			
		||||
                        if (!int.TryParse(input, out var index)
 | 
			
		||||
                            || --index < 0
 | 
			
		||||
                            || index >= submissions.Count
 | 
			
		||||
                            || (toVoteFor = submissions.ToArray()[index].Key).UserId == user.UserId
 | 
			
		||||
                            || !_usersWhoVoted.Add(userId))
 | 
			
		||||
                            break;
 | 
			
		||||
                        ++submissions[toVoteFor];
 | 
			
		||||
                        var _ = Task.Run(() => OnUserVoted(userName));
 | 
			
		||||
                        return true;
 | 
			
		||||
                    default:
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                locker.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool IsValidAnswer(string input)
 | 
			
		||||
        {
 | 
			
		||||
            input = input.ToUpperInvariant();
 | 
			
		||||
 | 
			
		||||
            var inputWords = input.Split(' ');
 | 
			
		||||
 | 
			
		||||
            if (inputWords.Length != StartingLetters.Length) // number of words must be the same as the number of the starting letters
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            for (int i = 0; i < StartingLetters.Length; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var letter = StartingLetters[i];
 | 
			
		||||
 | 
			
		||||
                if (!inputWords[i].StartsWith(letter.ToString(), StringComparison.InvariantCulture)) // all first letters must match
 | 
			
		||||
                    return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            this.CurrentPhase = Phase.Ended;
 | 
			
		||||
            OnStarted = null;
 | 
			
		||||
            OnEnded = null;
 | 
			
		||||
            OnUserVoted = null;
 | 
			
		||||
            OnVotingStarted = null;
 | 
			
		||||
            _usersWhoVoted.Clear();
 | 
			
		||||
            submissions.Clear();
 | 
			
		||||
            locker.Dispose();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.Acrophobia
 | 
			
		||||
{
 | 
			
		||||
    public class AcrophobiaUser
 | 
			
		||||
    {
 | 
			
		||||
        public string UserName { get; }
 | 
			
		||||
        public ulong UserId { get; }
 | 
			
		||||
        public string Input { get; }
 | 
			
		||||
 | 
			
		||||
        public AcrophobiaUser(ulong userId, string userName, string input)
 | 
			
		||||
        {
 | 
			
		||||
            this.UserName = userName;
 | 
			
		||||
            this.UserId = userId;
 | 
			
		||||
            this.Input = input;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override int GetHashCode()
 | 
			
		||||
        {
 | 
			
		||||
            return UserId.GetHashCode();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override bool Equals(object obj)
 | 
			
		||||
        {
 | 
			
		||||
            return obj is AcrophobiaUser x
 | 
			
		||||
                ? x.UserId == this.UserId
 | 
			
		||||
                : false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
 | 
			
		||||
{
 | 
			
		||||
    public class ChatterBotResponse
 | 
			
		||||
    {
 | 
			
		||||
        public string Convo_id { get; set; }
 | 
			
		||||
        public string BotSay { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
 | 
			
		||||
{
 | 
			
		||||
    public class ChatterBotSession : IChatterBotSession
 | 
			
		||||
    {
 | 
			
		||||
        private static NadekoRandom Rng { get; } = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
        private readonly string _chatterBotId;
 | 
			
		||||
        private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
        private readonly int _botId = 6;
 | 
			
		||||
 | 
			
		||||
        public ChatterBotSession(IHttpClientFactory httpFactory)
 | 
			
		||||
        {
 | 
			
		||||
            _chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64();
 | 
			
		||||
            _httpFactory = httpFactory;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string ApiEndpoint => "http://api.program-o.com/v2/chatbot/" +
 | 
			
		||||
                                      $"?bot_id={_botId}&" +
 | 
			
		||||
                                      "say={0}&" +
 | 
			
		||||
                                      $"convo_id=nadekobot_{_chatterBotId}&" +
 | 
			
		||||
                                      "format=json";
 | 
			
		||||
 | 
			
		||||
        public async Task<string> Think(string message)
 | 
			
		||||
        {
 | 
			
		||||
            using (var http = _httpFactory.CreateClient())
 | 
			
		||||
            {
 | 
			
		||||
                var res = await http.GetStringAsync(string.Format(ApiEndpoint, message)).ConfigureAwait(false);
 | 
			
		||||
                var cbr = JsonConvert.DeserializeObject<ChatterBotResponse>(res);
 | 
			
		||||
                return cbr.BotSay.Replace("<br/>", "\n", StringComparison.InvariantCulture);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user