788 lines
30 KiB
C#
788 lines
30 KiB
C#
using ChessCubing.Server.Data;
|
|
using Microsoft.Extensions.Options;
|
|
using MySqlConnector;
|
|
|
|
namespace ChessCubing.Server.Social;
|
|
|
|
public sealed class MySqlSocialStore(
|
|
IOptions<SiteDataOptions> options,
|
|
ILogger<MySqlSocialStore> logger)
|
|
{
|
|
private const string CreateFriendshipsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS social_friendships (
|
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
|
subject_low VARCHAR(190) NOT NULL,
|
|
subject_high VARCHAR(190) NOT NULL,
|
|
created_utc DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY uq_social_friendships_pair (subject_low, subject_high),
|
|
KEY ix_social_friendships_low (subject_low),
|
|
KEY ix_social_friendships_high (subject_high)
|
|
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateInvitationsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS social_friend_invitations (
|
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
|
sender_subject VARCHAR(190) NOT NULL,
|
|
recipient_subject VARCHAR(190) NOT NULL,
|
|
created_utc DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY uq_social_friend_invitations_pair (sender_subject, recipient_subject),
|
|
KEY ix_social_friend_invitations_sender (sender_subject),
|
|
KEY ix_social_friend_invitations_recipient (recipient_subject)
|
|
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string SelectOverviewFriendsSql = """
|
|
SELECT
|
|
u.subject,
|
|
u.username,
|
|
u.display_name,
|
|
u.email,
|
|
u.club,
|
|
u.city
|
|
FROM social_friendships f
|
|
INNER JOIN site_users u
|
|
ON u.subject = CASE
|
|
WHEN f.subject_low = @subject THEN f.subject_high
|
|
ELSE f.subject_low
|
|
END
|
|
WHERE f.subject_low = @subject OR f.subject_high = @subject
|
|
ORDER BY u.display_name ASC, u.username ASC;
|
|
""";
|
|
|
|
private const string SelectReceivedInvitationsSql = """
|
|
SELECT
|
|
i.id,
|
|
u.subject,
|
|
u.username,
|
|
u.display_name,
|
|
u.email,
|
|
i.created_utc
|
|
FROM social_friend_invitations i
|
|
INNER JOIN site_users u
|
|
ON u.subject = i.sender_subject
|
|
WHERE i.recipient_subject = @subject
|
|
ORDER BY i.created_utc DESC, u.display_name ASC, u.username ASC;
|
|
""";
|
|
|
|
private const string SelectSentInvitationsSql = """
|
|
SELECT
|
|
i.id,
|
|
u.subject,
|
|
u.username,
|
|
u.display_name,
|
|
u.email,
|
|
i.created_utc
|
|
FROM social_friend_invitations i
|
|
INNER JOIN site_users u
|
|
ON u.subject = i.recipient_subject
|
|
WHERE i.sender_subject = @subject
|
|
ORDER BY i.created_utc DESC, u.display_name ASC, u.username ASC;
|
|
""";
|
|
|
|
private const string SearchUsersTemplateSql = """
|
|
SELECT
|
|
u.subject,
|
|
u.username,
|
|
u.display_name,
|
|
u.email,
|
|
u.club,
|
|
u.city,
|
|
EXISTS(
|
|
SELECT 1
|
|
FROM social_friendships f
|
|
WHERE (f.subject_low = @subject AND f.subject_high = u.subject)
|
|
OR (f.subject_high = @subject AND f.subject_low = u.subject)
|
|
) AS is_friend,
|
|
EXISTS(
|
|
SELECT 1
|
|
FROM social_friend_invitations i
|
|
WHERE i.sender_subject = @subject
|
|
AND i.recipient_subject = u.subject
|
|
) AS has_sent_invitation,
|
|
EXISTS(
|
|
SELECT 1
|
|
FROM social_friend_invitations i
|
|
WHERE i.sender_subject = u.subject
|
|
AND i.recipient_subject = @subject
|
|
) AS has_received_invitation
|
|
FROM site_users u
|
|
WHERE u.subject <> @subject
|
|
AND (
|
|
u.username LIKE @pattern
|
|
OR u.display_name LIKE @pattern
|
|
OR COALESCE(u.email, '') LIKE @pattern
|
|
OR COALESCE(u.club, '') LIKE @pattern
|
|
OR COALESCE(u.city, '') LIKE @pattern
|
|
)
|
|
ORDER BY
|
|
is_friend DESC,
|
|
has_received_invitation DESC,
|
|
has_sent_invitation DESC,
|
|
u.display_name ASC,
|
|
u.username ASC
|
|
LIMIT @limit;
|
|
""";
|
|
|
|
private const string SelectRelevantPresenceSubjectsSql = """
|
|
SELECT participant_subject
|
|
FROM (
|
|
SELECT CASE
|
|
WHEN f.subject_low = @subject THEN f.subject_high
|
|
ELSE f.subject_low
|
|
END AS participant_subject
|
|
FROM social_friendships f
|
|
WHERE f.subject_low = @subject OR f.subject_high = @subject
|
|
|
|
UNION
|
|
|
|
SELECT i.sender_subject AS participant_subject
|
|
FROM social_friend_invitations i
|
|
WHERE i.recipient_subject = @subject
|
|
|
|
UNION
|
|
|
|
SELECT i.recipient_subject AS participant_subject
|
|
FROM social_friend_invitations i
|
|
WHERE i.sender_subject = @subject
|
|
) participants
|
|
WHERE participant_subject <> @subject;
|
|
""";
|
|
|
|
private const string SelectFriendSubjectsSql = """
|
|
SELECT CASE
|
|
WHEN f.subject_low = @subject THEN f.subject_high
|
|
ELSE f.subject_low
|
|
END AS friend_subject
|
|
FROM social_friendships f
|
|
WHERE f.subject_low = @subject OR f.subject_high = @subject;
|
|
""";
|
|
|
|
private const string SelectKnownUserSubjectSql = """
|
|
SELECT subject
|
|
FROM site_users
|
|
WHERE subject = @subject
|
|
LIMIT 1;
|
|
""";
|
|
|
|
private const string SelectInvitationBetweenSql = """
|
|
SELECT
|
|
id,
|
|
sender_subject,
|
|
recipient_subject
|
|
FROM social_friend_invitations
|
|
WHERE (sender_subject = @subjectA AND recipient_subject = @subjectB)
|
|
OR (sender_subject = @subjectB AND recipient_subject = @subjectA)
|
|
LIMIT 1;
|
|
""";
|
|
|
|
private const string SelectInvitationForRecipientSql = """
|
|
SELECT
|
|
id,
|
|
sender_subject,
|
|
recipient_subject
|
|
FROM social_friend_invitations
|
|
WHERE id = @invitationId
|
|
AND recipient_subject = @recipientSubject
|
|
LIMIT 1;
|
|
""";
|
|
|
|
private const string SelectFriendshipExistsSql = """
|
|
SELECT 1
|
|
FROM social_friendships
|
|
WHERE subject_low = @subjectLow
|
|
AND subject_high = @subjectHigh
|
|
LIMIT 1;
|
|
""";
|
|
|
|
private const string InsertInvitationSql = """
|
|
INSERT INTO social_friend_invitations (
|
|
sender_subject,
|
|
recipient_subject,
|
|
created_utc
|
|
)
|
|
VALUES (
|
|
@senderSubject,
|
|
@recipientSubject,
|
|
@createdUtc
|
|
);
|
|
""";
|
|
|
|
private const string InsertFriendshipSql = """
|
|
INSERT IGNORE INTO social_friendships (
|
|
subject_low,
|
|
subject_high,
|
|
created_utc
|
|
)
|
|
VALUES (
|
|
@subjectLow,
|
|
@subjectHigh,
|
|
@createdUtc
|
|
);
|
|
""";
|
|
|
|
private const string DeleteInvitationByRecipientSql = """
|
|
DELETE FROM social_friend_invitations
|
|
WHERE id = @invitationId
|
|
AND recipient_subject = @recipientSubject;
|
|
""";
|
|
|
|
private const string DeleteInvitationBySenderSql = """
|
|
DELETE FROM social_friend_invitations
|
|
WHERE id = @invitationId
|
|
AND sender_subject = @senderSubject;
|
|
""";
|
|
|
|
private const string DeleteInvitationsBetweenSql = """
|
|
DELETE FROM social_friend_invitations
|
|
WHERE (sender_subject = @subjectA AND recipient_subject = @subjectB)
|
|
OR (sender_subject = @subjectB AND recipient_subject = @subjectA);
|
|
""";
|
|
|
|
private const string DeleteFriendshipSql = """
|
|
DELETE FROM social_friendships
|
|
WHERE subject_low = @subjectLow
|
|
AND subject_high = @subjectHigh;
|
|
""";
|
|
|
|
private const string DeleteUserFriendshipsSql = """
|
|
DELETE FROM social_friendships
|
|
WHERE subject_low = @subject
|
|
OR subject_high = @subject;
|
|
""";
|
|
|
|
private const string DeleteUserInvitationsSql = """
|
|
DELETE FROM social_friend_invitations
|
|
WHERE sender_subject = @subject
|
|
OR recipient_subject = @subject;
|
|
""";
|
|
|
|
private readonly SiteDataOptions _options = options.Value;
|
|
private readonly ILogger<MySqlSocialStore> _logger = logger;
|
|
|
|
public async Task InitializeAsync(CancellationToken cancellationToken)
|
|
{
|
|
for (var attempt = 1; attempt <= _options.InitializationRetries; attempt++)
|
|
{
|
|
try
|
|
{
|
|
await using var connection = new MySqlConnection(_options.BuildConnectionString());
|
|
await connection.OpenAsync(cancellationToken);
|
|
await CreateSchemaAsync(connection, cancellationToken);
|
|
return;
|
|
}
|
|
catch (Exception exception) when (attempt < _options.InitializationRetries)
|
|
{
|
|
_logger.LogWarning(
|
|
exception,
|
|
"Initialisation MySQL impossible pour le module social (tentative {Attempt}/{MaxAttempts}).",
|
|
attempt,
|
|
_options.InitializationRetries);
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(_options.InitializationDelaySeconds), cancellationToken);
|
|
}
|
|
}
|
|
|
|
await using var finalConnection = new MySqlConnection(_options.BuildConnectionString());
|
|
await finalConnection.OpenAsync(cancellationToken);
|
|
await CreateSchemaAsync(finalConnection, cancellationToken);
|
|
}
|
|
|
|
public async Task<SocialOverviewResponse> GetOverviewAsync(
|
|
string subject,
|
|
Func<string, bool> isOnline,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
|
|
var friends = new List<SocialFriendResponse>();
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.CommandText = SelectOverviewFriendsSql;
|
|
command.Parameters.AddWithValue("@subject", subject);
|
|
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
var friendSubject = ReadString(reader, "subject");
|
|
friends.Add(new SocialFriendResponse
|
|
{
|
|
Subject = friendSubject,
|
|
Username = ReadString(reader, "username"),
|
|
DisplayName = ReadString(reader, "display_name"),
|
|
Email = ReadNullableString(reader, "email"),
|
|
Club = ReadNullableString(reader, "club"),
|
|
City = ReadNullableString(reader, "city"),
|
|
IsOnline = isOnline(friendSubject),
|
|
});
|
|
}
|
|
}
|
|
|
|
var receivedInvitations = new List<SocialInvitationResponse>();
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.CommandText = SelectReceivedInvitationsSql;
|
|
command.Parameters.AddWithValue("@subject", subject);
|
|
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
var senderSubject = ReadString(reader, "subject");
|
|
receivedInvitations.Add(new SocialInvitationResponse
|
|
{
|
|
InvitationId = ReadInt64(reader, "id"),
|
|
Subject = senderSubject,
|
|
Username = ReadString(reader, "username"),
|
|
DisplayName = ReadString(reader, "display_name"),
|
|
Email = ReadNullableString(reader, "email"),
|
|
CreatedUtc = ReadDateTime(reader, "created_utc"),
|
|
IsOnline = isOnline(senderSubject),
|
|
});
|
|
}
|
|
}
|
|
|
|
var sentInvitations = new List<SocialInvitationResponse>();
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.CommandText = SelectSentInvitationsSql;
|
|
command.Parameters.AddWithValue("@subject", subject);
|
|
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
var recipientSubject = ReadString(reader, "subject");
|
|
sentInvitations.Add(new SocialInvitationResponse
|
|
{
|
|
InvitationId = ReadInt64(reader, "id"),
|
|
Subject = recipientSubject,
|
|
Username = ReadString(reader, "username"),
|
|
DisplayName = ReadString(reader, "display_name"),
|
|
Email = ReadNullableString(reader, "email"),
|
|
CreatedUtc = ReadDateTime(reader, "created_utc"),
|
|
IsOnline = isOnline(recipientSubject),
|
|
});
|
|
}
|
|
}
|
|
|
|
return new SocialOverviewResponse
|
|
{
|
|
Friends = friends.ToArray(),
|
|
ReceivedInvitations = receivedInvitations.ToArray(),
|
|
SentInvitations = sentInvitations.ToArray(),
|
|
};
|
|
}
|
|
|
|
public async Task<IReadOnlyList<SocialSearchUserResponse>> SearchUsersAsync(
|
|
string subject,
|
|
string? query,
|
|
Func<string, bool> isOnline,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var normalizedQuery = NormalizeQuery(query);
|
|
if (normalizedQuery is null)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = SearchUsersTemplateSql;
|
|
command.Parameters.AddWithValue("@subject", subject);
|
|
command.Parameters.AddWithValue("@pattern", $"%{normalizedQuery}%");
|
|
command.Parameters.AddWithValue("@limit", 12);
|
|
|
|
var results = new List<SocialSearchUserResponse>();
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
var foundSubject = ReadString(reader, "subject");
|
|
results.Add(new SocialSearchUserResponse
|
|
{
|
|
Subject = foundSubject,
|
|
Username = ReadString(reader, "username"),
|
|
DisplayName = ReadString(reader, "display_name"),
|
|
Email = ReadNullableString(reader, "email"),
|
|
Club = ReadNullableString(reader, "club"),
|
|
City = ReadNullableString(reader, "city"),
|
|
IsOnline = isOnline(foundSubject),
|
|
IsFriend = ReadBoolean(reader, "is_friend"),
|
|
HasSentInvitation = ReadBoolean(reader, "has_sent_invitation"),
|
|
HasReceivedInvitation = ReadBoolean(reader, "has_received_invitation"),
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
public async Task SendInvitationAsync(
|
|
string senderSubject,
|
|
string recipientSubject,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (string.Equals(senderSubject, recipientSubject, StringComparison.Ordinal))
|
|
{
|
|
throw new SocialValidationException("Impossible de t'inviter toi-meme en ami.");
|
|
}
|
|
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
|
|
|
if (!await UserExistsAsync(connection, transaction, recipientSubject, cancellationToken))
|
|
{
|
|
throw new SocialValidationException("Le joueur cible est introuvable.");
|
|
}
|
|
|
|
var pair = NormalizePair(senderSubject, recipientSubject);
|
|
if (await FriendshipExistsAsync(connection, transaction, pair.SubjectLow, pair.SubjectHigh, cancellationToken))
|
|
{
|
|
throw new SocialValidationException("Ce joueur fait deja partie de tes amis.");
|
|
}
|
|
|
|
var invitation = await ReadInvitationBetweenAsync(connection, transaction, senderSubject, recipientSubject, cancellationToken);
|
|
if (invitation is not null)
|
|
{
|
|
throw new SocialValidationException(
|
|
string.Equals(invitation.SenderSubject, senderSubject, StringComparison.Ordinal)
|
|
? "Une invitation est deja en attente pour ce joueur."
|
|
: "Ce joueur t'a deja envoye une invitation. Accepte-la depuis la page utilisateur.");
|
|
}
|
|
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.Transaction = transaction;
|
|
command.CommandText = InsertInvitationSql;
|
|
command.Parameters.AddWithValue("@senderSubject", senderSubject);
|
|
command.Parameters.AddWithValue("@recipientSubject", recipientSubject);
|
|
command.Parameters.AddWithValue("@createdUtc", DateTime.UtcNow);
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
|
|
await transaction.CommitAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<string> AcceptInvitationAsync(long invitationId, string recipientSubject, CancellationToken cancellationToken)
|
|
{
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
|
|
|
var invitation = await ReadInvitationForRecipientAsync(connection, transaction, invitationId, recipientSubject, cancellationToken)
|
|
?? throw new SocialValidationException("Invitation introuvable ou deja traitee.");
|
|
|
|
var pair = NormalizePair(invitation.SenderSubject, invitation.RecipientSubject);
|
|
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.Transaction = transaction;
|
|
command.CommandText = InsertFriendshipSql;
|
|
command.Parameters.AddWithValue("@subjectLow", pair.SubjectLow);
|
|
command.Parameters.AddWithValue("@subjectHigh", pair.SubjectHigh);
|
|
command.Parameters.AddWithValue("@createdUtc", DateTime.UtcNow);
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.Transaction = transaction;
|
|
command.CommandText = DeleteInvitationsBetweenSql;
|
|
command.Parameters.AddWithValue("@subjectA", invitation.SenderSubject);
|
|
command.Parameters.AddWithValue("@subjectB", invitation.RecipientSubject);
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
|
|
await transaction.CommitAsync(cancellationToken);
|
|
return invitation.SenderSubject;
|
|
}
|
|
|
|
public async Task<string> DeclineInvitationAsync(long invitationId, string recipientSubject, CancellationToken cancellationToken)
|
|
{
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
var invitation = await ReadInvitationForRecipientAsync(connection, transaction: null, invitationId, recipientSubject, cancellationToken)
|
|
?? throw new SocialValidationException("Invitation introuvable ou deja traitee.");
|
|
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = DeleteInvitationByRecipientSql;
|
|
command.Parameters.AddWithValue("@invitationId", invitationId);
|
|
command.Parameters.AddWithValue("@recipientSubject", recipientSubject);
|
|
|
|
var deleted = await command.ExecuteNonQueryAsync(cancellationToken);
|
|
if (deleted <= 0)
|
|
{
|
|
throw new SocialValidationException("Invitation introuvable ou deja traitee.");
|
|
}
|
|
|
|
return invitation.SenderSubject;
|
|
}
|
|
|
|
public async Task<string> CancelInvitationAsync(long invitationId, string senderSubject, CancellationToken cancellationToken)
|
|
{
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
var invitation = await ReadInvitationForSenderAsync(connection, transaction: null, invitationId, senderSubject, cancellationToken)
|
|
?? throw new SocialValidationException("Invitation introuvable ou deja retiree.");
|
|
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = DeleteInvitationBySenderSql;
|
|
command.Parameters.AddWithValue("@invitationId", invitationId);
|
|
command.Parameters.AddWithValue("@senderSubject", senderSubject);
|
|
|
|
var deleted = await command.ExecuteNonQueryAsync(cancellationToken);
|
|
if (deleted <= 0)
|
|
{
|
|
throw new SocialValidationException("Invitation introuvable ou deja retiree.");
|
|
}
|
|
|
|
return invitation.RecipientSubject;
|
|
}
|
|
|
|
public async Task RemoveFriendAsync(string subject, string friendSubject, CancellationToken cancellationToken)
|
|
{
|
|
var pair = NormalizePair(subject, friendSubject);
|
|
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = DeleteFriendshipSql;
|
|
command.Parameters.AddWithValue("@subjectLow", pair.SubjectLow);
|
|
command.Parameters.AddWithValue("@subjectHigh", pair.SubjectHigh);
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<bool> AreFriendsAsync(string subject, string otherSubject, CancellationToken cancellationToken)
|
|
{
|
|
var pair = NormalizePair(subject, otherSubject);
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
return await FriendshipExistsAsync(connection, transaction: null, pair.SubjectLow, pair.SubjectHigh, cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<string>> GetRelevantPresenceSubjectsAsync(string subject, CancellationToken cancellationToken)
|
|
{
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = SelectRelevantPresenceSubjectsSql;
|
|
command.Parameters.AddWithValue("@subject", subject);
|
|
|
|
var subjects = new List<string>();
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
subjects.Add(ReadString(reader, "participant_subject"));
|
|
}
|
|
|
|
return subjects;
|
|
}
|
|
|
|
public async Task<IReadOnlyList<string>> GetFriendSubjectsAsync(string subject, CancellationToken cancellationToken)
|
|
{
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = SelectFriendSubjectsSql;
|
|
command.Parameters.AddWithValue("@subject", subject);
|
|
|
|
var subjects = new List<string>();
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
subjects.Add(ReadString(reader, "friend_subject"));
|
|
}
|
|
|
|
return subjects;
|
|
}
|
|
|
|
public async Task DeleteUserAsync(string subject, CancellationToken cancellationToken)
|
|
{
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.CommandText = DeleteUserFriendshipsSql;
|
|
command.Parameters.AddWithValue("@subject", subject);
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.CommandText = DeleteUserInvitationsSql;
|
|
command.Parameters.AddWithValue("@subject", subject);
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
private async Task CreateSchemaAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.CommandText = CreateFriendshipsTableSql;
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
|
|
await using (var command = connection.CreateCommand())
|
|
{
|
|
command.CommandText = CreateInvitationsTableSql;
|
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
|
|
{
|
|
var connection = new MySqlConnection(_options.BuildConnectionString());
|
|
await connection.OpenAsync(cancellationToken);
|
|
return connection;
|
|
}
|
|
|
|
private static (string SubjectLow, string SubjectHigh) NormalizePair(string subjectA, string subjectB)
|
|
=> string.CompareOrdinal(subjectA, subjectB) <= 0
|
|
? (subjectA, subjectB)
|
|
: (subjectB, subjectA);
|
|
|
|
private static string? NormalizeQuery(string? query)
|
|
{
|
|
var trimmed = query?.Trim();
|
|
if (string.IsNullOrWhiteSpace(trimmed))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (trimmed.Length < 2)
|
|
{
|
|
throw new SocialValidationException("La recherche d'amis demande au moins 2 caracteres.");
|
|
}
|
|
|
|
return trimmed.Length > 80
|
|
? trimmed[..80]
|
|
: trimmed;
|
|
}
|
|
|
|
private static async Task<bool> UserExistsAsync(
|
|
MySqlConnection connection,
|
|
MySqlTransaction transaction,
|
|
string subject,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using var command = connection.CreateCommand();
|
|
command.Transaction = transaction;
|
|
command.CommandText = SelectKnownUserSubjectSql;
|
|
command.Parameters.AddWithValue("@subject", subject);
|
|
return await command.ExecuteScalarAsync(cancellationToken) is not null;
|
|
}
|
|
|
|
private static async Task<bool> FriendshipExistsAsync(
|
|
MySqlConnection connection,
|
|
MySqlTransaction? transaction,
|
|
string subjectLow,
|
|
string subjectHigh,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using var command = connection.CreateCommand();
|
|
command.Transaction = transaction;
|
|
command.CommandText = SelectFriendshipExistsSql;
|
|
command.Parameters.AddWithValue("@subjectLow", subjectLow);
|
|
command.Parameters.AddWithValue("@subjectHigh", subjectHigh);
|
|
return await command.ExecuteScalarAsync(cancellationToken) is not null;
|
|
}
|
|
|
|
private static async Task<InvitationRow?> ReadInvitationBetweenAsync(
|
|
MySqlConnection connection,
|
|
MySqlTransaction transaction,
|
|
string subjectA,
|
|
string subjectB,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using var command = connection.CreateCommand();
|
|
command.Transaction = transaction;
|
|
command.CommandText = SelectInvitationBetweenSql;
|
|
command.Parameters.AddWithValue("@subjectA", subjectA);
|
|
command.Parameters.AddWithValue("@subjectB", subjectB);
|
|
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
if (!await reader.ReadAsync(cancellationToken))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new InvitationRow(
|
|
ReadInt64(reader, "id"),
|
|
ReadString(reader, "sender_subject"),
|
|
ReadString(reader, "recipient_subject"));
|
|
}
|
|
|
|
private static async Task<InvitationRow?> ReadInvitationForRecipientAsync(
|
|
MySqlConnection connection,
|
|
MySqlTransaction? transaction,
|
|
long invitationId,
|
|
string recipientSubject,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using var command = connection.CreateCommand();
|
|
command.Transaction = transaction;
|
|
command.CommandText = SelectInvitationForRecipientSql;
|
|
command.Parameters.AddWithValue("@invitationId", invitationId);
|
|
command.Parameters.AddWithValue("@recipientSubject", recipientSubject);
|
|
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
if (!await reader.ReadAsync(cancellationToken))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new InvitationRow(
|
|
ReadInt64(reader, "id"),
|
|
ReadString(reader, "sender_subject"),
|
|
ReadString(reader, "recipient_subject"));
|
|
}
|
|
|
|
private static async Task<InvitationRow?> ReadInvitationForSenderAsync(
|
|
MySqlConnection connection,
|
|
MySqlTransaction? transaction,
|
|
long invitationId,
|
|
string senderSubject,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using var command = connection.CreateCommand();
|
|
command.Transaction = transaction;
|
|
command.CommandText = """
|
|
SELECT
|
|
id,
|
|
sender_subject,
|
|
recipient_subject
|
|
FROM social_friend_invitations
|
|
WHERE id = @invitationId
|
|
AND sender_subject = @senderSubject
|
|
LIMIT 1;
|
|
""";
|
|
command.Parameters.AddWithValue("@invitationId", invitationId);
|
|
command.Parameters.AddWithValue("@senderSubject", senderSubject);
|
|
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
if (!await reader.ReadAsync(cancellationToken))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new InvitationRow(
|
|
ReadInt64(reader, "id"),
|
|
ReadString(reader, "sender_subject"),
|
|
ReadString(reader, "recipient_subject"));
|
|
}
|
|
|
|
private static bool ReadBoolean(MySqlDataReader reader, string columnName)
|
|
=> Convert.ToInt32(reader[columnName]) > 0;
|
|
|
|
private static string ReadString(MySqlDataReader reader, string columnName)
|
|
=> reader.GetString(reader.GetOrdinal(columnName));
|
|
|
|
private static long ReadInt64(MySqlDataReader reader, string columnName)
|
|
=> reader.GetInt64(reader.GetOrdinal(columnName));
|
|
|
|
private static DateTime ReadDateTime(MySqlDataReader reader, string columnName)
|
|
=> reader.GetDateTime(reader.GetOrdinal(columnName));
|
|
|
|
private static string? ReadNullableString(MySqlDataReader reader, string columnName)
|
|
{
|
|
var ordinal = reader.GetOrdinal(columnName);
|
|
return reader.IsDBNull(ordinal)
|
|
? null
|
|
: reader.GetString(ordinal);
|
|
}
|
|
|
|
private sealed record InvitationRow(long Id, string SenderSubject, string RecipientSubject);
|
|
}
|