aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Info.plist43
-rw-r--r--QSThingsPlugin.xcodeproj/project.pbxproj2
-rw-r--r--QSThingsPlugin/Actions/CompleteTaskAction.m7
-rw-r--r--QSThingsPlugin/Actions/CreateTaskAction.h13
-rw-r--r--QSThingsPlugin/Actions/CreateTaskAction.m47
-rw-r--r--QSThingsPlugin/Helpers/KeychainHelper.m117
-rw-r--r--QSThingsPlugin/Helpers/SQLHelper.m1
-rw-r--r--QSThingsPlugin/Helpers/TextParsingHelper.h22
-rw-r--r--QSThingsPlugin/Helpers/TextParsingHelper.m111
9 files changed, 297 insertions, 66 deletions
diff --git a/Info.plist b/Info.plist
index b43bf23..edc9527 100644
--- a/Info.plist
+++ b/Info.plist
@@ -22,6 +22,49 @@
<string>QSThingsPluginSource</string>
<key>QSActions</key>
<dict>
+ <key>CreateTaskInAction</key>
+ <dict>
+ <key>actionClass</key>
+ <string>CreateTaskAction</string>
+ <key>actionSelector</key>
+ <string>createTask:in:</string>
+ <key>directTypes</key>
+ <array>
+ <string>*</string>
+ </array>
+ <key>indirectTypes</key>
+ <array>
+ <string>things.project</string>
+ <string>things.area</string>
+ </array>
+ <key>enabled</key>
+ <true/>
+ <key>displaysResult</key>
+ <false/>
+ <key>name</key>
+ <string>Create task in...</string>
+ <key>icon</key>
+ <string>com.culturedcode.ThingsMac</string>
+ </dict>
+ <key>CreateTaskAction</key>
+ <dict>
+ <key>actionClass</key>
+ <string>CreateTaskAction</string>
+ <key>actionSelector</key>
+ <string>createTask:</string>
+ <key>directTypes</key>
+ <array>
+ <string>*</string>
+ </array>
+ <key>enabled</key>
+ <true/>
+ <key>displaysResult</key>
+ <false/>
+ <key>name</key>
+ <string>Create task</string>
+ <key>icon</key>
+ <string>com.culturedcode.ThingsMac</string>
+ </dict>
<key>CompleteTaskAction</key>
<dict>
<key>actionClass</key>
diff --git a/QSThingsPlugin.xcodeproj/project.pbxproj b/QSThingsPlugin.xcodeproj/project.pbxproj
index e46957e..fc4c27a 100644
--- a/QSThingsPlugin.xcodeproj/project.pbxproj
+++ b/QSThingsPlugin.xcodeproj/project.pbxproj
@@ -40,11 +40,13 @@
membershipExceptions = (
/Localized/Localizable.strings,
Actions/CompleteTaskAction.m,
+ Actions/CreateTaskAction.m,
Actions/SearchThingsAction.m,
Actions/SetThingsAuthenticationKeyAction.m,
Actions/ShowInThingsAction.m,
Helpers/KeychainHelper.m,
Helpers/SQLHelper.m,
+ Helpers/TextParsingHelper.m,
Helpers/ThingsHelper.m,
Parsers/QSThingsAreasParser.m,
Parsers/QSThingsProjectsParser.m,
diff --git a/QSThingsPlugin/Actions/CompleteTaskAction.m b/QSThingsPlugin/Actions/CompleteTaskAction.m
index 5ff1bd0..afc55ac 100644
--- a/QSThingsPlugin/Actions/CompleteTaskAction.m
+++ b/QSThingsPlugin/Actions/CompleteTaskAction.m
@@ -7,13 +7,8 @@ NS_ASSUME_NONNULL_BEGIN
- (QSObject *)completeTask:(QSObject *)directObject
{
NSSet *validTypes = [NSSet setWithObjects:kProjectType, kTaskType, nil];
- NSString *authenticationKey = [KeychainHelper authenticationKey];
- if (!authenticationKey) {
- NSLog(@"There is no authentication key.");
- }
-
- if (authenticationKey && [validTypes containsObject:directObject.primaryType]) {
+ if ([validTypes containsObject:directObject.primaryType]) {
NSDictionary *contents = [directObject objectForType:directObject.primaryType];
ThingsURLRoute route = ThingsURLRouteUpdate;
if (directObject.primaryType == kProjectType) {
diff --git a/QSThingsPlugin/Actions/CreateTaskAction.h b/QSThingsPlugin/Actions/CreateTaskAction.h
new file mode 100644
index 0000000..36c5c3c
--- /dev/null
+++ b/QSThingsPlugin/Actions/CreateTaskAction.h
@@ -0,0 +1,13 @@
+#import "Constants.h"
+#import "ThingsHelper.h"
+#import "TextParsingHelper.h"
+#import <Foundation/Foundation.h>
+#import <QSCore/QSCore.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface CreateTaskAction : QSActionProvider
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QSThingsPlugin/Actions/CreateTaskAction.m b/QSThingsPlugin/Actions/CreateTaskAction.m
new file mode 100644
index 0000000..f11936f
--- /dev/null
+++ b/QSThingsPlugin/Actions/CreateTaskAction.m
@@ -0,0 +1,47 @@
+#import "CreateTaskAction.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation CreateTaskAction
+
+- (QSObject *)createTask:(QSObject *)directObject
+{
+ NSString *contents = [directObject displayName];
+ if (contents) {
+ NSDictionary *parameters = [TextParsingHelper parseText: contents];
+ [ThingsHelper callRoute:ThingsURLRouteAdd withParameters:parameters];
+ } else {
+ NSLog(@"Could not get the text of the Things object.");
+ }
+ return directObject;
+}
+
+- (QSObject *)createTask:(QSObject *)directObject in:(QSObject *)indirectObject
+{
+ NSString *contents = [directObject displayName];
+
+ NSDictionary *indirectContents = [indirectObject objectForType:indirectObject.primaryType];
+ NSString *listId = nil;
+ if (indirectContents) {
+ listId = [indirectContents objectForKey:@"uuid"];
+ } else {
+ NSLog(@"Could not read the contents for the indirect object");
+ }
+
+ if (contents) {
+ NSMutableDictionary *parameters = [TextParsingHelper parseText: contents];
+ if (listId) {
+ [parameters setObject:listId forKey:@"list-id"];
+ } else {
+ NSLog(@"Could not find a list id");
+ }
+ [ThingsHelper callRoute:ThingsURLRouteAdd withParameters:parameters];
+ } else {
+ NSLog(@"Could not get the text of the Things object.");
+ }
+ return directObject;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QSThingsPlugin/Helpers/KeychainHelper.m b/QSThingsPlugin/Helpers/KeychainHelper.m
index fcf8291..50a8eac 100644
--- a/QSThingsPlugin/Helpers/KeychainHelper.m
+++ b/QSThingsPlugin/Helpers/KeychainHelper.m
@@ -5,70 +5,69 @@ NS_ASSUME_NONNULL_BEGIN
@implementation KeychainHelper
+ (NSString *)authenticationKey {
- const char *service = [kKeychainService UTF8String];
- const char *account = [kAuthenticationKeyKeychainKey UTF8String];
-
- UInt32 authenticationKeyLength = 0;
- void *authenticationKeyData = NULL;
-
- OSStatus status = SecKeychainFindGenericPassword(
- NULL, (UInt32)strlen(service), service, (UInt32)strlen(account), account,
- &authenticationKeyLength, &authenticationKeyData, NULL);
-
- if (status == errSecSuccess && authenticationKeyData != NULL) {
- NSString *authenticationKey = [[NSString alloc] initWithBytes:authenticationKeyData
- length:authenticationKeyLength
- encoding:NSUTF8StringEncoding];
- SecKeychainItemFreeContent(NULL, authenticationKeyData);
- return authenticationKey;
- }
-
- return nil;
+ CFDictionaryRef query = (__bridge CFDictionaryRef)@{
+ (__bridge NSString *)kSecClass: (__bridge NSString *)kSecClassGenericPassword,
+ (__bridge NSString *)kSecAttrService: kKeychainService,
+ (__bridge NSString *)kSecAttrAccount: kAuthenticationKeyKeychainKey,
+ (__bridge NSString *)kSecReturnData: @YES,
+ (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne
+ };
+
+ CFTypeRef result = NULL;
+ OSStatus status = SecItemCopyMatching(query, &result);
+
+ if (status == errSecSuccess && result != NULL) {
+ NSData *keyData = (__bridge NSData *)result;
+ NSString *authenticationKey = [[[NSString alloc] initWithData:keyData
+ encoding:NSUTF8StringEncoding] autorelease];
+ CFRelease(result);
+ return authenticationKey;
+ }
+
+ return nil;
}
+ (OSStatus)setAuthenticationKey:(NSString *)authenticationKey {
- const char *service = [kKeychainService UTF8String];
- const char *account = [kAuthenticationKeyKeychainKey UTF8String];
- const char *authenticationKeyCString = [authenticationKey UTF8String];
-
- // First try to find existing item
- SecKeychainItemRef item = NULL;
- OSStatus findStatus = SecKeychainFindGenericPassword(
- NULL, (UInt32)strlen(service), service, (UInt32)strlen(account), account,
- NULL, NULL, &item);
-
- OSStatus status;
- if (findStatus == errSecSuccess) {
- // Update existing item
- status = SecKeychainItemModifyAttributesAndData(
- item, NULL, (UInt32)strlen(authenticationKeyCString), authenticationKeyCString);
- CFRelease(item);
- } else {
- // Create new item
- status = SecKeychainAddGenericPassword(
- NULL, (UInt32)strlen(service), service, (UInt32)strlen(account),
- account, (UInt32)strlen(authenticationKeyCString), authenticationKeyCString, NULL);
- }
-
- return status;
+ NSData *passwordData = [authenticationKey dataUsingEncoding:NSUTF8StringEncoding];
+
+ // First try to update existing item
+ CFDictionaryRef query = (__bridge CFDictionaryRef)@{
+ (__bridge NSString *)kSecClass: (__bridge NSString *)kSecClassGenericPassword,
+ (__bridge NSString *)kSecAttrService: kKeychainService,
+ (__bridge NSString *)kSecAttrAccount: kAuthenticationKeyKeychainKey
+ };
+
+ CFDictionaryRef attributesToUpdate = (__bridge CFDictionaryRef)@{
+ (__bridge NSString *)kSecValueData: passwordData
+ };
+
+ OSStatus status = SecItemUpdate(query, attributesToUpdate);
+
+ if (status == errSecItemNotFound) {
+ // Item doesn't exist, create new one
+ CFDictionaryRef attributes = (__bridge CFDictionaryRef)@{
+ (__bridge NSString *)kSecClass: (__bridge NSString *)kSecClassGenericPassword,
+ (__bridge NSString *)kSecAttrService: kKeychainService,
+ (__bridge NSString *)kSecAttrAccount: kAuthenticationKeyKeychainKey,
+ (__bridge NSString *)kSecValueData: passwordData,
+ (__bridge NSString *)kSecAttrAccessible: (__bridge NSString *)kSecAttrAccessibleWhenUnlockedThisDeviceOnly
+ };
+
+ status = SecItemAdd(attributes, NULL);
+ }
+
+ return status;
}
-+ (OSStatus) deleteAuthenticationKey {
- const char *service = [kKeychainService UTF8String];
- const char *account = [kAuthenticationKeyKeychainKey UTF8String];
-
- SecKeychainItemRef item = NULL;
- OSStatus findStatus = SecKeychainFindGenericPassword(
- NULL, (UInt32)strlen(service), service, (UInt32)strlen(account), account,
- NULL, NULL, &item);
-
- if (findStatus == errSecSuccess) {
- OSStatus deleteStatus = SecKeychainItemDelete(item);
- CFRelease(item);
- return deleteStatus;
- }
-
- return findStatus;
++ (OSStatus)deleteAuthenticationKey {
+ CFDictionaryRef query = (__bridge CFDictionaryRef)@{
+ (__bridge NSString *)kSecClass: (__bridge NSString *)kSecClassGenericPassword,
+ (__bridge NSString *)kSecAttrService: kKeychainService,
+ (__bridge NSString *)kSecAttrAccount: kAuthenticationKeyKeychainKey
+ };
+
+ OSStatus status = SecItemDelete(query);
+ return status;
}
@end
diff --git a/QSThingsPlugin/Helpers/SQLHelper.m b/QSThingsPlugin/Helpers/SQLHelper.m
index 6d94bb5..c44ea22 100644
--- a/QSThingsPlugin/Helpers/SQLHelper.m
+++ b/QSThingsPlugin/Helpers/SQLHelper.m
@@ -6,7 +6,6 @@
+ (NSArray *) executeSql:(NSString *)query onFile:(NSString *)path {
NSMutableArray *objects = [NSMutableArray arrayWithCapacity:0];
- NSFileManager *manager = [NSFileManager defaultManager];
FMDatabase *database = [FMDatabase databaseWithPath:path];
if (![database openWithFlags:0x00000001]) { // 1 = SQLITE_OPEN_READONLY
diff --git a/QSThingsPlugin/Helpers/TextParsingHelper.h b/QSThingsPlugin/Helpers/TextParsingHelper.h
new file mode 100644
index 0000000..2c7029c
--- /dev/null
+++ b/QSThingsPlugin/Helpers/TextParsingHelper.h
@@ -0,0 +1,22 @@
+#import "Constants.h"
+#import <Foundation/Foundation.h>
+#import <Security/Security.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface TextParsingHelper : NSObject
+
+/**
+ Parses a string and returns a dictionary of parameters according to the following rules:
+ * The first line is parsed as title
+ * If the first line contains >> then everything after >> is considered notes.
+ * If a line starts with #, the rest will be interpreted as a comma separated list of tags.
+ * If a line starts with @, the rest will be interpreted as the date.
+ * If a line starts with !, the rest will be interpreted as the deadline.
+ @param text the
+ @return A dictionary with the parameters for the `add` route of the things API
+ */
++ (NSMutableDictionary *)parseText:(NSString*)text;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QSThingsPlugin/Helpers/TextParsingHelper.m b/QSThingsPlugin/Helpers/TextParsingHelper.m
new file mode 100644
index 0000000..3fbbd3d
--- /dev/null
+++ b/QSThingsPlugin/Helpers/TextParsingHelper.m
@@ -0,0 +1,111 @@
+#import "TextParsingHelper.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation TextParsingHelper
+
++ (NSMutableDictionary *)parseText:(NSString *)text {
+ if (!text || [text length] == 0) {
+ return nil;
+ }
+
+ NSArray *lines = [text componentsSeparatedByString:@"\n"];
+ if ([lines count] == 0) {
+ return nil;
+ }
+
+ NSMutableDictionary *result = [[NSMutableDictionary alloc] init];
+ NSMutableString *notes = nil;
+ BOOL processingNotes = NO;
+
+ for (NSInteger i = 0; i < [lines count]; i++) {
+ NSString *line = [lines objectAtIndex:i];
+
+ // The first line is always interpreted as the title. No matter what. Notes start with >>
+ if (i == 0) {
+ NSRange rangeOfSeparator = [line rangeOfString:@">>"];
+ if (rangeOfSeparator.location != NSNotFound) {
+ NSString *title = [line substringToIndex:rangeOfSeparator.location];
+ NSString *notesPart = [line substringFromIndex:rangeOfSeparator.location + 2];
+
+ if ([title length] > 0) {
+ [result setObject:title forKey:@"title"];
+ }
+
+ if ([notesPart length] > 0) {
+ notes = [[NSMutableString alloc] initWithString:notesPart];
+ processingNotes = YES;
+ }
+ } else {
+ if ([line length] > 0) {
+ [result setObject:line forKey:@"title"];
+ }
+ }
+ } else {
+ if ([line length] > 0) {
+ unichar firstChar = [line characterAtIndex:0];
+
+ // Once we encounter a special line, we stop parsing notes.
+ if (firstChar == '#' || firstChar == '@' || firstChar == '!') {
+ processingNotes = NO;
+
+ NSString *content = nil;
+ if ([line length] > 1) {
+ content = [line substringFromIndex:1];
+ }
+
+ if (content && [content length] > 0) {
+ switch (firstChar) {
+ case '#':
+ if (![result objectForKey:@"tags"]) {
+ [result setObject:content forKey:@"tags"];
+ }
+ break;
+ case '@':
+ if (![result objectForKey:@"when"]) {
+ [result setObject:content forKey:@"when"];
+ }
+ break;
+ case '!':
+ if (![result objectForKey:@"deadline"]) {
+ [result setObject:content forKey:@"deadline"];
+ }
+ break;
+ }
+ }
+ } else if (processingNotes) {
+ if (notes) {
+ [notes appendString:@"\n"];
+ [notes appendString:line];
+ } else {
+ notes = [[NSMutableString alloc] initWithString:line];
+ }
+ }
+ } else if (processingNotes) {
+ // This is an empty notes line
+ if (notes) {
+ [notes appendString:@"\n"];
+ }
+ }
+ }
+ }
+
+ if (notes && [notes length] > 0) {
+ [result setObject:[NSString stringWithString:notes] forKey:@"notes"];
+ }
+
+ if (notes) {
+ [notes release];
+ }
+
+ if ([result count] == 0) {
+ [result release];
+ return nil;
+ }
+
+ return [result autorelease];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END