Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shortcut: Append to note #1170

Merged
merged 13 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// NSURLSessionConfiguration+Extensions.swift
// IntentsExtension
//
// Created by Charlie Scheer on 6/3/24.
// Copyright © 2024 Simperium. All rights reserved.
//

import Foundation

extension URLSessionConfiguration {
/// Returns a new Background Session Configuration, with a random identifier.
///
class func backgroundSessionConfigurationWithRandomizedIdentifier() -> URLSessionConfiguration {
let identifier = IntentsConstants.extensionGroupName + "." + UUID().uuidString
let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
configuration.sharedContainerIdentifier = IntentsConstants.extensionGroupName

return configuration
}
}
2 changes: 2 additions & 0 deletions IntentsExtension/IntentHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class IntentHandler: INExtension {
return FindNoteWithTagIntentHandler()
case is CopyNoteContentIntent:
return CopyNoteContentIntentHandler()
case is AppendNoteIntent:
return AppendNoteIntentHandler()
default:
return self
}
Expand Down
40 changes: 40 additions & 0 deletions IntentsExtension/IntentHandlers/AppendNoteIntentHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Intents

class AppendNoteIntentHandler: NSObject, AppendNoteIntentHandling {
let coreDataWrapper = ExtensionCoreDataWrapper()

func resolveContent(for intent: AppendNoteIntent) async -> INStringResolutionResult {
guard let content = intent.content else {
return INStringResolutionResult.needsValue()
}
return INStringResolutionResult.success(with: content)
}

func resolveNote(for intent: AppendNoteIntent) async -> IntentNoteResolutionResult {
IntentNoteResolutionResult.resolve(intent.note, in: coreDataWrapper)
}

func provideNoteOptionsCollection(for intent: AppendNoteIntent) async throws -> INObjectCollection<IntentNote> {
let intentNotes = try IntentNote.allNotes(in: coreDataWrapper)
return INObjectCollection(items: intentNotes)
}

func handle(intent: AppendNoteIntent) async -> AppendNoteIntentResponse {
guard let identifier = intent.note?.identifier,
let content = intent.content,
let note = coreDataWrapper.resultsController?.note(forSimperiumKey: identifier),
let token = KeychainManager.extensionToken else {
return AppendNoteIntentResponse(code: .failure, userActivity: nil)
}

guard let existingContent = try? await Downloader(simperiumToken: token).getNoteContent(for: identifier) else {
return AppendNoteIntentResponse(code: .failure, userActivity: nil)
}

note.content = existingContent + "\n\(content)"
let uploader = Uploader(simperiumToken: token)
uploader.send(note)

return AppendNoteIntentResponse(code: .success, userActivity: nil)
}
}
49 changes: 49 additions & 0 deletions IntentsExtension/Models/Note+Intents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,55 @@ extension Note {
let result = String(content[range])
return result.droppingPrefix(Constants.titleMarkdownPrefix)
}

private func objectFromJSONString(_ json: String) -> Any? {
guard let data = json.data(using: .utf8) else {
return nil
}

return try? JSONSerialization.jsonObject(with: data)
}
charliescheer marked this conversation as resolved.
Show resolved Hide resolved

var tagsArray: [String] {
guard let tagsString = tags,
let array = tagsString.objectFromJSONString() as? [String] else {
return []
}

return array
}

var systemTagsArray: [String] {
guard let systemTagsString = systemTags,
let array = systemTagsString.objectFromJSONString() as? [String] else {
return []
}

return array
}

func toDictionary() -> [String: Any] {

return [
"tags": tagsArray,
"deleted": 0,
"shareURL": shareURL ?? String(),
"publishURL": publishURL ?? String(),
"content": content ?? "",
"systemTags": systemTagsArray,
"creationDate": (creationDate ?? .now).timeIntervalSince1970,
"modificationDate": (modificationDate ?? .now).timeIntervalSince1970
]
}

func toJsonData() -> Data? {
do {
return try JSONSerialization.data(withJSONObject: toDictionary(), options: .prettyPrinted)
} catch {
print("Error converting Note to JSON: \(error)")
return nil
}
}
}

