import Alamofire import Foundation import Siesta import SwiftUI final class AccountValidator: Service { let app: Binding let url: String let account: Account! var formObjectID: Binding var isValid: Binding var isValidated: Binding var isValidating: Binding var error: Binding? private var appsToValidateInstance = VideosApp.allCases init( app: Binding, url: String, account: Account? = nil, id: Binding, isValid: Binding, isValidated: Binding, isValidating: Binding, error: Binding? = nil ) { self.app = app self.url = url self.account = account formObjectID = id self.isValid = isValid self.isValidated = isValidated self.isValidating = isValidating self.error = error super.init(baseURL: url) configure() } func configure() { configure { $0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"]) } configure("/login", requestMethods: [.post]) { $0.headers["Content-Type"] = "application/json" } } func instanceValidationResource(_ app: VideosApp) -> Resource { switch app { case .invidious: return resource("/api/v1/videos/dQw4w9WgXcQ") case .piped: return resource("/streams/dQw4w9WgXcQ") case .peerTube: // TODO: fixme return resource("") case .local: return resource("") } } func validateInstance() { reset() guard let app = appsToValidateInstance.popLast() else { return } tryValidatingUsing(app) } func tryValidatingUsing(_ app: VideosApp) { instanceValidationResource(app) .load() .onSuccess { response in guard self.url == self.formObjectID.wrappedValue else { return } guard !response.json.isEmpty else { if app == .piped { if response.text.contains("property=\"og:title\" content=\"Piped\"") { self.isValid.wrappedValue = false self.isValidated.wrappedValue = true self.isValidating.wrappedValue = false self.error?.wrappedValue = "Trying to use Piped front-end URL, you need to use URL for Piped API instead" return } } guard let nextApp = self.appsToValidateInstance.popLast() else { self.isValid.wrappedValue = false self.isValidated.wrappedValue = true self.isValidating.wrappedValue = false return } self.tryValidatingUsing(nextApp) return } let json = response.json.dictionaryValue let author = app == .invidious ? json["author"] : json["uploader"] if author == "Rick Astley" { self.app.wrappedValue = app self.isValid.wrappedValue = true self.error?.wrappedValue = nil } else { self.isValid.wrappedValue = false } self.isValidated.wrappedValue = true self.isValidating.wrappedValue = false } .onFailure { error in guard self.url == self.formObjectID.wrappedValue else { return } if self.appsToValidateInstance.isEmpty { self.isValidating.wrappedValue = false self.isValidated.wrappedValue = true self.isValid.wrappedValue = false self.error?.wrappedValue = error.userMessage } else { guard let app = self.appsToValidateInstance.popLast() else { return } self.tryValidatingUsing(app) } } } func validateAccount() { reset() switch app.wrappedValue { case .invidious: validateInvidiousAccount() case .piped: validatePipedAccount() default: setValidationResult(false) } } func validateInvidiousAccount() { guard let username = account?.username, let password = account?.password else { setValidationResult(false) return } AF .request(login.url, method: .post, parameters: ["email": username, "password": password], encoding: URLEncoding.default) .redirect(using: .doNotFollow) .response { response in guard let headers = response.response?.headers, let cookies = headers["Set-Cookie"] else { self.setValidationResult(false) return } let sidRegex = #"SID=(?[^;]*);"# guard let sidRegex = try? NSRegularExpression(pattern: sidRegex), let match = sidRegex.matches(in: cookies, range: NSRange(cookies.startIndex..., in: cookies)).first else { self.setValidationResult(false) return } let matchRange = match.range(withName: "sid") if let substringRange = Range(matchRange, in: cookies) { let sid = String(cookies[substringRange]) if !sid.isEmpty { self.setValidationResult(true) } } else { self.setValidationResult(false) } } } func validatePipedAccount() { guard let request = accountRequest else { setValidationResult(false) return } request.onSuccess { response in guard self.account!.username == self.formObjectID.wrappedValue else { return } switch self.app.wrappedValue { case .invidious: self.isValid.wrappedValue = true case .piped: let error = response.json.dictionaryValue["error"]?.string let token = response.json.dictionaryValue["token"]?.string self.isValid.wrappedValue = error?.isEmpty ?? !(token?.isEmpty ?? true) self.error!.wrappedValue = error default: return } } .onFailure { _ in guard self.account!.username == self.formObjectID.wrappedValue else { return } self.isValid.wrappedValue = false } .onCompletion { _ in self.isValidated.wrappedValue = true self.isValidating.wrappedValue = false } } func setValidationResult(_ result: Bool) { isValid.wrappedValue = result isValidated.wrappedValue = true isValidating.wrappedValue = false } var accountRequest: Siesta.Request? { switch app.wrappedValue { case .invidious: guard let password = account.password else { return nil } return login.request(.post, urlEncoded: ["email": account.username, "password": password]) case .piped: return login.request(.post, json: ["username": account.username, "password": account.password]) default: return nil } } func reset() { appsToValidateInstance = VideosApp.allCases app.wrappedValue = nil isValid.wrappedValue = false isValidated.wrappedValue = false isValidating.wrappedValue = false error?.wrappedValue = nil } var login: Resource { resource("/login") } var videoResourceBasePath: String { app.wrappedValue == .invidious ? "/api/v1/videos" : "/streams" } var neverGonnaGiveYouUp: Resource { resource("\(videoResourceBasePath)/dQw4w9WgXcQ") } }