Quickblox user search from multiple sources - ios

I have a requirement to search QuickBlox users whose IDs match their
Facebook IDs OR
Twitter IDs OR
Login IDs
So I am firing multiple QBUser queries at once, using following code:
[QBUsers usersWithLogins:[NSArray arrayWithObject:m_searchString] delegate:self];
[QBUsers usersWithFacebookIDs:[NSArray arrayWithObject:m_searchString] delegate:self];
[QBUsers usersWithTwitterIDs:[NSArray arrayWithObject:m_searchString] delegate:self];
So for example, if I give search string as "Testuser" - it should search all users having login = Testuser, FB login = Testuser, and Twitter login = Testuser.
Based on source of request (FB/Twitter/My own app), I need to put results in different UI parts.
The problem is, I can't differentiate which result comes back for which request.
-(void)completedWithResult:(Result*)result
{
[self showActivityIndicator:NO];
// QuickBlox User creation result
if([result isKindOfClass:[QBUUserPagedResult class]])
{
// Success result
if(result.success)
{
}
}
}
I can see that above code is hit 3 times. But I don't see anything in QBUUserPagedResult class that tells me from which request this result has come.
Something like a tag for request should suffice, but I am not sure what thing it is, looking at the documentation.
Is there anything I can use?
Alternately, is there another approach to what I am trying to achieve (instead of multiple requests)?

I figured it out that context option which is an NSString works like a tag for each Quickblox request:
[QBUsers usersWithLogins:[NSArray arrayWithObject:m_searchString] delegate:self context:MY_STRING_CONSTANT];
Then in delegate function:
- (void)completedWithResult:(Result *)result context:(void *)contextInfo
{
if(result.success && [result isKindOfClass:QBUUserPagedResult.class])
{
NSString * _context = (__bridge NSString *) contextInfo;
if([_context isEqualToString:MY_STRING_CONSTANT])
{
}
}
}
As simple as that, but the documentation doesn't say much about it, or it's not visible as it should be. I had to dig into their forums to figure it out.

Related

Switch from unauth to developer authenticated cognito user - AWS iOS SDK

