Files
chesscubing/ChessCubing.Server/Social/SocialHub.cs

219 lines
8.4 KiB
C#

using ChessCubing.Server.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace ChessCubing.Server.Social;
[Authorize]
public sealed class SocialHub(
ConnectedUserTracker tracker,
MySqlSocialStore socialStore,
MySqlUserProfileStore profileStore,
PlayInviteCoordinator playInviteCoordinator,
CollaborativeMatchCoordinator collaborativeMatchCoordinator) : Hub
{
private readonly ConnectedUserTracker _tracker = tracker;
private readonly MySqlSocialStore _socialStore = socialStore;
private readonly MySqlUserProfileStore _profileStore = profileStore;
private readonly PlayInviteCoordinator _playInviteCoordinator = playInviteCoordinator;
private readonly CollaborativeMatchCoordinator _collaborativeMatchCoordinator = collaborativeMatchCoordinator;
public override async Task OnConnectedAsync()
{
var siteUser = RequireCurrentUser();
var becameOnline = _tracker.TrackConnection(Context.ConnectionId, siteUser.Subject);
await SendPresenceSnapshotAsync(siteUser.Subject, Context.ConnectionAborted);
if (becameOnline)
{
var relevantSubjects = await _socialStore.GetRelevantPresenceSubjectsAsync(siteUser.Subject, Context.ConnectionAborted);
await NotifyPresenceChangedAsync(relevantSubjects, siteUser.Subject, isOnline: true);
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var (subject, becameOffline) = _tracker.RemoveConnection(Context.ConnectionId);
if (becameOffline && !string.IsNullOrWhiteSpace(subject))
{
var relevantSubjects = await _socialStore.GetRelevantPresenceSubjectsAsync(subject, CancellationToken.None);
await NotifyPresenceChangedAsync(relevantSubjects, subject, isOnline: false);
}
await base.OnDisconnectedAsync(exception);
}
public async Task RequestPresenceSnapshot()
{
var siteUser = RequireCurrentUser();
await SendPresenceSnapshotAsync(siteUser.Subject, Context.ConnectionAborted);
}
public async Task<PlayInviteMessage> SendPlayInvite(string recipientSubject, string recipientColor)
{
var sender = RequireCurrentUser();
var normalizedRecipientSubject = recipientSubject.Trim();
if (!await _socialStore.AreFriendsAsync(sender.Subject, normalizedRecipientSubject, Context.ConnectionAborted))
{
throw new HubException("Seuls tes amis peuvent recevoir une invitation de partie.");
}
if (!_tracker.IsOnline(normalizedRecipientSubject))
{
throw new HubException("Cet ami n'est plus connecte pour le moment.");
}
var senderProfile = await _profileStore.GetOrCreateAsync(sender, Context.ConnectionAborted);
var recipientProfile = await _profileStore.FindBySubjectAsync(normalizedRecipientSubject, Context.ConnectionAborted);
if (recipientProfile is null)
{
throw new HubException("Le profil de cet ami est introuvable.");
}
try
{
var message = _playInviteCoordinator.CreateInvite(
new PlayInviteParticipant(sender.Subject, sender.Username, senderProfile.DisplayName),
new PlayInviteParticipant(recipientProfile.Subject, recipientProfile.Username, recipientProfile.DisplayName),
recipientColor);
await Clients.Users([sender.Subject, recipientProfile.Subject])
.SendAsync("PlayInviteUpdated", message, Context.ConnectionAborted);
return message;
}
catch (SocialValidationException exception)
{
throw new HubException(exception.Message);
}
}
public async Task RespondToPlayInvite(string inviteId, bool accept)
{
var recipient = RequireCurrentUser();
try
{
if (accept)
{
var accepted = _playInviteCoordinator.AcceptInvite(inviteId, recipient.Subject);
_collaborativeMatchCoordinator.RegisterSession(accepted.Session);
await Clients.Users([accepted.SenderSubject, accepted.RecipientSubject])
.SendAsync("PlayInviteAccepted", accepted.Session, Context.ConnectionAborted);
return;
}
var declined = _playInviteCoordinator.DeclineInvite(inviteId, recipient.Subject);
await Clients.Users([declined.SenderSubject, declined.RecipientSubject])
.SendAsync("PlayInviteClosed", declined.ClosedMessage, Context.ConnectionAborted);
}
catch (SocialValidationException exception)
{
throw new HubException(exception.Message);
}
}
public async Task CancelPlayInvite(string inviteId)
{
var sender = RequireCurrentUser();
try
{
var cancelled = _playInviteCoordinator.CancelInvite(inviteId, sender.Subject);
await Clients.Users([cancelled.SenderSubject, cancelled.RecipientSubject])
.SendAsync("PlayInviteClosed", cancelled.ClosedMessage, Context.ConnectionAborted);
}
catch (SocialValidationException exception)
{
throw new HubException(exception.Message);
}
}
public async Task<CollaborativeMatchStateMessage?> JoinPlaySession(string sessionId)
{
var siteUser = RequireCurrentUser();
var normalizedSessionId = sessionId.Trim();
try
{
var latestState = _collaborativeMatchCoordinator.GetLatestState(normalizedSessionId, siteUser.Subject);
await Groups.AddToGroupAsync(Context.ConnectionId, BuildSessionGroupName(normalizedSessionId), Context.ConnectionAborted);
return latestState;
}
catch (SocialValidationException exception)
{
throw new HubException(exception.Message);
}
}
public async Task LeavePlaySession(string sessionId)
{
if (string.IsNullOrWhiteSpace(sessionId))
{
return;
}
await Groups.RemoveFromGroupAsync(Context.ConnectionId, BuildSessionGroupName(sessionId.Trim()), Context.ConnectionAborted);
}
public async Task PublishMatchState(string sessionId, string? matchJson, string route)
{
var siteUser = RequireCurrentUser();
var normalizedSessionId = sessionId.Trim();
try
{
var message = _collaborativeMatchCoordinator.PublishState(normalizedSessionId, siteUser.Subject, matchJson, route);
await Clients.GroupExcept(BuildSessionGroupName(normalizedSessionId), [Context.ConnectionId])
.SendAsync("CollaborativeMatchStateUpdated", message, Context.ConnectionAborted);
}
catch (SocialValidationException exception)
{
throw new HubException(exception.Message);
}
}
private AuthenticatedSiteUser RequireCurrentUser()
=> AuthenticatedSiteUserFactory.FromClaimsPrincipal(Context.User ?? new System.Security.Claims.ClaimsPrincipal())
?? throw new HubException("La session utilisateur est incomplete.");
private async Task SendPresenceSnapshotAsync(string subject, CancellationToken cancellationToken)
{
var relevantSubjects = await _socialStore.GetRelevantPresenceSubjectsAsync(subject, cancellationToken);
var onlineSubjects = relevantSubjects
.Where(_tracker.IsOnline)
.Distinct(StringComparer.Ordinal)
.ToArray();
await Clients.Caller.SendAsync(
"PresenceSnapshot",
new PresenceSnapshotMessage { OnlineSubjects = onlineSubjects },
cancellationToken);
}
private Task NotifyPresenceChangedAsync(IReadOnlyList<string> subjects, string changedSubject, bool isOnline)
{
var distinctSubjects = subjects
.Where(subject => !string.IsNullOrWhiteSpace(subject))
.Distinct(StringComparer.Ordinal)
.ToArray();
return distinctSubjects.Length == 0
? Task.CompletedTask
: Clients.Users(distinctSubjects).SendAsync(
"PresenceChanged",
new PresenceChangedMessage
{
Subject = changedSubject,
IsOnline = isOnline,
});
}
private static string BuildSessionGroupName(string sessionId)
=> $"play-session:{sessionId}";
}