// // Mastodon+API+OAuth.swift // // // Created by MainasuK Cirno on 2021/1/27. // import Foundation import Combine extension Mastodon.API.OAuth { public static let authorizationField = "Authorization" public struct Authorization { public let accessToken: String public init(accessToken: String) { self.accessToken = accessToken } } } extension Mastodon.API.OAuth { static func authorizeEndpointURL(domain: String) -> URL { return Mastodon.API.oauthEndpointURL(domain: domain).appendingPathComponent("authorize") } static func accessTokenEndpointURL(domain: String) -> URL { return Mastodon.API.oauthEndpointURL(domain: domain).appendingPathComponent("token") } static func revokeTokenEndpointURL(domain: String) -> URL { return Mastodon.API.oauthEndpointURL(domain: domain).appendingPathComponent("revoke") } /// Construct user authorize endpoint URL /// /// This method construct a URL for user authorize /// /// - Since: 0.0.0 /// - Version: 3.3.0 /// # Last Update /// 2021/1/29 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/apps/oauth/) /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - query: `AuthorizeQuery` public static func authorizeURL( domain: String, query: AuthorizeQuery ) -> URL { let request = Mastodon.API.get( url: authorizeEndpointURL(domain: domain), query: query, authorization: nil ) let url = request.url! return url } /// Obtain User Access Token /// /// - Since: 0.0.0 /// - Version: 3.3.0 /// # Last Update /// 2021/2/2 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/apps/oauth/) /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - query: `AccessTokenQuery` /// - Returns: `AnyPublisher` contains `Token` nested in the response public static func accessToken( session: URLSession, domain: String, query: AccessTokenQuery ) -> AnyPublisher, Error> { let request = Mastodon.API.post( url: accessTokenEndpointURL(domain: domain), query: query, authorization: nil ) return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.Token.self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } .eraseToAnyPublisher() } /// Revoke User Access Token /// /// - Since: 0.0.0 /// - Version: 3.3.0 /// # Last Update /// 2021/2/9 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/apps/oauth/) /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - query: `RevokeTokenQuery` /// - Returns: `AnyPublisher` contains `Token` nested in the response public static func revokeToken( session: URLSession, domain: String, query: RevokeTokenQuery ) -> AnyPublisher { let request = Mastodon.API.post( url: revokeTokenEndpointURL(domain: domain), query: query, authorization: nil ) return session.dataTaskPublisher(for: request) .tryMap { data, response in // `RevokeToken` returns an empty response when success, so just check whether the data type is String to avoid _ = try Mastodon.API.decode(type: String.self, from: data, response: response) } .eraseToAnyPublisher() } } extension Mastodon.API.OAuth { public struct AuthorizeQuery: Codable, GetQuery { public let forceLogin: String? public let responseType: String public let clientID: String public let redirectURI: String public let scope: String? public init( forceLogin: String? = nil, responseType: String = "code", clientID: String, redirectURI: String, scope: String? = "read write follow push" ) { self.forceLogin = forceLogin self.responseType = responseType self.clientID = clientID self.redirectURI = redirectURI self.scope = scope } var queryItems: [URLQueryItem]? { var items: [URLQueryItem] = [] forceLogin.flatMap { items.append(URLQueryItem(name: "force_login", value: $0)) } items.append(URLQueryItem(name: "response_type", value: responseType)) items.append(URLQueryItem(name: "client_id", value: clientID)) items.append(URLQueryItem(name: "redirect_uri", value: redirectURI)) scope.flatMap { items.append(URLQueryItem(name: "scope", value: $0)) } guard !items.isEmpty else { return nil } return items } } public struct AccessTokenQuery: Codable, PostQuery { public init( clientID: String, clientSecret: String, redirectURI: String, scope: String? = "read write follow push", code: String?, grantType: String ) { self.clientID = clientID self.clientSecret = clientSecret self.redirectURI = redirectURI self.scope = scope self.code = code self.grantType = grantType } public let clientID: String public let clientSecret: String public let redirectURI: String public let scope: String? public let code: String? public let grantType: String enum CodingKeys: String, CodingKey { case clientID = "client_id" case clientSecret = "client_secret" case redirectURI = "redirect_uri" case scope case code case grantType = "grant_type" } } public struct RevokeTokenQuery: Codable, PostQuery { public let clientID: String public let clientSecret: String public let token: String enum CodingKeys: String, CodingKey { case clientID = "client_id" case clientSecret = "client_secret" case token } } }