diff --git a/PluralKit.API/APIJsonExt.cs b/PluralKit.API/APIJsonExt.cs index 46744297..29f446cf 100644 --- a/PluralKit.API/APIJsonExt.cs +++ b/PluralKit.API/APIJsonExt.cs @@ -1,7 +1,11 @@ +using System; using System.Collections.Generic; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NodaTime; + using PluralKit.Core; namespace PluralKit.API @@ -32,4 +36,19 @@ namespace PluralKit.API return o; } } + + public struct FrontersReturnNew + { + [JsonProperty("id")] public Guid Uuid { get; set; } + [JsonProperty("timestamp")] public Instant Timestamp { get; set; } + [JsonProperty("members")] public IEnumerable Members { get; set; } + } + + public struct SwitchesReturnNew + { + [JsonProperty("id")] public Guid Uuid { get; set; } + [JsonProperty("timestamp")] public Instant Timestamp { get; set; } + [JsonProperty("members")] public IEnumerable Members { get; set; } + } + } \ No newline at end of file diff --git a/PluralKit.API/Controllers/v1/SystemController.cs b/PluralKit.API/Controllers/v1/SystemController.cs index 30f648aa..f59f0670 100644 --- a/PluralKit.API/Controllers/v1/SystemController.cs +++ b/PluralKit.API/Controllers/v1/SystemController.cs @@ -32,6 +32,7 @@ namespace PluralKit.API public struct PostSwitchParams { + public Instant? Timestamp { get; set; } public ICollection Members { get; set; } } diff --git a/PluralKit.API/Controllers/v2/SwitchControllerV2.cs b/PluralKit.API/Controllers/v2/SwitchControllerV2.cs index f7c2f88c..6623edbc 100644 --- a/PluralKit.API/Controllers/v2/SwitchControllerV2.cs +++ b/PluralKit.API/Controllers/v2/SwitchControllerV2.cs @@ -16,13 +16,6 @@ using PluralKit.Core; namespace PluralKit.API { - public struct SwitchesReturnNew - { - [JsonProperty("timestamp")] public Instant Timestamp { get; set; } - [JsonProperty("id")] public Guid Uuid { get; set; } - [JsonProperty("members")] public IEnumerable Members { get; set; } - } - [ApiController] [ApiVersion("2.0")] [Route("v{version:apiVersion}")] @@ -77,21 +70,60 @@ namespace PluralKit.API return NoContent(); var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)); - return Ok(new FrontersReturn + return Ok(new FrontersReturnNew { Timestamp = sw.Timestamp, - Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync() + Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync(), + Uuid = sw.Uuid, }); } - [HttpPost("systems/{system}/switches")] - public async Task SwitchCreate(string system, [FromBody] JObject data) + [HttpPost("systems/@me/switches")] + public async Task SwitchCreate([FromBody] PostSwitchParams data) { - return new ObjectResult("Unimplemented") + if (data.Members.Distinct().Count() != data.Members.Count) + throw APIErrors.DuplicateMembersInList; + + var system = await ResolveSystem("@me"); + + if (data.Timestamp != null && await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(data.Timestamp.Value)) + throw APIErrors.SameSwitchTimestampError; + + var members = new List(); + + foreach (var memberRef in data.Members) { - StatusCode = 501 - }; + var member = await ResolveMember(memberRef); + if (member == null) + // todo: which member + throw APIErrors.MemberNotFound; + if (member.System != system.Id) + throw APIErrors.NotOwnMemberErrorWithRef(memberRef); + members.Add(member); + } + + // We get the current switch, if it exists + var latestSwitch = await _repo.GetLatestSwitch(system.Id); + if (latestSwitch != null && (data.Timestamp == null || data.Timestamp > latestSwitch.Timestamp)) + { + var latestSwitchMembers = _db.Execute(conn => _repo.GetSwitchMembers(conn, latestSwitch.Id)); + + // Bail if this switch is identical to the latest one + if (await latestSwitchMembers.Select(m => m.Hid).SequenceEqualAsync(members.Select(m => m.Hid).ToAsyncEnumerable())) + throw APIErrors.SameSwitchMembersError; + } + + var newSwitch = await _db.Execute(conn => _repo.AddSwitch(conn, system.Id, members.Select(m => m.Id).ToList())); + if (data.Timestamp != null) + await _repo.MoveSwitch(newSwitch.Id, data.Timestamp.Value); + + return Ok(new FrontersReturnNew + { + Uuid = newSwitch.Uuid, + Timestamp = data.Timestamp != null ? data.Timestamp.Value : newSwitch.Timestamp, + Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)), + }); } @@ -99,7 +131,7 @@ namespace PluralKit.API public async Task SwitchGet(string systemRef, string switchRef) { if (!Guid.TryParse(switchRef, out var switchId)) - throw APIErrors.SwitchNotFound; + throw APIErrors.InvalidSwitchId; var system = await ResolveSystem(systemRef); if (system == null) @@ -107,45 +139,115 @@ namespace PluralKit.API var sw = await _repo.GetSwitchByUuid(switchId); if (sw == null || system.Id != sw.System) - throw APIErrors.SwitchNotFound; + throw APIErrors.SwitchNotFoundPublic; var ctx = this.ContextFor(system); if (!system.FrontHistoryPrivacy.CanAccess(ctx)) - throw APIErrors.SwitchNotFound; + throw APIErrors.SwitchNotFoundPublic; var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)); - return Ok(new FrontersReturn + return Ok(new FrontersReturnNew { + Uuid = sw.Uuid, Timestamp = sw.Timestamp, Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync() }); } - [HttpPatch("systems/{system}/switches/{switch_id}")] - public async Task SwitchPatch(string system, [FromBody] JObject data) + [HttpPatch("systems/@me/switches/{switchRef}")] + public async Task SwitchPatch(string switchRef, [FromBody] JObject data) { - return new ObjectResult("Unimplemented") + // for now, don't need to make a PatchObject for this, since it's only one param + + if (!Guid.TryParse(switchRef, out var switchId)) + throw APIErrors.InvalidSwitchId; + + var value = data.Value("timestamp"); + if (value == null) + // todo + throw APIErrors.GenericBadRequest; + + var system = await ResolveSystem("@me"); + if (system == null) + throw APIErrors.SystemNotFound; + + var sw = await _repo.GetSwitchByUuid(switchId); + if (sw == null || system.Id != sw.System) + throw APIErrors.SwitchNotFoundPublic; + + if (await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(value)) + throw APIErrors.SameSwitchTimestampError; + + await _repo.MoveSwitch(sw.Id, value); + + var members = await _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)).ToListAsync(); + return Ok(new FrontersReturnNew { - StatusCode = 501 - }; + Uuid = sw.Uuid, + Timestamp = sw.Timestamp, + Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)), + }); + } + + [HttpPatch("systems/@me/switches/{switchRef}/members")] + public async Task SwitchMemberPatch(string switchRef, [FromBody] JArray data) + { + if (!Guid.TryParse(switchRef, out var switchId)) + + if (data.Distinct().Count() != data.Count) + throw APIErrors.DuplicateMembersInList; + + var system = await ResolveSystem("@me"); + + var sw = await _repo.GetSwitchByUuid(switchId); + if (sw == null) + throw APIErrors.SwitchNotFound; + + var members = new List(); + + foreach (var JmemberRef in data) + { + var memberRef = JmemberRef.Value(); + + var member = await ResolveMember(memberRef); + if (member == null) + // todo: which member + throw APIErrors.MemberNotFound; + if (member.System != system.Id) + throw APIErrors.NotOwnMemberErrorWithRef(memberRef); + + members.Add(member); + } + + var latestSwitchMembers = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)); + + if (await latestSwitchMembers.Select(m => m.Hid).SequenceEqualAsync(members.Select(m => m.Hid).ToAsyncEnumerable())) + throw APIErrors.SameSwitchMembersError; + + await _db.Execute(conn => _repo.EditSwitch(conn, sw.Id, members.Select(x => x.Id).ToList())); + return Ok(new FrontersReturnNew + { + Uuid = sw.Uuid, + Timestamp = sw.Timestamp, + Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)), + }); } [HttpDelete("systems/@me/switches/{switchRef}")] public async Task SwitchDelete(string switchRef) { if (!Guid.TryParse(switchRef, out var switchId)) - throw APIErrors.SwitchNotFound; + throw APIErrors.InvalidSwitchId; var system = await ResolveSystem("@me"); var sw = await _repo.GetSwitchByUuid(switchId); if (sw == null || system.Id != sw.System) - throw APIErrors.SwitchNotFound; + throw APIErrors.SwitchNotFoundPublic; await _repo.DeleteSwitch(sw.Id); return NoContent(); } - } } \ No newline at end of file diff --git a/PluralKit.API/Errors.cs b/PluralKit.API/Errors.cs index 01b2cd85..39ee1122 100644 --- a/PluralKit.API/Errors.cs +++ b/PluralKit.API/Errors.cs @@ -46,7 +46,8 @@ namespace PluralKit.API public static PKError MemberNotFound = new(404, 20002, "Member not found."); public static PKError GroupNotFound = new(404, 20003, "Group not found."); public static PKError MessageNotFound = new(404, 20004, "Message not found."); - public static PKError SwitchNotFound = new(404, 20005, "Switch not found, switch is associated to different system, or unauthorized to view front history."); + public static PKError SwitchNotFound = new(404, 20005, "Switch not found."); + public static PKError SwitchNotFoundPublic = new(404, 20005, "Switch not found, switch associated with different system, or unauthorized to view front history."); public static PKError SystemGuildNotFound = new(404, 20006, "No system guild settings found for target guild."); public static PKError MemberGuildNotFound = new(404, 20007, "No member guild settings found for target guild."); public static PKError UnauthorizedMemberList = new(403, 30001, "Unauthorized to view member list"); @@ -55,11 +56,15 @@ namespace PluralKit.API public static PKError UnauthorizedCurrentFronters = new(403, 30004, "Unauthorized to view current fronters."); public static PKError UnauthorizedFrontHistory = new(403, 30005, "Unauthorized to view front history."); public static PKError NotOwnMemberError = new(403, 30006, "Target member is not part of your system."); - public static PKError NotOwnGroupError = new(403, 30006, "Target group is not part of your system."); + public static PKError NotOwnGroupError = new(403, 30007, "Target group is not part of your system."); // todo: somehow add the memberRef to the JSON public static PKError NotOwnMemberErrorWithRef(string memberRef) => new(403, 30008, $"Member '{memberRef}' is not part of your system."); public static PKError NotOwnGroupErrorWithRef(string groupRef) => new(403, 30009, $"Group '{groupRef}' is not part of your system."); public static PKError MissingAutoproxyMember = new(400, 40002, "Missing autoproxy member for member-mode autoproxy."); + public static PKError DuplicateMembersInList = new(400, 40003, "Duplicate members in member list."); + public static PKError SameSwitchMembersError = new(400, 40004, "Member list identical to current fronter list."); + public static PKError SameSwitchTimestampError = new(400, 40005, "Switch with provided timestamp already exists."); + public static PKError InvalidSwitchId = new(400, 40006, "Invalid switch ID."); public static PKError Unimplemented = new(501, 50001, "Unimplemented"); } } \ No newline at end of file diff --git a/PluralKit.API/Startup.cs b/PluralKit.API/Startup.cs index 67626ffa..e3c70023 100644 --- a/PluralKit.API/Startup.cs +++ b/PluralKit.API/Startup.cs @@ -57,7 +57,12 @@ namespace PluralKit.API services.AddControllers() .SetCompatibilityVersion(CompatibilityVersion.Latest) - .AddNewtonsoftJson(); // sorry MS, this just does *more* + // sorry MS, this just does *more* + .AddNewtonsoftJson((opts) => + { + // ... though by default it messes up timestamps in JSON + opts.SerializerSettings.DateParseHandling = DateParseHandling.None; + }); services.AddApiVersioning();