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.";