diff --git a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs index dd3da730..afed262d 100644 --- a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs +++ b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs @@ -42,6 +42,115 @@ namespace PluralKit.API return Ok(o); } + [HttpPost("groups/{groupRef}/members/add")] + public async Task AddGroupMembers(string groupRef, [FromBody] JArray memberRefs) + { + if (memberRefs.Count == 0) + throw Errors.GenericBadRequest; + + var system = await ResolveSystem("@me"); + + var group = await ResolveGroup(groupRef); + if (group == null) + throw Errors.GroupNotFound; + if (group.System != system.Id) + throw Errors.NotOwnGroupError; + + var members = new List(); + + foreach (var JmemberRef in memberRefs) + { + var memberRef = JmemberRef.Value(); + var member = await ResolveMember(memberRef); + + // todo: have a list of these errors instead of immediately throwing + + if (member == null) + throw Errors.MemberNotFoundWithRef(memberRef); + if (member.System != system.Id) + throw Errors.NotOwnMemberErrorWithRef(memberRef); + + members.Add(member.Id); + } + + var existingMembers = await _repo.GetGroupMembers(group.Id).Select(x => x.Id).ToListAsync(); + members = members.Where(x => !existingMembers.Contains(x)).ToList(); + + if (members.Count > 0) + await _repo.AddMembersToGroup(group.Id, members); + + return NoContent(); + } + + [HttpPost("groups/{groupRef}/members/remove")] + public async Task RemoveGroupMembers(string groupRef, [FromBody] JArray memberRefs) + { + if (memberRefs.Count == 0) + throw Errors.GenericBadRequest; + + var system = await ResolveSystem("@me"); + + var group = await ResolveGroup(groupRef); + if (group == null) + throw Errors.GroupNotFound; + if (group.System != system.Id) + throw Errors.NotOwnGroupError; + + var members = new List(); + + foreach (var JmemberRef in memberRefs) + { + var memberRef = JmemberRef.Value(); + var member = await ResolveMember(memberRef); + + if (member == null) + throw Errors.MemberNotFoundWithRef(memberRef); + if (member.System != system.Id) + throw Errors.NotOwnMemberErrorWithRef(memberRef); + + members.Add(member.Id); + } + + await _repo.RemoveMembersFromGroup(group.Id, members); + + return NoContent(); + } + + [HttpPost("groups/{groupRef}/members/overwrite")] + public async Task OverwriteGroupMembers(string groupRef, [FromBody] JArray memberRefs) + { + var system = await ResolveSystem("@me"); + + var group = await ResolveGroup(groupRef); + if (group == null) + throw Errors.GroupNotFound; + if (group.System != system.Id) + throw Errors.NotOwnGroupError; + + var members = new List(); + + foreach (var JmemberRef in memberRefs) + { + var memberRef = JmemberRef.Value(); + var member = await ResolveMember(memberRef); + + if (member == null) + throw Errors.MemberNotFoundWithRef(memberRef); + if (member.System != system.Id) + throw Errors.NotOwnMemberErrorWithRef(memberRef); + + members.Add(member.Id); + } + + await _repo.ClearGroupMembers(group.Id); + + if (members.Count > 0) + await _repo.AddMembersToGroup(group.Id, members); + + return NoContent(); + } + + [HttpGet("members/{memberRef}/groups")] public async Task GetMemberGroups(string memberRef) { @@ -62,127 +171,8 @@ namespace PluralKit.API return Ok(o); } - [HttpPut("groups/{groupRef}/members/{memberRef}")] - public async Task GroupMemberPut(string groupRef, string memberRef) - { - var system = await ResolveSystem("@me"); - - var group = await ResolveGroup(groupRef); - if (group == null) - throw Errors.GroupNotFound; - if (group.System != system.Id) - throw Errors.NotOwnGroupError; - - var member = await ResolveMember(memberRef); - Console.WriteLine(member); - if (member == null) - throw Errors.MemberNotFound; - if (member.System != system.Id) - throw Errors.NotOwnMemberError; - - var existingMembers = await _repo.GetGroupMembers(group.Id).Select(x => x.Id).ToListAsync(); - if (!existingMembers.Contains(member.Id)) - await _repo.AddMembersToGroup(group.Id, new List() { member.Id }); - - return NoContent(); - } - - [HttpPut("groups/{groupRef}/members")] - public async Task GroupMembersPut(string groupRef, [FromBody] JArray memberRefs) - { - if (memberRefs.Count == 0) - throw Errors.GenericBadRequest; - - var system = await ResolveSystem("@me"); - - var group = await ResolveGroup(groupRef); - if (group == null) - throw Errors.GroupNotFound; - if (group.System != system.Id) - throw Errors.NotOwnGroupError; - - var members = new List(); - - foreach (var JmemberRef in memberRefs) - { - var memberRef = JmemberRef.Value(); - var member = await ResolveMember(memberRef); - - if (member == null) - throw Errors.MemberNotFound; - if (member.System != system.Id) - throw Errors.NotOwnMemberErrorWithRef(memberRef); - - members.Add(member.Id); - } - - var existingMembers = await _repo.GetGroupMembers(group.Id).Select(x => x.Id).ToListAsync(); - members = members.Where(x => !existingMembers.Contains(x)).ToList(); - - if (members.Count > 0) - await _repo.AddMembersToGroup(group.Id, members); - - return NoContent(); - } - - [HttpDelete("groups/{groupRef}/members/{memberRef}")] - public async Task GroupMemberDelete(string groupRef, string memberRef) - { - var system = await ResolveSystem("@me"); - - var group = await ResolveGroup(groupRef); - if (group == null) - throw Errors.GroupNotFound; - if (group.System != system.Id) - throw Errors.NotOwnGroupError; - - var member = await ResolveMember(memberRef); - if (member == null) - throw Errors.MemberNotFound; - if (member.System != system.Id) - throw Errors.NotOwnMemberError; - - await _repo.RemoveMembersFromGroup(group.Id, new List() { member.Id }); - - return NoContent(); - } - - [HttpDelete("groups/{groupRef}/members")] - public async Task GroupMembersDelete(string groupRef, [FromBody] JArray memberRefs) - { - if (memberRefs.Count == 0) - throw Errors.GenericBadRequest; - - var system = await ResolveSystem("@me"); - - var group = await ResolveGroup(groupRef); - if (group == null) - throw Errors.GroupNotFound; - if (group.System != system.Id) - throw Errors.NotOwnGroupError; - - var members = new List(); - - foreach (var JmemberRef in memberRefs) - { - var memberRef = JmemberRef.Value(); - var member = await ResolveMember(memberRef); - - if (member == null) - throw Errors.MemberNotFound; - if (member.System != system.Id) - throw Errors.NotOwnMemberError; - - members.Add(member.Id); - } - - await _repo.RemoveMembersFromGroup(group.Id, members); - - return NoContent(); - } - - [HttpPut("members/{memberRef}/groups")] - public async Task MemberGroupsPut(string memberRef, [FromBody] JArray groupRefs) + [HttpPost("members/{memberRef}/groups/add")] + public async Task AddMemberGroups(string memberRef, [FromBody] JArray groupRefs) { if (groupRefs.Count == 0) throw Errors.GenericBadRequest; @@ -219,8 +209,8 @@ namespace PluralKit.API return NoContent(); } - [HttpDelete("members/{memberRef}/groups")] - public async Task MemberGroupsDelete(string memberRef, [FromBody] JArray groupRefs) + [HttpPost("members/{memberRef}/groups/remove")] + public async Task RemoveMemberGroups(string memberRef, [FromBody] JArray groupRefs) { if (groupRefs.Count == 0) throw Errors.GenericBadRequest; @@ -241,7 +231,7 @@ namespace PluralKit.API var group = await ResolveGroup(groupRef); if (group == null) - throw Errors.GroupNotFound; + throw Errors.GroupNotFoundWithRef(groupRef); if (group.System != system.Id) throw Errors.NotOwnGroupErrorWithRef(groupRef); @@ -253,5 +243,39 @@ namespace PluralKit.API return NoContent(); } + [HttpPost("members/{memberRef}/groups/overwrite")] + public async Task OverwriteMemberGroups(string memberRef, [FromBody] JArray groupRefs) + { + var system = await ResolveSystem("@me"); + + var member = await ResolveMember(memberRef); + if (member == null) + throw Errors.MemberNotFound; + if (member.System != system.Id) + throw Errors.NotOwnMemberError; + + var groups = new List(); + + foreach (var JgroupRef in groupRefs) + { + var groupRef = JgroupRef.Value(); + var group = await ResolveGroup(groupRef); + + if (group == null) + throw Errors.GroupNotFoundWithRef(groupRef); + if (group.System != system.Id) + throw Errors.NotOwnGroupErrorWithRef(groupRef); + + groups.Add(group.Id); + } + + await _repo.ClearMemberGroups(member.Id); + + if (groups.Count > 0) + await _repo.AddGroupsToMember(member.Id, groups); + + return NoContent(); + } + } } \ No newline at end of file diff --git a/PluralKit.API/Errors.cs b/PluralKit.API/Errors.cs index 2db7d3a8..90281455 100644 --- a/PluralKit.API/Errors.cs +++ b/PluralKit.API/Errors.cs @@ -71,12 +71,14 @@ namespace PluralKit.API public static PKError GenericAuthError = new(401, 0, "401: Missing or invalid Authorization header"); public static PKError SystemNotFound = new(404, 20001, "System not found."); 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."); - 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 MemberNotFoundWithRef(string memberRef) => new(404, 20003, $"Member '{memberRef}' not found."); + public static PKError GroupNotFound = new(404, 20004, "Group not found."); + public static PKError GroupNotFoundWithRef(string groupRef) => new(404, 20005, $"Group '{groupRef}' not found."); + public static PKError MessageNotFound = new(404, 20006, "Message not found."); + public static PKError SwitchNotFound = new(404, 20007, "Switch not found."); + public static PKError SwitchNotFoundPublic = new(404, 20008, "Switch not found, switch associated with different system, or unauthorized to view front history."); + public static PKError SystemGuildNotFound = new(404, 20009, "No system guild settings found for target guild."); + public static PKError MemberGuildNotFound = new(404, 20010, "No member guild settings found for target guild."); public static PKError UnauthorizedMemberList = new(403, 30001, "Unauthorized to view member list"); public static PKError UnauthorizedGroupList = new(403, 30002, "Unauthorized to view group list"); public static PKError UnauthorizedGroupMemberList = new(403, 30003, "Unauthorized to view group member list"); diff --git a/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs b/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs index a42ffb70..06ff7319 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs @@ -76,5 +76,21 @@ namespace PluralKit.Core .WhereIn("member_id", members); return _db.ExecuteQuery(query); } + + public Task ClearGroupMembers(GroupId group) + { + _logger.Information("Cleared members of {GroupId}", group); + var query = new Query("group_members").AsDelete() + .Where("group_id", group); + return _db.ExecuteQuery(query); + } + + public Task ClearMemberGroups(MemberId member) + { + _logger.Information("Cleared groups of {GroupId}", member); + var query = new Query("group_members").AsDelete() + .Where("member_id", member); + return _db.ExecuteQuery(query); + } } } \ No newline at end of file diff --git a/docs/content/api/endpoints.md b/docs/content/api/endpoints.md index 50606fc7..03ab7cb8 100644 --- a/docs/content/api/endpoints.md +++ b/docs/content/api/endpoints.md @@ -89,20 +89,26 @@ GET `/members/{memberRef}/groups` ### Add Member To Groups -PUT `/members/{memberRef}/groups` +POST `/members/{memberRef}/groups/add` -::: warning -Not all HTTP implementations support PUT requests with a body. If yours does not, consider using the [Add Member To Group](#add-member-to-group) endpoint instead. -::: +Takes a list of group references as input. Returns 204 No Content on success. ### Remove Member From Groups -DELETE `/members/{memberRef}/groups` +POST `/members/{memberRef}/groups/remove` -::: warning -Not all HTTP implementations support DELETE requests with a body. If yours does not, consider using the [Remove Member From Group](#remove-member-from-group) endpoint instead. +::: tip +If you want to remove *all* groups from a member, consider using the [Overwrite Member Groups](#overwrite-member-groups) endpoint instead. ::: +Takes a list of group references as input. Returns 204 No Content on success. + +### Overwrite Member Groups + +POST `/members/{memberRef}/groups/overwrite` + +Takes a list of group references as input. (An empty list is accepted.) Returns 204 No Content on success. + ### Get Member Guild Settings GET `/members/{memberRef}/guilds/{guild_id}` @@ -164,30 +170,29 @@ Returns 204 No Content on success. GET `/groups/{groupRef}/members` -### Add Member To Group - -PUT `/groups/{groupRef}/members/{memberRef}` +Returns an array of [member objects](/api/models#member-model). ### Add Members To Group -PUT `/groups/{groupRef}/members` +POST `/groups/{groupRef}/members/add` -::: warning -Not all HTTP implementations support PUT requests with a body. If yours does not, consider using the [Add Member To Group](#add-group-member) endpoint instead. -::: +Takes an array of member references as input. Returns 204 No Content on success. ### Remove Member From Group -DELETE `/groups/{groupRef}/members/{memberRef}` +POST `/groups/{groupRef}/members/remove` -### Remove Members From Group - -DELETE `/groups/{groupRef}/members` - -::: warning -Not all HTTP implementations support DELETE requests with a body. If yours does not, consider using the [Remove Member From Group](#remove-member-from-group) endpoint instead. +::: tip +If you want to remove *all* members from a group, consider using the [Overwrite Group Members](#overwrite-group-members) endpoint instead. ::: +Takes an array of member references as input. Returns 204 No Content on success. + +### Overwrite Group Members + +POST `/groups/{groupRef}/members/overwrite` + +Takes an array of member references as input. (An empty list is accepted.) Returns 204 No Content on success. --- ## Switches diff --git a/docs/content/api/errors.md b/docs/content/api/errors.md index a39630f0..1e77d3a3 100644 --- a/docs/content/api/errors.md +++ b/docs/content/api/errors.md @@ -35,12 +35,14 @@ When something goes wrong, the API will send back a 4xx HTTP status code, along |0|401|Missing or invalid Authorization header| |20001|404|System not found.| |20002|404|Member not found.| -|20003|404|Group not found.| -|20004|404|Message not found.| -|20005|404|Switch not found.| -|20005|404|Switch not found, switch associated with different system, or unauthorized to view front history.| -|20006|404|No system guild settings found for target guild.| -|20007|404|No member guild settings found for target guild.| +|20003|404|Member '{memberRef}' not found.| +|20004|404|Group not found.| +|20005|404|Group '{groupRef}' not found.| +|20006|404|Message not found.| +|20007|404|Switch not found.| +|20008|404|Switch not found, switch associated with different system, or unauthorized to view front history.| +|20009|404|No system guild settings found for target guild.| +|20010|404|No member guild settings found for target guild.| |30001|403|Unauthorized to view member list| |30002|403|Unauthorized to view group list| |30003|403|Unauthorized to view group member list| @@ -48,8 +50,8 @@ When something goes wrong, the API will send back a 4xx HTTP status code, along |30005|403|Unauthorized to view front history.| |30006|403|Target member is not part of your system.| |30007|403|Target group is not part of your system.| -|30008|403|$Member '{memberRef}' is not part of your system.| -|30009|403|$Group '{groupRef}' is not part of your system.| +|30008|403|Member '{memberRef}' is not part of your system.| +|30009|403|Group '{groupRef}' is not part of your system.| |40002|400|Missing autoproxy member for member-mode autoproxy.| |40003|400|Duplicate members in member list.| |40004|400|Member list identical to current fronter list.|