mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-21 21:54:36 +00:00
Remove text-based commands
This commit is contained in:
parent
f59dbc0e19
commit
b9604dadfe
14 changed files with 9 additions and 1865 deletions
|
@ -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]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Obsolete(Database.ObsoleteReason)]
|
||||
public static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserConfiguration> guildUsers, string? defaultTzStr) {
|
||||
var tzdb = DateTimeZoneProviders.Tzdb;
|
||||
DateTimeZone defaultTz = (defaultTzStr != null ? DateTimeZoneProviders.Tzdb.GetZoneOrNull(defaultTzStr) : null)
|
||||
?? tzdb.GetZoneOrNull("UTC")!;
|
||||
|
||||
var birthdayUsers = new HashSet<ulong>();
|
||||
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;
|
||||
}
|
||||
/// <summary>
|
||||
/// 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.
|
||||
|
|
|
@ -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!;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up and opens a database connection.
|
||||
/// </summary>
|
||||
public static async Task<NpgsqlConnection> 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);
|
||||
}
|
||||
}
|
|
@ -1,253 +0,0 @@
|
|||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace BirthdayBot.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents guild-specific configuration as exists in the database.
|
||||
/// Updating any property requires a call to <see cref="UpdateAsync"/> for changes to take effect.
|
||||
/// </summary>
|
||||
[Obsolete(Database.ObsoleteReason, error: false)]
|
||||
class GuildConfiguration {
|
||||
/// <summary>
|
||||
/// Gets this configuration's corresponding guild ID.
|
||||
/// </summary>
|
||||
public ulong GuildId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the guild's designated usable role ID.
|
||||
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||
/// </summary>
|
||||
public ulong? RoleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the announcement channel ID.
|
||||
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||
/// </summary>
|
||||
public ulong? AnnounceChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the guild's default time zone ztring.
|
||||
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||
/// </summary>
|
||||
public string? TimeZone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the guild's moderated mode setting.
|
||||
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||
/// </summary>
|
||||
public bool IsModerated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the guild's corresponding bot moderator role ID.
|
||||
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||
/// </summary>
|
||||
public ulong? ModeratorRole { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the guild-specific birthday announcement message.
|
||||
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||
/// </summary>
|
||||
public (string?, string?) AnnounceMessages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the announcement ping setting.
|
||||
/// Updating this value requires a call to <see cref="UpdateAsync"/>.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the specified user is blocked by current guild policy (block list or moderated mode).
|
||||
/// </summary>
|
||||
public async Task<bool> IsUserBlockedAsync(ulong userId) {
|
||||
if (IsModerated) return true;
|
||||
return await IsUserInBlocklistAsync(userId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given user exists in the block list.
|
||||
/// </summary>
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified user to the block list corresponding to this guild.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified user from the block list corresponding to this guild.
|
||||
/// </summary>
|
||||
/// <returns>True if a user has been removed, false if the requested user was not in this list.</returns>
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches guild settings from the database. If no corresponding entry exists, it will be created.
|
||||
/// </summary>
|
||||
/// <param name="nullIfUnknown">
|
||||
/// If true, this method shall not create a new entry and will return null if the guild does
|
||||
/// not exist in the database.
|
||||
/// </param>
|
||||
public static async Task<GuildConfiguration?> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates values on the backing database with values from this object instance.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace BirthdayBot.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents configuration for a guild user as may exist in the database.
|
||||
/// </summary>
|
||||
[Obsolete(Database.ObsoleteReason, error: false)]
|
||||
class GuildUserConfiguration {
|
||||
public ulong GuildId { get; }
|
||||
public ulong UserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Month of birth as a numeric value. Range 1-12.
|
||||
/// </summary>
|
||||
public int BirthMonth { get; private set; }
|
||||
/// <summary>
|
||||
/// Day of birth as a numeric value. Ranges between 1-31 or lower based on month value.
|
||||
/// </summary>
|
||||
public int BirthDay { get; private set; }
|
||||
|
||||
public string? TimeZone { get; private set; }
|
||||
public bool IsKnown { get { return BirthMonth != 0 && BirthDay != 0; } }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new, data-less instance without a corresponding database entry.
|
||||
/// Calling <see cref="UpdateAsync(int, int, int)"/> will create a real database enty
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates user with given information.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes information of this user from the backing database.
|
||||
/// The corresponding object reference should ideally be discarded after calling this.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve a user's configuration. Returns a new, updateable instance if none is found.
|
||||
/// </summary>
|
||||
public static async Task<GuildUserConfiguration> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all known user configuration records associated with the specified guild.
|
||||
/// </summary>
|
||||
public static async Task<IEnumerable<GuildUserConfiguration>> 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<GuildUserConfiguration>();
|
||||
while (await r.ReadAsync().ConfigureAwait(false)) result.Add(new GuildUserConfiguration(r));
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
}
|
16
Program.cs
16
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 {
|
|||
/// </summary>
|
||||
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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Single shard instance for Birthday Bot. This shard independently handles all input and output to Discord.
|
||||
/// </summary>
|
||||
public sealed class ShardInstance : IDisposable {
|
||||
private readonly ShardManager _manager;
|
||||
private readonly ShardBackgroundWorker _background;
|
||||
private readonly Dictionary<string, CommandHandler> _textDispatch;
|
||||
private readonly InteractionService _interactionService;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
|
@ -36,15 +31,13 @@ public sealed class ShardInstance : IDisposable {
|
|||
/// <summary>
|
||||
/// Prepares and configures the shard instances, but does not yet start its connection.
|
||||
/// </summary>
|
||||
internal ShardInstance(ShardManager manager, IServiceProvider services, Dictionary<string, CommandHandler> textCmds) {
|
||||
internal ShardInstance(ShardManager manager, IServiceProvider services) {
|
||||
_manager = manager;
|
||||
_services = services;
|
||||
_textDispatch = textCmds;
|
||||
|
||||
DiscordClient = _services.GetRequiredService<DiscordSocketClient>();
|
||||
DiscordClient.Log += Client_Log;
|
||||
DiscordClient.Ready += Client_Ready;
|
||||
DiscordClient.MessageReceived += Client_MessageReceived;
|
||||
|
||||
_interactionService = _services.GetRequiredService<InteractionService>();
|
||||
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
|
||||
|
@ -105,14 +98,7 @@ public sealed class ShardInstance : IDisposable {
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers all available slash commands.
|
||||
/// Additionally, sets the shard's status to display the help command.
|
||||
/// </summary>
|
||||
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,49 +119,6 @@ public sealed class ShardInstance : IDisposable {
|
|||
#endif
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
/// <summary>
|
||||
/// Determines if the incoming message is an incoming command, and dispatches to the appropriate handler if necessary.
|
||||
/// </summary>
|
||||
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 {
|
||||
NoiTheCat.TextCommandRemovalWarning.Intercept(msg, channel.Guild.Id);
|
||||
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);
|
||||
|
@ -208,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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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 {
|
|||
/// </summary>
|
||||
private readonly Dictionary<int, ShardInstance?> _shards;
|
||||
|
||||
private readonly Dictionary<string, CommandHandler> _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<string, CommandHandler>(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<int, ShardInstance?>();
|
||||
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<DiscordSocketClient>()))
|
||||
.BuildServiceProvider();
|
||||
|
@ -144,7 +128,7 @@ class ShardManager : IDisposable {
|
|||
public string? ExecutingTask;
|
||||
}
|
||||
|
||||
private string StatusDisplay(IEnumerable<int> guildList, Dictionary<int, GuildStatusData> guildInfo, bool showDetail) {
|
||||
private static string StatusDisplay(IEnumerable<int> guildList, Dictionary<int, GuildStatusData> 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.
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
namespace NoiTheCat;
|
||||
static class TextCommandRemovalWarning {
|
||||
public const string StopUsingTextCommands = ":warning: **Reminder**: Text-based commands will be phased out by the end of August. " +
|
||||
"Please switch to using slash commands. For details on their usage, use this bot's `/help` command.";
|
||||
private static readonly RateLimit<ulong> _warnedList = new(8 * 60 * 60); // 8 hours
|
||||
|
||||
public static void Intercept(SocketMessage msg, ulong gid) {
|
||||
lock (_warnedList) {
|
||||
if (!_warnedList.IsPermitted(gid)) return;
|
||||
try {
|
||||
msg.Channel.SendMessageAsync(StopUsingTextCommands).GetAwaiter().GetResult();
|
||||
} catch (Exception e) {
|
||||
_warnedList.Reset(gid);
|
||||
Console.WriteLine(e.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
private class RateLimit<T> where T : notnull {
|
||||
private const int DefaultTimeout = 20;
|
||||
public int Timeout { get; }
|
||||
private Dictionary<T, DateTime> Entries { get; } = new Dictionary<T, DateTime>();
|
||||
public RateLimit() : this(DefaultTimeout) { }
|
||||
public RateLimit(int timeout) {
|
||||
if (timeout < 0) throw new ArgumentOutOfRangeException(nameof(timeout), "Timeout valie cannot be negative.");
|
||||
Timeout = timeout;
|
||||
}
|
||||
public bool IsPermitted(T value) {
|
||||
if (Timeout == 0) return true;
|
||||
var now = DateTime.Now;
|
||||
var expired = Entries.Where(x => x.Value.AddSeconds(Timeout) <= now).Select(x => x.Key).ToList();
|
||||
foreach (var item in expired) Entries.Remove(item);
|
||||
if (Entries.ContainsKey(value)) return false;
|
||||
else {
|
||||
Entries.Add(value, DateTime.Now);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public bool Reset(T value) => Entries.Remove(value);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<string> commands, string usage, string? examples) {
|
||||
var cmds = new List<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that can be inserted into a help or usage message.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an embeddable message containing the command documentation.
|
||||
/// </summary>
|
||||
public Embed UsageEmbed => new EmbedBuilder() {
|
||||
Author = new EmbedAuthorBuilder() { Name = "Usage" },
|
||||
Description = Export()
|
||||
}.Build();
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
#pragma warning disable CS0618
|
||||
using BirthdayBot.Data;
|
||||
using NodaTime;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BirthdayBot.TextCommands;
|
||||
|
||||
/// <summary>
|
||||
/// Common base class for common constants and variables.
|
||||
/// </summary>
|
||||
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<string, string> 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<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var name in DateTimeZoneProviders.Tzdb.Ids) dict.Add(name, name);
|
||||
TzNameMap = new(dict);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On command dispatcher initialization, it will retrieve all available commands through here.
|
||||
/// </summary>
|
||||
public abstract IEnumerable<(string, CommandHandler)> Commands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks given time zone input. Returns a valid string for use with NodaTime.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An alternative to <see cref="SocketGuild.HasAllMembers"/> to be called by command handlers needing a full member cache.
|
||||
/// Creates a download request if necessary.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// True if the member cache is already filled, false otherwise.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Any updates to the member cache aren't accessible until the event handler finishes execution, meaning proactive downloading
|
||||
/// is necessary, and is handled by <seealso cref="BackgroundServices.AutoUserDownload"/>. 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.
|
||||
/// </remarks>
|
||||
protected static async Task<bool> 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,313 +0,0 @@
|
|||
#pragma warning disable CS0618
|
||||
using BirthdayBot.Data;
|
||||
using System.Text;
|
||||
|
||||
namespace BirthdayBot.TextCommands;
|
||||
|
||||
/// <summary>
|
||||
/// Commands for listing upcoming and all birthdays.
|
||||
/// </summary>
|
||||
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<string>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all guild birthdays and places them into an easily usable structure.
|
||||
/// Users currently not in the guild are not included in the result.
|
||||
/// </summary>
|
||||
private static async Task<List<ListItem>> 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<ListItem>();
|
||||
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<ListItem> 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<ListItem> 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;
|
||||
}
|
||||
}
|
|
@ -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<string, ConfigSubcommand> _subcommands;
|
||||
private readonly Dictionary<string, CommandHandler> _usercommands;
|
||||
|
||||
public ManagerCommands(Configuration db, IEnumerable<(string, CommandHandler)> userCommands) : base(db) {
|
||||
_subcommands = new Dictionary<string, ConfigSubcommand>(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<string, CommandHandler>(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<bool> 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(@"<@?&(?<snowflake>\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
|
||||
}
|
|
@ -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(@"^(?<day>\d{1,2})[ -](?<month>[A-Za-z]+)$", RegexOptions.Compiled);
|
||||
private static readonly Regex DateParse2 = new(@"^(?<month>[A-Za-z]+)[ -](?<day>\d{1,2})$", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a date input.
|
||||
/// </summary>
|
||||
/// <returns>Tuple: month, day</returns>
|
||||
/// <exception cref="FormatException">
|
||||
/// Thrown for any parsing issue. Reason is expected to be sent to Discord as-is.
|
||||
/// </exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns information for a given month input.
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns>Tuple: Month value, upper limit of days in the month</returns>
|
||||
/// <exception cref="FormatException">
|
||||
/// Thrown on error. Send out to Discord as-is.
|
||||
/// </exception>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue