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 _tzNameMap; protected const string ErrInvalidZone = ":x: Not a valid zone name." + " To find your time zone, refer to: ."; 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 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; } /// /// 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. /// 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')'", DateTimeFormatInfo.InvariantInfo); return $"{sortpfx}● `{fullstr}`"; } /// /// Checks given time zone input. Returns a valid string for use with NodaTime, or null. /// 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; } /// /// Formats a user's name to a consistent, readable format which makes use of their nickname. /// 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}"; } /// /// Checks if the given user can be considered a guild admin ('Manage Server' is set). /// protected static bool IsUserAdmin(SocketGuildUser user) => user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild; // TODO port modrole feature from BB, implement in here /// /// Checks if the member cache for the specified guild needs to be filled, and sends a request if needed. /// /// /// 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. /// /// /// True if the guild's members are already downloaded. If false, the command handler must notify the user. /// protected static async Task 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; } } /// /// An alternative to . /// Returns true if *most* members have been downloaded. /// Used as a workaround check due to Discord.Net occasionally unable to actually download all members. /// /// Copied directly from BirthdayBot. Try to coordinate changes between projects... 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; } } }