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 { [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 GetSystemSwitches(string systemRef, [FromQuery(Name = "before")] Instant? before, [FromQuery(Name = "limit")] int? limit) { 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/{systemRef}/fronters")] public async Task GetSystemFronters(string systemRef) { 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 FrontersReturnNew { Timestamp = sw.Timestamp, Members = await members.Select(m => m.ToJson(ctx, v: APIVersion.V2)).ToListAsync(), Uuid = sw.Uuid, }); } [HttpPost("systems/@me/switches")] public async Task SwitchCreate([FromBody] PostSwitchParams data) { if (data.Members.Distinct().Count() != data.Members.Count) throw APIErrors.DuplicateMembersInList; var system = await ResolveSystem("@me"); if (data.Timestamp != null && await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(data.Timestamp.Value)) throw APIErrors.SameSwitchTimestampError; var members = new List(); foreach (var memberRef in data.Members) { var member = await ResolveMember(memberRef); if (member == null) // todo: which member throw APIErrors.MemberNotFound; if (member.System != system.Id) throw APIErrors.NotOwnMemberErrorWithRef(memberRef); members.Add(member); } // We get the current switch, if it exists var latestSwitch = await _repo.GetLatestSwitch(system.Id); if (latestSwitch != null && (data.Timestamp == null || data.Timestamp > latestSwitch.Timestamp)) { var latestSwitchMembers = _db.Execute(conn => _repo.GetSwitchMembers(conn, latestSwitch.Id)); // Bail if this switch is identical to the latest one if (await latestSwitchMembers.Select(m => m.Hid).SequenceEqualAsync(members.Select(m => m.Hid).ToAsyncEnumerable())) throw APIErrors.SameSwitchMembersError; } var newSwitch = await _db.Execute(conn => _repo.AddSwitch(conn, system.Id, members.Select(m => m.Id).ToList())); if (data.Timestamp != null) await _repo.MoveSwitch(newSwitch.Id, data.Timestamp.Value); return Ok(new FrontersReturnNew { Uuid = newSwitch.Uuid, Timestamp = data.Timestamp != null ? data.Timestamp.Value : newSwitch.Timestamp, Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)), }); } [HttpGet("systems/{systemRef}/switches/{switchRef}")] public async Task SwitchGet(string systemRef, string switchRef) { if (!Guid.TryParse(switchRef, out var switchId)) throw APIErrors.InvalidSwitchId; var system = await ResolveSystem(systemRef); if (system == null) throw APIErrors.SystemNotFound; var sw = await _repo.GetSwitchByUuid(switchId); if (sw == null || system.Id != sw.System) throw APIErrors.SwitchNotFoundPublic; var ctx = this.ContextFor(system); if (!system.FrontHistoryPrivacy.CanAccess(ctx)) throw APIErrors.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 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 APIErrors.InvalidSwitchId; var value = data.Value("timestamp"); if (value == null) // todo throw APIErrors.GenericBadRequest; var system = await ResolveSystem("@me"); if (system == null) throw APIErrors.SystemNotFound; var sw = await _repo.GetSwitchByUuid(switchId); if (sw == null || system.Id != sw.System) throw APIErrors.SwitchNotFoundPublic; if (await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(value)) throw APIErrors.SameSwitchTimestampError; await _repo.MoveSwitch(sw.Id, value); var members = await _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)).ToListAsync(); return Ok(new FrontersReturnNew { Uuid = sw.Uuid, Timestamp = sw.Timestamp, Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)), }); } [HttpPatch("systems/@me/switches/{switchRef}/members")] public async Task SwitchMemberPatch(string switchRef, [FromBody] JArray data) { if (!Guid.TryParse(switchRef, out var switchId)) if (data.Distinct().Count() != data.Count) throw APIErrors.DuplicateMembersInList; var system = await ResolveSystem("@me"); var sw = await _repo.GetSwitchByUuid(switchId); if (sw == null) throw APIErrors.SwitchNotFound; var members = new List(); foreach (var JmemberRef in data) { var memberRef = JmemberRef.Value(); var member = await ResolveMember(memberRef); if (member == null) // todo: which member throw APIErrors.MemberNotFound; if (member.System != system.Id) throw APIErrors.NotOwnMemberErrorWithRef(memberRef); members.Add(member); } var latestSwitchMembers = _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)); if (await latestSwitchMembers.Select(m => m.Hid).SequenceEqualAsync(members.Select(m => m.Hid).ToAsyncEnumerable())) throw APIErrors.SameSwitchMembersError; await _db.Execute(conn => _repo.EditSwitch(conn, sw.Id, members.Select(x => x.Id).ToList())); return Ok(new FrontersReturnNew { Uuid = sw.Uuid, Timestamp = sw.Timestamp, Members = members.Select(x => x.ToJson(LookupContext.ByOwner, v: APIVersion.V2)), }); } [HttpDelete("systems/@me/switches/{switchRef}")] public async Task SwitchDelete(string switchRef) { if (!Guid.TryParse(switchRef, out var switchId)) throw APIErrors.InvalidSwitchId; var system = await ResolveSystem("@me"); var sw = await _repo.GetSwitchByUuid(switchId); if (sw == null || system.Id != sw.System) throw APIErrors.SwitchNotFoundPublic; await _repo.DeleteSwitch(sw.Id); return NoContent(); } } }