From e2a56a198f8938165e6a95dc063746c54625693e Mon Sep 17 00:00:00 2001 From: spiral Date: Tue, 12 Oct 2021 05:17:54 -0400 Subject: [PATCH] feat(apiv2): GET endpoints except guilds - ResolveT methods in ControllerBase - ContextFor methods in ControllerBase --- PluralKit.API/Controllers/PKControllerBase.cs | 43 +++++++- .../Controllers/v2/GroupControllerV2.cs | 38 ++++--- .../Controllers/v2/GroupMemberControllerV2.cs | 51 +++++++--- .../Controllers/v2/MemberControllerV2.cs | 38 ++++--- .../Controllers/v2/MiscControllerV2.cs | 23 ++++- .../Controllers/v2/SwitchControllerV2.cs | 99 ++++++++++++++++--- .../Controllers/v2/SystemControllerV2.cs | 2 +- PluralKit.API/Errors.cs | 6 ++ .../Repository/ModelRepository.GroupMember.cs | 9 ++ 9 files changed, 249 insertions(+), 60 deletions(-) diff --git a/PluralKit.API/Controllers/PKControllerBase.cs b/PluralKit.API/Controllers/PKControllerBase.cs index 00856775..cd3db2f5 100644 --- a/PluralKit.API/Controllers/PKControllerBase.cs +++ b/PluralKit.API/Controllers/PKControllerBase.cs @@ -35,7 +35,8 @@ namespace PluralKit.API if (systemRef == "@me") { HttpContext.Items.TryGetValue("SystemId", out var systemId); - if (systemId == null) return null; + if (systemId == null) + throw APIErrors.GenericAuthError; return _repo.GetSystem((SystemId)systemId); } @@ -51,11 +52,47 @@ namespace PluralKit.API return null; } - public LookupContext LookupContextFor(PKSystem target) + protected Task ResolveMember(string memberRef) + { + if (Guid.TryParse(memberRef, out var guid)) + return _repo.GetMemberByGuid(guid); + + if (_shortIdRegex.IsMatch(memberRef)) + return _repo.GetMemberByHid(memberRef); + + return null; + } + + protected Task ResolveGroup(string groupRef) + { + if (Guid.TryParse(groupRef, out var guid)) + return _repo.GetGroupByGuid(guid); + + if (_shortIdRegex.IsMatch(groupRef)) + return _repo.GetGroupByHid(groupRef); + + return null; + } + + public LookupContext ContextFor(PKSystem system) { HttpContext.Items.TryGetValue("SystemId", out var systemId); if (systemId == null) return LookupContext.ByNonOwner; - return target.Id == (SystemId)systemId ? LookupContext.ByOwner : LookupContext.ByNonOwner; + return ((SystemId)systemId) == system.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner; + } + + public LookupContext ContextFor(PKMember member) + { + HttpContext.Items.TryGetValue("SystemId", out var systemId); + if (systemId == null) return LookupContext.ByNonOwner; + return ((SystemId)systemId) == member.System ? LookupContext.ByOwner : LookupContext.ByNonOwner; + } + + public LookupContext ContextFor(PKGroup group) + { + HttpContext.Items.TryGetValue("SystemId", out var systemId); + if (systemId == null) return LookupContext.ByNonOwner; + return ((SystemId)systemId) == group.System ? LookupContext.ByOwner : LookupContext.ByNonOwner; } } } \ No newline at end of file diff --git a/PluralKit.API/Controllers/v2/GroupControllerV2.cs b/PluralKit.API/Controllers/v2/GroupControllerV2.cs index ab1ff635..4a1f32ca 100644 --- a/PluralKit.API/Controllers/v2/GroupControllerV2.cs +++ b/PluralKit.API/Controllers/v2/GroupControllerV2.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -16,13 +17,23 @@ namespace PluralKit.API { public GroupControllerV2(IServiceProvider svc) : base(svc) { } - [HttpGet("systems/{system_id}/groups")] - public async Task GetSystemGroups(string system_id) + [HttpGet("systems/{systemRef}/groups")] + public async Task GetSystemGroups(string systemRef) { - return new ObjectResult("Unimplemented") - { - StatusCode = 501 - }; + var system = await ResolveSystem(systemRef); + if (system == null) + throw APIErrors.SystemNotFound; + + var ctx = this.ContextFor(system); + + if (!system.GroupListPrivacy.CanAccess(User.ContextFor(system))) + throw APIErrors.UnauthorizedGroupList; + + var groups = _repo.GetSystemGroups(system.Id); + return Ok(await groups + .Where(g => g.Visibility.CanAccess(ctx)) + .Select(g => g.ToJson(ctx)) + .ToListAsync()); } [HttpPost("groups")] @@ -34,13 +45,16 @@ namespace PluralKit.API }; } - [HttpGet("groups/{group_id}")] - public async Task GroupGet(string group_id) + [HttpGet("groups/{groupRef}")] + public async Task GroupGet(string groupRef) { - return new ObjectResult("Unimplemented") - { - StatusCode = 501 - }; + var group = await ResolveGroup(groupRef); + if (group == null) + throw APIErrors.GroupNotFound; + + var system = await _repo.GetSystem(group.System); + + return Ok(group.ToJson(this.ContextFor(group), systemStr: system.Hid)); } [HttpPatch("groups/{group_id}")] diff --git a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs index 97a971c7..6b0ce6c9 100644 --- a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs +++ b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs @@ -1,10 +1,13 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; +using PluralKit.Core; + namespace PluralKit.API { [ApiController] @@ -14,22 +17,46 @@ namespace PluralKit.API { public GroupMemberControllerV2(IServiceProvider svc) : base(svc) { } - [HttpGet("groups/{group_id}/members")] - public async Task GetGroupMembers(string group_id) + [HttpGet("groups/{groupRef}/members")] + public async Task GetGroupMembers(string groupRef) { - return new ObjectResult("Unimplemented") - { - StatusCode = 501 - }; + var group = await ResolveGroup(groupRef); + if (group == null) + throw APIErrors.GroupNotFound; + + var ctx = this.ContextFor(group); + + if (!group.ListPrivacy.CanAccess(ctx)) + throw APIErrors.UnauthorizedGroupMemberList; + + var members = _repo.GetGroupMembers(group.Id).Where(m => m.MemberVisibility.CanAccess(ctx)); + + var o = new JArray(); + + await foreach (var member in members) + o.Add(member.ToJson(ctx, v: APIVersion.V2)); + + return Ok(o); } - [HttpGet("members/{member_id}/groups")] - public async Task GetMemberGroups(string member_id) + [HttpGet("members/{memberRef}/groups")] + public async Task GetMemberGroups(string memberRef) { - return new ObjectResult("Unimplemented") - { - StatusCode = 501 - }; + var member = await ResolveMember(memberRef); + var ctx = this.ContextFor(member); + + var system = await _repo.GetSystem(member.System); + if (!system.GroupListPrivacy.CanAccess(ctx)) + throw APIErrors.UnauthorizedGroupList; + + var groups = _repo.GetMemberGroups(member.Id).Where(g => g.Visibility.CanAccess(ctx)); + + var o = new JArray(); + + await foreach (var group in groups) + o.Add(group.ToJson(ctx)); + + return Ok(o); } [HttpPut("groups/{group_id}/members/{member_id}")] diff --git a/PluralKit.API/Controllers/v2/MemberControllerV2.cs b/PluralKit.API/Controllers/v2/MemberControllerV2.cs index abb65a4c..5b97ce78 100644 --- a/PluralKit.API/Controllers/v2/MemberControllerV2.cs +++ b/PluralKit.API/Controllers/v2/MemberControllerV2.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -17,13 +18,23 @@ namespace PluralKit.API public MemberControllerV2(IServiceProvider svc) : base(svc) { } - [HttpGet("systems/{system}/members")] - public async Task GetSystemMembers(string system) + [HttpGet("systems/{systemRef}/members")] + public async Task GetSystemMembers(string systemRef) { - return new ObjectResult("Unimplemented") - { - StatusCode = 501 - }; + var system = await ResolveSystem(systemRef); + if (system == null) + throw APIErrors.SystemNotFound; + + var ctx = this.ContextFor(system); + + if (!system.MemberListPrivacy.CanAccess(this.ContextFor(system))) + throw APIErrors.UnauthorizedMemberList; + + var members = _repo.GetSystemMembers(system.Id); + return Ok(await members + .Where(m => m.MemberVisibility.CanAccess(ctx)) + .Select(m => m.ToJson(ctx, v: APIVersion.V2)) + .ToListAsync()); } [HttpPost("members")] @@ -35,13 +46,16 @@ namespace PluralKit.API }; } - [HttpGet("members/{member}")] - public async Task MemberGet(string member) + [HttpGet("members/{memberRef}")] + public async Task MemberGet(string memberRef) { - return new ObjectResult("Unimplemented") - { - StatusCode = 501 - }; + var member = await ResolveMember(memberRef); + if (member == null) + throw APIErrors.MemberNotFound; + + var system = await _repo.GetSystem(member.System); + + return Ok(member.ToJson(this.ContextFor(member), systemStr: system.Hid, v: APIVersion.V2)); } [HttpPatch("members/{member}")] diff --git a/PluralKit.API/Controllers/v2/MiscControllerV2.cs b/PluralKit.API/Controllers/v2/MiscControllerV2.cs index acabf6ea..1e015b37 100644 --- a/PluralKit.API/Controllers/v2/MiscControllerV2.cs +++ b/PluralKit.API/Controllers/v2/MiscControllerV2.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; +using NodaTime; + using PluralKit.Core; namespace PluralKit.API @@ -28,12 +30,25 @@ namespace PluralKit.API return Ok(o); } - [HttpGet("messages/{message_id}")] - public async Task MessageGet(ulong message_id) + [HttpGet("messages/{messageId}")] + public async Task> MessageGet(ulong messageId) { - return new ObjectResult("Unimplemented") + var msg = await _db.Execute(c => _repo.GetMessage(c, messageId)); + if (msg == null) + throw APIErrors.MessageNotFound; + + var ctx = this.ContextFor(msg.System); + + // todo: don't rely on v1 stuff + return new MessageReturn { - StatusCode = 501 + Timestamp = Instant.FromUnixTimeMilliseconds((long)(msg.Message.Mid >> 22) + 1420070400000), + Id = msg.Message.Mid.ToString(), + Channel = msg.Message.Channel.ToString(), + Sender = msg.Message.Sender.ToString(), + System = msg.System.ToJson(ctx, v: APIVersion.V2), + Member = msg.Member.ToJson(ctx, v: APIVersion.V2), + Original = msg.Message.OriginalMid?.ToString() }; } } diff --git a/PluralKit.API/Controllers/v2/SwitchControllerV2.cs b/PluralKit.API/Controllers/v2/SwitchControllerV2.cs index 73e09bb2..8143104f 100644 --- a/PluralKit.API/Controllers/v2/SwitchControllerV2.cs +++ b/PluralKit.API/Controllers/v2/SwitchControllerV2.cs @@ -1,14 +1,28 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Dapper; + using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NodaTime; + 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}")] @@ -17,22 +31,57 @@ namespace PluralKit.API public SwitchControllerV2(IServiceProvider svc) : base(svc) { } - [HttpGet("systems/{system}/switches")] - public async Task GetSystemSwitches(string system) + [HttpGet("systems/{systemRef}/switches")] + public async Task GetSystemSwitches(string systemRef, [FromQuery(Name = "before")] Instant? before, [FromQuery(Name = "limit")] int? limit) { - return new ObjectResult("Unimplemented") - { - StatusCode = 501 - }; + var system = await ResolveSystem(systemRef); + if (system == null) + throw APIErrors.SystemNotFound; + + var ctx = this.ContextFor(system); + + if (!system.FrontHistoryPrivacy.CanAccess(ctx)) + throw APIErrors.UnauthorizedFrontHistory; + + if (before == null) + before = SystemClock.Instance.GetCurrentInstant(); + + if (limit == null || limit > 100) + limit = 100; + + var res = await _db.Execute(conn => conn.QueryAsync( + @"select *, array( + select members.hid from switch_members, members + where switch_members.switch = switches.id and members.id = switch_members.member + ) as members from switches + where switches.system = @System and switches.timestamp <= @Before + order by switches.timestamp desc + limit @Limit;", new { System = system.Id, Before = before, Limit = limit })); + return Ok(res); } - [HttpGet("systems/{system}/fronters")] - public async Task GetSystemFronters(string system) + [HttpGet("systems/{systemRef}/fronters")] + public async Task GetSystemFronters(string systemRef) { - return new ObjectResult("Unimplemented") + var system = await ResolveSystem(systemRef); + if (system == null) + throw APIErrors.SystemNotFound; + + var ctx = this.ContextFor(system); + + if (!system.FrontPrivacy.CanAccess(ctx)) + throw APIErrors.UnauthorizedCurrentFronters; + + var sw = await _repo.GetLatestSwitch(system.Id); + if (sw == null) + return NoContent(); + + var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)); + return Ok(new FrontersReturn { - StatusCode = 501 - }; + Timestamp = sw.Timestamp, + Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync() + }); } @@ -46,13 +95,31 @@ namespace PluralKit.API } - [HttpGet("systems/{system}/switches/{switch_id}")] - public async Task SwitchGet(string system, string switch_id) + [HttpGet("systems/{systemRef}/switches/{switchRef}")] + public async Task SwitchGet(string systemRef, string switchRef) { - return new ObjectResult("Unimplemented") + if (!Guid.TryParse(switchRef, out var switchId)) + throw APIErrors.SwitchNotFound; + + var system = await ResolveSystem(systemRef); + if (system == null) + throw APIErrors.SystemNotFound; + + var ctx = this.ContextFor(system); + + if (!system.FrontHistoryPrivacy.CanAccess(ctx)) + throw APIErrors.UnauthorizedFrontHistory; + + var sw = await _repo.GetSwitchByUuid(switchId); + if (sw == null) + throw APIErrors.SwitchNotFound; + + var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)); + return Ok(new FrontersReturn { - StatusCode = 501 - }; + Timestamp = sw.Timestamp, + Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync() + }); } [HttpPatch("systems/{system}/switches/{switch_id}")] diff --git a/PluralKit.API/Controllers/v2/SystemControllerV2.cs b/PluralKit.API/Controllers/v2/SystemControllerV2.cs index f58b1c02..b7432166 100644 --- a/PluralKit.API/Controllers/v2/SystemControllerV2.cs +++ b/PluralKit.API/Controllers/v2/SystemControllerV2.cs @@ -21,7 +21,7 @@ namespace PluralKit.API { var system = await ResolveSystem(systemRef); if (system == null) return NotFound(); - else return Ok(system.ToJson(LookupContextFor(system))); + else return Ok(system.ToJson(this.ContextFor(system), v: APIVersion.V2)); } [HttpPatch("{system}")] diff --git a/PluralKit.API/Errors.cs b/PluralKit.API/Errors.cs index de0779f2..4320eadd 100644 --- a/PluralKit.API/Errors.cs +++ b/PluralKit.API/Errors.cs @@ -41,11 +41,17 @@ namespace PluralKit.API public static class APIErrors { public static PKError GenericBadRequest = new(400, 0, "400: Bad Request"); + 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 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"); + public static PKError UnauthorizedCurrentFronters = new(403, 30004, "Unauthorized to view current fronters."); + public static PKError UnauthorizedFrontHistory = new(403, 30004, "Unauthorized to view front history."); public static PKError Unimplemented = new(501, 50001, "Unimplemented"); } } \ No newline at end of file diff --git a/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs b/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs index 992b0ed5..a42ffb70 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs @@ -16,6 +16,15 @@ namespace PluralKit.Core return _db.QueryStream(query); } + public IAsyncEnumerable GetGroupMembers(GroupId id) + { + var query = new Query("group_members") + .Select("members.*") + .Join("members", "group_members.member_id", "members.id") + .Where("group_members.group_id", id); + return _db.QueryStream(query); + } + // todo: add this to metrics tracking public async Task AddGroupsToMember(MemberId member, IReadOnlyCollection groups) {