278 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			278 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
import Foundation
 | 
						||
 | 
						||
enum ChatServiceError: LocalizedError {
 | 
						||
    case unexpectedStatus(String)
 | 
						||
    case decoding(debugDescription: String)
 | 
						||
 | 
						||
    var errorDescription: String? {
 | 
						||
        switch self {
 | 
						||
        case .unexpectedStatus(let message):
 | 
						||
            return message
 | 
						||
        case .decoding(let debugDescription):
 | 
						||
            return AppConfig.DEBUG
 | 
						||
                ? debugDescription
 | 
						||
                : NSLocalizedString("Не удалось обработать данные чатов.", comment: "")
 | 
						||
        }
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
final class ChatService {
 | 
						||
    private let client: NetworkClient
 | 
						||
    private let decoder: JSONDecoder
 | 
						||
    private let cacheWriter: ChatCacheWriter
 | 
						||
 | 
						||
    init(client: NetworkClient = .shared, cacheWriter: ChatCacheWriter = .shared) {
 | 
						||
        self.client = client
 | 
						||
        self.decoder = JSONDecoder()
 | 
						||
        self.decoder.keyDecodingStrategy = .convertFromSnakeCase
 | 
						||
        self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
 | 
						||
        self.cacheWriter = cacheWriter
 | 
						||
    }
 | 
						||
 | 
						||
    func fetchPrivateChats(
 | 
						||
        offset: Int,
 | 
						||
        limit: Int,
 | 
						||
        completion: @escaping (Result<PrivateChatListData, Error>) -> Void
 | 
						||
    ) {
 | 
						||
        let query: [String: String?] = [
 | 
						||
            "offset": String(offset),
 | 
						||
            "limit": String(limit)
 | 
						||
        ]
 | 
						||
 | 
						||
        client.request(
 | 
						||
            path: "/v1/chat/private/list",
 | 
						||
            method: .get,
 | 
						||
            query: query,
 | 
						||
            requiresAuth: true
 | 
						||
        ) { [decoder, weak self] result in
 | 
						||
            switch result {
 | 
						||
            case .success(let response):
 | 
						||
                do {
 | 
						||
                    let apiResponse = try decoder.decode(APIResponse<PrivateChatListData>.self, from: response.data)
 | 
						||
                    guard apiResponse.status == "fine" else {
 | 
						||
                        let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить список чатов.", comment: "")
 | 
						||
                        completion(.failure(ChatServiceError.unexpectedStatus(message)))
 | 
						||
                        return
 | 
						||
                    }
 | 
						||
                    let chatData = apiResponse.data
 | 
						||
                    self?.cacheWriter.storePrivateChatList(chatData)
 | 
						||
                    completion(.success(chatData))
 | 
						||
                } catch {
 | 
						||
                    let debugMessage = Self.describeDecodingError(error: error, data: response.data)
 | 
						||
                    if AppConfig.DEBUG {
 | 
						||
                        print("[ChatService] decode private chats failed: \(debugMessage)")
 | 
						||
                    }
 | 
						||
                    completion(.failure(ChatServiceError.decoding(debugDescription: debugMessage)))
 | 
						||
                }
 | 
						||
            case .failure(let error):
 | 
						||
                completion(.failure(error))
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    func fetchPrivateChatHistory(
 | 
						||
        chatId: String,
 | 
						||
        beforeMessageId: String?,
 | 
						||
        limit: Int,
 | 
						||
        completion: @escaping (Result<PrivateChatHistoryData, Error>) -> Void
 | 
						||
    ) {
 | 
						||
        var query: [String: String?] = [
 | 
						||
            "chat_id": chatId,
 | 
						||
            "limit": String(limit)
 | 
						||
        ]
 | 
						||
 | 
						||
        if let beforeMessageId,
 | 
						||
           !beforeMessageId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
 | 
						||
            query["before_message_id"] = beforeMessageId
 | 
						||
        }
 | 
						||
 | 
						||
        client.request(
 | 
						||
            path: "/v1/chat/private/history",
 | 
						||
            method: .get,
 | 
						||
            query: query,
 | 
						||
            requiresAuth: true
 | 
						||
        ) { [decoder] result in
 | 
						||
            switch result {
 | 
						||
            case .success(let response):
 | 
						||
                do {
 | 
						||
                    let apiResponse = try decoder.decode(APIResponse<PrivateChatHistoryData>.self, from: response.data)
 | 
						||
                    guard apiResponse.status == "fine" else {
 | 
						||
                        let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить историю чата.", comment: "")
 | 
						||
                        completion(.failure(ChatServiceError.unexpectedStatus(message)))
 | 
						||
                        return
 | 
						||
                    }
 | 
						||
                    completion(.success(apiResponse.data))
 | 
						||
                } catch {
 | 
						||
                    let debugMessage = Self.describeDecodingError(error: error, data: response.data)
 | 
						||
                    if AppConfig.DEBUG {
 | 
						||
                        print("[ChatService] decode private chat history failed: \(debugMessage)")
 | 
						||
                    }
 | 
						||
                    completion(.failure(ChatServiceError.decoding(debugDescription: debugMessage)))
 | 
						||
                }
 | 
						||
            case .failure(let error):
 | 
						||
                completion(.failure(error))
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    func createOrFindPrivateChat(
 | 
						||
        targetUserId: String,
 | 
						||
        completion: @escaping (Result<PrivateChatCreateData, Error>) -> Void
 | 
						||
    ) {
 | 
						||
        let query: [String: String?] = [
 | 
						||
            "target_user_id": targetUserId
 | 
						||
        ]
 | 
						||
 | 
						||
        client.request(
 | 
						||
            path: "/v1/chat/private/create",
 | 
						||
            method: .post,
 | 
						||
            query: query,
 | 
						||
            requiresAuth: true
 | 
						||
        ) { [decoder] result in
 | 
						||
            switch result {
 | 
						||
            case .success(let response):
 | 
						||
                do {
 | 
						||
                    let apiResponse = try decoder.decode(APIResponse<PrivateChatCreateData>.self, from: response.data)
 | 
						||
                    guard apiResponse.status == "fine" else {
 | 
						||
                        let message = apiResponse.detail ?? NSLocalizedString("Не удалось создать чат.", comment: "")
 | 
						||
                        completion(.failure(ChatServiceError.unexpectedStatus(message)))
 | 
						||
                        return
 | 
						||
                    }
 | 
						||
                    completion(.success(apiResponse.data))
 | 
						||
                } catch {
 | 
						||
                    let debugMessage = Self.describeDecodingError(error: error, data: response.data)
 | 
						||
                    if AppConfig.DEBUG {
 | 
						||
                        print("[ChatService] create private chat decode failed: \(debugMessage)")
 | 
						||
                    }
 | 
						||
                    completion(.failure(ChatServiceError.decoding(debugDescription: debugMessage)))
 | 
						||
                }
 | 
						||
            case .failure(let error):
 | 
						||
                completion(.failure(error))
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    func sendPrivateMessage(
 | 
						||
        chatId: String,
 | 
						||
        content: String,
 | 
						||
        completion: @escaping (Result<PrivateMessageSendData, Error>) -> Void
 | 
						||
    ) {
 | 
						||
        let payload: [String: Any] = [
 | 
						||
            "chat_id": chatId,
 | 
						||
            "content": content,
 | 
						||
            "message_type": ["text"]
 | 
						||
        ]
 | 
						||
 | 
						||
        guard let body = try? JSONSerialization.data(withJSONObject: payload, options: []) else {
 | 
						||
            completion(.failure(ChatServiceError.decoding(debugDescription: "Невозможно сериализовать тело запроса.")))
 | 
						||
            return
 | 
						||
        }
 | 
						||
 | 
						||
        client.request(
 | 
						||
            path: "/v1/chat/private/send",
 | 
						||
            method: .post,
 | 
						||
            headers: ["Content-Type": "application/json"],
 | 
						||
            body: body,
 | 
						||
            requiresAuth: true
 | 
						||
        ) { [decoder] result in
 | 
						||
            switch result {
 | 
						||
            case .success(let response):
 | 
						||
                do {
 | 
						||
                    let apiResponse = try decoder.decode(APIResponse<PrivateMessageSendData>.self, from: response.data)
 | 
						||
                    guard apiResponse.status == "fine" else {
 | 
						||
                        let message = apiResponse.detail ?? NSLocalizedString("Не удалось отправить сообщение.", comment: "")
 | 
						||
                        completion(.failure(ChatServiceError.unexpectedStatus(message)))
 | 
						||
                        return
 | 
						||
                    }
 | 
						||
                    completion(.success(apiResponse.data))
 | 
						||
                } catch {
 | 
						||
                    let debugMessage = Self.describeDecodingError(error: error, data: response.data)
 | 
						||
                    if AppConfig.DEBUG {
 | 
						||
                        print("[ChatService] send private message decode failed: \(debugMessage)")
 | 
						||
                    }
 | 
						||
                    completion(.failure(ChatServiceError.decoding(debugDescription: debugMessage)))
 | 
						||
                }
 | 
						||
            case .failure(let error):
 | 
						||
                completion(.failure(error))
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private static func decodeDate(from decoder: Decoder) throws -> Date {
 | 
						||
        let container = try decoder.singleValueContainer()
 | 
						||
        let string = try container.decode(String.self)
 | 
						||
        if let date = iso8601WithFractionalSeconds.date(from: string) {
 | 
						||
            return date
 | 
						||
        }
 | 
						||
        if let date = iso8601Simple.date(from: string) {
 | 
						||
            return date
 | 
						||
        }
 | 
						||
        throw DecodingError.dataCorruptedError(
 | 
						||
            in: container,
 | 
						||
            debugDescription: "Невозможно декодировать дату: \(string)"
 | 
						||
        )
 | 
						||
    }
 | 
						||
 | 
						||
    private static func describeDecodingError(error: Error, data: Data) -> String {
 | 
						||
        var parts: [String] = []
 | 
						||
 | 
						||
        if let decodingError = error as? DecodingError {
 | 
						||
            parts.append(decodingDescription(from: decodingError))
 | 
						||
        } else {
 | 
						||
            parts.append(error.localizedDescription)
 | 
						||
        }
 | 
						||
 | 
						||
        if let payload = truncatedPayload(from: data) {
 | 
						||
            parts.append("payload=\(payload)")
 | 
						||
        }
 | 
						||
 | 
						||
        return parts.joined(separator: "\n")
 | 
						||
    }
 | 
						||
 | 
						||
    private static func decodingDescription(from error: DecodingError) -> String {
 | 
						||
        switch error {
 | 
						||
        case .typeMismatch(let type, let context):
 | 
						||
            return "Type mismatch for \(type) at \(codingPath(from: context)): \(context.debugDescription)"
 | 
						||
        case .valueNotFound(let type, let context):
 | 
						||
            return "Value not found for \(type) at \(codingPath(from: context)): \(context.debugDescription)"
 | 
						||
        case .keyNotFound(let key, let context):
 | 
						||
            return "Missing key '\(key.stringValue)' at \(codingPath(from: context)): \(context.debugDescription)"
 | 
						||
        case .dataCorrupted(let context):
 | 
						||
            return "Corrupted data at \(codingPath(from: context)): \(context.debugDescription)"
 | 
						||
        @unknown default:
 | 
						||
            return error.localizedDescription
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private static func codingPath(from context: DecodingError.Context) -> String {
 | 
						||
        let path = context.codingPath.map { $0.stringValue }.filter { !$0.isEmpty }
 | 
						||
        return path.isEmpty ? "root" : path.joined(separator: ".")
 | 
						||
    }
 | 
						||
 | 
						||
    private static func truncatedPayload(from data: Data, limit: Int = 512) -> String? {
 | 
						||
        guard let string = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
 | 
						||
              !string.isEmpty else {
 | 
						||
            return nil
 | 
						||
        }
 | 
						||
 | 
						||
        if string.count <= limit {
 | 
						||
            return string
 | 
						||
        }
 | 
						||
 | 
						||
        let index = string.index(string.startIndex, offsetBy: limit)
 | 
						||
        return String(string[string.startIndex..<index]) + "…"
 | 
						||
    }
 | 
						||
 | 
						||
    private static let iso8601WithFractionalSeconds: ISO8601DateFormatter = {
 | 
						||
        let formatter = ISO8601DateFormatter()
 | 
						||
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
 | 
						||
        return formatter
 | 
						||
    }()
 | 
						||
 | 
						||
    private static let iso8601Simple: ISO8601DateFormatter = {
 | 
						||
        let formatter = ISO8601DateFormatter()
 | 
						||
        formatter.formatOptions = [.withInternetDateTime]
 | 
						||
        return formatter
 | 
						||
    }()
 | 
						||
}
 |