Graham King

Solvitas perambulum

XMPP in your browser: Flex 2 with XIFF

software

An entry about using the XIFF API to get your Flex application talking XMPP. If you don’t know what I’m talking about no need to read on really.

Some definitions

  • XMPP is an open protocol for instant messaging, that runs the Jabber network and Google talk.
  • Flex 2 is a programmer-friendly way of developing Flash applications, using ActionScript 3, which looks a lot like Java.
  • XIFF is an ActionScript 3 API for XMPP.

If you put those three items together you get, yes, a paradigm shift in web based application development. But let’s not get fancy just yet (a later blog post might wax lyrical about the future of web apps), for now here are some recipes for doing just about anything with XMPP, ActionScript 3, and XIFF.

Permission to connect

The Flash client in the browser is by default only allowed to access the exact domain it came from, so if your XMPP server is on chat.myserver.com (or even just myserver.com), and your Flash was served from www.myserver.com, you need a crossdomain.xml file.

    < ?xml version='1.0'>
    <!DOCTYPE cross-domain-policy SYSTEM 'http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd'>
    <cross-domain-policy>
    <allow-access-from domain='*' />
    </cross-domain-policy>

[Remove the space between the first < and the ?]

Save this in the root of chat.myserver.com, and make sure an HTTP server is there ready to serve it. If you are not using the standard XMPP port 5222, but instead are using a port below 1024 (possibly to get through a firewall), add to-ports="*" to the allow-access-from line.

Now start your Flex application with:

import flash.system.Security;
Security.loadPolicyFile("http://"+ server +"/crossdomain.xml");

The Openfire server provides a built-in crossdomain.xml for you on an xml socket on port 5229, so on that server you can skip creating the file , you don’t need to run an HTTP server (although the user has to get your Flash from somewhere), and use:

Security.loadPolicyFile("xmlsocket://"+ server +":5229");

Only port 80 is permitted for HTTP serving of policy files – any other port must use an xmlsocket. See Actionscript docs for Security.loadPolicyFile

Create the connection

    import org.jivesoftware.xiff.core.XMPPSocketConnection;
    connection = new XMPPSocketConnection();

Use XMPPSocketConnection instead of just XMPPConnection, as the later doesn’t work correctly with ejabberd.

Get the roster ready

    import org.jivesoftware.xiff.im.Roster;

    roster = new Roster(connection);
    roster.addEventListener(RosterEvent.SUBSCRIPTION_DENIAL, rosterHandler);
    roster.addEventListener(RosterEvent.SUBSCRIPTION_REQUEST, rosterHandler);
    roster.addEventListener(RosterEvent.SUBSCRIPTION_REVOCATION, rosterHandler);
    roster.addEventListener(RosterEvent.USER_AVAILABLE, rosterHandler);
    roster.addEventListener(RosterEvent.USER_UNAVAILABLE, rosterHandler);

    private function rosterHandler(event:RosterEvent):void {

        trace("rosterHandler");

        switch (event.type){
            case RosterEvent.SUBSCRIPTION_REQUEST:
                // Fill this bit in, obviously
                break;
            case RosterEvent.USER_UNAVAILABLE :
                trace (event.jid + " is Unavailable (RosterEvent)");
                break;
            case RosterEvent.USER_AVAILABLE :
                trace (event.jid + " is Available (RosterEvent)");
                break;
            case RosterEvent.SUBSCRIPTION_DENIAL :
                trace (event.jid + " denied your request (RosterEvent)");
                break;
            case RosterEvent.SUBSCRIPTION_REVOCATION :
                // this fires at unexpected times so ignore it.
                // trace (event.jid + " revoked your presence (RosterEvent)");
                break;
            default :
                // do nothing... not recognized
        }
    }

Login

    connection.username = "webuser";
    connection.password = "secret";
    connection.server = "chat.myserver.com";
    connection.resource = "Web";
    connection.connect("standard");

At this stage if any of ‘webuser’s contacts are online the USER_AVAILABLE event will fire once for each, and connectionManager.isLoggedIn() should return true.

Register a new account

It seems you need to fail a login before you can register a new user, and after creating a new user you need to disconnect and reconnect as that user (assuming you want to use the account you just created):

connection.addEventListener(XIFFErrorEvent.XIFF_ERROR, registerNewUser);
connection.addEventListener(
    RegistrationSuccessEvent.REGISTRATION_SUCCESS, reconnect);

then try and login with a non existent username and password (see Login section above). The login failure will call your method:

    private function registerNewUser(event:XIFFErrorEvent):void {
        trace("registerNewUser");

        if ( event.errorCode != 401 || event.errorType != "auth" ) {
            // Only act on failed login, ignore other errors
            return;
        }

        username = "newUser";
        password = "newUserPassword";

        var registrationFields:Object = new Object();
        registrationFields.username = username;
        registrationFields.password = password;

        connection.sendRegistrationFields(registrationFields, null);

        connection.removeEventListener(XIFFErrorEvent.XIFF_ERROR, registerNewUser);
    }

