Ferrite-backup/Ferrite/API/PremiumizeWrapper.swift
kingbri 9f54397b77 Library: Add support for Premiumize cloud
Add the ability to view a user's Premiumize files in Ferrite. Files
can be deleted from a user's account directly in Ferrite's list.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-01-03 14:22:20 -05:00

234 lines
8.3 KiB
Swift

//
// PremiumizeWrapper.swift
// Ferrite
//
// Created by Brian Dashore on 11/28/22.
//
import Foundation
import KeychainSwift
public class Premiumize {
let jsonDecoder = JSONDecoder()
let keychain = KeychainSwift()
let baseAuthUrl = "https://www.premiumize.me/authorize"
let baseApiUrl = "https://www.premiumize.me/api"
let clientId = "791565696"
public func buildAuthUrl() throws -> URL {
var urlComponents = URLComponents(string: baseAuthUrl)!
urlComponents.queryItems = [
URLQueryItem(name: "client_id", value: clientId),
URLQueryItem(name: "response_type", value: "token"),
URLQueryItem(name: "state", value: UUID().uuidString)
]
if let url = urlComponents.url {
return url
} else {
throw PMError.InvalidUrl
}
}
public func handleAuthCallback(url: URL) throws {
let callbackComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let callbackFragment = callbackComponents?.fragment else {
throw PMError.InvalidResponse
}
var fragmentComponents = URLComponents()
fragmentComponents.query = callbackFragment
guard let accessToken = fragmentComponents.queryItems?.first(where: { $0.name == "access_token" })?.value else {
throw PMError.InvalidToken
}
keychain.set(accessToken, forKey: "Premiumize.AccessToken")
}
// Clears tokens. No endpoint to deregister a device
public func deleteTokens() {
keychain.delete("Premiumize.AccessToken")
}
// Wrapper request function which matches the responses and returns data
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
guard let token = keychain.get("Premiumize.AccessToken") else {
throw PMError.InvalidToken
}
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw PMError.FailedRequest(description: "No HTTP response given")
}
if response.statusCode >= 200, response.statusCode <= 299 {
return data
} else if response.statusCode == 401 {
deleteTokens()
throw PMError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.")
} else {
throw PMError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
}
}
// Function to divide and execute cache endpoint requests in parallel
// Calls this for 100 hashes at a time due to API limits
public func divideCacheRequests(magnets: [Magnet]) async throws -> [Magnet] {
let availableMagnets = try await withThrowingTaskGroup(of: [Magnet].self) { group in
for chunk in magnets.chunked(into: 100) {
group.addTask {
try await self.checkCache(magnets: chunk)
}
}
var chunkedMagnets: [Magnet] = []
for try await magnetArray in group {
chunkedMagnets += magnetArray
}
return chunkedMagnets
}
return availableMagnets
}
// Parent function for initial checking of the cache
func checkCache(magnets: [Magnet]) async throws -> [Magnet] {
var urlComponents = URLComponents(string: "\(baseApiUrl)/cache/check")!
urlComponents.queryItems = magnets.map { URLQueryItem(name: "items[]", value: $0.hash) }
guard let url = urlComponents.url else {
throw PMError.InvalidUrl
}
var request = URLRequest(url: url)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(CacheCheckResponse.self, from: data)
if rawResponse.response.isEmpty {
throw PMError.EmptyData
} else {
let availableMagnets = magnets.enumerated().compactMap { index, magnet in
if rawResponse.response[safe: index] == true {
return magnet
} else {
return nil
}
}
return availableMagnets
}
}
// Function to divide and execute DDL endpoint requests in parallel
// Calls this for 10 requests at a time to not overwhelm API servers
public func divideDDLRequests(magnetChunk: [Magnet]) async throws -> [IA] {
let tempIA = try await withThrowingTaskGroup(of: Premiumize.IA.self) { group in
for magnet in magnetChunk {
group.addTask {
try await self.fetchDDL(magnet: magnet)
}
}
var chunkedIA: [Premiumize.IA] = []
for try await ia in group {
chunkedIA.append(ia)
}
return chunkedIA
}
return tempIA
}
// Grabs DDL links
func fetchDDL(magnet: Magnet) async throws -> IA {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/directdl")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var bodyComponents = URLComponents()
bodyComponents.queryItems = [URLQueryItem(name: "src", value: magnet.link)]
request.httpBody = bodyComponents.query?.data(using: .utf8)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(DDLResponse.self, from: data)
if !rawResponse.content.isEmpty {
let files = rawResponse.content.map { file in
IAFile(
name: file.path.split(separator: "/").last.flatMap { String($0) } ?? file.path,
streamUrlString: file.link
)
}
return IA(
hash: magnet.hash,
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: files
)
} else {
throw PMError.EmptyData
}
}
func createTransfer(magnetLink: String) async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/create")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var bodyComponents = URLComponents()
bodyComponents.queryItems = [URLQueryItem(name: "src", value: magnetLink)]
request.httpBody = bodyComponents.query?.data(using: .utf8)
try await performRequest(request: &request, requestName: #function)
}
func userItems() async throws -> [UserItem] {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/listall")!)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(AllItemsResponse.self, from: data)
if rawResponse.files.isEmpty {
throw PMError.EmptyData
}
return rawResponse.files
}
func itemDetails(itemID: String) async throws -> ItemDetailsResponse {
var urlComponents = URLComponents(string: "\(baseApiUrl)/item/details")!
urlComponents.queryItems = [URLQueryItem(name: "id", value: itemID)]
guard let url = urlComponents.url else {
throw PMError.InvalidUrl
}
var request = URLRequest(url: url)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(ItemDetailsResponse.self, from: data)
return rawResponse
}
func deleteItem(itemID: String) async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/delete")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var bodyComponents = URLComponents()
bodyComponents.queryItems = [URLQueryItem(name: "id", value: itemID)]
request.httpBody = bodyComponents.query?.data(using: .utf8)
try await performRequest(request: &request, requestName: #function)
}
}