private struct Constants {
Expand Down
1 change: 1 addition & 0 deletions IntentsExtension/Support Files/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<key>IntentsSupported</key>
<array>
<string>CopyNoteContentIntent</string>
<string>AppendNoteIntent</string>
<string>FindNoteIntent</string>
<string>FindNoteWithTagIntent</string>
<string>OpenNewNoteIntent</string>
Expand Down
4 changes: 4 additions & 0 deletions IntentsExtension/Support Files/IntentsExtension.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<array>
<string>$(TeamIdentifierPrefix)com.automattic.SimplenoteMac</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.automattic.SimplenoteMac</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<array>
<string>$(TeamIdentifierPrefix)com.automattic.SimplenoteMac.Development</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.automattic.SimplenoteMac.Development</string>
Expand Down
43 changes: 43 additions & 0 deletions IntentsExtension/Tools/Downloader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation

class Downloader: NSObject {

/// Simperium's Token
///
private let token: String

/// Designated Initializer
///
init(simperiumToken: String) {
token = simperiumToken
}

func getNoteContent(for simperiumKey: String) async throws -> String? {
let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, SPCredentials.simperiumAppID, Settings.bucketName, simperiumKey)
let targetURL = URL(string: endpoint.lowercased())!

// Request
var request = URLRequest(url: targetURL)
request.httpMethod = Settings.httpMethodGet
request.setValue(token, forHTTPHeaderField: Settings.authHeader)

let session = Foundation.URLSession(configuration: .default, delegate: nil, delegateQueue: .main)

let downloadedData = try await session.data(for: request)

return try extractNoteContent(from: downloadedData.0)
}

private func extractNoteContent(from data: Data) throws -> String? {
let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any]
return jsonObject?["content"] as? String
}
}

// MARK: - Settings
//
private struct Settings {
static let authHeader = "X-Simperium-Token"
static let bucketName = "note"
static let httpMethodGet = "GET"
}
2 changes: 2 additions & 0 deletions IntentsExtension/Tools/IntentsConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ import Foundation

struct IntentsConstants {
static let noteIdentifierKey = "OpenNoteIntentHandlerIdentifierKey"
static let extensionGroupName = Bundle.main.sharedGroupDomain
static let simperiumBaseURL = "https://api.simperium.com/1"
}
70 changes: 70 additions & 0 deletions IntentsExtension/Tools/Uploader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation

/// The purpose of this class is to encapsulate NSURLSession's interaction code, required to upload
/// a note to Simperium's REST endpoint.
///
class Uploader: NSObject {

/// Simperium's Token
///
private let token: String

/// Designated Initializer
///
init(simperiumToken: String) {
token = simperiumToken
}

// MARK: - Public Methods
func send(_ note: Note) {
// Build the targetURL
let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, SPCredentials.simperiumAppID, Settings.bucketName, note.simperiumKey)
let targetURL = URL(string: endpoint.lowercased())!

// Request
var request = URLRequest(url: targetURL)
request.httpMethod = Settings.httpMethodPost
request.httpBody = note.toJsonData()
request.setValue(token, forHTTPHeaderField: Settings.authHeader)

// Task!
let sc = URLSessionConfiguration.backgroundSessionConfigurationWithRandomizedIdentifier()

let session = Foundation.URLSession(configuration: sc, delegate: self, delegateQueue: .main)
let task = session.downloadTask(with: request)
task.resume()
}
}

// MARK: - URLSessionDelegate
//
extension Uploader: URLSessionDelegate {

@objc
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
print("<> Uploader.didBecomeInvalidWithError: \(String(describing: error))")
}

@objc
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
print("<> Uploader.URLSessionDidFinishEventsForBackgroundURLSession")
}
}

// MARK: - URLSessionTaskDelegate
//
extension Uploader: URLSessionTaskDelegate {

@objc
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("<> Uploader.didCompleteWithError: \(String(describing: error))")
}
}

