Пример #1
0
        /// <summary>
        /// Sends mod log messages if configured, and re-processes messages if updated recently.
        /// </summary>
        private async Task HandleMessageUpdated(Cacheable <IMessage, ulong> messageBefore, SocketMessage messageAfter, ISocketMessageChannel channel)
        {
            if (messageAfter?.Channel is IGuildChannel guildChannel)
            {
                var textChannel = guildChannel as ITextChannel;
                var settings    = SettingsConfig.GetSettings(guildChannel.Guild.Id);

                if (settings.HasFlag(ModOptions.Mod_LogEdit) &&
                    this.Client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && modLogChannel.GetCurrentUserPermissions().SendMessages&&
                    messageAfter.Channel.Id != settings.Mod_LogId && !messageAfter.Author.IsBot)
                {
                    if (messageBefore.HasValue && messageBefore.Value.Content != messageAfter.Content && !string.IsNullOrEmpty(messageBefore.Value.Content))
                    {
                        string editText = $"**{messageAfter.Author.Username}** modified in {textChannel.Mention}: `{messageBefore.Value.Content}` to `{messageAfter.Content}`";
                        await modLogChannel.SendMessageAsync(editText.SubstringUpTo(Discord.DiscordConfig.MaxMessageSize));
                    }
                }

                // if the message is from the last hour, see if we can re-process it.
                if (messageBefore.HasValue && messageAfter.Content != messageBefore.Value.Content && messageAfter.Author.Id != this.Client.CurrentUser.Id &&
                    DateTimeOffset.UtcNow.Subtract(messageAfter.Timestamp) < TimeSpan.FromHours(1))
                {
                    await this.HandleMessageReceivedAsync(messageAfter);
                }
            }
        }
