mirror of
				https://github.com/yattee/yattee.git
				synced 2025-10-31 20:52:04 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			266 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			266 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
| import Alamofire
 | |
| import Foundation
 | |
| import Siesta
 | |
| import SwiftUI
 | |
| 
 | |
| final class AccountValidator: Service {
 | |
|     let app: Binding<VideosApp?>
 | |
|     let url: String
 | |
|     let account: Account!
 | |
| 
 | |
|     var formObjectID: Binding<String>
 | |
|     var isValid: Binding<Bool>
 | |
|     var isValidated: Binding<Bool>
 | |
|     var isValidating: Binding<Bool>
 | |
|     var error: Binding<String?>?
 | |
| 
 | |
|     private var appsToValidateInstance = VideosApp.allCases
 | |
| 
 | |
|     init(
 | |
|         app: Binding<VideosApp?>,
 | |
|         url: String,
 | |
|         account: Account? = nil,
 | |
|         id: Binding<String>,
 | |
|         isValid: Binding<Bool>,
 | |
|         isValidated: Binding<Bool>,
 | |
|         isValidating: Binding<Bool>,
 | |
|         error: Binding<String?>? = 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=(?<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")
 | |
|     }
 | |
| }
 | 
