From 8d268fad188ce84ab00f9f825abebc64fad8e15a Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Aug 2018 16:14:04 -0400 Subject: [PATCH] Start OAuth --- .gitmodules | 2 +- MastodonKit | 2 +- Tusker.xcodeproj/project.pbxproj | 12 +++ Tusker/AppDelegate.swift | 56 ++++++++++- Tusker/Controllers/MastodonController.swift | 95 +++++++++++-------- Tusker/Info.plist | 13 +++ Tusker/LocalData.swift | 80 ++++++++++++++++ Tusker/Storyboards/Onboarding.storyboard | 68 +++++++++++++ .../OnboardingViewController.swift | 93 ++++++++++++++++++ .../StatusesTableViewController.swift | 1 + 10 files changed, 379 insertions(+), 43 deletions(-) create mode 100644 Tusker/LocalData.swift create mode 100644 Tusker/Storyboards/Onboarding.storyboard create mode 100644 Tusker/View Controllers/OnboardingViewController.swift diff --git a/.gitmodules b/.gitmodules index d364a01a..4a73daed 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "MastodonKit"] path = MastodonKit - url = git://github.com/MastodonKit/MastodonKit.git + url = git://github.com/shadowfacts/MastodonKit.git [submodule "SwiftSoup"] path = SwiftSoup url = git://github.com/scinfu/SwiftSoup.git diff --git a/MastodonKit b/MastodonKit index 43144bf8..6a03c64b 160000 --- a/MastodonKit +++ b/MastodonKit @@ -1 +1 @@ -Subproject commit 43144bf87cea83c81ad899fc0488be3eae645a01 +Subproject commit 6a03c64b6788faf5915c2918d429e5031af04fe6 diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 7cd2edd2..07b7556e 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; + D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */; }; + D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; D6BED16F212663DA00F02DA0 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; }; @@ -57,6 +60,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; + D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = ""; }; + D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -139,6 +145,7 @@ isa = PBXGroup; children = ( D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, + D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D6F953F121251A2F00CF0F2B /* Controllers */, D6F953E9212519B800CF0F2B /* View Controllers */, D6BED1722126661300F02DA0 /* Views */, @@ -173,6 +180,7 @@ children = ( D6D4DDD1212518A000E1C4BB /* ViewController.swift */, D6F953EB212519E700CF0F2B /* StatusesTableViewController.swift */, + D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -182,6 +190,7 @@ children = ( D6D4DDD3212518A000E1C4BB /* Main.storyboard */, D6F953ED21251A0700CF0F2B /* Statuses.storyboard */, + D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */, ); path = Storyboards; sourceTree = ""; @@ -300,6 +309,7 @@ buildActionMask = 2147483647; files = ( D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */, + D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */, D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */, D6F953EE21251A0700CF0F2B /* Statuses.storyboard in Resources */, D6D4DDD5212518A000E1C4BB /* Main.storyboard in Resources */, @@ -330,6 +340,8 @@ D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */, D6D4DDD2212518A000E1C4BB /* ViewController.swift in Sources */, + D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, + D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D6F953EC212519E700CF0F2B /* StatusesTableViewController.swift in Sources */, D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */, ); diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index d930cd64..5155880e 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -13,15 +13,42 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - MastodonController.shared.connect() +// MastodonController.shared.connect() + + if LocalData.shared.hasLaunchedBefore { + MastodonController.shared.createClient() { + } + } else { + showOnboarding() + } return true } + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false } + + print("opened with url: \(url)") + + if components.host == "oauth" { + let code = components.queryItems?.first { + $0.name == "code" + } + if let authCode = code?.value { +// LocalData.shared.refreshToken = refreshToken + MastodonController.shared.authorize(authorizationCode: authCode) { + } + } + return true + } else { + return false + } + + } + func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. @@ -44,6 +71,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } - +} + +extension AppDelegate: OnboardingViewControllerDelegate { + + func showOnboarding() { + if let window = self.window, + let onboardingViewController = UIStoryboard(name: "Onboarding", bundle: nil).instantiateInitialViewController() as? OnboardingViewController { + + onboardingViewController.delegate = self + window.makeKeyAndVisible() + window.rootViewController?.present(onboardingViewController, animated: true, completion: nil) + } + } + + func hideOnboarding() { + if let window = UIApplication.shared.keyWindow { + window.rootViewController?.dismiss(animated: true, completion: nil) + } + } + + func didFinishOnboarding() { + hideOnboarding() + } + } diff --git a/Tusker/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index 5e48a338..70b3a753 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -13,62 +13,81 @@ class MastodonController { static let shared = MastodonController() - var userDefaults = UserDefaults() +// var userDefaults = UserDefaults() var client: Client! - lazy var clientID: String? = self.userDefaults.string(forKey: "clientID") - lazy var clientSecret: String? = self.userDefaults.string(forKey: "clientSecret") - - lazy var accessToken: String? = self.userDefaults.string(forKey: "accessToken") +// lazy var clientID: String? = self.userDefaults.string(forKey: "clientID") +// lazy var clientSecret: String? = self.userDefaults.string(forKey: "clientSecret") +// +// lazy var accessToken: String? = self.userDefaults.string(forKey: "accessToken") private init() { } - func connect() { - let url = ProcessInfo.processInfo.environment["mastodon_url"]! + func createClient(completion: @escaping () -> Void) { + guard let url = LocalData.shared.instanceURL else { fatalError("Can't connect without instance URL") } - if let accessToken = accessToken { - client = Client(baseURL: url, accessToken: accessToken) + client = Client(baseURL: url) + + if let refreshToken = LocalData.shared.refreshToken { +// client.accessToken = accessToken +// completion() + authorize(authorizationCode: refreshToken, completion: completion) } else { - client = Client(baseURL: url) - - login() + register(completion: completion) } } private func register(completion: @escaping () -> Void) { - if clientID != nil, - clientSecret != nil { - completion() - } else { - let registerRequest = Clients.register(clientName: "Tusker", scopes: [.read, .write, .follow]) - - client.run(registerRequest) { result in - guard case let .success(application, _) = result else { fatalError() } - self.clientID = application.clientID - self.clientSecret = application.clientSecret - self.userDefaults.set(self.clientID, forKey: "clientID") - self.userDefaults.set(self.clientSecret, forKey: "clientSecret") + guard LocalData.shared.clientID == nil, + LocalData.shared.clientSecret == nil else { completion() - } + return + } + + let registerRequest = Clients.register(clientName: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) + + client.run(registerRequest) { result in + guard case let .success(application, _) = result else { fatalError() } + LocalData.shared.clientID = application.clientID + LocalData.shared.clientSecret = application.clientSecret + completion() } } - private func login() { - // TODO: OAuth - let username = ProcessInfo.processInfo.environment["mastodon_username"]! - let password = ProcessInfo.processInfo.environment["mastodon_password"]! - - register() { - let loginReq = Login.silent(clientID: self.clientID!, clientSecret: self.clientSecret!, scopes: [.read, .write, .follow], username: username, password: password) - - self.client.run(loginReq) { result in - guard case let .success(loginSettings, _) = result else { fatalError() } - self.accessToken = loginSettings.accessToken - self.userDefaults.set(self.accessToken, forKey: "accessToken") - } + func authorize(authorizationCode: String, completion: @escaping () -> Void) { +// let parameters = [ +// Parameter(name: "client_id", value: LocalData.shared.clientID), +// Parameter(name: "client_secret", value: LocalData.shared.clientSecret), +// Parameter(name: "grant_type", value: "refresh_token"), +// Parameter(name: "refresh_token", value: LocalData.shared.refreshToken) +// ] +// let method = HTTPMethod.post(.parameters(parameters)) + let authorizeRequest = Login.authorize(code: authorizationCode, clientID: LocalData.shared.clientID!, clientSecret: LocalData.shared.clientSecret!) + client.run(authorizeRequest) { result in + guard case let .success(settings, _) = result else { fatalError() } + LocalData.shared.refreshToken = settings.refreshToken + LocalData.shared.accessToken = settings.accessToken + self.client.accessToken = settings.accessToken + completion() } } +// private func login() { +// // TODO: OAuth +// let username = ProcessInfo.processInfo.environment["mastodon_username"]! +// let password = ProcessInfo.processInfo.environment["mastodon_password"]! +// +// register() { +// let loginReq = Login.silent(clientID: self.clientID!, clientSecret: self.clientSecret!, scopes: [.read, .write, .follow], username: username, password: password) +// +// self.client.run(loginReq) { result in +// guard case let .success(loginSettings, _) = result else { fatalError() } +// self.accessToken = loginSettings.accessToken +// self.userDefaults.set(self.accessToken, forKey: "accessToken") +// } +// } +// } + } diff --git a/Tusker/Info.plist b/Tusker/Info.plist index 89d7858b..d629a297 100644 --- a/Tusker/Info.plist +++ b/Tusker/Info.plist @@ -16,6 +16,19 @@ APPL CFBundleShortVersionString 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + net.shadowfacts.Tusker + CFBundleURLSchemes + + tusker + + + CFBundleVersion 1 LSRequiresIPhoneOS diff --git a/Tusker/LocalData.swift b/Tusker/LocalData.swift new file mode 100644 index 00000000..7a7f8b58 --- /dev/null +++ b/Tusker/LocalData.swift @@ -0,0 +1,80 @@ +// +// LocalData.swift +// Tusker +// +// Created by Shadowfacts on 8/18/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +class LocalData { + + static let shared = LocalData() + + let defaults = UserDefaults() + + private let hasLaunchedBeforeKey = "hasLaunchedBefore" + var hasLaunchedBefore: Bool { + get { + return defaults.bool(forKey: hasLaunchedBeforeKey) + } + set { + defaults.set(newValue, forKey: hasLaunchedBeforeKey) + } + } + + private let instanceURLKey = "instanceURL" + var instanceURL: String? { + get { + return defaults.string(forKey: instanceURLKey) + } + set { + defaults.set(newValue, forKey: instanceURLKey) + } + } + + private let clientIDKey = "clientID" + var clientID: String? { + get { + return defaults.string(forKey: clientIDKey) + } + set { + defaults.set(newValue, forKey: clientIDKey) + } + } + + private let clientSecretKey = "clientSecret" + var clientSecret: String? { + get { + return defaults.string(forKey: clientSecretKey) + } + set { + defaults.set(newValue, forKey: clientSecretKey) + } + } + + private let refreshTokenKey = "refreshToken" + var refreshToken: String? { + get { + return defaults.string(forKey: refreshTokenKey) + } + set { + defaults.set(newValue, forKey: refreshTokenKey) + } + } + + private let accessTokenKey = "accessToken" + var accessToken: String? { + get { + return defaults.string(forKey: accessTokenKey) + } + set { + defaults.set(newValue, forKey: accessTokenKey) + } + } + + private init() { + } + +} diff --git a/Tusker/Storyboards/Onboarding.storyboard b/Tusker/Storyboards/Onboarding.storyboard new file mode 100644 index 00000000..e1bd4d4d --- /dev/null +++ b/Tusker/Storyboards/Onboarding.storyboard @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/View Controllers/OnboardingViewController.swift b/Tusker/View Controllers/OnboardingViewController.swift new file mode 100644 index 00000000..dfc115f2 --- /dev/null +++ b/Tusker/View Controllers/OnboardingViewController.swift @@ -0,0 +1,93 @@ +// +// OnboardingViewController.swift +// Tusker +// +// Created by Shadowfacts on 8/18/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit +import AuthenticationServices + +protocol OnboardingViewControllerDelegate { + + func didFinishOnboarding() + +} + +class OnboardingViewController: UIViewController { + + var delegate: OnboardingViewControllerDelegate? + + @IBOutlet weak var urlTextField: UITextField! + + var authenticationSession: ASWebAuthenticationSession? + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func loginPressed(_ sender: Any) { + guard let text = urlTextField.text, + var components = URLComponents(string: text) else { return } + + LocalData.shared.instanceURL = text + MastodonController.shared.createClient { + let clientID = LocalData.shared.clientID! + + let callbackURL = "tusker://oauth" + + components.path = "/oauth/authorize" + components.queryItems = [ + URLQueryItem(name: "client_id", value: clientID), + URLQueryItem(name: "response_type", value: "code"), + URLQueryItem(name: "scope", value: "read write follow"), + URLQueryItem(name: "redirect_uri", value: callbackURL) + ] + let url = components.url! + + print("oauth url: \(url)") + + DispatchQueue.main.async { + self.delegate?.didFinishOnboarding() + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + +// self.delegate?.didFinishOnboarding() +// self.authenticationSession = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURL) { url, error in +// guard error == nil, +// let url = url, +// let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { fatalError() } +// +// print("callback url: \(url)") +// +// let item = components.queryItems?.first { $0.name == "code" } +// if let accessToken = item?.value { +// LocalData.shared.accessToken = accessToken +// MastodonController.shared.client.accessToken = accessToken +// self.delegate?.didFinishOnboarding() +// self.authenticationSession = nil +// } +// } +// self.authenticationSession!.start() + } + } + + @IBAction func clearDataPressed(_ sender: Any) { + LocalData.shared.instanceURL = nil + LocalData.shared.clientID = nil + LocalData.shared.clientSecret = nil + LocalData.shared.refreshToken = nil + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/Tusker/View Controllers/StatusesTableViewController.swift b/Tusker/View Controllers/StatusesTableViewController.swift index 2280ace8..1137142b 100644 --- a/Tusker/View Controllers/StatusesTableViewController.swift +++ b/Tusker/View Controllers/StatusesTableViewController.swift @@ -24,6 +24,7 @@ class StatusesTableViewController: UITableViewController { var older: RequestRange? override func viewWillAppear(_ animated: Bool) { + guard MastodonController.shared.client?.accessToken != nil else { return } MastodonController.shared.client.run(Timelines.home()) { result in guard case let .success(statuses, pagination) = result else { fatalError() } self.statuses = statuses