diff --git a/BackgroundServices/BirthdayRoleUpdate.cs b/BackgroundServices/BirthdayRoleUpdate.cs
index b63fb6a..e97906f 100644
--- a/BackgroundServices/BirthdayRoleUpdate.cs
+++ b/BackgroundServices/BirthdayRoleUpdate.cs
@@ -78,36 +78,6 @@ class BirthdayRoleUpdate : BackgroundService {
else if (exceptions.Count == 1) throw new Exception("An unhandled exception occurred when processing a birthday.", exceptions[0]);
}
- ///
- /// Gets all known users from the given guild and returns a list including only those who are
- /// currently experiencing a birthday in the respective time zone.
- ///
- [Obsolete(Database.ObsoleteReason)]
- public static HashSet GetGuildCurrentBirthdays(IEnumerable guildUsers, string? defaultTzStr) {
- var tzdb = DateTimeZoneProviders.Tzdb;
- DateTimeZone defaultTz = (defaultTzStr != null ? DateTimeZoneProviders.Tzdb.GetZoneOrNull(defaultTzStr) : null)
- ?? tzdb.GetZoneOrNull("UTC")!;
-
- var birthdayUsers = new HashSet();
- foreach (var item in guildUsers) {
- // Determine final time zone to use for calculation
- DateTimeZone tz = (item.TimeZone != null ? tzdb.GetZoneOrNull(item.TimeZone) : null) ?? defaultTz;
-
- var targetMonth = item.BirthMonth;
- var targetDay = item.BirthDay;
-
- var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
- // Special case: If birthday is February 29 and it's not a leap year, recognize it on March 1st
- if (targetMonth == 2 && targetDay == 29 && !DateTime.IsLeapYear(checkNow.Year)) {
- targetMonth = 3;
- targetDay = 1;
- }
- if (targetMonth == checkNow.Month && targetDay == checkNow.Day) {
- birthdayUsers.Add(item.UserId);
- }
- }
- return birthdayUsers;
- }
///
/// Gets all known users from the given guild and returns a list including only those who are
/// currently experiencing a birthday in the respective time zone.
diff --git a/Data/Database.cs b/Data/Database.cs
deleted file mode 100644
index 2dfd980..0000000
--- a/Data/Database.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Npgsql;
-
-namespace BirthdayBot.Data;
-
-[Obsolete(ObsoleteReason, error: false)]
-internal static class Database {
- public const string ObsoleteReason = "Will be removed in favor of EF6 stuff when text commands are removed";
-
- public static string DBConnectionString { get; set; } = null!;
-
- ///
- /// Sets up and opens a database connection.
- ///
- public static async Task OpenConnectionAsync() {
- var db = new NpgsqlConnection(DBConnectionString);
- await db.OpenAsync().ConfigureAwait(false);
- return db;
- }
-
- public static async Task DoInitialDatabaseSetupAsync() {
- using var db = await OpenConnectionAsync().ConfigureAwait(false);
-
- // Refer to the methods being called for information on how the database is set up.
- // Note: The order these are called is important. (Foreign reference constraints.)
- await GuildConfiguration.DatabaseSetupAsync(db).ConfigureAwait(false);
- await GuildUserConfiguration.DatabaseSetupAsync(db).ConfigureAwait(false);
- }
-}
diff --git a/Data/GuildConfiguration.cs b/Data/GuildConfiguration.cs
deleted file mode 100644
index b844fb2..0000000
--- a/Data/GuildConfiguration.cs
+++ /dev/null
@@ -1,253 +0,0 @@
-using Npgsql;
-using NpgsqlTypes;
-using System.Data.Common;
-
-namespace BirthdayBot.Data;
-
-///
-/// Represents guild-specific configuration as exists in the database.
-/// Updating any property requires a call to for changes to take effect.
-///
-[Obsolete(Database.ObsoleteReason, error: false)]
-class GuildConfiguration {
- ///
- /// Gets this configuration's corresponding guild ID.
- ///
- public ulong GuildId { get; }
-
- ///
- /// Gets or sets the guild's designated usable role ID.
- /// Updating this value requires a call to .
- ///
- public ulong? RoleId { get; set; }
-
- ///
- /// Gets or sets the announcement channel ID.
- /// Updating this value requires a call to .
- ///
- public ulong? AnnounceChannelId { get; set; }
-
- ///
- /// Gets or sets the guild's default time zone ztring.
- /// Updating this value requires a call to .
- ///
- public string? TimeZone { get; set; }
-
- ///
- /// Gets or sets the guild's moderated mode setting.
- /// Updating this value requires a call to .
- ///
- public bool IsModerated { get; set; }
-
- ///
- /// Gets or sets the guild's corresponding bot moderator role ID.
- /// Updating this value requires a call to .
- ///
- public ulong? ModeratorRole { get; set; }
-
- ///
- /// Gets or sets the guild-specific birthday announcement message.
- /// Updating this value requires a call to .
- ///
- public (string?, string?) AnnounceMessages { get; set; }
-
- ///
- /// Gets or sets the announcement ping setting.
- /// Updating this value requires a call to .
- ///
- public bool AnnouncePing { get; set; }
-
- // Called by Load. Double-check ordinals when changes are made.
- private GuildConfiguration(DbDataReader reader) {
- GuildId = (ulong)reader.GetInt64(0);
- if (!reader.IsDBNull(1)) RoleId = (ulong)reader.GetInt64(1);
- if (!reader.IsDBNull(2)) AnnounceChannelId = (ulong)reader.GetInt64(2);
- TimeZone = reader.IsDBNull(3) ? null : reader.GetString(3);
- IsModerated = reader.GetBoolean(4);
- if (!reader.IsDBNull(5)) ModeratorRole = (ulong)reader.GetInt64(5);
- string? announceMsg = reader.IsDBNull(6) ? null : reader.GetString(6);
- string? announceMsgPl = reader.IsDBNull(7) ? null : reader.GetString(7);
- AnnounceMessages = (announceMsg, announceMsgPl);
- AnnouncePing = reader.GetBoolean(8);
- }
-
- ///
- /// Checks if the specified user is blocked by current guild policy (block list or moderated mode).
- ///
- public async Task IsUserBlockedAsync(ulong userId) {
- if (IsModerated) return true;
- return await IsUserInBlocklistAsync(userId).ConfigureAwait(false);
- }
-
- ///
- /// Checks if the given user exists in the block list.
- ///
- public async Task IsUserInBlocklistAsync(ulong userId) {
- using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
- using var c = db.CreateCommand();
- c.CommandText = $"select * from {BackingTableBans} "
- + "where guild_id = @Gid and user_id = @Uid";
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
- c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
- c.Prepare();
- using var r = await c.ExecuteReaderAsync().ConfigureAwait(false);
- if (!await r.ReadAsync().ConfigureAwait(false)) return false;
- return true;
- }
-
- ///
- /// Adds the specified user to the block list corresponding to this guild.
- ///
- public async Task BlockUserAsync(ulong userId) {
- using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
- using var c = db.CreateCommand();
- c.CommandText = $"insert into {BackingTableBans} (guild_id, user_id) "
- + "values (@Gid, @Uid) "
- + "on conflict (guild_id, user_id) do nothing";
- // There is no validation on whether the requested user is even in the guild. will this be a problem?
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
- c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
- c.Prepare();
- await c.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
-
- ///
- /// Removes the specified user from the block list corresponding to this guild.
- ///
- /// True if a user has been removed, false if the requested user was not in this list.
- public async Task UnblockUserAsync(ulong userId) {
- using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
- using var c = db.CreateCommand();
- c.CommandText = $"delete from {BackingTableBans} where "
- + "guild_id = @Gid and user_id = @Uid";
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
- c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
- c.Prepare();
- var result = await c.ExecuteNonQueryAsync().ConfigureAwait(false);
- return result != 0;
- }
-
- ///
- /// Checks if the given user can be considered a bot moderator.
- /// Checks for either the Manage Guild permission or if the user is within a predetermined role.
- ///
- public bool IsBotModerator(SocketGuildUser user)
- => user.GuildPermissions.ManageGuild || (ModeratorRole.HasValue && user.Roles.Any(r => r.Id == ModeratorRole.Value));
-
- #region Database
- public const string BackingTable = "settings";
- public const string BackingTableBans = "banned_users";
-
- [Obsolete("DELETE THIS", error: true)]
- internal static async Task DatabaseSetupAsync(NpgsqlConnection db) {
- using (var c = db.CreateCommand()) {
- c.CommandText = $"create table if not exists {BackingTable} ("
- + "guild_id bigint primary key, "
- + "role_id bigint null, "
- + "channel_announce_id bigint null, "
- + "time_zone text null, "
- + "moderated boolean not null default FALSE, "
- + "moderator_role bigint null, "
- + "announce_message text null, "
- + "announce_message_pl text null, "
- + "announce_ping boolean not null default FALSE, "
- + "last_seen timestamptz not null default NOW()"
- + ")";
- await c.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
- using (var c = db.CreateCommand()) {
- c.CommandText = $"create table if not exists {BackingTableBans} ("
- + $"guild_id bigint not null references {BackingTable} ON DELETE CASCADE, "
- + "user_id bigint not null, "
- + "PRIMARY KEY (guild_id, user_id)"
- + ")";
- await c.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
- }
-
- ///
- /// Fetches guild settings from the database. If no corresponding entry exists, it will be created.
- ///
- ///
- /// If true, this method shall not create a new entry and will return null if the guild does
- /// not exist in the database.
- ///
- public static async Task LoadAsync(ulong guildId, bool nullIfUnknown) {
- // TODO nullable static analysis problem: how to indicate non-null return when nullIfUnknown parameter is true?
- using (var db = await Database.OpenConnectionAsync().ConfigureAwait(false)) {
- using (var c = db.CreateCommand()) {
- // Take note of ordinals for the constructor
- c.CommandText = "select guild_id, role_id, channel_announce_id, time_zone, "
- + " moderated, moderator_role, announce_message, announce_message_pl, announce_ping "
- + $"from {BackingTable} where guild_id = @Gid";
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
- c.Prepare();
- using var r = await c.ExecuteReaderAsync().ConfigureAwait(false);
- if (await r.ReadAsync().ConfigureAwait(false)) return new GuildConfiguration(r);
- }
- if (nullIfUnknown) return null;
-
- // If we got here, no row exists. Create it with default values.
- using (var c = db.CreateCommand()) {
- c.CommandText = $"insert into {BackingTable} (guild_id) values (@Gid)";
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
- c.Prepare();
- await c.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
- }
- // With a new row created, try this again
- return await LoadAsync(guildId, nullIfUnknown).ConfigureAwait(false);
- }
-
- ///
- /// Updates values on the backing database with values from this object instance.
- ///
- public async Task UpdateAsync() {
- using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
- using var c = db.CreateCommand();
- c.CommandText = $"update {BackingTable} set "
- + "role_id = @RoleId, "
- + "channel_announce_id = @ChannelId, "
- + "time_zone = @TimeZone, "
- + "moderated = @Moderated, "
- + "moderator_role = @ModRole, "
- + "announce_message = @AnnounceMsg, "
- + "announce_message_pl = @AnnounceMsgPl, "
- + "announce_ping = @AnnouncePing "
- + "where guild_id = @Gid";
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
- NpgsqlParameter p;
-
- p = c.Parameters.Add("@RoleId", NpgsqlDbType.Bigint);
- if (RoleId.HasValue) p.Value = (long)RoleId.Value;
- else p.Value = DBNull.Value;
-
- p = c.Parameters.Add("@ChannelId", NpgsqlDbType.Bigint);
- if (AnnounceChannelId.HasValue) p.Value = (long)AnnounceChannelId.Value;
- else p.Value = DBNull.Value;
-
- p = c.Parameters.Add("@TimeZone", NpgsqlDbType.Text);
- if (TimeZone != null) p.Value = TimeZone;
- else p.Value = DBNull.Value;
-
- c.Parameters.Add("@Moderated", NpgsqlDbType.Boolean).Value = IsModerated;
-
- p = c.Parameters.Add("@ModRole", NpgsqlDbType.Bigint);
- if (ModeratorRole.HasValue) p.Value = (long)ModeratorRole.Value;
- else p.Value = DBNull.Value;
-
- p = c.Parameters.Add("@AnnounceMsg", NpgsqlDbType.Text);
- if (AnnounceMessages.Item1 != null) p.Value = AnnounceMessages.Item1;
- else p.Value = DBNull.Value;
-
- p = c.Parameters.Add("@AnnounceMsgPl", NpgsqlDbType.Text);
- if (AnnounceMessages.Item2 != null) p.Value = AnnounceMessages.Item2;
- else p.Value = DBNull.Value;
-
- c.Parameters.Add("@AnnouncePing", NpgsqlDbType.Boolean).Value = AnnouncePing;
-
- await c.PrepareAsync().ConfigureAwait(false);
- await c.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
- #endregion
-}
diff --git a/Data/GuildUserConfiguration.cs b/Data/GuildUserConfiguration.cs
deleted file mode 100644
index 930b264..0000000
--- a/Data/GuildUserConfiguration.cs
+++ /dev/null
@@ -1,139 +0,0 @@
-using Npgsql;
-using NpgsqlTypes;
-using System.Data.Common;
-
-namespace BirthdayBot.Data;
-
-///
-/// Represents configuration for a guild user as may exist in the database.
-///
-[Obsolete(Database.ObsoleteReason, error: false)]
-class GuildUserConfiguration {
- public ulong GuildId { get; }
- public ulong UserId { get; }
-
- ///
- /// Month of birth as a numeric value. Range 1-12.
- ///
- public int BirthMonth { get; private set; }
- ///
- /// Day of birth as a numeric value. Ranges between 1-31 or lower based on month value.
- ///
- public int BirthDay { get; private set; }
-
- public string? TimeZone { get; private set; }
- public bool IsKnown { get { return BirthMonth != 0 && BirthDay != 0; } }
-
- ///
- /// Creates a new, data-less instance without a corresponding database entry.
- /// Calling will create a real database enty
- ///
- private GuildUserConfiguration(ulong guildId, ulong userId) {
- GuildId = guildId;
- UserId = userId;
- }
-
- // Called by GetGuildUsersAsync. Double-check ordinals when changes are made.
- private GuildUserConfiguration(DbDataReader reader) {
- GuildId = (ulong)reader.GetInt64(0);
- UserId = (ulong)reader.GetInt64(1);
- BirthMonth = reader.GetInt32(2);
- BirthDay = reader.GetInt32(3);
- if (!reader.IsDBNull(4)) TimeZone = reader.GetString(4);
- }
-
- ///
- /// Updates user with given information.
- ///
- public async Task UpdateAsync(int month, int day, string? newtz) {
- using (var db = await Database.OpenConnectionAsync().ConfigureAwait(false)) {
- using var c = db.CreateCommand();
- c.CommandText = $"insert into {BackingTable} "
- + "(guild_id, user_id, birth_month, birth_day, time_zone) values "
- + "(@Gid, @Uid, @Month, @Day, @Tz) "
- + "on conflict (guild_id, user_id) do update "
- + "set birth_month = EXCLUDED.birth_month, birth_day = EXCLUDED.birth_day, time_zone = EXCLUDED.time_zone";
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
- c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId;
- c.Parameters.Add("@Month", NpgsqlDbType.Numeric).Value = month;
- c.Parameters.Add("@Day", NpgsqlDbType.Numeric).Value = day;
- var tzp = c.Parameters.Add("@Tz", NpgsqlDbType.Text);
- if (newtz != null) tzp.Value = newtz;
- else tzp.Value = DBNull.Value;
- c.Prepare();
- await c.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
-
- // Database update succeeded; update instance values
- BirthMonth = month;
- BirthDay = day;
- TimeZone = newtz;
- }
-
- ///
- /// Deletes information of this user from the backing database.
- /// The corresponding object reference should ideally be discarded after calling this.
- ///
- public async Task DeleteAsync() {
- using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
- using var c = db.CreateCommand();
- c.CommandText = $"delete from {BackingTable} "
- + "where guild_id = @Gid and user_id = @Uid";
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
- c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId;
- c.Prepare();
- await c.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
-
- #region Database
- public const string BackingTable = "user_birthdays";
- // Take note of ordinals for use in the constructor
- private const string SelectFields = "guild_id, user_id, birth_month, birth_day, time_zone";
-
- internal static async Task DatabaseSetupAsync(NpgsqlConnection db) {
- using var c = db.CreateCommand();
- c.CommandText = $"create table if not exists {BackingTable} ("
- + $"guild_id bigint not null references {GuildConfiguration.BackingTable} ON DELETE CASCADE, "
- + "user_id bigint not null, "
- + "birth_month integer not null, "
- + "birth_day integer not null, "
- + "time_zone text null, "
- + "last_seen timestamptz not null default NOW(), "
- + "PRIMARY KEY (guild_id, user_id)" // index automatically created with this
- + ")";
- await c.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
-
- ///
- /// Attempts to retrieve a user's configuration. Returns a new, updateable instance if none is found.
- ///
- public static async Task LoadAsync(ulong guildId, ulong userId) {
- using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
- using var c = db.CreateCommand();
- c.CommandText = $"select {SelectFields} from {BackingTable} where guild_id = @Gid and user_id = @Uid";
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
- c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
- c.Prepare();
-
- using var r = c.ExecuteReader();
- if (await r.ReadAsync().ConfigureAwait(false)) return new GuildUserConfiguration(r);
- else return new GuildUserConfiguration(guildId, userId);
- }
-
- ///
- /// Gets all known user configuration records associated with the specified guild.
- ///
- public static async Task> LoadAllAsync(ulong guildId) {
- using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
- using var c = db.CreateCommand();
- c.CommandText = $"select {SelectFields} from {BackingTable} where guild_id = @Gid";
- c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
- c.Prepare();
-
- using var r = await c.ExecuteReaderAsync().ConfigureAwait(false);
- var result = new List();
- while (await r.ReadAsync().ConfigureAwait(false)) result.Add(new GuildUserConfiguration(r));
- return result;
- }
- #endregion
-}
diff --git a/Program.cs b/Program.cs
index 05a548a..4581fb7 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,7 +1,4 @@
-using BirthdayBot.Data;
-
-namespace BirthdayBot;
-
+namespace BirthdayBot;
class Program {
private static ShardManager? _bot;
private static readonly DateTimeOffset _botStartTime = DateTimeOffset.UtcNow;
@@ -11,7 +8,7 @@ class Program {
///
public static string BotUptime => (DateTimeOffset.UtcNow - _botStartTime).ToString("d' days, 'hh':'mm':'ss");
- static async Task Main(string[] args) {
+ static async Task Main() {
Configuration? cfg = null;
try {
cfg = new Configuration();
@@ -20,15 +17,6 @@ class Program {
Environment.Exit((int)ExitCodes.ConfigError);
}
- Database.DBConnectionString = new Npgsql.NpgsqlConnectionStringBuilder() {
- Host = cfg.SqlHost ?? "localhost", // default to localhost
- Database = cfg.SqlDatabase,
- Username = cfg.SqlUsername,
- Password = cfg.SqlPassword,
- ApplicationName = cfg.SqlApplicationName,
- MaxPoolSize = Math.Max((int)Math.Ceiling(cfg.ShardAmount * 2 * 0.6), 8)
- }.ToString();
-
Console.CancelKeyPress += OnCancelKeyPressed;
_bot = new ShardManager(cfg);
diff --git a/ShardInstance.cs b/ShardInstance.cs
index f00313a..0ab8cad 100644
--- a/ShardInstance.cs
+++ b/ShardInstance.cs
@@ -1,21 +1,16 @@
using BirthdayBot.ApplicationCommands;
using BirthdayBot.BackgroundServices;
-using BirthdayBot.Data;
using Discord.Interactions;
-using Discord.Net;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
-using static BirthdayBot.TextCommands.CommandsCommon;
namespace BirthdayBot;
-
///
/// Single shard instance for Birthday Bot. This shard independently handles all input and output to Discord.
///
public sealed class ShardInstance : IDisposable {
private readonly ShardManager _manager;
private readonly ShardBackgroundWorker _background;
- private readonly Dictionary _textDispatch;
private readonly InteractionService _interactionService;
private readonly IServiceProvider _services;
@@ -36,15 +31,13 @@ public sealed class ShardInstance : IDisposable {
///
/// Prepares and configures the shard instances, but does not yet start its connection.
///
- internal ShardInstance(ShardManager manager, IServiceProvider services, Dictionary textCmds) {
+ internal ShardInstance(ShardManager manager, IServiceProvider services) {
_manager = manager;
_services = services;
- _textDispatch = textCmds;
DiscordClient = _services.GetRequiredService();
DiscordClient.Log += Client_Log;
DiscordClient.Ready += Client_Ready;
- DiscordClient.MessageReceived += Client_MessageReceived;
_interactionService = _services.GetRequiredService();
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
@@ -105,14 +98,7 @@ public sealed class ShardInstance : IDisposable {
return Task.CompletedTask;
}
- ///
- /// Registers all available slash commands.
- /// Additionally, sets the shard's status to display the help command.
- ///
private async Task Client_Ready() {
- // TODO get rid of this eventually? or change it to something fun...
- await DiscordClient.SetGameAsync("/help");
-
#if !DEBUG
// Update slash/interaction commands
if (ShardId == 0) {
@@ -133,48 +119,6 @@ public sealed class ShardInstance : IDisposable {
#endif
}
-#pragma warning disable CS0618
- ///
- /// Determines if the incoming message is an incoming command, and dispatches to the appropriate handler if necessary.
- ///
- private async Task Client_MessageReceived(SocketMessage msg) {
- if (msg.Channel is not SocketTextChannel channel) return;
- if (msg.Author.IsBot || msg.Author.IsWebhook) return;
- if (((IMessage)msg).Type != MessageType.Default) return;
- var author = (SocketGuildUser)msg.Author;
-
- // Limit 3:
- // For all cases: base command, 2 parameters.
- // Except this case: "bb.config", subcommand name, subcommand parameters in a single string
- var csplit = msg.Content.Split(" ", 3, StringSplitOptions.RemoveEmptyEntries);
- if (csplit.Length > 0 && csplit[0].StartsWith(CommandPrefix, StringComparison.OrdinalIgnoreCase)) {
- // Determine if it's something we're listening for.
- if (!_textDispatch.TryGetValue(csplit[0][CommandPrefix.Length..], out CommandHandler? command)) return;
-
- // Load guild information here
- var gconf = await GuildConfiguration.LoadAsync(channel.Guild.Id, false);
-
- // Ban check
- if (!gconf!.IsBotModerator(author)) // skip check if user is a moderator
- {
- if (await gconf.IsUserBlockedAsync(author.Id)) return; // silently ignore
- }
-
- // Execute the command
- try {
- Log("Command", $"{channel.Guild.Name}/{author.Username}#{author.Discriminator}: {msg.Content}");
- await command(this, gconf, csplit, channel, author);
- } catch (Exception ex) {
- if (ex is HttpException) return;
- Log("Command", ex.ToString());
- try {
- channel.SendMessageAsync(InternalError).Wait();
- } catch (HttpException) { } // Fail silently
- }
- }
- }
-#pragma warning restore CS0618
-
// Slash command preparation and invocation
private async Task DiscordClient_InteractionCreated(SocketInteraction arg) {
var context = new SocketInteractionContext(DiscordClient, arg);
@@ -207,7 +151,7 @@ public sealed class ShardInstance : IDisposable {
// Specific responses to errors, if necessary
if (result.Error == InteractionCommandError.UnmetPrecondition) {
- string errReply = result.ErrorReason switch {
+ var errReply = result.ErrorReason switch {
RequireBotModeratorAttribute.Error => RequireBotModeratorAttribute.Reply,
EnforceBlockingAttribute.FailBlocked => EnforceBlockingAttribute.ReplyBlocked,
EnforceBlockingAttribute.FailModerated => EnforceBlockingAttribute.ReplyModerated,
diff --git a/ShardManager.cs b/ShardManager.cs
index 00bf8c4..dda1d6b 100644
--- a/ShardManager.cs
+++ b/ShardManager.cs
@@ -1,14 +1,11 @@
global using Discord;
global using Discord.WebSocket;
using BirthdayBot.BackgroundServices;
-using BirthdayBot.TextCommands;
using Discord.Interactions;
using Microsoft.Extensions.DependencyInjection;
using System.Text;
-using static BirthdayBot.TextCommands.CommandsCommon;
namespace BirthdayBot;
-
///
/// More or less the main class for the program. Handles individual shards and provides frequent
/// status reports regarding the overall health of the application.
@@ -45,8 +42,6 @@ class ShardManager : IDisposable {
///
private readonly Dictionary _shards;
- private readonly Dictionary _textCommands;
-
private readonly Task _statusTask;
private readonly CancellationTokenSource _mainCancel;
private int _destroyedShards = 0;
@@ -59,20 +54,9 @@ class ShardManager : IDisposable {
Config = cfg;
- // Command handler setup
- _textCommands = new Dictionary(StringComparer.OrdinalIgnoreCase);
- var cmdsUser = new UserCommands(cfg);
- foreach (var item in cmdsUser.Commands) _textCommands.Add(item.Item1, item.Item2);
- var cmdsListing = new ListingCommands(cfg);
- foreach (var item in cmdsListing.Commands) _textCommands.Add(item.Item1, item.Item2);
- var cmdsHelp = new TextCommands.HelpInfoCommands(cfg);
- foreach (var item in cmdsHelp.Commands) _textCommands.Add(item.Item1, item.Item2);
- var cmdsMods = new ManagerCommands(cfg, cmdsUser.Commands);
- foreach (var item in cmdsMods.Commands) _textCommands.Add(item.Item1, item.Item2);
-
// Allocate shards based on configuration
_shards = new Dictionary();
- for (int i = Config.ShardStart; i < (Config.ShardStart + Config.ShardAmount); i++) {
+ for (var i = Config.ShardStart; i < (Config.ShardStart + Config.ShardAmount); i++) {
_shards.Add(i, null);
}
@@ -114,12 +98,12 @@ class ShardManager : IDisposable {
TotalShards = Config.ShardTotal,
LogLevel = LogSeverity.Info,
DefaultRetryMode = RetryMode.Retry502 | RetryMode.RetryTimeouts,
- GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages,
+ GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers,
SuppressUnknownDispatchWarnings = true,
LogGatewayIntentWarnings = false
};
var services = new ServiceCollection()
- .AddSingleton(s => new ShardInstance(this, s, _textCommands))
+ .AddSingleton(s => new ShardInstance(this, s))
.AddSingleton(s => new DiscordSocketClient(clientConf))
.AddSingleton(s => new InteractionService(s.GetRequiredService()))
.BuildServiceProvider();
@@ -144,7 +128,7 @@ class ShardManager : IDisposable {
public string? ExecutingTask;
}
- private string StatusDisplay(IEnumerable guildList, Dictionary guildInfo, bool showDetail) {
+ private static string StatusDisplay(IEnumerable guildList, Dictionary guildInfo, bool showDetail) {
if (!guildList.Any()) return "--";
var result = new StringBuilder();
foreach (var item in guildList) {
@@ -223,7 +207,7 @@ class ShardManager : IDisposable {
Program.ProgramStop();
} else {
// Start up any missing shards
- int startAllowance = MaxConcurrentOperations;
+ var startAllowance = MaxConcurrentOperations;
foreach (var id in nullShards) {
// To avoid possible issues with resources strained over so many shards starting at once,
// initialization is spread out by only starting a few at a time.
diff --git a/TextCommands/CommandDocumentation.cs b/TextCommands/CommandDocumentation.cs
deleted file mode 100644
index a0af16d..0000000
--- a/TextCommands/CommandDocumentation.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System.Text;
-
-namespace BirthdayBot.TextCommands;
-
-internal class CommandDocumentation {
- public string[] Commands { get; }
- public string Usage { get; }
- public string? Examples { get; }
-
- public CommandDocumentation(IEnumerable commands, string usage, string? examples) {
- var cmds = new List();
- foreach (var item in commands) cmds.Add(CommandsCommon.CommandPrefix + item);
- if (cmds.Count == 0) throw new ArgumentException(null, nameof(commands));
- Commands = cmds.ToArray();
- Usage = usage ?? throw new ArgumentException(null, nameof(usage));
- Examples = examples;
- }
-
- ///
- /// Returns a string that can be inserted into a help or usage message.
- ///
- public string Export() {
- var result = new StringBuilder();
- foreach (var item in Commands) result.Append(", `" + item + "`");
- result.Remove(0, 2);
- result.Insert(0, '●');
- result.AppendLine();
- result.Append("» " + Usage);
- if (Examples != null) {
- result.AppendLine();
- result.Append("» Examples: " + Examples);
- }
- return result.ToString();
- }
-
- ///
- /// Creates an embeddable message containing the command documentation.
- ///
- public Embed UsageEmbed => new EmbedBuilder() {
- Author = new EmbedAuthorBuilder() { Name = "Usage" },
- Description = Export()
- }.Build();
-}
diff --git a/TextCommands/CommandsCommon.cs b/TextCommands/CommandsCommon.cs
deleted file mode 100644
index 1cf9d5f..0000000
--- a/TextCommands/CommandsCommon.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-#pragma warning disable CS0618
-using BirthdayBot.Data;
-using NodaTime;
-using System.Collections.ObjectModel;
-using System.Text.RegularExpressions;
-
-namespace BirthdayBot.TextCommands;
-
-///
-/// Common base class for common constants and variables.
-///
-internal abstract class CommandsCommon {
-#if DEBUG
- public const string CommandPrefix = "bt.";
-#else
- public const string CommandPrefix = "bb.";
-#endif
- public const string BadUserError = ":x: Unable to find user. Specify their `@` mention or their ID.";
- public const string ParameterError = ":x: Invalid usage. Refer to how to use the command and try again.";
- public const string NoParameterError = ":x: This command does not accept any parameters.";
- public const string MemberCacheEmptyError = ":warning: Please try the command again.";
-
- public delegate Task CommandHandler(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser);
-
- protected static ReadOnlyDictionary TzNameMap { get; }
- protected static Regex ChannelMention { get; } = new Regex(@"<#(\d+)>");
- protected static Regex UserMention { get; } = new Regex(@"\!?(\d+)>");
-
- protected Configuration BotConfig { get; }
-
- protected CommandsCommon(Configuration db) {
- BotConfig = db;
- }
-
- static CommandsCommon() {
- var dict = new Dictionary(StringComparer.OrdinalIgnoreCase);
- foreach (var name in DateTimeZoneProviders.Tzdb.Ids) dict.Add(name, name);
- TzNameMap = new(dict);
- }
-
- ///
- /// On command dispatcher initialization, it will retrieve all available commands through here.
- ///
- public abstract IEnumerable<(string, CommandHandler)> Commands { get; }
-
- ///
- /// Checks given time zone input. Returns a valid string for use with NodaTime.
- ///
- protected static string ParseTimeZone(string tzinput) {
- if (!TzNameMap.TryGetValue(tzinput, out string? tz)) throw new FormatException(":x: Unexpected time zone name."
- + $" Refer to `{CommandPrefix}help-tzdata` to help determine the correct value.");
- return tz;
- }
-
- ///
- /// Given user input where a user-like parameter is expected, attempts to resolve to an ID value.
- /// Input must be a mention or explicit ID. No name resolution is done here.
- ///
- protected static bool TryGetUserId(string input, out ulong result) {
- string doParse;
- var m = UserMention.Match(input);
- if (m.Success) doParse = m.Groups[1].Value;
- else doParse = input;
-
- if (ulong.TryParse(doParse, out ulong resultVal)) {
- result = resultVal;
- return true;
- }
-
- result = default;
- return false;
- }
-
- ///
- /// An alternative to to be called by command handlers needing a full member cache.
- /// Creates a download request if necessary.
- ///
- ///
- /// True if the member cache is already filled, false otherwise.
- ///
- ///
- /// Any updates to the member cache aren't accessible until the event handler finishes execution, meaning proactive downloading
- /// is necessary, and is handled by . In situations where
- /// this approach fails, this is to be called, and the user must be asked to attempt the command again if this returns false.
- ///
- protected static async Task HasMemberCacheAsync(SocketGuild guild) {
- if (Common.HasMostMembersDownloaded(guild)) return true;
- // Event handling thread hangs if awaited normally or used with Task.Run
- await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false);
- return false;
- }
-}
diff --git a/TextCommands/HelpInfoCommands.cs b/TextCommands/HelpInfoCommands.cs
deleted file mode 100644
index 22314c5..0000000
--- a/TextCommands/HelpInfoCommands.cs
+++ /dev/null
@@ -1,164 +0,0 @@
-#pragma warning disable CS0618
-using BirthdayBot.Data;
-using System.Text;
-
-namespace BirthdayBot.TextCommands;
-
-internal class HelpInfoCommands : CommandsCommon {
- private readonly Embed _helpEmbed;
- private readonly Embed _helpConfigEmbed;
-
- public HelpInfoCommands(Configuration cfg) : base(cfg) {
- var embeds = BuildHelpEmbeds();
- _helpEmbed = embeds.Item1;
- _helpConfigEmbed = embeds.Item2;
- }
-
- public override IEnumerable<(string, CommandHandler)> Commands =>
- new List<(string, CommandHandler)>() {
- ("help", CmdHelp),
- ("help-config", CmdHelpConfig),
- ("helpconfig", CmdHelpConfig),
- ("help-tzdata", CmdHelpTzdata),
- ("helptzdata", CmdHelpTzdata),
- ("help-message", CmdHelpMessage),
- ("helpmessage", CmdHelpMessage),
- ("info", CmdInfo),
- ("about", CmdInfo),
- ("invite", CmdInfo)
- };
-
- private static (Embed, Embed) BuildHelpEmbeds() {
- var cpfx = $"●`{CommandPrefix}";
-
- // Normal section
- var cmdField = new EmbedFieldBuilder() {
- Name = "Commands",
- Value = "----\n**Notice**: Text commands will soon be __removed__. "
- + "For a list of new commands, see this bot's `/help` command.\n----\n"
- + $"{cpfx}help`, `{CommandPrefix}info`, `{CommandPrefix}help-tzdata`\n"
- + $" » Help and informational messages.\n"
- + ListingCommands.DocUpcoming.Export() + "\n"
- + UserCommands.DocSet.Export() + "\n"
- + UserCommands.DocZone.Export() + "\n"
- + UserCommands.DocRemove.Export() + "\n"
- + ListingCommands.DocWhen.Export()
- };
- var cmdModField = new EmbedFieldBuilder() {
- Name = "Moderator actions",
- Value = $"{cpfx}config`\n"
- + $" » Edit bot configuration. See `{CommandPrefix}help-config`.\n"
- + ListingCommands.DocList.Export() + "\n"
- + ManagerCommands.DocOverride.Export()
- };
- var helpRegular = new EmbedBuilder().AddField(cmdField).AddField(cmdModField);
-
- // Manager section
- var mpfx = cpfx + "config ";
- var configField1 = new EmbedFieldBuilder() {
- Name = "Basic settings",
- Value = $"{mpfx}role (role name or ID)`\n"
- + " » Sets the role to apply to users having birthdays.\n"
- + $"{mpfx}channel (channel name or ID)`\n"
- + " » Sets the announcement channel. Leave blank to disable.\n"
- + $"{mpfx}message (message)`, `{CommandPrefix}config messagepl (message)`\n"
- + $" » Sets a custom announcement message. See `{CommandPrefix}help-message`.\n"
- + $"{mpfx}ping (off|on)`\n"
- + $" » Sets whether to ping the respective users in the announcement message.\n"
- + $"{mpfx}zone (time zone name)`\n"
- + $" » Sets the default server time zone. See `{CommandPrefix}help-tzdata`."
- };
- var configField2 = new EmbedFieldBuilder() {
- Name = "Access management",
- Value = $"{mpfx}modrole (role name, role ping, or ID)`\n"
- + " » Establishes a role for bot moderators. Grants access to `bb.config` and `bb.override`.\n"
- + $"{mpfx}block/unblock (user ping or ID)`\n"
- + " » Prevents or allows usage of bot commands to the given user.\n"
- + $"{mpfx}moderated on/off`\n"
- + " » Prevents or allows using commands for all members excluding moderators."
- };
-
- var helpConfig = new EmbedBuilder() {
- Author = new EmbedAuthorBuilder() { Name = $"{CommandPrefix} config subcommands" },
- Description = "All the following subcommands are only usable by moderators and server managers."
- }.AddField(configField1).AddField(configField2);
-
- return (helpRegular.Build(), helpConfig.Build());
- }
-
- private async Task CmdHelp(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
- => await reqChannel.SendMessageAsync(embed: _helpEmbed).ConfigureAwait(false);
-
- private async Task CmdHelpConfig(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
- => await reqChannel.SendMessageAsync(embed: _helpConfigEmbed).ConfigureAwait(false);
-
- private async Task CmdHelpTzdata(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- const string tzhelp = "You may specify a time zone in order to have your birthday recognized with respect to your local time. "
- + "This bot only accepts zone names from the IANA Time Zone Database (a.k.a. Olson Database).\n\n"
- + "To find your zone: https://xske.github.io/tz/" + "\n"
- + "Interactive map: https://kevinnovak.github.io/Time-Zone-Picker/" + "\n"
- + "Complete list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones";
- var embed = new EmbedBuilder();
- embed.AddField(new EmbedFieldBuilder() {
- Name = "Time Zone Support",
- Value = tzhelp
- });
- await reqChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
- }
-
- private async Task CmdHelpMessage(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- const string msghelp = "The `message` and `messagepl` subcommands allow for editing the message sent into the announcement "
- + "channel (defined with `{0}config channel`). This feature is separated across two commands:\n"
- + "●`{0}config message`\n"
- + "●`{0}config messagepl`\n"
- + "The first command sets the message to be displayed when *one* user is having a birthday. The second command sets the "
- + "message for when *two or more* users are having birthdays ('pl' means plural). If only one of the two custom messages "
- + "are defined, it will be used for both cases.\n\n"
- + "To further allow customization, you may place the token `%n` in your message to specify where the name(s) should appear.\n"
- + "Leave the parameter blank to clear or reset the message to its default value.";
- const string msghelp2 = "As examples, these are the default announcement messages used by this bot:\n"
- + "`message`: {0}\n" + "`messagepl`: {1}";
- var embed = new EmbedBuilder().AddField(new EmbedFieldBuilder() {
- Name = "Custom announcement message",
- Value = string.Format(msghelp, CommandPrefix)
- }).AddField(new EmbedFieldBuilder() {
- Name = "Examples",
- Value = string.Format(msghelp2,
- BackgroundServices.BirthdayRoleUpdate.DefaultAnnounce, BackgroundServices.BirthdayRoleUpdate.DefaultAnnouncePl)
- });
- await reqChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
- }
-
- private async Task CmdInfo(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- var strStats = new StringBuilder();
- var asmnm = System.Reflection.Assembly.GetExecutingAssembly().GetName();
- strStats.AppendLine("BirthdayBot v" + asmnm.Version!.ToString(3));
- //strStats.AppendLine("Server count: " + Discord.Guilds.Count.ToString()); // TODO restore this statistic
- strStats.AppendLine("Shard #" + instance.ShardId.ToString("00"));
- strStats.AppendLine("Uptime: " + Program.BotUptime);
-
- // TODO fun stats
- // current birthdays, total names registered, unique time zones
-
- var embed = new EmbedBuilder() {
- Author = new EmbedAuthorBuilder() {
- Name = "Thank you for using Birthday Bot!",
- IconUrl = instance.DiscordClient.CurrentUser.GetAvatarUrl()
- },
- Description = "For more information regarding support, data retention, privacy, and other details, please refer to: "
- + "https://github.com/NoiTheCat/BirthdayBot/blob/master/Readme.md" + "\n\n"
- + "This bot is provided for free, without any paywalled 'premium' features. "
- + "If you've found this bot useful, please consider contributing via the "
- + "bot author's page on Ko-fi: https://ko-fi.com/noithecat."
- }.AddField(new EmbedFieldBuilder() {
- Name = "Statistics",
- Value = strStats.ToString()
- });
- await reqChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
- }
-}
diff --git a/TextCommands/ListingCommands.cs b/TextCommands/ListingCommands.cs
deleted file mode 100644
index 6bc870f..0000000
--- a/TextCommands/ListingCommands.cs
+++ /dev/null
@@ -1,313 +0,0 @@
-#pragma warning disable CS0618
-using BirthdayBot.Data;
-using System.Text;
-
-namespace BirthdayBot.TextCommands;
-
-///
-/// Commands for listing upcoming and all birthdays.
-///
-internal class ListingCommands : CommandsCommon {
- public ListingCommands(Configuration db) : base(db) { }
-
- public override IEnumerable<(string, CommandHandler)> Commands
- => new List<(string, CommandHandler)>()
- {
- ("list", CmdList),
- ("upcoming", CmdUpcoming),
- ("recent", CmdUpcoming),
- ("when", CmdWhen)
- };
-
- #region Documentation
- public static readonly CommandDocumentation DocList =
- new(new string[] { "list" }, "Exports all birthdays to a file."
- + " Accepts `csv` as an optional parameter.", null);
- public static readonly CommandDocumentation DocUpcoming =
- new(new string[] { "recent", "upcoming" }, "Lists recent and upcoming birthdays.", null);
- public static readonly CommandDocumentation DocWhen =
- new(new string[] { "when" }, "Displays the given user's birthday information.", null);
- #endregion
-
- private async Task CmdWhen(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- if (!await HasMemberCacheAsync(reqChannel.Guild)) {
- await reqChannel.SendMessageAsync(MemberCacheEmptyError);
- return;
- }
-
- // Requires a parameter
- if (param.Length == 1) {
- await reqChannel.SendMessageAsync(ParameterError, embed: DocWhen.UsageEmbed).ConfigureAwait(false);
- return;
- }
-
- var search = param[1];
- if (param.Length == 3) {
- // param maxes out at 3 values. param[2] might contain part of the search string (if name has a space)
- search += " " + param[2];
- }
-
- SocketGuildUser? searchTarget = null;
-
- if (!TryGetUserId(search, out ulong searchId)) // ID lookup
- {
- // name lookup without discriminator
- foreach (var searchuser in reqChannel.Guild.Users) {
- if (string.Equals(search, searchuser.Username, StringComparison.OrdinalIgnoreCase)) {
- searchTarget = searchuser;
- break;
- }
- }
- } else {
- searchTarget = reqChannel.Guild.GetUser(searchId);
- }
- if (searchTarget == null) {
- await reqChannel.SendMessageAsync(BadUserError, embed: DocWhen.UsageEmbed).ConfigureAwait(false);
- return;
- }
-
- var searchTargetData = await GuildUserConfiguration.LoadAsync(reqChannel.Guild.Id, searchId).ConfigureAwait(false);
- if (!searchTargetData.IsKnown) {
- await reqChannel.SendMessageAsync("I do not have birthday information for that user.").ConfigureAwait(false);
- return;
- }
-
- string result = Common.FormatName(searchTarget, false);
- result += ": ";
- result += $"`{searchTargetData.BirthDay:00}-{Common.MonthNames[searchTargetData.BirthMonth]}`";
- result += searchTargetData.TimeZone == null ? "" : $" - `{searchTargetData.TimeZone}`";
-
- await reqChannel.SendMessageAsync(result).ConfigureAwait(false);
- }
-
- // Creates a file with all birthdays.
- private async Task CmdList(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- // For now, we're restricting this command to moderators only. This may turn into an option later.
- if (!gconf.IsBotModerator(reqUser)) {
- // Do not add detailed usage information to this error message.
- await reqChannel.SendMessageAsync(":x: Only bot moderators may use this command.").ConfigureAwait(false);
- return;
- }
-
- if (!await HasMemberCacheAsync(reqChannel.Guild)) {
- await reqChannel.SendMessageAsync(MemberCacheEmptyError);
- return;
- }
-
- bool useCsv = false;
- // Check for CSV option
- if (param.Length == 2) {
- if (param[1].ToLower() == "csv") useCsv = true;
- else {
- await reqChannel.SendMessageAsync(":x: That is not available as an export format.", embed: DocList.UsageEmbed)
- .ConfigureAwait(false);
- return;
- }
- } else if (param.Length > 2) {
- await reqChannel.SendMessageAsync(ParameterError, embed: DocList.UsageEmbed).ConfigureAwait(false);
- return;
- }
-
- var bdlist = await GetSortedUsersAsync(reqChannel.Guild).ConfigureAwait(false);
-
- var filepath = Path.GetTempPath() + "birthdaybot-" + reqChannel.Guild.Id;
- string fileoutput;
- if (useCsv) {
- fileoutput = ListExportCsv(reqChannel, bdlist);
- filepath += ".csv";
- } else {
- fileoutput = ListExportNormal(reqChannel, bdlist);
- filepath += ".txt.";
- }
- await File.WriteAllTextAsync(filepath, fileoutput, Encoding.UTF8).ConfigureAwait(false);
-
- try {
- await reqChannel.SendFileAsync(filepath, $"Exported {bdlist.Count} birthdays to file.").ConfigureAwait(false);
- } catch (Discord.Net.HttpException) {
- reqChannel.SendMessageAsync(":x: Unable to send list due to a permissions issue. Check the 'Attach Files' permission.").Wait();
- } catch (Exception ex) {
- Program.Log("Listing", ex.ToString());
- reqChannel.SendMessageAsync(ShardInstance.InternalError).Wait();
- } finally {
- File.Delete(filepath);
- }
- }
-
- // "Recent and upcoming birthdays"
- // The 'recent' bit removes time zone ambiguity and spares us from extra time zone processing here
- private async Task CmdUpcoming(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- if (!await HasMemberCacheAsync(reqChannel.Guild)) {
- await reqChannel.SendMessageAsync(MemberCacheEmptyError);
- return;
- }
-
- var now = DateTimeOffset.UtcNow;
- var search = DateIndex(now.Month, now.Day) - 8; // begin search 8 days prior to current date UTC
- if (search <= 0) search = 366 - Math.Abs(search);
-
- var query = await GetSortedUsersAsync(reqChannel.Guild).ConfigureAwait(false);
-
- var output = new StringBuilder();
- var resultCount = 0;
- output.AppendLine("Recent and upcoming birthdays:");
- for (int count = 0; count <= 21; count++) // cover 21 days total (7 prior, current day, 14 upcoming)
- {
- var results = from item in query
- where item.DateIndex == search
- select item;
-
- // push up search by 1 now, in case we back out early
- search += 1;
- if (search > 366) search = 1; // wrap to beginning of year
-
- if (!results.Any()) continue; // back out early
- resultCount += results.Count();
-
- // Build sorted name list
- var names = new List();
- foreach (var item in results) {
- names.Add(item.DisplayName);
- }
- names.Sort(StringComparer.OrdinalIgnoreCase);
-
- var first = true;
- output.AppendLine();
- output.Append($"● `{Common.MonthNames[results.First().BirthMonth]}-{results.First().BirthDay:00}`: ");
- foreach (var item in names) {
- // If the output is starting to fill up, send out this message and prepare a new one.
- if (output.Length > 800) {
- await reqChannel.SendMessageAsync(output.ToString()).ConfigureAwait(false);
- output.Clear();
- first = true;
- output.Append($"● `{Common.MonthNames[results.First().BirthMonth]}-{results.First().BirthDay:00}`: ");
- }
-
- if (first) first = false;
- else output.Append(", ");
- output.Append(item);
- }
- }
-
- if (resultCount == 0)
- await reqChannel.SendMessageAsync(
- "There are no recent or upcoming birthdays (within the last 7 days and/or next 14 days).")
- .ConfigureAwait(false);
- else
- await reqChannel.SendMessageAsync(output.ToString()).ConfigureAwait(false);
- }
-
- ///
- /// 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 async Task> GetSortedUsersAsync(SocketGuild guild) {
- using var db = await Database.OpenConnectionAsync();
- using var c = db.CreateCommand();
- c.CommandText = "select user_id, birth_month, birth_day from " + GuildUserConfiguration.BackingTable
- + " where guild_id = @Gid order by birth_month, birth_day";
- c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guild.Id;
- c.Prepare();
- using var r = await c.ExecuteReaderAsync();
- var result = new List();
- while (await r.ReadAsync()) {
- var id = (ulong)r.GetInt64(0);
- var month = r.GetInt32(1);
- var day = r.GetInt32(2);
-
- var guildUser = guild.GetUser(id);
- if (guildUser == null) continue; // Skip user not in guild
-
- result.Add(new ListItem() {
- BirthMonth = month,
- BirthDay = day,
- DateIndex = DateIndex(month, day),
- UserId = guildUser.Id,
- DisplayName = Common.FormatName(guildUser, false)
- });
- }
- return result;
- }
-
- private string ListExportNormal(SocketGuildChannel channel, IEnumerable list) {
- // Output: "● Mon-dd: (user ID) Username [ - Nickname: (nickname)]"
- var result = new StringBuilder();
- result.AppendLine("Birthdays in " + channel.Guild.Name);
- result.AppendLine();
- foreach (var item in list) {
- var user = channel.Guild.GetUser(item.UserId);
- if (user == null) continue; // User disappeared in the instant between getting list and processing
- result.Append($"● {Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}: ");
- result.Append(item.UserId);
- result.Append(" " + user.Username + "#" + user.Discriminator);
- if (user.Nickname != null) result.Append(" - Nickname: " + user.Nickname);
- result.AppendLine();
- }
- return result.ToString();
- }
-
- private string ListExportCsv(SocketGuildChannel channel, IEnumerable list) {
- // Output: User ID, Username, Nickname, Month-Day, Month, Day
- var result = new StringBuilder();
-
- // Conforming to RFC 4180; with header
- result.Append("UserId,Username,Nickname,MonthDayDisp,Month,Day");
- result.Append("\r\n"); // crlf line break is specified by the standard
- foreach (var item in list) {
- var user = channel.Guild.GetUser(item.UserId);
- if (user == null) continue; // User disappeared in the instant between getting list and processing
- result.Append(item.UserId);
- result.Append(',');
- result.Append(CsvEscape(user.Username + "#" + user.Discriminator));
- result.Append(',');
- if (user.Nickname != null) result.Append(user.Nickname);
- result.Append(',');
- result.Append($"{Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}");
- result.Append(',');
- result.Append(item.BirthMonth);
- result.Append(',');
- result.Append(item.BirthDay);
- result.Append("\r\n");
- }
- return result.ToString();
- }
-
- 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;
- }
-}
diff --git a/TextCommands/ManagerCommands.cs b/TextCommands/ManagerCommands.cs
deleted file mode 100644
index 696972c..0000000
--- a/TextCommands/ManagerCommands.cs
+++ /dev/null
@@ -1,487 +0,0 @@
-#pragma warning disable CS0618
-using BirthdayBot.Data;
-using System.Text;
-using System.Text.RegularExpressions;
-
-namespace BirthdayBot.TextCommands;
-
-internal class ManagerCommands : CommandsCommon {
- private static readonly string ConfErrorPostfix =
- $" Refer to the `{CommandPrefix}help-config` command for information on this command's usage.";
- private delegate Task ConfigSubcommand(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel);
-
- private readonly Dictionary _subcommands;
- private readonly Dictionary _usercommands;
-
- public ManagerCommands(Configuration db, IEnumerable<(string, CommandHandler)> userCommands) : base(db) {
- _subcommands = new Dictionary(StringComparer.OrdinalIgnoreCase)
- {
- { "role", ScmdRole },
- { "channel", ScmdChannel },
- { "modrole", ScmdModRole },
- { "message", ScmdAnnounceMsg },
- { "messagepl", ScmdAnnounceMsg },
- { "ping", ScmdPing },
- { "zone", ScmdZone },
- { "block", ScmdBlock },
- { "unblock", ScmdBlock },
- { "moderated", ScmdModerated }
- };
-
- // Set up local copy of all user commands accessible by the override command
- _usercommands = new Dictionary(StringComparer.OrdinalIgnoreCase);
- foreach (var item in userCommands) _usercommands.Add(item.Item1, item.Item2);
- }
-
- public override IEnumerable<(string, CommandHandler)> Commands
- => new List<(string, CommandHandler)>()
- {
- ("config", CmdConfigDispatch),
- ("override", CmdOverride),
- ("check", CmdCheck),
- ("test", CmdCheck)
- };
-
- #region Documentation
- public static readonly CommandDocumentation DocOverride =
- new(new string[] { "override (user ping or ID) (command w/ parameters)" },
- "Perform certain commands on behalf of another user.", null);
- #endregion
-
- private async Task CmdConfigDispatch(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- // Ignore those without the proper permissions.
- if (!gconf.IsBotModerator(reqUser)) {
- await reqChannel.SendMessageAsync(":x: This command may only be used by bot moderators.").ConfigureAwait(false);
- return;
- }
-
- if (param.Length < 2) {
- await reqChannel.SendMessageAsync($":x: See `{CommandPrefix}help-config` for information on how to use this command.")
- .ConfigureAwait(false);
- return;
- }
-
- // Special case: Restrict 'modrole' to only guild managers, not mods
- if (string.Equals(param[1], "modrole", StringComparison.OrdinalIgnoreCase) && !reqUser.GuildPermissions.ManageGuild) {
- await reqChannel.SendMessageAsync(":x: This command may only be used by those with the `Manage Server` permission.")
- .ConfigureAwait(false);
- return;
- }
-
- // Subcommands get a subset of the parameters, to make things a little easier.
- var confparam = new string[param.Length - 1];
- Array.Copy(param, 1, confparam, 0, param.Length - 1);
-
- if (_subcommands.TryGetValue(confparam[0], out var h)) {
- await h(confparam, gconf, reqChannel).ConfigureAwait(false);
- }
- }
-
- #region Configuration sub-commands
- // Birthday role set
- private async Task ScmdRole(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel) {
- if (param.Length != 2) {
- await reqChannel.SendMessageAsync(":x: A role name, role mention, or ID value must be specified.")
- .ConfigureAwait(false);
- return;
- }
- var guild = reqChannel.Guild;
- var role = FindUserInputRole(param[1], guild);
-
- if (role == null) {
- await reqChannel.SendMessageAsync(RoleInputError).ConfigureAwait(false);
- } else if (role.Id == reqChannel.Guild.EveryoneRole.Id) {
- await reqChannel.SendMessageAsync(":x: You cannot set that as the birthday role.").ConfigureAwait(false);
- } else {
- gconf.RoleId = role.Id;
- await gconf.UpdateAsync().ConfigureAwait(false);
- await reqChannel.SendMessageAsync($":white_check_mark: The birthday role has been set as **{role.Name}**.")
- .ConfigureAwait(false);
- }
- }
-
- // Ping setting
- private async Task ScmdPing(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel) {
- const string InputErr = ":x: You must specify either `off` or `on` in this setting.";
- if (param.Length != 2) {
- await reqChannel.SendMessageAsync(InputErr).ConfigureAwait(false);
- return;
- }
-
- var input = param[1].ToLower();
- bool setting;
- string result;
- if (input == "off") {
- setting = false;
- result = ":white_check_mark: Announcement pings are now **off**.";
- } else if (input == "on") {
- setting = true;
- result = ":white_check_mark: Announcement pings are now **on**.";
- } else {
- await reqChannel.SendMessageAsync(InputErr).ConfigureAwait(false);
- return;
- }
-
- gconf.AnnouncePing = setting;
- await gconf.UpdateAsync().ConfigureAwait(false);
- await reqChannel.SendMessageAsync(result).ConfigureAwait(false);
- }
-
- // Announcement channel set
- private async Task ScmdChannel(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel) {
- if (param.Length == 1) // No extra parameter. Unset announcement channel.
- {
- // Extra detail: Show a unique message if a channel hadn't been set prior.
- if (!gconf.AnnounceChannelId.HasValue) {
- await reqChannel.SendMessageAsync(":x: There is no announcement channel set. Nothing to unset.")
- .ConfigureAwait(false);
- return;
- }
-
- gconf.AnnounceChannelId = null;
- await gconf.UpdateAsync();
- await reqChannel.SendMessageAsync(":white_check_mark: The announcement channel has been unset.")
- .ConfigureAwait(false);
- } else {
- // Determine channel from input
- ulong chId = 0;
-
- // Try channel mention
- var m = ChannelMention.Match(param[1]);
- if (m.Success) {
- chId = ulong.Parse(m.Groups[1].Value);
- } else if (ulong.TryParse(param[1], out chId)) {
- // Continue...
- } else {
- // Try text-based search
- var res = reqChannel.Guild.TextChannels
- .FirstOrDefault(ch => string.Equals(ch.Name, param[1], StringComparison.OrdinalIgnoreCase));
- if (res != null) {
- chId = res.Id; // Yep, we're throwing the full result away only to go look for it again later...
- }
- }
-
- // Attempt to find channel in guild
- SocketTextChannel? chTt = null;
- if (chId != 0) chTt = reqChannel.Guild.GetTextChannel(chId);
- if (chTt == null) {
- await reqChannel.SendMessageAsync(":x: Unable to find the specified channel.").ConfigureAwait(false);
- return;
- }
-
- // Update the value
- gconf.AnnounceChannelId = chId;
- await gconf.UpdateAsync().ConfigureAwait(false);
-
- // Report the success
- await reqChannel.SendMessageAsync($":white_check_mark: The announcement channel is now set to <#{chId}>.")
- .ConfigureAwait(false);
- }
- }
-
- // Moderator role set
- private async Task ScmdModRole(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel) {
- if (param.Length != 2) {
- await reqChannel.SendMessageAsync(":x: A role name, role mention, or ID value must be specified.")
- .ConfigureAwait(false);
- return;
- }
- var guild = reqChannel.Guild;
- var role = FindUserInputRole(param[1], guild);
-
- if (role == null) {
- await reqChannel.SendMessageAsync(RoleInputError).ConfigureAwait(false);
- } else {
- gconf.ModeratorRole = role.Id;
- await gconf.UpdateAsync().ConfigureAwait(false);
- await reqChannel.SendMessageAsync($":white_check_mark: The moderator role is now **{role.Name}**.")
- .ConfigureAwait(false);
- }
- }
-
- // Guild default time zone set/unset
- private async Task ScmdZone(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel) {
- if (param.Length == 1) // No extra parameter. Unset guild default time zone.
- {
- // Extra detail: Show a unique message if there is no set zone.
- if (!gconf.AnnounceChannelId.HasValue) {
- await reqChannel.SendMessageAsync(":x: A default zone is not set. Nothing to unset.").ConfigureAwait(false);
- return;
- }
-
- gconf.TimeZone = null;
- await gconf.UpdateAsync().ConfigureAwait(false);
- await reqChannel.SendMessageAsync(":white_check_mark: The default time zone preference has been removed.")
- .ConfigureAwait(false);
- } else {
- // Parameter check.
- string zone;
- try {
- zone = ParseTimeZone(param[1]);
- } catch (FormatException ex) {
- reqChannel.SendMessageAsync(ex.Message).Wait();
- return;
- }
-
- // Update value
- gconf.TimeZone = zone;
- await gconf.UpdateAsync().ConfigureAwait(false);
-
- // Report the success
- await reqChannel.SendMessageAsync($":white_check_mark: The server's time zone has been set to **{zone}**.")
- .ConfigureAwait(false);
- }
- }
-
- // Block/unblock individual non-manager users from using commands.
- private async Task ScmdBlock(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel) {
- if (param.Length != 2) {
- await reqChannel.SendMessageAsync(ParameterError + ConfErrorPostfix).ConfigureAwait(false);
- return;
- }
-
- bool doBan = param[0].ToLower() == "block"; // true = block, false = unblock
-
- if (!TryGetUserId(param[1], out ulong inputId)) {
- await reqChannel.SendMessageAsync(BadUserError).ConfigureAwait(false);
- return;
- }
-
- var isBanned = await gconf.IsUserBlockedAsync(inputId).ConfigureAwait(false);
- if (doBan) {
- if (!isBanned) {
- await gconf.BlockUserAsync(inputId).ConfigureAwait(false);
- await reqChannel.SendMessageAsync(":white_check_mark: User has been blocked.").ConfigureAwait(false);
- } else {
- // TODO bug: this is incorrectly always displayed when in moderated mode
- await reqChannel.SendMessageAsync(":white_check_mark: User is already blocked.").ConfigureAwait(false);
- }
- } else {
- if (await gconf.UnblockUserAsync(inputId).ConfigureAwait(false)) {
- await reqChannel.SendMessageAsync(":white_check_mark: User is now unblocked.").ConfigureAwait(false);
- } else {
- await reqChannel.SendMessageAsync(":white_check_mark: The specified user is not blocked.").ConfigureAwait(false);
- }
- }
- }
-
- // "moderated on/off" - Sets/unsets moderated mode.
- private async Task ScmdModerated(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel) {
- if (param.Length != 2) {
- await reqChannel.SendMessageAsync(ParameterError + ConfErrorPostfix).ConfigureAwait(false);
- return;
- }
-
- var parameter = param[1].ToLower();
- bool modSet;
- if (parameter == "on") modSet = true;
- else if (parameter == "off") modSet = false;
- else {
- await reqChannel.SendMessageAsync(":x: Expecting `on` or `off` as a parameter." + ConfErrorPostfix)
- .ConfigureAwait(false);
- return;
- }
-
- if (gconf.IsModerated == modSet) {
- await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode is already {parameter}.")
- .ConfigureAwait(false);
- } else {
- gconf.IsModerated = modSet;
- await gconf.UpdateAsync().ConfigureAwait(false);
- await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode has been turned {parameter}.")
- .ConfigureAwait(false);
- }
- }
-
- // Sets/unsets custom announcement message.
- private async Task ScmdAnnounceMsg(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel) {
- var plural = param[0].ToLower().EndsWith("pl");
-
- string? newmsg;
- bool clear;
- if (param.Length == 2) {
- newmsg = param[1];
- clear = false;
- } else {
- newmsg = null;
- clear = true;
- }
-
- (string?, string?) update;
- if (!plural) update = (newmsg, gconf.AnnounceMessages.Item2);
- else update = (gconf.AnnounceMessages.Item1, newmsg);
- gconf.AnnounceMessages = update;
- await gconf.UpdateAsync().ConfigureAwait(false);
- await reqChannel.SendMessageAsync(string.Format(":white_check_mark: The {0} birthday announcement message has been {1}.",
- plural ? "plural" : "singular", clear ? "reset" : "updated")).ConfigureAwait(false);
- }
- #endregion
-
- // Execute command as another user
- private async Task CmdOverride(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- // Moderators only. As with config, silently drop if this check fails.
- if (!gconf.IsBotModerator(reqUser)) return;
-
- if (!await HasMemberCacheAsync(reqChannel.Guild)) {
- await reqChannel.SendMessageAsync(MemberCacheEmptyError);
- return;
- }
-
- if (param.Length != 3) {
- await reqChannel.SendMessageAsync(ParameterError, embed: DocOverride.UsageEmbed).ConfigureAwait(false);
- return;
- }
-
- // Second parameter: determine the user to act as
- if (!TryGetUserId(param[1], out ulong user)) {
- await reqChannel.SendMessageAsync(BadUserError, embed: DocOverride.UsageEmbed).ConfigureAwait(false);
- return;
- }
- var overuser = reqChannel.Guild.GetUser(user);
- if (overuser == null) {
- await reqChannel.SendMessageAsync(BadUserError, embed: DocOverride.UsageEmbed).ConfigureAwait(false);
- return;
- }
-
- // Third parameter: determine command to invoke.
- // Reminder that we're only receiving a param array of size 3 at maximum. String must be split again.
- var overparam = param[2].Split(" ", 3, StringSplitOptions.RemoveEmptyEntries);
- var cmdsearch = overparam[0];
- if (cmdsearch.StartsWith(CommandPrefix)) {
- // Strip command prefix to search for the given command.
- cmdsearch = cmdsearch[CommandPrefix.Length..];
- } else {
- // Add command prefix to input, just in case.
- overparam[0] = CommandPrefix + overparam[0].ToLower();
- }
- if (!_usercommands.TryGetValue(cmdsearch, out var action)) {
- await reqChannel.SendMessageAsync(
- $":x: `{cmdsearch}` is not an overridable command.", embed: DocOverride.UsageEmbed)
- .ConfigureAwait(false);
- return;
- }
-
- // Preparations complete. Run the command.
- await reqChannel.SendMessageAsync(
- $"Executing `{cmdsearch.ToLower()}` on behalf of {overuser.Nickname ?? overuser.Username}:")
- .ConfigureAwait(false);
- await action.Invoke(instance, gconf, overparam, reqChannel, overuser).ConfigureAwait(false);
- }
-
- // Troubleshooting tool: Check for common problems regarding typical background operation.
- private async Task CmdCheck(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- // Moderators only. As with config, silently drop if this check fails.
- if (!gconf.IsBotModerator(reqUser)) return;
-
- if (param.Length != 1) {
- // Too many parameters
- // Note: Non-standard error display
- await reqChannel.SendMessageAsync(NoParameterError).ConfigureAwait(false);
- return;
- }
-
- static string DoTestFor(string label, Func test) => $"{label}: { (test() ? ":white_check_mark: Yes" : ":x: No") }";
- var result = new StringBuilder();
- var guild = reqChannel.Guild;
- var conf = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
- var userbdays = await GuildUserConfiguration.LoadAllAsync(guild.Id).ConfigureAwait(false);
-
- result.AppendLine($"Server ID: `{guild.Id}` | Bot shard ID: `{instance.ShardId:00}`");
- result.AppendLine($"Number of registered birthdays: `{ userbdays.Count() }`");
- result.AppendLine($"Server time zone: `{ (conf?.TimeZone ?? "Not set - using UTC") }`");
- result.AppendLine();
-
- bool hasMembers = Common.HasMostMembersDownloaded(guild);
- result.Append(DoTestFor("Bot has obtained the user list", () => hasMembers));
- result.AppendLine($" - Has `{guild.DownloadedMemberCount}` of `{guild.MemberCount}` members.");
- int bdayCount = -1;
- result.Append(DoTestFor("Birthday processing", delegate {
- if (!hasMembers) return false;
- bdayCount = BackgroundServices.BirthdayRoleUpdate.GetGuildCurrentBirthdays(userbdays, conf?.TimeZone).Count;
- return true;
- }));
- if (hasMembers) result.AppendLine($" - `{bdayCount}` user(s) currently having a birthday.");
- else result.AppendLine(" - Previous step failed.");
- result.AppendLine();
-
- result.AppendLine(DoTestFor("Birthday role set with `bb.config role`", delegate {
- if (conf == null) return false;
- SocketRole? role = guild.GetRole(conf.RoleId ?? 0);
- return role != null;
- }));
- result.AppendLine(DoTestFor("Birthday role can be managed by bot", delegate {
- if (conf == null) return false;
- SocketRole? role = guild.GetRole(conf.RoleId ?? 0);
- if (role == null) return false;
- return guild.CurrentUser.GuildPermissions.ManageRoles && role.Position < guild.CurrentUser.Hierarchy;
- }));
- result.AppendLine();
-
- SocketTextChannel? channel = null;
- result.AppendLine(DoTestFor("(Optional) Announcement channel set with `bb.config channel`", delegate {
- if (conf == null) return false;
- channel = guild.GetTextChannel(conf.AnnounceChannelId ?? 0);
- return channel != null;
- }));
- string disp = channel == null ? "announcement channel" : $"<#{channel.Id}>";
- result.AppendLine(DoTestFor($"(Optional) Bot can send messages into { disp }", delegate {
- if (channel == null) return false;
- return guild.CurrentUser.GetPermissions(channel).SendMessages;
- }));
-
- await reqChannel.SendMessageAsync(embed: new EmbedBuilder() {
- Author = new EmbedAuthorBuilder() { Name = "Status and config check" },
- Description = result.ToString()
- }.Build()).ConfigureAwait(false);
-
- const int announceMsgPreviewLimit = 350;
- static string prepareAnnouncePreview(string announce) {
- string trunc = announce.Length > announceMsgPreviewLimit ? announce[..announceMsgPreviewLimit] + "`(...)`" : announce;
- var result = new StringBuilder();
- foreach (var line in trunc.Split('\n'))
- result.AppendLine($"> {line}");
- return result.ToString();
- }
- if (conf != null && (conf.AnnounceMessages.Item1 != null || conf.AnnounceMessages.Item2 != null)) {
- var em = new EmbedBuilder().WithAuthor(new EmbedAuthorBuilder() { Name = "Custom announce messages:" });
- var dispAnnounces = new StringBuilder("Custom announcement message(s):\n");
- if (conf.AnnounceMessages.Item1 != null) {
- em = em.AddField("Single", prepareAnnouncePreview(conf.AnnounceMessages.Item1));
- }
- if (conf.AnnounceMessages.Item2 != null) {
- em = em.AddField("Plural", prepareAnnouncePreview(conf.AnnounceMessages.Item2));
- }
- await reqChannel.SendMessageAsync(embed: em.Build()).ConfigureAwait(false);
- }
- }
-
- #region Common/helper methods
- private const string RoleInputError = ":x: Unable to determine the given role.";
- private static readonly Regex RoleMention = new(@"<@?&(?\d+)>", RegexOptions.Compiled);
-
- private static SocketRole? FindUserInputRole(string inputStr, SocketGuild guild) {
- // Resembles a role mention? Strip it to the pure number
- var input = inputStr;
- var rmatch = RoleMention.Match(input);
- if (rmatch.Success) input = rmatch.Groups["snowflake"].Value;
-
- // Attempt to get role by ID, or null
- if (ulong.TryParse(input, out ulong rid)) {
- return guild.GetRole(rid);
- } else {
- // Reset the search value on the off chance there's a role name that actually resembles a role ping.
- input = inputStr;
- }
-
- // If not already found, attempt to search role by string name
- foreach (var search in guild.Roles) {
- if (string.Equals(search.Name, input, StringComparison.OrdinalIgnoreCase)) return search;
- }
-
- return null;
- }
- #endregion
-}
diff --git a/TextCommands/UserCommands.cs b/TextCommands/UserCommands.cs
deleted file mode 100644
index 9fd8d52..0000000
--- a/TextCommands/UserCommands.cs
+++ /dev/null
@@ -1,180 +0,0 @@
-#pragma warning disable CS0618
-using BirthdayBot.Data;
-using System.Text.RegularExpressions;
-
-namespace BirthdayBot.TextCommands;
-
-internal class UserCommands : CommandsCommon {
- public UserCommands(Configuration db) : base(db) { }
-
- public override IEnumerable<(string, CommandHandler)> Commands
- => new List<(string, CommandHandler)>()
- {
- ("set", CmdSet),
- ("zone", CmdZone),
- ("remove", CmdRemove)
- };
-
- #region Date parsing
- const string FormatError = ":x: Unrecognized date format. The following formats are accepted, as examples: "
- + "`15-jan`, `jan-15`, `15 jan`, `jan 15`, `15 January`, `January 15`.";
-
- private static readonly Regex DateParse1 = new(@"^(?\d{1,2})[ -](?[A-Za-z]+)$", RegexOptions.Compiled);
- private static readonly Regex DateParse2 = new(@"^(?[A-Za-z]+)[ -](?\d{1,2})$", RegexOptions.Compiled);
-
- ///
- /// Parses a date input.
- ///
- /// Tuple: month, day
- ///
- /// Thrown for any parsing issue. Reason is expected to be sent to Discord as-is.
- ///
- private static (int, int) ParseDate(string dateInput) {
- var m = DateParse1.Match(dateInput);
- if (!m.Success) {
- // Flip the fields around, try again
- m = DateParse2.Match(dateInput);
- if (!m.Success) throw new FormatException(FormatError);
- }
-
- int day, month;
- string monthVal;
- try {
- day = int.Parse(m.Groups["day"].Value);
- } catch (FormatException) {
- throw new Exception(FormatError);
- }
- monthVal = m.Groups["month"].Value;
-
- int dayUpper; // upper day of month check
- (month, dayUpper) = GetMonth(monthVal);
-
- if (day == 0 || day > dayUpper) throw new FormatException(":x: The date you specified is not a valid calendar date.");
-
- return (month, day);
- }
-
- ///
- /// Returns information for a given month input.
- ///
- ///
- /// Tuple: Month value, upper limit of days in the month
- ///
- /// Thrown on error. Send out to Discord as-is.
- ///
- private static (int, int) GetMonth(string input) {
- return input.ToLower() switch {
- "jan" or "january" => (1, 31),
- "feb" or "february" => (2, 29),
- "mar" or "march" => (3, 31),
- "apr" or "april" => (4, 30),
- "may" => (5, 31),
- "jun" or "june" => (6, 30),
- "jul" or "july" => (7, 31),
- "aug" or "august" => (8, 31),
- "sep" or "september" => (9, 30),
- "oct" or "october" => (10, 31),
- "nov" or "november" => (11, 30),
- "dec" or "december" => (12, 31),
- _ => throw new FormatException($":x: Can't determine month name `{input}`. Check your spelling and try again."),
- };
- }
- #endregion
-
- #region Documentation
- public static readonly CommandDocumentation DocSet =
- new(new string[] { "set (date)" }, "Registers your birth month and day.",
- $"`{CommandPrefix}set jan-31`, `{CommandPrefix}set 15 may`.");
- public static readonly CommandDocumentation DocZone =
- new(new string[] { "zone (zone)" }, "Sets your local time zone. "
- + $"See also `{CommandPrefix}help-tzdata`.", null);
- public static readonly CommandDocumentation DocRemove =
- new(new string[] { "remove" }, "Removes your birthday information from this bot.", null);
- #endregion
-
- private async Task CmdSet(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- if (param.Length < 2) {
- await reqChannel.SendMessageAsync(ParameterError, embed: DocSet.UsageEmbed).ConfigureAwait(false);
- return;
- }
-
- // Date format accepts spaces. Must coalesce parameters to a single string.
- var fullinput = "";
- foreach (var p in param[1..]) fullinput += " " + p;
- fullinput = fullinput[1..]; // trim leading space
-
- int bmonth, bday;
- try {
- (bmonth, bday) = ParseDate(fullinput);
- } catch (FormatException ex) {
- // Our parse method's FormatException has its message to send out to Discord.
- reqChannel.SendMessageAsync(ex.Message, embed: DocSet.UsageEmbed).Wait();
- return;
- }
-
- // Parsing successful. Update user information.
- bool known; // Extra detail: Bot's response changes if the user was previously unknown.
- try {
- var user = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id).ConfigureAwait(false);
- known = user.IsKnown;
- await user.UpdateAsync(bmonth, bday, user.TimeZone).ConfigureAwait(false);
- } catch (Exception ex) {
- Program.Log("Error", ex.ToString());
- reqChannel.SendMessageAsync(ShardInstance.InternalError).Wait();
- return;
- }
- await reqChannel.SendMessageAsync($":white_check_mark: Your birthday has been { (known ? "updated" : "recorded") }.")
- .ConfigureAwait(false);
- }
-
- private async Task CmdZone(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- if (param.Length != 2) {
- await reqChannel.SendMessageAsync(ParameterError, embed: DocZone.UsageEmbed).ConfigureAwait(false);
- return;
- }
-
- var user = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id).ConfigureAwait(false);
- if (!user.IsKnown) {
- await reqChannel.SendMessageAsync(":x: You may only update your time zone when you have a birthday registered."
- + $" Refer to the `{CommandPrefix}set` command.", embed: DocZone.UsageEmbed)
- .ConfigureAwait(false);
- return;
- }
-
- string btz;
- try {
- btz = ParseTimeZone(param[1]);
- } catch (Exception ex) {
- reqChannel.SendMessageAsync(ex.Message, embed: DocZone.UsageEmbed).Wait();
- return;
- }
- await user.UpdateAsync(user.BirthMonth, user.BirthDay, btz).ConfigureAwait(false);
-
- await reqChannel.SendMessageAsync($":white_check_mark: Your time zone has been updated to **{btz}**.")
- .ConfigureAwait(false);
- }
-
- private async Task CmdRemove(ShardInstance instance, GuildConfiguration gconf,
- string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
- // Parameter count check
- if (param.Length != 1) {
- await reqChannel.SendMessageAsync(NoParameterError, embed: DocRemove.UsageEmbed).ConfigureAwait(false);
- return;
- }
-
- // Extra detail: Send a notification if the user isn't actually known by the bot.
- bool known;
- var u = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id).ConfigureAwait(false);
- known = u.IsKnown;
- await u.DeleteAsync().ConfigureAwait(false);
- if (!known) {
- await reqChannel.SendMessageAsync(":white_check_mark: This bot already does not contain your information.")
- .ConfigureAwait(false);
- } else {
- await reqChannel.SendMessageAsync(":white_check_mark: Your information has been removed.")
- .ConfigureAwait(false);
- }
- }
-}