Plugins/Displays/SMS/GrowlSMSDisplay.m
author Rudy Richter
Sat Aug 01 20:43:39 2009 -0400 (2009-08-01)
changeset 4259 0e9b6b0b1e25
parent 4246 4f52d1d98978
child 4666 59b81a267426
child 4819 2e39ce17d1ea
permissions -rw-r--r--
Plugins: clang warnings
     1 //
     2 //  GrowlSMSDisplay.m
     3 //  Growl Display Plugins
     4 //
     5 //  Created by Diggory Laycock
     6 //  Copyright 2005-2006 The Growl Project All rights reserved.
     7 //
     8 #import "GrowlSMSDisplay.h"
     9 #import "GrowlSMSPrefs.h"
    10 #import "NSStringAdditions.h"
    11 #import "GrowlDefinesInternal.h"
    12 #import "GrowlApplicationNotification.h"
    13 #include <Security/SecKeychain.h>
    14 #include <Security/SecKeychainItem.h>
    15 
    16 #define keychainServiceName "GrowlSMS"
    17 #define keychainAccountName "SMSWebServicePassword"
    18 
    19 #define GrowlSMSPrefDomain		@"com.Growl.SMS"
    20 #define accountNameKey			@"SMS - Account Name"
    21 #define accountAPIIDKey			@"SMS - Account API ID"
    22 #define destinationNumberKey	@"SMS - Destination Number"
    23 
    24 
    25 @implementation GrowlSMSDisplay
    26 
    27 - (id) init {
    28 	if ((self = [super init])) {
    29 		commandQueue = [[NSMutableArray alloc] init];
    30 		xmlHoldingStringValue = [[NSMutableString alloc] init];
    31 		waitingForResponse = NO;
    32 		creditBalance = 0.0;
    33 	}
    34 	return self;
    35 }
    36 
    37 - (void) dealloc {
    38 	[commandQueue release];
    39 
    40 	[preferencePane release];
    41 	[super dealloc];
    42 }
    43 
    44 - (NSPreferencePane *) preferencePane {
    45 	if (!preferencePane)
    46 		preferencePane = [[GrowlSMSPrefs alloc] initWithBundle:[NSBundle bundleWithIdentifier:@"com.Growl.SMS"]];
    47 	return preferencePane;
    48 }
    49 
    50 - (void) displayNotification:(GrowlApplicationNotification *)notification {
    51 	NSString	*accountNameValue = nil;
    52 	NSString	*apiIDValue = nil;
    53 	NSString	*destinationNumberValue = nil;
    54 
    55 	READ_GROWL_PREF_VALUE(destinationNumberKey, GrowlSMSPrefDomain, NSString *, &destinationNumberValue);
    56 	if(destinationNumberValue)
    57 		CFMakeCollectable(destinationNumberValue);
    58 	[destinationNumberValue autorelease];
    59 	READ_GROWL_PREF_VALUE(accountAPIIDKey, GrowlSMSPrefDomain, NSString *, &apiIDValue);
    60 	if(apiIDValue)
    61 		CFMakeCollectable(apiIDValue);
    62 	[apiIDValue autorelease];
    63 	READ_GROWL_PREF_VALUE(accountNameKey, GrowlSMSPrefDomain, NSString *, &accountNameValue);
    64 	if(accountNameValue)
    65 		CFMakeCollectable(accountNameValue);
    66 	[accountNameValue autorelease];
    67 
    68 	if (!([destinationNumberValue length] && [apiIDValue length] && [accountNameValue length])) {
    69 		NSLog(@"SMS display: Cannot send SMS - not enough details in preferences.");
    70 		return;
    71 	}
    72 
    73 	NSDictionary *noteDict = [notification dictionaryRepresentation];
    74 	NSString *title = [noteDict objectForKey:GROWL_NOTIFICATION_TITLE];
    75 	NSString *desc = [noteDict objectForKey:GROWL_NOTIFICATION_DESCRIPTION];
    76 
    77 	//	Fetch the SMS password from the keychain
    78 	unsigned char *password;
    79 	UInt32 passwordLength;
    80 	OSStatus status;
    81 	status = SecKeychainFindGenericPassword(NULL,
    82 											(UInt32)strlen(keychainServiceName), keychainServiceName,
    83 											(UInt32)strlen(keychainAccountName), keychainAccountName,
    84 											&passwordLength, (void **)&password, NULL);
    85 
    86 	CFStringRef passwordString;
    87 	if (status == noErr) {
    88 		passwordString = CFStringCreateWithBytes(kCFAllocatorDefault, password, passwordLength, kCFStringEncodingUTF8, false);
    89 		SecKeychainItemFreeContent(NULL, password);
    90 	} else {
    91 		if (status != errSecItemNotFound)
    92 			NSLog(@"SMS display: Failed to retrieve SMS Account password from keychain. Error: %d", status);
    93 		passwordString = CFSTR("");
    94 	}
    95 
    96 
    97 	NSString *localHostName = [[NSHost currentHost] name];
    98 	NSString *smsSendCommand = [[NSString alloc] initWithFormat:
    99 		@"<clickAPI><sendMsg><api_id>%@</api_id><user>%@</user><password>%@</password><to>+%@</to><text>(%@) %@ (Growl from %@)</text><from>Growl</from></sendMsg></clickAPI>",
   100 		apiIDValue,
   101 		accountNameValue,
   102 		passwordString,
   103 		destinationNumberValue,
   104 		title,
   105 		desc,
   106 		localHostName];
   107 
   108 //	NSLog(@"SMS Display...  %@" , smsSendCommand);
   109 	[self sendXMLCommand:smsSendCommand];
   110 	[smsSendCommand release];
   111 
   112 	//	Check credit balance.
   113 	NSString *checkBalanceCommand = [[NSString alloc] initWithFormat:
   114 		@"<clickAPI><getBalance><api_id>%@</api_id><user>%@</user><password>%@</password></getBalance></clickAPI>",
   115 		apiIDValue,
   116 		accountNameValue,
   117 		passwordString];
   118 
   119 	CFRelease(passwordString);
   120 
   121 	[self sendXMLCommand:checkBalanceCommand];
   122 	[checkBalanceCommand release];
   123 
   124 	id clickContext = [noteDict objectForKey:GROWL_NOTIFICATION_CLICK_CONTEXT];
   125 	if (clickContext) {
   126 		NSDictionary *userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
   127 			[noteDict objectForKey:@"ClickHandlerEnabled"], @"ClickHandlerEnabled",
   128 			clickContext,                                   GROWL_KEY_CLICKED_CONTEXT,
   129 			[noteDict objectForKey:GROWL_APP_PID],          GROWL_APP_PID,
   130 			nil];
   131 		[[NSNotificationCenter defaultCenter] postNotificationName:GROWL_NOTIFICATION_TIMED_OUT
   132 															object:[notification applicationName]
   133 														  userInfo:userInfo];
   134 		[userInfo release];
   135 	}
   136 
   137 }
   138 
   139 
   140 #pragma mark -
   141 #pragma mark Accessors
   142 
   143 - (NSData *) responseData {
   144 	return responseData;
   145 }
   146 
   147 - (void) setResponseData:(NSData *)newResponseData {
   148 	[newResponseData retain];
   149 	[responseData release];
   150 	responseData = newResponseData;
   151 
   152 //	NSLog(@"SMS display: responseData:  %@", responseData);
   153 }
   154 
   155 
   156 #pragma mark -
   157 #pragma mark Instance Methods
   158 
   159 
   160 /*
   161  <clickAPI>
   162 	 <sendMsg>
   163 		 <api_id>your_api_id</api_id>
   164 		 <user>your_user_name</user>
   165 		 <password>your_pass</password>
   166 		 <to>+12343455667</to>
   167 		 <text>Test text message.</text>
   168 		 <from>Growl</from>
   169 	 </sendMsg>
   170  </clickAPI>
   171 
   172 
   173  API URL:
   174  ==========
   175  https://api.clickatell.com/xml/xml
   176  <input name="data" type="text" value="<clickAPI>$your_xml_data</clickAPI>">
   177 
   178  //	To do - use the unicode option - when needed - although, it halves the length of SMS we can send.
   179 
   180  */
   181 - (void) sendXMLCommand:(NSString *)commandString {
   182 	CFStringRef			dataString = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("data=%@"), commandString);
   183 	CFDataRef			postData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, dataString, kCFStringEncodingUTF8, 0U);
   184 	CFURLRef			clickatellURL = CFURLCreateWithString(kCFAllocatorDefault, CFSTR("https://api.clickatell.com/xml/xml"), NULL);
   185 	NSMutableURLRequest	*post = [[NSMutableURLRequest alloc] initWithURL:(NSURL *)clickatellURL];
   186 	CFStringRef			contentLength = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%u"), CFDataGetLength(postData));
   187 
   188 //	NSLog(@"SMS display: Sending data: %@", postData);
   189 
   190 	[post addValue:(NSString *)contentLength forHTTPHeaderField: @"Content-Length"];
   191 	[post setHTTPMethod:@"POST"];
   192 	[post setHTTPBody:(NSData *)postData];
   193 	[commandQueue addObject:post];
   194 	[post release];
   195 
   196 	CFRelease(postData);
   197 	CFRelease(dataString);
   198 	CFRelease(clickatellURL);
   199 	CFRelease(contentLength);
   200 
   201 	[self processQueue];
   202 }
   203 
   204 
   205 - (void) processQueue {
   206 	// NSLog(@"SMS display: Processing HTTP Command Queue");
   207 	if (![commandQueue count]) {
   208 		// NSLog(@"SMS display: Queue is empty...");
   209 		return;
   210 	}
   211 
   212 	if (!waitingForResponse) {
   213 		waitingForResponse = YES;
   214 //		NSLog(@"SMS display: Beginning Command Request Connection...");
   215 		[NSURLConnection connectionWithRequest:[commandQueue objectAtIndex:0U] delegate: self];
   216 	} else {
   217 		NSLog(@"SMS display: Holding request in queue - we are still waiting for an existing command's resonse..");
   218 	}
   219 }
   220 
   221 
   222 - (void) connectionDidRespond {
   223 //	NSLog(@"SMS display: Request/Response transaction complete...");
   224 	waitingForResponse = NO;
   225 	[commandQueue removeObjectAtIndex:0U];
   226 	[self processQueue];
   227 }
   228 
   229 - (void) handleResponse {
   230 	if (responseParser)
   231 		[responseParser release];
   232 	responseParser = [[NSXMLParser alloc] initWithData:[self responseData]];
   233 	[responseParser setDelegate:self];
   234 	[responseParser setShouldResolveExternalEntities:YES];
   235 	[responseParser parse]; // return value not used
   236 							// if not successful, delegate is informed of error}
   237 }
   238 
   239 
   240 #pragma mark -
   241 #pragma mark NSXMLParser Delegate methods:
   242 
   243 - (void) parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
   244 #pragma unused(parser,namespaceURI,qName,attributeDict)
   245 	if ([elementName isEqualToString:@"clickAPI"]) {
   246 //		NSLog(@"SMS display: Found the clickAPI element in the response.  That means we got the HTTP part right.");
   247 	} else if ([elementName isEqualToString:@"xmlErrorResp"]) {
   248 		NSLog(@"SMS display: Oh Noes! we got an error back from clickatell - we passed them a bad XML request...");
   249 	} else if ([elementName isEqualToString:@"fault"]) {
   250 		NSLog(@"SMS display: Here comes the fault:...");
   251 	} else if ([elementName isEqualToString:@"getBalanceResp"]) {
   252 //		NSLog(@"SMS display: Here comes the Balance response:...");
   253 		inBalanceResponseElement = YES;
   254 	} else if ([elementName isEqualToString:@"ok"]) {
   255 //		NSLog(@"SMS display: Command Success.");
   256 		if (inBalanceResponseElement) {
   257 //			NSLog(@"SMS display: Here comes the Balance value:...");
   258 		}
   259 	} else if ([elementName isEqualToString:@"sendMsgResp"]) {
   260 //		NSLog(@"SMS display: Here comes the Message Send response:...");
   261 		inMessageSendResponseElement = YES;
   262 	}
   263 }
   264 
   265 
   266 - (void) parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
   267 #pragma unused(parser)
   268 	if (!xmlHoldingStringValue)
   269 		xmlHoldingStringValue = [[NSMutableString alloc] initWithCapacity:50];
   270 	[xmlHoldingStringValue appendString:string];
   271 }
   272 
   273 
   274 - (void) parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
   275 #pragma unused(parser,namespaceURI,qName)
   276 	if (   [elementName isEqualToString:@"clickAPI"]
   277 		|| [elementName isEqualToString:@"xmlErrorResp"]) {
   278 		// nothing to do
   279 		return;
   280 	} else if ([elementName isEqualToString:@"getBalanceResp"]) {
   281 		inBalanceResponseElement = NO;
   282 		[xmlHoldingStringValue release];
   283 		xmlHoldingStringValue = nil;
   284 	} else if ([elementName isEqualToString:@"sendMsgResp"]) {
   285 		inMessageSendResponseElement = NO;
   286 		[xmlHoldingStringValue release];
   287 		xmlHoldingStringValue = nil;
   288 	} else if ([elementName isEqualToString:@"fault"]) {
   289 		NSLog(@"SMS display: The fault was: %@" , [xmlHoldingStringValue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] );
   290 		[xmlHoldingStringValue release];
   291 		xmlHoldingStringValue = nil;
   292 	} else if ([elementName isEqualToString:@"ok"]) {
   293 		if (inBalanceResponseElement) {
   294 			creditBalance = [[xmlHoldingStringValue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] floatValue];
   295 			NSLog(@"SMS display: Your Balance is: %4.1f 'credits'" , creditBalance);
   296 			[xmlHoldingStringValue release];
   297 			xmlHoldingStringValue = nil;
   298 		}
   299 	} else if ([elementName isEqualToString:@"apiMsgId"]) {
   300 		if (inMessageSendResponseElement) {
   301 			NSLog(@"SMS display: Your SMS Message has been sent by Clickatell (messageId: %@)" , [xmlHoldingStringValue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]);
   302 			[xmlHoldingStringValue release];
   303 			xmlHoldingStringValue = nil;
   304 		}
   305 	} else if ([elementName isEqualToString:@"sequence_no"]) {
   306 		if (inMessageSendResponseElement) {
   307 //			NSLog(@"SMS display: sequence_no: %@" , [xmlHoldingStringValue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]);
   308 			[xmlHoldingStringValue release];
   309 			xmlHoldingStringValue = nil;
   310 		}
   311 	} else {
   312 		NSLog(@"SMS display: unknown XML element: %@", elementName);
   313 	}
   314 }
   315 
   316 - (void) parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError {
   317 	NSLog(@"SMS display: Error Parsing XML response from SMS Gateway - %i, Description: %@, Line: %i, Column: %i",	[parseError code],
   318 		  [[parser parserError] localizedDescription],
   319 		  [parser lineNumber],
   320 		  [parser columnNumber]);
   321 }
   322 
   323 
   324 #pragma mark -
   325 #pragma mark NSURLConnection Delegate methods:
   326 
   327 
   328 /*
   329 	The delegate receives this message if connection has cancelled the authentication challenge specified by challenge.
   330  */
   331 - (void) connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
   332 #pragma unused(connection,challenge)
   333 	NSLog(@"SMS display: didCancelAuthenticationChallenge:");
   334 	[self connectionDidRespond];
   335 }
   336 
   337 
   338 /*
   339 	The delegate receives this message if connection has failed to load the request successfully. The details of the failure are specified in error.
   340 	Once the delegate receives this message, it will receive no further messages for connection.
   341  */
   342 - (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
   343 #pragma unused(connection)
   344 	NSLog(@"SMS display: Connection to SMS Web API failed: (%@)", [error localizedDescription]);
   345 
   346 	[self connectionDidRespond];
   347 }
   348 
   349 
   350 /*
   351  The delegate receives this message when connection must authenticate challenge in order to download the request. This method gives the delegate the opportunity to determine the course of action taken for the challenge: provide credentials, continue without providing credentials or cancel the authentication challenge and the download.
   352  The delegate can determine the number of previous authentication challenges by sending the message previousFailureCount to challenge.
   353 
   354  If the previous failure count is 0 and the value returned by proposedCredential is nil, the delegate can create a new NSURLCredential object, providing a user name and password, and send a useCredential:forAuthenticationChallenge: message to [challenge sender], passing the credential and challenge as parameters. If proposedCredential is not nil, the value is a credential from the URL or the shared credential storage that can be provided to the user as feedback.
   355  The delegate may decide to abandon further attempts at authentication at any time by sending [challenge sender] a continueWithoutCredentialForAuthenticationChallenge: or a cancelAuthenticationChallenge: message. The specific action will be implementation dependent.
   356 
   357  If the delegate implements this method, the download will suspend until [challenge sender] is sent one of the following messages: useCredential:forAuthenticationChallenge:, continueWithoutCredentialForAuthenticationChallenge: or cancelAuthenticationChallenge:.
   358  If the delegate does not implement this method the default implementation is used. If a valid credential for the request is provided as part of the URL, or is available from the NSURLCredentialStorage the [challenge sender] is sent a useCredential:forAuthenticationChallenge: with the credential. If the challenge has no credential or the credentials fail to authorize access, then continueWithoutCredentialForAuthenticationChallenge: is sent to [challenge sender] instead.
   359 
   360  See Also: – cancelAuthenticationChallenge:, – continueWithoutCredentialForAuthenticationChallenge:, – useCredential:forAuthenticationChallenge:
   361  */
   362 - (void) connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
   363 	NSLog(@"SMS display: didReceiveAuthenticationChallenge: %@", challenge);
   364 	//	It doesn't need web auth currently - so we're not going to handle this case.
   365 	[connection cancel];
   366 	//	[self connectionDidRespond];
   367 }
   368 
   369 
   370 /*
   371 	The delegate receives this message as connection loads data incrementally. The delegate should concatenate the contents of each data object delivered to build up the complete data for a URL load.
   372 	This method provides the only way for an asynchronous delegate to retrieve the loaded data. It is the responsibility of the delegate to retain or copy this data as it is delivered.
   373  */
   374 - (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
   375 #pragma unused(connection)
   376 	//	NSLog(@"SMS display: didReceiveData:  %@", data);
   377 	[self setResponseData: data];
   378 	[self handleResponse];
   379 }
   380 
   381 
   382 /*
   383 	The delegate receives this message when the URL loading system has received sufficient load data for connection to construct the NSURLResponse object, response.
   384  The response is immutable and will not be modified by the URL loading system once it is presented to the delegate.
   385 
   386  In rare cases, for example in the case of a HTTP load where the content type of the load data is multipart/x-mixed-replace, the delegate will receive more than one connection:didReceiveResponse: message.
   387  In the event this occurs, delegates should discard all data previously delivered by connection:didReceiveData:, and should be prepared to handle the, potentially different, MIME type reported by the NSURLResponse.
   388 
   389  Note that the only case where this message is not sent to the delegate is when the protocol implementation encounters an error before a response could be created.
   390 
   391  */
   392 
   393 - (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
   394 #pragma unused(connection,response)
   395 //	NSLog(@"SMS display: didReceiveResponse:  URL(%@) expectedDataLength:(%d)", [response URL], [response expectedContentLength]  );
   396 
   397 //	NSLog(@" MIME:(%@)" , [response MIMEType]);
   398 //	NSLog(@" textEncoding:(%@)" , [response textEncodingName]);
   399 }
   400 
   401 
   402 - (NSCachedURLResponse *) connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
   403 #pragma unused(connection,cachedResponse)
   404 	//	No Caching please...  Since we're using HTTPS none should occur - but no harm in being cautious.
   405 	return nil;
   406 }
   407 
   408 
   409 /*
   410 
   411  The delegate receives this message when connection determines that it must change URLs in order to continue loading a request.
   412  The delegate should inspect the redirected request specified by request and copy and modify request as necessary to change its attributes, or return request unmodified.
   413  The NSURLResponse that caused the redirect is specified by redirectResponse.
   414  The redirectResponse will be nil in cases where this method is not being sent as a result of involving the delegate in redirect processing.
   415  If the delegate wishes to cancel the redirect, it should call the connection object’s cancel method.
   416  Alternatively, the delegate method can return nil to cancel the redirect, and the connection will continue to process.
   417  This has special relevance in the case where redirectResponse is not nil.
   418  In this case, any data that is loaded for the connection will be sent to the delegate, and the delegate will receive a connectionDidFinishLoading or connection:didFailLoadingWithError: message, as appropriate.
   419 
   420  The delegate can receive this message as a result of transforming a request’s URL to its canonical form, or for protocol-specific reasons, such as an HTTP redirect.
   421  The delegate implementation should be prepared to receive this message multiple times.
   422 
   423  */
   424 
   425 - (NSURLRequest *) connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse {
   426 #pragma unused(request,redirectResponse, connection)
   427 //	NSLog(@"SMS display: redirectResponse:");
   428 //	[connection cancel];
   429 	return request;
   430 }
   431 
   432 
   433 // This delegate method is called when connection has finished loading successfully. The delegate will receive no further messages for connection.
   434 - (void) connectionDidFinishLoading:(NSURLConnection *)connection {
   435 #pragma unused(connection)
   436 	// NSLog(@"SMS display: connectionDidFinishLoading:");
   437 	[self connectionDidRespond];
   438 }
   439 
   440 
   441 - (BOOL) requiresPositioning {
   442 	return NO;
   443 }
   444 
   445 @end