Ajoute les amis et les invitations temps reel

This commit is contained in:
2026-04-15 23:08:48 +02:00
parent 9aae4cadc0
commit 8ea6ef8424
18 changed files with 3136 additions and 25 deletions

View File

@@ -0,0 +1,211 @@
namespace ChessCubing.Server.Social;
public sealed class PlayInviteCoordinator
{
private static readonly TimeSpan InviteLifetime = TimeSpan.FromMinutes(2);
private readonly object _sync = new();
private readonly Dictionary<string, PlayInviteState> _invitesById = new(StringComparer.Ordinal);
private readonly Dictionary<string, string> _inviteByParticipant = new(StringComparer.Ordinal);
public PlayInviteMessage CreateInvite(PlayInviteParticipant sender, PlayInviteParticipant recipient, string recipientColor)
{
var normalizedColor = NormalizeRecipientColor(recipientColor);
lock (_sync)
{
CleanupExpiredUnsafe();
if (_inviteByParticipant.ContainsKey(sender.Subject))
{
throw new SocialValidationException("Une invitation de partie est deja en cours pour ton compte.");
}
if (_inviteByParticipant.ContainsKey(recipient.Subject))
{
throw new SocialValidationException("Cet ami traite deja une autre invitation de partie.");
}
var nowUtc = DateTime.UtcNow;
var state = new PlayInviteState(
Guid.NewGuid().ToString("N"),
sender,
recipient,
normalizedColor,
nowUtc,
nowUtc.Add(InviteLifetime));
_invitesById[state.InviteId] = state;
_inviteByParticipant[sender.Subject] = state.InviteId;
_inviteByParticipant[recipient.Subject] = state.InviteId;
return MapInvite(state);
}
}
public PlayInviteCloseResult CancelInvite(string inviteId, string senderSubject)
{
lock (_sync)
{
var state = GetActiveInviteUnsafe(inviteId);
if (!string.Equals(state.Sender.Subject, senderSubject, StringComparison.Ordinal))
{
throw new SocialValidationException("Seul l'expediteur peut annuler cette invitation.");
}
RemoveInviteUnsafe(state);
return new PlayInviteCloseResult(
state.Sender.Subject,
state.Recipient.Subject,
new PlayInviteClosedMessage
{
InviteId = state.InviteId,
Reason = "cancelled",
Message = $"{state.Sender.DisplayName} a annule l'invitation de partie.",
});
}
}
public PlayInviteCloseResult DeclineInvite(string inviteId, string recipientSubject)
{
lock (_sync)
{
var state = GetActiveInviteUnsafe(inviteId);
if (!string.Equals(state.Recipient.Subject, recipientSubject, StringComparison.Ordinal))
{
throw new SocialValidationException("Seul le destinataire peut refuser cette invitation.");
}
RemoveInviteUnsafe(state);
return new PlayInviteCloseResult(
state.Sender.Subject,
state.Recipient.Subject,
new PlayInviteClosedMessage
{
InviteId = state.InviteId,
Reason = "declined",
Message = $"{state.Recipient.DisplayName} a refuse la partie.",
});
}
}
public PlayInviteAcceptResult AcceptInvite(string inviteId, string recipientSubject)
{
lock (_sync)
{
var state = GetActiveInviteUnsafe(inviteId);
if (!string.Equals(state.Recipient.Subject, recipientSubject, StringComparison.Ordinal))
{
throw new SocialValidationException("Seul le destinataire peut accepter cette invitation.");
}
RemoveInviteUnsafe(state);
var white = string.Equals(state.RecipientColor, "white", StringComparison.Ordinal)
? state.Recipient
: state.Sender;
var black = string.Equals(state.RecipientColor, "white", StringComparison.Ordinal)
? state.Sender
: state.Recipient;
var session = new PlaySessionResponse
{
SessionId = Guid.NewGuid().ToString("N"),
WhiteSubject = white.Subject,
WhiteName = white.DisplayName,
BlackSubject = black.Subject,
BlackName = black.DisplayName,
InitiatorSubject = state.Sender.Subject,
RecipientSubject = state.Recipient.Subject,
ConfirmedUtc = DateTime.UtcNow,
};
return new PlayInviteAcceptResult(state.Sender.Subject, state.Recipient.Subject, session);
}
}
private static string NormalizeRecipientColor(string recipientColor)
{
var normalized = recipientColor.Trim().ToLowerInvariant();
return normalized switch
{
"white" => "white",
"black" => "black",
_ => throw new SocialValidationException("La couleur demandee pour l'ami doit etre blanc ou noir."),
};
}
private static PlayInviteMessage MapInvite(PlayInviteState state)
=> new()
{
InviteId = state.InviteId,
SenderSubject = state.Sender.Subject,
SenderUsername = state.Sender.Username,
SenderDisplayName = state.Sender.DisplayName,
RecipientSubject = state.Recipient.Subject,
RecipientUsername = state.Recipient.Username,
RecipientDisplayName = state.Recipient.DisplayName,
RecipientColor = state.RecipientColor,
CreatedUtc = state.CreatedUtc,
ExpiresUtc = state.ExpiresUtc,
};
private PlayInviteState GetActiveInviteUnsafe(string inviteId)
{
CleanupExpiredUnsafe();
if (!_invitesById.TryGetValue(inviteId, out var state))
{
throw new SocialValidationException("Cette invitation de partie n'est plus disponible.");
}
return state;
}
private void CleanupExpiredUnsafe()
{
var nowUtc = DateTime.UtcNow;
var expiredInviteIds = _invitesById.Values
.Where(state => state.ExpiresUtc <= nowUtc)
.Select(state => state.InviteId)
.ToArray();
foreach (var expiredInviteId in expiredInviteIds)
{
if (_invitesById.TryGetValue(expiredInviteId, out var state))
{
RemoveInviteUnsafe(state);
}
}
}
private void RemoveInviteUnsafe(PlayInviteState state)
{
_invitesById.Remove(state.InviteId);
_inviteByParticipant.Remove(state.Sender.Subject);
_inviteByParticipant.Remove(state.Recipient.Subject);
}
private sealed record PlayInviteState(
string InviteId,
PlayInviteParticipant Sender,
PlayInviteParticipant Recipient,
string RecipientColor,
DateTime CreatedUtc,
DateTime ExpiresUtc);
}
public readonly record struct PlayInviteParticipant(
string Subject,
string Username,
string DisplayName);
public sealed record PlayInviteCloseResult(
string SenderSubject,
string RecipientSubject,
PlayInviteClosedMessage ClosedMessage);
public sealed record PlayInviteAcceptResult(
string SenderSubject,
string RecipientSubject,
PlaySessionResponse Session);