feat: upgrade to .NET 6, refactor everything

This commit is contained in:
spiral
2021-11-26 21:10:56 -05:00
parent d28e99ba43
commit 1918c56937
314 changed files with 27954 additions and 27966 deletions

View File

@@ -1,100 +1,92 @@
using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
public class PKControllerBase: ControllerBase
{
public class PKControllerBase: ControllerBase
private readonly Guid _requestId = Guid.NewGuid();
private readonly Regex _shortIdRegex = new("^[a-z]{5}$");
private readonly Regex _snowflakeRegex = new("^[0-9]{17,19}$");
protected readonly ApiConfig _config;
protected readonly IDatabase _db;
protected readonly ModelRepository _repo;
protected readonly DispatchService _dispatch;
public PKControllerBase(IServiceProvider svc)
{
private readonly Guid _requestId = Guid.NewGuid();
private readonly Regex _shortIdRegex = new Regex("^[a-z]{5}$");
private readonly Regex _snowflakeRegex = new Regex("^[0-9]{17,19}$");
_config = svc.GetRequiredService<ApiConfig>();
_db = svc.GetRequiredService<IDatabase>();
_repo = svc.GetRequiredService<ModelRepository>();
_dispatch = svc.GetRequiredService<DispatchService>();
}
protected readonly ApiConfig _config;
protected readonly IDatabase _db;
protected readonly ModelRepository _repo;
protected readonly DispatchService _dispatch;
public PKControllerBase(IServiceProvider svc)
{
_config = svc.GetRequiredService<ApiConfig>();
_db = svc.GetRequiredService<IDatabase>();
_repo = svc.GetRequiredService<ModelRepository>();
_dispatch = svc.GetRequiredService<DispatchService>();
}
protected Task<PKSystem?> ResolveSystem(string systemRef)
{
if (systemRef == "@me")
{
HttpContext.Items.TryGetValue("SystemId", out var systemId);
if (systemId == null)
throw Errors.GenericAuthError;
return _repo.GetSystem((SystemId)systemId);
}
if (Guid.TryParse(systemRef, out var guid))
return _repo.GetSystemByGuid(guid);
if (_snowflakeRegex.IsMatch(systemRef))
return _repo.GetSystemByAccount(ulong.Parse(systemRef));
if (_shortIdRegex.IsMatch(systemRef))
return _repo.GetSystemByHid(systemRef);
return Task.FromResult<PKSystem?>(null);
}
protected Task<PKMember?> ResolveMember(string memberRef)
{
if (Guid.TryParse(memberRef, out var guid))
return _repo.GetMemberByGuid(guid);
if (_shortIdRegex.IsMatch(memberRef))
return _repo.GetMemberByHid(memberRef);
return Task.FromResult<PKMember?>(null);
}
protected Task<PKGroup?> ResolveGroup(string groupRef)
{
if (Guid.TryParse(groupRef, out var guid))
return _repo.GetGroupByGuid(guid);
if (_shortIdRegex.IsMatch(groupRef))
return _repo.GetGroupByHid(groupRef);
return Task.FromResult<PKGroup?>(null);
}
protected LookupContext ContextFor(PKSystem system)
protected Task<PKSystem?> ResolveSystem(string systemRef)
{
if (systemRef == "@me")
{
HttpContext.Items.TryGetValue("SystemId", out var systemId);
if (systemId == null) return LookupContext.ByNonOwner;
return ((SystemId)systemId) == system.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner;
if (systemId == null)
throw Errors.GenericAuthError;
return _repo.GetSystem((SystemId)systemId);
}
protected 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;
}
if (Guid.TryParse(systemRef, out var guid))
return _repo.GetSystemByGuid(guid);
protected 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;
}
if (_snowflakeRegex.IsMatch(systemRef))
return _repo.GetSystemByAccount(ulong.Parse(systemRef));
if (_shortIdRegex.IsMatch(systemRef))
return _repo.GetSystemByHid(systemRef);
return Task.FromResult<PKSystem?>(null);
}
protected Task<PKMember?> ResolveMember(string memberRef)
{
if (Guid.TryParse(memberRef, out var guid))
return _repo.GetMemberByGuid(guid);
if (_shortIdRegex.IsMatch(memberRef))
return _repo.GetMemberByHid(memberRef);
return Task.FromResult<PKMember?>(null);
}
protected Task<PKGroup?> ResolveGroup(string groupRef)
{
if (Guid.TryParse(groupRef, out var guid))
return _repo.GetGroupByGuid(guid);
if (_shortIdRegex.IsMatch(groupRef))
return _repo.GetGroupByHid(groupRef);
return Task.FromResult<PKGroup?>(null);
}
protected LookupContext ContextFor(PKSystem system)
{
HttpContext.Items.TryGetValue("SystemId", out var systemId);
if (systemId == null) return LookupContext.ByNonOwner;
return (SystemId)systemId == system.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner;
}
protected 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;
}
protected 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;
}
}

View File

@@ -1,34 +1,32 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}/a")]
public class AccountController: ControllerBase
{
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}/a")]
public class AccountController: ControllerBase
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public AccountController(IDatabase db, ModelRepository repo)
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public AccountController(IDatabase db, ModelRepository repo)
{
_db = db;
_repo = repo;
}
_db = db;
_repo = repo;
}
[HttpGet("{aid}")]
public async Task<ActionResult<JObject>> GetSystemByAccount(ulong aid)
{
var system = await _repo.GetSystemByAccount(aid);
if (system == null)
return NotFound("Account not found.");
[HttpGet("{aid}")]
public async Task<ActionResult<JObject>> GetSystemByAccount(ulong aid)
{
var system = await _repo.GetSystemByAccount(aid);
if (system == null)
return NotFound("Account not found.");
return Ok(system.ToJson(User.ContextFor(system)));
}
return Ok(system.ToJson(User.ContextFor(system)));
}
}

View File

