diff options
| -rw-r--r-- | Info.plist | 43 | ||||
| -rw-r--r-- | QSThingsPlugin.xcodeproj/project.pbxproj | 2 | ||||
| -rw-r--r-- | QSThingsPlugin/Actions/CompleteTaskAction.m | 7 | ||||
| -rw-r--r-- | QSThingsPlugin/Actions/CreateTaskAction.h | 13 | ||||
| -rw-r--r-- | QSThingsPlugin/Actions/CreateTaskAction.m | 47 | ||||
| -rw-r--r-- | QSThingsPlugin/Helpers/KeychainHelper.m | 117 | ||||
| -rw-r--r-- | QSThingsPlugin/Helpers/SQLHelper.m | 1 | ||||
| -rw-r--r-- | QSThingsPlugin/Helpers/TextParsingHelper.h | 22 | ||||
| -rw-r--r-- | QSThingsPlugin/Helpers/TextParsingHelper.m | 111 |
9 files changed, 297 insertions, 66 deletions
@@ -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 |