From 0f2228582404ea80df161168d4d7dda9406b3716 Mon Sep 17 00:00:00 2001 From: Ske Date: Sat, 28 Dec 2019 15:52:59 +0100 Subject: [PATCH] Upgrade API serialisation code to enable potential context-based serialisation --- .../Controllers/AccountController.cs | 6 +- PluralKit.API/Controllers/MemberController.cs | 96 +++++-------- .../Controllers/MessageController.cs | 10 +- PluralKit.API/Controllers/SystemController.cs | 45 +++---- PluralKit.API/Startup.cs | 3 +- PluralKit.Core/Models.cs | 126 +++++++++++------- PluralKit.Core/Utils.cs | 7 + 7 files changed, 149 insertions(+), 144 deletions(-) diff --git a/PluralKit.API/Controllers/AccountController.cs b/PluralKit.API/Controllers/AccountController.cs index 591927da..c54f5849 100644 --- a/PluralKit.API/Controllers/AccountController.cs +++ b/PluralKit.API/Controllers/AccountController.cs @@ -1,6 +1,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; + namespace PluralKit.API.Controllers { [ApiController] @@ -16,12 +18,12 @@ namespace PluralKit.API.Controllers } [HttpGet("{aid}")] - public async Task> GetSystemByAccount(ulong aid) + public async Task> GetSystemByAccount(ulong aid) { var system = await _data.GetSystemByAccount(aid); if (system == null) return NotFound("Account not found."); - return Ok(system); + return Ok(system.ToJson()); } } } \ No newline at end of file diff --git a/PluralKit.API/Controllers/MemberController.cs b/PluralKit.API/Controllers/MemberController.cs index 73f44c78..2f8513c2 100644 --- a/PluralKit.API/Controllers/MemberController.cs +++ b/PluralKit.API/Controllers/MemberController.cs @@ -1,6 +1,9 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; + +using Newtonsoft.Json.Linq; + using PluralKit.Core; namespace PluralKit.API.Controllers @@ -20,103 +23,67 @@ namespace PluralKit.API.Controllers } [HttpGet("{hid}")] - public async Task> GetMember(string hid) + public async Task> GetMember(string hid) { var member = await _data.GetMemberByHid(hid); if (member == null) return NotFound("Member not found."); - return Ok(member); + return Ok(member.ToJson()); } [HttpPost] [RequiresSystem] - public async Task> PostMember([FromBody] PKMember newMember) + public async Task> PostMember([FromBody] JObject properties) { var system = _auth.CurrentSystem; - if (newMember.Name == null) - return BadRequest("Member name cannot be null."); + if (!properties.ContainsKey("name")) + return BadRequest("Member name must be specified."); // Enforce per-system member limit var memberCount = await _data.GetSystemMemberCount(system); if (memberCount >= Limits.MaxMemberCount) return BadRequest($"Member limit reached ({memberCount} / {Limits.MaxMemberCount})."); - // Explicit bounds checks - if (newMember.Name != null && newMember.Name.Length > Limits.MaxMemberNameLength) - return BadRequest($"Member name too long ({newMember.Name.Length} > {Limits.MaxMemberNameLength}."); - if (newMember.DisplayName != null && newMember.DisplayName.Length > Limits.MaxMemberNameLength) - return BadRequest($"Member display name too long ({newMember.DisplayName.Length} > {Limits.MaxMemberNameLength}."); - if (newMember.Pronouns != null && newMember.Pronouns.Length > Limits.MaxPronounsLength) - return BadRequest($"Member pronouns too long ({newMember.Pronouns.Length} > {Limits.MaxPronounsLength}."); - if (newMember.Description != null && newMember.Description.Length > Limits.MaxDescriptionLength) - return BadRequest($"Member descriptions too long ({newMember.Description.Length} > {Limits.MaxDescriptionLength}."); - - // Sanity bounds checks - if (newMember.AvatarUrl != null && newMember.AvatarUrl.Length > 1000) - return BadRequest(); - if (newMember.ProxyTags?.Any(tag => tag.Prefix.Length > 1000 || tag.Suffix.Length > 1000) ?? false) - return BadRequest(); - - var member = await _data.CreateMember(system, newMember.Name); - - member.Name = newMember.Name; - member.DisplayName = newMember.DisplayName; - member.Color = newMember.Color; - member.AvatarUrl = newMember.AvatarUrl; - member.Birthday = newMember.Birthday; - member.Pronouns = newMember.Pronouns; - member.Description = newMember.Description; - member.ProxyTags = newMember.ProxyTags; - member.KeepProxy = newMember.KeepProxy; + var member = await _data.CreateMember(system, properties.Value("name")); + try + { + member.Apply(properties); + } + catch (PKParseError e) + { + return BadRequest(e.Message); + } + await _data.SaveMember(member); - - return Ok(member); + return Ok(member.ToJson()); } [HttpPatch("{hid}")] [RequiresSystem] - public async Task> PatchMember(string hid, [FromBody] PKMember newMember) + public async Task> PatchMember(string hid, [FromBody] JObject changes) { var member = await _data.GetMemberByHid(hid); if (member == null) return NotFound("Member not found."); if (member.System != _auth.CurrentSystem.Id) return Unauthorized($"Member '{hid}' is not part of your system."); - if (newMember.Name == null) - return BadRequest("Member name can not be null."); - - // Explicit bounds checks - if (newMember.Name != null && newMember.Name.Length > Limits.MaxMemberNameLength) - return BadRequest($"Member name too long ({newMember.Name.Length} > {Limits.MaxMemberNameLength}."); - if (newMember.DisplayName != null && newMember.DisplayName.Length > Limits.MaxMemberNameLength) - return BadRequest($"Member display name too long ({newMember.DisplayName.Length} > {Limits.MaxMemberNameLength}."); - if (newMember.Pronouns != null && newMember.Pronouns.Length > Limits.MaxPronounsLength) - return BadRequest($"Member pronouns too long ({newMember.Pronouns.Length} > {Limits.MaxPronounsLength}."); - if (newMember.Description != null && newMember.Description.Length > Limits.MaxDescriptionLength) - return BadRequest($"Member descriptions too long ({newMember.Description.Length} > {Limits.MaxDescriptionLength}."); - - // Sanity bounds checks - if (newMember.ProxyTags?.Any(tag => (tag.Prefix?.Length ?? 0) > 1000 || (tag.Suffix?.Length ?? 0) > 1000) ?? false) - return BadRequest(); - - member.Name = newMember.Name; - member.DisplayName = newMember.DisplayName.NullIfEmpty(); - member.Color = newMember.Color.NullIfEmpty(); - member.AvatarUrl = newMember.AvatarUrl.NullIfEmpty(); - member.Birthday = newMember.Birthday; - member.Pronouns = newMember.Pronouns.NullIfEmpty(); - member.Description = newMember.Description.NullIfEmpty(); - member.ProxyTags = newMember.ProxyTags; - member.KeepProxy = newMember.KeepProxy; + try + { + member.Apply(changes); + } + catch (PKParseError e) + { + return BadRequest(e.Message); + } + await _data.SaveMember(member); - - return Ok(member); + return Ok(member.ToJson()); } [HttpDelete("{hid}")] [RequiresSystem] - public async Task> DeleteMember(string hid) + public async Task DeleteMember(string hid) { var member = await _data.GetMemberByHid(hid); if (member == null) return NotFound("Member not found."); @@ -124,7 +91,6 @@ namespace PluralKit.API.Controllers if (member.System != _auth.CurrentSystem.Id) return Unauthorized($"Member '{hid}' is not part of your system."); await _data.DeleteMember(member); - return Ok(); } } diff --git a/PluralKit.API/Controllers/MessageController.cs b/PluralKit.API/Controllers/MessageController.cs index 4635e3b1..82c0bcc5 100644 --- a/PluralKit.API/Controllers/MessageController.cs +++ b/PluralKit.API/Controllers/MessageController.cs @@ -1,6 +1,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + using NodaTime; namespace PluralKit.API.Controllers @@ -13,8 +15,8 @@ namespace PluralKit.API.Controllers [JsonProperty("sender")] public string Sender; [JsonProperty("channel")] public string Channel; - [JsonProperty("system")] public PKSystem System; - [JsonProperty("member")] public PKMember Member; + [JsonProperty("system")] public JObject System; + [JsonProperty("member")] public JObject Member; } [ApiController] @@ -41,8 +43,8 @@ namespace PluralKit.API.Controllers Id = msg.Message.Mid.ToString(), Channel = msg.Message.Channel.ToString(), Sender = msg.Message.Sender.ToString(), - Member = msg.Member, - System = msg.System, + Member = msg.Member.ToJson(), + System = msg.System.ToJson(), Original = msg.Message.OriginalMid?.ToString() }; } diff --git a/PluralKit.API/Controllers/SystemController.cs b/PluralKit.API/Controllers/SystemController.cs index ce43700a..6e2f48e6 100644 --- a/PluralKit.API/Controllers/SystemController.cs +++ b/PluralKit.API/Controllers/SystemController.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using Dapper; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + using NodaTime; using PluralKit.Core; @@ -18,7 +20,7 @@ namespace PluralKit.API.Controllers public struct FrontersReturn { [JsonProperty("timestamp")] public Instant Timestamp { get; set; } - [JsonProperty("members")] public IEnumerable Members { get; set; } + [JsonProperty("members")] public IEnumerable Members { get; set; } } public struct PostSwitchParams @@ -44,27 +46,27 @@ namespace PluralKit.API.Controllers [HttpGet] [RequiresSystem] - public Task> GetOwnSystem() + public Task> GetOwnSystem() { - return Task.FromResult>(Ok(_auth.CurrentSystem)); + return Task.FromResult>(Ok(_auth.CurrentSystem.ToJson())); } [HttpGet("{hid}")] - public async Task> GetSystem(string hid) + public async Task> GetSystem(string hid) { var system = await _data.GetSystemByHid(hid); if (system == null) return NotFound("System not found."); - return Ok(system); + return Ok(system.ToJson()); } [HttpGet("{hid}/members")] - public async Task>> GetMembers(string hid) + public async Task>> GetMembers(string hid) { var system = await _data.GetSystemByHid(hid); if (system == null) return NotFound("System not found."); var members = await _data.GetSystemMembers(system); - return Ok(members); + return Ok(members.Select(m => m.ToJson())); } [HttpGet("{hid}/switches")] @@ -102,32 +104,27 @@ namespace PluralKit.API.Controllers return Ok(new FrontersReturn { Timestamp = sw.Timestamp, - Members = members + Members = members.Select(m => m.ToJson()) }); } [HttpPatch] [RequiresSystem] - public async Task> EditSystem([FromBody] PKSystem newSystem) + public async Task> EditSystem([FromBody] JObject changes) { var system = _auth.CurrentSystem; - - // Bounds checks - if (newSystem.Name != null && newSystem.Name.Length > Limits.MaxSystemNameLength) - return BadRequest($"System name too long ({newSystem.Name.Length} > {Limits.MaxSystemNameLength}."); - if (newSystem.Tag != null && newSystem.Tag.Length > Limits.MaxSystemTagLength) - return BadRequest($"System tag too long ({newSystem.Tag.Length} > {Limits.MaxSystemTagLength}."); - if (newSystem.Description != null && newSystem.Description.Length > Limits.MaxDescriptionLength) - return BadRequest($"System description too long ({newSystem.Description.Length} > {Limits.MaxDescriptionLength}."); - system.Name = newSystem.Name.NullIfEmpty(); - system.Description = newSystem.Description.NullIfEmpty(); - system.Tag = newSystem.Tag.NullIfEmpty(); - system.AvatarUrl = newSystem.AvatarUrl.NullIfEmpty(); - system.UiTz = newSystem.UiTz ?? "UTC"; - + try + { + system.Apply(changes); + } + catch (PKParseError e) + { + return BadRequest(e.Message); + } + await _data.SaveSystem(system); - return Ok(system); + return Ok(system.ToJson()); } [HttpPost("switches")] diff --git a/PluralKit.API/Startup.cs b/PluralKit.API/Startup.cs index 2547697e..4294f768 100644 --- a/PluralKit.API/Startup.cs +++ b/PluralKit.API/Startup.cs @@ -22,8 +22,7 @@ namespace PluralKit.API services.AddCors(); services.AddControllers() .SetCompatibilityVersion(CompatibilityVersion.Latest) - .AddNewtonsoftJson(); - // .AddJsonOptions(opts => { opts.SerializerSettings.BuildSerializerSettings(); }); + .AddNewtonsoftJson(); // sorry MS, this just does *more* services .AddTransient() diff --git a/PluralKit.Core/Models.cs b/PluralKit.Core/Models.cs index e933cd05..8f7ffdd6 100644 --- a/PluralKit.Core/Models.cs +++ b/PluralKit.Core/Models.cs @@ -1,15 +1,23 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using Dapper.Contrib.Extensions; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + using NodaTime; using NodaTime.Text; +using PluralKit.Core; + namespace PluralKit { + public class PKParseError: Exception + { + public PKParseError(string message): base(message) { } + } + public struct ProxyTag { public ProxyTag(string prefix, string suffix) @@ -52,16 +60,26 @@ namespace PluralKit [JsonProperty("tz")] public string UiTz { get; set; } [JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); - public void ToJson(System.Text.Json.Utf8JsonWriter w) + public JObject ToJson() { - w.WriteStartObject(); - w.WriteString("id", Hid); - w.WriteString("description", Description); - w.WriteString("tag", Tag); - w.WriteString("avatar_url", AvatarUrl); - w.WriteString("created", Formats.TimestampExportFormat.Format(Created)); - w.WriteString("tz", UiTz); - w.WriteEndObject(); + var o = new JObject(); + o.Add("id", Hid); + o.Add("name", Name); + o.Add("description", Description); + o.Add("tag", Tag); + o.Add("avatar_url", AvatarUrl); + o.Add("created", Formats.TimestampExportFormat.Format(Created)); + o.Add("tz", UiTz); + return o; + } + + public void Apply(JObject o) + { + if (o.ContainsKey("name")) Name = o.Value("name").NullIfEmpty().BoundsCheck(Limits.MaxSystemNameLength, "System name"); + if (o.ContainsKey("description")) Description = o.Value("description").NullIfEmpty().BoundsCheck(Limits.MaxDescriptionLength, "System description"); + if (o.ContainsKey("tag")) Tag = o.Value("tag").NullIfEmpty().BoundsCheck(Limits.MaxSystemTagLength, "System tag"); + if (o.ContainsKey("avatar_url")) AvatarUrl = o.Value("avatar_url").NullIfEmpty(); + if (o.ContainsKey("tz")) UiTz = o.Value("tz") ?? "UTC"; } } @@ -81,20 +99,6 @@ namespace PluralKit [JsonProperty("proxy_tags")] public ICollection ProxyTags { get; set; } [JsonProperty("keep_proxy")] public bool KeepProxy { get; set; } [JsonProperty("created")] public Instant Created { get; set; } - - // These are deprecated as fuck, and are kinda hacky - // Don't use, unless you're the API's serialization library - [JsonProperty("prefix")] [Obsolete("Use PKMember.ProxyTags")] public string Prefix - { - get => ProxyTags?.FirstOrDefault().Prefix; - set => ProxyTags = new[] {new ProxyTag(Prefix, value)}; - } - - [JsonProperty("suffix")] [Obsolete("Use PKMember.ProxyTags")] public string Suffix - { - get => ProxyTags?.FirstOrDefault().Suffix; - set => ProxyTags = new[] {new ProxyTag(Suffix, value)}; - } /// Returns a formatted string representing the member's birthday, taking into account that a year of "0001" is hidden [JsonIgnore] public string BirthdayString @@ -116,36 +120,64 @@ namespace PluralKit return $"{guildDisplayName ?? DisplayName ?? Name} {systemTag}"; } - public void ToJson(Utf8JsonWriter w) + public JObject ToJson() { - w.WriteStartObject(); - w.WriteString("id", Hid); - w.WriteString("name", Name); - w.WriteString("color", Color); - w.WriteString("display_name", DisplayName); - w.WriteString("birthday", Birthday.HasValue ? Formats.DateExportFormat.Format(Birthday.Value) : null); - w.WriteString("pronouns", Pronouns); - w.WriteString("description", Description); - w.WriteStartArray("proxy_tags"); - foreach (var tag in ProxyTags) - { - w.WriteStartObject(); - w.WriteString("prefix", tag.Prefix); - w.WriteString("suffix", tag.Suffix); - w.WriteEndObject(); - } - w.WriteEndArray(); - w.WriteBoolean("keep_proxy", KeepProxy); - w.WriteString("created", Formats.TimestampExportFormat.Format(Created)); + var o = new JObject(); + o.Add("id", Hid); + o.Add("name", Name); + o.Add("color", Color); + o.Add("display_name", DisplayName); + o.Add("birthday", Birthday.HasValue ? Formats.DateExportFormat.Format(Birthday.Value) : null); + o.Add("pronouns", Pronouns); + o.Add("description", Description); + + var tagArray = new JArray(); + foreach (var tag in ProxyTags) + tagArray.Add(new JObject {{"prefix", tag.Prefix}, {"suffix", tag.Suffix}}); + o.Add("proxy_tags", tagArray); + + o.Add("keep_proxy", KeepProxy); + o.Add("created", Formats.TimestampExportFormat.Format(Created)); if (ProxyTags.Count > 0) { // Legacy compatibility only, TODO: remove at some point - w.WriteString("prefix", ProxyTags?.FirstOrDefault().Prefix); - w.WriteString("suffix", ProxyTags?.FirstOrDefault().Suffix); + o.Add("prefix", ProxyTags?.FirstOrDefault().Prefix); + o.Add("suffix", ProxyTags?.FirstOrDefault().Suffix); } - w.WriteEndObject(); + return o; + } + + public void Apply(JObject o) + { + if (o.ContainsKey("name") && o["name"].Type == JTokenType.Null) + throw new PKParseError("Member name can not be set to null."); + + if (o.ContainsKey("name")) Name = o.Value("name").BoundsCheck(Limits.MaxMemberNameLength, "Member name"); + if (o.ContainsKey("color")) Color = o.Value("color").NullIfEmpty(); + if (o.ContainsKey("display_name")) DisplayName = o.Value("display_name").NullIfEmpty().BoundsCheck(Limits.MaxMemberNameLength, "Member display name"); + if (o.ContainsKey("birthday")) + { + var str = o.Value("birthday").NullIfEmpty(); + var res = Formats.DateExportFormat.Parse(str); + if (res.Success) Birthday = res.Value; + else if (str == null) Birthday = null; + else throw new PKParseError("Could not parse member birthday."); + } + + if (o.ContainsKey("pronouns")) Pronouns = o.Value("pronouns").NullIfEmpty().BoundsCheck(Limits.MaxPronounsLength, "Member pronouns"); + if (o.ContainsKey("description")) Description = o.Value("description").NullIfEmpty().BoundsCheck(Limits.MaxDescriptionLength, "Member descriptoin"); + if (o.ContainsKey("keep_proxy")) KeepProxy = o.Value("keep_proxy"); + + if (o.ContainsKey("prefix") || o.ContainsKey("suffix") && !o.ContainsKey("proxy_tags")) + ProxyTags = new[] {new ProxyTag(o.Value("prefix"), o.Value("suffix"))}; + else if (o.ContainsKey("proxy_tags")) + { + ProxyTags = o.Value("proxy_tags") + .OfType().Select(o => new ProxyTag(o.Value("prefix"), o.Value("suffix"))) + .ToList(); + } } } diff --git a/PluralKit.Core/Utils.cs b/PluralKit.Core/Utils.cs index b671a890..300f27bc 100644 --- a/PluralKit.Core/Utils.cs +++ b/PluralKit.Core/Utils.cs @@ -253,6 +253,13 @@ namespace PluralKit if (input.Trim().Length == 0) return null; return input; } + + public static string BoundsCheck(this string input, int maxLength, string nameInError) + { + if (input != null && input.Length > maxLength) + throw new PKParseError($"{nameInError} too long ({input.Length} > {maxLength})."); + return input; + } } public static class Emojis {