import Foundation class APIClient: ObservableObject { private let baseURL = "https://mkch.pooziqo.xyz" private let apiURL = "https://mkch.pooziqo.xyz/api" private let session = URLSession.shared private var authKey: String = "" private var passcode: String = "" private let userAgent = "MobileMkch/2.0.0-ios-alpha" func authenticate(authKey: String, completion: @escaping (Error?) -> Void) { self.authKey = authKey let url = URL(string: "\(baseURL)/key/auth/")! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(error) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completion(APIError(message: "Ошибка получения формы аутентификации", code: 0)) return } guard let data = data, let html = String(data: data, encoding: .utf8) else { completion(APIError(message: "Ошибка чтения формы аутентификации", code: 0)) return } let csrfToken = self.extractCSRFToken(from: html) guard !csrfToken.isEmpty else { completion(APIError(message: "Не удалось извлечь CSRF токен", code: 0)) return } var formData = URLComponents() formData.queryItems = [ URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken), URLQueryItem(name: "key", value: authKey) ] var postRequest = URLRequest(url: url) postRequest.httpMethod = "POST" postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") postRequest.setValue("\(self.baseURL)/key/auth/", forHTTPHeaderField: "Referer") postRequest.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") postRequest.httpBody = formData.query?.data(using: .utf8) self.session.dataTask(with: postRequest) { _, postResponse, postError in DispatchQueue.main.async { if let postError = postError { completion(postError) return } guard let postHttpResponse = postResponse as? HTTPURLResponse, (postHttpResponse.statusCode == 200 || postHttpResponse.statusCode == 302) else { completion(APIError(message: "Ошибка аутентификации", code: 0)) return } completion(nil) } }.resume() } }.resume() } func loginWithPasscode(passcode: String, completion: @escaping (Error?) -> Void) { self.passcode = passcode let url = URL(string: "\(baseURL)/passcode/enter/")! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(error) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completion(APIError(message: "Ошибка получения формы passcode", code: 0)) return } guard let data = data, let html = String(data: data, encoding: .utf8) else { completion(APIError(message: "Ошибка чтения формы passcode", code: 0)) return } let csrfToken = self.extractCSRFToken(from: html) guard !csrfToken.isEmpty else { completion(APIError(message: "Не удалось извлечь CSRF токен для passcode", code: 0)) return } var formData = URLComponents() formData.queryItems = [ URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken), URLQueryItem(name: "passcode", value: passcode) ] var postRequest = URLRequest(url: url) postRequest.httpMethod = "POST" postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") postRequest.setValue("\(self.baseURL)/passcode/enter/", forHTTPHeaderField: "Referer") postRequest.httpBody = formData.query?.data(using: .utf8) self.session.dataTask(with: postRequest) { _, postResponse, postError in DispatchQueue.main.async { if let postError = postError { completion(postError) return } guard let postHttpResponse = postResponse as? HTTPURLResponse, (postHttpResponse.statusCode == 200 || postHttpResponse.statusCode == 302) else { completion(APIError(message: "Ошибка входа с passcode", code: 0)) return } completion(nil) } }.resume() } }.resume() } func getBoards(completion: @escaping (Result<[Board], Error>) -> Void) { if let cachedBoards = Cache.shared.getBoards() { completion(.success(cachedBoards)) return } let url = URL(string: "\(apiURL)/boards/")! var request = URLRequest(url: url) request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completion(.failure(APIError(message: "Ошибка получения досок", code: 0))) return } guard let data = data else { completion(.failure(APIError(message: "Нет данных", code: 0))) return } do { let boards = try JSONDecoder().decode([Board].self, from: data) Cache.shared.setBoards(boards) completion(.success(boards)) } catch { completion(.failure(error)) } } }.resume() } func getThreads(forBoard boardCode: String, completion: @escaping (Result<[Thread], Error>) -> Void) { if let cachedThreads = Cache.shared.getThreads(forBoard: boardCode) { completion(.success(cachedThreads)) return } let url = URL(string: "\(apiURL)/board/\(boardCode)")! var request = URLRequest(url: url) request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completion(.failure(APIError(message: "Ошибка получения тредов", code: 0))) return } guard let data = data else { completion(.failure(APIError(message: "Нет данных", code: 0))) return } do { let threads = try JSONDecoder().decode([Thread].self, from: data) Cache.shared.setThreads(threads, forBoard: boardCode) completion(.success(threads)) } catch { completion(.failure(error)) } } }.resume() } func getThreadDetail(boardCode: String, threadId: Int, completion: @escaping (Result) -> Void) { if let cachedThread = Cache.shared.getThreadDetail(forThreadId: threadId) { completion(.success(cachedThread)) return } let url = URL(string: "\(apiURL)/board/\(boardCode)/thread/\(threadId)")! var request = URLRequest(url: url) request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completion(.failure(APIError(message: "Ошибка получения треда", code: 0))) return } guard let data = data else { completion(.failure(APIError(message: "Нет данных", code: 0))) return } do { let thread = try JSONDecoder().decode(ThreadDetail.self, from: data) Cache.shared.setThreadDetail(thread, forThreadId: threadId) completion(.success(thread)) } catch { completion(.failure(error)) } } }.resume() } func getComments(boardCode: String, threadId: Int, completion: @escaping (Result<[Comment], Error>) -> Void) { if let cachedComments = Cache.shared.getComments(forThreadId: threadId) { completion(.success(cachedComments)) return } let url = URL(string: "\(apiURL)/board/\(boardCode)/thread/\(threadId)/comments")! var request = URLRequest(url: url) request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completion(.failure(APIError(message: "Ошибка получения комментариев", code: 0))) return } guard let data = data else { completion(.failure(APIError(message: "Нет данных", code: 0))) return } do { let comments = try JSONDecoder().decode([Comment].self, from: data) Cache.shared.setComments(comments, forThreadId: threadId) completion(.success(comments)) } catch { completion(.failure(error)) } } }.resume() } func getFullThread(boardCode: String, threadId: Int, completion: @escaping (Result<(ThreadDetail, [Comment]), Error>) -> Void) { let group = DispatchGroup() var threadDetail: ThreadDetail? var comments: [Comment]? var threadError: Error? var commentsError: Error? group.enter() getThreadDetail(boardCode: boardCode, threadId: threadId) { result in switch result { case .success(let detail): threadDetail = detail case .failure(let error): threadError = error } group.leave() } group.enter() getComments(boardCode: boardCode, threadId: threadId) { result in switch result { case .success(let commentList): comments = commentList case .failure(let error): commentsError = error } group.leave() } group.notify(queue: .main) { if let threadError = threadError { completion(.failure(threadError)) return } if let commentsError = commentsError { completion(.failure(commentsError)) return } guard let detail = threadDetail, let commentList = comments else { completion(.failure(APIError(message: "Не удалось загрузить данные", code: 0))) return } completion(.success((detail, commentList))) } } func createThread(boardCode: String, title: String, text: String, passcode: String, completion: @escaping (Error?) -> Void) { if !passcode.isEmpty { loginWithPasscode(passcode: passcode) { error in if let error = error { completion(error) return } self.performCreateThread(boardCode: boardCode, title: title, text: text, completion: completion) } } else { performCreateThread(boardCode: boardCode, title: title, text: text, completion: completion) } } private func performCreateThread(boardCode: String, title: String, text: String, completion: @escaping (Error?) -> Void) { let formURL = "\(baseURL)/boards/board/\(boardCode)/new" let url = URL(string: formURL)! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(error) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completion(APIError(message: "Ошибка получения формы", code: 0)) return } guard let data = data, let html = String(data: data, encoding: .utf8) else { completion(APIError(message: "Ошибка чтения формы", code: 0)) return } let csrfToken = self.extractCSRFToken(from: html) guard !csrfToken.isEmpty else { completion(APIError(message: "Не удалось извлечь CSRF токен", code: 0)) return } var formData = URLComponents() formData.queryItems = [ URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken), URLQueryItem(name: "title", value: title), URLQueryItem(name: "text", value: text) ] var postRequest = URLRequest(url: url) postRequest.httpMethod = "POST" postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") postRequest.setValue(formURL, forHTTPHeaderField: "Referer") postRequest.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") postRequest.httpBody = formData.query?.data(using: .utf8) self.session.dataTask(with: postRequest) { _, postResponse, postError in DispatchQueue.main.async { if let postError = postError { completion(postError) return } guard let postHttpResponse = postResponse as? HTTPURLResponse, (postHttpResponse.statusCode == 200 || postHttpResponse.statusCode == 302) else { completion(APIError(message: "Ошибка создания треда", code: 0)) return } Cache.shared.delete("threads_\(boardCode)") completion(nil) } }.resume() } }.resume() } func addComment(boardCode: String, threadId: Int, text: String, passcode: String, completion: @escaping (Error?) -> Void) { if !passcode.isEmpty { loginWithPasscode(passcode: passcode) { error in if let error = error { completion(error) return } self.performAddComment(boardCode: boardCode, threadId: threadId, text: text, completion: completion) } } else { performAddComment(boardCode: boardCode, threadId: threadId, text: text, completion: completion) } } private func performAddComment(boardCode: String, threadId: Int, text: String, completion: @escaping (Error?) -> Void) { let formURL = "\(baseURL)/boards/board/\(boardCode)/thread/\(threadId)/comment" let url = URL(string: formURL)! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(error) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completion(APIError(message: "Ошибка получения формы", code: 0)) return } guard let data = data, let html = String(data: data, encoding: .utf8) else { completion(APIError(message: "Ошибка чтения формы", code: 0)) return } let csrfToken = self.extractCSRFToken(from: html) guard !csrfToken.isEmpty else { completion(APIError(message: "Не удалось извлечь CSRF токен", code: 0)) return } var formData = URLComponents() formData.queryItems = [ URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken), URLQueryItem(name: "text", value: text) ] var postRequest = URLRequest(url: url) postRequest.httpMethod = "POST" postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") postRequest.setValue(formURL, forHTTPHeaderField: "Referer") postRequest.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") postRequest.httpBody = formData.query?.data(using: .utf8) self.session.dataTask(with: postRequest) { _, postResponse, postError in DispatchQueue.main.async { if let postError = postError { completion(postError) return } guard let postHttpResponse = postResponse as? HTTPURLResponse, (postHttpResponse.statusCode == 200 || postHttpResponse.statusCode == 302) else { completion(APIError(message: "Ошибка добавления комментария", code: 0)) return } Cache.shared.delete("comments_\(threadId)") completion(nil) } }.resume() } }.resume() } private func extractCSRFToken(from html: String) -> String { let pattern = #"name=['"]csrfmiddlewaretoken['"]\s+value=['"]([^'"]+)['"]"# let regex = try? NSRegularExpression(pattern: pattern) guard let match = regex?.firstMatch(in: html, range: NSRange(html.startIndex..., in: html)) else { return "" } guard let range = Range(match.range(at: 1), in: html) else { return "" } return String(html[range]) } func checkNewThreads(forBoard boardCode: String, lastKnownThreadId: Int, completion: @escaping (Result<[Thread], Error>) -> Void) { let url = URL(string: "\(apiURL)/board/\(boardCode)")! var request = URLRequest(url: url) request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { completion(.failure(APIError(message: "Ошибка получения тредов", code: 0))) return } guard let data = data else { completion(.failure(APIError(message: "Нет данных", code: 0))) return } do { let threads = try JSONDecoder().decode([Thread].self, from: data) let newThreads = threads.filter { $0.id > lastKnownThreadId } completion(.success(newThreads)) } catch { completion(.failure(error)) } } }.resume() } }