Your server needs to allow registrations with only the username and password, and to allow registrations from anyone. This is usually the server default, although they might insist on an e-mail. Successful registration will call the method you registered earlier:

    private function reconnect(event:RegistrationSuccessEvent):void {
        trace("reconnect");

        connection.disconnect();

        initRoster();
        login();
    }

The initRoster and login will do the obvious – see the sections above.

Subscribe to the presence of another user

    var userJID:String = "bob@yourserver.com";
    roster.addContact(agentJID, "Bobby", "People", true);

This will ask ‘bob@yourserver.com’ if they allow your user to subscribe to their presence. When they reply, which may be some time if they are not online, it will fire one of RosterEvent.SUBSCRIPTION_DENIAL if they deny you, RosterEvent.SUBSCRIPTION_REQUEST if they choose to subscribe to your presence in turn, and, if they authorized you and are online, RosterEvent.USER_AVAILABLE.

Multi-user chat

To chat with people you create a room, join that room, then invite them to join that room. room = new Room(); room.setRoomJID( roomJID ); // See explanation after the code room.setConnection( connection );

    room.addEventListener(RoomEvent.USER_JOIN, roomHandler);
    room.addEventListener(RoomEvent.USER_DEPARTURE, roomHandler);
    room.addEventListener(RoomEvent.GROUP_MESSAGE, roomHandler);
    room.addEventListener(RoomEvent.ROOM_JOIN, inviteToRoom);   // Invite the guests when we join the room
    // We need the room in our roster to handle presence messages of people in the room
    if ( roster.getContactInformation(roomJID) == null ) {
        roster.addContact(roomJID, "Something", "People", false);
    }

    room.join();

Multi-User chat is an extension to Jabber, so usually uses a separate server. In the case of jabberd this is a physically separate server called ‘mu-conference’. For Openfire and ejabberd this is built in. Hence the roomJID is usually at the server prefixed with ‘conference’: ‘myroomname@conference.yourserver.com’

When you join the room your method will be called: private function inviteToRoom(event:RoomEvent):void { trace(“ConnectionManager.inviteToRoom”); room.invite( “bob@yourserver.com”, “Come and chat” ); }

The roomHandler method should be straighforward – copy the earlier rosterHandler and change the events.

Auto-reconnect

If your Flash is going to be sitting in someone’s open browser for any length of time, the little pixies that run down the pipes that make up the Internet might accidentally disconnect you – remember that your Flex / Flash app is holding an open socket to the XMPP server the whole time the page is open. Hence you need to auto-reconnect:

    connection.addEventListener(XIFFErrorEvent.XIFF_ERROR, reconnectOnError);

    private function reconnectOnError(event:XIFFErrorEvent):void {
        trace("reconnectOnError");

        if ( event.errorCode == 503 ) { // Service unavailable, means we got disconnected
            this.connection = null;
            connect();  // Creates a new connection, a new roster, and logs in

            if ( room != null ) {  // If in a chat rejoin that room
                rejoinRoom( room.roomName );
            }
        }
    }

Choosing an XMPP Server

You’re obviously going to need a server to do this. If you have lots of memory, prefer:

  • Openfire – Built by the company behind XIFF, this is the easiest to install and most fully featured XMPP server out there right now. It’s in Java so it’s not appropriate if your server memory is limited, such as on a shared host or UML server. It has a great web admin interface (port 9090 by default). I use this for development.

    Otherwise choose between these two:

  • Ejabberd – Written in Erlang, this is the one to choose if your site has an insanely large amount of visitors. Also has a pretty decent web admin interface, on yourserver.com:5280/admin/. To login you need to create an account via your Jabber client, authorize that account as admin in /etc/ejabberd/ejabberd.cfg, and login using your full JID, such as ‘admin@yourserver.com’, and your password.

  • Jabberd 1.4 with mu-conference – Standard Unix C/C++ server, and I think the original Jabber server. Makes your sysadmin happy because they are good o’ regular Unix daemons. This is the one I use live because it takes 9M of memory, with only 2M resident. No fancy admin interfaces here – there’s some shell scripts floating around, or hack the database directly.

Debugging

Get Wireshark so you can see what your Flex app and the XMPP server are saying to each other.

Create a mm.cfg file in your home directory with these lines: ErrorReportingEnable=1 TraceOutputFileEnable=1 MaxWarnings=1 SecurityDialogReportingEnable=true

this allows you to use the trace method in ActionScript to log to ~/.macromedia/Flash_Player/Logs/flashlog.txt.

Editing tips

In Eclipse associate .mxml with the XML editor, and .as with the Javascript editor, and you’ll get syntax highlighting and in ActionScript the Outline window will work.

References