@@ -1,6 +1,3 @@
using System;
using System.Threading.Tasks;
using Dapper;
using Microsoft.AspNetCore.Authorization;
@@ -10,117 +7,115 @@ using Newtonsoft.Json.Linq;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}/m")]
public class MemberController: ControllerBase
{
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}/m")]
public class MemberController: ControllerBase
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly IAuthorizationService _auth;
public MemberController(IAuthorizationService auth, IDatabase db, ModelRepository repo)
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly IAuthorizationService _auth;
_auth = auth;
_db = db;
_repo = repo;
}
public MemberController(IAuthorizationService auth, IDatabase db, ModelRepository repo)
[HttpGet("{hid}")]
public async Task<ActionResult<JObject>> GetMember(string hid)
{
var member = await _repo.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
return Ok(member.ToJson(User.ContextFor(member), true));
}
[HttpPost]
[Authorize]
public async Task<ActionResult<JObject>> PostMember([FromBody] JObject properties)
{
if (!properties.ContainsKey("name"))
return BadRequest("Member name must be specified.");
var systemId = User.CurrentSystem();
var systemData = await _repo.GetSystem(systemId);
await using var conn = await _db.Obtain();
// Enforce per-system member limit
var memberCount = await conn.QuerySingleAsync<int>("select count(*) from members where system = @System",
new { System = systemId });
var memberLimit = systemData?.MemberLimitOverride ?? Limits.MaxMemberCount;
if (memberCount >= memberLimit)
return BadRequest($"Member limit reached ({memberCount} / {memberLimit}).");
await using var tx = await conn.BeginTransactionAsync();
var member = await _repo.CreateMember(systemId, properties.Value<string>("name"), conn);
var patch = MemberPatch.FromJSON(properties);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
{
_auth = auth;
_db = db;
_repo = repo;
await tx.RollbackAsync();
var err = patch.Errors[0];
if (err is FieldTooLongError)
return BadRequest($"Field {err.Key} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
if (err.Text != null)
return BadRequest(err.Text);
return BadRequest($"Field {err.Key} is invalid.");
}
[HttpGet("{hid}")]
public async Task<ActionResult<JObject>> GetMember(string hid)
{
var member = await _repo.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
member = await _repo.UpdateMember(member.Id, patch, conn);
await tx.CommitAsync();
return Ok(member.ToJson(User.ContextFor(member), true));
}
return Ok(member.ToJson(User.ContextFor(member), needsLegacyProxyTags: true));
[HttpPatch("{hid}")]
[Authorize]
public async Task<ActionResult<JObject>> PatchMember(string hid, [FromBody] JObject changes)
{
var member = await _repo.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
if (!res.Succeeded) return Unauthorized($"Member '{hid}' is not part of your system.");
var patch = MemberPatch.FromJSON(changes);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
{
var err = patch.Errors[0];
if (err is FieldTooLongError)
return BadRequest($"Field {err.Key} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
if (err.Text != null)
return BadRequest(err.Text);
return BadRequest($"Field {err.Key} is invalid.");
}
[HttpPost]
[Authorize]
public async Task<ActionResult<JObject>> PostMember([FromBody] JObject properties)
{
if (!properties.ContainsKey("name"))
return BadRequest("Member name must be specified.");
var newMember = await _repo.UpdateMember(member.Id, patch);
return Ok(newMember.ToJson(User.ContextFor(newMember), true));
}
var systemId = User.CurrentSystem();
var systemData = await _repo.GetSystem(systemId);
[HttpDelete("{hid}")]
[Authorize]
public async Task<ActionResult> DeleteMember(string hid)
{
var member = await _repo.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
await using var conn = await _db.Obtain();
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
if (!res.Succeeded) return Unauthorized($"Member '{hid}' is not part of your system.");
// Enforce per-system member limit
var memberCount = await conn.QuerySingleAsync<int>("select count(*) from members where system = @System", new { System = systemId });
var memberLimit = systemData?.MemberLimitOverride ?? Limits.MaxMemberCount;
if (memberCount >= memberLimit)
return BadRequest($"Member limit reached ({memberCount} / {memberLimit}).");
await using var tx = await conn.BeginTransactionAsync();
var member = await _repo.CreateMember(systemId, properties.Value<string>("name"), conn);
var patch = MemberPatch.FromJSON(properties);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
{
await tx.RollbackAsync();
var err = patch.Errors[0];
if (err is FieldTooLongError)
return BadRequest($"Field {err.Key} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
else if (err.Text != null)
return BadRequest(err.Text);
else
return BadRequest($"Field {err.Key} is invalid.");
}
member = await _repo.UpdateMember(member.Id, patch, conn);
await tx.CommitAsync();
return Ok(member.ToJson(User.ContextFor(member), needsLegacyProxyTags: true));
}
[HttpPatch("{hid}")]
[Authorize]
public async Task<ActionResult<JObject>> PatchMember(string hid, [FromBody] JObject changes)
{
var member = await _repo.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
if (!res.Succeeded) return Unauthorized($"Member '{hid}' is not part of your system.");
var patch = MemberPatch.FromJSON(changes);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
{
var err = patch.Errors[0];
if (err is FieldTooLongError)
return BadRequest($"Field {err.Key} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
else if (err.Text != null)
return BadRequest(err.Text);
else
return BadRequest($"Field {err.Key} is invalid.");
}
var newMember = await _repo.UpdateMember(member.Id, patch);
return Ok(newMember.ToJson(User.ContextFor(newMember), needsLegacyProxyTags: true));
}
[HttpDelete("{hid}")]
[Authorize]
public async Task<ActionResult> DeleteMember(string hid)
{
var member = await _repo.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
if (!res.Succeeded) return Unauthorized($"Member '{hid}' is not part of your system.");
await _repo.DeleteMember(member.Id);
return Ok();
}
await _repo.DeleteMember(member.Id);
return Ok();
}
}

View File

@@ -1,37 +1,31 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}/msg")]
public class MessageController: ControllerBase
{
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}/msg")]
public class MessageController: ControllerBase
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public MessageController(ModelRepository repo, IDatabase db)
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
_repo = repo;
_db = db;
}
public MessageController(ModelRepository repo, IDatabase db)
{
_repo = repo;
_db = db;
}
[HttpGet("{mid}")]
public async Task<ActionResult<JObject>> GetMessage(ulong mid)
{
var msg = await _db.Execute(c => _repo.GetMessage(c, mid));
if (msg == null) return NotFound("Message not found.");
[HttpGet("{mid}")]
public async Task<ActionResult<JObject>> GetMessage(ulong mid)
{
var msg = await _db.Execute(c => _repo.GetMessage(c, mid));
if (msg == null) return NotFound("Message not found.");
return msg.ToJson(User.ContextFor(msg.System), APIVersion.V1);
}
return msg.ToJson(User.ContextFor(msg.System), APIVersion.V1);
}
}

View File

@@ -1,40 +1,35 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}")]
public class MetaController: ControllerBase
{
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}")]
public class MetaController: ControllerBase
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public MetaController(IDatabase db, ModelRepository repo)
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public MetaController(IDatabase db, ModelRepository repo)
{
_db = db;
_repo = repo;
}
_db = db;
_repo = repo;
}
[HttpGet("meta")]
public async Task<ActionResult<JObject>> GetMeta()
{
await using var conn = await _db.Obtain();
var shards = await _repo.GetShards();
[HttpGet("meta")]
public async Task<ActionResult<JObject>> GetMeta()
{
await using var conn = await _db.Obtain();
var shards = await _repo.GetShards();
var o = new JObject();
o.Add("shards", shards.ToJSON());
o.Add("version", BuildInfoService.Version);
var o = new JObject();
o.Add("shards", shards.ToJSON());
o.Add("version", BuildInfoService.Version);
return Ok(o);
}
return Ok(o);
}
}

View File

