diff --git a/PluralKit.Bot/Commands/APICommands.cs b/PluralKit.Bot/Commands/APICommands.cs new file mode 100644 index 00000000..426646a3 --- /dev/null +++ b/PluralKit.Bot/Commands/APICommands.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using Discord; +using Discord.Commands; + +namespace PluralKit.Bot.Commands +{ + [Group("token")] + public class APICommands: ModuleBase + { + public SystemStore Systems { get; set; } + + [Command] + [MustHaveSystem] + [Remarks("token")] + public async Task GetToken() + { + // Get or make a token + var token = Context.SenderSystem.Token ?? await MakeAndSetNewToken(); + + // If we're not already in a DM, reply with a reminder to check + if (!(Context.Channel is IDMChannel)) + { + await Context.Channel.SendMessageAsync($"{Emojis.Success} Check your DMs!"); + } + + // DM the user a security disclaimer, and then the token in a separate message (for easy copying on mobile) + await Context.User.SendMessageAsync($"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:"); + await Context.User.SendMessageAsync(token); + } + + private async Task MakeAndSetNewToken() + { + Context.SenderSystem.Token = PluralKit.Utils.GenerateToken(); + await Systems.Save(Context.SenderSystem); + return Context.SenderSystem.Token; + } + + [Command("refresh")] + [MustHaveSystem] + [Alias("expire", "invalidate", "update", "new")] + [Remarks("token refresh")] + public async Task RefreshToken() + { + if (Context.SenderSystem.Token == null) + { + // If we don't have a token, call the other method instead + // This does pretty much the same thing, except words the messages more appropriately for that :) + await GetToken(); + return; + } + + // Make a new token from scratch + var token = await MakeAndSetNewToken(); + + // If we're not already in a DM, reply with a reminder to check + if (!(Context.Channel is IDMChannel)) + { + await Context.Channel.SendMessageAsync($"{Emojis.Success} Check your DMs!"); + } + + // DM the user an invalidation disclaimer, and then the token in a separate message (for easy copying on mobile) + await Context.User.SendMessageAsync($"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:"); + await Context.User.SendMessageAsync(token); + } + } +} \ No newline at end of file diff --git a/PluralKit.Core/Utils.cs b/PluralKit.Core/Utils.cs index 0026234a..24871607 100644 --- a/PluralKit.Core/Utils.cs +++ b/PluralKit.Core/Utils.cs @@ -1,8 +1,6 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; using System.Linq; +using System.Security.Cryptography; using System.Text.RegularExpressions; using NodaTime; using NodaTime.Text; @@ -24,6 +22,13 @@ namespace PluralKit return hid; } + public static string GenerateToken() + { + var buf = new byte[48]; // Results in a 64-byte Base64 string (no padding) + new RNGCryptoServiceProvider().GetBytes(buf); + return Convert.ToBase64String(buf); + } + public static string Truncate(this string str, int maxLength, string ellipsis = "...") { if (str.Length < maxLength) return str; return str.Substring(0, maxLength - ellipsis.Length) + ellipsis; @@ -225,21 +230,19 @@ namespace PluralKit { public static IPattern TimestampExportFormat = InstantPattern.CreateWithInvariantCulture("g"); public static IPattern DateExportFormat = LocalDatePattern.CreateWithInvariantCulture("yyyy-MM-dd"); - public static IPattern DurationFormat; + + // We create a composite pattern that only shows the two most significant things + // eg. if we have something with nonzero day component, we show d h, but if it's + // a smaller duration we may only bother with showing h m or m s + public static IPattern DurationFormat = new CompositePatternBuilder + { + {DurationPattern.CreateWithInvariantCulture("D'd' h'h'"), d => d.Days > 0}, + {DurationPattern.CreateWithInvariantCulture("H'h' m'm'"), d => d.Hours > 0}, + {DurationPattern.CreateWithInvariantCulture("m'm' s's'"), d => d.Minutes > 0}, + {DurationPattern.CreateWithInvariantCulture("s's'"), d => true} + }.Build(); + public static IPattern LocalDateTimeFormat = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss"); public static IPattern ZonedDateTimeFormat = ZonedDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss x", DateTimeZoneProviders.Tzdb); - - static Formats() - { - // We create a composite pattern that only shows the two most significant things - // eg. if we have something with nonzero day component, we show d h, but if it's - // a smaller duration we may only bother with showing h m or m s - var compositeDuration = new CompositePatternBuilder(); - compositeDuration.Add(DurationPattern.CreateWithInvariantCulture("D'd' h'h'"), d => d.Days > 0); - compositeDuration.Add(DurationPattern.CreateWithInvariantCulture("H'h' m'm'"), d => d.Hours > 0); - compositeDuration.Add(DurationPattern.CreateWithInvariantCulture("m'm' s's'"), d => d.Minutes > 0); - compositeDuration.Add(DurationPattern.CreateWithInvariantCulture("s's'"), d => true); - DurationFormat = compositeDuration.Build(); - } } } \ No newline at end of file