Overall Problem:
I have a problem using a developer authenticated identity with my front end (iOS). I know my backend produces the correct token and identityID but my refresh method never gets called. I've also looked at the sample but I get slightly confused with everything going on.
Flow Explanation:
Currently I have a login screen that has a login button. The user presses the login button, then my api class takes the credentials, encrypts the password and stores it in keychain (commented out for now since it doesn't work on simulator). My DeveloperAuthenticatedIdentityProvider is called my app BusytimeAuthenticated. I have completed all the methods (I'm using AWS lambda and DynamoDB to authenticate users so) I start with unauthenticated access which allows me to access only two methods, login and signup. Then I want to assume my authenticated user which allows me to call my other methods.
my API Code:
[AWSLogger defaultLogger].logLevel = AWSLogLevelVerbose;
id<AWSCognitoIdentityProvider> identityProvider = [[BusytimeAuthenticated alloc] initWithRegionType:AWSRegionUSEast1
identityId:nil
identityPoolId:#"SOMEIDENTITYPOOLID"
logins:#{#"SOMEPROVIDERNAME": #"SOMEUSERNAME"}
providerName:#"SOMEPROVIDERNAME"
];
credentialsProvider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:AWSRegionUSEast1
identityProvider:identityProvider
unauthRoleArn:nil
authRoleArn:nil];
configuration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSEast1
credentialsProvider:self.credentialsProvider];
AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = configuration;
[[credentialsProvider refresh] continueWithBlock:^id(BFTask *task){
[self testAuth];
return nil;
}];
my DeveloperAuthenticatedIdentityProvider code (BusytimeAuthenticated) :
#import "BusytimeAuthenticated.h"
#interface BusytimeAuthenticated()
#property (strong, atomic) NSString *providerName;
#property (strong, atomic) NSString *token;
#end
#implementation BusytimeAuthenticated
#synthesize providerName=_providerName;
#synthesize token=_token;
- (instancetype)initWithRegionType:(AWSRegionType)regionType
identityId:(NSString *)identityId
identityPoolId:(NSString *)identityPoolId
logins:(NSDictionary *)logins
providerName:(NSString *)providerName{
if (self = [super initWithRegionType:regionType identityId:identityId accountId:nil identityPoolId:identityPoolId logins:logins]) {
self.providerName = providerName;
}
return self;
}
// Return the developer provider name which you choose while setting up the
// identity pool in the Amazon Cognito Console
- (BOOL)authenticatedWithProvider {
return [self.logins objectForKey:self.providerName] != nil;
}
// If the app has a valid identityId return it, otherwise get a valid
// identityId from your backend.
- (BFTask *)getIdentityId {
// already cached the identity id, return it
if (self.identityId) {
return [BFTask taskWithResult:nil];
}
// not authenticated with our developer provider
else if (![self authenticatedWithProvider]) {
return [super getIdentityId];
}
// authenticated with our developer provider, use refresh logic to get id/token pair
else {
return [[BFTask taskWithResult:nil] continueWithBlock:^id(BFTask *task) {
if (!self.identityId) {
return [self refresh];
}
return [BFTask taskWithResult:self.identityId];
}];
}
}
// Use the refresh method to communicate with your backend to get an
// identityId and token.
- (BFTask *)refresh {
if (![self authenticatedWithProvider]) {
return [super getIdentityId];
}else{
// KeychainWrapper *keychain = [[KeychainWrapper alloc]init];
AWSLambdaInvoker *lambdaInvoker = [AWSLambdaInvoker defaultLambdaInvoker];
NSDictionary *parameters = #{#"username" : #"SOMEUSERNAME",
#"password":#"SOMEENCRYPTEDPASS",
#"isError" : #NO};
NSLog(#"Here");
[[lambdaInvoker invokeFunction:#"login" JSONObject:parameters] continueWithBlock:^id(BFTask* task) {
if (task.error) {
NSLog(#"Error: %#", task.error);
}
if (task.exception) {
NSLog(#"Exception: %#", task.exception);
}
if (task.result) {
self.identityId = [task.result objectForKey:#"IdentityId" ];
self.token = [task.result objectForKey:#"Token" ];
// [keychain mySetObject:[task.result objectForKey:#"Token" ] forKey:#"Token"];
// [keychain mySetObject:[task.result objectForKey:#"IdentityId" ] forKey:#"IdentityId"];
NSLog(#"Result: %#", task.result);
}
return [BFTask taskWithResult:self.identityId];
}];
}
return NULL;
}
#end
Summary Problem:
Unfortunately when I test my new priveleges, I see from the error: "Unauth_Role/CognitoIdentityCredentials is not authorized to perform: lambda:InvokeFunction". Clearly I'm not switching properly. I've placed a breakpoint in my refresh method to see if it's getting called. It's not. I'm not quite understanding how I switch properly. Any help with getting this to work is much appreciated.
Note: One big change I did make though is I took out the "DeveloperAuthenticationClient" class because I assumed I could do it without it.
The fundamental problem is that you are trying to call a Lambda function (which requires credentials) to get credentials. Because you are using the "default" client configuration, when your developer authenticated client comes back with a response it is going to override the credentials used to access your Lambda function. Additionally, once that id has been transitioned to authenticated, you won't be able to use it to get credentials in an unauth flow and would need to generate a new unauthenticated id just to authenticate again and then get back to your authenticated id.
I would strongly encourage you to setup API Gateway in front of your Lambda function to remove this circular dependency.
Update based on new information in the question...
A few things here:
1. Avoid code like while(!finished) to wait on an async task to complete. In the best case, this style of busy waiting will consume a CPU/core at 100% while doing nothing useful and adversely affect battery life and will only hurt performance of your app. Instead, use a notification with a block. Since you have already have a AWSTask in this instance, instead of returning nil at the end of the [credentialsProvider refresh] continueWithBlock... just call your [self testAuth] right there and do away with the finished/while code.
2. In your getIdentityId implementation the first if condition checks if there is an identityId and if there is it returns nil. I'm guessing you goal here is to cache the identityId after a successful authentication and return that so that you don't have to call your backend every time getIdentityId is called. If that is the case, pretty sure you want to return identityId instead of nil
3. I don't think this is the cause of your issue but will simplify things: As long as you've configured your identity pool with Auth/UnAuth roles in the console, you don't have to explicitly use them when initializing the AWSCognitoCredentialsProvider.
Once these are resolved if you continue to have problems, please debug the code in more detail and tell us things like the following:
Does the refresh method get called? If so, which parts of your if statement does it enter and what is the result? Does it ever enter the else block and call your backend identity provider? Does it successfully retrieve an identity id and return it?
If you get further but start experiencing a slightly different issue then please mark this question answered and post a separate question instead of continuing to edit this question. This will help keep things clear (this question/answer is getting pretty long and has changed).
Original answer to initial posted question/code... The getIdentity method of the AWSCognitoCredentialsProvider returns an AWSTask (i.e. a BFTask). So you'll need to call something like continueWithBlock in order to actually execute the method. In the first block of code above it looks like you're not doing that.

Onedrive SDK - Authentication Token is getting nil value

During OneDrive Authentication using OneDrive SDK for iOS, after the authentication screen, At times, the highlighted values of “response” objects are NIL . I’m not sure why this is happening.
Out of 10 trials, 2-3 times the values inside the Repsonse object is NIL. So this is landing the App in crash.Any guess why this is happening? May be because of poor network? Or any other thing I might have missed? In Onedrive SDK - LiveAuthRequest.m file has the following lines of code.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
id response = [LiveAuthHelper readAuthResponse:self.tokenResponseData];
if ([response isKindOfClass:[LiveConnectSession class]])
{
_client.session = response; => Crash is Happening Here*
self.session = response;
[self updateStatus:AuthTokenRetrieved];
}
else
{
self.error = response;
[self updateStatus:AuthFailed];
}
self.tokenResponseData = nil;
self.tokenConnection = nil;
}
The Response from the Onedrive Server side. 1. authentication_Token, 2.refreshToken, 3.scope 4.expires_in 5.tokenType",
iOS version:5.1.1
OneDrive SDK Version:5.0

Fetching Gmails Via Mailcore 2: Thread ID vs Message ID vs UID

I have an iPad application that allows users to access their Gmail accounts using Mailcore2. I thought I had an understanding of the difference between Gmail's Thread Id, Message ID and UID until I looked closely at what Mailcore2 is returning to me when I perform a message fetch operation. I am hoping someone can clarify my confusion.
Here is what I think I know from the Gmail docs:
1) A thread ID groups together messages (which have their own message IDs and UIDs) that are part of the same conversation
2) A UID is specific to a message and is unique only to the folder which contains it
3) A message ID is specific to a message and is unique across all folders of an account
I am also making the following assumptions:
1) A thread has a thread ID and is a collection of messages. A thread does not have a message ID or a UID.
2) A message has a message ID, UID and thread ID (even if it is the only message in a thread)
3) Fetching messages by UID fetches MESSAGES which have a UID that falls in the requested range of UIDs.
4) Messages that belong to the same thread will have different UIDs and message IDs but the same Thread ID.
Ok, so assuming that the above is correct, I would think that during a typical fetch of messages in Mailcore2 by UID, I would receive an array of emails, and from those emails I could look at their thread ID, for example and reconstruct threads on the client side. However, I seem to get back threads rather than emails. Additionally, each thread I get does not necessarily contain all its 'child' messages.
So for example, if I have two threads in my inbox, each containing five messages, Mailcore returns to me an array of 2 "emails" in the form of MCOIMAPMessages. And each "email" has one thread ID, one message ID and one UID. So I am not sure how to access contained emails on these two threads. I see that there is a references array... but inspecting this object doesn't reveal anything useful. When I log the contents of each thread, I get only part of the contents - say 4 out of the 5 messages on the thread. Not sure if this is Mailcore or an error in my saving process due to my incomplete understanding of how this is all working.
Here is my code to fetch messages:
//create fetch operation to get first (10) messages in folder (first fetch is done by sequence number, subsequent fetches are done by UID
uint64_t location = MAX([info messageCount] - DefaultPageSize + 1, 1);
uint64_t size = serverMessageCount < DefaultPageSize ? serverMessageCount - 1 : DefaultPageSize - 1;
MCOIndexSet *numbers = [MCOIndexSet indexSetWithRange:MCORangeMake(location, size)];
MCOIMAPMessagesRequestKind kind = MCOIMAPMessagesRequestKindUid |
MCOIMAPMessagesRequestKindFullHeaders |
MCOIMAPMessagesRequestKindFlags |
MCOIMAPMessagesRequestKindHeaders |
MCOIMAPMessagesRequestKindInternalDate;
if ([capabilities containsIndex:MCOIMAPCapabilityGmail]) {
kind |= MCOIMAPMessagesRequestKindGmailLabels | MCOIMAPMessagesRequestKindGmailThreadID | MCOIMAPMessagesRequestKindGmailMessageID;
self.gmailCapability = YES;
}
fetchLatestEmails ([self.imapSession fetchMessagesByNumberOperationWithFolder:folder.folderId requestKind:kind numbers:numbers]);
//perform fetch
void (^fetchLatestEmails)(MCOIMAPFetchMessagesOperation *) = ^(MCOIMAPFetchMessagesOperation *fetchOperation) {
[fetchOperation start:^(NSError *error, NSArray *emails, MCOIndexSet *vanishedMessages) {
if (nil != error) {
failure(error);
NSLog(#"the fetch error is %#", error);
return;
}
[self.dataManager performBatchedChanges:^{
if ([emails count] !=0) {
MCOIndexSet *savedThreadIds = [[MCOIndexSet alloc]init];
for (MCOIMAPMessage *email in emails) {
//do stuff with emails
Thread *thread = [self.dataManager fetchOrInsertNewThreadForFolder:folder threadId:email.gmailThreadID ?: email.gmailMessageID ?: email.uid error:nil];
if (nil != thread) {
[savedThreadIds addIndex:thread.threadId];
[self.dataManager updateOrInsertNewEmailForThread:thread uid:email.uid messageId:email.gmailMessageID date:email.header.receivedDate subject:email.header.subject from:email.header.from.mailbox to:[email.header.to valueForKey:#"mailbox"] cc:[email.header.cc valueForKey:#"mailbox"] labels:labels flags:flags error:nil];
}
if (nil != error) {
failure(error);
return;
}
}
[savedThreadIds enumerateIndexes:^(uint64_t threadId) {
[self.dataManager updateFlagsForThreadWithThreadId:threadId inFolder:folder];
}];
}
NSError *folderUpdateError;
[self.dataManager updateFolder:folder withMessageCount:serverMessageCount error:&folderUpdateError];
} error:&error];
if (nil == error) {
[self refreshFolder:folder success:^{
success();
}failure:^(NSError *error) {
}];
} else {
failure(error);
}
}];
};
Clearly something is amiss here in terms of my understanding of either Gmail or Mailcore2. If anyone can point out my misunderstanding I would appreciate it.
After opening an issue with Mailcore and a bit of research I have found the answers to my own questions.
First, my above assumptions about UID, gmailMessageID and gmailThreadID are correct. The confusion was in my looking at the Gmail conversation view of my email account on the web, and expecting my Mailcore fetch to match it. This does not happen because the conversation view, as the name implies, stitches together ALL messages of a conversation, even those that were replies to an incoming message in the inbox - ordinarily such messages would be found in the 'sent' or 'all mail' folder. In other words, what is fetched from Mailcore looks incomplete only because it is fetching what is ACTUALLY in your inbox (or whatever folder you are fetching messages from). This does NOT include replies to incoming messages. In order to include such messages (i.e. to re-create Gmail's conversation view) I understand that one must fetch sent messages from 'all mail' (or I suppose 'sent' mail) by doing a search operation using the gmailThreadID of the message of interest.
Switching the conversation view to 'off' for my Gmail account on the web while testing my Mailcore fetch operations makes testing much clearer.
Found the answer
for getting gmailThreadID we need to add MCOIMAPMessagesRequestKindGmailThreadID. in Fetch request like below example.
MCOIMAPMessagesRequestKind requestKind = (MCOIMAPMessagesRequestKind)
(MCOIMAPMessagesRequestKindHeaders | MCOIMAPMessagesRequestKindStructure |
MCOIMAPMessagesRequestKindInternalDate | MCOIMAPMessagesRequestKindHeaderSubject |
MCOIMAPMessagesRequestKindFlags | MCOIMAPMessagesRequestKindGmailThreadID);

Google OAuth Login Error: Invalid credentials

I have an iPad application which allows users to login to their Gmail account(s) using OAuth2. Thus far, the login process and email fetching is successful. However, when the app is closed and then re-opened after a (long) period of time, an error is produced "invalid credentials,' even though previous logins with the same credentials were successful.
Login Flow:
1) User logs in to gmail using OAuth 2.
2) User email address and oAuthToken provided by the GTMOAuth2Authentication object are saved to core data for future logins.
3) IMAP Session is created using saved email address and OAuthToken.
Here is the relevant code
Google Login
- (void)gmailOAuthLogin
{
NSDictionary *googleSettings = [[EmailServicesInfo emailServicesInfoDict] objectForKey:Gmail];
GTMOAuth2ViewControllerTouch *googleSignInController =
[[GTMOAuth2ViewControllerTouch alloc] initWithScope:GmailScope clientID:GmailAppClientID clientSecret:GmailClientSecret keychainItemName:KeychainItemName completionHandler:^(GTMOAuth2ViewControllerTouch *googleSignInController, GTMOAuth2Authentication *auth, NSError *error){
if (error != nil) {
//handle error
} else {
[[ModelManager sharedInstance] authenticateWithEmailAddress:[auth userEmail]
oAuthToken:[auth accessToken] imapHostname:[googleSettings objectForKey:IMAPHostName] imapPort:[[googleSettings objectForKey:IMAPPort]integerValue] smtpHostname:[googleSettings objectForKey:SMTPHostName] smtpPort:[[googleSettings objectForKey:SMTPPort]integerValue] type:EmailProtocolTypeImapAndSmtpGMail success:^(Account *account) {
//create IMAP session using above arguments
} failure:^(NSError *error) {
//handle error
}];
}
}];
[self presentGoogleSignInController:googleSignInController];
}
Create IMAP Session Using MailCore2
- (void)authenticateWithEmailAddress:(NSString *)emailAddress password:(NSString *)password oAuthToken:(NSString *)oAuthToken imapHostname:(NSString *)imapHostname imapPort:(NSInteger)imapPort smtpHostname:(NSString *)smtpHostname smtpPort:(NSInteger)smtpPort success:(void (^)())success failure:(void (^)(NSError *))failure
{
self.imapSession = [[MCOIMAPSession alloc] init];
self.imapSession.hostname = imapHostname;
self.imapSession.port = imapPort;
self.imapSession.username = emailAddress;
self.imapSession.connectionType = MCOConnectionTypeTLS;
self.imapSession.password = nil;
self.imapSession.OAuth2Token = oAuthToken;
self.imapSession.authType = nil != oAuthToken ? MCOAuthTypeXOAuth2 :
self.imapSession.authType;
[self.imapSession setConnectionLogger:^(void * connectionID, MCOConnectionLogType type,
NSData * data){
NSLog(#"MCOIMAPSession: [%i] %#", type, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
self.smtpSession = [[MCOSMTPSession alloc] init];
self.smtpSession.hostname = smtpHostname;
self.smtpSession.port = smtpPort;
self.smtpSession.username = emailAddress;
self.smtpSession.connectionType = MCOConnectionTypeTLS;
self.smtpSession.password = nil;
self.smtpSession.OAuth2Token = oAuthToken;
self.smtpSession.authType = nil != oAuthToken ? MCOAuthTypeXOAuth2 :
self.smtpSession.authType;
[self.smtpSession setConnectionLogger:^(void * connectionID, MCOConnectionLogType type, NSData * data){
NSLog(#"MCOSMTPSession: [%i] %#", type, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
[[self.imapSession checkAccountOperation] start:^(NSError *error) {
if (nil == error) {
success();
} else {
failure(error); //FAILS WITH INVALID CREDENTIALS ERROR
}
}];
}
Once again, the above code works fine, unless the application has not been used in some time. I was not sure if I needed to refresh the OAuthToken or not, so I tried doing the following on launch of the application:
GTMOAuth2Authentication *auth = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:KeychainItemName clientID:GmailAppClientID clientSecret:GmailClientSecret];
BOOL canAuthorize = [auth canAuthorize]; //returns YES
NSDictionary *googleSettings = [[EmailServicesInfo emailServicesInfoDict] objectForKey:Gmail];
[[ModelManager sharedDefaultInstance] authenticateWithEmailAddress:[auth userEmail] oAuthToken:[auth refreshToken] imapHostname:[googleSettings objectForKey:IMAPHostName] imapPort:[[googleSettings objectForKey:IMAPPort]integerValue] smtpHostname:[googleSettings objectForKey:SMTPHostName] smtpPort:[[googleSettings objectForKey:SMTPPort]integerValue] type:EmailProtocolTypeImapAndSmtpGMail success:^(Account *account) {
//create IMAP session
} failure:^(NSError *error) {
NSLog(#"failure %#", error);
}];
But I still get the same error. I have no idea why the OAuth token stops working or how to resolve this. Since the user is able to save multiple accounts, I am wondering if I need to save the refresh token for each account in core data and use that if the access token stops working?
(Disclaimer - I don't know iOS or the gtm-oauth2 libraries, but I do know Google's OAuth implementation.)
Conceptually you do need to persist the refresh token for the user. The refresh token is a long-lived credential which is used (along with your client secret) to get a short-lived access token that is used for actual API calls.
If you anticipate making multiple calls in a short period of time then your app will normally actually persist both the refresh token and access token (currently access tokens will last 1 hour).
That all said, it looks like the gtm-oauth2 library should be taking care of persisting these already (looks like authForGoogleFromKeychainForName does this).
What I think you need help with is getting an up-to-date access token that you can use to initiate your IMAP session.
The gtm-oauth2 library does contain an authorizeRequest method. It takes information about an HTTP request you intend to make and adds the appropriate authorization headers. It looks like this code will examine the state of the access token, and refresh it if necessary.
While I know you aren't able to make an HTTP request (you need to speak IMAP), my suggestion is to use this method with a dummy NSMutableURLRequest - and then, once it's finished, don't actually send the HTTP request, instead examine the headers it added and pull the access token from there.
See:
https://code.google.com/p/gtm-oauth2/wiki/Introduction#Using_the_Authentication_Tokens
Hope that helps - I don't have an iOS environment to test it on.

How to know which Facebook friends of the user have my application installed?

Is it possible to implement the following? I want to create an iOS application which allows the user to see all their Facebook friends who already have this application in their Facebook list. Is it achievable via Facebook Connect or Graph API or maybe something else?
Yes, it's possible.
You will need to maintain your own database of who has installed the application.
When a user installs the application, they connect to Facebook through one of the APIs, get their userID, and submit it to your database with the flag that the user installed the application.
You then ask Facebook through one of its APIs for that user's list of friends' IDs, and then ask your database if any of those IDs have the associated flag set.
If you have followed the Facebook tutorial iOS Tutorial.
You should have access to a "facebook" object in your AppDelegate.m file. That "facebook" object will already have been registered with your specific application id.
You can then use it to query for the current user's friends who have already installed the application.
Here is the Facebook query to retrieve friend data:
NSString *fql = [NSString stringWithFormat:#"Select name, uid, pic_small from user where is_app_user = 1 and uid in (select uid2 from friend where uid1 = %#) order by concat(first_name,last_name) asc", _replace_this_with_the_fb_id_of_the_current_user_ ];
Yes you can do it with the new Facebook SDK 3.5 and greater (2013 June) using GraphAPI.
You can use the deviceFilteredFriends mutable array later as you like...
// The Array for the filtered friends
NSMutableArray *installedFilteredFriends = [[NSMutableArray alloc] init];
// Request the list of friends of the logged in user (me)
[[FBRequest requestForGraphPath:#"me/friends?fields=installed"]
startWithCompletionHandler:
^(FBRequestConnection *connection,
NSDictionary *result,
NSError *error) {
// If we get a result with no errors...
if (!error && result)
{
// here is the result in a dictionary under a key "data"
NSArray *allFriendsResultData = [result objectForKey:#"data"];
if ([allFriendsResultData count] > 0)
{
// Loop through the friends
for (NSDictionary *friendObject in allFriendsResultData) {
// Check if installed data available
if ([friendObject objectForKey:#"installed"]) {
[installedFilteredFriends addObject: [friendObject objectForKey:#"id"]];
break;
}
}
}
}
}];
Use Facebook login and then ask for the user's friends. You will only get those that have the requesting app installed (it used to return all friends with an 'installed' flag)

Resources