Skip to content

Commit

Permalink
Start on user settings
Browse files Browse the repository at this point in the history
  • Loading branch information
OoLunar committed Sep 29, 2024
1 parent 1fbd246 commit b2545b3
Show file tree
Hide file tree
Showing 11 changed files with 499 additions and 0 deletions.
67 changes: 67 additions & 0 deletions src/AutocompleteProviders/CultureInfoAutocompleteProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using DSharpPlus.Commands.Processors.SlashCommands;
using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers;
using DSharpPlus.Entities;

namespace OoLunar.Tomoe.AutocompleteProviders
{
public sealed class CultureInfoAutocompleteProvider : IAutoCompleteProvider

Check failure on line 12 in src/AutocompleteProviders/CultureInfoAutocompleteProvider.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoAutocompleteProvider' does not implement interface member 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)'. 'CultureInfoAutocompleteProvider.AutoCompleteAsync(AutoCompleteContext)' cannot implement 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)' because it does not have the matching return type of 'ValueTask<IReadOnlyDictionary<string, object>>'.

Check failure on line 12 in src/AutocompleteProviders/CultureInfoAutocompleteProvider.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoAutocompleteProvider' does not implement interface member 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)'. 'CultureInfoAutocompleteProvider.AutoCompleteAsync(AutoCompleteContext)' cannot implement 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)' because it does not have the matching return type of 'ValueTask<IReadOnlyDictionary<string, object>>'.

Check failure on line 12 in src/AutocompleteProviders/CultureInfoAutocompleteProvider.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoAutocompleteProvider' does not implement interface member 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)'. 'CultureInfoAutocompleteProvider.AutoCompleteAsync(AutoCompleteContext)' cannot implement 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)' because it does not have the matching return type of 'ValueTask<IReadOnlyDictionary<string, object>>'.

Check failure on line 12 in src/AutocompleteProviders/CultureInfoAutocompleteProvider.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoAutocompleteProvider' does not implement interface member 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)'. 'CultureInfoAutocompleteProvider.AutoCompleteAsync(AutoCompleteContext)' cannot implement 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)' because it does not have the matching return type of 'ValueTask<IReadOnlyDictionary<string, object>>'.
{
private static readonly CultureInfo[] _cultures;
private static readonly FrozenSet<DiscordAutoCompleteChoice> _defaultCultureList;
private static readonly FrozenDictionary<CultureInfo, string> _cultureInfoDisplayNames;

static CultureInfoAutocompleteProvider()
{
_cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
Array.Sort(_cultures, (x, y) => string.Compare(x.DisplayName, y.DisplayName, StringComparison.Ordinal));

List<DiscordAutoCompleteChoice> choices = [];
Dictionary<CultureInfo, string> cultureInfoDisplayNames = [];
for (int i = 0; i < _cultures.Length; i++)
{
string displayName = $"{_cultures[i].DisplayName} ({_cultures[i].IetfLanguageTag})";
cultureInfoDisplayNames[_cultures[i]] = displayName;

// Only add the first 25 cultures to the default list
// which is returned when the user input is empty
if (i < 25)
{
choices.Add(new DiscordAutoCompleteChoice(displayName, _cultures[i].IetfLanguageTag));
}
}

_defaultCultureList = choices.ToFrozenSet();
_cultureInfoDisplayNames = cultureInfoDisplayNames.ToFrozenDictionary();
}

public ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext context)
{
if (string.IsNullOrWhiteSpace(context.UserInput))
{
return ValueTask.FromResult<IEnumerable<DiscordAutoCompleteChoice>>(_defaultCultureList);
}

List<DiscordAutoCompleteChoice> choices = [];
foreach (CultureInfo cultureInfo in _cultures)
{
if (choices.Count >= 25)
{
break;
}
else if (cultureInfo.DisplayName.Contains(context.UserInput, StringComparison.OrdinalIgnoreCase)
|| cultureInfo.EnglishName.Contains(context.UserInput, StringComparison.OrdinalIgnoreCase)
|| cultureInfo.IetfLanguageTag.Contains(context.UserInput, StringComparison.OrdinalIgnoreCase))
{
choices.Add(new DiscordAutoCompleteChoice(_cultureInfoDisplayNames[cultureInfo], cultureInfo.Name));
}
}

return ValueTask.FromResult<IEnumerable<DiscordAutoCompleteChoice>>(choices);
}
}
}
66 changes: 66 additions & 0 deletions src/AutocompleteProviders/TimeZoneInfoAutocompleteProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Threading.Tasks;
using DSharpPlus.Commands.Processors.SlashCommands;
using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers;
using DSharpPlus.Entities;

