MobileMkch-iOS/MobileMkch/APIClient.swift
2025-08-07 13:27:12 +03:00

562 lines
24 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ThreadDetail, Error>) -> 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()
}
}