// // AppDelegate.swift // MastoSearch // // Created by Shadowfacts on 12/10/21. // import Cocoa import UniformTypeIdentifiers import AuthenticationServices import Combine import OSLog @main class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var accountMenu: NSMenu! let onSync = PassthroughSubject() private let syncLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Sync") private var authSession: ASWebAuthenticationSession? private var syncTotal = 0 func applicationWillFinishLaunching(_ notification: Notification) { DatabaseController.shared.initialize() updateAccountMenu() } func applicationDidFinishLaunching(_ aNotification: Notification) { syncStatuses() } func applicationWillTerminate(_ aNotification: Notification) { DatabaseController.shared.close() } func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } private func updateAccountMenu() { accountMenu.removeAllItems() if let account = LocalData.account { let item = accountMenu.addItem(withTitle: "Logged in to \(account.instanceURL.host!)", action: nil, keyEquivalent: "") item.isEnabled = false accountMenu.addItem(withTitle: "Log out", action: #selector(logOut), keyEquivalent: "") } else { accountMenu.addItem(withTitle: "Log in...", action: #selector(logIn), keyEquivalent: "") } } private func syncStatuses() { DatabaseController.shared.getNewestStatus { status in guard let status = status else { return } self.syncLogger.log("Starting sync...") self.syncTotal = 0 self.syncStatuses(range: .after(status.id)) } } private func syncStatuses(range: APIController.RequestRange) { APIController.shared.getStatuses(range: range) { response in switch response { case .failure(let error): self.syncLogger.error("Erorr syncing statuses: \(String(describing: error), privacy: .public)") DispatchQueue.main.async { let alert = NSAlert() alert.alertStyle = .warning alert.messageText = "Error syncing statuses" alert.informativeText = error.localizedDescription alert.runModal() } case .success(let statuses): guard statuses.count > 0 else { DispatchQueue.main.async { self.syncLogger.log("Finished sync of \(self.syncTotal, privacy: .public) statuses") self.onSync.send() } return } DatabaseController.shared.addStatuses(statuses.compactMap { if $0.hasReblog { return nil } else { return Status(id: $0.id, url: $0.url, summary: $0.spoiler_text, content: $0.content, published: $0.created_at) } }) self.syncTotal += statuses.count self.syncStatuses(range: .after(statuses.first!.id)) } } } @IBAction func importFile(_ sender: Any) { let panel = NSOpenPanel() panel.canChooseFiles = true panel.canChooseDirectories = false panel.allowsMultipleSelection = false panel.allowedContentTypes = [.commaSeparatedText] panel.beginSheetModal(for: NSApp.mainWindow!) { (resp) in guard resp == .OK else { return } ImportController.shared.importCSV(url: panel.url!) self.onSync.send() self.syncStatuses() } } @objc func logIn() { let alert = NSAlert() alert.messageText = "Enter instance URL:" alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Cancel") let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) textField.placeholderString = "https://mastodon.social/" alert.accessoryView = textField guard alert.runModal() == .alertFirstButtonReturn, let url = URL(string: textField.stringValue) else { return } LocalData.account = LocalData.AccountInfo(instanceURL: url, clientID: nil, clientSecret: nil, accessToken: nil) APIController.shared.register { response in guard case .success(let registration) = response else { fatalError() } LocalData.account!.clientID = registration.client_id LocalData.account!.clientSecret = registration.client_secret var authorizeComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)! authorizeComponents.path = "/oauth/authorize" authorizeComponents.queryItems = [ URLQueryItem(name: "client_id", value: LocalData.account!.clientID), URLQueryItem(name: "response_type", value: "code"), URLQueryItem(name: "scope", value: APIController.shared.scopes), URLQueryItem(name: "redirect_uri", value: APIController.shared.redirectURI), ] self.authSession = ASWebAuthenticationSession(url: authorizeComponents.url!, callbackURLScheme: "mastosearch", completionHandler: { url, error in guard error == nil, let url = url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let item = components.queryItems?.first(where: { $0.name == "code" }), let authCode = item.value else { fatalError() } APIController.shared.getAccessToken(authCode: authCode) { response in guard case .success(let settings) = response else { fatalError() } LocalData.account!.accessToken = settings.access_token DispatchQueue.main.async { self.updateAccountMenu() self.syncStatuses() } } }) DispatchQueue.main.async { self.authSession!.presentationContextProvider = self self.authSession!.start() } } } @objc func logOut() { LocalData.account = nil updateAccountMenu() } } extension AppDelegate: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { return NSApp.keyWindow! } }