Plugins/Purple Service/AIFacebookXMPPAccount.m
branchadium-1.5.11-merge
changeset 6013 f8d0dc659e3f
parent 5941 307f53385811
parent 6012 200a01709ba4
child 6014 fcb71cb71a3d
child 6015 2b01cc935b7c
equal deleted inserted replaced
5941:307f53385811 6013:f8d0dc659e3f
     1 //
       
     2 //  AIFacebookXMPPAccount.m
       
     3 //  Adium
       
     4 //
       
     5 //  Created by Colin Barrett on 11/18/10.
       
     6 //  Copyright 2010 __MyCompanyName__. All rights reserved.
       
     7 //
       
     8 
       
     9 #import "AIFacebookXMPPAccount.h"
       
    10 #import "AIFacebookXMPPService.h"
       
    11 #import <Adium/AIStatus.h>
       
    12 #import <Adium/AIStatusControllerProtocol.h>
       
    13 #import <Adium/AIListContact.h>
       
    14 #import "adiumPurpleCore.h"
       
    15 #import <libpurple/jabber.h>
       
    16 #import "ESPurpleJabberAccount.h"
       
    17 #import "auth_fb.h"
       
    18 #import "auth.h"
       
    19 
       
    20 #import <AIUtilities/AIKeychain.h>
       
    21 
       
    22 #import "AIFacebookXMPPOAuthWebViewWindowController.h"
       
    23 #import "JSONKit.h"
       
    24 
       
    25 #import <Adium/AIAccountControllerProtocol.h>
       
    26 #import <Adium/AIPasswordPromptController.h>
       
    27 #import <Adium/AIService.h>
       
    28 
       
    29 #import <libpurple/auth.h>
       
    30 #import "auth_fb.h"
       
    31 
       
    32 enum {
       
    33     AINoNetworkState,
       
    34     AIMeGraphAPINetworkState,
       
    35     AIPromoteSessionNetworkState
       
    36 };
       
    37 
       
    38 @interface AIFacebookXMPPAccount ()
       
    39 
       
    40 @property (nonatomic, copy) NSString *oAuthToken;
       
    41 @property (nonatomic, assign) NSUInteger networkState;
       
    42 @property (nonatomic, assign) NSURLConnection *connection; // assign because NSURLConnection retains its delegate.
       
    43 @property (nonatomic, retain) NSURLResponse *connectionResponse;
       
    44 @property (nonatomic, retain) NSMutableData *connectionData;
       
    45 
       
    46 - (void)meGraphAPIDidFinishLoading:(NSData *)graphAPIData response:(NSURLResponse *)response error:(NSError *)inError;
       
    47 - (void)promoteSessionDidFinishLoading:(NSData *)secretData response:(NSURLResponse *)response error:(NSError *)inError;
       
    48 @end
       
    49 
       
    50 @implementation AIFacebookXMPPAccount
       
    51 
       
    52 @synthesize oAuthWC;
       
    53 @synthesize migrationData;
       
    54 @synthesize oAuthToken;
       
    55 @synthesize networkState, connection, connectionResponse, connectionData;
       
    56 
       
    57 + (BOOL)uidIsValidForFacebook:(NSString *)inUID
       
    58 {
       
    59 	return ((inUID.length > 0) &&
       
    60 			[inUID stringByTrimmingCharactersInSet:[NSCharacterSet decimalDigitCharacterSet]].length == 0);
       
    61 }
       
    62 
       
    63 - (id)initWithUID:(NSString *)inUID internalObjectID:(NSString *)inInternalObjectID service:(AIService *)inService
       
    64 {
       
    65 	if ((self = [super initWithUID:inUID internalObjectID:inInternalObjectID service:inService])) {
       
    66 		if (![[self class] uidIsValidForFacebook:self.UID]) {
       
    67 			[self setValue:[NSNumber numberWithBool:YES]
       
    68 			   forProperty:@"Prompt For Password On Next Connect"
       
    69 					notify:NotifyNever];
       
    70 		}
       
    71 	}
       
    72 	
       
    73 	return self;
       
    74 }
       
    75 
       
    76 - (void)dealloc
       
    77 {
       
    78     [oAuthWC release];
       
    79     [oAuthToken release];
       
    80     
       
    81     [connection cancel];
       
    82     [connectionResponse release];
       
    83     [connectionData release];
       
    84 
       
    85     [super dealloc];
       
    86 }
       
    87 
       
    88 #pragma mark Connectivitiy
       
    89 
       
    90 - (const char*)protocolPlugin
       
    91 {
       
    92 	return "prpl-jabber";
       
    93 }
       
    94 
       
    95 - (NSString *)serverSuffix
       
    96 {
       
    97 	return @"@chat.facebook.com";
       
    98 }
       
    99 
       
   100 /* Specify a host for network-reachability-code purposes */
       
   101 - (NSString *)host
       
   102 {
       
   103 	return @"chat.facebook.com";
       
   104 }
       
   105 
       
   106 - (NSString *)apiSecretAccountName
       
   107 {
       
   108 	return [NSString stringWithFormat:@"Adium.FB.%@", [self internalObjectID]];
       
   109 }
       
   110 
       
   111 - (void)configurePurpleAccount
       
   112 {
       
   113 	[super configurePurpleAccount];
       
   114 
       
   115 	purple_account_set_username(account, self.purpleAccountName);
       
   116 	purple_account_set_string(account, "connection_security", "");
       
   117     purple_account_set_string(account, "fb_api_key", ADIUM_API_KEY);
       
   118 
       
   119 /* 
       
   120  //Uncomment along with storage code in promoteSessionDidFinishLoading::: to use the session secret. 
       
   121 	NSString *apiSecret = [[AIKeychain defaultKeychain_error:NULL] findGenericPasswordForService:self.service.serviceID
       
   122 																						 account:self.apiSecretAccountName
       
   123 																					keychainItem:NULL
       
   124 																						   error:NULL];	
       
   125 	purple_account_set_string(account, "fb_api_secret", [apiSecret UTF8String]);
       
   126 */	
       
   127 
       
   128     purple_account_set_string(account, "fb_api_secret", ADIUM_API_SECRET);
       
   129 }
       
   130 
       
   131 
       
   132 /* Add the authentication mechanism for X-FACEBOOK-PLATFORM. Note that if the server offers it,
       
   133  * it will be used preferentially over any other mechanism e.g. DIGEST-MD5. */
       
   134 - (void)setFacebookMechEnabled:(BOOL)inEnabled
       
   135 {
       
   136 	static BOOL enabledFacebookMech = NO;
       
   137 	if (inEnabled != enabledFacebookMech) {
       
   138 		if (inEnabled)
       
   139 			jabber_auth_add_mech(jabber_auth_get_fb_mech());
       
   140 		else
       
   141 			jabber_auth_remove_mech(jabber_auth_get_fb_mech());
       
   142 		
       
   143 		enabledFacebookMech = inEnabled;
       
   144 	}
       
   145 }
       
   146 
       
   147 - (void)connect
       
   148 {
       
   149 	[self setFacebookMechEnabled:YES];
       
   150 	[super connect];
       
   151 }
       
   152 
       
   153 - (void)didConnect
       
   154 {
       
   155 	[self setFacebookMechEnabled:NO];
       
   156 	[super didConnect];	
       
   157 }
       
   158 
       
   159 - (void)didDisconnect
       
   160 {
       
   161 	[self setFacebookMechEnabled:NO];
       
   162 	[super didDisconnect];	
       
   163 }
       
   164 
       
   165 - (const char *)purpleAccountName
       
   166 {
       
   167 	NSString	*userNameWithHost = nil, *completeUserName = nil;
       
   168 	BOOL		serverAppendedToUID;
       
   169 	
       
   170 	serverAppendedToUID = ([UID rangeOfString:[self serverSuffix]
       
   171 									  options:(NSCaseInsensitiveSearch | NSBackwardsSearch | NSAnchoredSearch)].location != NSNotFound);
       
   172 	
       
   173 	if (serverAppendedToUID) {
       
   174 		userNameWithHost = UID;
       
   175 	} else {
       
   176 		userNameWithHost = [UID stringByAppendingString:[self serverSuffix]];
       
   177 	}
       
   178 	
       
   179 	completeUserName = [NSString stringWithFormat:@"-%@/Adium" ,userNameWithHost];
       
   180 	
       
   181 	return [completeUserName UTF8String];
       
   182 }
       
   183 
       
   184 - (BOOL)encrypted
       
   185 {
       
   186 	return NO;
       
   187 }
       
   188 
       
   189 - (BOOL)allowAccountUnregistrationIfSupportedByLibpurple
       
   190 {
       
   191 	return NO;
       
   192 }
       
   193 
       
   194 /*!
       
   195  * @brief Password entered callback
       
   196  *
       
   197  * Callback after the user enters her password for connecting; finish the connect process.
       
   198  */
       
   199 - (void)passwordReturnedForConnect:(NSString *)inPassword returnCode:(AIPasswordPromptReturn)returnCode context:(id)inContext
       
   200 {
       
   201     if ((returnCode == AIPasswordPromptOKReturn) && (inPassword.length == 0)) {
       
   202 		/* No password retrieved from the keychain */
       
   203 		[self requestFacebookAuthorization];
       
   204 
       
   205 	} else {
       
   206 		[self setValue:nil
       
   207 		   forProperty:@"mustPromptForPasswordOnNextConnect"
       
   208 				notify:NotifyNever];
       
   209 		[super passwordReturnedForConnect:inPassword returnCode:returnCode context:inContext];
       
   210 	}
       
   211 }
       
   212 
       
   213 - (void)retrievePasswordThenConnect
       
   214 {
       
   215 	if ([self boolValueForProperty:@"Prompt For Password On Next Connect"] ||
       
   216 		[self boolValueForProperty:@"mustPromptForPasswordOnNextConnect"])
       
   217 		/* We attempted to connect, but we had incorrect authorization. Display our auth request window. */
       
   218 		[self requestFacebookAuthorization];
       
   219 
       
   220 	else {
       
   221 		/* Retrieve the user's password. Never prompt for a password, as we'll implement our own authorization handling
       
   222 		 * if the password can't be retrieved.
       
   223 		 */
       
   224 		[adium.accountController passwordForAccount:self 
       
   225 									   promptOption:AIPromptNever
       
   226 									notifyingTarget:self
       
   227 										   selector:@selector(passwordReturnedForConnect:returnCode:context:)
       
   228 											context:nil];	
       
   229 	}
       
   230 }
       
   231 
       
   232 #pragma mark Account configuration
       
   233 
       
   234 - (void)setName:(NSString *)name UID:(NSString *)inUID
       
   235 {
       
   236 	[self filterAndSetUID:inUID];
       
   237 	
       
   238 	[self setFormattedUID:name notify:NotifyNever];
       
   239 }
       
   240 
       
   241 #pragma mark Contacts
       
   242 
       
   243 /*!
       
   244  * @brief Set an alias for a contact
       
   245  *
       
   246  * Normally, we consider the name a 'serverside alias' unless it matches the UID's characters
       
   247  * However, the UID in facebook should never be presented to the user if possible; it's for internal use
       
   248  * only.  We'll therefore consider any alias a formatted UID such that it will replace the UID when displayed
       
   249  * in Adium.
       
   250  */
       
   251 - (void)updateContact:(AIListContact *)theContact toAlias:(NSString *)purpleAlias
       
   252 {
       
   253 	if (![purpleAlias isEqualToString:theContact.formattedUID] && 
       
   254 		![purpleAlias isEqualToString:theContact.UID]) {
       
   255 		[theContact setFormattedUID:purpleAlias
       
   256 							 notify:NotifyLater];
       
   257 		
       
   258 		//Apply any changes
       
   259 		[theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
       
   260 	}
       
   261 }
       
   262 
       
   263 - (NSMutableArray *)arrayOfDictionariesFromPurpleNotifyUserInfo:(PurpleNotifyUserInfo *)user_info forContact:(AIListContact *)contact
       
   264 {
       
   265 	NSMutableArray *array = [super arrayOfDictionariesFromPurpleNotifyUserInfo:user_info forContact:contact];
       
   266 	
       
   267 	NSString *displayUID = contact.UID;
       
   268 	displayUID = [displayUID stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"-"]];
       
   269 	if ([displayUID hasSuffix:@"@chat.facebook.com"])
       
   270 		displayUID = [displayUID substringToIndex:(displayUID.length - @"@chat.facebook.com".length)];
       
   271 
       
   272 	[array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
       
   273 					  AILocalizedString(@"Facebook ID", nil), KEY_KEY,
       
   274 					  displayUID, KEY_VALUE,
       
   275 					  nil]];
       
   276 
       
   277 	return array;
       
   278 }
       
   279 
       
   280 #pragma mark Authorization
       
   281 
       
   282 - (void)requestFacebookAuthorization
       
   283 {
       
   284 	self.oAuthWC = [[[AIFacebookXMPPOAuthWebViewWindowController alloc] init] autorelease];
       
   285 	self.oAuthWC.account = self;
       
   286 
       
   287 	[[NSNotificationCenter defaultCenter] postNotificationName:AIFacebookXMPPAuthProgressNotification
       
   288 														object:self
       
   289 													  userInfo:
       
   290 	 [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:AIFacebookXMPPAuthProgressPromptingUser]
       
   291 								 forKey:KEY_FB_XMPP_AUTH_STEP]];
       
   292 
       
   293 	if (![[self class] uidIsValidForFacebook:self.UID]) {
       
   294 		/* We have a UID which isn't a Facebook numeric username. That can come from:
       
   295 		 *	 1. The setup wizard
       
   296 		 *   2. Facebook-HTTP account from Adium <= 1.4.2
       
   297 		 */
       
   298 		self.oAuthWC.autoFillUsername = self.UID;
       
   299 		self.oAuthWC.autoFillPassword = [adium.accountController passwordForAccount:self];
       
   300 		self.oAuthWC.isMigrating = ![self.service.serviceID isEqualToString:FACEBOOK_XMPP_SERVICE_ID];
       
   301 
       
   302 		self.migrationData = [NSDictionary dictionaryWithObjectsAndKeys:
       
   303 							  self.UID, @"originalUID",
       
   304 							  self.service.serviceID, @"originalServiceID",
       
   305 							  nil];
       
   306 	}
       
   307 
       
   308 	[self.oAuthWC showWindow:self];
       
   309 }
       
   310 
       
   311 - (void)oAuthWebViewController:(AIFacebookXMPPOAuthWebViewWindowController *)wc didSucceedWithToken:(NSString *)token
       
   312 {
       
   313     [self setOAuthToken:token];
       
   314     
       
   315     NSString *urlstring = [NSString stringWithFormat:@"https://graph.facebook.com/me?access_token=%@", [self oAuthToken]];
       
   316     NSURL *url = [NSURL URLWithString:[urlstring stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]];
       
   317     NSURLRequest *request = [NSURLRequest requestWithURL:url];
       
   318     
       
   319     self.networkState = AIMeGraphAPINetworkState;
       
   320     self.connectionData = [NSMutableData data];
       
   321     self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
       
   322 	
       
   323 	[[NSNotificationCenter defaultCenter] postNotificationName:AIFacebookXMPPAuthProgressNotification
       
   324 														object:self
       
   325 													  userInfo:
       
   326 	 [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:AIFacebookXMPPAuthProgressContactingServer]
       
   327 								 forKey:KEY_FB_XMPP_AUTH_STEP]];	
       
   328 }
       
   329 
       
   330 - (void)oAuthWebViewControllerDidFail:(AIFacebookXMPPOAuthWebViewWindowController *)wc
       
   331 {
       
   332 	[self setOAuthToken:nil];
       
   333 
       
   334 	[[NSNotificationCenter defaultCenter] postNotificationName:AIFacebookXMPPAuthProgressNotification
       
   335 														object:self
       
   336 													  userInfo:
       
   337 	 [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:AIFacebookXMPPAuthProgressFailure]
       
   338 								 forKey:KEY_FB_XMPP_AUTH_STEP]];	
       
   339 	
       
   340 }
       
   341 
       
   342 - (void)meGraphAPIDidFinishLoading:(NSData *)graphAPIData response:(NSURLResponse *)inResponse error:(NSError *)inError
       
   343 {
       
   344     if (inError) {
       
   345         NSLog(@"error loading graph API: %@", inError);
       
   346         // TODO: indicate setup failed 
       
   347         return;
       
   348     }
       
   349     
       
   350     NSError *error = nil;
       
   351     NSDictionary *resp = [graphAPIData objectFromJSONDataWithParseOptions:JKParseOptionNone error:&error];
       
   352     if (!resp) {
       
   353         NSLog(@"error decoding graph API response: %@", error);
       
   354         // TODO: indicate setup failed
       
   355         return;
       
   356     }
       
   357     
       
   358     NSString *uuid = [resp objectForKey:@"id"];
       
   359     NSString *name = [resp objectForKey:@"name"];
       
   360     
       
   361     /* Passwords are keyed by UID, so we need to make this change before storing the password */
       
   362 	[self setName:name UID:uuid];
       
   363         
       
   364     NSString *secretURLString = [NSString stringWithFormat:@"https://api.facebook.com/method/auth.promoteSession?access_token=%@&format=JSON", [self oAuthToken]];
       
   365     NSURL *secretURL = [NSURL URLWithString:[secretURLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
       
   366     NSURLRequest *secretRequest = [NSURLRequest requestWithURL:secretURL];
       
   367 
       
   368     self.networkState = AIPromoteSessionNetworkState;
       
   369     self.connectionData = [NSMutableData data];
       
   370     self.connection = [NSURLConnection connectionWithRequest:secretRequest delegate:self];
       
   371 	
       
   372 	[[NSNotificationCenter defaultCenter] postNotificationName:AIFacebookXMPPAuthProgressNotification
       
   373 														object:self
       
   374 													  userInfo:
       
   375 	 [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:AIFacebookXMPPAuthProgressPromotingForChat]
       
   376 								 forKey:KEY_FB_XMPP_AUTH_STEP]];		
       
   377 }
       
   378 
       
   379 - (void)didCompleteFacebookAuthorization
       
   380 {
       
   381 	/* Restart the connect process; we're currently considered 'connecting', so passwordReturnedForConnect:::
       
   382 	 * isn't going to restart it for us. */
       
   383 	[self connect];
       
   384 	
       
   385 	[[NSNotificationCenter defaultCenter] postNotificationName:AIFacebookXMPPAuthProgressNotification
       
   386 														object:self
       
   387 													  userInfo:
       
   388 	 [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:AIFacebookXMPPAuthProgressSuccess]
       
   389 								 forKey:KEY_FB_XMPP_AUTH_STEP]];	
       
   390 }
       
   391 
       
   392 - (void)promoteSessionDidFinishLoading:(NSData *)secretData response:(NSURLResponse *)response error:(NSError *)inError
       
   393 {
       
   394     if (inError) {
       
   395         NSLog(@"error promoting session: %@", inError);
       
   396         // TODO: indicate setup failed
       
   397         return;
       
   398     }    
       
   399     
       
   400 
       
   401 	/* Uncomment the below to store the Session Secret in the keychain. It doesn't seem to be used.
       
   402 	 
       
   403 	 NSString *secret = [[[NSString alloc] initWithData:secretData encoding:NSUTF8StringEncoding] autorelease];
       
   404 	 secret = [secret substringWithRange:NSMakeRange(1, [secret length] - 2)]; // strip off the quotes    
       
   405 	 
       
   406 	 
       
   407 	//Delete before adding; otherwise we'll just get errSecDuplicateItem
       
   408 	[[AIKeychain defaultKeychain_error:NULL] deleteGenericPasswordForService:self.service.serviceID
       
   409 																	 account:self.apiSecretAccountName
       
   410 																	   error:NULL];
       
   411 	[[AIKeychain defaultKeychain_error:NULL] addGenericPassword:secret
       
   412 													 forService:self.service.serviceID
       
   413 														account:self.apiSecretAccountName
       
   414 												   keychainItem:NULL
       
   415 														  error:NULL];
       
   416 	 */
       
   417 	
       
   418 	NSString *sessionKey = [self oAuthToken];
       
   419 	[[adium accountController] setPassword:sessionKey forAccount:self];
       
   420 
       
   421 	/* When we're newly authorized, connect! */
       
   422 	[self passwordReturnedForConnect:sessionKey
       
   423 						  returnCode:AIPasswordPromptOKReturn
       
   424 							 context:nil];
       
   425 
       
   426 	[self didCompleteFacebookAuthorization];
       
   427 
       
   428 	self.oAuthWC = nil;
       
   429     self.oAuthToken = nil;
       
   430 }
       
   431 
       
   432 #pragma mark NSURLConnectionDelegate
       
   433 
       
   434 - (void)connection:(NSURLConnection *)inConnection didReceiveResponse:(NSURLResponse *)response
       
   435 {
       
   436     [[self connectionData] setLength:0];
       
   437     [self setConnectionResponse:response];
       
   438 }
       
   439 
       
   440 - (void)connection:(NSURLConnection *)inConnection didReceiveData:(NSData *)data
       
   441 {
       
   442     [[self connectionData] appendData:data];
       
   443 }
       
   444 
       
   445 - (void)connection:(NSURLConnection *)inConnection didFailWithError:(NSError *)error
       
   446 {
       
   447     NSUInteger state = [self networkState];
       
   448     
       
   449     [self setNetworkState:AINoNetworkState];
       
   450     [self setConnection:nil];
       
   451     [self setConnectionResponse:nil];
       
   452     [self setConnectionData:nil];    
       
   453     
       
   454     if (state == AIMeGraphAPINetworkState) {
       
   455         [self meGraphAPIDidFinishLoading:nil response:nil error:error];
       
   456     } else if (state == AIPromoteSessionNetworkState) {
       
   457         [self promoteSessionDidFinishLoading:nil response:nil error:error];
       
   458     }
       
   459 }
       
   460 
       
   461 - (void)connectionDidFinishLoading:(NSURLConnection *)inConnection
       
   462 {
       
   463     NSURLResponse *response = [[[self connectionResponse] retain] autorelease];
       
   464     NSMutableData *data = [[[self connectionData] retain] autorelease];
       
   465     NSUInteger state = [self networkState]; 
       
   466     
       
   467     [self setNetworkState:AINoNetworkState];
       
   468     [self setConnection:nil];
       
   469     [self setConnectionResponse:nil];
       
   470     [self setConnectionData:nil];
       
   471     
       
   472     if (state == AIMeGraphAPINetworkState) {
       
   473         [self meGraphAPIDidFinishLoading:data response:response error:nil];
       
   474     } else if (state == AIPromoteSessionNetworkState) {
       
   475         [self promoteSessionDidFinishLoading:data response:response error:nil];
       
   476     }    
       
   477 }
       
   478 
       
   479 @end