mirror of
https://github.com/yattee/yattee.git
synced 2025-08-09 20:24:06 +00:00
@@ -5,13 +5,12 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
static var bridge = AccountsBridge()
|
||||
|
||||
let id: String
|
||||
let app: VideosApp
|
||||
var app: VideosApp?
|
||||
let instanceID: String?
|
||||
var name: String?
|
||||
let url: String
|
||||
let username: String
|
||||
let password: String?
|
||||
var token: String?
|
||||
var username: String
|
||||
var password: String?
|
||||
let anonymous: Bool
|
||||
let country: String?
|
||||
let region: String?
|
||||
@@ -24,7 +23,6 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
url: String? = nil,
|
||||
username: String? = nil,
|
||||
password: String? = nil,
|
||||
token: String? = nil,
|
||||
anonymous: Bool = false,
|
||||
country: String? = nil,
|
||||
region: String? = nil
|
||||
@@ -32,19 +30,26 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
self.anonymous = anonymous
|
||||
|
||||
self.id = id ?? (anonymous ? "anonymous-\(instanceID ?? url ?? UUID().uuidString)" : UUID().uuidString)
|
||||
self.app = app ?? .invidious
|
||||
self.instanceID = instanceID
|
||||
self.name = name
|
||||
self.url = url ?? ""
|
||||
self.username = username ?? ""
|
||||
self.token = token
|
||||
self.password = password ?? ""
|
||||
self.country = country
|
||||
self.region = region
|
||||
self.app = app ?? instance.app
|
||||
}
|
||||
|
||||
var token: String? {
|
||||
KeychainModel.shared.getAccountKey(self, "token")
|
||||
}
|
||||
|
||||
var credentials: (String?, String?) {
|
||||
AccountsModel.getCredentials(self)
|
||||
}
|
||||
|
||||
var instance: Instance! {
|
||||
Defaults[.instances].first { $0.id == instanceID } ?? Instance(app: app, name: url, apiURL: url)
|
||||
Defaults[.instances].first { $0.id == instanceID } ?? Instance(app: app ?? .invidious, name: url, apiURL: url)
|
||||
}
|
||||
|
||||
var isPublic: Bool {
|
||||
@@ -52,8 +57,12 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
var shortUsername: String {
|
||||
guard username.count > 10 else {
|
||||
return username
|
||||
let (username, _) = credentials
|
||||
|
||||
guard let username = username,
|
||||
username.count > 10
|
||||
else {
|
||||
return username ?? ""
|
||||
}
|
||||
|
||||
let index = username.index(username.startIndex, offsetBy: 11)
|
||||
@@ -61,7 +70,11 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
(name != nil && name!.isEmpty) ? shortUsername : name!
|
||||
guard let name = name, !name.isEmpty else {
|
||||
return shortUsername
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
|
@@ -43,14 +43,6 @@ final class AccountValidator: Service {
|
||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||
}
|
||||
|
||||
configure("/api/v1/auth/feed", requestMethods: [.get]) {
|
||||
guard self.account != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
$0.headers["Cookie"] = self.invidiousCookieHeader
|
||||
}
|
||||
|
||||
configure("/login", requestMethods: [.post]) {
|
||||
$0.headers["Content-Type"] = "application/json"
|
||||
}
|
||||
@@ -167,7 +159,8 @@ final class AccountValidator: Service {
|
||||
var accountRequest: Request? {
|
||||
switch app.wrappedValue {
|
||||
case .invidious:
|
||||
return feed.load()
|
||||
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:
|
||||
@@ -184,18 +177,10 @@ final class AccountValidator: Service {
|
||||
error?.wrappedValue = nil
|
||||
}
|
||||
|
||||
var invidiousCookieHeader: String {
|
||||
"SID=\(account.username)"
|
||||
}
|
||||
|
||||
var login: Resource {
|
||||
resource("/login")
|
||||
}
|
||||
|
||||
var feed: Resource {
|
||||
resource("/api/v1/auth/feed")
|
||||
}
|
||||
|
||||
var videoResourceBasePath: String {
|
||||
app.wrappedValue == .invidious ? "/api/v1/videos" : "/streams"
|
||||
}
|
||||
|
@@ -93,22 +93,47 @@ final class AccountsModel: ObservableObject {
|
||||
Defaults[.accounts].first { $0.id == id }
|
||||
}
|
||||
|
||||
static func add(instance: Instance, name: String, username: String, password: String? = nil) -> Account {
|
||||
let account = Account(
|
||||
instanceID: instance.id,
|
||||
name: name,
|
||||
url: instance.apiURL,
|
||||
username: username,
|
||||
password: password
|
||||
)
|
||||
static func add(instance: Instance, name: String, username: String, password: String) -> Account {
|
||||
let account = Account(instanceID: instance.id, name: name, url: instance.apiURL)
|
||||
Defaults[.accounts].append(account)
|
||||
|
||||
setCredentials(account, username: username, password: password)
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
static func remove(_ account: Account) {
|
||||
if let accountIndex = Defaults[.accounts].firstIndex(where: { $0.id == account.id }) {
|
||||
let account = Defaults[.accounts][accountIndex]
|
||||
KeychainModel.shared.removeAccountKeys(account)
|
||||
Defaults[.accounts].remove(at: accountIndex)
|
||||
}
|
||||
}
|
||||
|
||||
static func setToken(_ account: Account, _ token: String) {
|
||||
KeychainModel.shared.updateAccountKey(account, "token", token)
|
||||
}
|
||||
|
||||
static func setCredentials(_ account: Account, username: String, password: String) {
|
||||
KeychainModel.shared.updateAccountKey(account, "username", username)
|
||||
KeychainModel.shared.updateAccountKey(account, "password", password)
|
||||
}
|
||||
|
||||
static func getCredentials(_ account: Account) -> (String?, String?) {
|
||||
(
|
||||
KeychainModel.shared.getAccountKey(account, "username"),
|
||||
KeychainModel.shared.getAccountKey(account, "password")
|
||||
)
|
||||
}
|
||||
|
||||
static func removeDefaultsCredentials(_ account: Account) {
|
||||
if let accountIndex = Defaults[.accounts].firstIndex(where: { $0.id == account.id }) {
|
||||
var account = Defaults[.accounts][accountIndex]
|
||||
account.name = ""
|
||||
account.username = ""
|
||||
account.password = nil
|
||||
|
||||
Defaults[.accounts][accountIndex] = account
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import Alamofire
|
||||
import AVKit
|
||||
import Defaults
|
||||
import Foundation
|
||||
@@ -10,7 +11,12 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
@Published var account: Account!
|
||||
|
||||
@Published var validInstance = true
|
||||
@Published var signedIn = false
|
||||
|
||||
var signedIn: Bool {
|
||||
guard let account = account else { return false }
|
||||
|
||||
return !account.anonymous && !(account.token?.isEmpty ?? true)
|
||||
}
|
||||
|
||||
init(account: Account? = nil) {
|
||||
super.init()
|
||||
@@ -25,7 +31,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
func setAccount(_ account: Account) {
|
||||
self.account = account
|
||||
signedIn = false
|
||||
|
||||
validInstance = account.anonymous
|
||||
|
||||
@@ -57,28 +62,23 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
func validateSID() {
|
||||
guard !signedIn else {
|
||||
guard signedIn, !(account.token?.isEmpty ?? true) else {
|
||||
return
|
||||
}
|
||||
|
||||
feed?
|
||||
.load()
|
||||
.onSuccess { _ in
|
||||
self.signedIn = true
|
||||
}
|
||||
.onFailure { requestError in
|
||||
self.signedIn = false
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Could not connect with your account",
|
||||
message: "\(requestError.httpStatusCode ?? -1) - \(requestError.userMessage)\nIf this issue persists, try removing and adding your account again in Settings."
|
||||
)
|
||||
.onFailure { _ in
|
||||
self.updateToken(force: true)
|
||||
}
|
||||
}
|
||||
|
||||
func configure() {
|
||||
invalidateConfiguration()
|
||||
|
||||
configure {
|
||||
if !self.account.username.isEmpty {
|
||||
$0.headers["Cookie"] = self.cookieHeader
|
||||
if let cookie = self.cookieHeader {
|
||||
$0.headers["Cookie"] = cookie
|
||||
}
|
||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||
}
|
||||
@@ -170,6 +170,71 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled)
|
||||
}
|
||||
|
||||
updateToken()
|
||||
}
|
||||
|
||||
func updateToken(force: Bool = false) {
|
||||
let (username, password) = AccountsModel.getCredentials(account)
|
||||
guard !account.anonymous,
|
||||
(account.token?.isEmpty ?? true) || force
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let username = username,
|
||||
let password = password,
|
||||
!username.isEmpty,
|
||||
!password.isEmpty
|
||||
else {
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Account Error",
|
||||
message: "Remove and add your account again in Settings."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let presentTokenUpdateFailedAlert: (AFDataResponse<Data?>?, String?) -> Void = { response, message in
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Account Error",
|
||||
message: message ?? "\(response?.response?.statusCode ?? -1) - \(response?.error?.errorDescription ?? "unknown")\nIf this issue persists, try removing and adding your account again in Settings."
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
presentTokenUpdateFailedAlert(response, nil)
|
||||
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 {
|
||||
presentTokenUpdateFailedAlert(nil, "Could not extract SID from received cookies: \(cookies)")
|
||||
return
|
||||
}
|
||||
|
||||
let matchRange = match.range(withName: "sid")
|
||||
|
||||
if let substringRange = Range(matchRange, in: cookies) {
|
||||
print("updating invidious token")
|
||||
let sid = String(cookies[substringRange])
|
||||
AccountsModel.setToken(self.account, sid)
|
||||
self.configure()
|
||||
} else {
|
||||
presentTokenUpdateFailedAlert(nil, "Could not extract SID from received cookies: \(cookies)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var login: Resource {
|
||||
resource(baseURL: account.url, path: "login")
|
||||
}
|
||||
|
||||
private func pathPattern(_ path: String) -> String {
|
||||
@@ -180,8 +245,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
"\(Self.basePath)/\(path)"
|
||||
}
|
||||
|
||||
private var cookieHeader: String {
|
||||
"SID=\(account.username)"
|
||||
private var cookieHeader: String? {
|
||||
guard let token = account?.token, !token.isEmpty else { return nil }
|
||||
return "SID=\(token)"
|
||||
}
|
||||
|
||||
var popular: Resource? {
|
||||
|
@@ -109,23 +109,33 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
func updateToken() {
|
||||
guard !account.anonymous else {
|
||||
let (username, password) = AccountsModel.getCredentials(account)
|
||||
|
||||
guard !account.anonymous,
|
||||
let username = username,
|
||||
let password = password
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
account.token = nil
|
||||
|
||||
login.request(
|
||||
.post,
|
||||
json: ["username": account.username, "password": account.password]
|
||||
json: ["username": username, "password": password]
|
||||
)
|
||||
.onSuccess { response in
|
||||
self.account.token = response.json.dictionaryValue["token"]?.string ?? ""
|
||||
let token = response.json.dictionaryValue["token"]?.string ?? ""
|
||||
if let error = response.json.dictionaryValue["error"]?.string {
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Could not connect with your account",
|
||||
title: "Account Error",
|
||||
message: error
|
||||
)
|
||||
} else if !token.isEmpty {
|
||||
AccountsModel.setToken(self.account, token)
|
||||
} else {
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Account Error",
|
||||
message: "Could not update your token."
|
||||
)
|
||||
}
|
||||
|
||||
self.configure()
|
||||
|
@@ -11,10 +11,6 @@ enum VideosApp: String, CaseIterable {
|
||||
true
|
||||
}
|
||||
|
||||
var accountsUsePassword: Bool {
|
||||
self == .piped
|
||||
}
|
||||
|
||||
var supportsPopular: Bool {
|
||||
self == .invidious
|
||||
}
|
||||
|
26
Model/KeychainModel.swift
Normal file
26
Model/KeychainModel.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
import KeychainAccess
|
||||
|
||||
struct KeychainModel {
|
||||
static var shared = KeychainModel()
|
||||
|
||||
var keychain = Keychain(service: "stream.yattee.app")
|
||||
|
||||
func updateAccountKey(_ account: Account, _ key: String, _ value: String) {
|
||||
keychain[accountKey(account, key)] = value
|
||||
}
|
||||
|
||||
func getAccountKey(_ account: Account, _ key: String) -> String? {
|
||||
keychain[accountKey(account, key)]
|
||||
}
|
||||
|
||||
func accountKey(_ account: Account, _ key: String) -> String {
|
||||
"\(account.id)-\(key)"
|
||||
}
|
||||
|
||||
func removeAccountKeys(_ account: Account) {
|
||||
try? keychain.remove(accountKey(account, "token"))
|
||||
try? keychain.remove(accountKey(account, "username"))
|
||||
try? keychain.remove(accountKey(account, "password"))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user