Move common code to swift package

This commit is contained in:
Shadowfacts 2022-07-03 11:58:59 -07:00
parent 8547950d80
commit 2b0ab45c12
18 changed files with 318 additions and 217 deletions

View File

@ -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 = "<group>"; };
D6451242276A408F0046CCD2 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
D669039C2769236F00819C4D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
D66903BD2769250B00819C4D /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; };
D6A4B8A727C1BC5A0016F458 /* APIController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D6A4B8B627C2B18C0016F458 /* ImportControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportControllerTests.swift; sourceTree = "<group>"; };
D6A4B8B927C2BE330016F458 /* UInt128.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt128.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D6B24DEA27640CE200BA23B8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D6B24DEF27640CE200BA23B8 /* MastoSearch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MastoSearch.entitlements; sourceTree = "<group>"; };
D6B24DF827640DD700BA23B8 /* DatabaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseController.swift; sourceTree = "<group>"; };
D6D9CFE72764196E006FE2E7 /* ImportController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportController.swift; sourceTree = "<group>"; };
D6D9CFE927641D4A006FE2E7 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
D6E77D1428721A7600D8B732 /* MastoSearchCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MastoSearchCore; sourceTree = "<group>"; };
/* 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 = "<group>";
};
D6A4B8AE27C2B1770016F458 /* MastoSearchTests */ = {
isa = PBXGroup;
children = (
@ -82,20 +78,14 @@
path = MastoSearchTests;
sourceTree = "<group>";
};
D6A4B8B827C2BE250016F458 /* Vendor */ = {
isa = PBXGroup;
children = (
D6A4B8B927C2BE330016F458 /* UInt128.swift */,
);
path = Vendor;
sourceTree = "<group>";
};
D6B24DDC27640CE100BA23B8 = {
isa = PBXGroup;
children = (
D6E77D1428721A7600D8B732 /* MastoSearchCore */,
D6B24DE727640CE100BA23B8 /* MastoSearch */,
D6A4B8AE27C2B1770016F458 /* MastoSearchTests */,
D6B24DE627640CE100BA23B8 /* Products */,
D6559A5028721BAF000EEB4D /* Frameworks */,
);
sourceTree = "<group>";
};
@ -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 */;

View File

@ -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<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()
@ -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,60 +77,25 @@ 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 {
LoginController.shared.logIn(with: url, presentationContextProvider: self) {
self.updateAccountMenu()
self.syncStatuses()
SyncController.shared.syncStatuses(errorHandler: self.handleSyncError(_:))
}
}
})
DispatchQueue.main.async {
self.authSession!.presentationContextProvider = self
self.authSession!.start()
}
}
}
@objc func logOut() {
LocalData.account = nil
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 {

View File

@ -734,7 +734,7 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="date" id="yUJ-7Q-rvg">
<rect key="frame" x="8" y="0.0" width="146" height="24"/>
<rect key="frame" x="18" y="0.0" width="146" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField identifier="dateCell" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kPp-F2-rtb">
@ -767,7 +767,7 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="contentWarning" id="m4I-Cd-y2k">
<rect key="frame" x="171" y="0.0" width="173" height="24"/>
<rect key="frame" x="181" y="0.0" width="173" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField identifier="contentWarningCell" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Zz1-AR-4EW">
@ -800,7 +800,7 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="content" id="BHM-ku-S45">
<rect key="frame" x="361" y="0.0" width="303" height="17"/>
<rect key="frame" x="371" y="0.0" width="303" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Hwo-JH-icI">

View File

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

View File

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

View File

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

9
MastoSearchCore/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

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

View File

@ -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"]),
]
)

View File

@ -0,0 +1,3 @@
# MastoSearchCore
A description of this package.

View File

@ -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<ClientRegistration, Error>) -> Void) {
public func register(completion: @escaping (Result<ClientRegistration, Error>) -> 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<LoginSettings, Error>) -> Void) {
public func getAccessToken(authCode: String, completion: @escaping (Result<LoginSettings, Error>) -> 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)"

View File

@ -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<S: Sequence>(_ statuses: S) where S.Element == Status {
public func addStatuses<S: Sequence>(_ 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")!,

View File

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

View File

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

View File

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

View File

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

View File

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