Start OAuth

This commit is contained in:
Shadowfacts 2018-08-19 16:14:04 -04:00
parent 8526613189
commit 8d268fad18
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
10 changed files with 379 additions and 43 deletions

2
.gitmodules vendored
View File

@ -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

@ -1 +1 @@
Subproject commit 43144bf87cea83c81ad899fc0488be3eae645a01
Subproject commit 6a03c64b6788faf5915c2918d429e5031af04fe6

View File

@ -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 = "<group>"; };
D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>";
@ -182,6 +190,7 @@
children = (
D6D4DDD3212518A000E1C4BB /* Main.storyboard */,
D6F953ED21251A0700CF0F2B /* Statuses.storyboard */,
D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */,
);
path = Storyboards;
sourceTree = "<group>";
@ -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 */,
);

View File

@ -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()
}
}

View File

@ -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")
// }
// }
// }
}

View File

@ -16,6 +16,19 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>net.shadowfacts.Tusker</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tusker</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>

80
Tusker/LocalData.swift Normal file
View File

@ -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() {
}
}

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.13.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="xbJ-7P-Ihc">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.9"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Onboarding View Controller-->
<scene sceneID="3hu-e1-Uan">
<objects>
<viewController id="xbJ-7P-Ihc" customClass="OnboardingViewController" customModule="Tusker" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="ZMe-1q-zVz">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="aEz-P4-y4f">
<rect key="frame" x="41.5" y="248.5" width="292.5" height="170.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enter your instance URL to get started" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4h2-ae-nVd">
<rect key="frame" x="0.0" y="0.0" width="292.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="https://mastodon.social" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="UEc-G9-MKo">
<rect key="frame" x="0.0" y="40.5" width="292.5" height="30"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="D3p-9x-aDh">
<rect key="frame" x="0.0" y="90.5" width="292.5" height="30"/>
<state key="normal" title="Login"/>
<connections>
<action selector="loginPressed:" destination="xbJ-7P-Ihc" eventType="touchUpInside" id="Fs9-m3-flX"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="peM-5x-Puc">
<rect key="frame" x="0.0" y="140.5" width="292.5" height="30"/>
<state key="normal" title="Clear Data"/>
<connections>
<action selector="clearDataPressed:" destination="xbJ-7P-Ihc" eventType="touchUpInside" id="qjw-j7-p7m"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="aEz-P4-y4f" firstAttribute="centerY" secondItem="ZMe-1q-zVz" secondAttribute="centerY" id="KG2-wQ-pZk"/>
<constraint firstItem="aEz-P4-y4f" firstAttribute="centerX" secondItem="ZMe-1q-zVz" secondAttribute="centerX" id="M0Y-OB-8Rx"/>
</constraints>
<viewLayoutGuide key="safeArea" id="lgk-Lx-3MP"/>
</view>
<connections>
<outlet property="urlTextField" destination="UEc-G9-MKo" id="ui8-wT-DaN"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="ilw-Uq-lDu" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-257" y="81"/>
</scene>
</scenes>
</document>

View File

@ -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.
}
*/
}

View File

@ -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