Ajoute les amis et les invitations temps reel
This commit is contained in:
211
ChessCubing.Server/Social/PlayInviteCoordinator.cs
Normal file
211
ChessCubing.Server/Social/PlayInviteCoordinator.cs
Normal 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);
|
||||
Reference in New Issue
Block a user