Expand system time selection logic
This commit is contained in:
parent
72cb838ad7
commit
cd9a3e0abd
@ -157,8 +157,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var zones = DateTimeZoneProviders.Tzdb;
|
var zone = await FindTimeZone(zoneStr);
|
||||||
var zone = zones.GetZoneOrNull(zoneStr);
|
|
||||||
if (zone == null) throw Errors.InvalidTimeZone(zoneStr);
|
if (zone == null) throw Errors.InvalidTimeZone(zoneStr);
|
||||||
|
|
||||||
var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone);
|
var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone);
|
||||||
@ -171,6 +170,64 @@ namespace PluralKit.Bot.Commands
|
|||||||
await Context.Channel.SendMessageAsync($"System time zone changed to {zone.Id}.");
|
await Context.Channel.SendMessageAsync($"System time zone changed to {zone.Id}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DateTimeZone> FindTimeZone(string zoneStr) {
|
||||||
|
// First, if we're given a flag emoji, we extract the flag emoji code from it.
|
||||||
|
zoneStr = PluralKit.Utils.ExtractCountryFlag(zoneStr) ?? zoneStr;
|
||||||
|
|
||||||
|
// Then, we find all *locations* matching either the given country code or the country name.
|
||||||
|
var locations = TzdbDateTimeZoneSource.Default.Zone1970Locations;
|
||||||
|
var matchingLocations = locations.Where(l => l.Countries.Any(c =>
|
||||||
|
string.Equals(c.Code, zoneStr, StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
|
string.Equals(c.Name, zoneStr, StringComparison.InvariantCultureIgnoreCase)));
|
||||||
|
|
||||||
|
// Then, we find all (unique) time zone IDs that match.
|
||||||
|
var matchingZones = matchingLocations.Select(l => DateTimeZoneProviders.Tzdb.GetZoneOrNull(l.ZoneId))
|
||||||
|
.Distinct().ToList();
|
||||||
|
|
||||||
|
// If the set of matching zones is empty (ie. we didn't find anything), we try a few other things.
|
||||||
|
if (matchingZones.Count == 0)
|
||||||
|
{
|
||||||
|
// First, we try to just find the time zone given directly and return that.
|
||||||
|
var givenZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(zoneStr);
|
||||||
|
if (givenZone != null) return givenZone;
|
||||||
|
|
||||||
|
// If we didn't find anything there either, we try parsing the string as an offset, then
|
||||||
|
// find all possible zones that match that offset. For an offset like UTC+2, this doesn't *quite*
|
||||||
|
// work, since there are 57(!) matching zones (as of 2019-06-13) - but for less populated time zones
|
||||||
|
// this could work nicely.
|
||||||
|
var inputWithoutUtc = zoneStr.Replace("UTC", "").Replace("GMT", "");
|
||||||
|
|
||||||
|
var res = OffsetPattern.CreateWithInvariantCulture("+H").Parse(inputWithoutUtc);
|
||||||
|
if (!res.Success) res = OffsetPattern.CreateWithInvariantCulture("+H:mm").Parse(inputWithoutUtc);
|
||||||
|
|
||||||
|
// If *this* didn't parse correctly, fuck it, bail.
|
||||||
|
if (!res.Success) return null;
|
||||||
|
var offset = res.Value;
|
||||||
|
|
||||||
|
// To try to reduce the count, we go by locations from the 1970+ database instead of just the full database
|
||||||
|
// This elides regions that have been identical since 1970, omitting small distinctions due to Ancient History(tm).
|
||||||
|
var allZones = TzdbDateTimeZoneSource.Default.Zone1970Locations.Select(l => l.ZoneId).Distinct();
|
||||||
|
matchingZones = allZones.Select(z => DateTimeZoneProviders.Tzdb.GetZoneOrNull(z))
|
||||||
|
.Where(z => z.GetUtcOffset(SystemClock.Instance.GetCurrentInstant()) == offset).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a list of viable time zones, we ask the user which is correct.
|
||||||
|
|
||||||
|
// If we only have one, return that one.
|
||||||
|
if (matchingZones.Count == 1)
|
||||||
|
return matchingZones.First();
|
||||||
|
|
||||||
|
// Otherwise, prompt and return!
|
||||||
|
return await Context.Choose("There were multiple matches for your time zone query. Please select the region that matches you the closest:", matchingZones,
|
||||||
|
z =>
|
||||||
|
{
|
||||||
|
if (TzdbDateTimeZoneSource.Default.Aliases.Contains(z.Id))
|
||||||
|
return $"**{z.Id}**, {string.Join(", ", TzdbDateTimeZoneSource.Default.Aliases[z.Id])}";
|
||||||
|
|
||||||
|
return $"**{z.Id}**";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<PKSystem> ReadContextParameterAsync(string value)
|
public override async Task<PKSystem> ReadContextParameterAsync(string value)
|
||||||
{
|
{
|
||||||
var res = await new PKSystemTypeReader().ReadAsync(Context, value, _services);
|
var res = await new PKSystemTypeReader().ReadAsync(Context, value, _services);
|
||||||
|
@ -56,6 +56,8 @@ namespace PluralKit.Bot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Paginate<T>(this ICommandContext ctx, ICollection<T> items, int itemsPerPage, string title, Action<EmbedBuilder, IEnumerable<T>> renderer) {
|
public static async Task Paginate<T>(this ICommandContext ctx, ICollection<T> items, int itemsPerPage, string title, Action<EmbedBuilder, IEnumerable<T>> renderer) {
|
||||||
|
// TODO: make this generic enough we can use it in Choose<T> below
|
||||||
|
|
||||||
var pageCount = (items.Count / itemsPerPage) + 1;
|
var pageCount = (items.Count / itemsPerPage) + 1;
|
||||||
Embed MakeEmbedForPage(int page) {
|
Embed MakeEmbedForPage(int page) {
|
||||||
var eb = new EmbedBuilder();
|
var eb = new EmbedBuilder();
|
||||||
@ -94,6 +96,88 @@ namespace PluralKit.Bot {
|
|||||||
else await msg.RemoveReactionsAsync(ctx.Client.CurrentUser, botEmojis);
|
else await msg.RemoveReactionsAsync(ctx.Client.CurrentUser, botEmojis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<T> Choose<T>(this ICommandContext ctx, string description, IList<T> items, Func<T, string> display = null)
|
||||||
|
{
|
||||||
|
// Generate a list of :regional_indicator_?: emoji surrogate pairs (starting at codepoint 0x1F1E6)
|
||||||
|
// We just do 7 (ABCDEFG), this amount is arbitrary (although sending a lot of emojis takes a while)
|
||||||
|
var pageSize = 7;
|
||||||
|
var indicators = new string[pageSize];
|
||||||
|
for (var i = 0; i < pageSize; i++) indicators[i] = char.ConvertFromUtf32(0x1F1E6 + i);
|
||||||
|
|
||||||
|
// Default to x.ToString()
|
||||||
|
if (display == null) display = x => x.ToString();
|
||||||
|
|
||||||
|
string MakeOptionList(int page)
|
||||||
|
{
|
||||||
|
var makeOptionList = string.Join("\n", items
|
||||||
|
.Skip(page * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select((x, i) => $"{indicators[i]} {display(x)}"));
|
||||||
|
return makeOptionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have more items than the page size, we paginate as appropriate
|
||||||
|
if (items.Count > pageSize)
|
||||||
|
{
|
||||||
|
var currPage = 0;
|
||||||
|
var pageCount = (items.Count-1) / pageSize + 1;
|
||||||
|
|
||||||
|
// Send the original message
|
||||||
|
var msg = await ctx.Channel.SendMessageAsync($"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}");
|
||||||
|
|
||||||
|
// Add back/forward reactions and the actual indicator emojis
|
||||||
|
async Task AddEmojis()
|
||||||
|
{
|
||||||
|
await msg.AddReactionAsync(new Emoji("\u2B05"));
|
||||||
|
await msg.AddReactionAsync(new Emoji("\u27A1"));
|
||||||
|
for (int i = 0; i < items.Count; i++) await msg.AddReactionAsync(new Emoji(indicators[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
AddEmojis(); // Not concerned about awaiting
|
||||||
|
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// Wait for a reaction
|
||||||
|
var reaction = await ctx.AwaitReaction(msg, ctx.User);
|
||||||
|
|
||||||
|
// If it's a movement reaction, inc/dec the page index
|
||||||
|
if (reaction.Emote.Name == "\u2B05") currPage -= 1; // <
|
||||||
|
if (reaction.Emote.Name == "\u27A1") currPage += 1; // >
|
||||||
|
if (currPage < 0) currPage += pageCount;
|
||||||
|
if (currPage >= pageCount) currPage -= pageCount;
|
||||||
|
|
||||||
|
// If it's an indicator emoji, return the relevant item
|
||||||
|
if (indicators.Contains(reaction.Emote.Name))
|
||||||
|
{
|
||||||
|
var idx = Array.IndexOf(indicators, reaction.Emote.Name) + pageSize * currPage;
|
||||||
|
// only if it's in bounds, though
|
||||||
|
// eg. 8 items, we're on page 2, and I hit D (3 + 1*7 = index 10 on an 8-long list) = boom
|
||||||
|
if (idx < items.Count) return items[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.RemoveReactionAsync(reaction.Emote, ctx.User); // don't care about awaiting
|
||||||
|
await msg.ModifyAsync(mp => mp.Content = $"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var msg = await ctx.Channel.SendMessageAsync($"{description}\n{MakeOptionList(0)}");
|
||||||
|
|
||||||
|
// Add the relevant reactions (we don't care too much about awaiting)
|
||||||
|
async Task AddEmojis()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < items.Count; i++) await msg.AddReactionAsync(new Emoji(indicators[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
AddEmojis();
|
||||||
|
|
||||||
|
// Then wait for a reaction and return whichever one we found
|
||||||
|
var reaction = await ctx.AwaitReaction(msg, ctx.User,rx => indicators.Contains(rx.Emote.Name));
|
||||||
|
return items[Array.IndexOf(indicators, reaction.Emote.Name)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<ChannelPermissions> Permissions(this ICommandContext ctx) {
|
public static async Task<ChannelPermissions> Permissions(this ICommandContext ctx) {
|
||||||
if (ctx.Channel is IGuildChannel) {
|
if (ctx.Channel is IGuildChannel) {
|
||||||
var gu = await ctx.Guild.GetCurrentUserAsync();
|
var gu = await ctx.Guild.GetCurrentUserAsync();
|
||||||
|
@ -60,5 +60,7 @@ namespace PluralKit.Bot {
|
|||||||
|
|
||||||
public static PKError InvalidTimeZone(string zoneStr) => new PKError($"Invalid time zone ID '{zoneStr}'. To find your time zone ID, use the following website: <https://xske.github.io/tz>");
|
public static PKError InvalidTimeZone(string zoneStr) => new PKError($"Invalid time zone ID '{zoneStr}'. To find your time zone ID, use the following website: <https://xske.github.io/tz>");
|
||||||
public static PKError TimezoneChangeCancelled => new PKError("Time zone change cancelled.");
|
public static PKError TimezoneChangeCancelled => new PKError("Time zone change cancelled.");
|
||||||
|
|
||||||
|
public static PKError AmbiguousTimeZone(string zoneStr, int count) => new PKError($"The time zone query '{zoneStr}' resulted in **{count}** different time zone regions. Try being more specific - e.g. pass an exact time zone specifier from the following website: <https://xske.github.io/tz>");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@ -9,6 +10,7 @@ using Discord.Commands;
|
|||||||
using Discord.Commands.Builders;
|
using Discord.Commands.Builders;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NodaTime;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
|
|
||||||
namespace PluralKit.Bot
|
namespace PluralKit.Bot
|
||||||
|
@ -194,6 +194,23 @@ namespace PluralKit
|
|||||||
// Still haven't parsed something, we just give up lmao
|
// Still haven't parsed something, we just give up lmao
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ExtractCountryFlag(string flag)
|
||||||
|
{
|
||||||
|
if (flag.Length != 4) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cp1 = char.ConvertToUtf32(flag, 0);
|
||||||
|
var cp2 = char.ConvertToUtf32(flag, 2);
|
||||||
|
if (cp1 < 0x1F1E6 || cp1 > 0x1F1FF) return null;
|
||||||
|
if (cp2 < 0x1F1E6 || cp2 > 0x1F1FF) return null;
|
||||||
|
return $"{(char) (cp1 - 0x1F1E6 + 'A')}{(char) (cp2 - 0x1F1E6 + 'A')}";
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Emojis {
|
public static class Emojis {
|
||||||
|
Loading…
Reference in New Issue
Block a user