Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions commands/convert-group/convertCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,31 +90,44 @@ 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<Timezone>(sourceTimezoneStr, out var sourceTimezone) ||
!Enum.TryParse<Timezone>(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))
{
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)
{
Expand Down
31 changes: 31 additions & 0 deletions commands/convert-group/timezoneAutoCompleteHandler.cs
Original file line number Diff line number Diff line change
@@ -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<AutocompletionResult> 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));
}
}
6 changes: 3 additions & 3 deletions commands/no-group/helpers/help.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,10 @@ public static int GetCommandCount()
/// <summary>
/// Generates a Discord command mention string.
/// </summary>
/// <param name="commandName">The name of the command.</param>
/// <param name="commandName">The name of the command. (without the slash)</param>
/// <returns>
/// 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`.
/// </returns>
public static string GetCommandMention(string commandName)
{
Expand Down Expand Up @@ -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 =
[
Expand Down
84 changes: 33 additions & 51 deletions general-helpers/time/Timezone.cs
Original file line number Diff line number Diff line change
@@ -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<string, Timezone> 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
Expand Down
3 changes: 3 additions & 0 deletions general-helpers/time/TimezoneConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

/// <summary>
/// Converts a local time specified by month, day, hour, and minute into UTC time, based on the provided timezone.
/// </summary>
Expand Down
30 changes: 11 additions & 19 deletions general-helpers/time/timestamps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,34 +53,26 @@ public enum Formats
/// <returns>The generated timestamp string.</returns>
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 $"<t:{dto.ToUnixTimeSeconds()}:{(char)format}>";
}

// 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 $"<t:{dateTimeOffset.ToUnixTimeSeconds()}:{(char)format}>";
}

Expand Down
29 changes: 19 additions & 10 deletions main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -626,8 +634,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()
Expand Down
Loading