XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.Data.Sqlite;
using XFEToolBox.Core.Chat;

namespace XFEToolBox.Server.Core.Chat;

public sealed partial class ChatRepository
{
    public bool AreFriends(string userA, string userB) =>
        AreFriendsAsync(userA, userB).GetAwaiter().GetResult();

    public async Task<bool> AreFriendsAsync(
        string userA,
        string userB,
        CancellationToken cancellationToken = default)
    {
        var (low, high) = NormalizePair(userA, userB);
        await using var connection = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
        await using var command = connection.CreateCommand();
        command.CommandText = "SELECT EXISTS(SELECT 1 FROM friendships WHERE user_low = $low AND user_high = $high);";
        command.Parameters.AddWithValue("$low", low);
        command.Parameters.AddWithValue("$high", high);
        return Convert.ToInt64(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false)) != 0;
    }

    public async Task<IReadOnlyList<ChatFriendRecord>> GetFriendsAsync(
        string userId,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(userId);
        await using var connection = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
        await using var command = connection.CreateCommand();
        command.CommandText = """
                              SELECT CASE WHEN f.user_low = $userId THEN f.user_high ELSE f.user_low END AS friend_id,
                                     f.created_at_utc,
                                     COALESCE(c.id, '')
                              FROM friendships AS f
                              LEFT JOIN conversations AS c
                                ON c.kind = 0
                               AND c.direct_user_low = f.user_low
                               AND c.direct_user_high = f.user_high
                              WHERE f.user_low = $userId OR f.user_high = $userId
                              ORDER BY f.created_at_utc DESC, friend_id;
                              """;
        command.Parameters.AddWithValue("$userId", userId);
        var result = new List<ChatFriendRecord>();
        await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            result.Add(new ChatFriendRecord(
                reader.GetString(0),
                FromUnixMilliseconds(reader.GetInt64(1)),
                reader.GetString(2)));
        return result;
    }

    public async Task<IReadOnlyList<ChatFriendRequestRecord>> GetFriendRequestsAsync(
        string userId,
        int limit = 100,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(userId);
        limit = Math.Clamp(limit, 1, 200);
        await using var connection = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
        await using var command = connection.CreateCommand();
        command.CommandText = """
                              SELECT id, from_user_id, to_user_id, status, message,
                                     created_at_utc, responded_at_utc
                              FROM friend_requests
                              WHERE from_user_id = $userId OR to_user_id = $userId
                              ORDER BY CASE WHEN status = 0 THEN 0 ELSE 1 END,
                                       created_at_utc DESC
                              LIMIT $limit;
                              """;
        command.Parameters.AddWithValue("$userId", userId);
        command.Parameters.AddWithValue("$limit", limit);
        var result = new List<ChatFriendRequestRecord>();
        await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            result.Add(ReadFriendRequest(reader));
        return result;
    }

    public async Task<ChatFriendRequestRecord> CreateFriendRequestAsync(
        string fromUserId,
        string toUserId,
        string? message,
        CancellationToken cancellationToken = default)
    {
        var (low, high) = NormalizePair(fromUserId, toUserId);
        message = (message ?? string.Empty).Trim();
        if (message.Length > 300)
            throw new ChatRepositoryException(ChatRepositoryError.InvalidRequest, "好友申请附言不能超过 300 个字符。");

        await using var connection = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
        await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
        if (await AreFriendsAsync(connection, transaction, low, high, cancellationToken).ConfigureAwait(false))
            throw new ChatRepositoryException(ChatRepositoryError.Conflict, "双方已经是好友。");

        var id = NewId();
        var now = DateTimeOffset.UtcNow;
        try
        {
            await using var command = connection.CreateCommand();
            command.Transaction = transaction;
            command.CommandText = """
                                  INSERT INTO friend_requests(
                                      id, from_user_id, to_user_id, user_low, user_high,
                                      message, status, created_at_utc)
                                  VALUES($id, $from, $to, $low, $high, $message, 0, $created);
                                  """;
            command.Parameters.AddWithValue("$id", id);
            command.Parameters.AddWithValue("$from", fromUserId);
            command.Parameters.AddWithValue("$to", toUserId);
            command.Parameters.AddWithValue("$low", low);
            command.Parameters.AddWithValue("$high", high);
            command.Parameters.AddWithValue("$message", message);
            command.Parameters.AddWithValue("$created", ToUnixMilliseconds(now));
            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (SqliteException exception) when (IsConstraintViolation(exception))
        {
            throw new ChatRepositoryException(ChatRepositoryError.Conflict, "双方已有待处理的好友申请。");
        }

        return new ChatFriendRequestRecord(
            id, fromUserId, toUserId, ChatFriendRequestStatus.Pending, message, now, null);
    }

    public async Task<ChatFriendRequestRecord> RespondToFriendRequestAsync(
        string requestId,
        string respondingUserId,
        bool accept,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(requestId);
        ArgumentException.ThrowIfNullOrWhiteSpace(respondingUserId);
        await using var connection = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
        await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
        var request = await FindFriendRequestAsync(connection, transaction, requestId, cancellationToken).ConfigureAwait(false)
                      ?? throw new ChatRepositoryException(ChatRepositoryError.NotFound, "好友申请不存在。");
        if (!string.Equals(request.ToUserId, respondingUserId, StringComparison.Ordinal))
            throw new ChatRepositoryException(ChatRepositoryError.Forbidden, "只能响应发给自己的好友申请。");
        if (request.Status != ChatFriendRequestStatus.Pending)
            throw new ChatRepositoryException(ChatRepositoryError.Conflict, "好友申请已经处理。");

        var now = DateTimeOffset.UtcNow;
        var status = accept ? ChatFriendRequestStatus.Accepted : ChatFriendRequestStatus.Rejected;
        await using (var command = connection.CreateCommand())
        {
            command.Transaction = transaction;
            command.CommandText = """
                                  UPDATE friend_requests
                                  SET status = $status, responded_at_utc = $responded
                                  WHERE id = $id AND status = 0;
                                  """;
            command.Parameters.AddWithValue("$status", (int)status);
            command.Parameters.AddWithValue("$responded", ToUnixMilliseconds(now));
            command.Parameters.AddWithValue("$id", requestId);
            if (await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false) != 1)
                throw new ChatRepositoryException(ChatRepositoryError.Conflict, "好友申请已被其他请求处理。");
        }

        if (accept)
        {
            var (low, high) = NormalizePair(request.FromUserId, request.ToUserId);
            await using (var command = connection.CreateCommand())
            {
                command.Transaction = transaction;
                command.CommandText = """
                                      INSERT INTO friendships(user_low, user_high, created_at_utc)
                                      VALUES($low, $high, $created)
                                      ON CONFLICT(user_low, user_high) DO NOTHING;
                                      """;
                command.Parameters.AddWithValue("$low", low);
                command.Parameters.AddWithValue("$high", high);
                command.Parameters.AddWithValue("$created", ToUnixMilliseconds(now));
                await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            }
            _ = await GetOrCreateDirectConversationAsync(
                connection, transaction, low, high, now, cancellationToken).ConfigureAwait(false);
        }

        await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
        return request with { Status = status, RespondedAtUtc = now };
    }

    public async Task<bool> DeleteFriendshipAsync(
        string userId,
        string friendUserId,
        CancellationToken cancellationToken = default)
    {
        var (low, high) = NormalizePair(userId, friendUserId);
        await using var connection = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
        await using var command = connection.CreateCommand();
        command.CommandText = "DELETE FROM friendships WHERE user_low = $low AND user_high = $high;";
        command.Parameters.AddWithValue("$low", low);
        command.Parameters.AddWithValue("$high", high);
        return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false) == 1;
    }

    public async Task<ChatConversationRecord> GetOrCreateDirectConversationAsync(
        string userId,
        string friendUserId,
        CancellationToken cancellationToken = default)
    {
        var (low, high) = NormalizePair(userId, friendUserId);
        await using var connection = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
        await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
        if (!await AreFriendsAsync(connection, transaction, low, high, cancellationToken).ConfigureAwait(false))
            throw new ChatRepositoryException(ChatRepositoryError.Forbidden, "只有好友之间可以发起私聊。");
        var conversation = await GetOrCreateDirectConversationAsync(
            connection, transaction, low, high, DateTimeOffset.UtcNow, cancellationToken).ConfigureAwait(false);
        await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
        return conversation;
    }

    private static async Task<bool> AreFriendsAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        string low,
        string high,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = "SELECT EXISTS(SELECT 1 FROM friendships WHERE user_low = $low AND user_high = $high);";
        command.Parameters.AddWithValue("$low", low);
        command.Parameters.AddWithValue("$high", high);
        return Convert.ToInt64(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false)) != 0;
    }

    private static async Task<ChatConversationRecord> GetOrCreateDirectConversationAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        string low,
        string high,
        DateTimeOffset now,
        CancellationToken cancellationToken)
    {
        await using (var findCommand = connection.CreateCommand())
        {
            findCommand.Transaction = transaction;
            findCommand.CommandText = """
                                      SELECT id, created_at_utc, updated_at_utc
                                      FROM conversations
                                      WHERE kind = 0 AND direct_user_low = $low AND direct_user_high = $high;
                                      """;
            findCommand.Parameters.AddWithValue("$low", low);
            findCommand.Parameters.AddWithValue("$high", high);
            await using var reader = await findCommand.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
            if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
                return new ChatConversationRecord(
                    reader.GetString(0),
                    ChatConversationKind.Direct,
                    low,
                    high,
                    null,
                    FromUnixMilliseconds(reader.GetInt64(1)),
                    FromUnixMilliseconds(reader.GetInt64(2)));
        }

        var id = NewId();
        await using (var insertCommand = connection.CreateCommand())
        {
            insertCommand.Transaction = transaction;
            insertCommand.CommandText = """
                                        INSERT INTO conversations(
                                            id, kind, direct_user_low, direct_user_high,
                                            created_at_utc, updated_at_utc)
                                        VALUES($id, 0, $low, $high, $created, $updated);
                                        """;
            insertCommand.Parameters.AddWithValue("$id", id);
            insertCommand.Parameters.AddWithValue("$low", low);
            insertCommand.Parameters.AddWithValue("$high", high);
            insertCommand.Parameters.AddWithValue("$created", ToUnixMilliseconds(now));
            insertCommand.Parameters.AddWithValue("$updated", ToUnixMilliseconds(now));
            await insertCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        }
        return new ChatConversationRecord(id, ChatConversationKind.Direct, low, high, null, now, now);
    }

    private static async Task<ChatFriendRequestRecord?> FindFriendRequestAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        string requestId,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = """
                              SELECT id, from_user_id, to_user_id, status, message,
                                     created_at_utc, responded_at_utc
                              FROM friend_requests WHERE id = $id;
                              """;
        command.Parameters.AddWithValue("$id", requestId);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
        return await reader.ReadAsync(cancellationToken).ConfigureAwait(false) ? ReadFriendRequest(reader) : null;
    }

    private static ChatFriendRequestRecord ReadFriendRequest(SqliteDataReader reader) => new(
        reader.GetString(0),
        reader.GetString(1),
        reader.GetString(2),
        (ChatFriendRequestStatus)reader.GetInt32(3),
        reader.GetString(4),
        FromUnixMilliseconds(reader.GetInt64(5)),
        FromNullableUnixMilliseconds(reader, 6));
}