package org.limewire.facebook.service; import java.io.IOException; import java.util.Map; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.limewire.facebook.service.livemessage.LiveMessageHandler; import org.limewire.facebook.service.livemessage.LiveMessageHandlerRegistry; import org.limewire.facebook.service.settings.FacebookAppID; import org.limewire.facebook.service.settings.FacebookReportBugs; import org.limewire.facebook.service.settings.FacebookURLs; import org.limewire.friend.api.ChatState; import org.limewire.friend.api.MessageReader; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.util.ExceptionUtils; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; /** * This listens for new chat messages and live messages, and dispatches them to the * appropriate handlers. It does this via http polling, where the requests are long running (COMET style). */ public class ChatListener implements Runnable { private static final Log LOG = LogFactory.getLog(org.limewire.facebook.service.ChatListener.class); private final FacebookFriendConnection connection; private final LiveMessageHandlerRegistry handlerRegistry; private final Provider<String> facebookAppID; private final ChatManager chatManager; private final String uid; private final String channel; private int seq; private volatile boolean done; private final Provider<Boolean> reportBugs; private final Provider<Map<String, Provider<String>>> facebookURLs; @Inject ChatListener(@Assisted FacebookFriendConnection connection, LiveMessageHandlerRegistry handlerRegistry, @FacebookAppID Provider<String> facebookAppID, @FacebookReportBugs Provider<Boolean> reportBugs, @FacebookURLs Provider<Map<String, Provider<String>>> facebookURLs) { this.connection = connection; this.handlerRegistry = handlerRegistry; this.facebookAppID = facebookAppID; this.reportBugs = reportBugs; this.facebookURLs = facebookURLs; this.seq = -1; this.uid = connection.getUID(); this.channel = connection.getChannel(); this.chatManager = connection.getChatManager(); } void setDone() { this.done = true; } @Override public void run() { done = false; try { seq = getSeq(); } catch(IOException e1){ LOG.debug("error getting initial sequence number", e1); connection.logout(); } catch(JSONException e1){ LOG.debug("error parsing initial sequence number", e1); } int currentSeq; while(!done) { try { currentSeq = getSeq(); if(seq > currentSeq) { seq = currentSeq; } while(seq <= currentSeq && !done) { //get the old message between oldseq and seq String msgResponseBody = connection.httpGET(getMessageRequestingUrl(seq)); if(msgResponseBody != null) { String prefix = "for (;;);"; if(msgResponseBody.startsWith(prefix)) { msgResponseBody = msgResponseBody.substring(prefix.length()); } JSONObject response = FacebookUtils.parse(msgResponseBody); LOG.debugf("message: {0}", response); if(response.getString("t").equals("msg")) { dispatchMessage(response); } else if(response.getString("t").equals("refresh")) { connection.reconnect(); } //seq++; } seq++; } } catch (IOException e) { LOG.debug("error getting chat message", e); if(!done) { connection.logout(); } return; } catch (JSONException e) { LOG.debug("error parsing chat message", e); // only report exceptions if thread is not done yet if (!done && reportBugs.get()) { ExceptionUtils.reportOrReturn(e); } } } LOG.debug("chat listener is done"); } private void dispatchMessage(JSONObject message) throws JSONException { if(message.has("ms")) { JSONArray ms = message.getJSONArray("ms"); final JSONObject payload = ms.getJSONObject(0); String msgType = payload.getString("type"); String appId = payload.optString("app_id", ""); if("app_msg".equals(msgType) && appId.equals(facebookAppID.get())) { processLiveMessage(payload); } else if ("msg".equals(msgType) || "typ".equals(msgType)) { // LWC-4069 processChatMessage(payload, msgType); } else { LOG.debugf("unhandled payload: {0}", payload.toString()); } } else { LOG.debugf("unhandled message: {0}", message.toString()); } } private void processLiveMessage(JSONObject payload) throws JSONException { if (!payload.has("response")) { LOG.debugf("no 'response' in message payload: {0}", payload); return; } JSONObject lwMessage = payload.getJSONObject("response"); String presenceId = lwMessage.getString("from"); connection.addPresence(presenceId); String to = lwMessage.getString("to"); if (!to.equals(connection.getPresenceId())) { LOG.debugf("message not for us: {0}", payload); return; } String messageType = payload.getString("event_name"); LiveMessageHandler handler = handlerRegistry.getHandler(messageType); if (handler != null) { handler.handle(messageType, lwMessage); } else { LOG.debugf("no handler for type: {0}", messageType); } } @SuppressWarnings("unused") private void processChatMessage(JSONObject payload, String msgType) throws JSONException { String parsedSenderId = payload.getString("from"); // look up the MessageReader based on the sender (friend) id if (parsedSenderId != null && !parsedSenderId.equals(uid)) { connection.addPresence(parsedSenderId); MessageReader handler = chatManager.getMessageReader(parsedSenderId); if (handler != null) { if (msgType.equals("msg")) { JSONObject messageJson = payload.getJSONObject("msg"); String msg = messageJson.getString("text"); handler.readMessage(msg); } else { ChatState state = payload.getInt("st") == 1 ? ChatState.composing : ChatState.active; handler.newChatState(state); } } else { // this can happen, when we just signed off and removed all friend presences // but the friend is currently typing to us an the connection is not // closed yet, it's ok in this case LOG.debugf("no handler for sender: {0}", parsedSenderId); } } else { if(parsedSenderId == null) { LOG.debugf("no 'from' in message payload: {0}", payload); } else if(parsedSenderId.equals(uid)){ LOG.debugf("ignoring chat message sent from logged in user: {0}", payload); } else { LOG.debugf("dropped chat message: {0}", payload); } } } private int getSeq() throws IOException, JSONException { for (int i = 0; i < 3; i++) { String seqResponseBody = connection.httpGET(getMessageRequestingUrl(-1)); if (seqResponseBody == null) { LOG.debug("null response for seq"); continue; } int sequenceNumber = parseSeq(seqResponseBody); if(sequenceNumber >= 0){ return sequenceNumber; } try { LOG.debug("retrying to retrieve the seq code after 1 second..."); Thread.sleep(1000); } catch (InterruptedException e) { LOG.debug(e.getMessage(), e); } } throw new IOException("could not parse sequence number"); } private String getMessageRequestingUrl(long seq) { return facebookURLs.get().get(FacebookURLs.RECEIVE_CHAT_URL).get(). replaceAll("\\$channel", channel). replaceAll("\\$uid", uid). replaceAll("\\$seq", Long.toString(seq)); } /** * @return -1 if sequence number could not be parsed */ private int parseSeq(String msgResponseBody) throws JSONException, IOException { LOG.debugf("parsing seq from: {0}", msgResponseBody); //for (;;);{"t":"refresh", "seq":0} String prefix = "for (;;);"; if(msgResponseBody.startsWith(prefix)) { msgResponseBody = msgResponseBody.substring(prefix.length()); } JSONObject body = FacebookUtils.parse(msgResponseBody); if(body.has("seq")) { return body.getInt("seq"); } else if(body.has("t") && body.getString("t").equals("refresh")) { LOG.debug("refreshing post form id"); connection.reconnect(); return -1; } else { return -1; } } }