namespace OoLunar.Tomoe.AutocompleteProviders
{
public sealed class TimeZoneInfoAutocompleteProvider : IAutoCompleteProvider

Check failure on line 11 in src/AutocompleteProviders/TimeZoneInfoAutocompleteProvider.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'TimeZoneInfoAutocompleteProvider' does not implement interface member 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)'. 'TimeZoneInfoAutocompleteProvider.AutoCompleteAsync(AutoCompleteContext)' cannot implement 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)' because it does not have the matching return type of 'ValueTask<IReadOnlyDictionary<string, object>>'.

Check failure on line 11 in src/AutocompleteProviders/TimeZoneInfoAutocompleteProvider.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'TimeZoneInfoAutocompleteProvider' does not implement interface member 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)'. 'TimeZoneInfoAutocompleteProvider.AutoCompleteAsync(AutoCompleteContext)' cannot implement 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)' because it does not have the matching return type of 'ValueTask<IReadOnlyDictionary<string, object>>'.

Check failure on line 11 in src/AutocompleteProviders/TimeZoneInfoAutocompleteProvider.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'TimeZoneInfoAutocompleteProvider' does not implement interface member 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)'. 'TimeZoneInfoAutocompleteProvider.AutoCompleteAsync(AutoCompleteContext)' cannot implement 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)' because it does not have the matching return type of 'ValueTask<IReadOnlyDictionary<string, object>>'.

Check failure on line 11 in src/AutocompleteProviders/TimeZoneInfoAutocompleteProvider.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'TimeZoneInfoAutocompleteProvider' does not implement interface member 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)'. 'TimeZoneInfoAutocompleteProvider.AutoCompleteAsync(AutoCompleteContext)' cannot implement 'IAutoCompleteProvider.AutoCompleteAsync(AutoCompleteContext)' because it does not have the matching return type of 'ValueTask<IReadOnlyDictionary<string, object>>'.
{
private static readonly TimeZoneInfo[] _timezones;
private static readonly FrozenSet<DiscordAutoCompleteChoice> _defaultTimezoneList;
private static readonly FrozenDictionary<TimeZoneInfo, string> _timezoneDisplayNames;

static TimeZoneInfoAutocompleteProvider()
{
_timezones = [.. TimeZoneInfo.GetSystemTimeZones()];
Array.Sort(_timezones, (x, y) => string.Compare(x.DisplayName, y.DisplayName, StringComparison.Ordinal));

List<DiscordAutoCompleteChoice> choices = [];
Dictionary<TimeZoneInfo, string> timezoneDisplayNames = [];
for (int i = 0; i < _timezones.Length; i++)
{
string displayName = $"{_timezones[i].DisplayName} ({_timezones[i].Id})";
timezoneDisplayNames[_timezones[i]] = displayName;

// Only add the first 25 timezones to the default list
// which is returned when the user input is empty
if (i < 25)
{
choices.Add(new DiscordAutoCompleteChoice(displayName, _timezones[i].Id));
}
}

_defaultTimezoneList = choices.ToFrozenSet();
_timezoneDisplayNames = timezoneDisplayNames.ToFrozenDictionary();
}

public ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext context)
{
if (string.IsNullOrWhiteSpace(context.UserInput))
{
return ValueTask.FromResult<IEnumerable<DiscordAutoCompleteChoice>>(_defaultTimezoneList);
}

List<DiscordAutoCompleteChoice> choices = [];
foreach (TimeZoneInfo timezone in _timezones)
{
if (choices.Count >= 25)
{
break;
}
else if (timezone.DisplayName.Contains(context.UserInput, StringComparison.OrdinalIgnoreCase)
|| timezone.StandardName.Contains(context.UserInput, StringComparison.OrdinalIgnoreCase)
|| timezone.Id.Contains(context.UserInput, StringComparison.OrdinalIgnoreCase))
{
choices.Add(new DiscordAutoCompleteChoice(_timezoneDisplayNames[timezone], timezone.Id));
}
}

return ValueTask.FromResult<IEnumerable<DiscordAutoCompleteChoice>>(choices);
}
}
}
10 changes: 10 additions & 0 deletions src/Commands/Settings/SettingsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using DSharpPlus.Commands;

