mirror of
https://github.com/NoiTheCat/WorldTime.git
synced 2024-11-21 14:34:36 +00:00
Implement slash commands
Functionality between existing text and new slash commands have been reimplemented, with common functionality between both methods moved to a common base class. This may see another rewrite later to make use of the library's interactions framework.
This commit is contained in:
parent
5fcc36ab10
commit
12c421ef17
5 changed files with 438 additions and 121 deletions
121
CommandsCommon.cs
Normal file
121
CommandsCommon.cs
Normal file
|
@ -0,0 +1,121 @@
|
|||
using NodaTime;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace WorldTime;
|
||||
|
||||
internal abstract class CommandsCommon {
|
||||
protected readonly Database _database;
|
||||
protected readonly WorldTime _instance;
|
||||
|
||||
private static readonly ReadOnlyDictionary<string, string> _tzNameMap;
|
||||
|
||||
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 ErrTargetUserNotFound = ":x: Unable to find the target user.";
|
||||
protected const string ErrNoUserCache = ":warning: Please try the command again.";
|
||||
protected const int MaxSingleLineLength = 750;
|
||||
protected const int MaxSingleOutputLength = 900;
|
||||
|
||||
static CommandsCommon() {
|
||||
Dictionary<string, string> tzNameMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var name in DateTimeZoneProviders.Tzdb.Ids) tzNameMap.Add(name, name);
|
||||
_tzNameMap = new(tzNameMap);
|
||||
}
|
||||
|
||||
public CommandsCommon(Database database, WorldTime instance) {
|
||||
_database = database;
|
||||
_instance = instance;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
protected static bool IsUserAdmin(SocketGuildUser user)
|
||||
=> user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild;
|
||||
// TODO port modrole feature from BB, implement in here
|
||||
|
||||
/// <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.
|
||||
/// </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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An alternative to <see cref="SocketGuild.HasAllMembers"/>.
|
||||
/// Returns true if *most* members have been downloaded.
|
||||
/// Used as a workaround check due to Discord.Net occasionally unable to actually download all members.
|
||||
/// </summary>
|
||||
/// <remarks>Copied directly from BirthdayBot. Try to coordinate changes between projects...</remarks>
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
}
|
304
CommandsSlash.cs
Normal file
304
CommandsSlash.cs
Normal file
|
@ -0,0 +1,304 @@
|
|||
using System.Text;
|
||||
|
||||
namespace WorldTime;
|
||||
|
||||
internal class CommandsSlash : CommandsCommon {
|
||||
delegate Task CommandResponder(SocketSlashCommand arg);
|
||||
|
||||
const string ErrGuildOnly = ":x: This command can only be run within a server.";
|
||||
const string ErrNotAllowed = ":x: Only server moderators may use this command.";
|
||||
|
||||
const string EmbedHelpField1 = $"`/help` - {HelpHelp}\n"
|
||||
+ $"`/list` - {HelpList}\n"
|
||||
+ $"`/set` - {HelpSet}\n"
|
||||
+ $"`/remove` - {HelpRemove}";
|
||||
const string EmbedHelpField2 = $"`/set-for` - {HelpSetFor}\n`/remove-for` - {HelpRemoveFor}";
|
||||
|
||||
#region Help strings
|
||||
const string HelpHelp = "Displays a list of available bot commands.";
|
||||
const string HelpList = "Shows the current time for all recently active known users.";
|
||||
const string HelpSet = "Adds or updates your time zone to the bot.";
|
||||
const string HelpSetFor = "Sets/updates time zone for a given user.";
|
||||
const string HelpRemove = "Removes your time zone information from this bot.";
|
||||
const string HelpRemoveFor = "Removes time zone for a given user.";
|
||||
#endregion
|
||||
|
||||
public CommandsSlash(WorldTime inst, Database db) : base(db, inst) {
|
||||
inst.DiscordClient.SlashCommandExecuted += DiscordClient_SlashCommandExecuted;
|
||||
inst.DiscordClient.ShardReady += DiscordClient_ShardReady;
|
||||
}
|
||||
|
||||
private async Task DiscordClient_ShardReady(DiscordSocketClient arg) {
|
||||
#if !DEBUG
|
||||
// Update our commands here, only when the first shard connects
|
||||
if (arg.ShardId != 0) return;
|
||||
#endif
|
||||
var cmds = new ApplicationCommandProperties[] {
|
||||
new SlashCommandBuilder()
|
||||
.WithName("help").WithDescription(HelpHelp).Build(),
|
||||
new SlashCommandBuilder()
|
||||
.WithName("list")
|
||||
.WithDescription(HelpList)
|
||||
.AddOption("user", ApplicationCommandOptionType.User, "A specific user whose time to look up.", isRequired: false)
|
||||
.Build(),
|
||||
new SlashCommandBuilder()
|
||||
.WithName("set")
|
||||
.WithDescription(HelpSet)
|
||||
.AddOption("zone", ApplicationCommandOptionType.String, "The new time zone to set.", isRequired: true)
|
||||
.Build(),
|
||||
new SlashCommandBuilder()
|
||||
.WithName("set-for")
|
||||
.WithDescription(HelpSetFor)
|
||||
.AddOption("user", ApplicationCommandOptionType.User, "The user whose time zone to modify.", isRequired: true)
|
||||
.AddOption("zone", ApplicationCommandOptionType.String, "The new time zone to set.", isRequired: true)
|
||||
.Build(),
|
||||
new SlashCommandBuilder()
|
||||
.WithName("remove").WithDescription(HelpRemove).Build(),
|
||||
new SlashCommandBuilder()
|
||||
.WithName("remove-for")
|
||||
.WithDescription(HelpRemoveFor)
|
||||
.AddOption("user", ApplicationCommandOptionType.User, "The user whose time zone to remove.", isRequired: true)
|
||||
.Build()
|
||||
};
|
||||
#if !DEBUG
|
||||
// Remove any unneeded/unused commands
|
||||
var existingcmdnames = cmds.Select(c => c.Name.Value).ToHashSet();
|
||||
foreach (var gcmd in await arg.GetGlobalApplicationCommandsAsync()) {
|
||||
if (!existingcmdnames.Contains(gcmd.Name)) {
|
||||
Program.Log("Command registration", $"Found registered unused command /{gcmd.Name} - sending removal request");
|
||||
await gcmd.DeleteAsync();
|
||||
}
|
||||
}
|
||||
// And update what we have
|
||||
Program.Log("Command registration", $"Bulk updating {cmds.Length} global command(s)");
|
||||
await arg.BulkOverwriteGlobalApplicationCommandsAsync(cmds).ConfigureAwait(false);
|
||||
#else
|
||||
// Debug: Register our commands locally instead, in each guild we're in
|
||||
foreach (var g in arg.Guilds) {
|
||||
await g.DeleteApplicationCommandsAsync().ConfigureAwait(false);
|
||||
await g.BulkOverwriteApplicationCommandAsync(cmds).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var gcmd in await arg.GetGlobalApplicationCommandsAsync()) {
|
||||
Program.Log("Command registration", $"Found global command /{gcmd.Name} and we're DEBUG - sending removal request");
|
||||
await gcmd.DeleteAsync();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private async Task DiscordClient_SlashCommandExecuted(SocketSlashCommand arg) {
|
||||
SocketGuildChannel? rptChannel = arg.Channel as SocketGuildChannel;
|
||||
var rptId = rptChannel?.Guild.Id ?? arg.User.Id;
|
||||
Program.Log("Command executed", $"/{arg.CommandName} by {arg.User} { (rptChannel != null ? "in guild" : "with ID") } {rptId}");
|
||||
|
||||
CommandResponder responder = arg.Data.Name switch {
|
||||
"help" => CmdHelp,
|
||||
"list" => CmdList,
|
||||
"set" => CmdSet,
|
||||
"set-for" => CmdSetFor,
|
||||
"remove" => CmdRemove,
|
||||
"remove-for" => CmdRemoveFor,
|
||||
_ => UnknownCommandHandler
|
||||
};
|
||||
try {
|
||||
await responder(arg).ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
Program.Log("Command exception", e.ToString());
|
||||
// TODO respond with error message?
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnknownCommandHandler(SocketSlashCommand arg) {
|
||||
string place;
|
||||
// Unknown command - set up a report
|
||||
if (arg.Channel is SocketGuildChannel gch) {
|
||||
place = $"Guild {gch.Guild.Id}";
|
||||
} else {
|
||||
place = "Global command";
|
||||
}
|
||||
Program.Log("Command invoked", $"/{arg.Data.Name} is an unknown command! Executed at: {place}");
|
||||
await arg.RespondAsync("Oops, that command isn't supposed to be there... Please try something else.",
|
||||
ephemeral: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task CmdHelp(SocketSlashCommand arg) {
|
||||
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
|
||||
var guildct = _instance.DiscordClient.Guilds.Count;
|
||||
var uniquetz = await _database.GetDistinctZoneCountAsync();
|
||||
await arg.RespondAsync(embed: new EmbedBuilder() {
|
||||
Title = "Help & About",
|
||||
Description = $"World Time v{version} - Serving {guildct} communities across {uniquetz} time zones.\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.",
|
||||
Footer = new EmbedFooterBuilder() {
|
||||
IconUrl = _instance.DiscordClient.CurrentUser.GetAvatarUrl(),
|
||||
Text = "World Time"
|
||||
}
|
||||
}.AddField(inline: false, name: "Commands", value: EmbedHelpField1
|
||||
).AddField(inline: false, name: "Admin commands", value: EmbedHelpField2
|
||||
).AddField(inline: false, name: "Zones", value:
|
||||
"This bot accepts zone names from the IANA Time Zone Database (a.k.a. Olson Database). " +
|
||||
"A useful tool to determine yours can be found at: https://kevinnovak.github.io/Time-Zone-Picker/"
|
||||
).Build());
|
||||
}
|
||||
|
||||
private async Task CmdList(SocketSlashCommand arg) {
|
||||
if (arg.Channel is not SocketGuildChannel gc) {
|
||||
await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg.Data.Options.FirstOrDefault()?.Value is SocketGuildUser parameter) {
|
||||
await CmdListUser(arg, parameter);
|
||||
return;
|
||||
}
|
||||
|
||||
var guild = gc.Guild;
|
||||
if (!await AreUsersDownloadedAsync(guild).ConfigureAwait(false)) {
|
||||
await arg.RespondAsync(ErrNoUserCache, ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var userlist = await _database.GetGuildZonesAsync(guild.Id).ConfigureAwait(false);
|
||||
if (userlist.Count == 0) {
|
||||
await arg.RespondAsync(":x: Nothing to show. " +
|
||||
$"To register your time zone with the bot, use the `/set` command.").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
// Order times by popularity to limit how many are shown, group by printed name
|
||||
var sortedlist = new SortedDictionary<string, List<ulong>>();
|
||||
foreach ((string area, List<ulong> users) in userlist.OrderByDescending(o => o.Value.Count).Take(20)) {
|
||||
// Filter further to top 20 distinct timezones, even if they are not displayed in the final result
|
||||
var areaprint = TzPrint(area);
|
||||
if (!sortedlist.ContainsKey(areaprint)) sortedlist.Add(areaprint, new List<ulong>());
|
||||
sortedlist[areaprint].AddRange(users);
|
||||
}
|
||||
|
||||
// Build zone listings with users
|
||||
var outputlines = new List<string>();
|
||||
foreach ((string area, List<ulong> users) in sortedlist) {
|
||||
var buffer = new StringBuilder();
|
||||
buffer.Append(area[4..] + ": ");
|
||||
bool empty = true;
|
||||
foreach (var userid in users) {
|
||||
var userinstance = guild.GetUser(userid);
|
||||
if (userinstance == null) continue;
|
||||
if (empty) empty = !empty;
|
||||
else buffer.Append(", ");
|
||||
var useradd = FormatName(userinstance);
|
||||
if (buffer.Length + useradd.Length > MaxSingleLineLength) {
|
||||
buffer.Append("others...");
|
||||
break;
|
||||
} else buffer.Append(useradd);
|
||||
}
|
||||
if (!empty) outputlines.Add(buffer.ToString());
|
||||
}
|
||||
|
||||
// Prepare for output - send buffers out if they become too large
|
||||
outputlines.Sort();
|
||||
bool hasOutputOneLine = false;
|
||||
// First output is shown as an interaction response, followed then as regular channel messages
|
||||
async Task doOutput(Embed msg) {
|
||||
if (!hasOutputOneLine) {
|
||||
await arg.RespondAsync(embed: msg).ConfigureAwait(false);
|
||||
hasOutputOneLine = true;
|
||||
} else {
|
||||
await arg.Channel.SendMessageAsync(embed: msg).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var resultout = new StringBuilder();
|
||||
foreach (var line in outputlines) {
|
||||
if (resultout.Length + line.Length > MaxSingleOutputLength) {
|
||||
await doOutput(new EmbedBuilder().WithDescription(resultout.ToString()).Build()).ConfigureAwait(false);
|
||||
resultout.Clear();
|
||||
}
|
||||
if (resultout.Length > 0) resultout.AppendLine(); // avoids trailing newline by adding to the previous line
|
||||
resultout.Append(line);
|
||||
}
|
||||
if (resultout.Length > 0) {
|
||||
await doOutput(new EmbedBuilder().WithDescription(resultout.ToString()).Build()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CmdListUser(SocketSlashCommand arg, SocketGuildUser parameter) {
|
||||
// Not meant as a command handler - called by CmdList
|
||||
var result = await _database.GetUserZoneAsync(parameter).ConfigureAwait(false);
|
||||
if (result == null) {
|
||||
bool isself = arg.User.Id == parameter.Id;
|
||||
if (isself) await arg.RespondAsync(":x: You do not have a time zone. Set it with `tz.set`.", ephemeral: true)
|
||||
.ConfigureAwait(false);
|
||||
else await arg.RespondAsync(":x: The given user does not have a time zone set.", ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var resulttext = TzPrint(result)[4..] + ": " + FormatName(parameter);
|
||||
await arg.RespondAsync(embed: new EmbedBuilder().WithDescription(resulttext).Build()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task CmdSet(SocketSlashCommand arg) {
|
||||
if (arg.Channel is not SocketGuildChannel) {
|
||||
await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var input = (string)arg.Data.Options.First().Value;
|
||||
input = ParseTimeZone(input);
|
||||
if (input == null) {
|
||||
await arg.RespondAsync(ErrInvalidZone, ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
await _database.UpdateUserAsync((SocketGuildUser)arg.User, input).ConfigureAwait(false);
|
||||
await arg.RespondAsync($":white_check_mark: Your time zone has been set to **{input}**.").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task CmdSetFor(SocketSlashCommand arg) {
|
||||
if (arg.Channel is not SocketGuildChannel) {
|
||||
await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsUserAdmin((SocketGuildUser)arg.User)) {
|
||||
await arg.RespondAsync(ErrNotAllowed, ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract parameters
|
||||
var opts = arg.Data.Options.ToDictionary(o => o.Name, o => o);
|
||||
var user = (SocketGuildUser)opts["user"].Value;
|
||||
var zone = (string)opts["zone"].Value;
|
||||
|
||||
var newtz = ParseTimeZone(zone);
|
||||
if (newtz == null) {
|
||||
await arg.RespondAsync(ErrInvalidZone).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await _database.UpdateUserAsync(user, newtz).ConfigureAwait(false);
|
||||
await arg.RespondAsync($":white_check_mark: Time zone for **{user}** set to **{newtz}**.").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task CmdRemove(SocketSlashCommand arg) {
|
||||
if (arg.Channel is not SocketGuildChannel) {
|
||||
await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var success = await _database.DeleteUserAsync((SocketGuildUser)arg.User).ConfigureAwait(false);
|
||||
if (success) await arg.RespondAsync(":white_check_mark: Your zone has been removed.").ConfigureAwait(false);
|
||||
else await arg.RespondAsync(":x: You don't have a time zone set.").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task CmdRemoveFor(SocketSlashCommand arg) {
|
||||
if (arg.Channel is not SocketGuildChannel) {
|
||||
await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsUserAdmin((SocketGuildUser)arg.User)) {
|
||||
await arg.RespondAsync(ErrNotAllowed, ephemeral: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +1,27 @@
|
|||
using NodaTime;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace WorldTime;
|
||||
|
||||
internal class Commands {
|
||||
internal class CommandsText : CommandsCommon {
|
||||
#if DEBUG
|
||||
public const string CommandPrefix = "tt.";
|
||||
#else
|
||||
public const string CommandPrefix = "tz.";
|
||||
#endif
|
||||
const string ErrInvalidZone = ":x: Not a valid zone name."
|
||||
+ " To find your time zone, refer to: <https://kevinnovak.github.io/Time-Zone-Picker/>.";
|
||||
const string ErrTargetUserNotFound = ":x: Unable to find the target user.";
|
||||
const string ErrNoUserCache = ":warning: Please try the command again.";
|
||||
const int MaxSingleLineLength = 750;
|
||||
const int MaxSingleOutputLength = 900;
|
||||
|
||||
delegate Task Command(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message);
|
||||
|
||||
private readonly Dictionary<string, Command> _commands;
|
||||
private readonly Database _database;
|
||||
private readonly WorldTime _instance;
|
||||
private static readonly ReadOnlyDictionary<string, string> _tzNameMap;
|
||||
|
||||
private static readonly Regex _userExplicit;
|
||||
private static readonly Regex _userMention;
|
||||
|
||||
static Commands() {
|
||||
Dictionary<string, string> tzNameMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var name in DateTimeZoneProviders.Tzdb.Ids) tzNameMap.Add(name, name);
|
||||
_tzNameMap = new(tzNameMap);
|
||||
static CommandsText() {
|
||||
_userExplicit = new Regex(@"(.+)#(\d{4})", RegexOptions.Compiled);
|
||||
_userMention = new Regex(@"\!?(\d+)>", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
public Commands(WorldTime inst, Database db) {
|
||||
_instance = inst;
|
||||
_database = db;
|
||||
|
||||
public CommandsText(WorldTime inst, Database db) : base(db, inst) {
|
||||
_commands = new(StringComparer.OrdinalIgnoreCase) {
|
||||
{ "help", CmdHelp },
|
||||
{ "list", CmdList },
|
||||
|
@ -262,60 +244,6 @@ internal class Commands {
|
|||
await channel.SendMessageAsync($":white_check_mark: Removed zone information for {targetuser}.");
|
||||
}
|
||||
|
||||
#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>
|
||||
private static bool IsUserAdmin(SocketGuildUser user)
|
||||
=> user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild;
|
||||
// TODO port modrole feature from BB, implement in here
|
||||
|
||||
/// <summary>
|
||||
/// Given parameter input, attempts to find the corresponding SocketGuildUser.
|
||||
/// </summary>
|
||||
|
@ -346,42 +274,4 @@ internal class Commands {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An alternative to <see cref="SocketGuild.HasAllMembers"/>.
|
||||
/// Returns true if *most* members have been downloaded.
|
||||
/// Used as a workaround check due to Discord.Net occasionally unable to actually download all members.
|
||||
/// </summary>
|
||||
/// <remarks>Copied directly from BirthdayBot. Try to coordinate changes between projects...</remarks>
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
|
@ -25,7 +25,8 @@ internal class WorldTime : IDisposable {
|
|||
|
||||
private readonly Task _statusTask;
|
||||
private readonly CancellationTokenSource _mainCancel;
|
||||
private readonly Commands _commands;
|
||||
private readonly CommandsSlash _commands;
|
||||
private readonly CommandsText _commandsTxt;
|
||||
|
||||
internal Configuration Config { get; }
|
||||
internal DiscordShardedClient DiscordClient { get; }
|
||||
|
@ -48,7 +49,8 @@ internal class WorldTime : IDisposable {
|
|||
DiscordClient.Log += DiscordClient_Log;
|
||||
DiscordClient.ShardReady += DiscordClient_ShardReady;
|
||||
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
|
||||
_commands = new Commands(this, Database);
|
||||
_commands = new CommandsSlash(this, Database);
|
||||
_commandsTxt = new CommandsText(this, Database);
|
||||
|
||||
// Start status reporting thread
|
||||
_mainCancel = new CancellationTokenSource();
|
||||
|
@ -146,7 +148,7 @@ internal class WorldTime : IDisposable {
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task DiscordClient_ShardReady(DiscordSocketClient arg) => arg.SetGameAsync(Commands.CommandPrefix + "help");
|
||||
private Task DiscordClient_ShardReady(DiscordSocketClient arg) => arg.SetGameAsync(CommandsText.CommandPrefix + "help");
|
||||
|
||||
/// <summary>
|
||||
/// Non-specific handler for incoming events.
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>2.0.1</Version>
|
||||
<Version>2.1.0</Version>
|
||||
<Authors>NoiTheCat</Authors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||
<PackageReference Include="Discord.Net" Version="3.1.0" />
|
||||
<PackageReference Include="Discord.Net" Version="3.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NodaTime" Version="3.0.9" />
|
||||
<PackageReference Include="Npgsql" Version="6.0.2" />
|
||||
<PackageReference Include="Npgsql" Version="6.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
Loading…
Reference in a new issue