feat(api)!: fold devices into users and report member presence
Contract 7.0.0 (breaking). Devices become an attribute of the account;
members are managed per user, and only unauthenticated guests keep a
per-terminal registration.
- rename seats -> max_members, seats_used -> members_used,
seats_full -> members_full, SeatJoined -> JoinResult, the `seats`
stream control message -> `members`, and POST /channels/{id}/seats ->
POST /channels/{id}/join (stored channel settings are migrated on start)
- drop per-terminal encrypted device labels and the seat list/release
endpoints; add GET/DELETE /channels/{id}/guests for guest terminals
- authenticate account members by account session only: the terminal key
is optional for logged-in accounts, no seat row is written for them,
and gated-download grants are counted per user (per terminal for guests)
- GET /channels/{id}/members now returns online, devices and
online_devices per account; presence is tracked in memory from active
WebSocket/SSE subscriptions and is not pushed over the stream
- owner invitation list no longer returns accounts that already joined
This commit is contained in:
1 parent
0fd2a269d9
commit
d946fc5e8a
35 files changed
+517
-490
No files matched your search
@@ -116,13 +116,13 @@ public class StreamingTests(ServerFixture s) : IClassFixture<ServerFixture>
|
||||
public async Task WebSocketNotifiesSeatChangesAndClosesWhenRevoked()
|
||||
{
|
||||
var ch = await s.CreateAsync();
|
||||
var joined = await s.JoinAsync(ch, ch.Member);
|
||||
string memberHash = (await ReadJsonAsync(joined)).GetProperty("endpoint_hash").GetString()!;
|
||||
await s.JoinAsync(ch, ch.Member);
|
||||
string memberHash = B64(System.Security.Cryptography.SHA256.HashData(ch.Member.EndpointSecret));
|
||||
|
||||
using var member = await ConnectWs(ch, ch.Member);
|
||||
|
||||
// 所有者释放席位:订阅者先收到 seats 控制消息,随后被以 not_joined 关闭
|
||||
using var release = await s.Http.SendAsync(s.Req(HttpMethod.Delete, $"/api/v1/channels/{ch.Id}/seats/{memberHash}", ch.Owner));
|
||||
using var release = await s.Http.SendAsync(s.Req(HttpMethod.Delete, $"/api/v1/channels/{ch.Id}/guests/{memberHash}", ch.Owner));
|
||||
Assert.Equal(HttpStatusCode.NoContent, release.StatusCode);
|
||||
|
||||
bool closed = false;
|
||||
@@ -137,7 +137,7 @@ public class StreamingTests(ServerFixture s) : IClassFixture<ServerFixture>
|
||||
}
|
||||
else if (type == WebSocketMessageType.Text)
|
||||
{
|
||||
Assert.Equal("seats", JsonDocument.Parse(data).RootElement.GetProperty("t").GetString());
|
||||
Assert.Equal("members", JsonDocument.Parse(data).RootElement.GetProperty("t").GetString());
|
||||
}
|
||||
}
|
||||
Assert.True(closed);
|
||||
@@ -240,4 +240,39 @@ public class StreamingTests(ServerFixture s) : IClassFixture<ServerFixture>
|
||||
var bad = new Who("owner", new byte[32], ch.OwnerSecret);
|
||||
await AssertErrorAsync(await s.Http.SendAsync(s.Req(HttpMethod.Get, $"/api/v1/channels/{ch.Id}/events", bad)), 401, "unauthorized");
|
||||
}
|
||||
[Fact]
|
||||
public async Task MembersReportAccountPresenceFromActiveSubscriptions()
|
||||
{
|
||||
var ch = await s.CreateAsync();
|
||||
async Task<(bool Online, int Devices, int OnlineDevices)> Owner()
|
||||
{
|
||||
using var response = await s.Http.SendAsync(s.Req(HttpMethod.Get, $"/api/v1/channels/{ch.Id}/members", ch.Owner));
|
||||
response.EnsureSuccessStatusCode();
|
||||
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
var owner = doc.RootElement.EnumerateArray().First(m => m.GetProperty("role").GetString() == "owner");
|
||||
return (owner.GetProperty("online").GetBoolean(), owner.GetProperty("devices").GetInt32(), owner.GetProperty("online_devices").GetInt32());
|
||||
}
|
||||
async Task<(bool Online, int Devices, int OnlineDevices)> Eventually(Func<(bool Online, int Devices, int OnlineDevices), bool> done)
|
||||
{
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var state = await Owner();
|
||||
if (done(state)) return state;
|
||||
await Task.Delay(100);
|
||||
}
|
||||
return await Owner();
|
||||
}
|
||||
|
||||
Assert.False((await Owner()).Online);
|
||||
using var ws = await ConnectWs(ch, ch.Owner);
|
||||
var online = await Eventually(state => state.Online);
|
||||
Assert.True(online.Online);
|
||||
Assert.Equal(1, online.OnlineDevices);
|
||||
Assert.True(online.Devices >= 1);
|
||||
|
||||
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None);
|
||||
var offline = await Eventually(state => !state.Online);
|
||||
Assert.False(offline.Online);
|
||||
Assert.Equal(0, offline.OnlineDevices);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user