namespace OoLunar.Tomoe.Commands
{
/// <summary>
/// Manages settings that the bot uses to personalize the experience for the user, server or globally.
/// </summary>
[Command("settings")]
public sealed partial class SettingsCommand;
}
37 changes: 37 additions & 0 deletions src/Commands/Settings/User/SettingsCommand.User.Culture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Globalization;
using System.Threading.Tasks;
using DSharpPlus.Commands;
using DSharpPlus.Commands.Trees.Metadata;
using OoLunar.Tomoe.Database.Models;

namespace OoLunar.Tomoe.Commands
{
public sealed partial class SettingsCommand
{
public static partial class UserSettingsCommand
{
/// <summary>
/// Sets the culture for the current user, which is responsible for formatting dates and numbers.
/// </summary>
[Command("culture"), DefaultGroupCommand]
public static async ValueTask CultureAsync(CommandContext context, CultureInfo? culture = null)
{
UserSettingsModel? userSettings = await UserSettingsModel.GetUserSettingsAsync(context.User.Id);
if (userSettings is null)
{
await context.RespondAsync(SETTINGS_NOT_SETUP);
return;
}
else if (culture is null)
{
await context.RespondAsync($"Your current culture is set to {userSettings.Culture.NativeName}/{userSettings.Culture.IetfLanguageTag}.");
return;
}

userSettings = userSettings with { Culture = culture };
await UserSettingsModel.UpdateUserSettingsAsync(userSettings);
await context.RespondAsync($"Your culture has been updated to {culture.NativeName}/{culture.IetfLanguageTag}.");
}
}
}
}
40 changes: 40 additions & 0 deletions src/Commands/Settings/User/SettingsCommand.User.List.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Threading.Tasks;
using DSharpPlus.Commands;
using DSharpPlus.Commands.Trees.Metadata;
using DSharpPlus.Entities;
using OoLunar.Tomoe.Database.Models;

namespace OoLunar.Tomoe.Commands
{
public sealed partial class SettingsCommand
{
public static partial class UserSettingsCommand
{
private const string SETTINGS_NOT_SETUP = "Your user settings have not been set up yet! Please run `/settings user setup` to set them up.";

/// <summary>
/// Lists all the settings for the current user.
/// </summary>
[Command("list"), DefaultGroupCommand]
public static async ValueTask ListSettingsAsync(CommandContext context)
{
UserSettingsModel? userSettings = await UserSettingsModel.GetUserSettingsAsync(context.User.Id);
if (userSettings is null)
{
await context.RespondAsync(SETTINGS_NOT_SETUP);
return;
}

DiscordEmbedBuilder embedBuilder = new()
{
Title = "User Settings",
Color = new DiscordColor("#6b73db")
};

embedBuilder.AddField("Culture", $"{userSettings.Culture.NativeName}/{userSettings.Culture.IetfLanguageTag}", true);
embedBuilder.AddField("Timezone", userSettings.Timezone.Id, true);
await context.RespondAsync(embedBuilder);
}
}
}
}
32 changes: 32 additions & 0 deletions src/Commands/Settings/User/SettingsCommand.User.Setup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using DSharpPlus.Commands;
using DSharpPlus.Commands.Trees.Metadata;
using OoLunar.Tomoe.Database.Models;

