mirror of
https://github.com/NoiTheCat/WorldTime.git
synced 2024-11-23 17:04:12 +00:00
Move commands into subdirectory, create base class
Base class created with potentially common properties from existing class
This commit is contained in:
parent
e872ba36df
commit
362d288424
4 changed files with 113 additions and 112 deletions
|
@ -1,14 +1,8 @@
|
|||
using Discord.Interactions;
|
||||
using NodaTime;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using WorldTime.Data;
|
||||
|
||||
namespace WorldTime;
|
||||
public class ApplicationCommands : InteractionModuleBase<ShardedInteractionContext> {
|
||||
const string ErrNotAllowed = ":x: Only server moderators may use this command.";
|
||||
|
||||
namespace WorldTime.Commands;
|
||||
public class ApplicationCommands : CommandsBase {
|
||||
const string EmbedHelpField1 = $"`/help` - {HelpHelp}\n"
|
||||
+ $"`/list` - {HelpList}\n"
|
||||
+ $"`/set` - {HelpSet}\n"
|
||||
|
@ -23,20 +17,6 @@ public class ApplicationCommands : InteractionModuleBase<ShardedInteractionConte
|
|||
const string HelpRemove = "Removes your time zone information from this bot.";
|
||||
const string HelpRemoveFor = "Removes time zone for a given user.";
|
||||
#endregion
|
||||
private const string ErrInvalidZone = ":x: Not a valid zone name."
|
||||
+ " To find your time zone, refer to: <https://kevinnovak.github.io/Time-Zone-Picker/>.";
|
||||
private const string ErrNoUserCache = ":warning: Please try the command again.";
|
||||
|
||||
private static readonly ReadOnlyDictionary<string, string> _tzNameMap;
|
||||
|
||||
public DiscordShardedClient ShardedClient { get; set; } = null!;
|
||||
public BotDatabaseContext DbContext { get; set; } = null!;
|
||||
|
||||
static ApplicationCommands() {
|
||||
Dictionary<string, string> tzNameMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var name in DateTimeZoneProviders.Tzdb.Ids) tzNameMap.Add(name, name);
|
||||
_tzNameMap = new(tzNameMap);
|
||||
}
|
||||
|
||||
[SlashCommand("help", HelpHelp)]
|
||||
public async Task CmdHelp() {
|
||||
|
@ -217,89 +197,4 @@ public class ApplicationCommands : InteractionModuleBase<ShardedInteractionConte
|
|||
else
|
||||
await RespondAsync($":white_check_mark: No time zone is set for {user}.");
|
||||
}
|
||||
|
||||
#region Helper methods
|
||||
/// <summary>
|
||||
/// Returns a string displaying the current time in the given time zone.
|
||||
/// The result begins with four numbers for sorting purposes. Must be trimmed before output.
|
||||
/// </summary>
|
||||
private static string TzPrint(string zone) {
|
||||
var tzdb = DateTimeZoneProviders.Tzdb;
|
||||
DateTimeZone tz = tzdb.GetZoneOrNull(zone)!;
|
||||
if (tz == null) throw new Exception("Encountered unknown time zone: " + zone);
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
||||
var sortpfx = now.ToString("MMdd", DateTimeFormatInfo.InvariantInfo);
|
||||
var fullstr = now.ToString("dd'-'MMM' 'HH':'mm' 'x' (UTC'o<g>')'", DateTimeFormatInfo.InvariantInfo);
|
||||
return $"{sortpfx}● `{fullstr}`";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks given time zone input. Returns a valid string for use with NodaTime, or null.
|
||||
/// </summary>
|
||||
private static string? ParseTimeZone(string tzinput) {
|
||||
if (tzinput.Equals("Asia/Calcutta", StringComparison.OrdinalIgnoreCase)) tzinput = "Asia/Kolkata";
|
||||
if (_tzNameMap.TryGetValue(tzinput, out var name)) return name;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a user's name to a consistent, readable format which makes use of their nickname.
|
||||
/// </summary>
|
||||
private static string FormatName(SocketGuildUser user) {
|
||||
static string escapeFormattingCharacters(string input) {
|
||||
var result = new StringBuilder();
|
||||
foreach (var c in input) {
|
||||
if (c is '\\' or '_' or '~' or '*' or '@') {
|
||||
result.Append('\\');
|
||||
}
|
||||
result.Append(c);
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
var username = escapeFormattingCharacters(user.Username);
|
||||
if (user.Nickname != null) {
|
||||
return $"**{escapeFormattingCharacters(user.Nickname)}** ({username}#{user.Discriminator})";
|
||||
}
|
||||
return $"**{username}**#{user.Discriminator}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given user can be considered a guild admin ('Manage Server' is set).
|
||||
/// </summary>
|
||||
// TODO replace this with a precondition, or there's also a new permission scheme going around?
|
||||
private static bool IsUserAdmin(SocketGuildUser user)
|
||||
=> user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the member cache for the specified guild needs to be filled, and sends a request if needed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Due to a quirk in Discord.Net, the user cache cannot be filled until the command handler is no longer executing
|
||||
/// regardless of if the request runs on its own thread, thus requiring the user to run the command again.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// True if the guild's members are already downloaded. If false, the command handler must notify the user.
|
||||
/// </returns>
|
||||
private static async Task<bool> AreUsersDownloadedAsync(SocketGuild guild) {
|
||||
static bool HasMostMembersDownloaded(SocketGuild guild) {
|
||||
if (guild.HasAllMembers) return true;
|
||||
if (guild.MemberCount > 30) {
|
||||
// For guilds of size over 30, require 85% or more of the members to be known
|
||||
// (26/30, 42/50, 255/300, etc)
|
||||
return guild.DownloadedMemberCount >= (int)(guild.MemberCount * 0.85);
|
||||
} else {
|
||||
// For smaller guilds, fail if two or more members are missing
|
||||
return guild.MemberCount - guild.DownloadedMemberCount <= 2;
|
||||
}
|
||||
}
|
||||
if (HasMostMembersDownloaded(guild)) return true;
|
||||
else {
|
||||
// Event handler hangs if awaited normally or used with Task.Run
|
||||
await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
108
Commands/CommandsBase.cs
Normal file
108
Commands/CommandsBase.cs
Normal file
|
@ -0,0 +1,108 @@
|
|||
using Discord.Interactions;
|
||||
using NodaTime;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using WorldTime.Data;
|
||||
|
||||
namespace WorldTime.Commands;
|
||||
public class CommandsBase : InteractionModuleBase<ShardedInteractionContext> {
|
||||
protected const string ErrInvalidZone = ":x: Not a valid zone name."
|
||||
+ " To find your time zone, refer to: <https://kevinnovak.github.io/Time-Zone-Picker/>.";
|
||||
protected const string ErrNoUserCache = ":warning: Please try the command again.";
|
||||
protected const string ErrNotAllowed = ":x: Only server moderators may use this command.";
|
||||
|
||||
private static readonly ReadOnlyDictionary<string, string> _tzNameMap;
|
||||
|
||||
static CommandsBase() {
|
||||
Dictionary<string, string> tzNameMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var name in DateTimeZoneProviders.Tzdb.Ids) tzNameMap.Add(name, name);
|
||||
_tzNameMap = new(tzNameMap);
|
||||
}
|
||||
|
||||
public DiscordShardedClient ShardedClient { get; set; } = null!;
|
||||
public BotDatabaseContext DbContext { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string displaying the current time in the given time zone.
|
||||
/// The result begins with four numbers for sorting purposes. Must be trimmed before output.
|
||||
/// </summary>
|
||||
protected static string TzPrint(string zone) {
|
||||
var tzdb = DateTimeZoneProviders.Tzdb;
|
||||
DateTimeZone tz = tzdb.GetZoneOrNull(zone)!;
|
||||
if (tz == null) throw new Exception("Encountered unknown time zone: " + zone);
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
||||
var sortpfx = now.ToString("MMdd", DateTimeFormatInfo.InvariantInfo);
|
||||
var fullstr = now.ToString("dd'-'MMM' 'HH':'mm' 'x' (UTC'o<g>')'", DateTimeFormatInfo.InvariantInfo);
|
||||
return $"{sortpfx}● `{fullstr}`";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks given time zone input. Returns a valid string for use with NodaTime, or null.
|
||||
/// </summary>
|
||||
protected static string? ParseTimeZone(string tzinput) {
|
||||
if (tzinput.Equals("Asia/Calcutta", StringComparison.OrdinalIgnoreCase)) tzinput = "Asia/Kolkata";
|
||||
if (_tzNameMap.TryGetValue(tzinput, out var name)) return name;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a user's name to a consistent, readable format which makes use of their nickname.
|
||||
/// </summary>
|
||||
protected static string FormatName(SocketGuildUser user) {
|
||||
static string escapeFormattingCharacters(string input) {
|
||||
var result = new StringBuilder();
|
||||
foreach (var c in input) {
|
||||
if (c is '\\' or '_' or '~' or '*' or '@') {
|
||||
result.Append('\\');
|
||||
}
|
||||
result.Append(c);
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
var username = escapeFormattingCharacters(user.Username);
|
||||
if (user.Nickname != null) {
|
||||
return $"**{escapeFormattingCharacters(user.Nickname)}** ({username}#{user.Discriminator})";
|
||||
}
|
||||
return $"**{username}**#{user.Discriminator}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given user can be considered a guild admin ('Manage Server' is set).
|
||||
/// </summary>
|
||||
// TODO replace this with a precondition, or there's also a new permission scheme going around?
|
||||
protected static bool IsUserAdmin(SocketGuildUser user)
|
||||
=> user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the member cache for the specified guild needs to be filled, and sends a request if needed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Due to a quirk in Discord.Net, the user cache cannot be filled until the command handler is no longer executing
|
||||
/// regardless of if the request runs on its own thread, thus requiring the user to run the command again.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// True if the guild's members are already downloaded. If false, the command handler must notify the user.
|
||||
/// </returns>
|
||||
protected static async Task<bool> AreUsersDownloadedAsync(SocketGuild guild) {
|
||||
static bool HasMostMembersDownloaded(SocketGuild guild) {
|
||||
if (guild.HasAllMembers) return true;
|
||||
if (guild.MemberCount > 30) {
|
||||
// For guilds of size over 30, require 85% or more of the members to be known
|
||||
// (26/30, 42/50, 255/300, etc)
|
||||
return guild.DownloadedMemberCount >= (int)(guild.MemberCount * 0.85);
|
||||
} else {
|
||||
// For smaller guilds, fail if two or more members are missing
|
||||
return guild.MemberCount - guild.DownloadedMemberCount <= 2;
|
||||
}
|
||||
}
|
||||
if (HasMostMembersDownloaded(guild)) return true;
|
||||
else {
|
||||
// Event handler hangs if awaited normally or used with Task.Run
|
||||
await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
using Discord.Interactions;
|
||||
|
||||
namespace WorldTime;
|
||||
namespace WorldTime.Commands;
|
||||
/// <summary>
|
||||
/// Implements the included precondition from Discord.Net, requiring a guild context while using our custom error message.<br/><br/>
|
||||
/// Combining this with <see cref="RequireBotModeratorAttribute"/> is redundant. If possible, only use the latter instead.
|
||||
/// Implements the included precondition from Discord.Net, requiring a guild context while using our custom error message.
|
||||
/// </summary>
|
||||
class RequireGuildContextAttribute : RequireContextAttribute {
|
||||
public const string Error = "Command not received within a guild context.";
|
|
@ -7,7 +7,6 @@ using System.Text;
|
|||
using WorldTime.Data;
|
||||
|
||||
namespace WorldTime;
|
||||
|
||||
/// <summary>
|
||||
/// Main class for the program. Configures the client on start and occasionally prints status information.
|
||||
/// </summary>
|
||||
|
@ -203,7 +202,7 @@ internal class WorldTime : IDisposable {
|
|||
// Specific responses to errors, if necessary
|
||||
if (result.Error == InteractionCommandError.UnmetPrecondition) {
|
||||
string errReply = result.ErrorReason switch {
|
||||
RequireGuildContextAttribute.Error => RequireGuildContextAttribute.Reply,
|
||||
Commands.RequireGuildContextAttribute.Error => Commands.RequireGuildContextAttribute.Reply,
|
||||
_ => result.ErrorReason
|
||||
};
|
||||
await context.Interaction.RespondAsync(errReply, ephemeral: true);
|
||||
|
|
Loading…
Reference in a new issue