// MARK: - Settings
//
private struct Settings {
static let authHeader = "X-Simperium-Token"
static let bucketName = "note"
static let httpMethodPost = "POST"
}
21 changes: 20 additions & 1 deletion Simplenote.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@
BA2C65CF26FE996A00FA84E1 /* NSButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2C65CA26FE996100FA84E1 /* NSButton+Extensions.swift */; };
BA4C6D16264CA8C000B723A7 /* SignupRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4C6D15264CA8C000B723A7 /* SignupRemoteTests.swift */; };
BA4C6D18264CAAF800B723A7 /* URLRequest+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4C6D17264CAAF800B723A7 /* URLRequest+Simplenote.swift */; };
BA4F223D2C1117E500144EDA /* SPCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E196C0230F5F5300F5658A /* SPCredentials.swift */; };
BA4F223E2C1255A500144EDA /* SPCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E196C0230F5F5300F5658A /* SPCredentials.swift */; };
BA54F2462C0E63C700DBCE9D /* AppendNoteIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */; };
BA553F0927065E20007737E9 /* FontSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA553F0727065E20007737E9 /* FontSettings.swift */; };
BA5A658A2C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5A65892C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift */; };
BA5A658C2C090F4800F605A6 /* SharedStorageMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5A658B2C090F4800F605A6 /* SharedStorageMigratorTests.swift */; };
Expand All @@ -270,6 +271,9 @@
BA71EC252BC88FFC00F42CB1 /* NSManagedObjectContext+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA52005C2BC88397003F1B75 /* NSManagedObjectContext+Simplenote.swift */; };
BA78AF712B5B2BC300DCF896 /* AutomatticTracks in Frameworks */ = {isa = PBXBuildFile; productRef = BA78AF702B5B2BC300DCF896 /* AutomatticTracks */; };
BA938CEE26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */; };
BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB642C0E46CC00B34EA7 /* Uploader.swift */; };
BA94CB682C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */; };
BA94CB6F2C0E4AFA00B34EA7 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */; };
BAA0A88B26BA39150006260E /* Date+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A88626B9F8B40006260E /* Date+Simplenote.swift */; };
BAA0A88F26BA39200006260E /* AccountDeletionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A88126B9F8970006260E /* AccountDeletionController.swift */; };
BAA0A89326BA39260006260E /* RemoteError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A87926B9F0B50006260E /* RemoteError.swift */; };
Expand Down Expand Up @@ -706,6 +710,7 @@
BA4C6D17264CAAF800B723A7 /* URLRequest+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Simplenote.swift"; sourceTree = "<group>"; };
BA52005A2BC878F1003F1B75 /* CSSearchable+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CSSearchable+Helpers.swift"; sourceTree = "<group>"; };
BA52005C2BC88397003F1B75 /* NSManagedObjectContext+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Simplenote.swift"; sourceTree = "<group>"; };
BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppendNoteIntentHandler.swift; sourceTree = "<group>"; };
BA553F0727065E20007737E9 /* FontSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSettings.swift; sourceTree = "<group>"; };
BA5A65892C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyNoteContentIntentHandler.swift; sourceTree = "<group>"; };
BA5A658B2C090F4800F605A6 /* SharedStorageMigratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedStorageMigratorTests.swift; sourceTree = "<group>"; };
Expand All @@ -718,6 +723,10 @@
BA8CF21B2BFD20770087F33D /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
BA938CEB26ACFF4A00BE5A1D /* Remote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Remote.swift; sourceTree = "<group>"; };
BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountVerificationController+TestHelpers.swift"; sourceTree = "<group>"; };
BA94CB642C0E46CC00B34EA7 /* Uploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uploader.swift; sourceTree = "<group>"; };
BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSURLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
BA94CB6B2C0E4A6100B34EA7 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = "<group>"; };
BAA0A87926B9F0B50006260E /* RemoteError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteError.swift; sourceTree = "<group>"; };
BAA0A88126B9F8970006260E /* AccountDeletionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountDeletionController.swift; sourceTree = "<group>"; };
BAA0A88626B9F8B40006260E /* Date+Simplenote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Simplenote.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -834,6 +843,7 @@
26F72A8B14032D2A00A7935E /* Frameworks */ = {
isa = PBXGroup;
children = (
BA94CB6B2C0E4A6100B34EA7 /* CoreFoundation.framework */,
26F72A8F14032D2A00A7935E /* AppKit.framework */,
26F72A8C14032D2A00A7935E /* Cocoa.framework */,
26F72A9014032D2A00A7935E /* CoreData.framework */,
Expand Down Expand Up @@ -1608,6 +1618,8 @@
BAAA71E52C07A35F00244C01 /* ExtensionCoreDataWrapper.swift */,
BAAA71EE2C07A6CF00244C01 /* IntentsError.swift */,
BAAA71F12C07A7FD00244C01 /* IntentsConstants.swift */,
BA94CB642C0E46CC00B34EA7 /* Uploader.swift */,
BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */,
);
path = Tools;
sourceTree = "<group>";
Expand All @@ -1617,6 +1629,7 @@
children = (
BAAA71EC2C07A69C00244C01 /* IntentNote+Helpers.swift */,
BAAD56482C0A42E40047E024 /* FileManager+Intents.swift */,
BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */,
BACA68702C0A7027005409C1 /* IntentTag+Helpers.swift */,
BACA68752C0A72F3005409C1 /* NSString+Intents.swift */,
);
Expand All @@ -1642,6 +1655,7 @@
BAB261742BFFD319009A98D7 /* OpenNewNoteIntentHandler.swift */,
BAC5DFB82C079E5A002AD7EF /* OpenNoteIntentHandler.swift */,
BA2BF33F2C07C75500A7C894 /* FindNoteIntentHandler.swift */,
BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */,
BACA686E2C0A6E2F005409C1 /* FindNoteWithTagIntentHandler.swift */,
BA5A65892C07CD0B00F605A6 /* CopyNoteContentIntentHandler.swift */,
);
Expand Down Expand Up @@ -2228,8 +2242,11 @@
BAAA71F22C07A7FD00244C01 /* IntentsConstants.swift in Sources */,
BAAA71D92C07A14600244C01 /* String+Simplenote.swift in Sources */,
BACA687C2C0A764C005409C1 /* KeychainPasswordItem.swift in Sources */,
BA4F223E2C1255A500144EDA /* SPCredentials.swift in Sources */,
BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */,
BAAA71E92C07A39300244C01 /* FileManager+Simplenote.swift in Sources */,
BAAA71E82C07A37700244C01 /* StorageSettings.swift in Sources */,
BA94CB6F2C0E4AFA00B34EA7 /* Downloader.swift in Sources */,
BAAA71E22C07A2BA00244C01 /* NSSortDescriptor+Simplenote.swift in Sources */,
BAAA71E62C07A35F00244C01 /* ExtensionCoreDataWrapper.swift in Sources */,
BAB261722BFFD0F4009A98D7 /* ShortcutIntents.intentdefinition in Sources */,
Expand All @@ -2246,6 +2263,7 @@
BAAA71D72C07A11500244C01 /* Note+Intents.swift in Sources */,
BAAA71D82C07A12F00244C01 /* NoteContentHelper.swift in Sources */,
BAAA71EA2C07A58100244C01 /* Simplenote.xcdatamodeld in Sources */,
BA54F2462C0E63C700DBCE9D /* AppendNoteIntentHandler.swift in Sources */,
BAB261662BFFD0AF009A98D7 /* IntentHandler.swift in Sources */,
BAAD56472C0A42000047E024 /* FileManagerProtocol.swift in Sources */,
BAC5DFB52C069568002AD7EF /* Bundle+Simplenote.swift in Sources */,
Expand All @@ -2256,6 +2274,7 @@
BAAA71E72C07A37000244C01 /* CoreDataManager.swift in Sources */,
BAAA71D52C07A10E00244C01 /* SPManagedObject+Intents.swift in Sources */,
BAAA71ED2C07A69C00244C01 /* IntentNote+Helpers.swift in Sources */,
BA94CB682C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Loading