From 766402b65a2d6cb3cbef4b4c7256c7fc6ed42d07 Mon Sep 17 00:00:00 2001 From: Zach Goodson Date: Wed, 18 Mar 2026 22:02:45 -0500 Subject: [PATCH 1/6] Update /convert timezones appearance --- commands/convert-group/convertCommands.cs | 26 ++++++++++++-------- general-helpers/time/TimezoneConverter.cs | 3 +++ general-helpers/time/timestamps.cs | 30 +++++++++-------------- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/commands/convert-group/convertCommands.cs b/commands/convert-group/convertCommands.cs index 74867bd..908c4bf 100644 --- a/commands/convert-group/convertCommands.cs +++ b/commands/convert-group/convertCommands.cs @@ -101,20 +101,26 @@ public async Task ConvertTime( await RespondAsync($"❌ Please enter a valid day between **1** and **{DateTime.DaysInMonth(DateTime.UtcNow.Year, month)}**.", ephemeral: true); return; } - - var sourceDateTime = new DateTime(DateTime.UtcNow.Year, month, day, hour, minute, 0, DateTimeKind.Unspecified); - Console.WriteLine("Source time: " + sourceDateTime); - var sourceTimestamp = Timestamp.FromDateTime(sourceDateTime, Timestamp.Formats.Exact, sourceTimezone); - Console.WriteLine("Source timestamp: " + sourceTimestamp); + var sourceDateTime = new DateTime( + DateTime.UtcNow.Year, month, day, hour, minute, 0, DateTimeKind.Unspecified + ); - var destinationDateTime = TimeConverter.ConvertBetweenTimezones(month, day, hour, minute, sourceTimezone, destinationTimezone); - Console.WriteLine("Destination time: " + destinationDateTime); + var destinationDateTime = TimeConverter.ConvertBetweenTimezones( + month, day, hour, minute, sourceTimezone, destinationTimezone + ); - var destinationTimestamp = Timestamp.FromDateTime(destinationDateTime, Timestamp.Formats.Exact, destinationTimezone); - Console.WriteLine("Destination timestamp: " + destinationTimestamp); + var embed = new EmbedBuilder() + .WithColor(Bot.theme) + .WithTitle($"{TimeConversion.GetClosestTimeEmoji(destinationDateTime)} Timezone Conversion") + .AddField(sourceTimezone.ToDisplayName(), $"`{TimeConverter.FormatDateTime(sourceDateTime)}`", inline: true) + .AddField("➡️", "\u200B", inline: true) + .AddField(destinationTimezone.ToDisplayName(), $"`{TimeConverter.FormatDateTime(destinationDateTime)}`", inline: true) + .AddField("\u200B", $"**Convert another:** {Help.GetCommandMention("convert timezones")}", inline: true) + .WithFooter("Times are absolute and not affected by your local timezone.") + .Build(); - await RespondAsync($"{TimeConversion.GetClosestTimeEmoji(destinationDateTime)} {sourceTimestamp} in {sourceTimezone.ToDisplayName()} is {destinationTimestamp} in {destinationTimezone.ToDisplayName()}."); + await RespondAsync(embed: embed); } catch (TimeZoneNotFoundException) { diff --git a/general-helpers/time/TimezoneConverter.cs b/general-helpers/time/TimezoneConverter.cs index a24df63..e1d7b2a 100644 --- a/general-helpers/time/TimezoneConverter.cs +++ b/general-helpers/time/TimezoneConverter.cs @@ -48,6 +48,9 @@ public static string GetTimezoneId(Timezone timezone) return TimezoneMappings[timezone]; } + public static string FormatDateTime(DateTime dt) => + dt.ToString("dddd, MMMM d, yyyy h:mm tt"); + /// /// Converts a local time specified by month, day, hour, and minute into UTC time, based on the provided timezone. /// diff --git a/general-helpers/time/timestamps.cs b/general-helpers/time/timestamps.cs index 565b85e..45a613d 100644 --- a/general-helpers/time/timestamps.cs +++ b/general-helpers/time/timestamps.cs @@ -53,34 +53,26 @@ public enum Formats /// The generated timestamp string. public static string FromDateTime(DateTime dateTime, Formats format, Timezone? timeZone = null) { - // Ensure the dateTime is within the range supported by DateTimeOffset if (dateTime < DateTimeOffset.MinValue.UtcDateTime) - { dateTime = DateTimeOffset.MinValue.UtcDateTime; - } else if (dateTime > DateTimeOffset.MaxValue.UtcDateTime) - { dateTime = DateTimeOffset.MaxValue.UtcDateTime; - } - - // Handle unspecified DateTime.Kind explicitly - if (dateTime.Kind == DateTimeKind.Unspecified) - { - dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Local); - } - // Adjust for the provided timezone if specified var timeZoneInfo = timeZone.HasValue - ? TZConvert.GetTimeZoneInfo(TimeConverter.GetTimezoneId(timeZone.Value)) // fix + ? TZConvert.GetTimeZoneInfo(TimeConverter.GetTimezoneId(timeZone.Value)) : TimeZoneInfo.Local; - // Convert the DateTime to the target timezone - var adjustedDateTime = TimeZoneInfo.ConvertTime(dateTime, timeZoneInfo); - - // Create DateTimeOffset with the correct offset for the timezone - var dateTimeOffset = new DateTimeOffset(adjustedDateTime, timeZoneInfo.GetUtcOffset(adjustedDateTime)); + // datetime is already in the target timezone — just attach the offset + if (dateTime.Kind == DateTimeKind.Unspecified) + { + var offset = timeZoneInfo.GetUtcOffset(dateTime); + var dto = new DateTimeOffset(dateTime, offset); + return $""; + } - // Return the formatted timestamp string + // For UTC/Local kinds, convert first + var adjusted = TimeZoneInfo.ConvertTime(dateTime, timeZoneInfo); + var dateTimeOffset = new DateTimeOffset(adjusted, timeZoneInfo.GetUtcOffset(adjusted)); return $""; } From 808bc51d6efeff0e7c66cc4f86df7c6fc3dbab25 Mon Sep 17 00:00:00 2001 From: Zach Goodson Date: Wed, 18 Mar 2026 22:03:57 -0500 Subject: [PATCH 2/6] Update Comments for Help.GetCommandMention() --- commands/no-group/helpers/help.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/no-group/helpers/help.cs b/commands/no-group/helpers/help.cs index ec03084..11d264f 100644 --- a/commands/no-group/helpers/help.cs +++ b/commands/no-group/helpers/help.cs @@ -251,10 +251,10 @@ public static int GetCommandCount() /// /// Generates a Discord command mention string. /// - /// The name of the command. + /// The name of the command. (without the slash) /// /// A properly formatted Discord command mention if the command exists; - /// otherwise, returns a plain text representation of the command. + /// otherwise, returns a plain text representation of the command such as `/command`. /// public static string GetCommandMention(string commandName) { From 526d2957652f0595be5f69c249dc5c4fb01392f3 Mon Sep 17 00:00:00 2001 From: Zach Goodson Date: Wed, 18 Mar 2026 22:05:49 -0500 Subject: [PATCH 3/6] Remove Removal MSG From Info of /concert timezones --- commands/no-group/helpers/help.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/no-group/helpers/help.cs b/commands/no-group/helpers/help.cs index 11d264f..ef7b896 100644 --- a/commands/no-group/helpers/help.cs +++ b/commands/no-group/helpers/help.cs @@ -1871,7 +1871,7 @@ public static Embed GetCommandEmbed(CommandInfo command) { Name = "timezones", InheritGroupName = true, - Description = "(TEMPORARILY REMOVED DUE TO ISSUES) Bob will convert a time from one timezone to another.", + Description = "Bob will convert a time from one timezone to another.", Url = "https://docs.bobthebot.net#convert-timezones", Parameters = [ From 53153b0be3a27cae8d692d819183575dec8a9d1b Mon Sep 17 00:00:00 2001 From: Zach Goodson Date: Wed, 18 Mar 2026 17:39:33 -0500 Subject: [PATCH 4/6] Better Error Handling For InteractionCreated() --- main.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.cs b/main.cs index c042dbe..62b978b 100644 --- a/main.cs +++ b/main.cs @@ -626,8 +626,9 @@ private static async Task InteractionCreated(SocketInteraction interaction) var ctx = new ShardedInteractionContext(Client, interaction); await interactionService.ExecuteCommandAsync(ctx, Services); } - catch + catch (Exception ex) { + Console.WriteLine($"InteractionCreated error ({interaction.Type}): {ex}"); if (interaction.Type == InteractionType.ApplicationCommand) { await interaction.GetOriginalResponseAsync() From a844ef5abc670cc5cfdd8a6cff0685a5f9d64482 Mon Sep 17 00:00:00 2001 From: Zach Goodson Date: Wed, 18 Mar 2026 17:49:31 -0500 Subject: [PATCH 5/6] Better Error Handling In Command Registration --- main.cs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/main.cs b/main.cs index 62b978b..e94fbdd 100644 --- a/main.cs +++ b/main.cs @@ -152,8 +152,11 @@ private static async Task RegisterSlashCommands() try { await interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), Services); - var globalCommands = await interactionService.RegisterCommandsGloballyAsync(); + Console.WriteLine($"[Notice] Modules loaded: {interactionService.Modules.Count()}"); + var globalCommands = await interactionService.RegisterCommandsGloballyAsync(); + Console.WriteLine($"[Notice] Commands registered: {globalCommands.Count()}"); + Help.PopulateCommandIds(globalCommands); // Optional: Register per-guild debug commands @@ -166,23 +169,28 @@ private static async Task RegisterSlashCommands() } catch (Discord.Net.HttpException ex) { - // Only ignore BASE_TYPE_REQUIRED errors if (ex.Message.Contains("BASE_TYPE_REQUIRED") || ex.Reason.Contains("BASE_TYPE_REQUIRED", StringComparison.OrdinalIgnoreCase)) { - Console.WriteLine("[Notice] Ignoring known Discord API bug BASE_TYPE_REQUIRED. Continuing startup..."); + Console.WriteLine("[Notice] BASE_TYPE_REQUIRED caught — registration was aborted."); + + var ids = await interactionService.RestClient.GetGlobalApplicationCommands(); + Help.PopulateCommandIds(ids); - if (_commandIds == null || _commandIds.Count == 0) + Console.WriteLine($"[Notice] Fetched {ids.Count()} commands via REST:"); + foreach (var cmd in ids) { - Console.WriteLine("[Notice] Attempting to fetch global command IDs via the REST client."); - var ids = await interactionService.RestClient.GetGlobalApplicationCommands(); - Help.PopulateCommandIds(ids); - Console.WriteLine("[Notice] Successfully populated command IDs via the REST client."); + Console.WriteLine($" /{cmd.Name}"); + foreach (var opt in cmd.Options) + { + Console.WriteLine( + $" Param: {opt.Name} | Type: {opt.Type} | Autocomplete: {opt.IsAutocomplete} | Choices: {opt.Choices?.Count ?? 0}" + ); + } } } else { - // Not known benign error throw; } } From d26b8e8c499232eb2515d2d31d4dd841be4c5dfb Mon Sep 17 00:00:00 2001 From: Zach Goodson Date: Wed, 18 Mar 2026 17:50:34 -0500 Subject: [PATCH 6/6] Autocompletion for Timezones | /convert timezones --- commands/convert-group/convertCommands.cs | 11 ++- .../timezoneAutoCompleteHandler.cs | 31 +++++++ general-helpers/time/Timezone.cs | 84 ++++++++----------- 3 files changed, 73 insertions(+), 53 deletions(-) create mode 100644 commands/convert-group/timezoneAutoCompleteHandler.cs diff --git a/commands/convert-group/convertCommands.cs b/commands/convert-group/convertCommands.cs index 908c4bf..ed41193 100644 --- a/commands/convert-group/convertCommands.cs +++ b/commands/convert-group/convertCommands.cs @@ -90,11 +90,18 @@ public async Task ConvertTime( [Summary("day", "The day for the time you want to convert.")][MinValue(1)][MaxValue(31)] int day, [Summary("hour", "The hour for the time you want to convert, in 24-hour format.")][MinValue(0)][MaxValue(23)] int hour, [Summary("minute", "The minute for the time you want to convert.")][MinValue(0)][MaxValue(59)] int minute, - [Summary("from-timezone", "The timezone to convert from.")] Timezone sourceTimezone, - [Summary("to-timezone", "The timezone you want to convert to.")] Timezone destinationTimezone) + [Summary("from-timezone", "The timezone to convert from.")][Autocomplete(typeof(TimezoneAutocompleteHandler))] string sourceTimezoneStr, + [Summary("to-timezone", "The timezone you want to convert to.")][Autocomplete(typeof(TimezoneAutocompleteHandler))] string destinationTimezoneStr) { try { + if (!Enum.TryParse(sourceTimezoneStr, out var sourceTimezone) || + !Enum.TryParse(destinationTimezoneStr, out var destinationTimezone)) + { + await RespondAsync("❌ Invalid timezone selected.", ephemeral: true); + return; + } + // Validate the day based on the month and current year if (day < 1 || day > DateTime.DaysInMonth(DateTime.UtcNow.Year, month)) { diff --git a/commands/convert-group/timezoneAutoCompleteHandler.cs b/commands/convert-group/timezoneAutoCompleteHandler.cs new file mode 100644 index 0000000..f68fbfd --- /dev/null +++ b/commands/convert-group/timezoneAutoCompleteHandler.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bob.Time.Timezones; +using Discord; +using Discord.Interactions; + +namespace Bob.Commands; + +public class TimezoneAutocompleteHandler : AutocompleteHandler +{ + public override Task GenerateSuggestionsAsync( + IInteractionContext context, + IAutocompleteInteraction autocompleteInteraction, + IParameterInfo parameter, + IServiceProvider services) + { + var userInput = + autocompleteInteraction.Data.Current.Value?.ToString() ?? ""; + + var results = TimezoneExtensions.FromChoiceDisplay.Keys + .Where(label => + label.Contains(userInput, StringComparison.OrdinalIgnoreCase) + ) + .Take(25) + .Select(label => new AutocompleteResult(label, label)) + .ToList(); + + return Task.FromResult(AutocompletionResult.FromSuccess(results)); + } +} \ No newline at end of file diff --git a/general-helpers/time/Timezone.cs b/general-helpers/time/Timezone.cs index cfc69b7..175b6a5 100644 --- a/general-helpers/time/Timezone.cs +++ b/general-helpers/time/Timezone.cs @@ -1,88 +1,70 @@ +using System.Collections.Generic; +using System.Linq; using Discord.Interactions; namespace Bob.Time.Timezones { - #nullable enable - public enum Timezone +#nullable enable + public enum Timezone { - [ChoiceDisplay("(DST) Dateline Standard Time")] DatelineStandardTime, // UTC-12:00 - - [ChoiceDisplay("(SST) Samoa Standard Time")] SamoaStandardTime, // UTC-11:00 - - [ChoiceDisplay("(HST) Hawaiian Standard Time")] HawaiianStandardTime, // UTC-10:00 - - [ChoiceDisplay("(AKST) Alaskan Standard Time")] AlaskanStandardTime, // UTC-09:00 - - [ChoiceDisplay("(PST) Pacific Standard Time")] PacificStandardTime, // UTC-08:00 - - [ChoiceDisplay("(MST) Mountain Standard Time")] MountainStandardTime, // UTC-07:00 - - [ChoiceDisplay("(CST) Central Standard Time")] CentralStandardTime, // UTC-06:00 - - [ChoiceDisplay("(EST) Eastern Standard Time")] EasternStandardTime, // UTC-05:00 - - [ChoiceDisplay("(AST) Atlantic Standard Time")] AtlanticStandardTime, // UTC-04:00 - - [ChoiceDisplay("(ART) Argentina Standard Time")] ArgentinaStandardTime, // UTC-03:00 - - [ChoiceDisplay("(MST) Mid-Atlantic Standard Time")] MidAtlanticStandardTime, // UTC-02:00 - - [ChoiceDisplay("(AZOT) Azores Standard Time")] AzoresStandardTime, // UTC-01:00 - - [ChoiceDisplay("(GMT) Greenwich Mean Time")] GreenwichMeanTime, // UTC±00:00 - - [ChoiceDisplay("(CET) Central European Time")] CentralEuropeanTime, // UTC+01:00 - - [ChoiceDisplay("(EET) Eastern European Time")] EasternEuropeanTime, // UTC+02:00 - - [ChoiceDisplay("(MSK) Moscow Standard Time")] MoscowStandardTime, // UTC+03:00 - - [ChoiceDisplay("(GST) Arabian Standard Time")] ArabianStandardTime, // UTC+04:00 - - [ChoiceDisplay("(PKT) Pakistan Standard Time")] PakistanStandardTime, // UTC+05:00 - - [ChoiceDisplay("(BST) Bangladesh Standard Time")] BangladeshStandardTime, // UTC+06:00 - - [ChoiceDisplay("(ICT) Indochina Time")] IndochinaTime, // UTC+07:00 - - [ChoiceDisplay("(CST) China Standard Time")] ChinaStandardTime, // UTC+08:00 - - [ChoiceDisplay("(JST) Japan Standard Time")] JapanStandardTime, // UTC+09:00 - - [ChoiceDisplay("(AEST) Australian Eastern Time")] AustralianEasternTime, // UTC+10:00 - - [ChoiceDisplay("(SBT) Solomon Islands Time")] SolomonIslandsTime, // UTC+11:00 - - [ChoiceDisplay("(NZST) New Zealand Standard Time")] NewZealandStandardTime // UTC+12:00 } public static class TimezoneExtensions { + public static readonly Dictionary FromChoiceDisplay = new() + { + { "(DST) Dateline Standard Time", Timezone.DatelineStandardTime }, + { "(SST) Samoa Standard Time", Timezone.SamoaStandardTime }, + { "(HST) Hawaiian Standard Time", Timezone.HawaiianStandardTime }, + { "(AKST) Alaskan Standard Time", Timezone.AlaskanStandardTime }, + { "(PST) Pacific Standard Time", Timezone.PacificStandardTime }, + { "(MST) Mountain Standard Time", Timezone.MountainStandardTime }, + { "(CST) Central Standard Time", Timezone.CentralStandardTime }, + { "(EST) Eastern Standard Time", Timezone.EasternStandardTime }, + { "(AST) Atlantic Standard Time", Timezone.AtlanticStandardTime }, + { "(ART) Argentina Standard Time", Timezone.ArgentinaStandardTime }, + { "(MST) Mid-Atlantic Standard Time", Timezone.MidAtlanticStandardTime }, + { "(AZOT) Azores Standard Time", Timezone.AzoresStandardTime }, + { "(GMT) Greenwich Mean Time", Timezone.GreenwichMeanTime }, + { "(CET) Central European Time", Timezone.CentralEuropeanTime }, + { "(EET) Eastern European Time", Timezone.EasternEuropeanTime }, + { "(MSK) Moscow Standard Time", Timezone.MoscowStandardTime }, + { "(GST) Arabian Standard Time", Timezone.ArabianStandardTime }, + { "(PKT) Pakistan Standard Time", Timezone.PakistanStandardTime }, + { "(BST) Bangladesh Standard Time", Timezone.BangladeshStandardTime }, + { "(ICT) Indochina Time", Timezone.IndochinaTime }, + { "(CST) China Standard Time", Timezone.ChinaStandardTime }, + { "(JST) Japan Standard Time", Timezone.JapanStandardTime }, + { "(AEST) Australian Eastern Time", Timezone.AustralianEasternTime }, + { "(SBT) Solomon Islands Time", Timezone.SolomonIslandsTime }, + { "(NZST) New Zealand Standard Time", Timezone.NewZealandStandardTime }, + }; + public static string ToDisplayName(this Timezone abbreviation) { return abbreviation switch