JavaScript Twitter-Feed Room Module

This example uses server-side JavaScript to load data from Twitter's public web service. The loaded data--in this case, a Twitter status update--is then assigned to a Union room attribute whose value is automatically shared with all clients in a room.

Here's a live demonstration of a JavaScript client receiving the Twitter update:

And here's a live demonstration of an Adobe Flash client receiving the Twitter update:

Here's the code for the JavaScript room module. To use the code, copy it to a file named TwitterStatus.js, and place that file in Union Server's modules directory.

//==============================================================================
// IMPORTS
//==============================================================================
importClass(net.user1.union.core.event.RoomEvent);
importClass(net.user1.union.api.Client);
importClass(net.user1.union.core.attribute.Attribute);

importClass(org.apache.log4j.Logger);

importPackage(java.lang);
importPackage(java.io);
importPackage(java.net);
importPackage(org.dom4j.io);

//==============================================================================
// VARIABLES
//==============================================================================
var moduleContext;
var wrapper;
var twitterIntervalID;
var log = Logger.getLogger("TwitterStatus");

//==============================================================================
// REQUIRED ROOM-MODULE INTERFACE METHODS
//==============================================================================
function init (ctx, wrap) {
  moduleContext = ctx;
  wrapper = wrap;

  // Set the twitterStatus attribute to "" by default
  moduleContext.getRoom().setAttribute("twitterStatus",
                                       "",
                                       Attribute.SCOPE_GLOBAL,
                                       Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);
  // Load the Twitter status
  getTwitterStatus();

  // Once a minute, check if the Twitter status has changed
  twitterIntervalID = setInterval(update, 60000);
}

function shutdown () {
  // Important: kill the update thread when the room is destroyed
  clearInterval(twitterIntervalID);
}

//==============================================================================
// TWITTER UPDATES
//==============================================================================
// Checks whether the latest Twitter status should be loaded
function update () {
  if (moduleContext.getRoom().getNumClients() > 0) {
    getTwitterStatus();
  }
}

// Loads a twitter status from twitter.com, then, if the status has changed,
// updates the value of the twitterStatus room attribute
function getTwitterStatus () {
  var data = doHttpGetRequest("http://api.twitter.com/1/statuses/user_timeline.xml?id=UnionPlatform&count=1");
  var reader = new SAXReader();
  var doc = reader.read(new StringReader(data));
  var root = doc.getRootElement();
  var twitterStatus = root.element("status").element("text").getText();
  log.debug("Twitter status: " + twitterStatus);

  var previousStatus = moduleContext.getRoom().getAttribute("twitterStatus", Attribute.SCOPE_GLOBAL).nullSafeGetValue();
  if (!twitterStatus.equals(previousStatus)) {
    // Update the twitterStatus room attribute, which is automatically
    // shared with all occupants of the room
    moduleContext.getRoom().setAttribute("twitterStatus",
                                         twitterStatus,
                                         Attribute.SCOPE_GLOBAL,
                                         Attribute.FLAG_SERVER_ONLY | Attribute.FLAG_SHARED);
  } else {
    log.debug("Twitter status has not changed, so no update sent to clients.")
  }
}

//==============================================================================
// HTTP REQUEST IMPLEMENTATION
//==============================================================================
function doHttpGetRequest (urlString) {
  var connection = null;
  var wr = null;
  var rd  = null;
  var sb = null;
  line = null;

  serverAddress = null;

  try {
    serverAddress = new URL(urlString);
    connection = serverAddress.openConnection();
    connection.setRequestMethod("GET");
    connection.setDoOutput(true);
    connection.setReadTimeout(10000);

    connection.connect();

    rd  = new BufferedReader(new InputStreamReader(connection.getInputStream()));
    sb = new StringBuilder();

    while ((line = rd.readLine()) != null) {
      sb.append(line + '\n');
    }
    return sb.toString();
  } catch (e) {
    log.debug(e.toString());
    return "";
  } finally {
    connection.disconnect();
    rd = null;
    sb = null;
    wr = null;
    connection = null;
  }
}

