static async Task NotifyActionLogsMentionsFound(Dictionary <ulong, List <DiscordMessage> > warnDict, DiscordChannel channel) { foreach (ulong snowflake in warnDict.Keys) { // Iterate through each user snowflake logged in the dictionary. // Only make an embed if there are actually mentions. if (warnDict[snowflake].Count > 0) { var mentionList = warnDict[snowflake]; var embed = new DiscordEmbedBuilder(); var stringBuilder = new StringBuilder(); // Build header. stringBuilder.Append($"__**{Generics.GetMention(snowflake)} has had {mentionList.Count + 1} total mention{(mentionList.Count == 1 ? String.Empty : @"s")} (including the most recent one)" + $"in {channel.Mention} in the last {Program.Settings.MaxActionAgeMonths} months:**__\n"); // Build link list. stringBuilder.AppendJoin(' ', Generics.BuildLimitedLinkList( links: mentionList .Select(a => Generics.GetMessageUrl(a)) .ToArray(), endMessage: @"... Too many to display...", maxLength: 2000 - stringBuilder.Length)); embed.WithDescription(stringBuilder.ToString()); embed.WithTitle($"Previous mention{(mentionList.Count == 1 ? String.Empty : @"s")} found"); embed.WithColor(DiscordColor.Red); await channel.SendMessageAsync(embed : embed); } // end if } // end foreach }
/// <summary>Remove a reminder if it exists.</summary> public static async Task RemoveReminder(CommandContext ctx, Reminder reminder) { // It's a reminder, so let's remove it. // Let's build the command. using var command = new SqliteCommand(BotDatabase.Instance.DataSource) { CommandText = QQ_RemoveReminder }; SqliteParameter a = new SqliteParameter("$id", reminder.OriginalMessageId.ToString()) { DbType = DbType.String }; command.Parameters.Add(a); // Now that we have the old reminder, let's remove the old one from the database. await BotDatabase.Instance.ExecuteNonQuery(command); // Now let's respond. var discordEmbedBuilder = new DiscordEmbedBuilder(Generics.GenericEmbedTemplate( color: Generics.PositiveColor, description: Generics.PositiveDirectResponseTemplate( mention: ctx.Member.Mention, @"I able to remove the reminder you gave me!"), thumbnail: Generics.URL_REMINDER_DELETED, title: @"Removed reminder")); DateTimeOffset dto = DateTimeOffset.FromUnixTimeSeconds(reminder.Time * 60); // The reminder's DTO. TimeSpan remainingTime = dto.Subtract(DateTimeOffset.UtcNow); // The remaining time left for the reminder. string originalAuthorMention = Generics.GetMention(reminder.User); discordEmbedBuilder.AddField(@"User", originalAuthorMention, true); discordEmbedBuilder.AddField(@"Time (UTC)", dto.ToString(Generics.DateFormat), true); discordEmbedBuilder.AddField(@"Notification Identifier", reminder.OriginalMessageId.ToString(), false); if (GetUsersToNotify(reminder.UsersToNotify, out string mentionsString)) { discordEmbedBuilder.AddField(@"Users to mention", mentionsString, false); } discordEmbedBuilder.AddField(@"Remaining time", Generics.GetRemainingTime(dto), false); discordEmbedBuilder.AddField(@"Message", reminder.Text, false); // Send the response. await ctx.Channel.SendMessageAsync(embed : discordEmbedBuilder); }
public static async Task SeekWarns(CommandContext ctx, ulong[] memberIds) { const int MAX_FIELDS = 10; DiscordChannel actionLogChannel = await Program.BotClient.GetChannelAsync(Program.Settings.ActionChannelId); Dictionary <ulong, List <DiscordMessage> > warnDict = await QueryMemberMentions(memberIds.Distinct().ToList(), actionLogChannel, Program.Settings.MaxActionAgeMonths, ctx.Message); // Let's start paginating. var pages = new Page[warnDict.Keys.Count]; int page = 0; if (warnDict.Keys.Count > 0) { // Want to generate a page for each member. foreach (var member in warnDict.Keys) { // We want a boolean to check first because if there's no key, we'll get an exception trying to get the count. bool warnsFound = warnDict.ContainsKey(member) && warnDict[member].Count > 0; var deb = new DiscordEmbedBuilder { Title = $"Mentions found in action logs", Description = Generics.NeutralDirectResponseTemplate(mention: ctx.Member.Mention, body: warnsFound ? // Warning, really f*****g long string ahead: $"I found {warnDict[member].Count} mention{(warnDict[member].Count == 1 ? String.Empty : @"s")} for " + $"{Generics.GetMention(member)} in {actionLogChannel.Mention} in the last {Program.Settings.MaxActionAgeMonths} months. " + $"{(warnDict[member].Count > MAX_FIELDS ? $"There are over {MAX_FIELDS}. I will only show the most recent." : String.Empty)}" : $"{ctx.Member.Mention}, I did not find any mentions for {Generics.GetMention(member)}. Good for them..."), Color = warnsFound ? Generics.NegativeColor : Generics.NeutralColor }; if (warnsFound) { // Only continue here if there are actually warns, otherwise just slap a footer on. foreach (var message in warnDict[member]) { // Generate a field for each detected message. if (deb.Fields.Count < MAX_FIELDS) { // Only continue if we have less than MAX_FIELDS fields. // This SB is for all the content. var stringBuilder = new StringBuilder(); // This SB is for all the misc information. var stringBuilderFooter = new StringBuilder(); stringBuilder.Append($"{ChatObjects.Generics.GetMention(message.Author.Id)}: "); stringBuilder.Append(message.Content); if (message.Attachments.Count > 0) { stringBuilderFooter.Append($"\n\n{Formatter.Bold(@"There is an image attached:")} "); stringBuilderFooter.Append(Formatter.MaskedUrl(@"Image", new Uri(message.Attachments[0].Url))); } // end if stringBuilderFooter.Append("\n\n"); stringBuilderFooter.Append(Formatter.MaskedUrl(@"Link", new Uri(Generics.GetMessageUrl(message)))); // We want to prefer the footer's information over the content. So let's figure out how much of the content we // need to trim out. var finalStringBuilder = new StringBuilder(); if (stringBuilder.Length + stringBuilderFooter.Length > 1000) { // We need to do some trimming. if (stringBuilder.Length > 0) { // Let's get the content in there. finalStringBuilder.Append(Generics.BuildLimitedString( originalString: stringBuilder.ToString(), endMessage: @". . . Unable to preview long message...", maxLength: 1000 - stringBuilderFooter.Length)); } if (stringBuilderFooter.Length > 0) { // Let's get the footer in there. finalStringBuilder.Append(stringBuilderFooter); } } else { // We don't need to do any trimming. if (stringBuilder.Length > 0) { // Let's get the content in there. finalStringBuilder.Append(stringBuilder); } if (stringBuilderFooter.Length > 0) { // Let's get the footer in there. finalStringBuilder.Append(stringBuilderFooter); } } deb.AddField($"Action on {message.Timestamp.ToString(Generics.DateFormat)}", finalStringBuilder.ToString()); } else { // Stop the loop if we have MAX_FIELDS fields. break; // NON-SESE ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! } // end else } // end foreach } // end if deb.WithFooter($"Page {page + 1}/{warnDict.Keys.Count}"); pages[page++] = new Page(embed: deb); } // end foreach } // end if // Delete the message if it's the action channel so it's kind of out of the way and doesn't get logged again in the future. if (ctx.Message.ChannelId == Program.Settings.ActionChannelId) { await ctx.Message.DeleteAsync(); } if (pages.Length > 1) { // More than 1 page. var interactivity = Program.BotClient.GetInteractivity(); await interactivity.SendPaginatedMessageAsync ( c : ctx.Channel, u : ctx.User, pages : pages, emojis : Generics.DefaultPaginationEmojis ); } else { // Only one page, we want to send it as a regular embed instead. var anotherDeb = new DiscordEmbedBuilder(pages[0].Embed); // Clear the footer. We don't want the page count. anotherDeb.WithFooter(null, null); await ctx.Channel.SendMessageAsync(embed : anotherDeb); } }
/// <summary>Find any reminders that need to be triggered and trigger them.</summary> static async Task LookTriggerReminders(int timeNowMinutes) { using var command = new SqliteCommand(BotDatabase.Instance.DataSource) { CommandText = QQ_CheckRemindersElapsed }; SqliteParameter a = new SqliteParameter("$timenow", timeNowMinutes) { DbType = DbType.Int32 }; command.Parameters.Add(a); Reminder[] pendingReminders = (Reminder[])await BotDatabase.Instance.ExecuteReaderAsync(command, processAction : readReminders); // Check if there are any reminders if (pendingReminders.Length > 0) { // There are reminders. using var delCommand = new SqliteCommand(BotDatabase.Instance.DataSource) { CommandText = QQ_DeleteRemindersElapsed }; delCommand.Parameters.Add(a); Task[] tasks = new Task[pendingReminders.Length + 1]; tasks[0] = BotDatabase.Instance.ExecuteNonQuery(delCommand); for (int i = 0; i < pendingReminders.Length; i++) { var reminder = pendingReminders[i]; DateTimeOffset reminderTime = DateTimeOffset.FromUnixTimeSeconds(reminder.Time * 60); DateTimeOffset utcNow = DateTimeOffset.UtcNow; var stringBuilder = new StringBuilder(); TimeSpan lateBy = utcNow.Subtract(reminderTime); DiscordEmbedBuilder deb = new DiscordEmbedBuilder() { Title = "Notification", Description = reminder.Text }; deb.WithThumbnail(Generics.URL_REMINDER_EXCLAIM); deb.AddField(@"Late by", value: String.Format("{0}day {1}hr {2}min {3}sec", /*0*/ lateBy.Days, /*1*/ lateBy.Hours, /*2*/ lateBy.Minutes, /*3*/ lateBy.Seconds)); // Get all the people we need to remind. stringBuilder.Append(Generics.GetMention(reminder.User)); Array.ForEach(reminder.UsersToNotify, // For every user (a), append them to sb in mention format <@id>. a => stringBuilder.Append($"{a} ")); tasks[i + 1] = (await Program.BotClient.GetChannelAsync(reminder.Channel)) .SendMessageAsync( content: stringBuilder.ToString(), embed: deb.Build()); } await Task.WhenAll(tasks); } }
public static async Task AddReminder(CommandContext ctx, string args) { // Firstly get all the matches. MatchCollection regexMatches = DateRegex.Matches(args); BitArray regexCoverage = new BitArray(args.Length); var dto = ctx.Message.CreationTimestamp; List <ulong> mentions = new List <ulong>(); // String processing - find the message and get the reminder end date. // To find what's not a date, we simply look for the first character that isn't in the boundaries of a Regex match. // // Structure of a possible string: // // Date String | Message String // DATE, DATE, DATE, DATE | message // // Beyond the Date String we want to stop processing time information as people may reference time in the message string, so we don't // erronously want that data added to the date string. // Populate the regexCoverage... foreach (Match match in regexMatches) { for (int i = match.Index; i < match.Index + match.Length; i++) { regexCoverage[i] = true; } } // So I want to explain what I'm about to do here. Every value in regexCoverage[] indicates if that's part of the initial time // string. We want to use it to determine if something is a message or a time value, so if at any point, we run into something that // isn't a time string, we want to set every instance thereafter as false so we know it's part of a message. if (regexMatches.Count > 0) { bool value = regexCoverage[0]; for (int k = 1; k < regexCoverage.Count; k++) { if (!IsWhitespace(args[k])) { if (!regexCoverage[k] && value) { value = false; } if (!value) { regexCoverage[k] = value; } } } } // We need to figure out where the date string ends. string messageString = String.Empty; int dateEndIndex = 0; bool messageFound = false; while (dateEndIndex < regexCoverage.Length && !messageFound) { char stringChar = args[dateEndIndex]; bool inRegexBoundaries = regexCoverage[dateEndIndex]; // This checks to see if the character is non-white-space and outside of any RegEx boundaries. messageFound = !IsWhitespace(stringChar) && !inRegexBoundaries; // If not found, continue; otherwise, keep incrementing. if (!messageFound) { dateEndIndex++; } } // If we aren't going out of bounds, let's set the string to this. if (dateEndIndex < regexCoverage.Length) { messageString = args.Substring(dateEndIndex); } // Get date information foreach (Match match in regexMatches) { // Only try to exclude Message String date information if a message string was found. if (!messageFound || (regexCoverage[match.Index] && regexCoverage[match.Index + match.Length - 1])) { InterpretTime(match.Groups[1].Value, match.Groups[2].Value, ref dto); } } // Get mentions foreach (DiscordUser user in ctx.Message.MentionedUsers) { ulong id = user.Id; if (!user.IsBot && !mentions.Contains(id)) { mentions.Add(id); } } // At this point, now we have the DateTimeOffset describing when this reminder needs to be set off, and we have a message string if // any. So now we just need to make sure it's within reasonable boundaries, set the reminder, and notify the user. DateTimeOffset maxtime = new DateTimeOffset(ctx.Message.CreationTimestamp.UtcDateTime).AddMonths(Program.Settings.MaxReminderTimeMonths); DiscordEmbedBuilder embed; bool sendErrorEmbed = false; if (dto.UtcTicks == ctx.Message.CreationTimestamp.UtcTicks) { // No time was added. embed = Generics.GenericEmbedTemplate( color: Generics.NegativeColor, description: Generics.NegativeDirectResponseTemplate( mention: ctx.Member.Mention, body: @"I was unable able to add the reminder you gave me. You didn't supply me a valid time..."), title: @"Unable to add reminder", thumbnail: Generics.URL_REMINDER_GENERIC ); sendErrorEmbed = true; } else if (dto.UtcTicks > maxtime.UtcTicks) { // More than our allowed time away. int maxMonths = Program.Settings.MaxReminderTimeMonths; embed = Generics.GenericEmbedTemplate( color: Generics.NegativeColor, description: Generics.NegativeDirectResponseTemplate( mention: ctx.Member.Mention, body: $"I was unable able to add the reminder you gave me. That's more than {maxMonths} month{(maxMonths > 0 ? @"s" : String.Empty)} away..."), title: @"Unable to add reminder", thumbnail: Generics.URL_REMINDER_GENERIC ); sendErrorEmbed = true; } else { // Everything is good in the world... except that the world is burning, but that's not something we're worried about here, for // now... embed = Generics.GenericEmbedTemplate( color: Generics.PositiveColor, description: Generics.PositiveDirectResponseTemplate( mention: ctx.Member.Mention, body: @"I added the reminder you gave me!"), title: @"Add reminder", thumbnail: Generics.URL_REMINDER_GENERIC ); Reminder reminder = new Reminder( originalMessageId: ctx.Message.Id.ToString(), text: messageString.Length.Equals(0) ? @"n/a" : messageString.ToString(), time: (int)(dto.ToUnixTimeSeconds() / 60), user: ctx.Member.Id, channel: ctx.Channel.Id, usersToNotify: mentions.Select(a => Generics.GetMention(a)).ToArray()); embed.AddField(@"User", ctx.Member.Mention, true); embed.AddField(@"Time (UTC)", dto.ToString(Generics.DateFormat), true); embed.AddField(@"Remaining time", Generics.GetRemainingTime(dto), true); embed.AddField(@"Notification Identifier", reminder.OriginalMessageId.ToString(), false); if (GetUsersToNotify(reminder.UsersToNotify, out string mentionsString)) { embed.AddField(@"Users to mention", mentionsString, false); } // Let's build the command. using var command = new SqliteCommand(BotDatabase.Instance.DataSource) { CommandText = QQ_AddReminder }; SqliteParameter a = new SqliteParameter("$id", reminder.OriginalMessageId.ToString()) { DbType = DbType.String }; SqliteParameter b = new SqliteParameter("$userid", reminder.User) { DbType = DbType.String }; SqliteParameter c = new SqliteParameter("$channelid", reminder.Channel) { DbType = DbType.String }; SqliteParameter d = new SqliteParameter("$message", reminder.Text) { DbType = DbType.String }; SqliteParameter e = new SqliteParameter("$time", reminder.Time) { DbType = DbType.Int32 }; var stringBuilder = new StringBuilder(); stringBuilder.AppendJoin(' ', reminder.UsersToNotify); SqliteParameter f = new SqliteParameter("$mention", stringBuilder.ToString()) { DbType = DbType.String }; command.Parameters.AddRange(new SqliteParameter[] { a, b, c, d, e, f }); await BotDatabase.Instance.ExecuteNonQuery(command); // Send the response. await ctx.Channel.SendMessageAsync(embed : embed); } if (sendErrorEmbed) { var a = ctx.Channel.SendMessageAsync(embed: embed); var b = GenericResponses.HandleInvalidArguments(ctx); await Task.WhenAll(a, b); } }
/// <summary>List all the reminders</summary> public static async Task ListReminders(CommandContext ctx) { Reminder[] reminders = await ReadTable(); // Check if there are any notifications. If there are none, let the user know. if (reminders.Length > 0) { // There are reminders. var interactivity = Program.BotClient.GetInteractivity(); List <Page> pages = new List <Page>(); var deb = new DiscordEmbedBuilder(); int count = 0; int curPage = 1; // Paginate all the results. const int REMINDERS_PER_PAGE = 5; for (int i = 0; i < reminders.Length; i++) { Reminder reminder = reminders[i]; var dto = DateTimeOffset.FromUnixTimeSeconds(reminder.Time * 60); var valueStringBuilder = new StringBuilder(); valueStringBuilder.Append($"{Generics.GetMention(reminder.User)}: {reminder.Text}\n"); if (GetUsersToNotify(reminder.UsersToNotify, out string mentionsString)) { valueStringBuilder.Append($"**Users to mention:** {mentionsString}\n"); } valueStringBuilder.Append($"**Id:** {reminder.OriginalMessageId}\n"); valueStringBuilder.Append($"**Remaining time:** {Generics.GetRemainingTime(dto)}"); #region a bunny // .". // / | // / / // / ," // .-------.--- / // "._ __.-/ o. o\ // " ( Y ) // ) / // / ( // / Y // .-" | // / _ \ \ // / `. ". ) /' ) // Y )( / /(,/ // ,| / ) // ( | / / // " \_ (__ (__ [nabis] // "-._,)--._,) // o < bunny poopy l0l // ------------------------------------------------ // This ASCII pic can be found at // https://asciiart.website/index.php?art=animals/rabbits #endregion a bunny string name = dto.ToString(Generics.DateFormat); deb.AddField(name, valueStringBuilder.ToString()); count++; if (count == REMINDERS_PER_PAGE || i == reminders.Length - 1) { // Create a new page. deb.WithDescription(Generics.NeutralDirectResponseTemplate( mention: ctx.User.Mention, body: $"Hello {ctx.Member.Mention}, please note you are the only one who can react to this message.\n\n" + $"**Showing {count} reminders out of a total of {reminders.Length}.**")); deb.WithTitle($"Reminders Page {curPage}/{Math.Ceiling((float)reminders.Length / (float)REMINDERS_PER_PAGE)}"); deb.WithColor(Generics.NeutralColor); deb.WithThumbnail(Generics.URL_REMINDER_GENERIC); pages.Add(new Page(embed: deb)); count = 0; curPage++; deb = new DiscordEmbedBuilder(); } // end if } // end for await interactivity.SendPaginatedMessageAsync(ctx.Channel, ctx.User, pages, emojis : Generics.DefaultPaginationEmojis); } else { // There are no reminders. await ctx.Channel.SendMessageAsync( embed : Generics.GenericEmbedTemplate( color: Generics.NeutralColor, description: Generics.NeutralDirectResponseTemplate( mention: ctx.Member.Mention, body: "there are no reminders."), thumbnail: Generics.URL_SPEECH_BUBBLE, title: "Reminders")); } }