@@ -1,12 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
@@ -16,183 +10,189 @@ using NodaTime;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
public struct SwitchesReturn
{
public struct SwitchesReturn
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
[JsonProperty("members")] public IEnumerable<string> Members { get; set; }
}
public struct FrontersReturn
{
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
[JsonProperty("members")] public IEnumerable<JObject> Members { get; set; }
}
public struct PostSwitchParams
{
public Instant? Timestamp { get; set; }
public ICollection<string> Members { get; set; }
}
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}/s")]
public class SystemController: ControllerBase
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly IAuthorizationService _auth;
public SystemController(IDatabase db, IAuthorizationService auth, ModelRepository repo)
{
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
[JsonProperty("members")] public IEnumerable<string> Members { get; set; }
_db = db;
_auth = auth;
_repo = repo;
}
public struct FrontersReturn
[HttpGet]
[Authorize]
public async Task<ActionResult<JObject>> GetOwnSystem()
{
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
[JsonProperty("members")] public IEnumerable<JObject> Members { get; set; }
var system = await _repo.GetSystem(User.CurrentSystem());
return system.ToJson(User.ContextFor(system));
}
public struct PostSwitchParams
[HttpGet("{hid}")]
public async Task<ActionResult<JObject>> GetSystem(string hid)
{
public Instant? Timestamp { get; set; }
public ICollection<string> Members { get; set; }
var system = await _repo.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
return Ok(system.ToJson(User.ContextFor(system)));
}
[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}/s")]
public class SystemController: ControllerBase
[HttpGet("{hid}/members")]
public async Task<ActionResult<IEnumerable<JObject>>> GetMembers(string hid)
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly IAuthorizationService _auth;
var system = await _repo.GetSystemByHid(hid);
if (system == null)
return NotFound("System not found.");
public SystemController(IDatabase db, IAuthorizationService auth, ModelRepository repo)
if (!system.MemberListPrivacy.CanAccess(User.ContextFor(system)))
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view member list.");
var members = _repo.GetSystemMembers(system.Id);
return Ok(await members
.Where(m => m.MemberVisibility.CanAccess(User.ContextFor(system)))
.Select(m => m.ToJson(User.ContextFor(system), needsLegacyProxyTags: true))
.ToListAsync());
}
[HttpGet("{hid}/switches")]
public async Task<ActionResult<IEnumerable<SwitchesReturn>>> GetSwitches(
string hid, [FromQuery(Name = "before")] Instant? before)
{
if (before == null) before = SystemClock.Instance.GetCurrentInstant();
var system = await _repo.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
var auth = await _auth.AuthorizeAsync(User, system, "ViewFrontHistory");
if (!auth.Succeeded)
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view front history.");
var res = await _db.Execute(conn => conn.QueryAsync<SwitchesReturn>(
@"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 100;",
new { System = system.Id, Before = before }
));
return Ok(res);
}
[HttpGet("{hid}/fronters")]
public async Task<ActionResult<FrontersReturn>> GetFronters(string hid)
{
var system = await _repo.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
var auth = await _auth.AuthorizeAsync(User, system, "ViewFront");
if (!auth.Succeeded) return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view fronter.");
var sw = await _repo.GetLatestSwitch(system.Id);
if (sw == null) return NotFound("System has no registered switches.");
var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
return Ok(new FrontersReturn
{
_db = db;
_auth = auth;
_repo = repo;
Timestamp = sw.Timestamp,
Members = await members.Select(m => m.ToJson(User.ContextFor(system), true)).ToListAsync()
});
}
[HttpPatch]
[Authorize]
public async Task<ActionResult<JObject>> EditSystem([FromBody] JObject changes)
{
var system = await _repo.GetSystem(User.CurrentSystem());
var patch = SystemPatch.FromJSON(changes);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
{
var err = patch.Errors[0];
if (err is FieldTooLongError)
return BadRequest($"Field {err.Key} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
return BadRequest($"Field {err.Key} is invalid.");
}
[HttpGet]
[Authorize]
public async Task<ActionResult<JObject>> GetOwnSystem()
system = await _repo.UpdateSystem(system!.Id, patch);
return Ok(system.ToJson(User.ContextFor(system)));
}
[HttpPost("switches")]
[Authorize]
public async Task<IActionResult> PostSwitch([FromBody] PostSwitchParams param)
{
if (param.Members.Distinct().Count() != param.Members.Count)
return BadRequest("Duplicate members in member list.");
await using var conn = await _db.Obtain();
// We get the current switch, if it exists
var latestSwitch = await _repo.GetLatestSwitch(User.CurrentSystem());
if (latestSwitch != null)
{
var system = await _repo.GetSystem(User.CurrentSystem());
return system.ToJson(User.ContextFor(system));
var latestSwitchMembers = _repo.GetSwitchMembers(conn, latestSwitch.Id);
// Bail if this switch is identical to the latest one
if (await latestSwitchMembers.Select(m => m.Hid).SequenceEqualAsync(param.Members.ToAsyncEnumerable()))
return BadRequest("New members identical to existing fronters.");
}
[HttpGet("{hid}")]
public async Task<ActionResult<JObject>> GetSystem(string hid)
// Resolve member objects for all given IDs
var membersList =
(await conn.QueryAsync<PKMember>("select * from members where hid = any(@Hids)",
new { Hids = param.Members })).ToList();
foreach (var member in membersList)
if (member.System != User.CurrentSystem())
return BadRequest($"Cannot switch to member '{member.Hid}' not in system.");
// membersList is in DB order, and we want it in actual input order
// so we go through a dict and map the original input appropriately
var membersDict = membersList.ToDictionary(m => m.Hid);
var membersInOrder = new List<PKMember>();
// We do this without .Select() since we want to have the early return bail if it doesn't find the member
foreach (var givenMemberId in param.Members)
{
var system = await _repo.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
return Ok(system.ToJson(User.ContextFor(system)));
if (!membersDict.TryGetValue(givenMemberId, out var member))
return BadRequest($"Member '{givenMemberId}' not found.");
membersInOrder.Add(member);
}
[HttpGet("{hid}/members")]
public async Task<ActionResult<IEnumerable<JObject>>> GetMembers(string hid)
{
var system = await _repo.GetSystemByHid(hid);
if (system == null)
return NotFound("System not found.");
if (!system.MemberListPrivacy.CanAccess(User.ContextFor(system)))
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view member list.");
var members = _repo.GetSystemMembers(system.Id);
return Ok(await members
.Where(m => m.MemberVisibility.CanAccess(User.ContextFor(system)))
.Select(m => m.ToJson(User.ContextFor(system), needsLegacyProxyTags: true))
.ToListAsync());
}
[HttpGet("{hid}/switches")]
public async Task<ActionResult<IEnumerable<SwitchesReturn>>> GetSwitches(string hid, [FromQuery(Name = "before")] Instant? before)
{
if (before == null) before = SystemClock.Instance.GetCurrentInstant();
var system = await _repo.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
var auth = await _auth.AuthorizeAsync(User, system, "ViewFrontHistory");
if (!auth.Succeeded) return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view front history.");
var res = await _db.Execute(conn => conn.QueryAsync<SwitchesReturn>(
@"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 100;", new { System = system.Id, Before = before }));
return Ok(res);
}
[HttpGet("{hid}/fronters")]
public async Task<ActionResult<FrontersReturn>> GetFronters(string hid)
{
var system = await _repo.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
var auth = await _auth.AuthorizeAsync(User, system, "ViewFront");
if (!auth.Succeeded) return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view fronter.");
var sw = await _repo.GetLatestSwitch(system.Id);
if (sw == null) return NotFound("System has no registered switches.");
var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
return Ok(new FrontersReturn
{
Timestamp = sw.Timestamp,
Members = await members.Select(m => m.ToJson(User.ContextFor(system), needsLegacyProxyTags: true)).ToListAsync()
});
}
[HttpPatch]
[Authorize]
public async Task<ActionResult<JObject>> EditSystem([FromBody] JObject changes)
{
var system = await _repo.GetSystem(User.CurrentSystem());
var patch = SystemPatch.FromJSON(changes);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
{
var err = patch.Errors[0];
if (err is FieldTooLongError)
return BadRequest($"Field {err.Key} is too long "
+ $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).");
return BadRequest($"Field {err.Key} is invalid.");
}
system = await _repo.UpdateSystem(system!.Id, patch);
return Ok(system.ToJson(User.ContextFor(system)));
}
[HttpPost("switches")]
[Authorize]
public async Task<IActionResult> PostSwitch([FromBody] PostSwitchParams param)
{
if (param.Members.Distinct().Count() != param.Members.Count)
return BadRequest("Duplicate members in member list.");
await using var conn = await _db.Obtain();
// We get the current switch, if it exists
var latestSwitch = await _repo.GetLatestSwitch(User.CurrentSystem());
if (latestSwitch != null)
{
var latestSwitchMembers = _repo.GetSwitchMembers(conn, latestSwitch.Id);
// Bail if this switch is identical to the latest one
if (await latestSwitchMembers.Select(m => m.Hid).SequenceEqualAsync(param.Members.ToAsyncEnumerable()))
return BadRequest("New members identical to existing fronters.");
}
// Resolve member objects for all given IDs
var membersList = (await conn.QueryAsync<PKMember>("select * from members where hid = any(@Hids)", new { Hids = param.Members })).ToList();
foreach (var member in membersList)
if (member.System != User.CurrentSystem())
return BadRequest($"Cannot switch to member '{member.Hid}' not in system.");
// membersList is in DB order, and we want it in actual input order
// so we go through a dict and map the original input appropriately
var membersDict = membersList.ToDictionary(m => m.Hid);
var membersInOrder = new List<PKMember>();
// We do this without .Select() since we want to have the early return bail if it doesn't find the member
foreach (var givenMemberId in param.Members)
{
if (!membersDict.TryGetValue(givenMemberId, out var member))
return BadRequest($"Member '{givenMemberId}' not found.");
membersInOrder.Add(member);
}
// Finally, log the switch (yay!)
await _repo.AddSwitch(conn, User.CurrentSystem(), membersInOrder.Select(m => m.Id).ToList());
return NoContent();
}
// Finally, log the switch (yay!)
await _repo.AddSwitch(conn, User.CurrentSystem(), membersInOrder.Select(m => m.Id).ToList());
return NoContent();
}
}