namespace OoLunar.Tomoe.Commands
{
public sealed partial class SettingsCommand
{
public static partial class UserSettingsCommand
{
/// <summary>
/// Sets the timezone for the current user, which is responsible for time-based commands such as reminders.
/// </summary>
[Command("setup"), DefaultGroupCommand]
public static async ValueTask SetupAsync(CommandContext context, CultureInfo? culture = null, TimeZoneInfo? timezone = null)
{
UserSettingsModel userSettings = new()
{
UserId = context.User.Id,
Culture = culture ?? CultureInfo.CurrentCulture,
Timezone = timezone ?? TimeZoneInfo.Local
};

await UserSettingsModel.UpdateUserSettingsAsync(userSettings);
await context.RespondAsync($"Your user settings have been set up with the culture {userSettings.Culture.NativeName}/{userSettings.Culture.IetfLanguageTag} and timezone {userSettings.Timezone.DisplayName}/{userSettings.Timezone.Id}.");
}
}
}
}
37 changes: 37 additions & 0 deletions src/Commands/Settings/User/SettingsCommand.User.Timezone.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Threading.Tasks;
using DSharpPlus.Commands;
using DSharpPlus.Commands.Trees.Metadata;
using OoLunar.Tomoe.Database.Models;

namespace OoLunar.Tomoe.Commands
{
public sealed partial class SettingsCommand
{
public static partial class UserSettingsCommand
{
/// <summary>
/// Sets the timezone for the current user, which is responsible for time-based commands such as reminders.
/// </summary>
[Command("timezone"), DefaultGroupCommand]
public static async ValueTask CultureAsync(CommandContext context, TimeZoneInfo? timezone = null)
{
UserSettingsModel? userSettings = await UserSettingsModel.GetUserSettingsAsync(context.User.Id);
if (userSettings is null)
{
await context.RespondAsync(SETTINGS_NOT_SETUP);
return;
}
else if (timezone is null)
{
await context.RespondAsync($"Your current culture is set to {userSettings.Timezone.DisplayName}/{userSettings.Timezone.Id}.");
return;
}

userSettings = userSettings with { Timezone = timezone };
await UserSettingsModel.UpdateUserSettingsAsync(userSettings);
await context.RespondAsync($"Your culture has been updated to {timezone.DisplayName}/{timezone.Id}.");
}
}
}
}
13 changes: 13 additions & 0 deletions src/Commands/Settings/User/SettingsCommand.User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using DSharpPlus.Commands;

namespace OoLunar.Tomoe.Commands
{
public sealed partial class SettingsCommand
{
/// <summary>
/// Manages settings for the current user, such as their culture and timezone.
/// </summary>
[Command("user")]
public static partial class UserSettingsCommand;
}
}
38 changes: 38 additions & 0 deletions src/Converters/CultureInfoConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Globalization;
using System.Threading.Tasks;
using DSharpPlus.Commands.Converters;
using DSharpPlus.Commands.Processors.SlashCommands;
using DSharpPlus.Commands.Processors.TextCommands;
using DSharpPlus.Entities;