//==============================================================================
// setInterval()/clearInterval() IMPLEMENTATION
//==============================================================================
// JavaScript's setInterval() and clearInterval() functions are not included
// with Rhino, so implement custom versions
var timers = new Array();
var setInterval = function (fn, time) {
  var num = timers.length;
  var isRunning = true;
  var timerObj = {
    thread: new java.lang.Thread(new java.lang.Runnable({
      run: function () {
        while (isRunning) {
          java.lang.Thread.currentThread().sleep(time);
          if (isRunning) {
            fn();
          }
        }
      }
    })),
    stop: function () {isRunning = false;}
  };
  timers[num] = timerObj;
  timers[num].thread.start();

  return num;
};

var clearInterval = function (num){
  if (timers[num]) {
    timers[num].stop();
    delete timers[num];
  }
};

Here's the HTML code for the JavaScript Union client that receives the Twitter status update. It loads the application code and OrbiterMicro, Union's javascript framework, which is used to connect to Union Server.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>OrbiterMicro TwitterStatus</title>

<!--CSS-->
<style type="text/css">
#output {
  border: inset 2px;
  height: 100px;
  width: 500px;
  overflow: auto;
  padding: 5px;
  margin-bottom: 5px
}
</style>

<!--Load the OrbiterMicro JavaScript library (non-minified version). Use during development.-->
<script type="text/javascript" src="http://unionplatform.com/cdn/OrbiterMicro_latest.js"></script>
<!--Load the OrbiterMicro JavaScript library (minified version). Use for production.-->
<!--<script type="text/javascript" src="http://unionplatform.com/cdn/OrbiterMicro_latest_min.js"></script>-->

<!--Load the application code-->
<script type="text/javascript" src="OrbiterMicroTwitterStatus.js"></script>
</head>

<body>

<!--Contains the application output-->
<div id="output"></div>

</body>
</html>

Here's the JavaScript code for the JavaScript Union client. It deploys the JavaScript room module by sending a CREATE_ROOM UPC, and reads the "tweet" (Twitter status update) from a room attribute.

//==============================================================================
// VARIABLES
//==============================================================================
var orbiter;
var UPC = net.user1.orbiter.UPC;
var roomID = "examples.twitterroom";
var msgManager;

//==============================================================================
// INITIALIZATION
//==============================================================================
window.onload = init;
function init () {
  // Create the Orbiter instance, used to connect to and communicate with Union
  orbiter = new net.user1.orbiter.Orbiter();
  orbiter.getLog().setLevel("DEBUG")

  // Output log messages in the browser's debugging console
  orbiter.enableConsole();

  // If required JavaScript capabilities are missing, abort
  if (!orbiter.getSystem().isJavaScriptCompatible()) {
    displayMessage("Your browser is not supported.")
    return;
  }

  // Register for Orbiter's connection events
  orbiter.addEventListener(net.user1.orbiter.OrbiterEvent.READY, readyListener, this);
  orbiter.addEventListener(net.user1.orbiter.OrbiterEvent.CLOSE, closeListener, this);

  // Connect to Union
  orbiter.connect("tryunion.com", 80);
  displayMessage("Connecting to Union...");
}

//==============================================================================
// ORBITER EVENT LISTENERS
//==============================================================================
// Triggered when the connection is ready
function readyListener (e) {
  displayMessage("Ready");
  msgManager = orbiter.getMessageManager();

  // Register for UPC messages from Union Server
  msgManager.addMessageListener(UPC.ROOM_SNAPSHOT, roomSnapshotListener, this);
  msgManager.addMessageListener(UPC.ROOM_ATTR_UPDATE, roomAttributeUpdateListener, this);

  // Create the Twitter room, and specify that it should load the
  // TwitterStatus.js JavaScript room module
  msgManager.sendUPC(UPC.CREATE_ROOM,
    roomID,
    null,
    null,
    "script|TwitterStatus.js");

  // Join the Twitter room
  msgManager.sendUPC(UPC.JOIN_ROOM, roomID);
}

