public async Task HandleMessage(MessageEvent messageEvent) { try { if (messageEvent.IsSystemMessage || messageEvent.CreatedByWebhook != null || messageEvent.CreatedBy == this.client.Me.Id || messageEvent.IsReply) { return; } if (this.Throttler.IsThrottled(messageEvent.CreatedBy.ToString(), ThrottleType.User)) { Log.Debug($"messaging throttle from user: {messageEvent.CreatedBy} on chan {messageEvent.ChannelId} server {messageEvent.ServerId}"); return; } if (this.Throttler.IsThrottled(messageEvent.ServerId.ToString(), ThrottleType.Guild)) { Log.Debug($"messaging throttle from guild: {messageEvent.CreatedBy} on chan {messageEvent.ChannelId} server {messageEvent.ServerId}"); return; } var settings = new Settings { FunResponsesEnabled = true, AutoTitlesEnabled = true, PreferEmbeds = true, }; var messageData = BotMessageData.Create(messageEvent.Message, settings); await this.PreProcessMessage(messageData, settings); BotResponseData responseData = await this.ProcessMessageAsync(messageData, settings); if (responseData.Embed != null) { await this.RespondAsync(messageEvent.Message, string.Empty, bypassEdit : true, responseData.Embed.CreateGuildedEmbed()); } else { foreach (string response in responseData.Responses) { await this.RespondAsync(messageEvent.Message, response, bypassEdit : responseData.Responses.Count > 1); } } } catch (Exception ex) { Log.Warning(ex, $"Error in HandleMessage"); this.AppInsights?.TrackException(ex); } }
protected async Task <BotResponseData> ProcessMessageAsync(BotMessageData messageData, Settings settings) { var responses = new List <string>(); var responseData = new BotResponseData { Responses = responses }; if (this.BotApi != null) { // if an explicit command is being used, it wins out over any implicitly parsed command string query = messageData.Query; string command = messageData.Command; string[] contentParts = messageData.Content.Split(new[] { ' ' }); if (string.IsNullOrEmpty(command)) { if (messageData.Content.IContains("remind ")) { // check for reminders Match timerAtMatch = Consts.TimerOnRegex.Match(messageData.Content); Match timerAt2Match = Consts.TimerOn2Regex.Match(messageData.Content); if (timerAtMatch.Success && Utilities.TryParseAbsoluteReminder(timerAtMatch, messageData, out query) || timerAt2Match.Success && Utilities.TryParseAbsoluteReminder(timerAt2Match, messageData, out query)) { command = "timer"; } else // try relative timers if absolute had no match { Match timerMatch = Consts.TimerRegex.Match(messageData.Content); Match timer2Match = Consts.Timer2Regex.Match(messageData.Content); if (timerMatch.Success || timer2Match.Success) { Match matchToUse = timerMatch.Success && !timerMatch.Groups["prep"].Value.All(char.IsDigit) ? timerMatch : timer2Match; if (Utilities.TryParseReminder(matchToUse, messageData, out query)) { command = "timer"; } } } } if (string.IsNullOrEmpty(command)) { if (settings.AutoTitlesEnabled && (CommandsConfig.Instance.AutoTitleMatches.Any(t => messageData.Content.Contains(t)) || Consts.RedditRegex.IsMatch(messageData.Content))) { Match match = Consts.HttpRegex.Match(messageData.Content); string matchValue = !string.IsNullOrEmpty(match.Value) ? match.Value : Consts.RedditRegex.Match(messageData.Content).Value; if (!string.IsNullOrEmpty(matchValue)) { command = "title"; query = $"{command} {matchValue}"; } } else if ((settings.FunResponsesEnabled || IsAuthorOwner(messageData)) && contentParts.Length > 1 && contentParts[1] == "face") { command = "face"; query = $"{command} {contentParts[0]}"; } } } // Ignore if the command is disabled on this server if (settings.IsCommandDisabled(CommandsConfig.Instance, command) && !IsAuthorOwner(messageData)) { return(responseData); } if (CommandsConfig.Instance.Commands.ContainsKey(command)) { if (!messageData.RateLimitChecked) { // make sure we're not rate limited var commandKey = command + messageData.Server; if (this.Throttler.IsThrottled(commandKey, ThrottleType.Command)) { return(responseData); } this.Throttler.Increment(commandKey, ThrottleType.Command); // if we're now throttled after this increment, return a "rate limited" message if (this.Throttler.IsThrottled(commandKey, ThrottleType.Command)) { responses.Add("rate limited try later"); return(responseData); } // increment the user/guild throttlers as well this.Throttler.Increment(messageData.UserId, ThrottleType.User); this.Throttler.Increment(messageData.Server, ThrottleType.Guild); messageData.RateLimitChecked = true; } var props = new Dictionary <string, string> { { "command", command.ToLowerInvariant() }, { "server", messageData.Server }, { "channel", messageData.Channel } }; this.TrackEvent("commandProcessed", props); // extra processing on ".title" command if ((command == "title" || command == "t") && messageData.Content.EndsWith(command) && urls.ContainsKey(messageData.Channel)) { query += $" {this.urls[messageData.Channel]}"; } using (DogStatsd.StartTimer("commandDuration", tags: new[] { $"shard:{this.Shard}", $"command:{command.ToLowerInvariant()}", $"{this.BotType}" })) { messageData.Content = $"{settings.Prefix}{query}"; responseData = await this.BotApi.IssueRequestAsync(messageData); } } } if (responseData.Responses.Count == 0 && responseData.Embed == null) { string response = null; if (messageData.MentionsBot(this.Config.Name, Convert.ToUInt64(this.UserId))) { var responseValue = PhrasesConfig.Instance.PartialMentionPhrases.FirstOrDefault(kvp => messageData.Content.IContains(kvp.Key)).Value; if (!string.IsNullOrEmpty(responseValue)) { response = PhrasesConfig.Instance.Responses[responseValue].Random(); } } if (response == null && PhrasesConfig.Instance.ExactPhrases.ContainsKey(messageData.Content) && (settings.FunResponsesEnabled && new Random().Next(1, 100) <= settings.FunResponseChance || IsAuthorOwner(messageData))) { response = PhrasesConfig.Instance.Responses[PhrasesConfig.Instance.ExactPhrases[messageData.Content]].Random(); } if (response == null) { response = settings.CustomCommands.FirstOrDefault(c => c.IsExactMatch && c.Command == messageData.Content || !c.IsExactMatch && messageData.Content.IContains(c.Command))?.Response; } if (response != null) { response = response.Replace("%from%", messageData.UserName); string[] resps = response.Split(new char[] { '|' }); responseData.Responses.AddRange(resps); } } return(responseData); }
/// <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(); } } }
public async Task <BotResponseData> IssueRequestAsync(BotMessageData messageData, string query) { var responses = new List <string>(); var responseData = new BotResponseData(); string requestUrl = string.Format("{0}?apikey={1}&nick={2}&host={3}&server={4}&channel={5}&bottype={6}&userId={7}&format={8}&query={9}", this.apiEndpoint, this.apiKey, WebUtility.UrlEncode(messageData.UserName), WebUtility.UrlEncode(messageData.UserHost ?? messageData.UserId), messageData.Server, WebUtility.UrlEncode(messageData.Channel), this.botType.ToString().ToLowerInvariant(), messageData.UserId, messageData.Format, WebUtility.UrlEncode(query)); var botResponse = await Utilities.GetApiResponseAsync <BotApiResponse>(new Uri(requestUrl)); if (botResponse != null) { if (botResponse.Embed != null) { responseData.Embed = botResponse.Embed; } else { // TODO: fix API stupidity and just return the array var botResponses = botResponse.Msgs.Length > 0 ? botResponse.Msgs : new string[] { botResponse.Msg }; if (botResponses[0] != null) { char a = (char)1; char b = (char)2; char c = (char)3; foreach (var response in botResponses) { if (response != null) { responses.Add(response.Replace("%a", a.ToString()).Replace("%b", b.ToString()).Replace("%c", c.ToString())); } } if (this.botType == BotType.Discord) { string response = string.Join("\n", responses); // Extra processing for figlet/cowsay on Discord if (query.StartsWith("cowsay", StringComparison.OrdinalIgnoreCase) || query.StartsWith("figlet", StringComparison.OrdinalIgnoreCase)) { // use a non printable character to force preceeding whitespace to display correctly response = "```" + (char)1 + response + "```"; } responses = new List <string> { response }; } } } responseData.Responses = responses; } return(responseData); }
protected async Task <BotResponseData> ProcessMessageAsync(BotMessageData messageData, Settings settings) { var responses = new List <string>(); var responseData = new BotResponseData { Responses = responses }; if (this.BotApi != null) { // if an explicit command is being used, it wins out over any implicitly parsed command string query = messageData.Query; string command = messageData.Command; string[] contentParts = messageData.Content.Split(new[] { ' ' }); if (string.IsNullOrEmpty(command)) { if (messageData.Content.ToLowerInvariant().Contains("remind ")) { // check for reminders Match timerAtMatch = Consts.TimerOnRegex.Match(messageData.Content); Match timerAt2Match = Consts.TimerOn2Regex.Match(messageData.Content); if (timerAtMatch.Success && Utilities.TryParseAbsoluteReminder(timerAtMatch, messageData, out query) || timerAt2Match.Success && Utilities.TryParseAbsoluteReminder(timerAt2Match, messageData, out query)) { command = "timer"; } else // try relative timers if absolute had no match { Match timerMatch = Consts.TimerRegex.Match(messageData.Content); Match timer2Match = Consts.Timer2Regex.Match(messageData.Content); if (timerMatch.Success || timer2Match.Success) { Match matchToUse = timerMatch.Success && !timerMatch.Groups["prep"].Value.All(char.IsDigit) ? timerMatch : timer2Match; if (Utilities.TryParseReminder(matchToUse, messageData, out query)) { command = "timer"; } } } } if (string.IsNullOrEmpty(command)) { if (settings.AutoTitlesEnabled && CommandsConfig.Instance.AutoTitleMatches.Any(t => messageData.Content.Contains(t))) { Match match = Consts.HttpRegex.Match(messageData.Content); string matchValue = !string.IsNullOrEmpty(match.Value) ? match.Value : Consts.RedditRegex.Match(messageData.Content).Value; if (!string.IsNullOrEmpty(matchValue)) { command = "title"; query = $"{command} {matchValue}"; } } else if ((settings.FunResponsesEnabled || IsAuthorOwner(messageData)) && contentParts.Length > 1 && contentParts[1] == "face") { command = "face"; query = $"{command} {contentParts[0]}"; } } } // Ignore if the command is disabled on this server if (settings.IsCommandDisabled(CommandsConfig.Instance, command) && !IsAuthorOwner(messageData)) { return(responseData); } if (CommandsConfig.Instance.Commands.ContainsKey(command)) { if (!messageData.RateLimitChecked) { // make sure we're not rate limited var commandKey = command + messageData.Server; var commandCount = this.commandsIssued.AddOrUpdate(commandKey, 1, (key, val) => { return(val + 1); }); if (commandCount > 12) { return(responseData); } else if (commandCount > 10) { responses.Add("rate limited try later"); return(responseData); } } var props = new Dictionary <string, string> { { "serverId", messageData.Server }, }; this.AppInsights?.TrackEvent(command.ToLowerInvariant(), props); // extra processing on ".title" command if ((command == "title" || command == "t") && messageData.Content.EndsWith(command) && urls.ContainsKey(messageData.Channel)) { query += $" {this.urls[messageData.Channel]}"; } responseData = await this.BotApi.IssueRequestAsync(messageData, query); } } if (responseData.Responses.Count == 0 && responseData.Embed == null) { string response = null; if (messageData.MentionsBot(this.Config.Name, Convert.ToUInt64(this.UserId))) { var responseValue = PhrasesConfig.Instance.PartialMentionPhrases.Where(kvp => messageData.Content.ToLowerInvariant().Contains(kvp.Key.ToLowerInvariant())).FirstOrDefault().Value; if (!string.IsNullOrEmpty(responseValue)) { response = PhrasesConfig.Instance.Responses[responseValue].Random(); } } if (response == null && (settings.FunResponsesEnabled || IsAuthorOwner(messageData)) && PhrasesConfig.Instance.ExactPhrases.ContainsKey(messageData.Content) && new Random().Next(1, 100) <= settings.FunResponseChance) { response = PhrasesConfig.Instance.Responses[PhrasesConfig.Instance.ExactPhrases[messageData.Content]].Random(); } if (response != null) { response = response.Replace("%from%", messageData.UserName); string[] resps = response.Split(new char[] { '|' }); responseData.Responses.AddRange(resps); } } return(responseData); }
/// <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(); } } }
/// <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(); } } }