212 lines
7.3 KiB
C#
212 lines
7.3 KiB
C#
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);
|