View File

@@ -1,136 +1,134 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using NodaTime;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class DiscordControllerV2: PKControllerBase
{
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class DiscordControllerV2: PKControllerBase
public DiscordControllerV2(IServiceProvider svc) : base(svc) { }
[HttpGet("systems/@me/guilds/{guild_id}")]
public async Task<IActionResult> SystemGuildGet(ulong guild_id)
{
public DiscordControllerV2(IServiceProvider svc) : base(svc) { }
var system = await ResolveSystem("@me");
var settings = await _repo.GetSystemGuild(guild_id, system.Id, false);
if (settings == null)
throw Errors.SystemGuildNotFound;
PKMember member = null;
if (settings.AutoproxyMember != null)
member = await _repo.GetMember(settings.AutoproxyMember.Value);
[HttpGet("systems/@me/guilds/{guild_id}")]
public async Task<IActionResult> SystemGuildGet(ulong guild_id)
return Ok(settings.ToJson(member?.Hid));
}
[HttpPatch("systems/@me/guilds/{guild_id}")]
public async Task<IActionResult> DoSystemGuildPatch(ulong guild_id, [FromBody] JObject data)
{
var system = await ResolveSystem("@me");
var settings = await _repo.GetSystemGuild(guild_id, system.Id, false);
if (settings == null)
throw Errors.SystemGuildNotFound;
MemberId? memberId = null;
if (data.ContainsKey("autoproxy_member"))
{
var system = await ResolveSystem("@me");
var settings = await _repo.GetSystemGuild(guild_id, system.Id, defaultInsert: false);
if (settings == null)
throw Errors.SystemGuildNotFound;
PKMember member = null;
if (settings.AutoproxyMember != null)
member = await _repo.GetMember(settings.AutoproxyMember.Value);
return Ok(settings.ToJson(member?.Hid));
}
[HttpPatch("systems/@me/guilds/{guild_id}")]
public async Task<IActionResult> DoSystemGuildPatch(ulong guild_id, [FromBody] JObject data)
{
var system = await ResolveSystem("@me");
var settings = await _repo.GetSystemGuild(guild_id, system.Id, defaultInsert: false);
if (settings == null)
throw Errors.SystemGuildNotFound;
MemberId? memberId = null;
if (data.ContainsKey("autoproxy_member"))
if (data["autoproxy_member"].Type != JTokenType.Null)
{
if (data["autoproxy_member"].Type != JTokenType.Null)
{
var member = await ResolveMember(data.Value<string>("autoproxy_member"));
if (member == null)
throw Errors.MemberNotFound;
var member = await ResolveMember(data.Value<string>("autoproxy_member"));
if (member == null)
throw Errors.MemberNotFound;
memberId = member.Id;
}
memberId = member.Id;
}
else
memberId = settings.AutoproxyMember;
}
else
{
memberId = settings.AutoproxyMember;
}
var patch = SystemGuildPatch.FromJson(data, memberId);
var patch = SystemGuildPatch.FromJson(data, memberId);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
// this is less than great, but at least it's legible
if (patch.AutoproxyMember.Value == null)
if (patch.AutoproxyMode.IsPresent)
{
if (patch.AutoproxyMode.Value == AutoproxyMode.Member)
throw Errors.MissingAutoproxyMember;
}
else if (settings.AutoproxyMode == AutoproxyMode.Member)
// this is less than great, but at least it's legible
if (patch.AutoproxyMember.Value == null)
if (patch.AutoproxyMode.IsPresent)
{
if (patch.AutoproxyMode.Value == AutoproxyMode.Member)
throw Errors.MissingAutoproxyMember;
}
else if (settings.AutoproxyMode == AutoproxyMode.Member)
{
throw Errors.MissingAutoproxyMember;
}
var newSettings = await _repo.UpdateSystemGuild(system.Id, guild_id, patch);
var newSettings = await _repo.UpdateSystemGuild(system.Id, guild_id, patch);
PKMember? newMember = null;
if (newSettings.AutoproxyMember != null)
newMember = await _repo.GetMember(newSettings.AutoproxyMember.Value);
return Ok(newSettings.ToJson(newMember?.Hid));
}
PKMember? newMember = null;
if (newSettings.AutoproxyMember != null)
newMember = await _repo.GetMember(newSettings.AutoproxyMember.Value);
return Ok(newSettings.ToJson(newMember?.Hid));
}
[HttpGet("members/{memberRef}/guilds/{guild_id}")]
public async Task<IActionResult> MemberGuildGet(string memberRef, ulong guild_id)
{
var system = await ResolveSystem("@me");
var member = await ResolveMember(memberRef);
if (member == null)
throw Errors.MemberNotFound;
if (member.System != system.Id)
throw Errors.NotOwnMemberError;
[HttpGet("members/{memberRef}/guilds/{guild_id}")]
public async Task<IActionResult> MemberGuildGet(string memberRef, ulong guild_id)
{
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 settings = await _repo.GetMemberGuild(guild_id, member.Id, defaultInsert: false);
if (settings == null)
throw Errors.MemberGuildNotFound;
var settings = await _repo.GetMemberGuild(guild_id, member.Id, false);
if (settings == null)
throw Errors.MemberGuildNotFound;
return Ok(settings.ToJson());
}
return Ok(settings.ToJson());
}
[HttpPatch("members/{memberRef}/guilds/{guild_id}")]
public async Task<IActionResult> DoMemberGuildPatch(string memberRef, ulong guild_id, [FromBody] JObject data)
{
var system = await ResolveSystem("@me");
var member = await ResolveMember(memberRef);
if (member == null)
throw Errors.MemberNotFound;
if (member.System != system.Id)
throw Errors.NotOwnMemberError;
[HttpPatch("members/{memberRef}/guilds/{guild_id}")]
public async Task<IActionResult> DoMemberGuildPatch(string memberRef, ulong guild_id, [FromBody] JObject data)
{
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 settings = await _repo.GetMemberGuild(guild_id, member.Id, defaultInsert: false);
if (settings == null)
throw Errors.MemberGuildNotFound;
var settings = await _repo.GetMemberGuild(guild_id, member.Id, false);
if (settings == null)
throw Errors.MemberGuildNotFound;
var patch = MemberGuildPatch.FromJson(data);
var patch = MemberGuildPatch.FromJson(data);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
var newSettings = await _repo.UpdateMemberGuild(member.Id, guild_id, patch);
return Ok(newSettings.ToJson());
}
var newSettings = await _repo.UpdateMemberGuild(member.Id, guild_id, patch);
return Ok(newSettings.ToJson());
}
[HttpGet("messages/{messageId}")]
public async Task<ActionResult<JObject>> MessageGet(ulong messageId)
{
var msg = await _db.Execute(c => _repo.GetMessage(c, messageId));
if (msg == null)
throw Errors.MessageNotFound;
[HttpGet("messages/{messageId}")]
public async Task<ActionResult<JObject>> MessageGet(ulong messageId)
{
var msg = await _db.Execute(c => _repo.GetMessage(c, messageId));
if (msg == null)
throw Errors.MessageNotFound;
var ctx = this.ContextFor(msg.System);
return msg.ToJson(ctx, APIVersion.V2);
}
var ctx = ContextFor(msg.System);
return msg.ToJson(ctx, APIVersion.V2);
}
}

View File