Пример #2
0
        /// <summary>
        /// Sends farewells and mod log messages, if configured.
        /// </summary>
        private async Task HandleUserLeftAsync(SocketGuildUser guildUser)
        {
            var settings = SettingsConfig.GetSettings(guildUser.Guild.Id);

            if (!string.IsNullOrEmpty(settings.Farewell) && settings.FarewellId != 0)
            {
                var farewell = settings.Farewell.Replace("%user%", guildUser.Mention);
                farewell = farewell.Replace("%username%", $"{guildUser.Username}#{guildUser.Discriminator}");

                farewell = Consts.ChannelRegex.Replace(farewell, new MatchEvaluator((Match chanMatch) =>
                {
                    string channelName = chanMatch.Captures[0].Value;
                    var channel        = guildUser.Guild.Channels.Where(c => c is ITextChannel && c.Name.Equals(channelName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
                    return((channel as ITextChannel)?.Mention ?? channelName);
                }));

                var farewellChannel = this.Client.GetChannel(settings.FarewellId) as ITextChannel ?? guildUser.Guild.DefaultChannel;
                if (farewellChannel.GetCurrentUserPermissions().SendMessages)
                {
                    await farewellChannel.SendMessageAsync(farewell);
                }
            }

            // mod log
            if (settings.HasFlag(ModOptions.Mod_LogUserLeave) && this.Client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && modLogChannel.GetCurrentUserPermissions().SendMessages)
            {
                this.BatchSendMessageAsync(modLogChannel, $"{guildUser.Username}#{guildUser.Discriminator} left.");
            }
        }
Пример #3
0
        protected void TrackEvent(string eventName, Dictionary <string, string> properties = null)
        {
            // set common properties/tags
            if (properties == null)
            {
                properties = new Dictionary <string, string>();
            }

            properties["botType"] = $"{this.BotType}";
            properties["shard"]   = $"{this.Shard}";

            // remove server/channel properties from discord unless sponsored by a patron
            if (this.BotType == BotType.Discord && properties.ContainsKey("server"))
            {
                var settings = SettingsConfig.GetSettings(properties["server"]);
                if (settings.PatronSponsor == 0)
                {
                    properties.Remove("server");
                    properties.Remove("channel");
                }
            }

            var tags = new List <string>();

            foreach (var kvp in properties)
            {
                tags.Add($"{kvp.Key}:{kvp.Value}");
            }

            this.DogStats?.Increment(eventName, tags: tags.ToArray());
        }
Пример #4
0
        /// <summary>
        /// Announces user voice join/leave and sends mod log messages, if configured.
        /// </summary>
        private async Task HandleUserVoiceStateUpdatedAsync(SocketUser user, SocketVoiceState beforeState, SocketVoiceState afterState)
        {
            // voice state detection
            var guildUser    = (user as SocketGuildUser);
            var botGuildUser = guildUser.Guild.CurrentUser;

            if (guildUser.Id != botGuildUser.Id) // ignore joins/leaves from the bot
            {
                if (beforeState.VoiceChannel != afterState.VoiceChannel && afterState.VoiceChannel == botGuildUser.VoiceChannel)
                {
                    // if they are connecting for the first time, wait a moment to account for possible conncetion delay. otherwise play immediately.
                    if (beforeState.VoiceChannel == null)
                    {
                        await Task.Delay(1000);
                    }

                    await this.audioManager.SendAudioAsync(guildUser, afterState.VoiceChannel, VoicePhraseType.UserJoin);
                }
                else if (beforeState.VoiceChannel != afterState.VoiceChannel && beforeState.VoiceChannel == botGuildUser.VoiceChannel)
                {
                    await this.audioManager.SendAudioAsync(guildUser, beforeState.VoiceChannel, VoicePhraseType.UserLeave);
                }
            }

            // mod logging
            var settings = SettingsConfig.GetSettings(guildUser.Guild.Id);

            if (settings.Mod_LogId != 0)
            {
                if (this.Client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && modLogChannel.GetCurrentUserPermissions().SendMessages)
                {
                    var msg = string.Empty;

                    if (settings.HasFlag(ModOptions.Mod_LogUserLeaveVoice) && beforeState.VoiceChannel != null && beforeState.VoiceChannel.Id != afterState.VoiceChannel?.Id)
                    {
                        msg = $"{guildUser.Username} left voice channel { beforeState.VoiceChannel.Name}";
                    }

                    if (settings.HasFlag(ModOptions.Mod_LogUserJoinVoice) && afterState.VoiceChannel != null && afterState.VoiceChannel.Id != beforeState.VoiceChannel?.Id)
                    {
                        if (string.IsNullOrEmpty(msg))
                        {
                            msg = $"{guildUser.Username} joined voice channel {afterState.VoiceChannel.Name}";
                        }
                        else
                        {
                            msg += $" and joined voice channel {afterState.VoiceChannel.Name}";
                        }
                    }

                    if (!string.IsNullOrEmpty(msg))
                    {
                        this.BatchSendMessageAsync(modLogChannel, msg);
                    }
                }
            }
        }
Пример #5
0
        private async Task HandleReactionAdded(Cacheable <IUserMessage, ulong> message, ISocketMessageChannel channel, SocketReaction reaction)
        {
            if (reaction.User.IsSpecified && reaction.User.Value.IsBot)
            {
                return;
            }

            string reactionEmote = reaction.Emote.Name;

            // if an Eye emoji was added, let's process it
            if ((reactionEmote == "👁" || reactionEmote == "🖼") &&
                reaction.Message.IsSpecified &&
                (IsAuthorPatron(reaction.UserId) || BotConfig.Instance.OcrAutoIds.Contains(channel.Id)) &&
                reaction.Message.Value.ParseImageUrl() != null)
            {
                if (reaction.Message.Value.Reactions.Any(r => (r.Key.Name == "👁" || r.Key.Name == "🖼") && r.Value.ReactionCount > 1))
                {
                    return;
                }

                await this.HandleMessageReceivedAsync(reaction.Message.Value, reactionEmote);
            }

            var guildChannel = channel as SocketTextChannel;
            var settings     = SettingsConfig.GetSettings(guildChannel.Guild.Id);
            var customEmote  = reaction.Emote as Emote;

            if ((reactionEmote == "💬" || reactionEmote == "🗨️" || reactionEmote == "❓" || reactionEmote == "🤖") && reaction.Message.IsSpecified && !string.IsNullOrEmpty(reaction.Message.Value?.Content))
            {
                // if the reaction already exists, don't re-process.
                if (reaction.Message.Value.Reactions.Any(r => (r.Key.Name == "💬" || r.Key.Name == "🗨️" || r.Key.Name == "❓" || r.Key.Name == "🤖") && r.Value.ReactionCount > 1))
                {
                    return;
                }

                await this.HandleMessageReceivedAsync(reaction.Message.Value, reactionEmote, reaction.User.Value);
            }
            else if (reactionEmote == "➕" || reactionEmote == "➖" || customEmote?.Id == settings.RoleAddEmoteId || customEmote?.Id == settings.RoleRemoveEmoteId)
            {
                // handle possible role adds/removes
                IUserMessage reactionMessage = null;
                if (reaction.Message.IsSpecified)
                {
                    reactionMessage = reaction.Message.Value;
                }
                else
                {
                    reactionMessage = await reaction.Channel.GetMessageAsync(reaction.MessageId) as IUserMessage;
                }

                if (await RoleCommand.AddRoleViaReaction(reactionMessage, reaction))
                {
                    await reactionMessage.RemoveReactionAsync(reaction.Emote, reaction.User.Value);
                }
            }
        }
        /// <summary>
        /// Sends mod log messages for user bans, if configured.
        /// </summary>
        private async Task HandleUserBanned(SocketUser user, SocketGuild guild)
        {
            // mod log
            var settings = SettingsConfig.GetSettings(guild.Id);

            if (settings.HasFlag(ModOptions.Mod_LogUserBan) && this.Client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && modLogChannel.GetCurrentUserPermissions().SendMessages)
            {
                string userIdentifier = user != null ? $"{user.Username}#{user.Discriminator}" : "Unknown user";
                await modLogChannel.SendMessageAsync($"{userIdentifier} was banned.");
            }
        }
Пример #7
0
        /// <summary>
        /// Sends greetings and mod log messages, and sets an auto role, if configured.
        /// </summary>
        private async Task HandleUserJoinedAsync(SocketGuildUser guildUser)
        {
            var settings = SettingsConfig.GetSettings(guildUser.Guild.Id);

            if (!string.IsNullOrEmpty(settings.Greeting) && settings.GreetingId != 0)
            {
                var greeting = settings.Greeting.Replace("%user%", guildUser.Mention);
                greeting = greeting.Replace("%username%", $"{guildUser.Username}#{guildUser.Discriminator}");

                greeting = Consts.ChannelRegex.Replace(greeting, new MatchEvaluator((Match chanMatch) =>
                {
                    string channelName = chanMatch.Groups[1].Value;
                    var channel        = guildUser.Guild.Channels.Where(c => c is ITextChannel && c.Name.Equals(channelName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
                    return((channel as ITextChannel)?.Mention ?? $"#{channelName}");
                }));

                var greetingChannel = this.Client.GetChannel(settings.GreetingId) as ITextChannel ?? guildUser.Guild.DefaultChannel;
                if (greetingChannel.GetCurrentUserPermissions().SendMessages)
                {
                    await greetingChannel.SendMessageAsync(greeting);
                }
            }

            if (settings.JoinRoleId != 0 && guildUser.Guild.CurrentUser.GuildPermissions.ManageRoles)
            {
                var role = guildUser.Guild.GetRole(settings.JoinRoleId);
                if (role != null)
                {
                    try
                    {
                        await guildUser.AddRolesAsync(new[] { role });
                    }
                    catch (HttpException ex) when(ex.HttpCode == HttpStatusCode.Forbidden)
                    {
                        await guildUser.Guild.SendOwnerDMAsync($"Permissions error detected for {guildUser.Guild.Name}: Auto role add on user joined failed, role `{role.Name}` is higher in order than my role");
                    }
                    catch (HttpException ex) when(ex.HttpCode == HttpStatusCode.NotFound)
                    {
                        await guildUser.Guild.SendOwnerDMAsync($"Error detected for {guildUser.Guild.Name}: Auto role add on user joined failed, role `{role.Name}` does not exist");
                    }
                }
            }

            // mod log
            if (settings.Mod_LogId != 0 && settings.HasFlag(ModOptions.Mod_LogUserJoin))
            {
                string joinText = $"{guildUser.Username}#{guildUser.Discriminator} joined.";
                if (this.Client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && modLogChannel.GetCurrentUserPermissions().SendMessages)
                {
                    this.BatchSendMessageAsync(modLogChannel, joinText);
                }
            }
        }
Пример #8
0
        /// <summary>
        /// Sends mod log messages for role and nickname changes, if configured.
        /// </summary>
        private Task HandleGuildMemberUpdated(SocketGuildUser guildUserBefore, SocketGuildUser guildUserAfter)
        {
            // Mod log
            var settings = SettingsConfig.GetSettings(guildUserBefore.Guild.Id);

            if (this.Client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && (modLogChannel.GetCurrentUserPermissions().SendMessages))
            {
                if (settings.HasFlag(ModOptions.Mod_LogUserRole))
                {
                    var rolesAdded = (from role in guildUserAfter.Roles
                                      where guildUserBefore.Roles.All(r => r.Id != role.Id)
                                      select guildUserAfter.Guild.Roles.First(g => g.Id == role.Id).Name.TrimStart('@')).ToList();

                    var rolesRemoved = (from role in guildUserBefore.Roles
                                        where guildUserAfter.Roles.All(r => r.Id != role.Id)
                                        select guildUserBefore.Guild.Roles.First(g => g.Id == role.Id).Name.TrimStart('@')).ToList();

                    if (rolesAdded.Count > 0)
                    {
                        string roleText = $"**{guildUserAfter.Username}#{guildUserAfter.Discriminator}** had these roles added: `{string.Join(",", rolesAdded)}`";
                        this.BatchSendMessageAsync(modLogChannel, roleText);
                    }

                    if (rolesRemoved.Count > 0)
                    {
                        string roleText = $"**{guildUserAfter.Username}#{guildUserAfter.Discriminator}** had these roles removed: `{string.Join(",", rolesRemoved)}`";
                        this.BatchSendMessageAsync(modLogChannel, roleText);
                    }
                }

                if (settings.HasFlag(ModOptions.Mod_LogUserNick) && guildUserAfter.Nickname != guildUserBefore.Nickname)
                {
                    if (string.IsNullOrEmpty(guildUserAfter.Nickname))
                    {
                        string nickText = $"{guildUserAfter.Username}#{guildUserAfter.Discriminator} removed their nickname (was {guildUserBefore.Nickname})";
                        this.BatchSendMessageAsync(modLogChannel, nickText);
                    }
                    else if (string.IsNullOrEmpty(guildUserBefore.Nickname))
                    {
                        string nickText = $"{guildUserAfter.Username}#{guildUserAfter.Discriminator} set a new nickname to {guildUserAfter.Nickname}";
                        this.BatchSendMessageAsync(modLogChannel, nickText);
                    }
                    else
                    {
                        string nickText = $"{guildUserAfter.Username}#{guildUserAfter.Discriminator} changed their nickname from {guildUserBefore.Nickname} to {guildUserAfter.Nickname}";
                        this.BatchSendMessageAsync(modLogChannel, nickText);
                    }
                }
            }

            return(Task.CompletedTask);
        }
Пример #9
0
        /// <summary>
        /// Sends mod log messages for user bans, if configured.
        /// </summary>
        private Task HandleUserBanned(SocketUser user, SocketGuild guild)
        {
            // mod log
            var settings = SettingsConfig.GetSettings(guild.Id);

            if (settings.HasFlag(ModOptions.Mod_LogUserBan) && this.Client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && modLogChannel.GetCurrentUserPermissions().SendMessages)
            {
                string userIdentifier = user != null ? $"{user}" : "Unknown user";
                this.BatchSendMessageAsync(modLogChannel, $"{userIdentifier} was banned.");
            }

            return(Task.CompletedTask);
        }
Пример #10
0
        /// <summary>
        /// Handles leaving a guild. Calls the prune endpoint to clear out settings.
        /// </summary>
        private async Task HandleLeftGuildAsync(SocketGuild guild)
        {
            this.TrackEvent("serverLeave");

            SettingsConfig.RemoveSettings(guild.Id.ToString());

            if (this.Config.PruneEndpoint != null)
            {
                var req = WebRequest.Create($"{this.Config.PruneEndpoint}?id={guild.Id}");
                await req.GetResponseAsync();
            }

            await audioManager.LeaveAudioAsync(guild.Id);
        }
Пример #11
0
        /// <summary>
        /// Sends mod log messages if configured, and deletes any corresponding bot response.
        /// </summary>
        private async Task HandleMessageDeleted(Cacheable <IMessage, ulong> cachedMessage, ISocketMessageChannel channel)
        {
            var msg = this.botResponsesCache.Remove(cachedMessage.Id);

            if (msg != null)
            {
                try
                {
                    await msg.DeleteAsync();
                }
                catch (Exception)
                {
                    // ignore, don't care if we can't delete our own message
                }
            }

            if (cachedMessage.HasValue && channel is IGuildChannel guildChannel)
            {
                var message     = cachedMessage.Value;
                var textChannel = guildChannel as ITextChannel;
                var guild       = guildChannel.Guild as SocketGuild;
                var settings    = SettingsConfig.GetSettings(guildChannel.GuildId);

                if (settings.HasFlag(ModOptions.Mod_LogDelete) && guildChannel.Id != settings.Mod_LogId && !message.Author.IsBot &&
                    this.Client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && modLogChannel.GetCurrentUserPermissions().SendMessages)
                {
                    string delText = "";

                    if (settings.TriggersCensor(message.Content, out _))
                    {
                        delText = "```Word Censor Triggered```";
                    }

                    delText += $"**{message.Author.Mention} ({message.Author})** deleted in {textChannel.Mention}: {message.Content}";

                    // Include attachment URLs, if applicable
                    if (message.Attachments?.Count > 0)
                    {
                        delText += " " + string.Join(" ", message.Attachments.Select(a => a.Url));
                    }

                    this.BatchSendMessageAsync(modLogChannel, delText.SubstringUpTo(Discord.DiscordConfig.MaxMessageSize));
                }
            }
        }
Пример #12
0
        /// <summary>
        /// Handles leaving a guild. Calls the prune endpoint to clear out settings.
        /// </summary>
        private async Task HandleLeftGuildAsync(SocketGuild guild)
        {
            this.TrackEvent("serverLeave");

            SettingsConfig.RemoveSettings(guild.Id.ToString());

            if (this.BotApi != null)
            {
                try
                {
                    await this.BotApi.IssueRequestAsync(new BotMessageData(BotType.Discord) { Content = ".prune", Prefix = ".", Server = guild.Id.ToString() });
                }
                catch (Exception ex)
                {
                    Log.Error(ex, $"Error calling prune command");
                }
            }

            await audioManager.LeaveAudioAsync(guild.Id);
        }
Пример #13
0
        /// <summary>
        /// Sends mod log messages if configured, and deletes any corresponding bot response.
        /// </summary>
        private async Task HandleMessageDeleted(Cacheable <IMessage, ulong> cachedMessage, ISocketMessageChannel channel)
        {
            var msg = this.botResponsesCache.Remove(cachedMessage.Id);

            if (msg != null)
            {
                try
                {
                    await msg.DeleteAsync();
                }
                catch (Exception)
                {
                    // ignore, don't care if we can't delete our own message
                }
            }

            if (cachedMessage.HasValue && channel is IGuildChannel guildChannel)
            {
                var message     = cachedMessage.Value;
                var textChannel = guildChannel as ITextChannel;
                var guild       = guildChannel.Guild as SocketGuild;
                var settings    = SettingsConfig.GetSettings(guildChannel.GuildId);

                if (settings.HasFlag(ModOptions.Mod_LogDelete) && guildChannel.Id != settings.Mod_LogId && !message.Author.IsBot &&
                    this.Client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && modLogChannel.GetCurrentUserPermissions().SendMessages)
                {
                    string delText = "";

                    if (settings.TriggersCensor(message.Content, out _))
                    {
                        delText = "```Word Censor Triggered```";
                    }

                    var deletedContent = message.Content.Replace("@everyone", "@every\x200Bone").Replace("@here", "@he\x200Bre");
                    if (message.MentionedUserIds.Count > 0)
                    {
                        var guildUsers = guild.Users;

                        foreach (var userId in message.MentionedUserIds)
                        {
                            var guildUser = guildUsers.FirstOrDefault(u => u.Id == userId);
                            if (guildUser != null)
                            {
                                deletedContent = deletedContent.Replace($"{userId}", guildUser.Nickname ?? guildUser.Username);
                            }
                        }
                    }

                    if (message.MentionedRoleIds.Count > 0)
                    {
                        var guildRoles = guild.Roles;

                        foreach (var roleId in message.MentionedRoleIds)
                        {
                            var guildRole = guildRoles.FirstOrDefault(u => u.Id == roleId);
                            deletedContent = deletedContent.Replace($"{roleId}", guildRole.Name);
                        }
                    }

                    delText += $"**{message.Author.Username}#{message.Author.Discriminator}** deleted in {textChannel.Mention}: {deletedContent}";

                    // Include attachment URLs, if applicable
                    if (message.Attachments?.Count > 0)
                    {
                        delText += " " + string.Join(" ", message.Attachments.Select(a => a.Url));
                    }

                    this.BatchSendMessageAsync(modLogChannel, delText.SubstringUpTo(Discord.DiscordConfig.MaxMessageSize));
                }
            }
        }
Пример #14
0
        /// <summary>
        /// Handles responses for messages.
        /// TODO: This method is way too huge.
        /// TODO: read prior todo, IT'S GETTING WORSe, STAHP
        /// </summary>
        private async Task HandleMessageReceivedAsync(SocketMessage socketMessage, string reactionType = null, IUser reactionUser = null)
        {
            var props = new Dictionary <string, string> {
                { "server", (socketMessage.Channel as SocketGuildChannel)?.Guild.Id.ToString() ?? "private" },
                { "channel", socketMessage.Channel.Id.ToString() },
            };

            this.TrackEvent("messageReceived", props);

            // Ignore system and our own messages.
            var  message    = socketMessage as SocketUserMessage;
            bool isOutbound = false;

            // replicate to webhook, if configured
            this.CallOutgoingWebhookAsync(message).Forget();

            if (message == null || (isOutbound = message.Author.Id == this.Client.CurrentUser.Id))
            {
                if (isOutbound && this.Config.LogOutgoing)
                {
                    var logMessage = message.Embeds?.Count > 0 ? $"Sending [embed content] to {message.Channel.Name}" : $"Sending to {message.Channel.Name}: {message.Content}";
                    Log.Verbose($"{{Outgoing}} {logMessage}", ">>>");
                }

                return;
            }

            // Ignore other bots unless it's an allowed webhook
            // Always ignore bot reactions
            var webhookUser = message.Author as IWebhookUser;

            if (message.Author.IsBot && !this.Config.Discord.AllowedWebhooks.Contains(webhookUser?.WebhookId ?? 0) || reactionUser?.IsBot == true)
            {
                return;
            }

            // grab the settings for this server
            var botGuildUser = (message.Channel as SocketGuildChannel)?.Guild.CurrentUser;
            var guildUser    = message.Author as SocketGuildUser;
            var guildId      = webhookUser?.GuildId ?? guildUser?.Guild.Id;
            var settings     = SettingsConfig.GetSettings(guildId?.ToString());

            // if it's a globally blocked server, ignore it unless it's the owner
            if (message.Author.Id != this.Config.Discord.OwnerId && guildId != null && this.Config.Discord.BlockedServers.Contains(guildId.Value) && !this.Config.OcrAutoIds.Contains(message.Channel.Id))
            {
                return;
            }

            // if it's a globally blocked user, ignore them
            if (this.Config.Discord.BlockedUsers.Contains(message.Author.Id))
            {
                return;
            }

            if (this.Throttler.IsThrottled(message.Author.Id.ToString(), ThrottleType.User))
            {
                Log.Debug($"messaging throttle from user: {message.Author.Id} on chan {message.Channel.Id} server {guildId}");
                return;
            }

            // Bail out with help info if it's a PM
            if (message.Channel is IDMChannel)
            {
                await this.RespondAsync(message, "Info and commands can be found at: https://ub3r-b0t.com [Commands don't work in direct messages]");

                return;
            }

            if (this.Throttler.IsThrottled(guildId.ToString(), ThrottleType.Guild))
            {
                Log.Debug($"messaging throttle from guild: {message.Author.Id} on chan {message.Channel.Id} server {guildId}");
                return;
            }

            var botContext = new DiscordBotContext(this.Client, message)
            {
                Reaction     = reactionType,
                ReactionUser = reactionUser,
                BotApi       = this.BotApi,
                AudioManager = this.audioManager,
                Bot          = this,
            };

            foreach (var module in this.modules)
            {
                var typeInfo = module.GetType().GetTypeInfo();
                var permissionChecksPassed = await this.CheckPermissions(botContext, typeInfo);

                if (!permissionChecksPassed)
                {
                    continue;
                }

                var result = await module.Process(botContext);

                if (result == ModuleResult.Stop)
                {
                    return;
                }
            }

            var textChannel = message.Channel as ITextChannel;

            if (botGuildUser != null && !botGuildUser.GetPermissions(textChannel).SendMessages)
            {
                return;
            }

            // If it's a command, match that before anything else.
            await this.PreProcessMessage(botContext.MessageData, settings);

            string command = botContext.MessageData.Command;

            if (message.Attachments.FirstOrDefault() is Attachment attachment)
            {
                this.ImageUrls[botContext.MessageData.Channel] = attachment;
            }

            // if it's a blocked command, bail
            if (settings.IsCommandDisabled(CommandsConfig.Instance, command) && !IsAuthorOwner(message))
            {
                return;
            }

            if (this.discordCommands.TryGetValue(command, out IDiscordCommand discordCommand))
            {
                var commandProps = new Dictionary <string, string> {
                    { "command", command.ToLowerInvariant() },
                    { "server", botContext.MessageData.Server },
                    { "channel", botContext.MessageData.Channel }
                };
                this.TrackEvent("commandProcessed", commandProps);

                var typeInfo = discordCommand.GetType().GetTypeInfo();
                var permissionChecksPassed = await this.CheckPermissions(botContext, typeInfo);

                if (permissionChecksPassed)
                {
                    CommandResponse response;

                    using (this.DogStats?.StartTimer("commandDuration", tags: new[] { $"shard:{this.Shard}", $"command:{command.ToLowerInvariant()}", $"{this.BotType}" }))
                    {
                        response = await discordCommand.Process(botContext);
                    }

                    if (response?.Attachment != null)
                    {
                        var sentMessage = await message.Channel.SendFileAsync(response.Attachment.Stream, response.Attachment.Name, response.Text);

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                    else if (response?.MultiText != null)
                    {
                        foreach (var messageText in response.MultiText)
                        {
                            var sentMessage = await this.RespondAsync(message, messageText, response.Embed, bypassEdit : true);

                            this.botResponsesCache.Add(message.Id, sentMessage);
                        }
                    }
                    else if (!string.IsNullOrEmpty(response?.Text) || response?.Embed != null)
                    {
                        var sentMessage = await this.RespondAsync(message, response.Text, response.Embed);

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                }
            }
            else
            {
                // Enter typing state for valid commands; skip if throttled
                IDisposable typingState = null;
                var         commandKey  = command + botContext.MessageData.Server;
                if (this.Config.Discord.TriggerTypingOnCommands &&
                    CommandsConfig.Instance.Commands.ContainsKey(command) &&
                    !this.Throttler.IsThrottled(commandKey, ThrottleType.Command))
                {
                    // possible bug with typing state
                    Log.Debug($"typing triggered by {command}");
                    typingState = message.Channel.EnterTypingState();
                }

                if (botContext.MessageData.Command == "quote" && reactionUser != null)
                {
                    botContext.MessageData.UserName = reactionUser.Username;
                }

                try
                {
                    BotResponseData responseData = await this.ProcessMessageAsync(botContext.MessageData, settings);

                    if (Uri.TryCreate(responseData.AttachmentUrl, UriKind.Absolute, out Uri attachmentUri))
                    {
                        Stream fileStream = await attachmentUri.GetStreamAsync();

                        var sentMessage = await message.Channel.SendFileAsync(fileStream, Path.GetFileName(attachmentUri.AbsolutePath));

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                    else if (responseData.Embed != null)
                    {
                        var sentMessage = await this.RespondAsync(message, string.Empty, responseData.Embed.CreateEmbedBuilder().Build(), bypassEdit : false, rateLimitChecked : botContext.MessageData.RateLimitChecked);

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                    else
                    {
                        foreach (string response in responseData.Responses)
                        {
                            if (!string.IsNullOrWhiteSpace(response))
                            {
                                // if sending a multi part message, skip the edit optimization.
                                var sentMessage = await this.RespondAsync(message, response, embedResponse : null, bypassEdit : responseData.Responses.Count > 1, rateLimitChecked : botContext.MessageData.RateLimitChecked);

                                this.botResponsesCache.Add(message.Id, sentMessage);
                            }
                        }
                    }
                }
                finally
                {
                    typingState?.Dispose();
                }
            }
        }
Пример #15
0
        /// <summary>
        /// Handles responses for messages.
        /// TODO: This method is way too huge.
        /// </summary>
        private async Task HandleMessageReceivedAsync(SocketMessage socketMessage, string reactionType = null, IUser reactionUser = null)
        {
            var props = new Dictionary <string, string> {
                { "server", (socketMessage.Channel as SocketGuildChannel)?.Guild.Id.ToString() ?? "private" },
                { "channel", socketMessage.Channel.Id.ToString() },
            };

            this.TrackEvent("messageReceived", props);

            // Ignore system and our own messages.
            var  message    = socketMessage as SocketUserMessage;
            bool isOutbound = false;

            // replicate to webhook, if configured
            this.CallOutgoingWebhookAsync(message).Forget();

            if (message == null || (isOutbound = message.Author.Id == this.Client.CurrentUser.Id))
            {
                if (isOutbound)
                {
                    var logMessage = message.Embeds?.Count > 0 ? $"\tSending [embed content] to {message.Channel.Name}" : $"\tSending to {message.Channel.Name}: {message.Content}";
                    this.Logger.Log(LogType.Outgoing, logMessage);
                }

                return;
            }

            // Ignore other bots
            if (message.Author.IsBot)
            {
                return;
            }

            // grab the settings for this server
            var botGuildUser = (message.Channel as SocketGuildChannel)?.Guild.CurrentUser;
            var guildUser    = message.Author as SocketGuildUser;
            var guildId      = (guildUser != null && guildUser.IsWebhook) ? null : guildUser?.Guild.Id;
            var settings     = SettingsConfig.GetSettings(guildId?.ToString());

            // if it's a globally blocked server, ignore it unless it's the owner
            if (message.Author.Id != this.Config.Discord.OwnerId && guildId != null && this.Config.Discord.BlockedServers.Contains(guildId.Value) && !this.Config.OcrAutoIds.Contains(message.Channel.Id))
            {
                return;
            }

            // if the user is blocked based on role, return
            var botlessRoleId = guildUser?.Guild?.Roles?.FirstOrDefault(r => r.Name?.ToLowerInvariant() == "botless")?.Id;

            if ((message.Author as IGuildUser)?.RoleIds.Any(r => botlessRoleId != null && r == botlessRoleId.Value) ?? false)
            {
                return;
            }

            // Bail out with help info if it's a PM
            if (message.Channel is IDMChannel)
            {
                await this.RespondAsync(message, "Info and commands can be found at: https://ub3r-b0t.com [Commands don't work in direct messages]");

                return;
            }

            var botContext = new DiscordBotContext(this.Client, message)
            {
                Reaction     = reactionType,
                ReactionUser = reactionUser,
                BotApi       = this.BotApi,
                AudioManager = this.audioManager,
            };

            foreach (var module in this.modules)
            {
                var typeInfo = module.GetType().GetTypeInfo();
                var permissionChecksPassed = await this.CheckPermissions(botContext, typeInfo);

                if (!permissionChecksPassed)
                {
                    continue;
                }

                var result = await module.Process(botContext);

                if (result == ModuleResult.Stop)
                {
                    return;
                }
            }

            var textChannel = message.Channel as ITextChannel;

            if (botGuildUser != null && !botGuildUser.GetPermissions(textChannel).SendMessages)
            {
                return;
            }

            // If it's a command, match that before anything else.
            await this.PreProcessMessage(botContext.MessageData, settings);

            string command = botContext.MessageData.Command;

            if (message.Attachments.FirstOrDefault() is Attachment attachment)
            {
                imageUrls[botContext.MessageData.Channel] = attachment;
            }

            // if it's a blocked command, bail
            if (settings.IsCommandDisabled(CommandsConfig.Instance, command) && !IsAuthorOwner(message))
            {
                return;
            }

            if (this.discordCommands.TryGetValue(command, out IDiscordCommand discordCommand))
            {
                var typeInfo = discordCommand.GetType().GetTypeInfo();
                var permissionChecksPassed = await this.CheckPermissions(botContext, typeInfo);

                if (permissionChecksPassed)
                {
                    var response = await discordCommand.Process(botContext);

                    if (response?.Attachment != null)
                    {
                        var sentMessage = await message.Channel.SendFileAsync(response.Attachment.Stream, response.Attachment.Name, response.Text);

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                    else if (!string.IsNullOrEmpty(response?.Text) || response?.Embed != null)
                    {
                        var sentMessage = await this.RespondAsync(message, response.Text, response.Embed);

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                }
            }
            else
            {
                IDisposable typingState = null;
                if (CommandsConfig.Instance.Commands.ContainsKey(command))
                {
                    // possible bug with typing state
                    Console.WriteLine($"typing triggered by {command}");
                    typingState = message.Channel.EnterTypingState();
                }

                if (botContext.MessageData.Command == "quote" && reactionUser != null)
                {
                    botContext.MessageData.UserName = reactionUser.Username;
                }

                try
                {
                    BotResponseData responseData = await this.ProcessMessageAsync(botContext.MessageData, settings);

                    if (responseData.Embed != null)
                    {
                        var sentMessage = await this.RespondAsync(message, string.Empty, responseData.Embed.CreateEmbedBuilder());

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                    else
                    {
                        foreach (string response in responseData.Responses)
                        {
                            if (!string.IsNullOrEmpty(response))
                            {
                                // if sending a multi part message, skip the edit optimization.
                                var sentMessage = await this.RespondAsync(message, response, embedResponse : null, bypassEdit : responseData.Responses.Count > 1);

                                this.botResponsesCache.Add(message.Id, sentMessage);
                            }
                        }
                    }
                }
                finally
                {
                    typingState?.Dispose();
                }
            }
        }
Пример #16
0
        /// <inheritdoc />
        protected override async Task <bool> SendNotification(NotificationData notification)
        {
            var guild = this.Client.GetGuild(Convert.ToUInt64(notification.Server)) as IGuild;

            // if the guild wasn't found, it belongs to another shard.
            if (guild == null)
            {
                return(false);
            }

            var channelToUse = this.Client.GetChannel(Convert.ToUInt64(notification.Channel)) as ITextChannel;

            // if the channel doesn't exist or we don't have send permissions, try to get the default channel instead.
            if (!channelToUse?.GetCurrentUserPermissions().SendMessages ?? true)
            {
                channelToUse = await guild.GetDefaultChannelAsync() as ITextChannel;

                // if the default channel couldn't be found or we don't have permissions, then we're SOL.
                // flag as processed.
                if (channelToUse == null || !channelToUse.GetCurrentUserPermissions().SendMessages)
                {
                    return(true);
                }
            }

            var settings = SettingsConfig.GetSettings(notification.Server);

            // adjust the notification text to disable discord link parsing, if configured to do so
            if (settings.DisableLinkParsing)
            {
                notification.Text = Consts.UrlRegex.Replace(notification.Text, new MatchEvaluator((Match urlMatch) =>
                {
                    return($"<{urlMatch.Captures[0]}>");
                }));
            }

            try
            {
                if (notification.Embed != null && settings.HasFlag(notification.Type))
                {
                    // TODO: discord handles twitter embeds nicely; should adjust the notification data accordingly so we don't need this explicit check here
                    if (notification.Type == NotificationType.Twitter)
                    {
                        await channelToUse.SendMessageAsync(notification.Embed.Url);
                    }
                    else
                    {
                        var url = string.IsNullOrEmpty(notification.Embed.Url) ? string.Empty : $"<{notification.Embed.Url}>";
                        await channelToUse.SendMessageAsync(url, false, notification.Embed.CreateEmbedBuilder());
                    }
                }
                else
                {
                    await channelToUse.SendMessageAsync(notification.Text);
                }
            }
            catch (Exception ex)
            {
                // TODO:
                // Add retry support.
                string extraData = null;
                if (notification.Embed != null)
                {
                    extraData = JsonConvert.SerializeObject(notification.Embed);
                }
                this.Logger.Log(LogType.Error, $"Failed to send notification {extraData}: {ex}");
            }

            return(true);
        }
Пример #17
0
        /// <inheritdoc />
        protected override async Task <bool> SendNotification(NotificationData notification)
        {
            // if the guild wasn't found, it belongs to another shard.
            if (!(this.Client.GetGuild(Convert.ToUInt64(notification.Server)) is IGuild guild))
            {
                return(false);
            }

            Log.Information($"Sending {notification.Type} notification to {notification.Channel} on guild {notification.Server}");

            string extraText    = string.Empty;
            var    channelToUse = this.Client.GetChannel(Convert.ToUInt64(notification.Channel)) as ITextChannel;

            // if the channel doesn't exist or we don't have send permissions, try to get the default channel instead.
            if (!channelToUse?.GetCurrentUserPermissions().SendMessages ?? true)
            {
                // add some hint text about the misconfigured channel
                string hint = "fix it in the admin panel";
                if (notification.Type == NotificationType.Reminder)
                {
                    hint = "server owner: `.remove timer ###` and recreate it";
                }

                if (channelToUse == null)
                {
                    extraText = $" [Note: the channel configured is missing; {hint}]";
                }
                else
                {
                    extraText = $" [Note: missing permissions for the channel configured; adjust permissions or {hint}]";
                }

                channelToUse = await guild.GetDefaultChannelAsync();

                // if the default channel couldn't be found or we don't have permissions, then we're SOL.
                // flag as processed.
                if (channelToUse == null || !channelToUse.GetCurrentUserPermissions().SendMessages)
                {
                    return(true);
                }
            }

            var settings = SettingsConfig.GetSettings(notification.Server);

            // adjust the notification text to disable discord link parsing, if configured to do so
            if (settings.DisableLinkParsing && notification.Type != NotificationType.Reminder)
            {
                notification.Text = Consts.UrlRegex.Replace(notification.Text, new MatchEvaluator((Match urlMatch) =>
                {
                    return($"<{urlMatch.Captures[0]}>");
                }));
            }

            try
            {
                var customText      = settings.NotificationText.FirstOrDefault(n => n.Type == notification.Type)?.Text;
                var allowedMentions = notification.AllowMentions ?
                                      new AllowedMentions {
                    MentionRepliedUser = false, AllowedTypes = AllowedMentionTypes.Roles | AllowedMentionTypes.Users | AllowedMentionTypes.Everyone
                } :
                AllowedMentions.None;

                MessageReference messageReference = null;
                if (!string.IsNullOrEmpty(notification.MessageId) && ulong.TryParse(notification.MessageId, out var messageId))
                {
                    messageReference = new MessageReference(messageId, failIfNotExists: false);
                }

                if (notification.Embed != null && settings.HasFlag(notification.Type))
                {
                    // TODO: discord handles twitter embeds nicely; should adjust the notification data accordingly so we don't need this explicit check here
                    if (notification.Type == NotificationType.Twitter)
                    {
                        var messageText = $"{notification.Embed.Title} {notification.Embed.Url} {customText}{extraText}".Trim();
                        await channelToUse.SendMessageAsync(messageText, allowedMentions : allowedMentions, messageReference : messageReference);
                    }
                    else
                    {
                        var messageText = string.IsNullOrEmpty(notification.Embed.Url) ? string.Empty : $"<{notification.Embed.Url}>";
                        messageText += $" {customText}{extraText}".TrimEnd();
                        await channelToUse.SendMessageAsync(messageText, false, notification.Embed.CreateEmbedBuilder().Build(), allowedMentions : allowedMentions, messageReference : messageReference);
                    }
                }
                else
                {
                    var messageText = $"{notification.Text} {customText}{extraText}".TrimEnd();
                    var sentMesage  = await channelToUse.SendMessageAsync(messageText.SubstringUpTo(Discord.DiscordConfig.MaxMessageSize), allowedMentions : allowedMentions, messageReference : messageReference);

                    // update feedback messages to include the message ID
                    if (notification.Type == NotificationType.Feedback && notification.SubType != SubType.Reply)
                    {
                        await sentMesage.ModifyAsync(m => m.Content = $"{sentMesage.Content} (mid: {sentMesage.Id})");
                    }
                }

                var props = new Dictionary <string, string> {
                    { "server", notification.Server },
                    { "channel", notification.Channel },
                    { "notificationType", notification.Type.ToString() },
                };
                this.TrackEvent("notificationSent", props);
            }
            catch (Exception ex)
            {
                // TODO:
                // Add retry support.
                string extraData = null;
                if (notification.Embed != null)
                {
                    extraData = JsonConvert.SerializeObject(notification.Embed);
                }
                Log.Error(ex, $"Failed to send notification {extraData}");
            }

            return(true);
        }
Пример #18
0
        // TODO:
        // this is icky
        // it's getting worse...TODO: read the above todo and fix it
        internal DiscordCommands(DiscordSocketClient client, AudioManager audioManager, BotApi botApi)
        {
            this.client       = client;
            this.audioManager = audioManager;
            this.botApi       = botApi;
            this.Commands     = new Dictionary <string, Func <SocketUserMessage, Task <CommandResponse> > >();

            this.CreateScriptOptions();

            Commands.Add("debug", (message) =>
            {
                var serverId   = (message.Channel as IGuildChannel)?.GuildId.ToString() ?? "n/a";
                var botVersion = Assembly.GetEntryAssembly().GetName().Version.ToString();
                var response   = new CommandResponse {
                    Text = $"```Server ID: {serverId} | Channel ID: {message.Channel.Id} | Your ID: {message.Author.Id} | Shard ID: {client.ShardId} | Version: {botVersion} | Discord.NET Version: {DiscordSocketConfig.Version}```"
                };
                return(Task.FromResult(response));
            });

            Commands.Add("seen", async(message) =>
            {
                if (message.Channel is IGuildChannel guildChannel)
                {
                    var settings = SettingsConfig.GetSettings(guildChannel.GuildId.ToString());
                    if (!settings.SeenEnabled)
                    {
                        return(new CommandResponse {
                            Text = "Seen data is not being tracked for this server.  Enable it in the admin settings panel."
                        });
                    }

                    string[] parts = message.Content.Split(new[] { ' ' }, 2);
                    if (parts.Length != 2)
                    {
                        return(new CommandResponse {
                            Text = "Usage: .seen username"
                        });
                    }

                    var targetUser = (await guildChannel.Guild.GetUsersAsync()).Find(parts[1]).FirstOrDefault();
                    if (targetUser != null)
                    {
                        if (targetUser.Id == client.CurrentUser.Id)
                        {
                            return(new CommandResponse {
                                Text = $"I was last seen...wait...seriously? Ain't no one got time for your shit, {message.Author.Username}."
                            });
                        }

                        if (targetUser.Id == message.Author.Id)
                        {
                            return(new CommandResponse {
                                Text = $"You were last seen now, saying: ... god DAMN it {message.Author.Username}, quit wasting my time"
                            });
                        }

                        string query    = $"seen {targetUser.Id} {targetUser.Username}";
                        var messageData = BotMessageData.Create(message, query, settings);

                        var response = (await this.botApi.IssueRequestAsync(messageData, query)).Responses.FirstOrDefault();

                        if (response != null)
                        {
                            return(new CommandResponse {
                                Text = response
                            });
                        }
                    }

                    return(new CommandResponse {
                        Text = $"I...omg...I have not seen {parts[1]} in this channel :X I AM SOOOOOO SORRY"
                    });
                }

                return(null);
            });

            Commands.Add("remove", async(message) =>
            {
                if (message.Channel is IGuildChannel guildChannel)
                {
                    if (message.Author.Id != guildChannel.Guild.OwnerId)
                    {
                        return(new CommandResponse {
                            Text = "Restricted to server owner."
                        });
                    }

                    var settings = SettingsConfig.GetSettings(guildChannel.GuildId.ToString());

                    string[] parts = message.Content.Split(new[] { ' ' }, 3);

                    if (parts.Length != 3)
                    {
                        return(new CommandResponse {
                            Text = "Usage: .remove type #; valid types are timer and wc"
                        });
                    }

                    var type = parts[1].ToLowerInvariant();
                    var id   = parts[2];

                    string query    = $"remove {CommandsConfig.Instance.HelperKey} {type} {id}";
                    var messageData = BotMessageData.Create(message, query, settings);
                    var response    = (await this.botApi.IssueRequestAsync(messageData, query)).Responses.FirstOrDefault();

                    if (response != null)
                    {
                        return(new CommandResponse {
                            Text = response
                        });
                    }
                }

                return(null);
            });

            Commands.Add("status", async(message) =>
            {
                var serversStatus = await Utilities.GetApiResponseAsync <HeartbeatData[]>(BotConfig.Instance.HeartbeatEndpoint);

                var dataSb = new StringBuilder();
                dataSb.Append("```cs\n" +
                              "type       shard   server count      users   voice count\n");

                int serverTotal = 0;
                int userTotal   = 0;
                int voiceTotal  = 0;
                foreach (HeartbeatData heartbeat in serversStatus)
                {
                    serverTotal += heartbeat.ServerCount;
                    userTotal   += heartbeat.UserCount;
                    voiceTotal  += heartbeat.VoiceChannelCount;

                    var botType = heartbeat.BotType.PadRight(11);
                    var shard   = heartbeat.Shard.ToString().PadLeft(4);
                    var servers = heartbeat.ServerCount.ToString().PadLeft(13);
                    var users   = heartbeat.UserCount.ToString().PadLeft(10);
                    var voice   = heartbeat.VoiceChannelCount.ToString().PadLeft(13);

                    dataSb.Append($"{botType} {shard}  {servers} {users} {voice}\n");
                }

                // add up totals
                dataSb.Append($"-------\n");
                dataSb.Append($"Total:            {serverTotal.ToString().PadLeft(13)} {userTotal.ToString().PadLeft(10)} {voiceTotal.ToString().PadLeft(13)}\n");

                dataSb.Append("```");

                return(new CommandResponse {
                    Text = dataSb.ToString()
                });
            });

            Commands.Add("voice", (message) =>
            {
                var channel = (message.Author as IGuildUser)?.VoiceChannel;
                if (channel == null)
                {
                    return(Task.FromResult(new CommandResponse {
                        Text = "Join a voice channel first"
                    }));
                }

                Task.Run(async() =>
                {
                    try
                    {
                        await audioManager.JoinAudioAsync(channel);
                    }
                    catch (Exception ex)
                    {
                        // TODO: proper logging
                        Console.WriteLine(ex);
                    }
                }).Forget();

                return(Task.FromResult((CommandResponse)null));
            });

            Commands.Add("dvoice", (message) =>
            {
                if (message.Channel is IGuildChannel channel)
                {
                    Task.Run(async() =>
                    {
                        try
                        {
                            await audioManager.LeaveAudioAsync(channel);
                        }
                        catch (Exception ex)
                        {
                            // TODO: proper logging
                            Console.WriteLine(ex);
                        }
                    }).Forget();
                }

                return(Task.FromResult((CommandResponse)null));
            });

            Commands.Add("devoice", (message) =>
            {
                if (message.Channel is IGuildChannel channel)
                {
                    Task.Run(async() =>
                    {
                        try
                        {
                            await audioManager.LeaveAudioAsync(channel);
                        }
                        catch (Exception ex)
                        {
                            // TODO: proper logging
                            Console.WriteLine(ex);
                        }
                    }).Forget();
                }

                return(Task.FromResult((CommandResponse)null));
            });

            Commands.Add("clear", async(message) =>
            {
                if (message.Channel is IDMChannel)
                {
                    return(null);
                }

                var guildUser = message.Author as IGuildUser;
                if (!guildUser.GuildPermissions.ManageMessages)
                {
                    return(new CommandResponse {
                        Text = "you don't have permissions to clear messages, fartface"
                    });
                }

                var guildChannel = message.Channel as IGuildChannel;

                string[] parts = message.Content.Split(new[] { ' ' }, 3);
                if (parts.Length != 2 && parts.Length != 3)
                {
                    return(new CommandResponse {
                        Text = "Usage: .clear #; Usage for user specific messages: .clear # username"
                    });
                }

                IUser deletionUser = null;

                if (parts.Length == 3)
                {
                    if (ulong.TryParse(parts[2], out ulong userId))
                    {
                        deletionUser = await message.Channel.GetUserAsync(userId);
                    }
                    else
                    {
                        deletionUser = (await guildUser.Guild.GetUsersAsync().ConfigureAwait(false)).Find(parts[2]).FirstOrDefault();
                    }

                    if (deletionUser == null)
                    {
                        return(new CommandResponse {
                            Text = "Couldn't find the specified user. Try their ID if nick matching is struggling"
                        });
                    }
                }

                var botGuildUser = await guildChannel.GetUserAsync(client.CurrentUser.Id);
                bool botOnly     = deletionUser == botGuildUser;

                if (!botOnly && !botGuildUser.GetPermissions(guildChannel).ManageMessages)
                {
                    return(new CommandResponse {
                        Text = "yeah I don't have the permissions to delete messages, buttwad."
                    });
                }

                if (int.TryParse(parts[1], out int count))
                {
                    var textChannel = message.Channel as ITextChannel;
                    // +1 for the current .clear message
                    if (deletionUser == null)
                    {
                        count = Math.Min(99, count) + 1;
                    }
                    else
                    {
                        count = Math.Min(100, count);
                    }

                    // download messages until we've hit the limit
                    var msgsToDelete      = new List <IMessage>();
                    var msgsToDeleteCount = 0;
                    ulong?lastMsgId       = null;
                    var i = 0;
                    while (msgsToDeleteCount < count)
                    {
                        i++;
                        IEnumerable <IMessage> downloadedMsgs;
                        try
                        {
                            if (!lastMsgId.HasValue)
                            {
                                downloadedMsgs = await textChannel.GetMessagesAsync(count).Flatten();
                            }
                            else
                            {
                                downloadedMsgs = await textChannel.GetMessagesAsync(lastMsgId.Value, Direction.Before, count).Flatten();
                            }
                        }
                        catch (Exception ex)
                        {
                            downloadedMsgs = new IMessage[0];
                            Console.WriteLine(ex);
                        }

                        if (downloadedMsgs.Count() > 0)
                        {
                            lastMsgId = downloadedMsgs.Last().Id;

                            var msgs           = downloadedMsgs.Where(m => (deletionUser == null || m.Author?.Id == deletionUser.Id)).Take(count - msgsToDeleteCount);
                            msgsToDeleteCount += msgs.Count();
                            msgsToDelete.AddRange(msgs);
                        }
                        else
                        {
                            break;
                        }

                        if (i >= 5)
                        {
                            break;
                        }
                    }

                    var settings = SettingsConfig.GetSettings(guildUser.GuildId.ToString());
                    if (settings.HasFlag(ModOptions.Mod_LogDelete) && this.client.GetChannel(settings.Mod_LogId) is ITextChannel modLogChannel && modLogChannel.GetCurrentUserPermissions().SendMessages)
                    {
                        modLogChannel.SendMessageAsync($"{guildUser.Username}#{guildUser.Discriminator} cleared {msgsToDeleteCount} messages from {textChannel.Mention}").Forget();
                    }

                    try
                    {
                        await(message.Channel as ITextChannel).DeleteMessagesAsync(msgsToDelete);
                    }
                    catch (ArgumentOutOfRangeException)
                    {
                        return(new CommandResponse {
                            Text = "Bots cannot delete messages older than 2 weeks."
                        });
                    }

                    return(null);
                }
                else
                {
                    return(new CommandResponse {
                        Text = "Usage: .clear #"
                    });
                }
            });

            Commands.Add("jpeg", async(message) =>
            {
                var messageParts = message.Content.Split(new[] { ' ' }, 2);
                var fileName     = "moar.jpeg";
                var url          = string.Empty;
                if (messageParts.Length == 2 && Uri.IsWellFormedUriString(messageParts[1], UriKind.Absolute))
                {
                    url = messageParts[1];
                }
                else
                {
                    Attachment img = message.Attachments.FirstOrDefault();
                    if (img != null || DiscordBot.imageUrls.TryGetValue(message.Channel.Id.ToString(), out img))
                    {
                        url      = img.Url;
                        fileName = img.Filename;
                    }
                }

                if (!string.IsNullOrEmpty(url))
                {
                    using (var httpClient = new HttpClient())
                    {
                        var response = await httpClient.GetAsync(CommandsConfig.Instance.JpegEndpoint.AppendQueryParam("url", url));
                        var stream   = await response.Content.ReadAsStreamAsync();

                        return(new CommandResponse
                        {
                            Attachment = new FileResponse
                            {
                                Name = fileName,
                                Stream = stream,
                            }
                        });
                    }
                }

                return(null);
            });

            Commands.Add("userinfo", async(message) =>
            {
                SocketUser targetUser = message.MentionedUsers?.FirstOrDefault();

                if (targetUser == null)
                {
                    var messageParts = message.Content.Split(new[] { ' ' }, 2);

                    if (messageParts.Length == 2)
                    {
                        if (message.Channel is IGuildChannel guildChannel)
                        {
                            targetUser = (await guildChannel.Guild.GetUsersAsync()).Find(messageParts[1]).FirstOrDefault() as SocketUser;
                        }

                        if (targetUser == null)
                        {
                            return(new CommandResponse {
                                Text = "User not found. Try a direct mention."
                            });
                        }
                    }
                    else
                    {
                        targetUser = message.Author;
                    }
                }

                var guildUser = targetUser as IGuildUser;

                if (guildUser == null)
                {
                    return(null);
                }

                var userInfo = new
                {
                    Title        = $"UserInfo for {targetUser.Username}#{targetUser.Discriminator}",
                    AvatarUrl    = targetUser.GetAvatarUrl(),
                    NicknameInfo = !string.IsNullOrEmpty(guildUser.Nickname) ? $" aka {guildUser.Nickname}" : "",
                    Footnote     = CommandsConfig.Instance.UserInfoSnippets.Random(),
                    Created      = $"{targetUser.GetCreatedDate().ToString("dd MMM yyyy")} {targetUser.GetCreatedDate().ToString("hh:mm:ss tt")} UTC",
                    Joined       = guildUser.JoinedAt.HasValue ? $"{guildUser.JoinedAt.Value.ToString("dd MMM yyyy")} {guildUser.JoinedAt.Value.ToString("hh:mm:ss tt")} UTC" : "[data temporarily missing]",
                    Id           = targetUser.Id.ToString(),
                };

                EmbedBuilder embedBuilder = null;
                string text = string.Empty;

                var settings = SettingsConfig.GetSettings(guildUser.GuildId.ToString());

                if ((message.Channel as ITextChannel).GetCurrentUserPermissions().EmbedLinks&& settings.PreferEmbeds)
                {
                    embedBuilder = new EmbedBuilder
                    {
                        Title        = userInfo.Title,
                        ThumbnailUrl = userInfo.AvatarUrl,
                    };

                    if (!string.IsNullOrEmpty(userInfo.NicknameInfo))
                    {
                        embedBuilder.Description = userInfo.NicknameInfo;
                    }

                    embedBuilder.Footer = new EmbedFooterBuilder
                    {
                        Text = userInfo.Footnote,
                    };

                    embedBuilder.AddField((field) =>
                    {
                        field.IsInline = true;
                        field.Name     = "created";
                        field.Value    = userInfo.Created;
                    });

                    if (guildUser.JoinedAt.HasValue)
                    {
                        embedBuilder.AddField((field) =>
                        {
                            field.IsInline = true;
                            field.Name     = "joined";
                            field.Value    = userInfo.Joined;
                        });
                    }

                    var roles = new List <string>();
                    foreach (ulong roleId in guildUser.RoleIds)
                    {
                        roles.Add(guildUser.Guild.Roles.First(g => g.Id == roleId).Name.TrimStart('@'));
                    }

                    embedBuilder.AddField((field) =>
                    {
                        field.IsInline = false;
                        field.Name     = "roles";
                        field.Value    = string.Join(", ", roles);
                    });

                    embedBuilder.AddField((field) =>
                    {
                        field.IsInline = true;
                        field.Name     = "Id";
                        field.Value    = userInfo.Id;
                    });
                }
                else
                {
                    text = $"{userInfo.Title}{userInfo.NicknameInfo}: ID: {userInfo.Id} | Created: {userInfo.Created} | Joined: {userInfo.Joined} | word on the street: {userInfo.Footnote}";
                }

                return(new CommandResponse {
                    Text = text, Embed = embedBuilder
                });
            });

            Commands.Add("serverinfo", async(message) =>
            {
                EmbedBuilder embedBuilder = null;
                string text = string.Empty;

                if (message.Channel is IGuildChannel guildChannel && message.Channel is ITextChannel textChannel)
                {
                    var guild      = guildChannel.Guild;
                    var emojiCount = guild.Emotes.Count();
                    var emojiText  = "no custom emojis? I am ASHAMED to be here";
                    if (emojiCount > 50)
                    {
                        emojiText = $"...{emojiCount} emojis? hackers";
                    }
                    else if (emojiCount == 50)
                    {
                        emojiText = "wow 50 custom emojis! that's the max";
                    }
                    else if (emojiCount >= 40)
                    {
                        emojiText = $"{emojiCount} custom emojis in here. impressive...most impressive...";
                    }
                    else if (emojiCount > 25)
                    {
                        emojiText = $"the custom emoji force is strong with this guild. {emojiCount} is over halfway to the max.";
                    }
                    else if (emojiCount > 10)
                    {
                        emojiText = $"{emojiCount} custom emoji is...passable";
                    }
                    else if (emojiCount > 0)
                    {
                        emojiText = $"really, only {emojiCount} custom emoji? tsk tsk.";
                    }

                    await(message.Channel as SocketGuildChannel).Guild.DownloadUsersAsync();
                    var serverInfo = new
                    {
                        Title     = $"Server Info for {guild.Name}",
                        UserCount = (await guild.GetUsersAsync()).Count(),
                        Owner     = (await guild.GetOwnerAsync()).Username,
                        Created   = $"{guild.CreatedAt.ToString("dd MMM yyyy")} {guild.CreatedAt.ToString("hh:mm:ss tt")} UTC",
                        EmojiText = emojiText,
                    };

                    var settings = SettingsConfig.GetSettings(guildChannel.GuildId.ToString());

                    if (textChannel.GetCurrentUserPermissions().EmbedLinks&& settings.PreferEmbeds)
                    {
                        embedBuilder = new EmbedBuilder
                        {
                            Title        = serverInfo.Title,
                            ThumbnailUrl = guildChannel.Guild.IconUrl,
                        };

                        embedBuilder.AddField((field) =>
                        {
                            field.IsInline = true;
                            field.Name     = "Users";
                            field.Value    = serverInfo.UserCount;
                        });

                        embedBuilder.AddField((field) =>
                        {
                            field.IsInline = true;
                            field.Name     = "Owner";
                            field.Value    = serverInfo.Owner;
                        });

                        embedBuilder.AddField((field) =>
                        {
                            field.IsInline = true;
                            field.Name     = "Id";
                            field.Value    = guild.Id;
                        });

                        embedBuilder.AddField((field) =>
                        {
                            field.IsInline = true;
                            field.Name     = "created";
                            field.Value    = serverInfo.Created;
                        });

                        embedBuilder.Footer = new EmbedFooterBuilder
                        {
                            Text = serverInfo.EmojiText,
                        };

                        if (!string.IsNullOrEmpty(guild.SplashUrl))
                        {
                            embedBuilder.Footer.IconUrl = guild.SplashUrl;
                        }
                    }
                    else
                    {
                        text = $"{serverInfo.Title}: Users: {serverInfo.UserCount} | Owner: {serverInfo.Owner} | Id: {guild.Id} | Created: {serverInfo.Created} | {serverInfo.EmojiText}";
                    }
                }
Пример #19
0
        /// <inheritdoc />
        protected override async Task <bool> SendNotification(NotificationData notification)
        {
            // if the guild wasn't found, it belongs to another shard.
            if (!(this.Client.GetGuild(Convert.ToUInt64(notification.Server)) is IGuild guild))
            {
                return(false);
            }

            var channelToUse = this.Client.GetChannel(Convert.ToUInt64(notification.Channel)) as ITextChannel;

            // if the channel doesn't exist or we don't have send permissions, try to get the default channel instead.
            if (!channelToUse?.GetCurrentUserPermissions().SendMessages ?? true)
            {
                channelToUse = await guild.GetDefaultChannelAsync() as ITextChannel;

                // if the default channel couldn't be found or we don't have permissions, then we're SOL.
                // flag as processed.
                if (channelToUse == null || !channelToUse.GetCurrentUserPermissions().SendMessages)
                {
                    return(true);
                }
            }

            var settings = SettingsConfig.GetSettings(notification.Server);

            // adjust the notification text to disable discord link parsing, if configured to do so
            if (settings.DisableLinkParsing && notification.Type != NotificationType.Reminder)
            {
                notification.Text = Consts.UrlRegex.Replace(notification.Text, new MatchEvaluator((Match urlMatch) =>
                {
                    return($"<{urlMatch.Captures[0]}>");
                }));
            }

            try
            {
                if (notification.Embed != null && settings.HasFlag(notification.Type))
                {
                    // TODO: discord handles twitter embeds nicely; should adjust the notification data accordingly so we don't need this explicit check here
                    if (notification.Type == NotificationType.Twitter)
                    {
                        await channelToUse.SendMessageAsync(notification.Embed.Url);
                    }
                    else
                    {
                        var customText  = settings.NotificationText.FirstOrDefault(n => n.Type == notification.Type)?.Text;
                        var messageText = string.IsNullOrEmpty(notification.Embed.Url) ? string.Empty : $"<{notification.Embed.Url}>";
                        if (!string.IsNullOrEmpty(customText))
                        {
                            messageText += $" {customText}";
                        }

                        await channelToUse.SendMessageAsync(messageText, false, notification.Embed.CreateEmbedBuilder().Build());
                    }
                }
                else
                {
                    await channelToUse.SendMessageAsync(notification.Text.SubstringUpTo(Discord.DiscordConfig.MaxMessageSize));
                }

                var props = new Dictionary <string, string> {
                    { "server", notification.Server },
                    { "channel", notification.Channel },
                    { "notificationType", notification.Type.ToString() },
                };
                this.TrackEvent("notificationSent", props);
            }
            catch (Exception ex)
            {
                // TODO:
                // Add retry support.
                string extraData = null;
                if (notification.Embed != null)
                {
                    extraData = JsonConvert.SerializeObject(notification.Embed);
                }
                Log.Error(ex, $"Failed to send notification {extraData}");
            }

            return(true);
        }
Пример #20
0
        /// <summary>
        /// Handles responses for messages.
        /// TODO: This method is way too huge.
        /// </summary>
        private async Task HandleMessageReceivedAsync(SocketMessage socketMessage, string reactionType = null, IUser reactionUser = null)
        {
            // Ignore system and our own messages.
            var  message    = socketMessage as SocketUserMessage;
            bool isOutbound = false;

            // replicate to webhook, if configured
            this.CallOutgoingWebhookAsync(message).Forget();

            if (message == null || (isOutbound = message.Author.Id == this.Client.CurrentUser.Id))
            {
                if (isOutbound)
                {
                    if (message.Embeds?.Count > 0)
                    {
                        this.Logger.Log(LogType.Outgoing, $"\tSending [embed content] to {message.Channel.Name}");
                    }
                    else
                    {
                        this.Logger.Log(LogType.Outgoing, $"\tSending to {message.Channel.Name}: {message.Content}");
                    }
                }

                return;
            }

            // Ignore other bots
            if (message.Author.IsBot)
            {
                return;
            }

            // grab the settings for this server
            var botGuildUser = (message.Channel as SocketGuildChannel)?.Guild.CurrentUser;
            var guildUser    = message.Author as IGuildUser;
            var guildId      = (guildUser != null && guildUser.IsWebhook) ? null : guildUser?.GuildId;
            var settings     = SettingsConfig.GetSettings(guildId?.ToString());

            // if it's a globally blocked server, ignore it unless it's the owner
            if (message.Author.Id != this.Config.Discord.OwnerId && guildId != null && this.Config.Discord.BlockedServers.Contains(guildId.Value))
            {
                return;
            }

            // if the user is blocked based on role, return
            var botlessRoleId = guildUser?.Guild?.Roles?.FirstOrDefault(r => r.Name?.ToLowerInvariant() == "botless")?.Id;

            if ((message.Author as IGuildUser)?.RoleIds.Any(r => botlessRoleId != null && r == botlessRoleId.Value) ?? false)
            {
                return;
            }

            // Bail out with help info if it's a PM
            if (message.Channel is IDMChannel && (message.Content.Contains("help") || message.Content.Contains("info") || message.Content.Contains("commands")))
            {
                await this.RespondAsync(message, "Info and commands can be found at: https://ub3r-b0t.com");

                return;
            }

            // check for word censors
            if (botGuildUser?.GuildPermissions.ManageMessages ?? false)
            {
                if (settings.TriggersCensor(message.Content, out string offendingWord))
                {
                    offendingWord = offendingWord != null ? $"`{offendingWord}`" : "*FANCY lanuage filters*";
                    await message.DeleteAsync();

                    var dmChannel = await message.Author.GetOrCreateDMChannelAsync();

                    await dmChannel.SendMessageAsync($"hi uh sorry but your most recent message was tripped up by {offendingWord} and thusly was deleted. complain to management, i'm just the enforcer");

                    return;
                }
            }

            var textChannel = message.Channel as ITextChannel;

            if (botGuildUser != null && !botGuildUser.GetPermissions(textChannel).SendMessages)
            {
                return;
            }

            // special case FAQ channel
            if (message.Channel.Id == this.Config.FaqChannel && message.Content.EndsWith("?") && this.Config.FaqEndpoint != null)
            {
                string content = message.Content.Replace("<@85614143951892480>", "ub3r-b0t");
                var    result  = await this.Config.FaqEndpoint.ToString().WithHeader("Ocp-Apim-Subscription-Key", this.Config.FaqKey).PostJsonAsync(new { question = content });

                if (result.IsSuccessStatusCode)
                {
                    var response = await result.Content.ReadAsStringAsync();

                    var qnaData = JsonConvert.DeserializeObject <QnAMakerData>(response);
                    var score   = Math.Floor(qnaData.Score);
                    var answer  = WebUtility.HtmlDecode(qnaData.Answer);
                    await message.Channel.SendMessageAsync($"{answer} ({score}% match)");
                }
                else
                {
                    await message.Channel.SendMessageAsync("An error occurred while fetching data");
                }

                return;
            }

            string messageContent = message.Content;

            // OCR for fun if requested (patrons only)
            // TODO: need to drive this via config
            // TODO: Need to generalize even further due to more reaction types
            // TODO: oh my god stop writing TODOs and just make the code less awful
            if (!string.IsNullOrEmpty(reactionType))
            {
                string newMessageContent = string.Empty;

                if (reactionType == "💬" || reactionType == "🗨️")
                {
                    newMessageContent = $".quote add \"{messageContent}\" - userid:{message.Author.Id} {message.Author.Username}";
                    await message.AddReactionAsync(new Emoji("💬"));
                }
                else if (string.IsNullOrEmpty(message.Content) && message.Attachments?.FirstOrDefault()?.Url is string attachmentUrl)
                {
                    if (reactionType == "👁")
                    {
                        var result = await this.Config.OcrEndpoint.ToString()
                                     .WithHeader("Ocp-Apim-Subscription-Key", this.Config.VisionKey)
                                     .PostJsonAsync(new { url = attachmentUrl });

                        if (result.IsSuccessStatusCode)
                        {
                            var response = await result.Content.ReadAsStringAsync();

                            var ocrData = JsonConvert.DeserializeObject <OcrData>(response);
                            if (!string.IsNullOrEmpty(ocrData.GetText()))
                            {
                                newMessageContent = ocrData.GetText();
                            }
                        }
                    }
                    else if (reactionType == "🖼")
                    {
                        var analyzeResult = await this.Config.AnalyzeEndpoint.ToString()
                                            .WithHeader("Ocp-Apim-Subscription-Key", this.Config.VisionKey)
                                            .PostJsonAsync(new { url = attachmentUrl });

                        if (analyzeResult.IsSuccessStatusCode)
                        {
                            var response = await analyzeResult.Content.ReadAsStringAsync();

                            var analyzeData = JsonConvert.DeserializeObject <AnalyzeImageData>(response);
                            if (analyzeData.Description.Tags.Contains("ball"))
                            {
                                newMessageContent = ".8ball foo";
                            }
                            else if (analyzeData.Description.Tags.Contains("outdoor"))
                            {
                                newMessageContent = ".fw";
                            }
                        }
                    }
                }

                messageContent = newMessageContent ?? messageContent;
            }

            // If it's a command, match that before anything else.
            string query         = string.Empty;
            bool   hasBotMention = message.MentionedUsers.Any(u => u.Id == this.Client.CurrentUser.Id);

            int argPos = 0;

            if (message.HasMentionPrefix(this.Client.CurrentUser, ref argPos))
            {
                query = messageContent.Substring(argPos);
            }
            else if (messageContent.StartsWith(settings.Prefix))
            {
                query = messageContent.Substring(settings.Prefix.Length);
            }

            var messageData = BotMessageData.Create(message, query, settings);

            messageData.Content = messageContent;
            await this.PreProcessMessage(messageData, settings);

            string command = messageData.Command;

            if (message.Attachments.FirstOrDefault() is Attachment attachment)
            {
                imageUrls[messageData.Channel] = attachment;
            }

            // if it's a blocked command, bail
            if (settings.IsCommandDisabled(CommandsConfig.Instance, command) && !IsAuthorOwner(message))
            {
                return;
            }

            // Check discord specific commands prior to general ones.
            if (discordCommands.Commands.ContainsKey(command))
            {
                var response = await discordCommands.Commands[command].Invoke(message).ConfigureAwait(false);
                if (response != null)
                {
                    if (response.Attachment != null)
                    {
                        var sentMessage = await message.Channel.SendFileAsync(response.Attachment.Stream, response.Attachment.Name, response.Text);

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                    else if (!string.IsNullOrEmpty(response.Text) || response.Embed != null)
                    {
                        var sentMessage = await this.RespondAsync(message, response.Text, response.Embed);

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                }
            }
            else
            {
                IDisposable typingState = null;
                if (CommandsConfig.Instance.Commands.ContainsKey(command))
                {
                    // possible bug with typing state
                    Console.WriteLine($"typing triggered by {command}");
                    typingState = message.Channel.EnterTypingState();
                }

                if (messageData.Command == "quote" && reactionUser != null)
                {
                    messageData.UserName = reactionUser.Username;
                }

                try
                {
                    BotResponseData responseData = await this.ProcessMessageAsync(messageData, settings);

                    if (responseData.Embed != null)
                    {
                        var sentMessage = await this.RespondAsync(message, string.Empty, responseData.Embed.CreateEmbedBuilder());

                        this.botResponsesCache.Add(message.Id, sentMessage);
                    }
                    else
                    {
                        foreach (string response in responseData.Responses)
                        {
                            if (!string.IsNullOrEmpty(response))
                            {
                                // if sending a multi part message, skip the edit optimization.
                                var sentMessage = await this.RespondAsync(message, response, embedResponse : null, bypassEdit : responseData.Responses.Count > 1);

                                this.botResponsesCache.Add(message.Id, sentMessage);
                            }
                        }
                    }
                }
                finally
                {
                    typingState?.Dispose();
                }
            }
        }