MastoSearch/MastoSearch/AppDelegate.swift

195 lines
7.0 KiB
Swift

//
// 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<Void, Never>()
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!
}
}