@@ -1,142 +1,135 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class GroupControllerV2: PKControllerBase
{
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class GroupControllerV2: PKControllerBase
public GroupControllerV2(IServiceProvider svc) : base(svc) { }
[HttpGet("systems/{systemRef}/groups")]
public async Task<IActionResult> GetSystemGroups(string systemRef, [FromQuery] bool with_members)
{
public GroupControllerV2(IServiceProvider svc) : base(svc) { }
var system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
[HttpGet("systems/{systemRef}/groups")]
public async Task<IActionResult> GetSystemGroups(string systemRef, [FromQuery] bool with_members)
var ctx = ContextFor(system);
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
throw Errors.UnauthorizedMemberList;
if (!system.GroupListPrivacy.CanAccess(User.ContextFor(system)))
throw Errors.UnauthorizedGroupList;
var groups = _repo.GetSystemGroups(system.Id);
var j_groups = await groups
.Where(g => g.Visibility.CanAccess(ctx))
.Select(g => g.ToJson(ctx, needsMembersArray: with_members))
.ToListAsync();
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
throw Errors.UnauthorizedMemberList;
if (with_members && j_groups.Count > 0)
{
var system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
var q = await _repo.GetGroupMemberInfo(await groups.Select(x => x.Id).ToListAsync());
var ctx = this.ContextFor(system);
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
throw Errors.UnauthorizedMemberList;
if (!system.GroupListPrivacy.CanAccess(User.ContextFor(system)))
throw Errors.UnauthorizedGroupList;
var groups = _repo.GetSystemGroups(system.Id);
var j_groups = await groups
.Where(g => g.Visibility.CanAccess(ctx))
.Select(g => g.ToJson(ctx, needsMembersArray: with_members))
.ToListAsync();
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
throw Errors.UnauthorizedMemberList;
if (with_members && j_groups.Count > 0)
{
var q = await _repo.GetGroupMemberInfo(await groups.Select(x => x.Id).ToListAsync());
foreach (var row in q)
if (row.MemberVisibility.CanAccess(ctx))
((JArray)j_groups.Find(x => x.Value<string>("id") == row.Group)["members"]).Add(row.MemberUuid);
}
return Ok(j_groups);
foreach (var row in q)
if (row.MemberVisibility.CanAccess(ctx))
((JArray)j_groups.Find(x => x.Value<string>("id") == row.Group)["members"]).Add(row.MemberUuid);
}
[HttpPost("groups")]
public async Task<IActionResult> GroupCreate([FromBody] JObject data)
return Ok(j_groups);
}
[HttpPost("groups")]
public async Task<IActionResult> GroupCreate([FromBody] JObject data)
{
var system = await ResolveSystem("@me");
// Check group cap
var existingGroupCount = await _repo.GetSystemGroupCount(system.Id);
var groupLimit = system.GroupLimitOverride ?? Limits.MaxGroupCount;
if (existingGroupCount >= groupLimit)
throw Errors.GroupLimitReached;
var patch = GroupPatch.FromJson(data);
patch.AssertIsValid();
if (!patch.Name.IsPresent)
patch.Errors.Add(new ValidationError("name", "Key 'name' is required when creating new group."));
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
using var conn = await _db.Obtain();
using var tx = await conn.BeginTransactionAsync();
var newGroup = await _repo.CreateGroup(system.Id, patch.Name.Value, conn);
newGroup = await _repo.UpdateGroup(newGroup.Id, patch, conn);
_ = _dispatch.Dispatch(newGroup.Id, new UpdateDispatchData()
{
var system = await ResolveSystem("@me");
Event = DispatchEvent.CREATE_GROUP,
EventData = patch.ToJson(),
});
// Check group cap
var existingGroupCount = await _repo.GetSystemGroupCount(system.Id);
var groupLimit = system.GroupLimitOverride ?? Limits.MaxGroupCount;
if (existingGroupCount >= groupLimit)
throw Errors.GroupLimitReached;
await tx.CommitAsync();
var patch = GroupPatch.FromJson(data);
patch.AssertIsValid();
if (!patch.Name.IsPresent)
patch.Errors.Add(new ValidationError("name", $"Key 'name' is required when creating new group."));
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
return Ok(newGroup.ToJson(LookupContext.ByOwner));
}
using var conn = await _db.Obtain();
using var tx = await conn.BeginTransactionAsync();
[HttpGet("groups/{groupRef}")]
public async Task<IActionResult> GroupGet(string groupRef)
{
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
var newGroup = await _repo.CreateGroup(system.Id, patch.Name.Value, conn);
newGroup = await _repo.UpdateGroup(newGroup.Id, patch, conn);
var system = await _repo.GetSystem(group.System);
return Ok(group.ToJson(ContextFor(group), system.Hid));
}
_ = _dispatch.Dispatch(newGroup.Id, new UpdateDispatchData()
{
Event = DispatchEvent.CREATE_GROUP,
EventData = patch.ToJson(),
});
[HttpPatch("groups/{groupRef}")]
public async Task<IActionResult> DoGroupPatch(string groupRef, [FromBody] JObject data)
{
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 patch = GroupPatch.FromJson(data);
await tx.CommitAsync();
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
return Ok(newGroup.ToJson(LookupContext.ByOwner));
}
var newGroup = await _repo.UpdateGroup(group.Id, patch);
return Ok(newGroup.ToJson(LookupContext.ByOwner));
}
[HttpGet("groups/{groupRef}")]
public async Task<IActionResult> GroupGet(string groupRef)
{
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
[HttpDelete("groups/{groupRef}")]
public async Task<IActionResult> GroupDelete(string groupRef)
{
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
var system = await _repo.GetSystem(group.System);
var system = await ResolveSystem("@me");
if (system.Id != group.System)
throw Errors.NotOwnGroupError;
return Ok(group.ToJson(this.ContextFor(group), systemStr: system.Hid));
}
await _repo.DeleteGroup(group.Id);
[HttpPatch("groups/{groupRef}")]
public async Task<IActionResult> DoGroupPatch(string groupRef, [FromBody] JObject data)
{
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 patch = GroupPatch.FromJson(data);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
var newGroup = await _repo.UpdateGroup(group.Id, patch);
return Ok(newGroup.ToJson(LookupContext.ByOwner));
}
[HttpDelete("groups/{groupRef}")]
public async Task<IActionResult> GroupDelete(string groupRef)
{
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
var system = await ResolveSystem("@me");
if (system.Id != group.System)
throw Errors.NotOwnGroupError;
await _repo.DeleteGroup(group.Id);
return NoContent();
}
return NoContent();
}
}

View File

