diff --git a/Discord.Net b/Discord.Net index fa2568bc..b9f76733 160000 --- a/Discord.Net +++ b/Discord.Net @@ -1 +1 @@ -Subproject commit fa2568bc312ba35f1518e47601c62fccdb949731 +Subproject commit b9f767337d2b7c07ed76eb83c3bc5030109d5238 diff --git a/src/NadekoBot/DataStructures/ExecuteCommandResult.cs b/src/NadekoBot/DataStructures/ExecuteCommandResult.cs new file mode 100644 index 00000000..c8568c43 --- /dev/null +++ b/src/NadekoBot/DataStructures/ExecuteCommandResult.cs @@ -0,0 +1,24 @@ +๏ปฟusing Discord.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static NadekoBot.Modules.Permissions.Permissions; + +namespace NadekoBot.DataStructures +{ + public struct ExecuteCommandResult + { + public readonly CommandInfo CommandInfo; + public readonly PermissionCache PermissionCache; + public readonly IResult Result; + + public ExecuteCommandResult(CommandInfo commandInfo, PermissionCache cache, IResult result) + { + this.CommandInfo = commandInfo; + this.PermissionCache = cache; + this.Result = result; + } + } +} diff --git a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs index 04bde59b..856d729d 100644 --- a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs @@ -78,7 +78,10 @@ namespace NadekoBot.Modules.Administration _client.UserPresenceUpdated += _client_UserPresenceUpdated; _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated; _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated_TTS; + _client.GuildUserUpdated += _client_GuildUserUpdated; +#if !GLOBAL_NADEKO _client.UserUpdated += _client_UserUpdated; +#endif _client.ChannelCreated += _client_ChannelCreated; _client.ChannelDestroyed += _client_ChannelDestroyed; @@ -88,6 +91,38 @@ namespace NadekoBot.Modules.Administration MuteCommands.UserUnmuted += MuteCommands_UserUnmuted; } + private static async void _client_UserUpdated(SocketUser before, SocketUser after) + { + try + { + var str = ""; + if (before.Username != after.Username) + str = $"๐Ÿ‘ค__**{before.Username}#{before.Discriminator}**__ **| Name Changed |** ๐Ÿ†” `{before.Id}`\n\t\t`New:` **{after.ToString()}**"; + else if (before.AvatarUrl != after.AvatarUrl) + str = $"๐Ÿ‘ค__**{before.Username}#{before.Discriminator}**__ **| Avatar Changed |** ๐Ÿ†” `{before.Id}`\n\t๐Ÿ–ผ {await NadekoBot.Google.ShortenUrl(before.AvatarUrl)} `=>` {await NadekoBot.Google.ShortenUrl(after.AvatarUrl)}"; + + if (string.IsNullOrWhiteSpace(str)) + return; + + var guildsMemberOf = NadekoBot.Client.GetGuilds().Where(g => g.Users.Select(u => u.Id).Contains(before.Id)).ToList(); + foreach (var g in guildsMemberOf) + { + LogSetting logSetting; + if (!GuildLogSettings.TryGetValue(g.Id, out logSetting) + || (logSetting.UserUpdatedId == null)) + return; + + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(g, logSetting, LogType.UserUpdated)) == null) + return; + + try { await logChannel.SendMessageAsync(str).ConfigureAwait(false); } catch { } + } + } + catch + { } + } + private static async void _client_UserVoiceStateUpdated_TTS(SocketUser iusr, SocketVoiceState before, SocketVoiceState after) { try @@ -228,17 +263,10 @@ namespace NadekoBot.Modules.Administration catch (Exception ex) { _log.Warn(ex); } } - private static async void _client_UserUpdated(SocketUser uBefore, SocketUser uAfter) + private static async void _client_GuildUserUpdated(SocketGuildUser before, SocketGuildUser after) { try { - var before = uBefore as SocketGuildUser; - if (before == null) - return; - var after = uAfter as SocketGuildUser; - if (after == null) - return; - LogSetting logSetting; if (!GuildLogSettings.TryGetValue(before.Guild.Id, out logSetting) || (logSetting.UserUpdatedId == null)) @@ -247,30 +275,25 @@ namespace NadekoBot.Modules.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserUpdated)) == null) return; - string str = $"๐Ÿ•”`{prettyCurrentTime}`"; - - if (before.Username != after.Username) - str += $"๐Ÿ‘ค__**{before.Username}#{before.Discriminator}**__ **| Name Changed |** ๐Ÿ†” `{before.Id}`\n\t\t`New:` **{after.ToString()}**"; - else if (before.Nickname != after.Nickname) - str += $"๐Ÿ‘ค__**{before.Username}#{before.Discriminator}**__ **| Nickname Changed |** ๐Ÿ†” `{before.Id}`\n\t\t`Old:` **{before.Nickname}#{before.Discriminator}**\n\t\t`New:` **{after.Nickname}#{after.Discriminator}**"; - else if (before.AvatarUrl != after.AvatarUrl) - str += $"๐Ÿ‘ค__**{before.Username}#{before.Discriminator}**__ **| Avatar Changed |** ๐Ÿ†” `{before.Id}`\n\t๐Ÿ–ผ {await NadekoBot.Google.ShortenUrl(before.AvatarUrl)} `=>` {await NadekoBot.Google.ShortenUrl(after.AvatarUrl)}"; + var str = ""; + if (before.Nickname != after.Nickname) + str = $"๐Ÿ‘ค__**{before.Username}#{before.Discriminator}**__ **| Nickname Changed |** ๐Ÿ†” `{before.Id}`\n\t\t`Old:` **{before.Nickname}**\n\t\t`New:` **{after.Nickname}**"; else if (!before.RoleIds.SequenceEqual(after.RoleIds)) { if (before.RoleIds.Count < after.RoleIds.Count) { var diffRoles = after.RoleIds.Where(r => !before.RoleIds.Contains(r)).Select(r => "**" + before.Guild.GetRole(r).Name + "**"); - str += $"๐Ÿ‘ค__**{before.ToString()}**__ **| User's Role Added |** ๐Ÿ†” `{before.Id}`\n\tโœ… {string.Join(", ", diffRoles).SanitizeMentions()}\n\t\tโš” **`{string.Join(", ", after.GetRoles().Select(r => r.Name)).SanitizeMentions()}`** โš”"; + str = $"๐Ÿ‘ค__**{before.ToString()}**__ **| User's Role Added |** ๐Ÿ†” `{before.Id}`\n\tโœ… {string.Join(", ", diffRoles).SanitizeMentions()}\n\t\tโš” **`{string.Join(", ", after.GetRoles().Select(r => r.Name)).SanitizeMentions()}`** โš”"; } else if (before.RoleIds.Count > after.RoleIds.Count) { var diffRoles = before.RoleIds.Where(r => !after.RoleIds.Contains(r)).Select(r => "**" + before.Guild.GetRole(r).Name + "**"); - str += $"๐Ÿ‘ค__**{before.ToString()}**__ **| User's Role Removed |** ๐Ÿ†” `{before.Id}`\n\t๐Ÿšฎ {string.Join(", ", diffRoles).SanitizeMentions()}\n\t\tโš” **`{string.Join(", ", after.GetRoles().Select(r => r.Name)).SanitizeMentions()}`** โš”"; + str = $"๐Ÿ‘ค__**{before.ToString()}**__ **| User's Role Removed |** ๐Ÿ†” `{before.Id}`\n\t๐Ÿšฎ {string.Join(", ", diffRoles).SanitizeMentions()}\n\t\tโš” **`{string.Join(", ", after.GetRoles().Select(r => r.Name)).SanitizeMentions()}`** โš”"; } } else return; - try { await logChannel.SendMessageAsync(str).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } + try { await logChannel.SendMessageAsync($"๐Ÿ•”`{prettyCurrentTime}` " + str).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } } catch (Exception ex) { _log.Warn(ex); } } @@ -745,9 +768,9 @@ namespace NadekoBot.Modules.Administration await uow.CompleteAsync().ConfigureAwait(false); } if (action.Value) - await channel.SendMessageAsync("โœ… Logging all events on this channel.").ConfigureAwait(false); + await channel.SendConfirmAsync("Logging all events in this channel.").ConfigureAwait(false); else - await channel.SendMessageAsync("โ„น๏ธ Logging disabled.").ConfigureAwait(false); + await channel.SendConfirmAsync("Logging disabled.").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -774,9 +797,9 @@ namespace NadekoBot.Modules.Administration } if (removed == 0) - await channel.SendMessageAsync($"๐Ÿ†— Logging will **now ignore** #โƒฃ `{channel.Name} ({channel.Id})`").ConfigureAwait(false); + await channel.SendConfirmAsync($"Logging will IGNORE **{channel.Mention} ({channel.Id})**").ConfigureAwait(false); else - await channel.SendMessageAsync($"โ„น๏ธ Logging will **no longer ignore** #โƒฃ `{channel.Name} ({channel.Id})`").ConfigureAwait(false); + await channel.SendConfirmAsync($"Logging will NOT IGNORE **{channel.Mention} ({channel.Id})**").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -853,9 +876,9 @@ namespace NadekoBot.Modules.Administration } if (channelId != null) - await channel.SendMessageAsync($"โœ… Logging `{type}` event in #โƒฃ `{channel.Name} ({channel.Id})`").ConfigureAwait(false); + await channel.SendConfirmAsync($"Logging **{type}** event in this channel.").ConfigureAwait(false); else - await channel.SendMessageAsync($"โ„น๏ธ Stopped logging `{type}` event.").ConfigureAwait(false); + await channel.SendConfirmAsync($"Stopped logging **{type}** event.").ConfigureAwait(false); } } } diff --git a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs index c9c49f60..b2e8115f 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs @@ -71,8 +71,8 @@ namespace NadekoBot.Modules.Administration public static Dictionary> PlayingPlaceholders { get; } = new Dictionary> { - {"%servers%", () => NadekoBot.Client.GetGuilds().Count().ToString()}, - {"%users%", () => NadekoBot.Client.GetGuilds().Select(s => s.Users.Count).Sum().ToString()}, + {"%servers%", () => NadekoBot.Client.GetGuildsCount().ToString()}, + {"%users%", () => NadekoBot.Client.GetGuilds().Sum(s => s.Users.Count).ToString()}, {"%playing%", () => { var cnt = Music.Music.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); if (cnt != 1) return cnt.ToString(); diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index 915b6373..753f85f5 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Modules.Administration public async Task Leave([Remainder] string guildStr) { guildStr = guildStr.Trim().ToUpperInvariant(); - var server = NadekoBot.Client.GetGuilds().FirstOrDefault(g => g.Id.ToString().Trim().ToUpperInvariant() == guildStr) ?? + var server = NadekoBot.Client.GetGuilds().FirstOrDefault(g => g.Id.ToString() == guildStr) ?? NadekoBot.Client.GetGuilds().FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == guildStr); if (server == null) diff --git a/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs b/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs index 3e8c01a5..389c7927 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs @@ -26,9 +26,9 @@ namespace NadekoBot.Modules.Gambling if (count == 1) { if (rng.Next(0, 2) == 1) - await Context.Channel.SendFileAsync(headsPath, $"{Context.User.Mention} flipped " + Format.Code("Heads") + ".").ConfigureAwait(false); + await Context.Channel.SendFileAsync(File.Open(headsPath, FileMode.OpenOrCreate), "heads.jpg", $"{Context.User.Mention} flipped " + Format.Code("Heads") + ".").ConfigureAwait(false); else - await Context.Channel.SendFileAsync(tailsPath, $"{Context.User.Mention} flipped " + Format.Code("Tails") + ".").ConfigureAwait(false); + await Context.Channel.SendFileAsync(File.Open(tailsPath, FileMode.OpenOrCreate), "tails.jpg", $"{Context.User.Mention} flipped " + Format.Code("Tails") + ".").ConfigureAwait(false); return; } if (count > 10 || count < 1) @@ -93,7 +93,7 @@ namespace NadekoBot.Modules.Gambling str = $"{Context.User.Mention}`Better luck next time.`"; } - await Context.Channel.SendFileAsync(imgPathToSend, str).ConfigureAwait(false); + await Context.Channel.SendFileAsync(File.Open(imgPathToSend, FileMode.OpenOrCreate), "coin.jpg", str).ConfigureAwait(false); } } } diff --git a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs b/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs index a65787fb..8197035b 100644 --- a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs @@ -88,10 +88,10 @@ namespace NadekoBot.Modules.Games } return true; } -#if !GLOBAL_NADEKO + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [RequireUserPermission(ChannelPermission.ManageMessages)] + [RequireUserPermission(GuildPermission.ManageMessages)] public async Task Cleverbot() { var channel = (ITextChannel)Context.Channel; @@ -120,7 +120,6 @@ namespace NadekoBot.Modules.Games await Context.Channel.SendConfirmAsync($"{Context.User.Mention} Enabled cleverbot on this server.").ConfigureAwait(false); } -#endif } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs index d096b452..58071453 100644 --- a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs @@ -12,37 +12,15 @@ using System.Threading.Tasks; namespace NadekoBot.Modules.Games.Commands.Hangman { - public class HangmanModel - { - public List All { get; set; } - public List Animals { get; set; } - public List Countries { get; set; } - public List Movies { get; set; } - public List Things { get; set; } - } - public class HangmanTermPool { - public enum HangmanTermType - { - All, - Animals, - Countries, - Movies, - Things - } - const string termsPath = "data/hangman.json"; - public static HangmanModel data { get; } + public static IReadOnlyDictionary data { get; } static HangmanTermPool() { try { - data = JsonConvert.DeserializeObject(File.ReadAllText(termsPath)); - data.All = data.Animals.Concat(data.Countries) - .Concat(data.Movies) - .Concat(data.Things) - .ToList(); + data = JsonConvert.DeserializeObject>(File.ReadAllText(termsPath)); } catch (Exception ex) { @@ -50,23 +28,27 @@ namespace NadekoBot.Modules.Games.Commands.Hangman } } - public static HangmanObject GetTerm(HangmanTermType type) + public static HangmanObject GetTerm(string type) { + if (string.IsNullOrWhiteSpace(type)) + throw new ArgumentNullException(nameof(type)); + + type = type.Trim(); + var rng = new NadekoRandom(); - switch (type) - { - case HangmanTermType.Animals: - return data.Animals[rng.Next(0, data.Animals.Count)]; - case HangmanTermType.Countries: - return data.Countries[rng.Next(0, data.Countries.Count)]; - case HangmanTermType.Movies: - return data.Movies[rng.Next(0, data.Movies.Count)]; - case HangmanTermType.Things: - return data.Things[rng.Next(0, data.Things.Count)]; - default: - return data.All[rng.Next(0, data.All.Count)]; + + if (type == "All") { + var keys = data.Keys.ToArray(); + type = keys[rng.Next(0, keys.Length)]; } + HangmanObject[] termTypes; + data.TryGetValue(type, out termTypes); + + if (termTypes.Length == 0) + return null; + + return termTypes[rng.Next(0, termTypes.Length)]; } } @@ -95,20 +77,23 @@ namespace NadekoBot.Modules.Games.Commands.Hangman public bool GuessedAll => Guesses.IsSupersetOf(Term.Word.ToUpperInvariant() .Where(c => char.IsLetter(c) || char.IsDigit(c))); - public HangmanTermPool.HangmanTermType TermType { get; } + public string TermType { get; } public event Action OnEnded; - public HangmanGame(IMessageChannel channel, HangmanTermPool.HangmanTermType type) + public HangmanGame(IMessageChannel channel, string type) { _log = LogManager.GetCurrentClassLogger(); this.GameChannel = channel; - this.TermType = type; + this.TermType = type.ToTitleCase(); } public void Start() { this.Term = HangmanTermPool.GetTerm(TermType); + + if (this.Term == null) + throw new KeyNotFoundException("Can't find a term with that type. Use hangmanlist command."); // start listening for answers when game starts NadekoBot.Client.MessageReceived += PotentialGuess; } diff --git a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs index eba11193..0827ab94 100644 --- a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs @@ -23,7 +23,7 @@ namespace NadekoBot.Modules.Games static HangmanCommands() { _log = LogManager.GetCurrentClassLogger(); - typesStr = $"`List of \"{NadekoBot.ModulePrefixes[typeof(Games).Name]}hangman\" term types:`\n" + String.Join(", ", Enum.GetNames(typeof(HangmanTermPool.HangmanTermType))); + typesStr = $"`List of \"{NadekoBot.ModulePrefixes[typeof(Games).Name]}hangman\" term types:`\n" + String.Join(", ", HangmanTermPool.data.Keys); } [NadekoCommand, Usage, Description, Aliases] @@ -33,7 +33,7 @@ namespace NadekoBot.Modules.Games } [NadekoCommand, Usage, Description, Aliases] - public async Task Hangman(HangmanTermPool.HangmanTermType type = HangmanTermPool.HangmanTermType.All) + public async Task Hangman([Remainder]string type = "All") { var hm = new HangmanGame(Context.Channel, type); @@ -48,7 +48,14 @@ namespace NadekoBot.Modules.Games HangmanGame throwaway; HangmanGames.TryRemove(g.GameChannel.Id, out throwaway); }; - hm.Start(); + try + { + hm.Start(); + } + catch (Exception ex) { + try { await Context.Channel.SendErrorAsync($"Starting errored: {ex.Message}").ConfigureAwait(false); } catch { } + return; + } await Context.Channel.SendConfirmAsync("Hangman game started", hm.ScrambledWord + "\n" + hm.GetHangman() + "\n" + hm.ScrambledWord); } diff --git a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs index 69fbc862..078b9183 100644 --- a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs @@ -94,7 +94,8 @@ namespace NadekoBot.Modules.Games lastGenerations.AddOrUpdate(channel.Id, DateTime.Now, (id, old) => DateTime.Now); var sent = await channel.SendFileAsync( - GetRandomCurrencyImagePath(), + File.Open(GetRandomCurrencyImagePath(), FileMode.OpenOrCreate), + "RandomFlower.jpg", $"โ— A random { Gambling.Gambling.CurrencyName } appeared! Pick it up by typing `{NadekoBot.ModulePrefixes[typeof(Games).Name]}pick`") .ConfigureAwait(false); plantedFlowers.AddOrUpdate(channel.Id, new List() { sent }, (id, old) => { old.Add(sent); return old; }); @@ -104,7 +105,7 @@ namespace NadekoBot.Modules.Games } catch { } } -#if !GLOBAL_NADEKO + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Pick() @@ -163,11 +164,10 @@ namespace NadekoBot.Modules.Games } else { - msg = await Context.Channel.SendFileAsync(file, msgToSend).ConfigureAwait(false); + msg = await Context.Channel.SendFileAsync(File.Open(file, FileMode.OpenOrCreate), "plant.jpg", msgToSend).ConfigureAwait(false); } plantedFlowers.AddOrUpdate(Context.Channel.Id, new List() { msg }, (id, old) => { old.Add(msg); return old; }); } -#endif [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] diff --git a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs index f7321a72..01d0477c 100644 --- a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -17,7 +18,7 @@ namespace NadekoBot.Modules.Games [Group] public class PollCommands : ModuleBase { - public static ConcurrentDictionary ActivePolls = new ConcurrentDictionary(); + public static ConcurrentDictionary ActivePolls = new ConcurrentDictionary(); [NadekoCommand, Usage, Description, Aliases] [RequireUserPermission(GuildPermission.ManageMessages)] @@ -31,6 +32,18 @@ namespace NadekoBot.Modules.Games public Task PublicPoll([Remainder] string arg = null) => InternalStartPoll(arg, isPublic: true); + [NadekoCommand, Usage, Description, Aliases] + [RequireUserPermission(GuildPermission.ManageMessages)] + [RequireContext(ContextType.Guild)] + public async Task PollStats() + { + Games.Poll poll; + if (!ActivePolls.TryGetValue(Context.Guild.Id, out poll)) + return; + + await Context.Channel.EmbedAsync(poll.GetStats("Current Poll Results")); + } + private async Task InternalStartPoll(string arg, bool isPublic = false) { var channel = (ITextChannel)Context.Channel; @@ -44,7 +57,7 @@ namespace NadekoBot.Modules.Games return; var poll = new Poll(Context.Message, data[0], data.Skip(1), isPublic: isPublic); - if (ActivePolls.TryAdd(channel.Guild, poll)) + if (ActivePolls.TryAdd(channel.Guild.Id, poll)) { await poll.StartPoll().ConfigureAwait(false); } @@ -60,7 +73,7 @@ namespace NadekoBot.Modules.Games var channel = (ITextChannel)Context.Channel; Poll poll; - ActivePolls.TryRemove(channel.Guild, out poll); + ActivePolls.TryRemove(channel.Guild.Id, out poll); await poll.StopPoll().ConfigureAwait(false); } } @@ -69,20 +82,55 @@ namespace NadekoBot.Modules.Games { private readonly IUserMessage originalMessage; private readonly IGuild guild; - private readonly string[] answers; + private string[] Answers { get; } private ConcurrentDictionary participants = new ConcurrentDictionary(); private readonly string question; private DateTime started; private CancellationTokenSource pollCancellationSource = new CancellationTokenSource(); - private readonly bool isPublic; + public bool IsPublic { get; } public Poll(IUserMessage umsg, string question, IEnumerable enumerable, bool isPublic = false) { this.originalMessage = umsg; this.guild = ((ITextChannel)umsg.Channel).Guild; this.question = question; - this.answers = enumerable as string[] ?? enumerable.ToArray(); - this.isPublic = isPublic; + this.Answers = enumerable as string[] ?? enumerable.ToArray(); + this.IsPublic = isPublic; + } + + public EmbedBuilder GetStats(string title) + { + var results = participants.GroupBy(kvp => kvp.Value) + .ToDictionary(x => x.Key, x => x.Sum(kvp => 1)) + .OrderByDescending(kvp => kvp.Value) + .ToArray(); + + var eb = new EmbedBuilder().WithTitle(title); + + var sb = new StringBuilder() + .AppendLine(Format.Bold(question)) + .AppendLine(); + + var totalVotesCast = 0; + if (results.Length == 0) + { + sb.AppendLine("No votes cast."); + } + else + { + for (int i = 0; i < results.Length; i++) + { + var result = results[i]; + sb.AppendLine($"`{i + 1}.` {Format.Bold(Answers[result.Key - 1])} with {Format.Bold(result.Value.ToString())} votes."); + totalVotesCast += result.Value; + } + } + + + eb.WithDescription(sb.ToString()) + .WithFooter(efb => efb.WithText(totalVotesCast + " total votes cast.")); + + return eb; } public async Task StartPoll() @@ -91,8 +139,8 @@ namespace NadekoBot.Modules.Games NadekoBot.Client.MessageReceived += Vote; var msgToSend = $"๐Ÿ“ƒ**{originalMessage.Author.Username}** has created a poll which requires your attention:\n\n**{question}**\n"; var num = 1; - msgToSend = answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n"); - if (!isPublic) + msgToSend = Answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n"); + if (!IsPublic) msgToSend += "\n**Private Message me with the corresponding number of the answer.**"; else msgToSend += "\n**Send a Message here with the corresponding number of the answer.**"; @@ -102,30 +150,7 @@ namespace NadekoBot.Modules.Games public async Task StopPoll() { NadekoBot.Client.MessageReceived -= Vote; - try - { - var results = participants.GroupBy(kvp => kvp.Value) - .ToDictionary(x => x.Key, x => x.Sum(kvp => 1)) - .OrderByDescending(kvp => kvp.Value); - - var totalVotesCast = results.Sum(kvp => kvp.Value); - if (totalVotesCast == 0) - { - await originalMessage.Channel.SendMessageAsync("๐Ÿ“„ **No votes have been cast.**").ConfigureAwait(false); - return; - } - var closeMessage = $"--------------**POLL CLOSED**--------------\n" + - $"๐Ÿ“„ , here are the results:\n"; - closeMessage = results.Aggregate(closeMessage, (current, kvp) => current + $"`{kvp.Key}.` **[{answers[kvp.Key - 1]}]**" + - $" has {kvp.Value} votes." + - $"({kvp.Value * 1.0f / totalVotesCast * 100}%)\n"); - - await originalMessage.Channel.SendConfirmAsync($"๐Ÿ“„ **Total votes cast**: {totalVotesCast}\n{closeMessage}").ConfigureAwait(false); - } - catch (Exception ex) - { - Console.WriteLine($"Error in poll game {ex}"); - } + await originalMessage.Channel.EmbedAsync(GetStats("POLL CLOSED")).ConfigureAwait(false); } private async void Vote(SocketMessage imsg) @@ -141,11 +166,11 @@ namespace NadekoBot.Modules.Games int vote; if (!int.TryParse(imsg.Content, out vote)) return; - if (vote < 1 || vote > answers.Length) + if (vote < 1 || vote > Answers.Length) return; IMessageChannel ch; - if (isPublic) + if (IsPublic) { //if public, channel must be the same the poll started in if (originalMessage.Channel.Id != imsg.Channel.Id) @@ -167,7 +192,7 @@ namespace NadekoBot.Modules.Games //user can vote only once if (participants.TryAdd(msg.Author.Id, vote)) { - if (!isPublic) + if (!IsPublic) { await ch.SendConfirmAsync($"Thanks for voting **{msg.Author.Username}**.").ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 7c692f0b..4db4c1b3 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -49,7 +49,7 @@ namespace NadekoBot.Modules.NSFW var link = await provider.ConfigureAwait(false); if (string.IsNullOrWhiteSpace(link)) { - if (noError) + if (!noError) await channel.SendErrorAsync("No results found.").ConfigureAwait(false); return; } diff --git a/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs b/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs index 72c41bc3..e02dc895 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs +++ b/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs @@ -23,13 +23,18 @@ namespace NadekoBot.Modules.Permissions [Group] public class BlacklistCommands : ModuleBase { - public static ConcurrentHashSet BlacklistedItems { get; set; } = new ConcurrentHashSet(); + public static ConcurrentHashSet BlacklistedUsers { get; set; } = new ConcurrentHashSet(); + public static ConcurrentHashSet BlacklistedGuilds { get; set; } = new ConcurrentHashSet(); + public static ConcurrentHashSet BlacklistedChannels { get; set; } = new ConcurrentHashSet(); static BlacklistCommands() { using (var uow = DbHandler.UnitOfWork()) { - BlacklistedItems = new ConcurrentHashSet(uow.BotConfig.GetOrCreate().Blacklist); + var blacklist = uow.BotConfig.GetOrCreate().Blacklist; + BlacklistedUsers = new ConcurrentHashSet(blacklist.Where(bi => bi.Type == BlacklistType.User).Select(c => c.ItemId)); + BlacklistedGuilds = new ConcurrentHashSet(blacklist.Where(bi => bi.Type == BlacklistType.Server).Select(c => c.ItemId)); + BlacklistedChannels = new ConcurrentHashSet(blacklist.Where(bi => bi.Type == BlacklistType.Channel).Select(c => c.ItemId)); } } @@ -66,12 +71,34 @@ namespace NadekoBot.Modules.Permissions { var item = new BlacklistItem { ItemId = id, Type = type }; uow.BotConfig.GetOrCreate().Blacklist.Add(item); - BlacklistedItems.Add(item); + if (type == BlacklistType.Server) + { + BlacklistedGuilds.Add(id); + } + else if (type == BlacklistType.Channel) + { + BlacklistedChannels.Add(id); + } + else if (type == BlacklistType.User) + { + BlacklistedUsers.Add(id); + } } else { uow.BotConfig.GetOrCreate().Blacklist.RemoveWhere(bi => bi.ItemId == id && bi.Type == type); - BlacklistedItems.RemoveWhere(bi => bi.ItemId == id && bi.Type == type); + if (type == BlacklistType.Server) + { + BlacklistedGuilds.TryRemove(id); + } + else if (type == BlacklistType.Channel) + { + BlacklistedChannels.TryRemove(id); + } + else if (type == BlacklistType.User) + { + BlacklistedUsers.TryRemove(id); + } } await uow.CompleteAsync().ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs b/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs index d3c5742f..371b2065 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs +++ b/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs @@ -23,8 +23,8 @@ namespace NadekoBot.Modules.Searches.Commands.Models public class Main { public double temp { get; set; } - public int pressure { get; set; } - public int humidity { get; set; } + public float pressure { get; set; } + public float humidity { get; set; } public double temp_min { get; set; } public double temp_max { get; set; } } diff --git a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs index ca2ab738..37d80ba6 100644 --- a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs @@ -44,7 +44,7 @@ namespace NadekoBot.Modules.Utility .AddField(fb => fb.WithName("**Voice Channels**").WithValue(voicechn.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName("**Created At**").WithValue($"{createdAt.ToString("dd.MM.yyyy HH:mm")}").WithIsInline(true)) .AddField(fb => fb.WithName("**Region**").WithValue(guild.VoiceRegionId.ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName("**Roles**").WithValue(guild.Roles.Count().ToString()).WithIsInline(true)) + .AddField(fb => fb.WithName("**Roles**").WithValue((guild.Roles.Count - 1).ToString()).WithIsInline(true)) .WithImageUrl(guild.IconUrl) .WithColor(NadekoBot.OkColor); if (guild.Emojis.Count() > 0) @@ -92,8 +92,7 @@ namespace NadekoBot.Modules.Utility embed.AddField(fb => fb.WithName("**ID**").WithValue(user.Id.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName("**Joined Server**").WithValue($"{user.JoinedAt?.ToString("dd.MM.yyyy HH:mm")}").WithIsInline(true)) .AddField(fb => fb.WithName("**Joined Discord**").WithValue($"{user.CreatedAt.ToString("dd.MM.yyyy HH:mm")}").WithIsInline(true)) - .AddField(fb => fb.WithName("**Current Game**").WithValue($"{(user.Game?.Name == null ? "-" : user.Game.Value.Name)}").WithIsInline(true)) - .AddField(fb => fb.WithName("**Roles**").WithValue($"**({user.RoleIds.Count})** - {string.Join(", ", user.GetRoles().Select(r => r.Name)).SanitizeMentions()}").WithIsInline(true)) + .AddField(fb => fb.WithName("**Roles**").WithValue($"**({user.RoleIds.Count})** - {string.Join(", ", user.GetRoles().Where(r => r.Id != r.Guild.EveryoneRole.Id).Select(r => r.Name)).SanitizeMentions()}").WithIsInline(true)) .WithThumbnailUrl(user.AvatarUrl) .WithColor(NadekoBot.OkColor); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index eb7c7404..3c1150f3 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -240,15 +240,27 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task ChannelTopic() + public async Task ChannelTopic([Remainder]ITextChannel channel = null) { - var channel = (ITextChannel)Context.Channel; + if (channel == null) + channel = (ITextChannel)Context.Channel; var topic = channel.Topic; if (string.IsNullOrWhiteSpace(topic)) - await channel.SendErrorAsync("No topic set."); + await channel.SendErrorAsync("No topic set.").ConfigureAwait(false); else - await channel.SendConfirmAsync("Channel topic", topic); + await channel.SendConfirmAsync("Channel topic", topic).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireBotPermission(ChannelPermission.CreateInstantInvite)] + [RequireUserPermission(ChannelPermission.CreateInstantInvite)] + public async Task CreateInvite() + { + var invite = await ((ITextChannel)Context.Channel).CreateInviteAsync(0, null, isUnique: true); + + await Context.Channel.SendConfirmAsync($"{Context.User.Mention} https://discord.gg/{invite.Code}"); } [NadekoCommand, Usage, Description, Aliases] @@ -269,8 +281,10 @@ namespace NadekoBot.Modules.Utility .AddField(efb => efb.WithName(Format.Bold("Memory")).WithValue($"{stats.Heap} MB").WithIsInline(true)) .AddField(efb => efb.WithName(Format.Bold("Owner ID(s)")).WithValue(stats.OwnerIds).WithIsInline(true)) .AddField(efb => efb.WithName(Format.Bold("Uptime")).WithValue(stats.GetUptimeString("\n")).WithIsInline(true)) - .AddField(efb => efb.WithName(Format.Bold("Presence")).WithValue($"{NadekoBot.Client.GetGuilds().Count} Servers\n{stats.TextChannels} Text Channels\n{stats.VoiceChannels} Voice Channels").WithIsInline(true)) + .AddField(efb => efb.WithName(Format.Bold("Presence")).WithValue($"{NadekoBot.Client.GetGuildsCount()} Servers\n{stats.TextChannels} Text Channels\n{stats.VoiceChannels} Voice Channels").WithIsInline(true)) +#if !GLOBAL_NADEKO .WithFooter(efb => efb.WithText($"Playing {Music.Music.MusicPlayers.Where(mp => mp.Value.CurrentSong != null).Count()} songs, {Music.Music.MusicPlayers.Sum(mp => mp.Value.Playlist.Count)} queued.")) +#endif ); } @@ -298,7 +312,7 @@ namespace NadekoBot.Modules.Utility if (page < 0) return; - var guilds = NadekoBot.Client.GetGuilds().OrderBy(g => g.Name).Skip((page - 1) * 15).Take(15); + var guilds = await Task.Run(() => NadekoBot.Client.GetGuilds().OrderBy(g => g.Name).Skip((page - 1) * 15).Take(15)).ConfigureAwait(false); if (!guilds.Any()) { diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 8b7bba44..9f72a07f 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -91,7 +91,9 @@ namespace NadekoBot //connect await Client.LoginAsync(TokenType.Bot, Credentials.Token).ConfigureAwait(false); await Client.ConnectAsync().ConfigureAwait(false); - //await Client.DownloadAllUsersAsync().ConfigureAwait(false); +#if !GLOBAL_NADEKO + await Client.DownloadAllUsersAsync().ConfigureAwait(false); +#endif _log.Info("Connected"); diff --git a/src/NadekoBot/Resources/CommandStrings.Designer.cs b/src/NadekoBot/Resources/CommandStrings.Designer.cs index 950eeaad..58126412 100644 --- a/src/NadekoBot/Resources/CommandStrings.Designer.cs +++ b/src/NadekoBot/Resources/CommandStrings.Designer.cs @@ -1841,6 +1841,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to createinvite crinv. + /// + public static string createinvite_cmd { + get { + return ResourceManager.GetString("createinvite_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Creates a new invite which has infinite max uses and never expires.. + /// + public static string createinvite_desc { + get { + return ResourceManager.GetString("createinvite_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}crinv`. + /// + public static string createinvite_usage { + get { + return ResourceManager.GetString("createinvite_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to createrole cr. /// @@ -5135,6 +5162,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to pollstats. + /// + public static string pollstats_cmd { + get { + return ResourceManager.GetString("pollstats_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows the poll results without stopping the poll on this server.. + /// + public static string pollstats_desc { + get { + return ResourceManager.GetString("pollstats_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}pollstats`. + /// + public static string pollstats_usage { + get { + return ResourceManager.GetString("pollstats_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to prune clr. /// diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index c540455d..3b13300e 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -2871,4 +2871,22 @@ `{0}rrc 60 MyLsdRole #ff0000 #00ff00 #0000ff` or `{0}rrc 0 MyLsdRole` + + createinvite crinv + + + Creates a new invite which has infinite max uses and never expires. + + + `{0}crinv` + + + pollstats + + + Shows the poll results without stopping the poll on this server. + + + `{0}pollstats` + \ No newline at end of file diff --git a/src/NadekoBot/Services/CleverBotApi/ChatterBotFactory.cs b/src/NadekoBot/Services/CleverBotApi/ChatterBotFactory.cs index 36589fe6..7b952b1c 100644 --- a/src/NadekoBot/Services/CleverBotApi/ChatterBotFactory.cs +++ b/src/NadekoBot/Services/CleverBotApi/ChatterBotFactory.cs @@ -29,10 +29,16 @@ namespace Services.CleverBotApi public static ChatterBot Create(ChatterBotType type, object arg) { +#if GLOBAL_NADEKO + var url = "http://www.cleverbot.com/webservicemin?uc=321&botapi=nadekobot"; +#else + var url = "http://www.cleverbot.com/webservicemin?uc=321"; +#endif + switch (type) { case ChatterBotType.CLEVERBOT: - return new Cleverbot("http://www.cleverbot.com/", "http://www.cleverbot.com/webservicemin?uc=321", 26); + return new Cleverbot("http://www.cleverbot.com/", url, 26); case ChatterBotType.JABBERWACKY: return new Cleverbot("http://jabberwacky.com", "http://jabberwacky.com/webservicemin", 20); case ChatterBotType.PANDORABOTS: diff --git a/src/NadekoBot/Services/CleverBotApi/Cleverbot.cs b/src/NadekoBot/Services/CleverBotApi/Cleverbot.cs index 5599d7b0..45109863 100644 --- a/src/NadekoBot/Services/CleverBotApi/Cleverbot.cs +++ b/src/NadekoBot/Services/CleverBotApi/Cleverbot.cs @@ -46,7 +46,7 @@ namespace Services.CleverBotApi private readonly int endIndex; private readonly string url; private readonly IDictionary vars; - private readonly CookieCollection cookies; + private readonly CookieCollection cookies; public CleverbotSession(string baseUrl, string url, int endIndex) { @@ -60,7 +60,7 @@ namespace Services.CleverBotApi //vars["fno"] = "0"; //vars["sub"] = "Say"; //vars["cleanslate"] = "false"; - cookies = Utils.GetCookies(baseUrl); + cookies = Utils.GetCookies(baseUrl); } public async Task Think(ChatterBotThought thought) diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index f196e20e..e4e30c4d 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -17,6 +17,8 @@ using static NadekoBot.Modules.Administration.Administration; using NadekoBot.Modules.CustomReactions; using NadekoBot.Modules.Games; using System.Collections.Concurrent; +using System.Threading; +using NadekoBot.DataStructures; namespace NadekoBot.Services { @@ -28,6 +30,8 @@ namespace NadekoBot.Services } public class CommandHandler { + public const int GlobalCommandsCooldown = 1500; + private ShardedDiscordClient _client; private CommandService _commandService; private Logger _log; @@ -39,11 +43,19 @@ namespace NadekoBot.Services //userid/msg count public ConcurrentDictionary UserMessagesSent { get; } = new ConcurrentDictionary(); + public ConcurrentHashSet UsersOnShortCooldown { get; } = new ConcurrentHashSet(); + private Timer clearUsersOnShortCooldown { get; } + public CommandHandler(ShardedDiscordClient client, CommandService commandService) { _client = client; _commandService = commandService; _log = LogManager.GetCurrentClassLogger(); + + clearUsersOnShortCooldown = new Timer((_) => + { + UsersOnShortCooldown.Clear(); + }, null, GlobalCommandsCooldown, GlobalCommandsCooldown); } public async Task StartHandling() { @@ -62,162 +74,202 @@ namespace NadekoBot.Services _client.MessageReceived += MessageReceivedHandler; } - private async void MessageReceivedHandler(SocketMessage msg) + private async Task TryRunCleverbot(SocketUserMessage usrMsg, IGuild guild) { + if (guild == null) + return false; try { - var usrMsg = msg as SocketUserMessage; - if (usrMsg == null) - return; - - //if (!usrMsg.IsAuthor()) - // UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (key, old) => ++old); - - if (msg.Author.IsBot || !NadekoBot.Ready) //no bots - return; - - var guild = (msg.Channel as SocketTextChannel)?.Guild; - - if (guild != null && guild.OwnerId != msg.Author.Id) + var cleverbotExecuted = await Games.CleverBotCommands.TryAsk(usrMsg).ConfigureAwait(false); + if (cleverbotExecuted) { - //todo split checks into their own modules - if (Permissions.FilterCommands.InviteFilteringChannels.Contains(msg.Channel.Id) || - Permissions.FilterCommands.InviteFilteringServers.Contains(guild.Id)) - { - if (usrMsg.Content.IsDiscordInvite()) - { - try - { - await usrMsg.DeleteAsync().ConfigureAwait(false); - return; - } - catch (HttpException ex) - { - _log.Warn("I do not have permission to filter invites in channel with id " + msg.Channel.Id, ex); - } - } - } - - var filteredWords = Permissions.FilterCommands.FilteredWordsForChannel(msg.Channel.Id, guild.Id).Concat(Permissions.FilterCommands.FilteredWordsForServer(guild.Id)); - var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' '); - if (filteredWords.Any(w => wordsInMessage.Contains(w))) - { - try - { - await usrMsg.DeleteAsync().ConfigureAwait(false); - return; - } - catch (HttpException ex) - { - _log.Warn("I do not have permission to filter words in channel with id " + msg.Channel.Id, ex); - } - } - } - - BlacklistItem blacklistedItem; - if ((blacklistedItem = Permissions.BlacklistCommands.BlacklistedItems.FirstOrDefault(bi => - (bi.Type == BlacklistItem.BlacklistType.Server && bi.ItemId == guild?.Id) || - (bi.Type == BlacklistItem.BlacklistType.Channel && bi.ItemId == msg.Channel.Id) || - (bi.Type == BlacklistItem.BlacklistType.User && bi.ItemId == msg.Author.Id))) != null) - { - return; - } - - try - { - var cleverbotExecuted = await Games.CleverBotCommands.TryAsk(usrMsg); - if (cleverbotExecuted) - { - _log.Info($@"CleverBot Executed + _log.Info($@"CleverBot Executed Server: {guild.Name} [{guild.Id}] Channel: {usrMsg.Channel?.Name} [{usrMsg.Channel?.Id}] UserId: {usrMsg.Author} [{usrMsg.Author.Id}] Message: {usrMsg.Content}"); - return; - } + return true; } - catch (Exception ex) { _log.Warn(ex, "Error in cleverbot"); } + } + catch (Exception ex) { _log.Warn(ex, "Error in cleverbot"); } + return false; + } - try - { - // maybe this message is a custom reaction - var crExecuted = await CustomReactions.TryExecuteCustomReaction(usrMsg).ConfigureAwait(false); + private bool IsBlacklisted(IGuild guild, SocketUserMessage usrMsg) => + (guild != null && BlacklistCommands.BlacklistedGuilds.Contains(guild.Id)) || + BlacklistCommands.BlacklistedChannels.Contains(usrMsg.Channel.Id) || + BlacklistCommands.BlacklistedUsers.Contains(usrMsg.Author.Id); - //if it was, don't execute the command - if (crExecuted) - return; - } - catch { } - string messageContent = usrMsg.Content; + private async Task LogSuccessfulExecution(SocketUserMessage usrMsg, ExecuteCommandResult exec, SocketTextChannel channel, Stopwatch sw) + { + await CommandExecuted(usrMsg, exec.CommandInfo).ConfigureAwait(false); + _log.Info("Command Executed after {4}s\n\t" + + "User: {0}\n\t" + + "Server: {1}\n\t" + + "Channel: {2}\n\t" + + "Message: {3}", + usrMsg.Author + " [" + usrMsg.Author.Id + "]", // {0} + (channel == null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]"), // {1} + (channel == null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]"), // {2} + usrMsg.Content, // {3} + sw.Elapsed.TotalSeconds); + } - var sw = new Stopwatch(); - sw.Start(); - var exec = await ExecuteCommand(new CommandContext(_client.MainClient, usrMsg), messageContent, DependencyMap.Empty, MultiMatchHandling.Best); - var command = exec.CommandInfo; - var permCache = exec.PermissionCache; - var result = exec.Result; - sw.Stop(); - var channel = (msg.Channel as ITextChannel); - if (result.IsSuccess) - { - await CommandExecuted(usrMsg, command); - _log.Info("Command Executed after {4}s\n\t" + - "User: {0}\n\t" + - "Server: {1}\n\t" + - "Channel: {2}\n\t" + - "Message: {3}", - msg.Author + " [" + msg.Author.Id + "]", // {0} - (channel == null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]"), // {1} - (channel == null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]"), // {2} - usrMsg.Content, // {3} - sw.Elapsed.TotalSeconds // {4} - ); - } - else if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) - { - _log.Warn("Command Errored after {5}s\n\t" + + private void LogErroredExecution(SocketUserMessage usrMsg, ExecuteCommandResult exec, SocketTextChannel channel, Stopwatch sw) + { + _log.Warn("Command Errored after {5}s\n\t" + "User: {0}\n\t" + "Server: {1}\n\t" + "Channel: {2}\n\t" + "Message: {3}\n\t" + "Error: {4}", - msg.Author + " [" + msg.Author.Id + "]", // {0} + usrMsg.Author + " [" + usrMsg.Author.Id + "]", // {0} (channel == null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]"), // {1} (channel == null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]"), // {2} usrMsg.Content,// {3} - result.ErrorReason, // {4} + exec.Result.ErrorReason, // {4} sw.Elapsed.TotalSeconds // {5} ); - if (guild != null && command != null && result.Error == CommandError.Exception) + } + + private async Task InviteFiltered(IGuild guild, SocketUserMessage usrMsg) + { + if ((Permissions.FilterCommands.InviteFilteringChannels.Contains(usrMsg.Channel.Id) || + Permissions.FilterCommands.InviteFilteringServers.Contains(guild.Id)) && + usrMsg.Content.IsDiscordInvite()) + { + try + { + await usrMsg.DeleteAsync().ConfigureAwait(false); + return true; + } + catch (HttpException ex) + { + _log.Warn("I do not have permission to filter invites in channel with id " + usrMsg.Channel.Id, ex); + return true; + } + } + return false; + } + + private async Task WordFiltered(IGuild guild, SocketUserMessage usrMsg) + { + var filteredChannelWords = Permissions.FilterCommands.FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id); + var filteredServerWords = Permissions.FilterCommands.FilteredWordsForServer(guild.Id); + var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' '); + if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0) + { + foreach (var word in wordsInMessage) + { + if (filteredChannelWords.Contains(word) || + filteredServerWords.Contains(word)) { - if (permCache != null && permCache.Verbose) - try { await msg.Channel.SendMessageAsync("โš ๏ธ " + result.ErrorReason).ConfigureAwait(false); } catch { } + try + { + await usrMsg.DeleteAsync().ConfigureAwait(false); + } + catch (HttpException ex) + { + _log.Warn("I do not have permission to filter words in channel with id " + usrMsg.Channel.Id, ex); + } + return true; + } + } + } + return false; + } + + private async void MessageReceivedHandler(SocketMessage msg) + { + try + { + if (msg.Author.IsBot || !NadekoBot.Ready) //no bots, wait until bot connected and initialized + return; + + var usrMsg = msg as SocketUserMessage; + if (usrMsg == null) //has to be an user message, not system/other messages. + return; + + // track how many messagges each user is sending + UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (key, old) => ++old); + + // Bot will ignore commands which are ran more often than what specified by + // GlobalCommandsCooldown constant (miliseconds) + if (!UsersOnShortCooldown.Add(usrMsg.Author.Id)) + return; + + var channel = msg.Channel as SocketTextChannel; + var guild = channel?.Guild; + + if (guild != null && guild.OwnerId != msg.Author.Id) + { + if (await InviteFiltered(guild, usrMsg).ConfigureAwait(false)) + return; + + if (await WordFiltered(guild, usrMsg).ConfigureAwait(false)) + return; + } + + if (IsBlacklisted(guild, usrMsg)) + return; + + var cleverBotRan = await TryRunCleverbot(usrMsg, guild).ConfigureAwait(false); + if (cleverBotRan) + return; + + // maybe this message is a custom reaction + var crExecuted = await CustomReactions.TryExecuteCustomReaction(usrMsg).ConfigureAwait(false); + if (crExecuted) //if it was, don't execute the command + return; + + string messageContent = usrMsg.Content; + + // execute the command and measure the time it took + var sw = Stopwatch.StartNew(); + var exec = await ExecuteCommand(new CommandContext(_client.MainClient, usrMsg), messageContent, DependencyMap.Empty, MultiMatchHandling.Best); + sw.Stop(); + + if (exec.Result.IsSuccess) + { + await LogSuccessfulExecution(usrMsg, exec, channel, sw).ConfigureAwait(false); + } + else if (!exec.Result.IsSuccess && exec.Result.Error != CommandError.UnknownCommand) + { + LogErroredExecution(usrMsg, exec, channel, sw); + if (guild != null && exec.CommandInfo != null && exec.Result.Error == CommandError.Exception) + { + if (exec.PermissionCache != null && exec.PermissionCache.Verbose) + try { await msg.Channel.SendMessageAsync("โš ๏ธ " + exec.Result.ErrorReason).ConfigureAwait(false); } catch { } } } else { if (msg.Channel is IPrivateChannel) { - //rofl, gotta do this to prevent this message from occuring on polls + // rofl, gotta do this to prevent dm help message being sent to + // users who are voting on private polls (sending a number in a DM) int vote; if (int.TryParse(msg.Content, out vote)) return; - + await msg.Channel.SendMessageAsync(Help.DMHelpString).ConfigureAwait(false); - await DMForwardCommands.HandleDMForwarding(msg, ownerChannels); + await DMForwardCommands.HandleDMForwarding(msg, ownerChannels).ConfigureAwait(false); } } } catch (Exception ex) { - _log.Warn(ex, "Error in CommandHandler"); + _log.Warn("Error in CommandHandler"); + _log.Warn(ex); if (ex.InnerException != null) - _log.Warn(ex.InnerException, "Inner Exception of the error in CommandHandler"); + { + _log.Warn("Inner Exception of the error in CommandHandler"); + _log.Warn(ex.InnerException); + } } - - return; } + public Task ExecuteCommandAsync(CommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) => ExecuteCommand(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling); @@ -312,19 +364,5 @@ namespace NadekoBot.Services return new ExecuteCommandResult(null, null, SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload.")); } - - public struct ExecuteCommandResult - { - public readonly CommandInfo CommandInfo; - public readonly PermissionCache PermissionCache; - public readonly IResult Result; - - public ExecuteCommandResult(CommandInfo commandInfo, PermissionCache cache, IResult result) - { - this.CommandInfo = commandInfo; - this.PermissionCache = cache; - this.Result = result; - } - } } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs b/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs new file mode 100644 index 00000000..7c24b564 --- /dev/null +++ b/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs @@ -0,0 +1,66 @@ +๏ปฟusing Discord; +using Discord.WebSocket; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Discord +{ + public class ReactionEventWrapper : IDisposable + { + public SocketMessage Message { get; } + public event Action OnReactionAdded = delegate { }; + public event Action OnReactionRemoved = delegate { }; + public event Action OnReactionsCleared = delegate { }; + + public ReactionEventWrapper(SocketMessage msg) + { + if (msg == null) + throw new ArgumentNullException(nameof(msg)); + Message = msg; + + msg.Discord.ReactionAdded += Discord_ReactionAdded; + msg.Discord.ReactionRemoved += Discord_ReactionRemoved; + msg.Discord.ReactionsCleared += Discord_ReactionsCleared; + } + + private Task Discord_ReactionsCleared(ulong messageId, Optional reaction) + { + if (messageId == Message.Id) + OnReactionsCleared?.Invoke(); + return Task.CompletedTask; + } + + private Task Discord_ReactionRemoved(ulong messageId, Optional arg2, SocketReaction reaction) + { + if (messageId == Message.Id) + OnReactionRemoved?.Invoke(reaction); + return Task.CompletedTask; + } + + private Task Discord_ReactionAdded(ulong messageId, Optional message, SocketReaction reaction) + { + if(messageId == Message.Id) + OnReactionAdded?.Invoke(reaction); + return Task.CompletedTask; + } + + public void UnsubAll() + { + Message.Discord.ReactionAdded -= Discord_ReactionAdded; + Message.Discord.ReactionRemoved -= Discord_ReactionRemoved; + Message.Discord.ReactionsCleared -= Discord_ReactionsCleared; + } + + private bool disposing = false; + public void Dispose() + { + if (disposing) + return; + disposing = true; + UnsubAll(); + } + } +} diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index c767ecec..558feed7 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,12 +20,15 @@ namespace NadekoBot.Services.Impl public string Library => "Discord.Net"; public int MessageCounter { get; private set; } = 0; public int CommandsRan { get; private set; } = 0; - public string Heap => Math.Round((double)GC.GetTotalMemory(false) / 1.MiB(), 2).ToString(); - public double MessagesPerSecond => MessageCounter / (double)GetUptime().TotalSeconds; - public int TextChannels => client.GetGuilds().SelectMany(g => g.Channels.Where(c => c is ITextChannel)).Count(); - public int VoiceChannels => client.GetGuilds().SelectMany(g => g.Channels.Where(c => c is IVoiceChannel)).Count(); + public string Heap => + Math.Round((double)GC.GetTotalMemory(false) / 1.MiB(), 2).ToString(); + public double MessagesPerSecond => MessageCounter / GetUptime().TotalSeconds; + private int _textChannels = 0; + public int TextChannels => _textChannels; + private int _voiceChannels = 0; + public int VoiceChannels => _voiceChannels; public string OwnerIds => string.Join(", ", NadekoBot.Credentials.OwnerIds); - + Timer carbonitexTimer { get; } public StatsService(ShardedDiscordClient client, CommandHandler cmdHandler) @@ -39,17 +42,56 @@ namespace NadekoBot.Services.Impl this.client.Disconnected += _ => Reset(); + this.client.Connected += () => + { + var guilds = this.client.GetGuilds(); + _textChannels = guilds.Sum(g => g.Channels.Where(cx => cx is ITextChannel).Count()); + _voiceChannels = guilds.Sum(g => g.Channels.Count) - _textChannels; + }; + + this.client.ChannelCreated += (c) => + { + if (c is ITextChannel) + ++_textChannels; + else if (c is IVoiceChannel) + ++_voiceChannels; + }; + + this.client.ChannelDestroyed += (c) => + { + if (c is ITextChannel) + --_textChannels; + else if (c is IVoiceChannel) + --_voiceChannels; + }; + + this.client.JoinedGuild += (g) => + { + var tc = g.Channels.Where(cx => cx is ITextChannel).Count(); + var vc = g.Channels.Count - tc; + _textChannels += tc; + _voiceChannels += vc; + }; + + this.client.LeftGuild += (g) => + { + var tc = g.Channels.Where(cx => cx is ITextChannel).Count(); + var vc = g.Channels.Count - tc; + _textChannels -= tc; + _voiceChannels -= vc; + }; + this.carbonitexTimer = new Timer(async (state) => { - if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.CarbonKey)) - return; + if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.CarbonKey)) + return; try { using (var http = new HttpClient()) { using (var content = new FormUrlEncodedContent( new Dictionary { - { "servercount", this.client.GetGuilds().Count.ToString() }, + { "servercount", this.client.GetGuildsCount().ToString() }, { "key", NadekoBot.Credentials.CarbonKey }})) { content.Headers.Clear(); @@ -71,7 +113,7 @@ Bot Version: [{BotVersion}] Bot ID: {curUser.Id} Owner ID(s): {OwnerIds} Uptime: {GetUptimeString()} -Servers: {client.GetGuilds().Count} | TextChannels: {TextChannels} | VoiceChannels: {VoiceChannels} +Servers: {client.GetGuildsCount()} | TextChannels: {TextChannels} | VoiceChannels: {VoiceChannels} Commands Ran this session: {CommandsRan} Messages: {MessageCounter} [{MessagesPerSecond:F2}/sec] Heap: [{Heap} MB]"); } diff --git a/src/NadekoBot/ShardedDiscordClient.cs b/src/NadekoBot/ShardedDiscordClient.cs index c2300bd0..7a538192 100644 --- a/src/NadekoBot/ShardedDiscordClient.cs +++ b/src/NadekoBot/ShardedDiscordClient.cs @@ -18,6 +18,7 @@ namespace NadekoBot public event Action MessageReceived = delegate { }; public event Action UserLeft = delegate { }; public event Action UserUpdated = delegate { }; + public event Action GuildUserUpdated = delegate { }; public event Action, SocketMessage> MessageUpdated = delegate { }; public event Action> MessageDeleted = delegate { }; public event Action UserBanned = delegate { }; @@ -27,11 +28,18 @@ namespace NadekoBot public event Action ChannelCreated = delegate { }; public event Action ChannelDestroyed = delegate { }; public event Action ChannelUpdated = delegate { }; + + public event Action JoinedGuild = delegate { }; + public event Action LeftGuild = delegate { }; + public event Action Disconnected = delegate { }; + public event Action Connected = delegate { }; private uint _connectedCount = 0; private uint _downloadedCount = 0; + private int _guildCount = 0; + private IReadOnlyList Clients { get; } public ShardedDiscordClient(DiscordSocketConfig discordSocketConfig) @@ -54,6 +62,7 @@ namespace NadekoBot }; client.UserLeft += arg1 => { UserLeft(arg1); return Task.CompletedTask; }; client.UserUpdated += (arg1, gu2) => { UserUpdated(arg1, gu2); return Task.CompletedTask; }; + client.GuildMemberUpdated += (arg1, arg2) => { GuildUserUpdated(arg1, arg2); return Task.CompletedTask; }; client.MessageUpdated += (arg1, m2) => { MessageUpdated(arg1, m2); return Task.CompletedTask; }; client.MessageDeleted += (arg1, arg2) => { MessageDeleted(arg1, arg2); return Task.CompletedTask; }; client.UserBanned += (arg1, arg2) => { UserBanned(arg1, arg2); return Task.CompletedTask; }; @@ -63,9 +72,13 @@ namespace NadekoBot client.ChannelCreated += arg => { ChannelCreated(arg); return Task.CompletedTask; }; client.ChannelDestroyed += arg => { ChannelDestroyed(arg); return Task.CompletedTask; }; client.ChannelUpdated += (arg1, arg2) => { ChannelUpdated(arg1, arg2); return Task.CompletedTask; }; + client.JoinedGuild += (arg1) => { JoinedGuild(arg1); ++_guildCount; return Task.CompletedTask; }; + client.LeftGuild += (arg1) => { LeftGuild(arg1); --_guildCount; return Task.CompletedTask; }; _log.Info($"Shard #{i} initialized."); +#if GLOBAL_NADEKO client.Log += Client_Log; +#endif var j = i; client.Disconnected += (ex) => { @@ -91,11 +104,22 @@ namespace NadekoBot public SocketSelfUser CurrentUser() => Clients[0].CurrentUser; - public IReadOnlyCollection GetGuilds() => - Clients.SelectMany(c => c.Guilds).ToList(); + public IEnumerable GetGuilds() => + Clients.SelectMany(c => c.Guilds); - public SocketGuild GetGuild(ulong id) => - Clients.Select(c => c.GetGuild(id)).FirstOrDefault(g => g != null); + public int GetGuildsCount() => + _guildCount; + + public SocketGuild GetGuild(ulong id) + { + foreach (var c in Clients) + { + var g = c.GetGuild(id); + if (g != null) + return g; + } + return null; + } public Task GetDMChannelAsync(ulong channelId) => Clients[0].GetDMChannelAsync(channelId); @@ -120,6 +144,7 @@ namespace NadekoBot await c.ConnectAsync().ConfigureAwait(false); sw.Stop(); _log.Info($"Shard #{c.ShardId} connected after {sw.Elapsed.TotalSeconds:F2}s ({++_connectedCount}/{Clients.Count})"); + _guildCount += c.Guilds.Count; } catch { @@ -132,6 +157,7 @@ namespace NadekoBot } } } + Connected(); } internal Task DownloadAllUsersAsync() => diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index f9a8157b..3994e54f 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -1,6 +1,8 @@ ๏ปฟusing Discord; using Discord.Commands; +using Discord.WebSocket; using ImageSharp; +using NadekoBot.Services.Discord; using Newtonsoft.Json; using System; using System.Collections.Concurrent; @@ -16,6 +18,13 @@ namespace NadekoBot.Extensions { public static class Extensions { + public static ReactionEventWrapper OnReactionAdded(this SocketMessage msg, Action reactionAdded) + { + var wrap = new ReactionEventWrapper(msg); + wrap.OnReactionAdded += reactionAdded; + return wrap; + } + public static void AddFakeHeaders(this HttpClient http) { http.DefaultRequestHeaders.Clear();