From 62dc2ce78eeb94f1659b2216d0962f505b8106ad Mon Sep 17 00:00:00 2001 From: Ske Date: Mon, 13 May 2019 22:44:49 +0200 Subject: [PATCH] bot: add birthday command --- PluralKit.Bot/Bot.cs | 9 ++++ PluralKit.Bot/Commands/MemberCommands.cs | 23 +++++++- PluralKit.Bot/Errors.cs | 3 +- PluralKit.Bot/Utils.cs | 14 +++++ PluralKit.Core/Models.cs | 14 +++-- PluralKit.Core/PluralKit.Core.csproj | 1 + PluralKit.Core/Utils.cs | 67 ++++++++++++++++++++++++ 7 files changed, 124 insertions(+), 7 deletions(-) diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index 13f4d545..bd3810f2 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -10,6 +10,7 @@ using Discord.Commands; using Discord.WebSocket; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using NodaTime; using Npgsql; namespace PluralKit.Bot @@ -34,6 +35,14 @@ namespace PluralKit.Bot SqlMapper.AddTypeHandler(new UlongEncodeAsLongHandler()); Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; + // Also, use NodaTime. it's good. + NpgsqlConnection.GlobalTypeMapper.UseNodaTime(); + // With the thing we add above, Npgsql already handles NodaTime integration + // This makes Dapper confused since it thinks it has to convert it anyway and doesn't understand the types + // So we add a custom type handler that literally just passes the type through to Npgsql + SqlMapper.AddTypeHandler(new PassthroughTypeHandler()); + SqlMapper.AddTypeHandler(new PassthroughTypeHandler()); + using (var services = BuildServiceProvider()) { Console.WriteLine("- Connecting to database..."); diff --git a/PluralKit.Bot/Commands/MemberCommands.cs b/PluralKit.Bot/Commands/MemberCommands.cs index 51e91060..bf511d1f 100644 --- a/PluralKit.Bot/Commands/MemberCommands.cs +++ b/PluralKit.Bot/Commands/MemberCommands.cs @@ -2,6 +2,7 @@ using System; using System.Text.RegularExpressions; using System.Threading.Tasks; using Discord.Commands; +using NodaTime; namespace PluralKit.Bot.Commands { @@ -118,11 +119,31 @@ namespace PluralKit.Bot.Commands await Context.Channel.SendMessageAsync($"{Emojis.Success} Member color {(color == null ? "cleared" : "changed")}."); } + [Command("birthday")] + [Alias("birthdate", "bday", "cakeday", "bdate")] + [Remarks("member birthday ")] + [MustPassOwnMember] + public async Task MemberBirthday([Remainder] string birthday = null) + { + LocalDate? date = null; + if (birthday != null) + { + date = PluralKit.Utils.ParseDate(birthday, true); + if (date == null) throw Errors.BirthdayParseError(birthday); + } + + ContextEntity.Birthday = date; + await Members.Save(ContextEntity); + + await Context.Channel.SendMessageAsync($"{Emojis.Success} Member birthdate {(date == null ? "cleared" : $"changed to {ContextEntity.BirthdayString}")}."); + } + [Command] + [Alias("view", "show", "info")] [Remarks("member")] public async Task ViewMember(PKMember member) { - var system = await Systems.GetById(member.Id); + var system = await Systems.GetById(member.System); await Context.Channel.SendMessageAsync(embed: await Embeds.CreateMemberEmbed(system, member)); } diff --git a/PluralKit.Bot/Errors.cs b/PluralKit.Bot/Errors.cs index f4caee3c..45b4e0df 100644 --- a/PluralKit.Bot/Errors.cs +++ b/PluralKit.Bot/Errors.cs @@ -14,6 +14,7 @@ namespace PluralKit.Bot { public static PKError MemberNameTooLongError(int length) => new PKError($"Member name too long ({length}/{Limits.MaxMemberNameLength} characters)."); public static PKError MemberPronounsTooLongError(int length) => new PKError($"Member pronouns too long ({length}/{Limits.MaxMemberNameLength} characters)."); - public static PKError InvalidColorError(string color) => new PKError($"{color} is not a valid color. Color must be in hex format (eg. #ff0000)."); + public static PKError InvalidColorError(string color) => new PKError($"\"{color}\" is not a valid color. Color must be in hex format (eg. #ff0000)."); + public static PKError BirthdayParseError(string birthday) => new PKError($"\"{birthday}\" could not be parsed as a valid date. Try a format like \"2016-12-24\" or \"May 3 1996\"."); } } \ No newline at end of file diff --git a/PluralKit.Bot/Utils.cs b/PluralKit.Bot/Utils.cs index 161fa8c1..a9b9dfa1 100644 --- a/PluralKit.Bot/Utils.cs +++ b/PluralKit.Bot/Utils.cs @@ -9,6 +9,7 @@ using Discord.Commands; using Discord.Commands.Builders; using Discord.WebSocket; using Microsoft.Extensions.DependencyInjection; +using NodaTime; namespace PluralKit.Bot { @@ -32,6 +33,19 @@ namespace PluralKit.Bot } } + class PassthroughTypeHandler : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, T value) + { + parameter.Value = value; + } + + public override T Parse(object value) + { + return (T) value; + } + } + class PKSystemTypeReader : TypeReader { public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) diff --git a/PluralKit.Core/Models.cs b/PluralKit.Core/Models.cs index 5afacfa5..98031ad4 100644 --- a/PluralKit.Core/Models.cs +++ b/PluralKit.Core/Models.cs @@ -1,5 +1,7 @@ using System; using Dapper.Contrib.Extensions; +using NodaTime; +using NodaTime.Text; namespace PluralKit { @@ -14,7 +16,7 @@ namespace PluralKit public string Tag { get; set; } public string AvatarUrl { get; set; } public string Token { get; set; } - public DateTime Created { get; set; } + public Instant Created { get; set; } public string UiTz { get; set; } public int MaxMemberNameLength => Tag != null ? 32 - Tag.Length - 1 : 32; @@ -29,12 +31,12 @@ namespace PluralKit public string Color { get; set; } public string AvatarUrl { get; set; } public string Name { get; set; } - public DateTime? Birthday { get; set; } + public LocalDate? Birthday { get; set; } public string Pronouns { get; set; } public string Description { get; set; } public string Prefix { get; set; } public string Suffix { get; set; } - public DateTime Created { get; set; } + public Instant Created { get; set; } /// Returns a formatted string representing the member's birthday, taking into account that a year of "0001" is hidden public string BirthdayString @@ -42,8 +44,10 @@ namespace PluralKit get { if (Birthday == null) return null; - if (Birthday?.Year == 1) return Birthday?.ToString("MMMM dd"); - return Birthday?.ToString("MMMM dd, yyyy"); + + var format = LocalDatePattern.CreateWithInvariantCulture("MMM dd, yyyy"); + if (Birthday?.Year == 1) format = LocalDatePattern.CreateWithInvariantCulture("MMM dd"); + return format.Format(Birthday.Value); } } diff --git a/PluralKit.Core/PluralKit.Core.csproj b/PluralKit.Core/PluralKit.Core.csproj index 12484212..817f3249 100644 --- a/PluralKit.Core/PluralKit.Core.csproj +++ b/PluralKit.Core/PluralKit.Core.csproj @@ -12,6 +12,7 @@ + diff --git a/PluralKit.Core/Utils.cs b/PluralKit.Core/Utils.cs index a4afe474..943ec7fc 100644 --- a/PluralKit.Core/Utils.cs +++ b/PluralKit.Core/Utils.cs @@ -1,4 +1,11 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using NodaTime; +using NodaTime.Text; namespace PluralKit @@ -27,6 +34,66 @@ namespace PluralKit if (str != null) return str.Length > length; return false; } + + public static Duration? ParsePeriod(string str) + { + + Duration d = Duration.Zero; + + foreach (Match match in Regex.Matches(str, "(\\d{1,3})(\\w)")) + { + var amount = int.Parse(match.Groups[1].Value); + var type = match.Groups[2].Value; + + if (type == "w") d += Duration.FromDays(7) * amount; + else if (type == "d") d += Duration.FromDays(1) * amount; + else if (type == "h") d += Duration.FromHours(1) * amount; + else if (type == "m") d += Duration.FromMinutes(1) * amount; + else if (type == "s") d += Duration.FromSeconds(1) * amount; + else return null; + } + + if (d == Duration.Zero) return null; + return d; + } + + public static LocalDate? ParseDate(string str, bool allowNullYear = false) + { + // NodaTime can't parse constructs like "1st" and "2nd" so we quietly replace those away + // Gotta make sure to do the regex otherwise we'll catch things like the "st" in "August" too + str = Regex.Replace(str, "(\\d+)(st|nd|rd|th)", "$1"); + + var patterns = new[] + { + "MMM d yyyy", // Jan 1 2019 + "MMM d, yyyy", // Jan 1, 2019 + "MMMM d yyyy", // January 1 2019 + "MMMM d, yyyy", // January 1, 2019 + "yyyy-MM-dd", // 2019-01-01 + "yyyy MM dd", // 2019 01 01 + "yyyy/MM/dd" // 2019/01/01 + }.ToList(); + + if (allowNullYear) patterns.AddRange(new[] + { + "MMM d", // Jan 1 + "MMMM d", // January 1 + "MM-dd", // 01-01 + "MM dd", // 01 01 + "MM/dd" // 01-01 + }); + + // Giving a template value so year will be parsed as 0001 if not present + // This means we can later disambiguate whether a null year was given + // TODO: should we be using invariant culture here? + foreach (var pattern in patterns.Select(p => LocalDatePattern.CreateWithInvariantCulture(p).WithTemplateValue(new LocalDate(0001, 1, 1)))) + { + var result = pattern.Parse(str); + if (result.Success) return result.Value; + } + + return null; + } } public static class Emojis {