From 952188a8cf2287b33c40bd0428c508bdefd8fc86 Mon Sep 17 00:00:00 2001 From: Noi Date: Sat, 27 May 2023 21:54:47 -0700 Subject: [PATCH] Split birthday exporting to its own command --- ApplicationCommands/BirthdayModule.cs | 149 ------------------ ApplicationCommands/BirthdayOverrideModule.cs | 6 +- ApplicationCommands/BotModuleBase.cs | 68 +++++++- ApplicationCommands/ConfigModule.cs | 14 +- ApplicationCommands/ExportModule.cs | 94 +++++++++++ ApplicationCommands/HelpModule.cs | 2 +- 6 files changed, 169 insertions(+), 164 deletions(-) create mode 100644 ApplicationCommands/ExportModule.cs diff --git a/ApplicationCommands/BirthdayModule.cs b/ApplicationCommands/BirthdayModule.cs index 6c7c267..43429ef 100644 --- a/ApplicationCommands/BirthdayModule.cs +++ b/ApplicationCommands/BirthdayModule.cs @@ -12,7 +12,6 @@ public class BirthdayModule : BotModuleBase { public const string HelpCmdRemove = "Removes your birthday information from this bot."; public const string HelpCmdGet = "Gets a user's birthday."; public const string HelpCmdNearest = "Get a list of users who recently had or will have a birthday."; - public const string HelpCmdExport = "Generates a text file with all known and available birthdays."; public const string ErrNotSetFk = $":x: The bot has not yet been set up. Please configure a birthday role."; // foreign key violation // Note that these methods have largely been copied to BirthdayOverrideModule. Changes here should be reflected there as needed. @@ -190,152 +189,4 @@ public class BirthdayModule : BotModuleBase { else await doOutput(output.ToString()).ConfigureAwait(false); } - - [DefaultMemberPermissions(GuildPermission.ManageGuild)] - [SlashCommand("export", HelpPfxModOnly + HelpCmdExport)] - public async Task CmdExport([Summary(description: "Specify whether to export the list in CSV format.")] bool asCsv = false) { - if (!await HasMemberCacheAsync(Context.Guild)) { - await RespondAsync(MemberCacheEmptyError, ephemeral: true).ConfigureAwait(false); - return; - } - - var bdlist = GetSortedUserList(Context.Guild); - - var filename = "birthdaybot-" + Context.Guild.Id; - Stream fileoutput; - if (asCsv) { - fileoutput = ListExportCsv(Context.Guild, bdlist); - filename += ".csv"; - } else { - fileoutput = ListExportNormal(Context.Guild, bdlist); - filename += ".txt."; - } - await RespondWithFileAsync(fileoutput, filename, text: $"Exported {bdlist.Count} birthdays to file."); - } - - #region Listing helper methods - /// - /// Fetches all guild birthdays and places them into an easily usable structure. - /// Users currently not in the guild are not included in the result. - /// - private static List GetSortedUserList(SocketGuild guild) { - using var db = new BotDatabaseContext(); - var query = from row in db.UserEntries - where row.GuildId == guild.Id - orderby row.BirthMonth, row.BirthDay - select new { - row.UserId, - Month = row.BirthMonth, - Day = row.BirthDay, - Zone = row.TimeZone - }; - - var result = new List(); - foreach (var row in query) { - var guildUser = guild.GetUser(row.UserId); - if (guildUser == null) continue; // Skip user not in guild - - result.Add(new ListItem() { - BirthMonth = row.Month, - BirthDay = row.Day, - DateIndex = DateIndex(row.Month, row.Day), - UserId = guildUser.Id, - DisplayName = Common.FormatName(guildUser, false), - TimeZone = row.Zone - }); - } - return result; - } - - private static Stream ListExportNormal(SocketGuild guild, IEnumerable list) { - // Output: "● Mon-dd: (user ID) Username [ - Nickname: (nickname)]" - var result = new MemoryStream(); - var writer = new StreamWriter(result, Encoding.UTF8); - - writer.WriteLine("Birthdays in " + guild.Name); - writer.WriteLine(); - foreach (var item in list) { - var user = guild.GetUser(item.UserId); - if (user == null) continue; // User disappeared in the instant between getting list and processing - writer.Write($"● {Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}: "); - writer.Write(item.UserId); - writer.Write(" " + user.Username + "#" + user.Discriminator); - if (user.Nickname != null) writer.Write(" - Nickname: " + user.Nickname); - if (item.TimeZone != null) writer.Write(" | Time zone: " + item.TimeZone); - writer.WriteLine(); - } - writer.Flush(); - result.Position = 0; - return result; - } - - private static Stream ListExportCsv(SocketGuild guild, IEnumerable list) { - // Output: User ID, Username, Nickname, Month-Day, Month, Day - var result = new MemoryStream(); - var writer = new StreamWriter(result, Encoding.UTF8); - - // Conforming to RFC 4180; with header - writer.Write("UserId,Username,Nickname,MonthDayDisp,Month,Day,TimeZone"); - writer.Write("\r\n"); // crlf line break is specified by the standard - foreach (var item in list) { - var user = guild.GetUser(item.UserId); - if (user == null) continue; // User disappeared in the instant between getting list and processing - writer.Write(item.UserId); - writer.Write(','); - writer.Write(CsvEscape(user.Username + "#" + user.Discriminator)); - writer.Write(','); - if (user.Nickname != null) writer.Write(user.Nickname); - writer.Write(','); - writer.Write($"{Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}"); - writer.Write(','); - writer.Write(item.BirthMonth); - writer.Write(','); - writer.Write(item.BirthDay); - writer.Write(','); - writer.Write(item.TimeZone); - writer.Write("\r\n"); - } - writer.Flush(); - result.Position = 0; - return result; - } - - private static string CsvEscape(string input) { - var result = new StringBuilder(); - result.Append('"'); - foreach (var ch in input) { - if (ch == '"') result.Append('"'); - result.Append(ch); - } - result.Append('"'); - return result.ToString(); - } - - private static int DateIndex(int month, int day) { - var dateindex = 0; - // Add month offsets - if (month > 1) dateindex += 31; // Offset January - if (month > 2) dateindex += 29; // Offset February (incl. leap day) - if (month > 3) dateindex += 31; // etc - if (month > 4) dateindex += 30; - if (month > 5) dateindex += 31; - if (month > 6) dateindex += 30; - if (month > 7) dateindex += 31; - if (month > 8) dateindex += 31; - if (month > 9) dateindex += 30; - if (month > 10) dateindex += 31; - if (month > 11) dateindex += 30; - dateindex += day; - return dateindex; - } - - private struct ListItem { - public int DateIndex; - public int BirthMonth; - public int BirthDay; - public ulong UserId; - public string DisplayName; - public string? TimeZone; - } - #endregion } \ No newline at end of file diff --git a/ApplicationCommands/BirthdayOverrideModule.cs b/ApplicationCommands/BirthdayOverrideModule.cs index b6a059b..38c199d 100644 --- a/ApplicationCommands/BirthdayOverrideModule.cs +++ b/ApplicationCommands/BirthdayOverrideModule.cs @@ -13,7 +13,7 @@ public class BirthdayOverrideModule : BotModuleBase { // Note that these methods have largely been copied from BirthdayModule. Changes there should be reflected here as needed. // TODO possible to use a common base class for shared functionality instead? - [SlashCommand("set-birthday", HelpPfxModOnly + "Set a user's birthday on their behalf.")] + [SlashCommand("set-birthday", "Set a user's birthday on their behalf.")] public async Task OvSetBirthday([Summary(description: HelpOptOvTarget)]SocketGuildUser target, [Summary(description: HelpOptDate)]string date) { int inmonth, inday; @@ -42,7 +42,7 @@ public class BirthdayOverrideModule : BotModuleBase { $"**{FormatDate(inmonth, inday)}**.").ConfigureAwait(false); } - [SlashCommand("set-timezone", HelpPfxModOnly + "Set a user's time zone on their behalf.")] + [SlashCommand("set-timezone", "Set a user's time zone on their behalf.")] public async Task OvSetTimezone([Summary(description: HelpOptOvTarget)]SocketGuildUser target, [Summary(description: HelpOptZone)]string zone) { using var db = new BotDatabaseContext(); @@ -67,7 +67,7 @@ public class BirthdayOverrideModule : BotModuleBase { $"**{newzone}**.").ConfigureAwait(false); } - [SlashCommand("remove-birthday", HelpPfxModOnly + "Remove a user's birthday information on their behalf.")] + [SlashCommand("remove-birthday", "Remove a user's birthday information on their behalf.")] public async Task OvRemove([Summary(description: HelpOptOvTarget)]SocketGuildUser target) { using var db = new BotDatabaseContext(); var user = target.GetUserEntryOrNew(db); diff --git a/ApplicationCommands/BotModuleBase.cs b/ApplicationCommands/BotModuleBase.cs index c5a9c14..2a6ddd1 100644 --- a/ApplicationCommands/BotModuleBase.cs +++ b/ApplicationCommands/BotModuleBase.cs @@ -1,4 +1,5 @@ -using Discord.Interactions; +using BirthdayBot.Data; +using Discord.Interactions; using NodaTime; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -10,9 +11,6 @@ namespace BirthdayBot.ApplicationCommands; /// Base class for our interaction module classes. Contains common data for use in implementing classes. /// public abstract class BotModuleBase : InteractionModuleBase { - protected const string HelpPfxModOnly = "Bot moderators only: "; - protected const string ErrGuildOnly = ":x: This command can only be run within a server."; - protected const string ErrNotAllowed = ":x: Only server moderators may use this command."; protected const string MemberCacheEmptyError = ":warning: Please try the command again."; public const string AccessDeniedError = ":warning: You are not allowed to run this command."; @@ -133,4 +131,66 @@ public abstract class BotModuleBase : InteractionModuleBase protected static string FormatDate(int month, int day) => $"{day:00}-{Common.MonthNames[month]}"; #endregion + + #region Listing helper methods + /// + /// Fetches all guild birthdays and places them into an easily usable structure. + /// Users currently not in the guild are not included in the result. + /// + protected static List GetSortedUserList(SocketGuild guild) { + using var db = new BotDatabaseContext(); + var query = from row in db.UserEntries + where row.GuildId == guild.Id + orderby row.BirthMonth, row.BirthDay + select new { + row.UserId, + Month = row.BirthMonth, + Day = row.BirthDay, + Zone = row.TimeZone + }; + + var result = new List(); + foreach (var row in query) { + var guildUser = guild.GetUser(row.UserId); + if (guildUser == null) continue; // Skip user not in guild + + result.Add(new ListItem() { + BirthMonth = row.Month, + BirthDay = row.Day, + DateIndex = DateIndex(row.Month, row.Day), + UserId = guildUser.Id, + DisplayName = Common.FormatName(guildUser, false), + TimeZone = row.Zone + }); + } + return result; + } + + protected static int DateIndex(int month, int day) { + var dateindex = 0; + // Add month offsets + if (month > 1) dateindex += 31; // Offset January + if (month > 2) dateindex += 29; // Offset February (incl. leap day) + if (month > 3) dateindex += 31; // etc + if (month > 4) dateindex += 30; + if (month > 5) dateindex += 31; + if (month > 6) dateindex += 30; + if (month > 7) dateindex += 31; + if (month > 8) dateindex += 31; + if (month > 9) dateindex += 30; + if (month > 10) dateindex += 31; + if (month > 11) dateindex += 30; + dateindex += day; + return dateindex; + } + + protected struct ListItem { + public int DateIndex; + public int BirthMonth; + public int BirthDay; + public ulong UserId; + public string DisplayName; + public string? TimeZone; + } + #endregion } \ No newline at end of file diff --git a/ApplicationCommands/ConfigModule.cs b/ApplicationCommands/ConfigModule.cs index 0818dd2..2ce338a 100644 --- a/ApplicationCommands/ConfigModule.cs +++ b/ApplicationCommands/ConfigModule.cs @@ -16,7 +16,7 @@ public class ConfigModule : BotModuleBase { const string HelpOptChannel = "The corresponding channel to use."; const string HelpOptRole = "The corresponding role to use."; - [Group("announce", HelpPfxModOnly + HelpCmdAnnounce)] + [Group("announce", HelpCmdAnnounce)] public class SubCmdsConfigAnnounce : BotModuleBase { private const string HelpSubCmdChannel = "Set which channel will receive announcement messages."; private const string HelpSubCmdMessage = "Modify the announcement message."; @@ -54,14 +54,14 @@ public class ConfigModule : BotModuleBase { .Build()).ConfigureAwait(false); } - [SlashCommand("set-channel", HelpPfxModOnly + HelpSubCmdChannel + HelpPofxBlankUnset)] + [SlashCommand("set-channel", HelpSubCmdChannel + HelpPofxBlankUnset)] public async Task CmdSetChannel([Summary(description: HelpOptChannel)] SocketTextChannel? channel = null) { await DoDatabaseUpdate(Context, s => s.AnnouncementChannel = channel?.Id); await RespondAsync(":white_check_mark: The announcement channel has been " + (channel == null ? "unset." : $"set to **{channel.Name}**.")); } - [SlashCommand("set-message", HelpPfxModOnly + HelpSubCmdMessage)] + [SlashCommand("set-message", HelpSubCmdMessage)] public async Task CmdSetMessage() { using var db = new BotDatabaseContext(); var settings = Context.Guild.GetConfigOrNew(db); @@ -111,14 +111,14 @@ public class ConfigModule : BotModuleBase { await modal.RespondAsync(":white_check_mark: Announcement messages have been updated."); } - [SlashCommand("set-ping", HelpPfxModOnly + HelpSubCmdPing)] + [SlashCommand("set-ping", HelpSubCmdPing)] public async Task CmdSetPing([Summary(description: "Set True to ping users, False to display them normally.")]bool option) { await DoDatabaseUpdate(Context, s => s.AnnouncePing = option); await RespondAsync($":white_check_mark: Announcement pings are now **{(option ? "on" : "off")}**.").ConfigureAwait(false); } } - [SlashCommand("birthday-role", HelpPfxModOnly + HelpCmdBirthdayRole)] + [SlashCommand("birthday-role", HelpCmdBirthdayRole)] public async Task CmdSetBRole([Summary(description: HelpOptRole)] SocketRole role) { if (role.IsEveryone || role.IsManaged) { await RespondAsync(":x: This role cannot be used for this setting.", ephemeral: true); @@ -128,7 +128,7 @@ public class ConfigModule : BotModuleBase { await RespondAsync($":white_check_mark: The birthday role has been set to **{role.Name}**.").ConfigureAwait(false); } - [SlashCommand("check", HelpPfxModOnly + HelpCmdCheck)] + [SlashCommand("check", HelpCmdCheck)] public async Task CmdCheck() { static string DoTestFor(string label, Func test) => $"{label}: { (test() ? ":white_check_mark: Yes" : ":x: No") }"; @@ -191,7 +191,7 @@ public class ConfigModule : BotModuleBase { }.Build()).ConfigureAwait(false); } - [SlashCommand("set-timezone", HelpPfxModOnly + "Configure the time zone to use by default in the server." + HelpPofxBlankUnset)] + [SlashCommand("set-timezone", "Configure the time zone to use by default in the server." + HelpPofxBlankUnset)] public async Task CmdSetTimezone([Summary(description: HelpOptZone)] string? zone = null) { const string Response = ":white_check_mark: The server's time zone has been "; diff --git a/ApplicationCommands/ExportModule.cs b/ApplicationCommands/ExportModule.cs new file mode 100644 index 0000000..77ca85b --- /dev/null +++ b/ApplicationCommands/ExportModule.cs @@ -0,0 +1,94 @@ +using Discord.Interactions; +using System.Text; + +namespace BirthdayBot.ApplicationCommands; +public class ExportModule : BotModuleBase { + public const string HelpCmdExport = "Generates a text file with all known and available birthdays."; + + [SlashCommand("export-birthdays", HelpCmdExport)] + [DefaultMemberPermissions(GuildPermission.ManageGuild)] + [EnabledInDm(false)] + public async Task CmdExport([Summary(description: "Specify whether to export the list in CSV format.")] bool asCsv = false) { + if (!await HasMemberCacheAsync(Context.Guild)) { + await RespondAsync(MemberCacheEmptyError, ephemeral: true); + return; + } + + var bdlist = GetSortedUserList(Context.Guild); + + var filename = "birthdaybot-" + Context.Guild.Id; + Stream fileoutput; + if (asCsv) { + fileoutput = ListExportCsv(Context.Guild, bdlist); + filename += ".csv"; + } else { + fileoutput = ListExportNormal(Context.Guild, bdlist); + filename += ".txt."; + } + await RespondWithFileAsync(fileoutput, filename, text: $"Exported {bdlist.Count} birthdays to file."); + } + + private static Stream ListExportNormal(SocketGuild guild, IEnumerable list) { + // Output: "● Mon-dd: (user ID) Username [ - Nickname: (nickname)]" + var result = new MemoryStream(); + var writer = new StreamWriter(result, Encoding.UTF8); + + writer.WriteLine("Birthdays in " + guild.Name); + writer.WriteLine(); + foreach (var item in list) { + var user = guild.GetUser(item.UserId); + if (user == null) continue; // User disappeared in the instant between getting list and processing + writer.Write($"● {Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}: "); + writer.Write(item.UserId); + writer.Write(" " + user.Username + "#" + user.Discriminator); + if (user.Nickname != null) writer.Write(" - Nickname: " + user.Nickname); + if (item.TimeZone != null) writer.Write(" | Time zone: " + item.TimeZone); + writer.WriteLine(); + } + writer.Flush(); + result.Position = 0; + return result; + } + + private static Stream ListExportCsv(SocketGuild guild, IEnumerable list) { + // Output: User ID, Username, Nickname, Month-Day, Month, Day + var result = new MemoryStream(); + var writer = new StreamWriter(result, Encoding.UTF8); + + static string csvEscape(string input) { + var result = new StringBuilder(); + result.Append('"'); + foreach (var ch in input) { + if (ch == '"') result.Append('"'); + result.Append(ch); + } + result.Append('"'); + return result.ToString(); + } + + // Conforming to RFC 4180; with header + writer.Write("UserId,Username,Nickname,MonthDayDisp,Month,Day,TimeZone"); + writer.Write("\r\n"); // crlf line break is specified by the standard + foreach (var item in list) { + var user = guild.GetUser(item.UserId); + if (user == null) continue; // User disappeared in the instant between getting list and processing + writer.Write(item.UserId); + writer.Write(','); + writer.Write(csvEscape(user.Username + "#" + user.Discriminator)); + writer.Write(','); + if (user.Nickname != null) writer.Write(user.Nickname); + writer.Write(','); + writer.Write($"{Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}"); + writer.Write(','); + writer.Write(item.BirthMonth); + writer.Write(','); + writer.Write(item.BirthDay); + writer.Write(','); + writer.Write(item.TimeZone); + writer.Write("\r\n"); + } + writer.Flush(); + result.Position = 0; + return result; + } +} \ No newline at end of file diff --git a/ApplicationCommands/HelpModule.cs b/ApplicationCommands/HelpModule.cs index fbd7a3b..3da4bb2 100644 --- a/ApplicationCommands/HelpModule.cs +++ b/ApplicationCommands/HelpModule.cs @@ -16,12 +16,12 @@ public class HelpModule : BotModuleBase { $"` ⤷set timezone` - {BirthdayModule.HelpCmdSetZone}\n" + $"` ⤷remove` - {BirthdayModule.HelpCmdRemove}"; private const string ModCommandsField = - $"`/birthday export` - {BirthdayModule.HelpCmdExport}\n" + $"`/config` - {ConfigModule.HelpCmdConfig}\n" + $"` ⤷check` - {ConfigModule.HelpCmdCheck}\n" + $"` ⤷announce` - {ConfigModule.HelpCmdAnnounce}\n" + $"` ⤷` See also: `/config announce help`.\n" + $"` ⤷birthday-role` - {ConfigModule.HelpCmdBirthdayRole}\n" + + $"`/export-birthdays` - {ExportModule.HelpCmdExport}\n" + $"`/override` - {BirthdayOverrideModule.HelpCmdOverride}\n" + $"` ⤷set-birthday`, `⤷set-timezone`, `⤷remove`\n" + "**Caution:** Skipping optional parameters __removes__ their configuration.";