namespace OoLunar.Tomoe.Converters
{
public sealed class CultureInfoConverter : ISlashArgumentConverter<CultureInfo>, ITextArgumentConverter<CultureInfo>

Check failure on line 10 in src/Converters/CultureInfoConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoConverter' does not implement interface member 'IArgumentConverter<InteractionConverterContext, InteractionCreatedEventArgs, CultureInfo>.ConvertAsync(InteractionConverterContext, InteractionCreatedEventArgs)'

Check failure on line 10 in src/Converters/CultureInfoConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoConverter' does not implement interface member 'IArgumentConverter<TextConverterContext, MessageCreatedEventArgs, CultureInfo>.ConvertAsync(TextConverterContext, MessageCreatedEventArgs)'

Check failure on line 10 in src/Converters/CultureInfoConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoConverter' does not implement interface member 'IArgumentConverter<InteractionConverterContext, InteractionCreatedEventArgs, CultureInfo>.ConvertAsync(InteractionConverterContext, InteractionCreatedEventArgs)'

Check failure on line 10 in src/Converters/CultureInfoConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoConverter' does not implement interface member 'IArgumentConverter<TextConverterContext, MessageCreatedEventArgs, CultureInfo>.ConvertAsync(TextConverterContext, MessageCreatedEventArgs)'

Check failure on line 10 in src/Converters/CultureInfoConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoConverter' does not implement interface member 'IArgumentConverter<InteractionConverterContext, InteractionCreatedEventArgs, CultureInfo>.ConvertAsync(InteractionConverterContext, InteractionCreatedEventArgs)'

Check failure on line 10 in src/Converters/CultureInfoConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoConverter' does not implement interface member 'IArgumentConverter<TextConverterContext, MessageCreatedEventArgs, CultureInfo>.ConvertAsync(TextConverterContext, MessageCreatedEventArgs)'

Check failure on line 10 in src/Converters/CultureInfoConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoConverter' does not implement interface member 'IArgumentConverter<InteractionConverterContext, InteractionCreatedEventArgs, CultureInfo>.ConvertAsync(InteractionConverterContext, InteractionCreatedEventArgs)'

Check failure on line 10 in src/Converters/CultureInfoConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'CultureInfoConverter' does not implement interface member 'IArgumentConverter<TextConverterContext, MessageCreatedEventArgs, CultureInfo>.ConvertAsync(TextConverterContext, MessageCreatedEventArgs)'
{
public bool RequiresText => true;
public string ReadableName => "Culture Info";
public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String;

public Task<Optional<CultureInfo>> ConvertAsync(ConverterContext context)
{
try
{
return context.Argument switch
{
// String will be from text commands
string languageTag => Task.FromResult(Optional.FromValue(CultureInfo.GetCultureInfoByIetfLanguageTag(languageTag))),

// Int will be from slash commands, either through a choice provider or autocomplete
int cultureId => Task.FromResult(Optional.FromValue(CultureInfo.GetCultureInfo(cultureId))),

// What the fuck.
_ => Task.FromResult(Optional.FromNoValue<CultureInfo>())
};
}
catch
{
return Task.FromResult(Optional.FromNoValue<CultureInfo>());
}
}
}
}
30 changes: 30 additions & 0 deletions src/Converters/TimeZoneConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
using DSharpPlus.Commands.Converters;
using DSharpPlus.Commands.Processors.SlashCommands;
using DSharpPlus.Commands.Processors.TextCommands;
using DSharpPlus.Entities;

namespace OoLunar.Tomoe.Converters
{
public sealed class TimeZoneInfoConverter : ISlashArgumentConverter<TimeZoneInfo>, ITextArgumentConverter<TimeZoneInfo>

Check failure on line 10 in src/Converters/TimeZoneConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'TimeZoneInfoConverter' does not implement interface member 'IArgumentConverter<InteractionConverterContext, InteractionCreatedEventArgs, TimeZoneInfo>.ConvertAsync(InteractionConverterContext, InteractionCreatedEventArgs)'

Check failure on line 10 in src/Converters/TimeZoneConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'TimeZoneInfoConverter' does not implement interface member 'IArgumentConverter<TextConverterContext, MessageCreatedEventArgs, TimeZoneInfo>.ConvertAsync(TextConverterContext, MessageCreatedEventArgs)'

Check failure on line 10 in src/Converters/TimeZoneConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'TimeZoneInfoConverter' does not implement interface member 'IArgumentConverter<InteractionConverterContext, InteractionCreatedEventArgs, TimeZoneInfo>.ConvertAsync(InteractionConverterContext, InteractionCreatedEventArgs)'

Check failure on line 10 in src/Converters/TimeZoneConverter.cs

View workflow job for this annotation

GitHub Actions / Build Commit

'TimeZoneInfoConverter' does not implement interface member 'IArgumentConverter<TextConverterContext, MessageCreatedEventArgs, TimeZoneInfo>.ConvertAsync(TextConverterContext, MessageCreatedEventArgs)'
{
public bool RequiresText => true;
public string ReadableName => "Timezone";
public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String;

public Task<Optional<TimeZoneInfo>> ConvertAsync(ConverterContext context)
{
if (context.Argument is not string argument)
{
return Task.FromResult(Optional.FromNoValue<TimeZoneInfo>());
}
else if (TimeZoneInfo.TryFindSystemTimeZoneById(argument, out TimeZoneInfo? timeZoneInfo))
{
return Task.FromResult(Optional.FromValue(timeZoneInfo));
}

return Task.FromResult(Optional.FromNoValue<TimeZoneInfo>());
}
}
}
Loading

0 comments on commit b2545b3

Please sign in to comment.