@@ -1,281 +1,272 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class GroupMemberControllerV2: PKControllerBase
{
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class GroupMemberControllerV2: PKControllerBase
public GroupMemberControllerV2(IServiceProvider svc) : base(svc) { }
[HttpGet("groups/{groupRef}/members")]
public async Task<IActionResult> GetGroupMembers(string groupRef)
{
public GroupMemberControllerV2(IServiceProvider svc) : base(svc) { }
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
[HttpGet("groups/{groupRef}/members")]
public async Task<IActionResult> GetGroupMembers(string groupRef)
var ctx = ContextFor(group);
if (!group.ListPrivacy.CanAccess(ctx))
throw Errors.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);
}
[HttpPost("groups/{groupRef}/members/add")]
public async Task<IActionResult> 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<MemberId>();
foreach (var JmemberRef in memberRefs)
{
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
var memberRef = JmemberRef.Value<string>();
var member = await ResolveMember(memberRef);
var ctx = this.ContextFor(group);
// todo: have a list of these errors instead of immediately throwing
if (!group.ListPrivacy.CanAccess(ctx))
throw Errors.UnauthorizedGroupMemberList;
if (member == null)
throw Errors.MemberNotFoundWithRef(memberRef);
if (member.System != system.Id)
throw Errors.NotOwnMemberErrorWithRef(memberRef);
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);
members.Add(member.Id);
}
[HttpPost("groups/{groupRef}/members/add")]
public async Task<IActionResult> AddGroupMembers(string groupRef, [FromBody] JArray memberRefs)
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<IActionResult> 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<MemberId>();
foreach (var JmemberRef in memberRefs)
{
if (memberRefs.Count == 0)
throw Errors.GenericBadRequest;
var memberRef = JmemberRef.Value<string>();
var member = await ResolveMember(memberRef);
var system = await ResolveSystem("@me");
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<IActionResult> 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<MemberId>();
foreach (var JmemberRef in memberRefs)
{
var memberRef = JmemberRef.Value<string>();
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<IActionResult> GetMemberGroups(string memberRef)
{
var member = await ResolveMember(memberRef);
var ctx = ContextFor(member);
var system = await _repo.GetSystem(member.System);
if (!system.GroupListPrivacy.CanAccess(ctx))
throw Errors.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);
}
[HttpPost("members/{memberRef}/groups/add")]
public async Task<IActionResult> AddMemberGroups(string memberRef, [FromBody] JArray groupRefs)
{
if (groupRefs.Count == 0)
throw Errors.GenericBadRequest;
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<GroupId>();
foreach (var JgroupRef in groupRefs)
{
var groupRef = JgroupRef.Value<string>();
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
if (group.System != system.Id)
throw Errors.NotOwnGroupError;
throw Errors.NotOwnGroupErrorWithRef(groupRef);
var members = new List<MemberId>();
foreach (var JmemberRef in memberRefs)
{
var memberRef = JmemberRef.Value<string>();
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();
groups.Add(group.Id);
}
[HttpPost("groups/{groupRef}/members/remove")]
public async Task<IActionResult> RemoveGroupMembers(string groupRef, [FromBody] JArray memberRefs)
var existingGroups = await _repo.GetMemberGroups(member.Id).Select(x => x.Id).ToListAsync();
groups = groups.Where(x => !existingGroups.Contains(x)).ToList();
if (groups.Count > 0)
await _repo.AddGroupsToMember(member.Id, groups);
return NoContent();
}
[HttpPost("members/{memberRef}/groups/remove")]
public async Task<IActionResult> RemoveMemberGroups(string memberRef, [FromBody] JArray groupRefs)
{
if (groupRefs.Count == 0)
throw Errors.GenericBadRequest;
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<GroupId>();
foreach (var JgroupRef in groupRefs)
{
if (memberRefs.Count == 0)
throw Errors.GenericBadRequest;
var system = await ResolveSystem("@me");
var groupRef = JgroupRef.Value<string>();
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
throw Errors.GroupNotFoundWithRef(groupRef);
if (group.System != system.Id)
throw Errors.NotOwnGroupError;
throw Errors.NotOwnGroupErrorWithRef(groupRef);
var members = new List<MemberId>();
foreach (var JmemberRef in memberRefs)
{
var memberRef = JmemberRef.Value<string>();
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();
groups.Add(group.Id);
}
[HttpPost("groups/{groupRef}/members/overwrite")]
public async Task<IActionResult> OverwriteGroupMembers(string groupRef, [FromBody] JArray memberRefs)
{
var system = await ResolveSystem("@me");
await _repo.RemoveGroupsFromMember(member.Id, groups);
return NoContent();
}
[HttpPost("members/{memberRef}/groups/overwrite")]
public async Task<IActionResult> 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<GroupId>();
foreach (var JgroupRef in groupRefs)
{
var groupRef = JgroupRef.Value<string>();
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
throw Errors.GroupNotFoundWithRef(groupRef);
if (group.System != system.Id)
throw Errors.NotOwnGroupError;
throw Errors.NotOwnGroupErrorWithRef(groupRef);
var members = new List<MemberId>();
foreach (var JmemberRef in memberRefs)
{
var memberRef = JmemberRef.Value<string>();
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();
groups.Add(group.Id);
}
await _repo.ClearMemberGroups(member.Id);
[HttpGet("members/{memberRef}/groups")]
public async Task<IActionResult> GetMemberGroups(string memberRef)
{
var member = await ResolveMember(memberRef);
var ctx = this.ContextFor(member);
var system = await _repo.GetSystem(member.System);
if (!system.GroupListPrivacy.CanAccess(ctx))
throw Errors.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);
}
[HttpPost("members/{memberRef}/groups/add")]
public async Task<IActionResult> AddMemberGroups(string memberRef, [FromBody] JArray groupRefs)
{
if (groupRefs.Count == 0)
throw Errors.GenericBadRequest;
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<GroupId>();
foreach (var JgroupRef in groupRefs)
{
var groupRef = JgroupRef.Value<string>();
var group = await ResolveGroup(groupRef);
if (group == null)
throw Errors.GroupNotFound;
if (group.System != system.Id)
throw Errors.NotOwnGroupErrorWithRef(groupRef);
groups.Add(group.Id);
}
var existingGroups = await _repo.GetMemberGroups(member.Id).Select(x => x.Id).ToListAsync();
groups = groups.Where(x => !existingGroups.Contains(x)).ToList();
if (groups.Count > 0)
await _repo.AddGroupsToMember(member.Id, groups);
return NoContent();
}
[HttpPost("members/{memberRef}/groups/remove")]
public async Task<IActionResult> RemoveMemberGroups(string memberRef, [FromBody] JArray groupRefs)
{
if (groupRefs.Count == 0)
throw Errors.GenericBadRequest;
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<GroupId>();
foreach (var JgroupRef in groupRefs)
{
var groupRef = JgroupRef.Value<string>();
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.RemoveGroupsFromMember(member.Id, groups);
return NoContent();
}
[HttpPost("members/{memberRef}/groups/overwrite")]
public async Task<IActionResult> 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<GroupId>();
foreach (var JgroupRef in groupRefs)
{
var groupRef = JgroupRef.Value<string>();
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();
}
if (groups.Count > 0)
await _repo.AddGroupsToMember(member.Id, groups);
return NoContent();
}
}

View File

@@ -1,122 +1,117 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class MemberControllerV2: PKControllerBase
{
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class MemberControllerV2: PKControllerBase
public MemberControllerV2(IServiceProvider svc) : base(svc) { }
[HttpGet("systems/{systemRef}/members")]
public async Task<IActionResult> GetSystemMembers(string systemRef)
{
public MemberControllerV2(IServiceProvider svc) : base(svc) { }
var system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
var ctx = ContextFor(system);
[HttpGet("systems/{systemRef}/members")]
public async Task<IActionResult> GetSystemMembers(string systemRef)
if (!system.MemberListPrivacy.CanAccess(ContextFor(system)))
throw Errors.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")]
public async Task<IActionResult> MemberCreate([FromBody] JObject data)
{
var system = await ResolveSystem("@me");
var memberCount = await _repo.GetSystemMemberCount(system.Id);
var memberLimit = system.MemberLimitOverride ?? Limits.MaxMemberCount;
if (memberCount >= memberLimit)
throw Errors.MemberLimitReached;
var patch = MemberPatch.FromJSON(data);
patch.AssertIsValid();
if (!patch.Name.IsPresent)
patch.Errors.Add(new ValidationError("name", "Key 'name' is required when creating new member."));
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
using var conn = await _db.Obtain();
using var tx = await conn.BeginTransactionAsync();
var newMember = await _repo.CreateMember(system.Id, patch.Name.Value, conn);
newMember = await _repo.UpdateMember(newMember.Id, patch, conn);
_ = _dispatch.Dispatch(newMember.Id, new()
{
var system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
Event = DispatchEvent.CREATE_MEMBER,
EventData = patch.ToJson(),
});
var ctx = this.ContextFor(system);
await tx.CommitAsync();
if (!system.MemberListPrivacy.CanAccess(this.ContextFor(system)))
throw Errors.UnauthorizedMemberList;
return Ok(newMember.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
}
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());
}
[HttpGet("members/{memberRef}")]
public async Task<IActionResult> MemberGet(string memberRef)
{
var member = await ResolveMember(memberRef);
if (member == null)
throw Errors.MemberNotFound;
[HttpPost("members")]
public async Task<IActionResult> MemberCreate([FromBody] JObject data)
{
var system = await ResolveSystem("@me");
var system = await _repo.GetSystem(member.System);
var memberCount = await _repo.GetSystemMemberCount(system.Id);
var memberLimit = system.MemberLimitOverride ?? Limits.MaxMemberCount;
if (memberCount >= memberLimit)
throw Errors.MemberLimitReached;
return Ok(member.ToJson(ContextFor(member), systemStr: system.Hid, v: APIVersion.V2));
}
var patch = MemberPatch.FromJSON(data);
patch.AssertIsValid();
if (!patch.Name.IsPresent)
patch.Errors.Add(new ValidationError("name", $"Key 'name' is required when creating new member."));
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
[HttpPatch("members/{memberRef}")]
public async Task<IActionResult> DoMemberPatch(string memberRef, [FromBody] JObject data)
{
var system = await ResolveSystem("@me");
var member = await ResolveMember(memberRef);
if (member == null)
throw Errors.MemberNotFound;
if (member.System != system.Id)
throw Errors.NotOwnMemberError;
using var conn = await _db.Obtain();
using var tx = await conn.BeginTransactionAsync();
var patch = MemberPatch.FromJSON(data, APIVersion.V2);
var newMember = await _repo.CreateMember(system.Id, patch.Name.Value, conn);
newMember = await _repo.UpdateMember(newMember.Id, patch, conn);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
_ = _dispatch.Dispatch(newMember.Id, new()
{
Event = DispatchEvent.CREATE_MEMBER,
EventData = patch.ToJson(),
});
var newMember = await _repo.UpdateMember(member.Id, patch);
return Ok(newMember.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
}
await tx.CommitAsync();
[HttpDelete("members/{memberRef}")]
public async Task<IActionResult> MemberDelete(string memberRef)
{
var member = await ResolveMember(memberRef);
if (member == null)
throw Errors.MemberNotFound;
return Ok(newMember.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
}
var system = await ResolveSystem("@me");
if (system.Id != member.System)
throw Errors.NotOwnMemberError;
[HttpGet("members/{memberRef}")]
public async Task<IActionResult> MemberGet(string memberRef)
{
var member = await ResolveMember(memberRef);
if (member == null)
throw Errors.MemberNotFound;
await _repo.DeleteMember(member.Id);
var system = await _repo.GetSystem(member.System);
return Ok(member.ToJson(this.ContextFor(member), systemStr: system.Hid, v: APIVersion.V2));
}
[HttpPatch("members/{memberRef}")]
public async Task<IActionResult> DoMemberPatch(string memberRef, [FromBody] JObject data)
{
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 patch = MemberPatch.FromJSON(data, APIVersion.V2);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
var newMember = await _repo.UpdateMember(member.Id, patch);
return Ok(newMember.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
}
[HttpDelete("members/{memberRef}")]
public async Task<IActionResult> MemberDelete(string memberRef)
{
var member = await ResolveMember(memberRef);
if (member == null)
throw Errors.MemberNotFound;
var system = await ResolveSystem("@me");
if (system.Id != member.System)
throw Errors.NotOwnMemberError;
await _repo.DeleteMember(member.Id);
return NoContent();
}
return NoContent();
}
}

View File

@@ -1,30 +1,26 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class PrivateControllerV2: PKControllerBase
{
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class PrivateControllerV2: PKControllerBase
public PrivateControllerV2(IServiceProvider svc) : base(svc) { }
[HttpGet("meta")]
public async Task<ActionResult<JObject>> Meta()
{
public PrivateControllerV2(IServiceProvider svc) : base(svc) { }
var shards = await _repo.GetShards();
var stats = await _repo.GetStats();
[HttpGet("meta")]
public async Task<ActionResult<JObject>> Meta()
{
var shards = await _repo.GetShards();
var stats = await _repo.GetStats();
var o = new JObject();
o.Add("shards", shards.ToJSON());
o.Add("stats", stats.ToJson());
var o = new JObject();
o.Add("shards", shards.ToJSON());
o.Add("stats", stats.ToJson());
return Ok(o);
}
return Ok(o);
}
}

View File

@@ -1,254 +1,255 @@
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
namespace PluralKit.API;
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class SwitchControllerV2: PKControllerBase
{
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}")]
public class SwitchControllerV2: PKControllerBase
public SwitchControllerV2(IServiceProvider svc) : base(svc) { }
[HttpGet("systems/{systemRef}/switches")]
public async Task<IActionResult> GetSystemSwitches(string systemRef,
[FromQuery(Name = "before")] Instant? before,
[FromQuery(Name = "limit")] int? limit)
{
public SwitchControllerV2(IServiceProvider svc) : base(svc) { }
var system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
var ctx = ContextFor(system);
[HttpGet("systems/{systemRef}/switches")]
public async Task<IActionResult> GetSystemSwitches(string systemRef, [FromQuery(Name = "before")] Instant? before, [FromQuery(Name = "limit")] int? limit)
if (!system.FrontHistoryPrivacy.CanAccess(ctx))
throw Errors.UnauthorizedFrontHistory;
if (before == null)
before = SystemClock.Instance.GetCurrentInstant();
if (limit == null || limit > 100)
limit = 100;
var res = await _db.Execute(conn => conn.QueryAsync<SwitchesReturnNew>(
@"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/{systemRef}/fronters")]
public async Task<IActionResult> GetSystemFronters(string systemRef)
{
var system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
var ctx = ContextFor(system);
if (!system.FrontPrivacy.CanAccess(ctx))
throw Errors.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 FrontersReturnNew
{
var system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
Timestamp = sw.Timestamp,
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync(),
Uuid = sw.Uuid,
});
}
var ctx = this.ContextFor(system);
if (!system.FrontHistoryPrivacy.CanAccess(ctx))
throw Errors.UnauthorizedFrontHistory;
[HttpPost("systems/@me/switches")]
public async Task<IActionResult> SwitchCreate([FromBody] PostSwitchParams data)
{
if (data.Members.Distinct().Count() != data.Members.Count)
throw Errors.DuplicateMembersInList;
if (before == null)
before = SystemClock.Instance.GetCurrentInstant();
var system = await ResolveSystem("@me");
if (limit == null || limit > 100)
limit = 100;
if (data.Timestamp != null && await _repo.GetSwitches(system.Id).Select(x => x.Timestamp)
.ContainsAsync(data.Timestamp.Value))
throw Errors.SameSwitchTimestampError;
var res = await _db.Execute(conn => conn.QueryAsync<SwitchesReturnNew>(
@"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);
var members = new List<PKMember>();
foreach (var memberRef in data.Members)
{
var member = await ResolveMember(memberRef);
if (member == null)
// todo: which member
throw Errors.MemberNotFound;
if (member.System != system.Id)
throw Errors.NotOwnMemberErrorWithRef(memberRef);
members.Add(member);
}
[HttpGet("systems/{systemRef}/fronters")]
public async Task<IActionResult> GetSystemFronters(string systemRef)
// 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 system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
var latestSwitchMembers = _db.Execute(conn => _repo.GetSwitchMembers(conn, latestSwitch.Id));
var ctx = this.ContextFor(system);
if (!system.FrontPrivacy.CanAccess(ctx))
throw Errors.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 FrontersReturnNew
{
Timestamp = sw.Timestamp,
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync(),
Uuid = sw.Uuid,
});
// 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 Errors.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);
[HttpPost("systems/@me/switches")]
public async Task<IActionResult> SwitchCreate([FromBody] PostSwitchParams data)
return Ok(new FrontersReturnNew
{
if (data.Members.Distinct().Count() != data.Members.Count)
Uuid = newSwitch.Uuid,
Timestamp = data.Timestamp != null ? data.Timestamp.Value : newSwitch.Timestamp,
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)),
});
}
[HttpGet("systems/{systemRef}/switches/{switchRef}")]
public async Task<IActionResult> SwitchGet(string systemRef, string switchRef)
{
if (!Guid.TryParse(switchRef, out var switchId))
throw Errors.InvalidSwitchId;
var system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
var sw = await _repo.GetSwitchByUuid(switchId);
if (sw == null || system.Id != sw.System)
throw Errors.SwitchNotFoundPublic;
var ctx = ContextFor(system);
if (!system.FrontHistoryPrivacy.CanAccess(ctx))
throw Errors.SwitchNotFoundPublic;
var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
return Ok(new FrontersReturnNew
{
Uuid = sw.Uuid,
Timestamp = sw.Timestamp,
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync()
});
}
[HttpPatch("systems/@me/switches/{switchRef}")]
public async Task<IActionResult> SwitchPatch(string switchRef, [FromBody] JObject data)
{
// 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 Errors.InvalidSwitchId;
var valueStr = data.Value<string>("timestamp").NullIfEmpty();
if (valueStr == null)
throw new ModelParseError(new List<ValidationError> { new("timestamp", "Key 'timestamp' is required.") });
var value = Instant.FromDateTimeOffset(DateTime.Parse(valueStr).ToUniversalTime());
var system = await ResolveSystem("@me");
if (system == null)
throw Errors.SystemNotFound;
var sw = await _repo.GetSwitchByUuid(switchId);
if (sw == null || system.Id != sw.System)
throw Errors.SwitchNotFoundPublic;
if (await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(value))
throw Errors.SameSwitchTimestampError;
await _repo.MoveSwitch(sw.Id, value);
var members = await _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)).ToListAsync();
return Ok(new FrontersReturnNew
{
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<IActionResult> SwitchMemberPatch(string switchRef, [FromBody] JArray data)
{
if (!Guid.TryParse(switchRef, out var switchId))
if (data.Distinct().Count() != data.Count)
throw Errors.DuplicateMembersInList;
var system = await ResolveSystem("@me");
var system = await ResolveSystem("@me");
if (data.Timestamp != null && await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(data.Timestamp.Value))
throw Errors.SameSwitchTimestampError;
var sw = await _repo.GetSwitchByUuid(switchId);
if (sw == null)
throw Errors.SwitchNotFound;
var members = new List<PKMember>();
var members = new List<PKMember>();
foreach (var memberRef in data.Members)
{
var member = await ResolveMember(memberRef);
if (member == null)
// todo: which member
throw Errors.MemberNotFound;
if (member.System != system.Id)
throw Errors.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 Errors.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)),
});
}
[HttpGet("systems/{systemRef}/switches/{switchRef}")]
public async Task<IActionResult> SwitchGet(string systemRef, string switchRef)
foreach (var JmemberRef in data)
{
if (!Guid.TryParse(switchRef, out var switchId))
throw Errors.InvalidSwitchId;
var memberRef = JmemberRef.Value<string>();
var system = await ResolveSystem(systemRef);
if (system == null)
throw Errors.SystemNotFound;
var member = await ResolveMember(memberRef);
if (member == null)
// todo: which member
throw Errors.MemberNotFound;
if (member.System != system.Id)
throw Errors.NotOwnMemberErrorWithRef(memberRef);
var sw = await _repo.GetSwitchByUuid(switchId);
if (sw == null || system.Id != sw.System)
throw Errors.SwitchNotFoundPublic;
var ctx = this.ContextFor(system);
if (!system.FrontHistoryPrivacy.CanAccess(ctx))
throw Errors.SwitchNotFoundPublic;
var members = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id));
return Ok(new FrontersReturnNew
{
Uuid = sw.Uuid,
Timestamp = sw.Timestamp,
Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync()
});
members.Add(member);
}
[HttpPatch("systems/@me/switches/{switchRef}")]
public async Task<IActionResult> SwitchPatch(string switchRef, [FromBody] JObject data)
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 Errors.SameSwitchMembersError;
await _db.Execute(conn => _repo.EditSwitch(conn, sw.Id, members.Select(x => x.Id).ToList()));
return Ok(new FrontersReturnNew
{
// for now, don't need to make a PatchObject for this, since it's only one param
Uuid = sw.Uuid,
Timestamp = sw.Timestamp,
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2))
});
}
if (!Guid.TryParse(switchRef, out var switchId))
throw Errors.InvalidSwitchId;
[HttpDelete("systems/@me/switches/{switchRef}")]
public async Task<IActionResult> SwitchDelete(string switchRef)
{
if (!Guid.TryParse(switchRef, out var switchId))
throw Errors.InvalidSwitchId;
var valueStr = data.Value<string>("timestamp").NullIfEmpty();
if (valueStr == null)
throw new ModelParseError(new List<ValidationError>() { new ValidationError("timestamp", $"Key 'timestamp' is required.") });
var system = await ResolveSystem("@me");
var sw = await _repo.GetSwitchByUuid(switchId);
if (sw == null || system.Id != sw.System)
throw Errors.SwitchNotFoundPublic;
var value = Instant.FromDateTimeOffset(DateTime.Parse(valueStr).ToUniversalTime());
await _repo.DeleteSwitch(sw.Id);
var system = await ResolveSystem("@me");
if (system == null)
throw Errors.SystemNotFound;
var sw = await _repo.GetSwitchByUuid(switchId);
if (sw == null || system.Id != sw.System)
throw Errors.SwitchNotFoundPublic;
if (await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(value))
throw Errors.SameSwitchTimestampError;
await _repo.MoveSwitch(sw.Id, value);
var members = await _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)).ToListAsync();
return Ok(new FrontersReturnNew
{
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<IActionResult> SwitchMemberPatch(string switchRef, [FromBody] JArray data)
{
if (!Guid.TryParse(switchRef, out var switchId))
if (data.Distinct().Count() != data.Count)
throw Errors.DuplicateMembersInList;
var system = await ResolveSystem("@me");
var sw = await _repo.GetSwitchByUuid(switchId);
if (sw == null)
throw Errors.SwitchNotFound;
var members = new List<PKMember>();
foreach (var JmemberRef in data)
{
var memberRef = JmemberRef.Value<string>();
var member = await ResolveMember(memberRef);
if (member == null)
// todo: which member
throw Errors.MemberNotFound;
if (member.System != system.Id)
throw Errors.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 Errors.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<IActionResult> SwitchDelete(string switchRef)
{
if (!Guid.TryParse(switchRef, out var switchId))
throw Errors.InvalidSwitchId;
var system = await ResolveSystem("@me");
var sw = await _repo.GetSwitchByUuid(switchId);
if (sw == null || system.Id != sw.System)
throw Errors.SwitchNotFoundPublic;
await _repo.DeleteSwitch(sw.Id);
return NoContent();
}
return NoContent();
}
}

View File

@@ -1,41 +1,37 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using PluralKit.Core;
namespace PluralKit.API
namespace PluralKit.API;
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}/systems")]
public class SystemControllerV2: PKControllerBase
{
[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}/systems")]
public class SystemControllerV2: PKControllerBase
public SystemControllerV2(IServiceProvider svc) : base(svc) { }
[HttpGet("{systemRef}")]
public async Task<IActionResult> SystemGet(string systemRef)
{
public SystemControllerV2(IServiceProvider svc) : base(svc) { }
var system = await ResolveSystem(systemRef);
if (system == null) throw Errors.SystemNotFound;
return Ok(system.ToJson(ContextFor(system), APIVersion.V2));
}
[HttpGet("{systemRef}")]
public async Task<IActionResult> SystemGet(string systemRef)
{
var system = await ResolveSystem(systemRef);
if (system == null) throw Errors.SystemNotFound;
else return Ok(system.ToJson(this.ContextFor(system), v: APIVersion.V2));
}
[HttpPatch("@me")]
public async Task<IActionResult> DoSystemPatch([FromBody] JObject data)
{
var system = await ResolveSystem("@me");
var patch = SystemPatch.FromJSON(data, APIVersion.V2);
[HttpPatch("@me")]
public async Task<IActionResult> DoSystemPatch([FromBody] JObject data)
{
var system = await ResolveSystem("@me");
var patch = SystemPatch.FromJSON(data, APIVersion.V2);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
patch.AssertIsValid();
if (patch.Errors.Count > 0)
throw new ModelParseError(patch.Errors);
var newSystem = await _repo.UpdateSystem(system.Id, patch);
return Ok(newSystem.ToJson(LookupContext.ByOwner, v: APIVersion.V2));
}
var newSystem = await _repo.UpdateSystem(system.Id, patch);
return Ok(newSystem.ToJson(LookupContext.ByOwner, APIVersion.V2));
}
}