/// <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(); } } }
/// <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(); } } }