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