package org.karmaexchange.provider; import static org.karmaexchange.util.Properties.Property.FACEBOOK_APP_ID; import static org.karmaexchange.util.Properties.Property.FACEBOOK_APP_SECRET; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import lombok.Data; import lombok.Getter; import org.karmaexchange.auth.AuthProviderCredentials; import org.karmaexchange.auth.AuthProviderType; import org.karmaexchange.auth.GlobalUid; import org.karmaexchange.auth.GlobalUidType; import org.karmaexchange.dao.Address; import org.karmaexchange.dao.AgeRange; import org.karmaexchange.dao.Gender; import org.karmaexchange.dao.GeoPtWrapper; import org.karmaexchange.dao.ImageProviderType; import org.karmaexchange.dao.Organization; import org.karmaexchange.dao.PageRef; import org.karmaexchange.dao.User; import org.karmaexchange.dao.User.RegisteredEmail; import org.karmaexchange.resources.msg.ErrorResponseMsg; import org.karmaexchange.resources.msg.ErrorResponseMsg.ErrorInfo; import org.karmaexchange.util.Properties; import com.google.appengine.api.datastore.GeoPt; import com.restfb.DefaultFacebookClient; import com.restfb.Facebook; import com.restfb.FacebookClient; import com.restfb.FacebookClient.AccessToken; import com.restfb.Parameter; import com.restfb.exception.FacebookException; import com.restfb.exception.FacebookOAuthException; import com.restfb.types.Location; import com.restfb.types.Page; public final class FacebookSocialNetworkProvider implements SocialNetworkProvider { private static final Logger logger = Logger.getLogger(FacebookSocialNetworkProvider.class.getName()); private static final String PROFILE_IMAGE_URL_FMT = "https://graph.facebook.com/%s/picture"; public static final String PAGE_BASE_URL = "https://www.facebook.com/"; @Override public CredentialVerificationResult verifyUserCredentials(AuthProviderCredentials userCredentials, HttpServletRequest req) { DefaultFacebookClient fbClient = new DefaultFacebookClient(userCredentials.getToken()); com.restfb.types.User fbUser = fetchObject(fbClient, "me", com.restfb.types.User.class, Parameter.with("fields", "id")); return new CredentialVerificationResult( new GlobalUid(GlobalUidType.toGlobalUidType(AuthProviderType.FACEBOOK), fbUser.getId()), new FacebookCredentialVerificationCtx(userCredentials.getToken())); } @Override public UserInfo createUser(CredentialVerificationResult verificationResult) { FacebookCredentialVerificationCtx ctx = (FacebookCredentialVerificationCtx) verificationResult.getVerificationCtx(); DefaultFacebookClient fbClient = new DefaultFacebookClient(ctx.getAuthToken()); // Getting age_range unfortunately requires explicitly specifiying the fields. ExtFbUser fbUser = fetchObject(fbClient, "me", ExtFbUser.class, Parameter.with("fields", "id, first_name, last_name, email, location, age_range, gender")); User user = User.create(); user.setFirstName(fbUser.getFirstName()); user.setLastName(fbUser.getLastName()); if (fbUser.getEmail() != null) { user.getRegisteredEmails().add(new RegisteredEmail(fbUser.getEmail(), true)); } user.setAddress(parseCity(fbUser)); user.setGender(parseGender(fbUser)); user.setAgeRange(parseAgeRange(fbUser)); return new UserInfo(user, new UserInfo.ProfileImage( ImageProviderType.FACEBOOK, getProfileImageUrl(fbUser.getId())) ); } private static Address parseCity(com.restfb.types.User fbUser) { if ((fbUser.getLocation() != null) && (fbUser.getLocation().getName() != null)) { return parseCity(fbUser.getLocation().getName()); } else { return null; } } public static Address parseCity(String currentCity) { Address address = new Address(); String[] cityState = currentCity.split(","); address.setCity(cityState[0].trim()); if (cityState.length > 1) { address.setState(cityState[1].trim()); } // TODO(avaliani): use the fb location id to get a complete address. return address; } private static Gender parseGender(com.restfb.types.User fbUser) { return (fbUser.getGender() == null) ? null : Gender.valueOf(fbUser.getGender().toUpperCase()); } private static AgeRange parseAgeRange(ExtFbUser fbUser) { ExtFbUser.FbAgeRange fbAgeRange = fbUser.getAgeRange(); if (fbAgeRange == null) { return null; } if (fbAgeRange.min == null) { logger.log(Level.WARNING, "Failed to parse facebook age_range: no min" + fbAgeRange); return null; } return new AgeRange(fbAgeRange.min, fbAgeRange.max); } public static String getProfileImageUrl(String fbUserId) { return String.format(PROFILE_IMAGE_URL_FMT, fbUserId); } @Override public void initOrganization(Organization org, String fbPageName) { DefaultFacebookClient fbClient = new DefaultFacebookClient(getAppCredentials().getToken()); // Getting age_range unfortunately requires explicitly specifiying the fields. Page fbPage = fetchObject(fbClient, fbPageName, Page.class); // Relying on fb page name uniqueness org.setName(Organization.orgIdToName(fbPageName)); org.setOrgName(fbPage.getName()); org.setPage(PageRef.create(fbPageName, PAGE_BASE_URL + fbPageName, SocialNetworkProviderType.FACEBOOK)); String mission = null; if (fbPage.getMission() != null) { mission = fbPage.getMission(); } else if (fbPage.getAbout() != null) { mission = fbPage.getAbout(); } org.setMission(mission); org.setAddress(getAddress(fbPage.getLocation())); } private Address getAddress(@Nullable Location fbLocation) { if (fbLocation == null) { return null; } Address address = new Address(); address.setStreet(fbLocation.getStreet()); address.setCity(fbLocation.getCity()); address.setState(fbLocation.getState()); address.setCountry(fbLocation.getCountry()); address.setZip(fbLocation.getZip()); if ((fbLocation.getLatitude() != null) && (fbLocation.getLongitude() != null)) { address.setGeoPt(GeoPtWrapper.create( new GeoPt(fbLocation.getLatitude().floatValue(), fbLocation.getLongitude().floatValue()))); } return address; } public static <T> T fetchObject(FacebookClient fbClient, String name, Class<T> objClass, Parameter... parameters) { try { return fbClient.fetchObject(name, objClass, parameters); } catch(FacebookException e) { if (e instanceof FacebookOAuthException) { throw ErrorResponseMsg.createException(e.getMessage(), ErrorInfo.Type.AUTHENTICATION); } if (e.getMessage().toLowerCase().contains("unsupported get request")) { // This seems to be thrown when there is a permissions issue. // http://stackoverflow.com/questions/6843796/graph-api-returns-false-or-unsupported-get-request-accessing-public-facebook throw ErrorResponseMsg.createException( "Access to faceboook object '" + name + "' is restricted", ErrorInfo.Type.PARTNER_SERVICE_FAILURE); } throw ErrorResponseMsg.createException(e, ErrorInfo.Type.PARTNER_SERVICE_FAILURE); } } private static AuthProviderCredentials getAppCredentials() { String appId = Properties.get(FACEBOOK_APP_ID); String appSecret = Properties.get(FACEBOOK_APP_SECRET); AccessToken accessToken = new DefaultFacebookClient().obtainAppAccessToken(appId, appSecret); return new AuthProviderCredentials(accessToken.getAccessToken()); } @Data private static class FacebookCredentialVerificationCtx implements CredentialVerificationCtx { private final String authToken; } private static class ExtFbUser extends com.restfb.types.User { private static final long serialVersionUID = 1L; @Facebook("age_range") @Getter private FbAgeRange ageRange; @Data public static class FbAgeRange { @Facebook private Integer min; @Facebook private Integer max; } } }