feat(calls): add consent-based signaling to existing ChatHub

Wire shared PostIt call signaling, account-derived relay policy and temporary TURN credentials. Keep calls disabled pending native Android/Linux media integration. Document experimental Android crash and laboratory removal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-10-26 01:00:19 +00:00
commit 5e48831d6d
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
20 changed files with 1368 additions and 16 deletions

View file

@ -4,10 +4,15 @@ using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.SignalR;
using Yavsc.Abstract.Identity;
using Yavsc.Abstract.Chat.Calls;
using Yavsc.Api.Test.Fixtures;
using Yavsc.Models;
using Yavsc.Tests.Shared;
using Yavsc.Services;
using Yavsc.Settings;
namespace Yavsc.Api.Test;
@ -146,4 +151,32 @@ public sealed class AccountCallPreferenceTests(ApiWebServerFixture fixture) : IC
Assert.False(profile.HideIpAddressInCalls);
Assert.NotEqual("Must not be saved", profile.FullName);
}
[Fact]
public async Task Activating_privacy_ends_an_existing_call_before_returning_success()
{
var ct = TestContext.Current.CancellationToken;
var id = await CreateUserAsync();
var other = await CreateUserAsync();
var options = fixture.Services.GetRequiredService<IOptions<AudioVideoCallOptions>>().Value;
var calls = fixture.Services.GetRequiredService<CallSignalingService>();
var delivery = fixture.Services.GetRequiredService<RecordingMessageDelivery>();
options.Enabled = true;
try
{
var call = await calls.InviteAsync("General", id, other, "preference-caller", ["preference-callee"], false);
await calls.AcceptAsync(call.Id, other, "preference-callee", false);
using var client = Client(id);
using var response = await client.PutAsJsonAsync(Endpoint, new { hideIpAddressInCalls = true }, ct);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains(delivery.Pushes, p => p.Method == CallHubMethods.Ended &&
p.Arguments[0] is CallEnded ended && ended.CallId == call.Id && ended.Reason == "profile-changed");
await Assert.ThrowsAsync<HubException>(() => calls.GetSessionAsync(call.Id, id, "preference-caller"));
}
finally
{
await calls.EndForUserAsync(id, "test-cleanup");
options.Enabled = false;
}
}
}

View file

@ -84,6 +84,9 @@ public sealed class ApiWebServerFixture : WebHostFixture
builder.Services.AddTransient<ICalendarManager, CalendarManager>();
builder.Services.AddSingleton<IHubContext<ChatHub>>(sp => sp.GetRequiredService<RecordingMessageDelivery>());
builder.Services.AddSingleton<IConnexionManager, HubConnectionManager>();
builder.Services.Configure<Yavsc.Settings.AudioVideoCallOptions>(_ => { });
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<CallSignalingService>();
builder.Services.AddTransient<IYavscMessageSender, YavscMessageSender>();
builder.Services.AddAuthorization(options =>
{

View file

@ -16,8 +16,8 @@ public sealed class RecordingMessageDelivery : ITrueEmailSender, IHubContext<Cha
IGroupManager IHubContext<ChatHub>.Groups => throw new NotSupportedException();
public IClientProxy All => throw new NotSupportedException();
public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => throw new NotSupportedException();
public IClientProxy Client(string connectionId) => new RecordingClient(this, connectionId);
public IClientProxy Clients(IReadOnlyList<string> connectionIds) => throw new NotSupportedException();
public IClientProxy Client(string connectionId) => new RecordingClient(this, [connectionId]);
public IClientProxy Clients(IReadOnlyList<string> connectionIds) => new RecordingClient(this, connectionIds);
public IClientProxy Group(string groupName) => throw new NotSupportedException();
public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => throw new NotSupportedException();
public IClientProxy Groups(IReadOnlyList<string> groupNames) => throw new NotSupportedException();
@ -36,11 +36,12 @@ public sealed class RecordingMessageDelivery : ITrueEmailSender, IHubContext<Cha
Pushes.Clear();
}
private sealed class RecordingClient(RecordingMessageDelivery delivery, string connectionId) : IClientProxy
private sealed class RecordingClient(RecordingMessageDelivery delivery, IReadOnlyList<string> connectionIds) : IClientProxy
{
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
{
delivery.Pushes.Enqueue(new Push(connectionId, method, args));
foreach (var connectionId in connectionIds)
delivery.Pushes.Enqueue(new Push(connectionId, method, args));
return Task.CompletedTask;
}
}