package org.limewire.facebook.service;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.limewire.concurrent.ListeningFuture;
import org.limewire.concurrent.ScheduledListeningExecutorService;
import org.limewire.concurrent.ThreadExecutor;
import org.limewire.facebook.service.livemessage.AddressHandler;
import org.limewire.facebook.service.livemessage.AddressHandlerFactory;
import org.limewire.facebook.service.livemessage.AuthTokenHandler;
import org.limewire.facebook.service.livemessage.AuthTokenHandlerFactory;
import org.limewire.facebook.service.livemessage.ConnectBackRequestHandler;
import org.limewire.facebook.service.livemessage.ConnectBackRequestHandlerFactory;
import org.limewire.facebook.service.livemessage.DiscoInfoHandler;
import org.limewire.facebook.service.livemessage.DiscoInfoHandlerFactory;
import org.limewire.facebook.service.livemessage.FileOfferHandler;
import org.limewire.facebook.service.livemessage.FileOfferHandlerFactory;
import org.limewire.facebook.service.livemessage.LibraryRefreshHandler;
import org.limewire.facebook.service.livemessage.LibraryRefreshHandlerFactory;
import org.limewire.facebook.service.settings.ChatChannel;
import org.limewire.facebook.service.settings.FacebookAPIKey;
import org.limewire.facebook.service.settings.FacebookAuthServerUrls;
import org.limewire.facebook.service.settings.FacebookURLs;
import org.limewire.friend.api.ChatState;
import org.limewire.friend.api.Friend;
import org.limewire.friend.api.FriendConnection;
import org.limewire.friend.api.FriendConnectionConfiguration;
import org.limewire.friend.api.FriendConnectionEvent;
import org.limewire.friend.api.FriendException;
import org.limewire.friend.api.FriendPresence;
import org.limewire.friend.api.FriendPresenceEvent;
import org.limewire.friend.api.IncomingChatListener;
import org.limewire.friend.api.MessageReader;
import org.limewire.friend.api.MessageWriter;
import org.limewire.friend.api.MutableFriendManager;
import org.limewire.friend.api.Network;
import org.limewire.friend.api.feature.AddressFeature;
import org.limewire.friend.api.feature.AuthTokenFeature;
import org.limewire.friend.api.feature.ConnectBackRequestFeature;
import org.limewire.friend.api.feature.FeatureEvent;
import org.limewire.friend.api.feature.FileOfferFeature;
import org.limewire.friend.api.feature.LibraryChangedNotifierFeature;
import org.limewire.friend.api.feature.LimewireFeature;
import org.limewire.friend.impl.address.FriendAddress;
import org.limewire.friend.impl.util.PresenceUtils;
import org.limewire.http.httpclient.HttpClientUtils;
import org.limewire.inject.MutableProvider;
import org.limewire.listener.AsynchronousEventBroadcaster;
import org.limewire.listener.EventBroadcaster;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.security.SecurityUtils;
import com.google.code.facebookapi.FacebookException;
import com.google.code.facebookapi.FacebookJsonRestClient;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.name.Named;
/**
* Implements a {@link FriendConnection} using facebook.
* <p>
* There is no actual TCP connection kept with the facebook server. The connection
* object keeps all the state necessary to send facebook api calls to the facebook
* servers and also to listen for incoming chat messages.
*/
public class FacebookFriendConnection implements FriendConnection {
private static final Log LOG = LogFactory.getLog(FacebookFriendConnection.class);
/**
* Maximum number of consecutive HTTP GET and POST tries before IOException
* is thrown.
* The value will be changed to 1, once n tries failed consecutively.
*/
private volatile int maxTries = 2;
private static final String USER_AGENT_HEADER = "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10";
private final FacebookFriendConnectionConfiguration configuration;
private final Provider<String> apiKey;
private final ChatListenerFactory chatListenerFactory;
private final ScheduledListeningExecutorService executorService;
private final MutableProvider<String> chatChannel;
private final AtomicBoolean loggedIn = new AtomicBoolean(false);
private final AtomicBoolean loggingIn = new AtomicBoolean(false);
private final AsynchronousEventBroadcaster<FriendConnectionEvent> connectionBroadcaster;
private final Map<String, FacebookFriend> friends = Collections.synchronizedMap(new TreeMap<String, FacebookFriend>(String.CASE_INSENSITIVE_ORDER));
private final BasicCookieStore cookieStore = new BasicCookieStore();
private final AtomicReference<String> postFormID = new AtomicReference<String>();
/**
* Lock being held for adding and removing presences from friends.
*/
private final Object presenceLock = new Object();
private final EventBroadcaster<FriendPresenceEvent> friendPresenceBroadcaster;
private final AddressHandler addressHandler;
private final AuthTokenHandler authTokenHandler;
private final LibraryRefreshHandler libraryRefreshHandler;
private final ConnectBackRequestHandler connectBackRequestHandler;
private final FileOfferHandler fileOfferHandler;
private final EventBroadcaster<FeatureEvent> featureEventBroadcaster;
/**
* Adapt connection configuration to ensure the facebook user id is returned in
* {@link Network#getCanonicalizedLocalID()}.
*/
private final Network network = new Network() {
@Override
public String getCanonicalizedLocalID() {
return uid;
}
@Override
public String getNetworkName() {
return getConfiguration().getNetworkName();
}
@Override
public Type getType() {
return Type.FACEBOOK;
}
};
private final MutableFriendManager friendManager;
private final PresenceListenerFactory presenceListenerFactory;
private final FacebookFriendFactory friendFactory;
private final DiscoInfoHandlerFactory discoInfoHandlerFactory;
private final ChatManager chatManager;
/**
* Session id as part of the full presence id. Follows the lifetime of
* the connection object. A new connection object should have a new
* session id.
*/
private final String sessionId;
private FacebookJsonRestClient facebookClient;
private ChatListener chatListener;
private ScheduledFuture presenceListenerFuture;
private String logoutURL;
private DiscoInfoHandler discoInfoHandler;
private String session;
private String uid;
private String secret;
private final Provider<String[]> authUrls;
private final ClientConnectionManager httpConnectionManager;
private final Provider<Map<String, Provider<String>>> facebookURLs;
@Inject
public FacebookFriendConnection(@Assisted FacebookFriendConnectionConfiguration configuration,
@FacebookAPIKey Provider<String> apiKey,
AsynchronousEventBroadcaster<FriendConnectionEvent> connectionBroadcaster,
EventBroadcaster<FeatureEvent> featureEventBroadcaster,
EventBroadcaster<FriendPresenceEvent> friendPresenceBroadcaster,
MutableFriendManager friendManager,
AddressHandlerFactory addressHandlerFactory,
AuthTokenHandlerFactory authTokenHandlerFactory,
ConnectBackRequestHandlerFactory connectBackRequestHandlerFactory,
LibraryRefreshHandlerFactory libraryRefreshHandlerFactory,
FileOfferHandlerFactory fileOfferHandlerFactory,
PresenceListenerFactory presenceListenerFactory,
FacebookFriendFactory friendFactory,
ChatListenerFactory chatListenerFactory,
DiscoInfoHandlerFactory discoInfoHandlerFactory,
@Facebook ScheduledListeningExecutorService executorService,
@ChatChannel MutableProvider<String> chatChannel,
@FacebookAuthServerUrls Provider<String[]> authUrls,
@Named("sslConnectionManager") ClientConnectionManager httpConnectionManager,
@FacebookURLs Provider<Map<String, Provider<String>>> facebookURLs) {
this.configuration = configuration;
this.apiKey = apiKey;
this.connectionBroadcaster = connectionBroadcaster;
this.featureEventBroadcaster = featureEventBroadcaster;
this.friendPresenceBroadcaster = friendPresenceBroadcaster;
this.friendManager = friendManager;
this.presenceListenerFactory = presenceListenerFactory;
this.friendFactory = friendFactory;
this.chatListenerFactory = chatListenerFactory;
this.executorService = executorService;
this.chatChannel = chatChannel;
this.authUrls = authUrls;
this.httpConnectionManager = httpConnectionManager;
this.facebookURLs = facebookURLs;
this.addressHandler = addressHandlerFactory.create(this);
this.authTokenHandler = authTokenHandlerFactory.create(this);
this.connectBackRequestHandler = connectBackRequestHandlerFactory.create(this);
this.libraryRefreshHandler = libraryRefreshHandlerFactory.create(this);
this.fileOfferHandler = fileOfferHandlerFactory.create(this);
this.discoInfoHandlerFactory = discoInfoHandlerFactory;
this.chatManager = new ChatManager(this);
this.sessionId = createSessionId();
for (Cookie cookie : parseCookies(configuration)) {
cookieStore.addCookie(cookie);
}
}
private static String createSessionId() {
byte[] sessionId = new byte[8];
SecurityUtils.createSecureRandomNoBlock().nextBytes(sessionId);
return org.limewire.util.StringUtils.getUTF8String(Base64.encodeBase64(sessionId));
}
private void setPostFormID(String postFormID) {
this.postFormID.set(postFormID);
}
@Override
public ListeningFuture<Void> login() {
return executorService.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
loginImpl();
return null;
}
});
}
@Override
public ListeningFuture<Void> logout() {
return executorService.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
logoutImpl(true);
return null;
}
});
}
synchronized void logoutImpl(boolean forceExpireSession) {
logoutImpl(forceExpireSession, null);
}
synchronized void logoutImpl(boolean forceExpireSession, Exception e) {
closeConnection(forceExpireSession);
connectionBroadcaster.broadcast(new FriendConnectionEvent(this, FriendConnectionEvent.Type.DISCONNECTED, e));
}
/**
* Close the connection by logging out of facebook and cleaning up objects
* associated with this connection.
*
* @param forceExpireSession true if we need to or should
* attempt to expire our facebook JSON client. This should be
*/
private void closeConnection(boolean forceExpireSession) {
LOG.debug("logging out from facebook...");
loggedIn.set(false);
loggingIn.set(false);
// clean up data structures associated with this connection
cancelListeners();
// over-the-network logout activities
endChatSession(forceExpireSession);
// remove all friends
synchronized (friends) {
for (FacebookFriend friend : friends.values()) {
removeAllPresences(friend);
}
friends.clear();
}
LOG.debug("logged out from facebook.");
}
public void reconnect() throws IOException {
LOG.debug("reconnecting...");
String response = httpGET(facebookURLs.get().get(FacebookURLs.RECONNECT_URL).get() + "&post_form_id=" + postFormID.get());
LOG.debugf("reconnect response: {0}", response);
}
/**
* Performs all network related steps to ending the chat, such as
* signing out of facebook website, sending messages to other
* presences to let them know we are going offline, etc.
*
* @param expireSession same as in {@link #closeConnection}
*/
private void endChatSession(boolean expireSession) {
sendOfflinePresences();
if (expireSession || !configuration.isAutologin()) {
try {
expireSession();
} catch (FacebookException e) {
LOG.debug("error expiring facebook session", e);
} catch (IOException e) {
LOG.debug("error expiring facebook session", e);
}
// todo: what to do in case of error? what are the repercussions of not logging out of fb
try {
logoutFromFacebook();
} catch (IOException e) {
LOG.debug("logout from facebook failed", e);
}
configuration.clearCookies();
configuration.setAutoLogin(false);
}
}
/**
* Cancels presence listenting thread, chat listener, thread, and other listeners
*/
private void cancelListeners() {
// stop and remove essential listeners/handlers
if (chatListener != null) {
chatListener.setDone();
chatListener = null;
}
if (presenceListenerFuture != null) {
presenceListenerFuture.cancel(false);
presenceListenerFuture = null;
}
if (discoInfoHandler != null) {
discoInfoHandler.unregister();
discoInfoHandler = null;
}
}
private void sendOfflinePresences() {
for(FacebookFriend friend : friends.values()) {
for (FriendPresence presence : friend.getPresences().values()) {
if (presence.hasFeatures(LimewireFeature.ID)) {
Map<String, Object> message = new HashMap<String, Object>();
message.put("type", "unavailable");
try {
sendLiveMessageDirect(presence, "presence", message);
} catch (FacebookException e) {
LOG.debug("error sending offline presence notification", e);
} catch (IOException e) {
LOG.debug("error sending offline presence notification", e);
}
}
}
}
}
private void expireSession() throws FacebookException, IOException {
synchronized (this) {
if(facebookClient != null) {
try {
facebookClient.auth_expireSession();
} catch (RuntimeException re) {
handleFacebookAPIRuntimeException(re);
}
}
}
}
private void handleFacebookAPIRuntimeException(RuntimeException re) throws IOException {
//LWC-3678
if(re.getCause() == null || !(re.getCause() instanceof IOException)) {
throw re;
} else {
throw (IOException)re.getCause();
}
}
private void logoutFromFacebook() throws IOException {
List <NameValuePair> nvps = new ArrayList <NameValuePair>();
nvps.add(new BasicNameValuePair("confirm", "1"));
URL logout = new URL(logoutURL);
String logouthost = logout.getProtocol() + "://" + logout.getHost();
String logoutpath = logout.getPath();
httpPOST(logouthost + logoutpath, nvps);
}
@Override
public boolean isLoggedIn() {
return loggedIn.get();
}
@Override
public boolean isLoggingIn() {
return loggingIn.get();
}
void loginImpl() throws FriendException {
synchronized (this) {
try {
loggingIn.set(true);
connectionBroadcaster.broadcast(new FriendConnectionEvent(this, FriendConnectionEvent.Type.CONNECTING));
if(needsNewSession()) {
requestSession();
} else {
loadSession();
}
facebookClient = new FacebookJsonRestClient(apiKey.get(), secret, session);
fetchAllFriends();
readMetadataFromPages();
discoInfoHandler = discoInfoHandlerFactory.create(this);
chatListener = chatListenerFactory.createChatListener(this);
ThreadExecutor.startThread(chatListener, "chat-listener-thread");
setVisible();
PresenceListener presenceListener = presenceListenerFactory.createPresenceListener(this);
presenceListenerFuture = executorService.scheduleWithFixedDelay(presenceListener, 0, 60, TimeUnit.SECONDS);
loggedIn.set(true);
loggingIn.set(false);
connectionBroadcaster.broadcast(new FriendConnectionEvent(this, FriendConnectionEvent.Type.CONNECTED));
} catch (IOException e) {
LOG.debug("login error", e);
closeConnection(true);
connectionBroadcaster.broadcast(new FriendConnectionEvent(this, FriendConnectionEvent.Type.CONNECT_FAILED, e));
throw new FriendException(e);
} catch (JSONException e) {
LOG.debug("login error", e);
closeConnection(true);
connectionBroadcaster.broadcast(new FriendConnectionEvent(this, FriendConnectionEvent.Type.CONNECT_FAILED, e));
throw new FriendException(e);
} catch (RuntimeException e) {
LOG.debug("unexpected login error; probable bug", e);
closeConnection(true);
connectionBroadcaster.broadcast(new FriendConnectionEvent(this, FriendConnectionEvent.Type.CONNECT_FAILED, e));
throw e;
}
}
}
private boolean needsNewSession() {
return configuration.getAttribute("auth-token") != null;
}
private void loadSession() {
secret = (String)configuration.getAttribute("secret");
session = (String)configuration.getAttribute("session_key");
uid = (String)configuration.getAttribute("uid");
}
/**
* Sets this facebook user visible for chat.
*/
private void setVisible() throws IOException {
HttpPost httpPost = new HttpPost(facebookURLs.get().get(FacebookURLs.CHAT_SETTINGS_URL).get());
httpPost.addHeader("User-Agent", USER_AGENT_HEADER);
List <NameValuePair> nvps = new ArrayList<NameValuePair>();
nvps.add(new BasicNameValuePair("visibility", "true"));
String post_form_id = postFormID.get();
if(post_form_id != null) {
nvps.add(new BasicNameValuePair("post_form_id", post_form_id));
}
httpPost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
HttpClient httpClient = createHttpClient();
HttpResponse response = httpClient.execute(httpPost);
HttpClientUtils.releaseConnection(response);
}
String getUID() {
return uid;
}
/**
* Fetches all friends and adds them as known friends.
*/
private void fetchAllFriends() throws IOException {
try {
JSONArray friends = null;
try {
friends = facebookClient.friends_get();
} catch (RuntimeException re) {
handleFacebookAPIRuntimeException(re);
}
// friends is null when i have no friends
if (friends == null) {
return;
}
List<Long> friendIds = new ArrayList<Long>(friends.length());
for (int i = 0; i < friends.length(); i++) {
friendIds.add(friends.getLong(i));
}
JSONArray users = new JSONArray();
try {
users = (JSONArray) facebookClient.users_getInfo(friendIds, new HashSet<CharSequence>(Arrays.asList("uid", "first_name", "name", "status", "locale")));
} catch (RuntimeException re) {
handleFacebookAPIRuntimeException(re);
}
Set<String> limeWireFriends = fetchLimeWireFriends();
LOG.debugf("all friends: {0}", users);
for (int i = 0; i < users.length(); i++) {
JSONObject user = users.getJSONObject(i);
String id = user.getString("uid");
FacebookFriend friend = friendFactory.create(id, user,
getNetwork(), limeWireFriends.contains(id), this);
LOG.debugf("adding {0}", friend);
addKnownFriend(friend);
}
} catch (FacebookException e) {
LOG.debug("friend error", e);
throw new IOException(e);
} catch (JSONException e) {
LOG.debug("json error", e);
throw new RuntimeException("FIX ME!", e);
}
}
/**
* Fetches friend ids that have the LimeWire application installed
* and marks the existing friends as LimeWire capable.
*/
private Set<String> fetchLimeWireFriends() throws FacebookException, IOException {
JSONArray limeWireFriendIds;
try {
Set<String> limeWireIds = new HashSet<String>();
Object friends = null;
try {
friends = facebookClient.friends_getAppUsers();
} catch (RuntimeException re) {
handleFacebookAPIRuntimeException(re);
}
if(friends instanceof JSONArray) { // is JSONObject when user has no friends with LW installed
limeWireFriendIds = (JSONArray)friends;
LOG.debugf("limewire friends: {0}", limeWireFriendIds);
for (int i = 0; i < limeWireFriendIds.length(); i++) {
limeWireIds.add(limeWireFriendIds.getString(i));
}
}
return limeWireIds;
} catch (JSONException e) {
throw new RuntimeException("FIX ME!",e);
}
}
private void requestSession() throws IOException, JSONException {
String authToken = (String)configuration.getAttribute("auth-token");
String authUrl = FacebookUtils.getRandomElement(authUrls.get()) + "getsession/" + authToken + "/";
LOG.debugf("requesting session from {0}...", authUrl);
HttpGet sessionRequest = new HttpGet(authUrl);
// keep alive in-between getToken and getSession. Close after.
sessionRequest.addHeader("Connection", "close");
HttpClient httpClient = createHttpClient();
HttpResponse response = httpClient.execute(sessionRequest);
parseSessionResponse(response);
HttpClientUtils.releaseConnection(response);
}
private void parseSessionResponse(HttpResponse response) throws IOException, JSONException {
String responseBody = EntityUtils.toString(response.getEntity());
JSONObject json = new JSONObject(responseBody);
session = json.getString("session_key");
secret = json.getString("secret");
uid = json.getString("uid");
int expires = json.getInt("expires");
LOG.debugf("received session {0}, secret {1}, uid: {2}, expires: {3}", session, secret, uid, expires);
configuration.setAttribute("session_key", session);
configuration.setAttribute("secret", secret);
configuration.setAttribute("uid", uid);
configuration.setAutoLogin(expires == 0);
}
public void readMetadataFromPages() throws IOException {
String homePage = httpGET(facebookURLs.get().get(FacebookURLs.HOME_PAGE_URL).get());
if(homePage == null){
throw new IOException("no response");
}
if(uid == null){
throw new IOException("no uid");
}
readLogoutURL(homePage);
String presencePopoutPage = httpGET(facebookURLs.get().get(FacebookURLs.PRESENCE_POPOUT_PAGE_URL).get());
readChannel(presencePopoutPage);
readPOSTFormID(presencePopoutPage);
}
// the logout url is dynamic - it contains a few query params that seem to change across sessions.
// read the logout url here
private void readLogoutURL(String homePage) throws IOException {
String logoutURLPrefix = "<a href=\"" + facebookURLs.get().get(FacebookURLs.LOGOUT_URL).get();
int logoutURLBeginPos = homePage.indexOf(logoutURLPrefix);
int logoutURLEndPos = homePage.indexOf("\">", logoutURLBeginPos);
if (logoutURLBeginPos < 0){
LOG.debugf("logout url not in homepage: {0}", homePage);
throw new IOException("can't find logout URL");
}
else {
logoutURL = homePage.substring(logoutURLBeginPos + "<a href=\"".length(),
logoutURLEndPos);
}
}
// the post_form_id is a hidden input to the form used for posting chat messages.
// Its value is dynamic and changes across sessions, sometimes during a session.
// We read the value here.
private void readPOSTFormID(String homePage) throws IOException {
String post_form_id;
String postFormIDPrefix = "<input type=\"hidden\" id=\"post_form_id\" name=\"post_form_id\" value=\"";
int formIdBeginPos = homePage.indexOf(postFormIDPrefix)
+ postFormIDPrefix.length();
if (formIdBeginPos < postFormIDPrefix.length()){
throw new IOException("can't find post form id");
}
else {
post_form_id = homePage.substring(formIdBeginPos,
formIdBeginPos + 32);
}
setPostFormID(post_form_id);
}
// There is a pool of chat servers. Each user is assigned a single chat server forever (it appears).
// That server is identified by a number called a "channel".
// We read its value here.
private void readChannel(String page) throws IOException {
String channel;
String channelPrefix = " \"channel";
int channelBeginPos = page.indexOf(channelPrefix)
+ channelPrefix.length();
if (channelBeginPos < channelPrefix.length()){
channel = chatChannel.get();
// no cached value
if (channel.length() == 0) {
throw new IOException("can't find channel");
}
LOG.debugf("using cached channel: {0}", channel);
}
else {
channel = page.substring(channelBeginPos,
channelBeginPos + 2);
chatChannel.set(channel);
}
}
public String httpGET(String url) throws IOException {
LOG.debugf("facebook GET: {0}", url);
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("User-Agent", USER_AGENT_HEADER);
httpGet.addHeader("Connection", "close");
return executeRequest(httpGet);
}
/**
* Executes the http request potentially several times catching any
* {@link IOException} occurring and sleeping between requests to make
* failure less likely the next time.
*/
private String executeRequest(HttpUriRequest request) throws IOException {
for (int i = 0; i < maxTries; i++) {
HttpClient httpClient = createHttpClient();
try {
HttpResponse response = httpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (statusCode != 200) {
throw new IOException("wrong status code: " + statusCode + ", content: " + entity != null ? EntityUtils.toString(entity) : "none");
}
if (entity != null) {
String responseStr = EntityUtils.toString(entity);
HttpClientUtils.releaseConnection(response);
return responseStr;
} else {
return null;
}
} catch (IOException ie) {
// throw exception if max tries have been done
if (i == maxTries - 1) {
maxTries = 1;
LOG.debug("all tries failed", ie);
throw ie;
} else {
LOG.debug("ignoring intermittent error", ie);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
}
return null;
}
/**
*
* @return null if there is no response data
*/
public String httpPOST(String url, List <NameValuePair> nvps) throws IOException {
LOG.debugf("facebook POST: {0}", url);
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("Connection", "close");
httpPost.addHeader("User-Agent", USER_AGENT_HEADER);
httpPost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
return executeRequest(httpPost);
}
@Override
public FriendConnectionConfiguration getConfiguration() {
return configuration;
}
@Override
public boolean supportsMode() {
return false;
}
@Override
public ListeningFuture<Void> setMode(FriendPresence.Mode mode) {
throw new UnsupportedOperationException();
}
@Override
public boolean supportsAddRemoveFriend() {
return false;
}
@Override
public ListeningFuture<Void> addNewFriend(String id, String name) {
throw new UnsupportedOperationException();
}
@Override
public ListeningFuture<Void> removeFriend(String id) {
throw new UnsupportedOperationException();
}
@Override
public FacebookFriend getFriend(String id) {
return friends.get(id);
}
@Override
public Collection<Friend> getFriends() {
synchronized (friends) {
return new ArrayList<Friend>(friends.values());
}
}
public String getChannel() {
return chatChannel.get();
}
public String getPresenceId() {
return uid + "/" + configuration.getResource() + sessionId;
}
ChatManager getChatManager() {
return chatManager;
}
public void sendLiveMessage(final FriendPresence presence,
final String type, final Map<String, Object> messageMap) {
executorService.submit(new Runnable() {
public void run() {
synchronized (FacebookFriendConnection.this) {
try {
sendLiveMessageDirect(presence, type, messageMap);
}
catch (FacebookException e) {
LOG.debug("Error sending live message:", e);
if (loggedIn.get()) {
logoutImpl(true, new FriendException("chat session expired", e));
}
} catch (IOException e) {
LOG.debug("Error sending live message:", e);
// TODO logout?
}
}
}
});
}
private void sendLiveMessageDirect(FriendPresence presence,
String type, Map<String, Object> messageMap) throws FacebookException, IOException {
messageMap.put("to", presence.getPresenceId());
messageMap.put("from", getPresenceId());
final Long userId = Long.parseLong(presence.getFriend().getId());
JSONObject message = new JSONObject(messageMap);
LOG.debugf("live message {0} to {1} : {2}", type, userId, message);
try {
facebookClient.liveMessage_send(userId, type, message);
} catch (RuntimeException re) {
handleFacebookAPIRuntimeException(re);
}
}
void sendChatMessage(String friendId, String message) throws FriendException {
List <NameValuePair> nvps = new ArrayList <NameValuePair>();
nvps.add(new BasicNameValuePair("msg_text", (message == null)? "":message));
nvps.add(new BasicNameValuePair("msg_id", (int)(Math.random() * 1000000000) + ""));
nvps.add(new BasicNameValuePair("client_time", System.currentTimeMillis() + ""));
nvps.add(new BasicNameValuePair("to", friendId));
String post_form_id = postFormID.get();
if(post_form_id != null) {
nvps.add(new BasicNameValuePair("post_form_id", post_form_id));
}
try {
String resp = httpPOST(facebookURLs.get().get(FacebookURLs.SEND_CHAT_URL).get(), nvps);
handleChatResponseError(friendId, resp);
} catch (IOException e) {
throw new FriendException(e);
}
}
/**
* Sends a chat state update to a friend.
* <p>
* Side effect: If the friend is offline, all presences of the friend are
* removed and he's no longer available.
*
* @return true if the friend is online, false otherwise
*/
boolean sendChatStateUpdate(String friendId, ChatState state) throws FriendException {
List <NameValuePair> nvps = new ArrayList <NameValuePair>();
nvps.add(new BasicNameValuePair("typ", (state == ChatState.composing)? "1" : "0"));
nvps.add(new BasicNameValuePair("to", friendId));
String post_form_id = postFormID.get();
if(post_form_id != null) {
nvps.add(new BasicNameValuePair("post_form_id", post_form_id));
}
try {
String resp = httpPOST(facebookURLs.get().get(FacebookURLs.SEND_CHAT_STATE_URL).get(), nvps);
return handleChatResponseError(friendId, resp);
} catch (IOException e) {
LOG.debug("error sending chat update", e);
throw new FriendException(e);
}
}
/**
* Sends a chat state update which causes a friend to be removed from available
* friends if he's no longer online. This is a blocking call.
*
* @return false if the friend is offline, true otherwise
*/
public boolean sendFriendIsOnline(String friendId) throws FriendException {
return sendChatStateUpdate(friendId, ChatState.active);
}
/**
* @return false if the friend is offline, otherwise true, also true in other
* error cases
*/
private boolean handleChatResponseError(String friendId, String response) {
String prefix = "for (;;);";
if (response.startsWith(prefix)) {
response = response.substring(prefix.length());
}
try {
JSONObject json = FacebookUtils.parse(response);
int error = json.getInt("error");
if (error == 1356003) {
LOG.debugf("friend offline: {0}, full response: {1}", friendId, response);
FacebookFriend friend = getFriend(friendId);
if (friend != null) {
removeAllPresences(friend);
} else {
LOG.debug("friend already removed");
}
return false;
} else if (error != 0) {
LOG.debugf("unhandled error: {0}", response);
}
} catch (JSONException e) {
LOG.debugf(e, "error parsing chat response {0}", response);
}
return true;
}
private Network getNetwork() {
return network;
}
private HttpClient createHttpClient() {
DefaultHttpClient httpClient = new DefaultHttpClient(httpConnectionManager, null);
httpClient.setCookieStore(cookieStore);
cookieStore.clearExpired(new Date());
return httpClient;
}
/**
* Adds a friend to connection and friend manager.
*/
void addKnownFriend(FacebookFriend friend) {
if(loggedIn.get() || loggingIn.get()) {
String friendId = friend.getId();
boolean added = false;
synchronized (friends) {
if (!friends.containsKey(friendId)) {
friends.put(friendId, friend);
added = true;
}
}
if (added) {
friendManager.addKnownFriend(friend);
}
}
}
/**
* Creates a <code>presence</code> for a <code>friend</code> if the friend doesn't have
* a presence yet. If that's the case also notifies friend manager that the
* friend is available now.
*
*/
public void addPresence(String presenceId) {
FacebookFriendPresence newPresence = null;
synchronized (presenceLock) {
String friendId = PresenceUtils.parseBareAddress(presenceId);
FacebookFriend facebookFriend = getFriend(friendId);
if(facebookFriend != null) {
Map<String, FriendPresence> presences = facebookFriend.getPresences();
if(!presences.containsKey(presenceId)) {
// remove old presence with same resource prefix
String newResourcePrefix = FriendAddress.parseIdPrefix(presenceId);
for (String id : presences.keySet()) {
String fullResource = PresenceUtils.parseResource(id);
if (fullResource.startsWith(newResourcePrefix)) {
LOG.debugf("found old presence to replace: {0}, new id: {1}", id, presenceId);
removePresence(id);
}
}
boolean firstPresence = facebookFriend.getPresences().isEmpty();
LOG.debugf("new friend is available: {0}", presenceId);
newPresence = new FacebookFriendPresence(presenceId, facebookFriend, featureEventBroadcaster);
if (facebookFriend.hasLimeWireAppInstalled()) {
addTransports(newPresence);
}
facebookFriend.addPresence(newPresence);
if(firstPresence) {
friendManager.addAvailableFriend(facebookFriend);
}
friendPresenceBroadcaster.broadcast(new FriendPresenceEvent(newPresence, FriendPresenceEvent.Type.ADDED));
}
} else {
LOG.debugf("friend not known yet: {0}", presenceId);
}
}
}
private void addTransports(FacebookFriendPresence presence) {
presence.addTransport(AddressFeature.class, addressHandler);
presence.addTransport(AuthTokenFeature.class, authTokenHandler);
presence.addTransport(ConnectBackRequestFeature.class, connectBackRequestHandler);
presence.addTransport(LibraryChangedNotifierFeature.class, libraryRefreshHandler);
presence.addTransport(FileOfferFeature.class, fileOfferHandler);
}
public void removePresence(String presenceId) {
synchronized (presenceLock) {
String friendId = PresenceUtils.parseBareAddress(presenceId);
FacebookFriend facebookFriend = getFriend(friendId);
if(facebookFriend != null) {
FriendPresence presence = facebookFriend.getPresences().get(presenceId);
if(presence != null) {
LOG.debugf("removing presence {0}", presence.getPresenceId());
FacebookFriendPresence facebookFriendPresence = (FacebookFriendPresence)presence;
facebookFriend.removePresence(facebookFriendPresence);
friendPresenceBroadcaster.broadcast(new FriendPresenceEvent(facebookFriendPresence, FriendPresenceEvent.Type.REMOVED));
if(!facebookFriend.isSignedIn()) {
removeIncomingChatListener(presence.getFriend().getId());
friendManager.removeAvailableFriend(facebookFriend);
}
} else {
LOG.debugf("remove presence, no presence to remove: {0}", presenceId);
}
} else {
LOG.debugf("remove presence, no friend found for id {0}", presenceId);
}
}
}
void removeAllPresences(FacebookFriend friend) {
synchronized (presenceLock) {
Map<String, FriendPresence> presenceMap = friend.getPresences();
LOG.debugf("removing all presences for {0}", friend.getId());
for(FriendPresence presence : presenceMap.values()) {
LOG.debugf("removing presence {0}", presence);
friend.removePresence((FacebookFriendPresence)presence);
friendPresenceBroadcaster.broadcast(new FriendPresenceEvent(presence, FriendPresenceEvent.Type.REMOVED));
}
friendManager.removeAvailableFriend(friend);
}
}
void setIncomingChatListener(String friendId, IncomingChatListener listener) {
chatManager.setIncomingChatListener(friendId, listener);
}
void removeIncomingChatListener(String friendId) {
chatManager.removeChat(friendId);
}
MessageWriter createChat(String friendId, MessageReader reader) {
return chatManager.addMessageReader(friendId, reader);
}
@SuppressWarnings("unchecked")
private static List<Cookie> parseCookies(FriendConnectionConfiguration configuration) {
return (List<Cookie>)configuration.getAttribute("cookie");
}
public void sendNotification(Friend friend, final String text) {
final Long userId = Long.parseLong(friend.getId());
executorService.submit(new Runnable() {
public void run() {
try {
facebookClient.notifications_send(Collections.singleton(userId), text);
} catch (RuntimeException re) {
try {
handleFacebookAPIRuntimeException(re);
} catch (IOException e) {
LOG.debug("error sending notification", e);
}
} catch (FacebookException e) {
LOG.debug("error sending notifcation", e);
}
}
});
}
public void publishUserAction(final Long templateBundleId) {
executorService.submit(new Runnable() {
public void run() {
try {
facebookClient.feed_publishUserAction(templateBundleId);
} catch (RuntimeException re) {
try {
handleFacebookAPIRuntimeException(re);
} catch (IOException e) {
LOG.debug("error publishing user action", e);
}
} catch (FacebookException e) {
LOG.debug("error publishing user action", e);
}
}
});
}
}