diff --git a/MastoSearch.xcodeproj/project.pbxproj b/MastoSearch.xcodeproj/project.pbxproj index fe76bfe..588d3e0 100644 --- a/MastoSearch.xcodeproj/project.pbxproj +++ b/MastoSearch.xcodeproj/project.pbxproj @@ -8,20 +8,14 @@ /* Begin PBXBuildFile section */ D6451241276981A40046CCD2 /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6451240276981A40046CCD2 /* WindowController.swift */; }; - D6451243276A408F0046CCD2 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6451242276A408F0046CCD2 /* LocalData.swift */; }; + D6559A5228721BAF000EEB4D /* MastoSearchCore in Frameworks */ = {isa = PBXBuildFile; productRef = D6559A5128721BAF000EEB4D /* MastoSearchCore */; }; D669039E2769236F00819C4D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D669039C2769236F00819C4D /* ViewController.swift */; }; D66903BE2769250B00819C4D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D66903BD2769250B00819C4D /* Main.storyboard */; }; D66903C127692EAB00819C4D /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D66903C027692EAB00819C4D /* SwiftSoup */; }; - D6A4B8A827C1BC5A0016F458 /* APIController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4B8A727C1BC5A0016F458 /* APIController.swift */; }; D6A4B8B027C2B1770016F458 /* MastoSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4B8AF27C2B1770016F458 /* MastoSearchTests.swift */; }; D6A4B8B727C2B18C0016F458 /* ImportControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4B8B627C2B18C0016F458 /* ImportControllerTests.swift */; }; - D6A4B8BA27C2BE330016F458 /* UInt128.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4B8B927C2BE330016F458 /* UInt128.swift */; }; D6B24DE927640CE100BA23B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B24DE827640CE100BA23B8 /* AppDelegate.swift */; }; D6B24DEB27640CE200BA23B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6B24DEA27640CE200BA23B8 /* Assets.xcassets */; }; - D6B24DF727640D2600BA23B8 /* FMDB in Frameworks */ = {isa = PBXBuildFile; productRef = D6B24DF627640D2600BA23B8 /* FMDB */; }; - D6B24DF927640DD700BA23B8 /* DatabaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B24DF827640DD700BA23B8 /* DatabaseController.swift */; }; - D6D9CFE82764196E006FE2E7 /* ImportController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9CFE72764196E006FE2E7 /* ImportController.swift */; }; - D6D9CFEA27641D4A006FE2E7 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9CFE927641D4A006FE2E7 /* Status.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,21 +30,16 @@ /* Begin PBXFileReference section */ D6451240276981A40046CCD2 /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; - D6451242276A408F0046CCD2 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D669039C2769236F00819C4D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; D66903BD2769250B00819C4D /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; - D6A4B8A727C1BC5A0016F458 /* APIController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIController.swift; sourceTree = ""; }; D6A4B8AD27C2B1770016F458 /* MastoSearchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastoSearchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D6A4B8AF27C2B1770016F458 /* MastoSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastoSearchTests.swift; sourceTree = ""; }; D6A4B8B627C2B18C0016F458 /* ImportControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportControllerTests.swift; sourceTree = ""; }; - D6A4B8B927C2BE330016F458 /* UInt128.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt128.swift; sourceTree = ""; }; D6B24DE527640CE100BA23B8 /* MastoSearch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MastoSearch.app; sourceTree = BUILT_PRODUCTS_DIR; }; D6B24DE827640CE100BA23B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D6B24DEA27640CE200BA23B8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D6B24DEF27640CE200BA23B8 /* MastoSearch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MastoSearch.entitlements; sourceTree = ""; }; - D6B24DF827640DD700BA23B8 /* DatabaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseController.swift; sourceTree = ""; }; - D6D9CFE72764196E006FE2E7 /* ImportController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportController.swift; sourceTree = ""; }; - D6D9CFE927641D4A006FE2E7 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; + D6E77D1428721A7600D8B732 /* MastoSearchCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MastoSearchCore; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -65,14 +54,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D6B24DF727640D2600BA23B8 /* FMDB in Frameworks */, D66903C127692EAB00819C4D /* SwiftSoup in Frameworks */, + D6559A5228721BAF000EEB4D /* MastoSearchCore in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + D6559A5028721BAF000EEB4D /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; D6A4B8AE27C2B1770016F458 /* MastoSearchTests */ = { isa = PBXGroup; children = ( @@ -82,20 +78,14 @@ path = MastoSearchTests; sourceTree = ""; }; - D6A4B8B827C2BE250016F458 /* Vendor */ = { - isa = PBXGroup; - children = ( - D6A4B8B927C2BE330016F458 /* UInt128.swift */, - ); - path = Vendor; - sourceTree = ""; - }; D6B24DDC27640CE100BA23B8 = { isa = PBXGroup; children = ( + D6E77D1428721A7600D8B732 /* MastoSearchCore */, D6B24DE727640CE100BA23B8 /* MastoSearch */, D6A4B8AE27C2B1770016F458 /* MastoSearchTests */, D6B24DE627640CE100BA23B8 /* Products */, + D6559A5028721BAF000EEB4D /* Frameworks */, ); sourceTree = ""; }; @@ -112,14 +102,8 @@ isa = PBXGroup; children = ( D6B24DE827640CE100BA23B8 /* AppDelegate.swift */, - D6451242276A408F0046CCD2 /* LocalData.swift */, - D6B24DF827640DD700BA23B8 /* DatabaseController.swift */, - D6D9CFE72764196E006FE2E7 /* ImportController.swift */, - D6A4B8A727C1BC5A0016F458 /* APIController.swift */, - D6D9CFE927641D4A006FE2E7 /* Status.swift */, D6451240276981A40046CCD2 /* WindowController.swift */, D669039C2769236F00819C4D /* ViewController.swift */, - D6A4B8B827C2BE250016F458 /* Vendor */, D6B24DEA27640CE200BA23B8 /* Assets.xcassets */, D66903BD2769250B00819C4D /* Main.storyboard */, D6B24DEF27640CE200BA23B8 /* MastoSearch.entitlements */, @@ -162,8 +146,8 @@ ); name = MastoSearch; packageProductDependencies = ( - D6B24DF627640D2600BA23B8 /* FMDB */, D66903C027692EAB00819C4D /* SwiftSoup */, + D6559A5128721BAF000EEB4D /* MastoSearchCore */, ); productName = MastoSearch; productReference = D6B24DE527640CE100BA23B8 /* MastoSearch.app */; @@ -246,13 +230,7 @@ files = ( D669039E2769236F00819C4D /* ViewController.swift in Sources */, D6451241276981A40046CCD2 /* WindowController.swift in Sources */, - D6B24DF927640DD700BA23B8 /* DatabaseController.swift in Sources */, D6B24DE927640CE100BA23B8 /* AppDelegate.swift in Sources */, - D6A4B8A827C1BC5A0016F458 /* APIController.swift in Sources */, - D6D9CFE82764196E006FE2E7 /* ImportController.swift in Sources */, - D6A4B8BA27C2BE330016F458 /* UInt128.swift in Sources */, - D6D9CFEA27641D4A006FE2E7 /* Status.swift in Sources */, - D6451243276A408F0046CCD2 /* LocalData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -524,16 +502,15 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + D6559A5128721BAF000EEB4D /* MastoSearchCore */ = { + isa = XCSwiftPackageProductDependency; + productName = MastoSearchCore; + }; D66903C027692EAB00819C4D /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; package = D66903BF27692EAB00819C4D /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; - D6B24DF627640D2600BA23B8 /* FMDB */ = { - isa = XCSwiftPackageProductDependency; - package = D6B24DF527640D2600BA23B8 /* XCRemoteSwiftPackageReference "fmdb" */; - productName = FMDB; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = D6B24DDD27640CE100BA23B8 /* Project object */; diff --git a/MastoSearch/AppDelegate.swift b/MastoSearch/AppDelegate.swift index 026219b..9012de4 100644 --- a/MastoSearch/AppDelegate.swift +++ b/MastoSearch/AppDelegate.swift @@ -10,18 +10,13 @@ import UniformTypeIdentifiers import AuthenticationServices import Combine import OSLog +import MastoSearchCore @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() @@ -29,7 +24,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationDidFinishLaunching(_ aNotification: Notification) { - syncStatuses() + SyncController.shared.syncStatuses(errorHandler: self.handleSyncError(_:)) } func applicationWillTerminate(_ aNotification: Notification) { @@ -51,55 +46,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - 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 @@ -111,8 +57,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { return } ImportController.shared.importCSV(url: panel.url!) - self.onSync.send() - self.syncStatuses() + SyncController.shared.onSync.send() + SyncController.shared.syncStatuses(errorHandler: self.handleSyncError(_:)) } } @@ -131,53 +77,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { 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() - } + LoginController.shared.logIn(with: url, presentationContextProvider: self) { + self.updateAccountMenu() + SyncController.shared.syncStatuses(errorHandler: self.handleSyncError(_:)) } - } @objc func logOut() { @@ -185,6 +88,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { updateAccountMenu() } + private func handleSyncError(_ error: APIController.Error) { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = "Error syncing statuses" + alert.informativeText = error.localizedDescription + alert.runModal() + } + } extension AppDelegate: ASWebAuthenticationPresentationContextProviding { diff --git a/MastoSearch/Main.storyboard b/MastoSearch/Main.storyboard index e476b19..65eb370 100644 --- a/MastoSearch/Main.storyboard +++ b/MastoSearch/Main.storyboard @@ -734,7 +734,7 @@ - + @@ -767,7 +767,7 @@ - + @@ -800,7 +800,7 @@ - + diff --git a/MastoSearch/Status.swift b/MastoSearch/Status.swift deleted file mode 100644 index 9823dd6..0000000 --- a/MastoSearch/Status.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Status.swift -// MastoSearch -// -// Created by Shadowfacts on 12/10/21. -// - -import Foundation - -struct Status: Identifiable { - let id: String - let url: String - let summary: String? - let content: String - let published: Date -} diff --git a/MastoSearch/ViewController.swift b/MastoSearch/ViewController.swift index 76e9ff4..3ebc8b1 100644 --- a/MastoSearch/ViewController.swift +++ b/MastoSearch/ViewController.swift @@ -8,6 +8,7 @@ import Cocoa import Combine import SwiftSoup +import MastoSearchCore class ViewController: NSViewController { @@ -65,7 +66,7 @@ class ViewController: NSViewController { .sink { [unowned self] in self.loadAll() } .store(in: &cancellables) - (NSApp.delegate as! AppDelegate).onSync + SyncController.shared.onSync .sink { [unowned self] in self.allStatusesSnapshot = nil self.loadAll() diff --git a/MastoSearch/WindowController.swift b/MastoSearch/WindowController.swift index bbb8a7f..30e0f43 100644 --- a/MastoSearch/WindowController.swift +++ b/MastoSearch/WindowController.swift @@ -7,6 +7,7 @@ import Cocoa import Combine +import MastoSearchCore class WindowController: NSWindowController { @@ -29,7 +30,7 @@ class WindowController: NSWindowController { } .store(in: &cancellables) - (NSApp.delegate as! AppDelegate).onSync + SyncController.shared.onSync .sink { [unowned self] in self.updateSubtitle() } .store(in: &cancellables) diff --git a/MastoSearchCore/.gitignore b/MastoSearchCore/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/MastoSearchCore/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/MastoSearchCore/Package.resolved b/MastoSearchCore/Package.resolved new file mode 100644 index 0000000..f7285a7 --- /dev/null +++ b/MastoSearchCore/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "fmdb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ccgus/fmdb.git", + "state" : { + "revision" : "61e51fde7f7aab6554f30ab061cc588b28a97d04", + "version" : "2.7.7" + } + } + ], + "version" : 2 +} diff --git a/MastoSearchCore/Package.swift b/MastoSearchCore/Package.swift new file mode 100644 index 0000000..6bde9d1 --- /dev/null +++ b/MastoSearchCore/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MastoSearchCore", + platforms: [ + .macOS(.v12), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "MastoSearchCore", + targets: ["MastoSearchCore"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + .package(url: "https://github.com/ccgus/fmdb.git", .upToNextMinor(from: "2.7.7")), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "MastoSearchCore", + dependencies: [ + .product(name: "FMDB", package: "fmdb"), + ]), + .testTarget( + name: "MastoSearchCoreTests", + dependencies: ["MastoSearchCore"]), + ] +) diff --git a/MastoSearchCore/README.md b/MastoSearchCore/README.md new file mode 100644 index 0000000..02542fa --- /dev/null +++ b/MastoSearchCore/README.md @@ -0,0 +1,3 @@ +# MastoSearchCore + +A description of this package. diff --git a/MastoSearch/APIController.swift b/MastoSearchCore/Sources/MastoSearchCore/APIController.swift similarity index 82% rename from MastoSearch/APIController.swift rename to MastoSearchCore/Sources/MastoSearchCore/APIController.swift index 8fc7759..90237fc 100644 --- a/MastoSearch/APIController.swift +++ b/MastoSearchCore/Sources/MastoSearchCore/APIController.swift @@ -5,15 +5,15 @@ // Created by Shadowfacts on 2/19/22. // -import Cocoa +import Foundation -struct APIController { +public struct APIController { - static let shared = APIController() + public static let shared = APIController() - let scopes = "read" - let redirectScheme = "mastosearch" - let redirectURI = "mastosearch://oauth" + public let scopes = "read" + public let redirectScheme = "mastosearch" + public let redirectURI = "mastosearch://oauth" private let decoder: JSONDecoder = { let decoder = JSONDecoder() @@ -71,7 +71,7 @@ struct APIController { task.resume() } - func register(completion: @escaping (Result) -> Void) { + public func register(completion: @escaping (Result) -> Void) { guard let account = LocalData.account else { return } @@ -88,7 +88,7 @@ struct APIController { run(request: req, completion: completion) } - func getAccessToken(authCode: String, completion: @escaping (Result) -> Void) { + public func getAccessToken(authCode: String, completion: @escaping (Result) -> Void) { guard let account = LocalData.account else { return } @@ -107,7 +107,7 @@ struct APIController { run(request: req, completion: completion) } - func getStatuses(range: RequestRange, completion: @escaping (Result<[Status], Error>) -> Void) { + public func getStatuses(range: RequestRange, completion: @escaping (Result<[Status], Error>) -> Void) { guard let account = LocalData.account else { return } @@ -115,6 +115,7 @@ struct APIController { components.path = "/api/v1/accounts/1/statuses" components.queryItems = range.queryParameters + [ URLQueryItem(name: "exclude_replies", value: "false"), + URLQueryItem(name: "limit", value: "50"), ] run(request: URLRequest(url: components.url!), completion: completion) } @@ -122,7 +123,7 @@ struct APIController { } extension APIController { - enum RequestRange { + public enum RequestRange { case `default` case after(String) @@ -136,24 +137,24 @@ extension APIController { } } - struct ClientRegistration: Decodable { - let client_id: String - let client_secret: String + public struct ClientRegistration: Decodable { + public let client_id: String + public let client_secret: String } - struct LoginSettings: Decodable { - let access_token: String + public struct LoginSettings: Decodable { + public let access_token: String } - struct Status: Decodable { - let id: String - let url: String - let spoiler_text: String - let content: String - let created_at: Date - let hasReblog: Bool + public struct Status: Decodable { + public let id: String + public let url: String + public let spoiler_text: String + public let content: String + public let created_at: Date + public let hasReblog: Bool - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(String.self, forKey: .id) self.url = try container.decode(String.self, forKey: .url) @@ -174,13 +175,13 @@ extension APIController { } extension APIController { - enum Error: Swift.Error { + public enum Error: Swift.Error { case unexpectedStatusCode(Int) case error(Swift.Error) case noData case decoding(Swift.Error) - var localizedDescription: String { + public var localizedDescription: String { switch self { case .unexpectedStatusCode(let code): return "Unexpected status code \(code)" diff --git a/MastoSearch/DatabaseController.swift b/MastoSearchCore/Sources/MastoSearchCore/DatabaseController.swift similarity index 86% rename from MastoSearch/DatabaseController.swift rename to MastoSearchCore/Sources/MastoSearchCore/DatabaseController.swift index 464bddc..5674207 100644 --- a/MastoSearch/DatabaseController.swift +++ b/MastoSearchCore/Sources/MastoSearchCore/DatabaseController.swift @@ -10,9 +10,9 @@ import FMDB import OSLog import Combine -class DatabaseController { +public class DatabaseController { - static let shared = DatabaseController() + public static let shared = DatabaseController() private let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DatabaseController") static let dateFormat = Date.ISO8601FormatStyle(includingFractionalSeconds: true) @@ -22,8 +22,8 @@ class DatabaseController { private var queue: FMDatabaseQueue! - private(set) var isInitialized = false - let onInitialize = PassthroughSubject<(), Never>() + public private(set) var isInitialized = false + public let onInitialize = PassthroughSubject<(), Never>() private init() { // this dir will be inside the application sandbox container @@ -31,7 +31,7 @@ class DatabaseController { databaseURL = applicationSupport.appendingPathComponent("statuses").appendingPathExtension("sqlite") } - func initialize() { + public func initialize() { if !FileManager.default.fileExists(atPath: databaseURL.absoluteString) { FileManager.default.createFile(atPath: databaseURL.absoluteString, contents: nil, attributes: nil) } @@ -80,12 +80,12 @@ class DatabaseController { onInitialize.send() } - func close() { + public func close() { // db.close() // log.info("Closed database") } - func addStatuses(_ statuses: S) where S.Element == Status { + public func addStatuses(_ statuses: S) where S.Element == Status { queue.inTransaction { db, rollback in var i = 0 for status in statuses { @@ -117,7 +117,7 @@ class DatabaseController { } } - func getStatuses(sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) { + public func getStatuses(sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) { queue.inDatabase { db in let sortKey = sortDescriptor?.key ?? "published" let asc = sortDescriptor?.ascending == true ? "ASC" : "DESC" @@ -127,7 +127,7 @@ class DatabaseController { } } - func getStatuses(query: String, sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) { + public func getStatuses(query: String, sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) { queue.inDatabase { db in let sortKey = sortDescriptor?.key ?? "rank" let asc = sortDescriptor?.ascending == false ? "DESC" : "ASC" @@ -137,14 +137,14 @@ class DatabaseController { } } - func getNewestStatus(completion: @escaping (Status?) -> Void) { + public func getNewestStatus(completion: @escaping (Status?) -> Void) { queue.inDatabase { db in let results = try! db.executeQuery("SELECT * FROM statuses ORDER BY published DESC LIMIT 1", values: nil) completion(StatusSequence(results: results).makeIterator().next()) } } - func countStatuses() -> Int { + public func countStatuses() -> Int { var res: Int! queue.inDatabase { db in let results = try! db.executeQuery("SELECT COUNT(*) AS count FROM statuses", values: nil) @@ -156,17 +156,17 @@ class DatabaseController { } -struct StatusSequence: Sequence { - typealias Element = Status +public struct StatusSequence: Sequence { + public typealias Element = Status let results: FMResultSet - func makeIterator() -> Iterator { + public func makeIterator() -> Iterator { return Iterator(results: results) } - class Iterator: IteratorProtocol { - typealias Element = Status + public class Iterator: IteratorProtocol { + public typealias Element = Status let results: FMResultSet @@ -178,7 +178,7 @@ struct StatusSequence: Sequence { results.close() } - func next() -> Status? { + public func next() -> Status? { if results.next() { return Status( id: results.string(forColumn: "api_id")!, diff --git a/MastoSearch/ImportController.swift b/MastoSearchCore/Sources/MastoSearchCore/ImportController.swift similarity index 97% rename from MastoSearch/ImportController.swift rename to MastoSearchCore/Sources/MastoSearchCore/ImportController.swift index 7f075ed..f1ef16f 100644 --- a/MastoSearch/ImportController.swift +++ b/MastoSearchCore/Sources/MastoSearchCore/ImportController.swift @@ -18,8 +18,8 @@ import OSLog */ -class ImportController { - static let shared = ImportController() +public class ImportController { + public static let shared = ImportController() private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ImportController") private let dateFormatter: DateFormatter = { @@ -32,7 +32,7 @@ class ImportController { private init() {} - func importCSV(url: URL) { + public func importCSV(url: URL) { var opts = CSVReadingOptions() opts.usesQuoting = true opts.addDateParseStrategy(Date.ISO8601FormatStyle(includingFractionalSeconds: true)) diff --git a/MastoSearch/LocalData.swift b/MastoSearchCore/Sources/MastoSearchCore/LocalData.swift similarity index 57% rename from MastoSearch/LocalData.swift rename to MastoSearchCore/Sources/MastoSearchCore/LocalData.swift index 6aaaabe..dfe3c17 100644 --- a/MastoSearch/LocalData.swift +++ b/MastoSearchCore/Sources/MastoSearchCore/LocalData.swift @@ -7,13 +7,13 @@ import Foundation -class LocalData { +public class LocalData { private init() {} private static let encoder = PropertyListEncoder() private static let decoder = PropertyListDecoder() - static var account: AccountInfo? { + public static var account: AccountInfo? { get { guard let data = UserDefaults.standard.data(forKey: "account") else { return nil @@ -30,11 +30,18 @@ class LocalData { } } - struct AccountInfo: Codable { - let instanceURL: URL - var clientID: String! - var clientSecret: String! - var accessToken: String! + public struct AccountInfo: Codable { + public let instanceURL: URL + public var clientID: String! + public var clientSecret: String! + public var accessToken: String! + + public init(instanceURL: URL, clientID: String? = nil, clientSecret: String? = nil, accessToken: String? = nil) { + self.instanceURL = instanceURL + self.clientID = clientID + self.clientSecret = clientSecret + self.accessToken = accessToken + } } } diff --git a/MastoSearchCore/Sources/MastoSearchCore/LoginController.swift b/MastoSearchCore/Sources/MastoSearchCore/LoginController.swift new file mode 100644 index 0000000..7af2e5a --- /dev/null +++ b/MastoSearchCore/Sources/MastoSearchCore/LoginController.swift @@ -0,0 +1,65 @@ +// +// LoginController.swift +// MastoSearchCore +// +// Created by Shadowfacts on 7/3/22. +// + +import Foundation +import AuthenticationServices + +public class LoginController { + private init() {} + + public static let shared = LoginController() + + private var authSession: ASWebAuthenticationSession? + + public func logIn(with instanceURL: URL, presentationContextProvider: ASWebAuthenticationPresentationContextProviding, completion: @escaping () -> Void) { + LocalData.account = LocalData.AccountInfo(instanceURL: instanceURL, 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: instanceURL, 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 { + completion() + } + } + }) + DispatchQueue.main.async { + self.authSession!.presentationContextProvider = presentationContextProvider + self.authSession!.start() + } + } + } +} diff --git a/MastoSearchCore/Sources/MastoSearchCore/Status.swift b/MastoSearchCore/Sources/MastoSearchCore/Status.swift new file mode 100644 index 0000000..19515b9 --- /dev/null +++ b/MastoSearchCore/Sources/MastoSearchCore/Status.swift @@ -0,0 +1,24 @@ +// +// Status.swift +// MastoSearch +// +// Created by Shadowfacts on 12/10/21. +// + +import Foundation + +public struct Status: Identifiable { + public let id: String + public let url: String + public let summary: String? + public let content: String + public let published: Date + + public init(id: String, url: String, summary: String?, content: String, published: Date) { + self.id = id + self.url = url + self.summary = summary + self.content = content + self.published = published + } +} diff --git a/MastoSearchCore/Sources/MastoSearchCore/SyncController.swift b/MastoSearchCore/Sources/MastoSearchCore/SyncController.swift new file mode 100644 index 0000000..5454538 --- /dev/null +++ b/MastoSearchCore/Sources/MastoSearchCore/SyncController.swift @@ -0,0 +1,70 @@ +// +// SyncController.swift +// MastoSearchCore +// +// Created by Shadowfacts on 7/3/22. +// + +import Foundation +import Combine +import OSLog + +public class SyncController { + private init() {} + + public static let shared = SyncController() + + public let onSync = PassthroughSubject() + + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Sync") + private var syncTotal = 0 + + public func syncStatuses(errorHandler: @escaping (APIController.Error) -> Void) { + DatabaseController.shared.getNewestStatus { status in + guard let status else { + return + } + + self.logger.log("Starting sync...") + self.syncTotal = 0 + self.syncStatuses(range: .after(status.id), errorHandler: errorHandler) + } + } + + private func syncStatuses(range: APIController.RequestRange, errorHandler: @escaping (APIController.Error) -> Void) { + APIController.shared.getStatuses(range: range) { response in + switch response { + case .failure(let error): + self.logger.error("Erorr syncing statuses: \(String(describing: error), privacy: .public)") + DispatchQueue.main.async { + errorHandler(error) + } + + case .success(let statuses): + guard statuses.count > 0 else { + DispatchQueue.main.async { + self.logger.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) + } + }) + DispatchQueue.main.async { + self.onSync.send() + } + + self.syncTotal += statuses.count + + self.syncStatuses(range: .after(statuses.first!.id), errorHandler: errorHandler) + } + } + } + +} diff --git a/MastoSearch/Vendor/UInt128.swift b/MastoSearchCore/Sources/MastoSearchCore/Vendor/UInt128.swift similarity index 100% rename from MastoSearch/Vendor/UInt128.swift rename to MastoSearchCore/Sources/MastoSearchCore/Vendor/UInt128.swift