package com.robonobo.midas;
import static com.robonobo.common.util.TextUtil.*;
import static com.robonobo.common.util.TimeUtil.*;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import com.restfb.*;
import com.restfb.types.FacebookType;
import com.restfb.types.User;
import com.robonobo.common.concurrent.CatchingRunnable;
import com.robonobo.core.api.model.Playlist;
import com.robonobo.core.api.model.UserConfig;
import com.robonobo.midas.dao.UserConfigDao;
import com.robonobo.midas.dao.UserDao;
import com.robonobo.midas.model.MidasUser;
import com.robonobo.midas.model.MidasUserConfig;
import com.robonobo.remote.service.MidasService;
import com.twmacinta.util.MD5;
@Service("facebook")
public class FacebookServiceImpl implements InitializingBean, FacebookService {
@Autowired
AppConfig appConfig;
@Autowired
UserConfigDao userConfigDao;
@Autowired
UserDao userDao;
@Autowired
MidasService midas;
@Autowired
PlatformTransactionManager transactionManager;
Log log = LogFactory.getLog(getClass());
static final long MIN_MS_BETWEEN_FB_HITS = 1000;
Lock rateLimitLock = new ReentrantLock(true);
Date lastHitFBTime = new Date(0);
String facebookVerifyTok;
public void afterPropertiesSet() throws Exception {
Random rand = new Random();
MD5 flarp = new MD5();
flarp.Update(rand.nextInt());
facebookVerifyTok = flarp.asHex();
subscribeToFBUpdates();
getUpdatedFBInfoForAllUsers();
}
/**
* Updates who knows whom based on facebook friends
*
* @throws FacebookException
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void updateFriends(MidasUser user, MidasUserConfig userCfg) {
// TODO removing friends?
// Get this user's list of friends from facebook
FacebookClient fbCli = new RateLimitFBClient(userCfg.getItem("facebookAccessToken"));
Connection<User> friends;
try {
friends = fbCli.fetchConnection("me/friends", User.class);
} catch (FacebookException e) {
log.error("Caught exception fetching friends from facebook for user id " + user.getUserId(), e);
return;
}
// For each friend in the list, see if there is a robonobo user with that facebook id
boolean changedUser = false;
for (User fbFriend : friends.getData()) {
UserConfig uc = userConfigDao.getUserConfig("facebookId", fbFriend.getId());
// If so, make a friendship between them
if (uc != null) {
long friendId = uc.getUserId();
MidasUser friend = userDao.getById(friendId);
if ((!user.getFriendIds().contains(friendId)) || (!friend.getFriendIds().contains(user.getUserId()))) {
log.info("Creating friendship between users "+user.getEmail()+" and "+friend.getEmail());
user.getFriendIds().add(friendId);
friend.getFriendIds().add(user.getUserId());
userDao.save(friend);
changedUser = true;
}
}
}
if (changedUser)
userDao.save(user);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateFacebookName(String fbId, String newName) {
MidasUserConfig muc = userConfigDao.getUserConfig("facebookId", fbId);
if (muc == null)
return;
MidasUser user = midas.getUserById(muc.getUserId());
if (user.getFriendlyName().equals(newName))
return;
// We only update the user's name if their old name was the same as their facebook name
if (user.getFriendlyName().equals(muc.getItem("facebookName"))) {
user.setFriendlyName(newName);
midas.saveUser(user);
}
muc.putItem("facebookName", newName);
midas.putUserConfig(muc);
}
@Override
public MidasUserConfig getUserConfigByFacebookId(String fbId) {
return userConfigDao.getUserConfig("facebookId", fbId);
}
@Override
public String getFacebookVerifyTok() {
return facebookVerifyTok;
}
@Override
public void postPlaylistUpdateToFacebook(MidasUserConfig muc, Playlist p, String msg) throws IOException {
String fbId = muc.getItem("facebookId");
if (fbId == null)
return;
if(msg == null)
msg = "I updated my playlist '" + p.getTitle() + "': ";
String playlistUrl = appConfig.getInitParam("shortUrlBase") + "p/" + Long.toHexString(p.getPlaylistId());
msg += playlistUrl;
postToFacebook(muc, msg);
}
@Override
public void postSpecialPlaylistToFacebook(MidasUserConfig muc, long uid, String plName, String msg) throws IOException {
String fbId = muc.getItem("facebookId");
if (fbId == null)
return;
String url = appConfig.getInitParam("shortUrlBase") + "sp/" + Long.toHexString(uid) + "/" + plName.toLowerCase();
msg += url;
postToFacebook(muc, msg);
}
@Override
public void postToFacebook(MidasUserConfig muc, String msg) throws IOException {
String fbAccessTok = muc.getItem("facebookAccessToken");
if (fbAccessTok == null)
return;
FacebookClient fbCli = new RateLimitFBClient(fbAccessTok);
FacebookType response;
try {
response = fbCli.publish("me/feed", FacebookType.class, Parameter.with("message", msg));
} catch (FacebookException e) {
throw new IOException(e);
}
log.debug(response);
}
protected void subscribeToFBUpdates() throws IOException {
final String authTokUrl = appConfig.getInitParam("facebookAuthTokenUrl");
if (isEmpty(authTokUrl)) {
log.info("Not subscribing to realtime updates from facebook, facebookAuthTokenUrl not set");
return;
}
Thread t = new Thread(new CatchingRunnable() {
public void doRun() throws Exception {
// Wait for 60 secs to let everything start - facebook needs the callback url to be there
Thread.sleep(60000L);
log.info("Getting facebook oauth token for realtime updates");
HttpClient httpCli = new HttpClient();
GetMethod get = new GetMethod(authTokUrl);
httpCli.executeMethod(get);
Pattern fbAccessTokPattern = Pattern.compile("^access_token=(.*)$");
Matcher m = fbAccessTokPattern.matcher(get.getResponseBodyAsString());
if (!m.matches())
throw new IOException("Facebook returned invalid body for access token request: "
+ get.getResponseBodyAsString());
String accessTok = m.group(1);
String subsUrl = appConfig.getInitParam("facebookSubscriptionsUrl") + "?access_token=" + accessTok;
PostMethod post = new PostMethod(subsUrl);
post.addParameter("object", "user");
post.addParameter("fields", "name,friends");
post.addParameter("callback_url", appConfig.getInitParam("facebookCallbackUrl"));
post.addParameter("verify_token", facebookVerifyTok);
log.info("Subscribing to Facebook realtime updates");
httpCli.executeMethod(post);
if (post.getStatusCode() != HttpStatus.SC_OK)
throw new IOException("Facebook returned unexpected status code for subscription: "
+ post.getStatusCode() + ", with body: " + post.getResponseBodyAsString());
log.info("Facebook subscription updated ok");
}
});
t.start();
}
protected void getUpdatedFBInfoForAllUsers() {
Thread t = new Thread(new CatchingRunnable() {
public void doRun() throws Exception {
// Wait for 90 secs to let everything settle down
Thread.sleep(90000L);
List<MidasUserConfig> fbUserCfgs = userConfigDao.getUserConfigsWithKey("facebookId");
log.info("Getting updated facebook info for " + fbUserCfgs.size() + " users");
TransactionTemplate tt = new TransactionTemplate(transactionManager);
for (final MidasUserConfig userCfg : fbUserCfgs) {
tt.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus arg0) {
FacebookClient fbCli = new RateLimitFBClient(userCfg.getItem("facebookAccessToken"));
User fbUser;
try {
fbUser = fbCli.fetchObject("me", User.class, Parameter.with("fields", "name"));
} catch (FacebookException e) {
log.error("Error fetching from facebook", e);
return;
}
String name = fbUser.getName();
updateFacebookName(userCfg.getItem("facebookId"), name);
MidasUser user = midas.getUserById(userCfg.getUserId());
updateFriends(user, userCfg);
}
});
}
log.info("Finished getting updated facebook info");
}
});
t.start();
}
@Override
public FacebookClient getFacebookClient(String accessToken) {
return new RateLimitFBClient(accessToken);
}
// rateLimitLock is fair, so threads will queue up here waiting to be allowed to hit fb
protected void rateLimit() {
rateLimitLock.lock();
try {
long elapsed = now().getTime() - lastHitFBTime.getTime();
if (elapsed < MIN_MS_BETWEEN_FB_HITS)
Thread.sleep(MIN_MS_BETWEEN_FB_HITS - elapsed);
lastHitFBTime = now();
} catch (InterruptedException justReturn) {
} finally {
rateLimitLock.unlock();
}
}
class RateLimitFBClient extends DefaultFacebookClient {
public RateLimitFBClient(String accessToken) {
super(accessToken);
}
@Override
public <T> Connection<T> fetchConnection(String connection, Class<T> connectionType, Parameter... parameters)
throws FacebookException {
rateLimit();
return super.fetchConnection(connection, connectionType, parameters);
}
@Override
public <T> T fetchObject(String object, Class<T> objectType, Parameter... parameters) throws FacebookException {
rateLimit();
return super.fetchObject(object, objectType, parameters);
}
}
}