mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
e33145307f
Most pushes were still shaped for the 2020 watch, so the 2023 client parsed them as empty or dropped them without any error. - RelationshipChanged carries the relationship row, to both players, on friend request / accept / remove / add and on favorite / mute / ignore. Friend requests and accepts also land in the notifications inbox. - ModerationKick uses the client's flat moderation block, with the reason in VoteKickReason (the only field the dialog shows). - Bans, account deletion, password resets and second-session takeover are a Logout with a numeric reason instead of a kick to the dorm; the old string reason failed the client's parse. - SubscribeToPlayers is honoured: each connection joins a presence group per subscribed player, and presence (only presence) reaches subscribers as well as friends. The hub allows 180 s of client silence, since the client reconnects at most 3 times per login. - Admin broadcast sends its text as a coach message and can start the client's own maintenance countdown; ServerMaintenance only reads StartsInMinutes. - ConsumableMapping, StorefrontBalance, gift, player event, level-up, RoomUpdate, profile-image and PlayerLeftChat pushes carry the client's DTOs. "Join rejected" is the client's RequestGameInvite message. Contract in docs/recroom-2023-notifications.md; push frames are checked by NotificationContractTests through a capturing hub context.
79 lines
4.0 KiB
C#
79 lines
4.0 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Text.Json;
|
|
using DorkNet.Server.Hubs;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
|
|
namespace DorkNet.Server.Tests;
|
|
|
|
/// <summary>
|
|
/// Stand-in for the notify hub that records every push instead of sending it,
|
|
/// so tests can assert on the exact frames the game client would receive.
|
|
/// A frame is the hub method plus its single string argument, parsed back into
|
|
/// the <c>{Id, Msg}</c> envelope.
|
|
/// </summary>
|
|
public sealed class CapturingHubContext : IHubContext<NotifyHub>
|
|
{
|
|
private readonly ConcurrentQueue<SentFrame> _sent = new();
|
|
|
|
public IHubClients Clients => new CapturingClients(this);
|
|
public IGroupManager Groups { get; } = new NoopGroups();
|
|
|
|
public IReadOnlyList<SentFrame> Sent => _sent.ToArray();
|
|
|
|
/// <summary>Frames sent to a target such as <c>group:player:12</c>, in
|
|
/// order, optionally filtered by envelope <c>Id</c>.</summary>
|
|
public IReadOnlyList<SentFrame> To(string target, string? id = null) =>
|
|
_sent.Where(f => f.Target == target && (id is null || f.Id == id)).ToList();
|
|
|
|
public static string PlayerGroup(long playerId) => $"group:{NotifyHub.GroupForPlayer(playerId)}";
|
|
|
|
internal void Record(string target, IReadOnlyList<string>? excluded, string method, object?[] args) =>
|
|
_sent.Enqueue(new SentFrame(target, excluded ?? Array.Empty<string>(), method,
|
|
args.Length == 1 && args[0] is string s ? s : JsonSerializer.Serialize(args)));
|
|
|
|
public sealed record SentFrame(string Target, IReadOnlyList<string> Excluded, string Method, string Payload)
|
|
{
|
|
public JsonElement Envelope => JsonDocument.Parse(Payload).RootElement;
|
|
public string? Id => Envelope.TryGetProperty("Id", out var id) ? id.GetString() : null;
|
|
public JsonElement Msg => Envelope.GetProperty("Msg");
|
|
}
|
|
|
|
private sealed class CapturingClients(CapturingHubContext owner) : IHubClients
|
|
{
|
|
private IClientProxy Proxy(string target, IReadOnlyList<string>? excluded = null) =>
|
|
new CapturingProxy(owner, target, excluded);
|
|
|
|
public IClientProxy All => Proxy("all");
|
|
public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => Proxy("all", excludedConnectionIds);
|
|
public IClientProxy Client(string connectionId) => Proxy($"conn:{connectionId}");
|
|
public IClientProxy Clients(IReadOnlyList<string> connectionIds) => Proxy($"conns:{string.Join(",", connectionIds)}");
|
|
public IClientProxy Group(string groupName) => Proxy($"group:{groupName}");
|
|
public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) =>
|
|
Proxy($"group:{groupName}", excludedConnectionIds);
|
|
public IClientProxy Groups(IReadOnlyList<string> groupNames) => new FanOutProxy(groupNames.Select(g => Proxy($"group:{g}")).ToList());
|
|
public IClientProxy User(string userId) => Proxy($"user:{userId}");
|
|
public IClientProxy Users(IReadOnlyList<string> userIds) => Proxy($"users:{string.Join(",", userIds)}");
|
|
}
|
|
|
|
private sealed class CapturingProxy(CapturingHubContext owner, string target, IReadOnlyList<string>? excluded) : IClientProxy
|
|
{
|
|
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
|
|
{
|
|
owner.Record(target, excluded, method, args);
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
private sealed class FanOutProxy(IReadOnlyList<IClientProxy> proxies) : IClientProxy
|
|
{
|
|
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) =>
|
|
Task.WhenAll(proxies.Select(p => p.SendCoreAsync(method, args, cancellationToken)));
|
|
}
|
|
|
|
private sealed class NoopGroups : IGroupManager
|
|
{
|
|
public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
|
public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
|
}
|
|
}
|