// Triggered when the connection is closed
function closeListener (e) {
  displayMessage("Disconnected from Union.")
}

//==============================================================================
// HANDLE INCOMING ROOM ATTRIBUTES
//==============================================================================
// Triggered when Union Server sends a "snapshot" describing the Twitter room.
// For a description of roomSnapshotListener()'s parameters, see "u54" in the
// UPC specification, at: http://unionplatform.com/specs/upc/. This client
// receives the room snapshot automatically when it the joins the Twitter room.
function roomSnapshotListener (requestID,
                               snapshotRoomID,
                               occupantCount,
                               observerCount,
                               roomAttributes) {
  var roomAttrPairs = roomAttributes.split("|");
  var attrName;
  var attrVal;

  // Loop through the list of room attributes to get the
  // "twitterStatus" attribute.
  for (var i = 0; i < roomAttrPairs.length; i+=2) {
    attrName = roomAttrPairs[i];
    attrVal  = roomAttrPairs[i+1];
    processRoomAttributeUpdate(snapshotRoomID, attrName, attrVal);
  }
}

// Triggered when one of the room's shared attributes changes. When triggered,
// check to see whether it was the "twitterStatus" attribute.
function roomAttributeUpdateListener (attrRoomID,
                                      clientID,
                                      attrName,
                                      attrVal) {
  processRoomAttributeUpdate(attrRoomID, attrName, attrVal);
}

// Checks for changes to the the "twitterStatus" attribute.
function processRoomAttributeUpdate (attrRoomID, attrName, attrVal) {
  if (attrRoomID == roomID && attrName == "twitterStatus") {
    displayMessage("UnionPlatform: " + attrVal);
  }
}

//==============================================================================
// UI
//==============================================================================
// Displays a single line of text in the output <div>
function displayMessage (message) {
  // Make the new chat message element
  var msg = document.createElement("span");
  msg.appendChild(document.createTextNode(message));
  msg.appendChild(document.createElement("br"));

  // Append the new message to the chat
  var outputPane = document.getElementById("output");
  outputPane.appendChild(msg);

  // Trim the chat to 500 messages
  if (outputPane.childNodes.length > 500) {
    outputPane.removeChild(outputPane.firstChild);
  }
  outputPane.scrollTop = outputPane.scrollHeight;
}

Finally, here's the ActionScript code for the Adobe Flash client.

package {
  import flash.display.Sprite;
  import flash.text.TextField;

  import net.user1.logger.Logger;
  import net.user1.reactor.AttributeEvent;
  import net.user1.reactor.ModuleType;
  import net.user1.reactor.Reactor;
  import net.user1.reactor.ReactorEvent;
  import net.user1.reactor.Room;
  import net.user1.reactor.RoomModules;

  public class UnionTwitterStatus extends Sprite {
    protected var reactor:Reactor;
    protected var room:Room;
    protected var output:TextField;

    public function UnionTwitterStatus () {
      reactor = new Reactor();
      reactor.getLog().setLevel(Logger.DEBUG);
      reactor.addEventListener(ReactorEvent.READY, readyListener);
      reactor.connect("tryunion.com", 80);

      output = new TextField();
      output.background = true;
      output.border = true;
      output.width = 499;
      output.height = 149;
      output.wordWrap = true;
      addChild(output);
      println("Connecting to Union...");
    }

    protected function readyListener (e:ReactorEvent):void {
      println("Ready");

      var modules:RoomModules = new RoomModules();
      modules.addModule("TwitterStatus.js", ModuleType.SCRIPT);
      room = reactor.getRoomManager().createRoom("examples.twitterroom", null, null, modules);
      room.addEventListener(AttributeEvent.UPDATE, updateRoomAttributeListener);
      room.join();
    }

    protected function updateRoomAttributeListener (e:AttributeEvent):void {
      if (e.getChangedAttr().name == "twitterStatus") {
        println("UnionPlatform: " + e.getChangedAttr().value);
      }
    }

    protected function println (msg:String):void {
      output.appendText(msg + "\n");
      output.scrollV = output.maxScrollV;
    }
  }
}