Get source code

APE demo : TCPSocket Demo (IRC)

About this demo

This demo is an exemple of the implementation of the TCPSockets in APE. Here, APE is used as a middleware, making it the gateway to an IP:Port : IRC.

Which features are used?

  • proxy.js
  • ServerSide JavaScript

Study the source code

Check out the Client JavaScript, HTML and ServerSide JavaScript source code of this demo by reading the following :

<link rel="stylesheet" type="text/css" href="./irc.css" />
<script type="text/javaScript" src="../Clients/mootools-core.js"></script>
<script type="text/javaScript" src="../Clients/MooTools.js"></script>
<script type="text/javaScript" src="../Clients/config.js"></script>
<script type="text/javaScript" src="./irc.js"></script>
<script type="text/javaScript" src="./apeirc.js"></script>
<script type="text/javaScript" src="./channel.js"></script>
<script type="text/javascript">
function rand_chars(){
        var keylist="abcdefghijklmnopqrstuvwxyz"
                var temp=''
                var plength=8;
        for (i=0;i<plength;i++){
                temp+=keylist.charAt(Math.floor(Math.random()*keylist.length))
        }
        return temp;
   }
   function setNick(){
       var nick = $('nick_input').value.replace(' ', '');
       if(nick.length == 0){
           alert('Veuillez entrer un login');
       }else{
           if( irc_client.setNick(nick.substr(0, 15)) ){
                var login = $('login');
                login.set('tween', {duration: 'short'});
                login.tween('opacity', 0);
                                setNick = function(){};
                        }
       }
   }
 
   var irc_client = new APE.IrcClient();
   window.addEvent('domready', function(){
       $('nick_input').focus();
       irc_client.load({
           'domain': APE.Config.domain,
           'server': APE.Config.server,
           'identifier':'ircdemo',
           'scripts': APE.Config.scripts,
           'complete': function(ape){
               irc_client.complete();
           }
       });
   });
</script>
<div id="ircchat">
	<div id="login">
	  <div class="popup">
	      <p>Choose a nickname to sign in to IRC on freenode</p>
	      Your nickname : <input maxlength="15" id="nick_input" onkeypress="if (event.keyCode == 13) setNick();"  type="text" /> <button onclick="setNick();">Ok</button>
	  </div>
	</div>
	<div class="header">
		<div class="server">IRC @ freenode</div>
		<div id="tabs"></div>
	</div>
	<div class="chat">
		<div class="texte-zone">
			<div id="line">&nbsp;</div>
			<div class="texte" id="history"></div>
		</div>
		<div class="list">
			<div class="header"><span id="usr_cnt">0</span> users</span></div>
			<div class="user-list">
		    	<div id="user-list"></div>
			</div>
		</div>
	    <div class="input-zone">
	    	<button id="chatButton" onclick="irc_client.sendClick()">Ok</button>
	        <input type="text" id="chat-input" />
	        <div class="show-nick">jchavarria_work</div>
	    </div>
	</div>
</div>
<div id="ape_master_container"></div>
irc.js

/* irc.js
 *  This IRC client runs in a web browser using pure JavaScript
 *  Orbited 0.5+ required
 *
 *  Methods:
 *      connect(hostname, port)
 *      ident(nickname, modes, real_name)
 *      join(channel)
 *      names(channel)
 *      part(channel)
 *      quit(reason)
 *      privmsg(destination, message)
 *
 *  Callbacks:
 *      Built-in callbacks are onconnect(), onerror(), onresponse(), and onclose()
 *      onerror and onreply are passed numerical reponse codes, see:
 *      http://www.irchelp.org/irchelp/rfc/chapter6.html for a list of IRC response
 *      codes.
 *
 *      To add callbacks for IRC actions, for instance PRIVMSG,
 *          set onPRIVMSG = function(command) {...you code here...}
 *      See the included IRC demo (/static/demos/irc) for example usage
 *
 * Frank Salim (frank.salim@gmail.com)
 * ©2008 The Orbited Project
 */

// TODO DRY this by creating a common logging infrastructure (this is also on stomp.js)
IRC_DEBUG = false;

if (IRC_DEBUG && typeof(Orbited)) {
    var getIrcLogger = function(name) {
        var logger = Orbited.getLogger(name);
        if (!("dir" in logger)) {
            logger.dir = function() {};
        }
        return logger;
    }
}
else if (IRC_DEBUG && typeof(console)) {
    var getIrcLogger = function(name) {
        return {
            debug: function() {
                var args = Array.prototype.slice.call(arguments);
                args.unshift(name, ": ");
                console.debug.apply(console, args);
            },
            dir: function() {
                console.debug(name, ":");
                console.dir.apply(console, arguments);
            }
        };
    };
}
else {
    var getIrcLogger = function(name) {
        return {
            debug: function() {},
            dir: function() {}
        };
    };
}

IRCClient = function() {
    var log = getIrcLogger("IRCClient");
    var self = this
    var conn = null
    var buffer = ""
    var ENDL = "\r\n"

    self.onopen = function() {};
    self.onconnect = function() {}      // Do nothing in default callbacks
    self.onclose = function() {}
    self.onerror = function(command) {}
    self.onresponse = function(command) {}     // used for numerical replies

    self.connect = function(hostname, port) {
        log.debug("connect");
        conn = self._createTransport();
        conn.onopen = conn_opened
        conn.onclose = conn_closed
        conn.onread = conn_read
        conn.open(hostname, port)
        // TODO set onerror.
    }
    self._createTransport = function() {
        return new TCPSocket();
    };
    self.close = function(code) {
        log.debug("close: "+code);
        conn.close();
        conn.onopen = null;
        conn.onclose = null;
        conn.onread = null;
        self.onclose(code);
    }
    self.ident = function(nickname, modes, real_name) {
        send("USER", nickname + " " + modes + " :" + real_name)
    }
    self.nick = function(nickname) {
        send("NICK", nickname)
    }
    self.join = function(channel) {
        send("JOIN", channel)
    }
    self.names = function(channel) {
        send("NAMES", channel)
    }
    self.ctcp = function(to, cmd, rep) {
        if(!rep)
            this.privmsg(to, '\01'+cmd+'\01');
        else
            this.notice(to, '\01'+cmd+'\01');
    }
    self.part = function(channel, reason) {
        send("PART", channel + " :" + reason)
    }
    self.quit = function(reason) {
        var reason = reason || "leaving";
        send("QUIT", ":" + reason)
        conn.close()
    }
    self.reset = function() {
        conn.reset();
    }
    self.action = function(destination, message) {
        send('PRIVMSG', destination + ' :\01ACTION ' + message + '\01')
    }
    self.notice = function(destination, message) {
        send('NOTICE', destination + ' :'+message)
    }
    self.privmsg = function(destination, message) {
        send('PRIVMSG', destination + ' :' + message)
    }

    // Socket Callbacks
    var conn_opened = function() {
        self.onopen()
    }
    var conn_closed = function(code) {
        self.onclose(code)
    }
    var conn_read = function(data) {
        log.debug("data:");
        log.debug(data);
        buffer += data
        parse_buffer()
    }

    // Internal Functions
    var send = function(type, payload) {
        log.debug("send: " + payload);
        conn.send(type + " " + payload + ENDL);
    };
    var parse_buffer= function() {
        var commands = buffer.split(ENDL);
        buffer = commands[commands.length-1];
        for (var i = 0, l = commands.length - 1; i < l; ++i) {
            var line = commands[i];
            if (line.length > 0)
                dispatch(line);
        }
    };
    var parse_command = function(s) {
        // See http://tools.ietf.org/html/rfc2812#section-2.3

        // all the arguments are split by a single space character until
        // the first ":" character.  the ":" marks the start of the last
        // trailing argument which can contain embeded space characters.
        var i = s.indexOf(" :");
        if (i >= 0) {
            var args = s.slice(0, i).split(' ');
            args.push(s.slice(i + 2));
        } else {
            var args = s.split(' ');
        }

        // extract the prefix (if there is one).
        if (args[0].charAt(0) == ":") {
          var prefix = args.shift().slice(1);
        } else {
          var prefix = null;
        }

        var command = {
            prefix: prefix,
            type: args.shift(),
            args: args
        };
        log.debug("command:");
        log.dir(command);
        return command;
    };
    var dispatch = function(line) {
        
		
		command = parse_command(line);
        
		
		//console.log('COMMAND',command.type);

		if (command.type == "PING") {
            send("PONG", ":" + command.args)
        }
        
		
		if (!isNaN(parseInt(command.type))) {
            var error_code = parseInt(command.type)
            if (error_code > 400)
                return self.onerror(command)
            else
                return self.onresponse(command)
        }
        if (command.type == "PRIVMSG") {
            msg = command.args[1]
            if (msg.charCodeAt(0) == 1 && msg.charCodeAt(msg.length-1) == 1) {
                var args = [command.args[0]]
                var newargs = msg.slice(1, msg.length - 1).split(' ')
                if (newargs[0] == 'ACTION') {
                    command.type = newargs.shift()
                }
                else {
                    command.type = 'CTCP'
                }

                for (var i = 0; i < newargs.length; ++i) {
                    args.push(newargs[i])
                }
                command.args = args
            }
        }
        if (typeof(self["on" + command.type]) == "function") {
            // XXX the user is able to define unknown command handlers,
            //     but cannot send any arbitrary command
            self["on" + command.type](command);
        } else {
            log.debug("unhandled command received: ", command.type);
        }
    };
};


apeirc.js
/**
 * This demo uses APE to create a Vitual TCPSocket and
 * connect to an irc server (Its transparent, but it's the ape server which
 * connects to the irc server).
 *
 *
 */

var TCPSocket;
APE.IrcClient = new Class({
	Extends: APE.Client,
	Implements: Options,
	irc: null,
	connected: false,
	joined: false,
	currentChannel: null,
	version: '0.1',
	options: {
		container: document.body,
	   /*
	   irc_server: '10.1.0.30',
	   irc_port: 6667,
	   */
	   
	   irc_server: 'irc.freenode.net',
	   irc_port: 6667,
	   /*

		irc_server: 'Vancouver.BC.CA.Undernet.org',
		irc_port: 6667,
	   */
		
		original_channels:['#ape-test','#ape-project'],
		systemChannel: '@freenode',
		systemUser: '*System*',
		helpUser: '*HELP*'
	},
	els: {},
	channels: new Hash(),
	initialize: function(options){
		this.currentChannel = this.options.systemChannel;

		this.setOptions(options);
		this.joinVirtualChannel(this.options.systemChannel);

		
	},
	joinVirtualChannel: function(chan, irc, buildTabs, doNotClose){
		this.setChannel(chan, new APE.IrcClient.Channel(chan, irc, doNotClose));
		if(buildTabs){
			this.buildTabs();
		}
	},
	joinUserChannel: function(user){
		if(!this.hasChannel(user)){
			this.joinVirtualChannel(user, this.irc, true, true);
		}
	},
	complete: function(){
		
		this.core.start({name:rand_chars()});
		this.onRaw('login', this.initPlayground);
	},
	initPlayground: function(){

		
		
		this.els.chatZone = $('history');
		this.els.tabs = $('tabs');
		this.els.input = $('chat-input');
		this.els.userList = $('user-list');
		this.els.userCnt = $('usr_cnt');

		//Adding special events
		this.els.input.addEvent('keydown', this.sendKey.bindWithEvent(this));


		//TCPSocket implementation
		TCPSocket = this.core.TCPSocket;

		//IRC events
		this.irc = new IRCClient();
		this.irc.onopen  = this.onopen.bind(this);
		this.irc.onclose = this.onclose.bind(this);
		this.irc.onTOPIC = this.onTOPIC.bind(this);
		this.irc.onNICK = this.onNICK.bind(this);
		this.irc.onJOIN = this.onJOIN.bind(this);
		this.irc.onQUIT = this.onQUIT.bind(this);
		this.irc.onPART = this.onPART.bind(this);
		this.irc.onACTION = this.onACTION.bind(this);
		this.irc.onCTCP  = this.onCTCP.bind(this);
		this.irc.onNOTICE  = this.onNOTICE.bind(this);
		this.irc.onPRIVMSG = this.onPRIVMSG.bind(this);
		this.irc.onMODE = this.onMODE.bind(this);
		this.irc.onERROR = this.onerror.bind(this);
		this.irc.onerror = this.onerror.bind(this);
		this.irc.onresponse = this.onresponse.bind(this);


	},
	changeMyNick: function(nick){
		this.nickname = nick;
		
		$$('.show-nick').set('html', nick);
	},
	setNick: function(nickname){
		if(!this.irc) return false;
		nickname = this.cleanNick(nickname);
		this.irc.connect(this.options.irc_server, this.options.irc_port);
		this.changeMyNick(nickname);
		return true;
	},
	/** CHANNELS ACCESSORS */
	getChannel: function(key){
		return this.channels.get(this.toChan(key));
	},
	hasChannel: function(key){
		return this.channels.has(this.toChan(key));
	},
	setChannel: function(key, chan){
		this.channels.set(this.toChan(key), chan);
	},
	eraseChannel: function(key){
		this.channels.erase(this.toChan(key));
	},
	compareChannels: function(a, b){
		return this.toChan(a) == this.toChan(b);
	},
	/**
	 * connect() is called two times, but only does something when irc is connected
	 * AND nickname is set
	 */
	connect: function(){
		if(this.nickname && this.connected){
			this.irc.ident(this.nickname, '8 *', this.nickname);
			this.irc.nick(this.nickname);
		}
	},
	onopen: function(){
		
		this.connected = true;
		this.connect();
		/*
		window.addEvent('unload', function(){
			this.irc.reset();
		}.bind(this));
		*/
	},
	onclose: function(){
		
	},
	onerror: function(cmd){
		if(cmd.args[0] == "Closing Link: 127.0.0.1 (Connection Timed Out)"){
			this.showInfo('Error occured, reconnecting...', 'error');
			this.irc.connect(this.options.irc_server, this.options.irc_port);
			return;
		}
		var responseCode = parseInt(cmd.type);
		if (responseCode == 431 || responseCode == 432 || responseCode == 433) {
		// 431	 ERR_NONICKNAMEGIVEN
		// 432	 ERR_ERRONEUSNICKNAME
		// 433	 ERR_NICKNAMEINUSE
			var nick = cmd.args[1];

			if(responseCode == 432){
				nick = this.cleanNick(nick);
				this.changeMyNick(nick);
			}else
				if(nick.length == 15){

				}

				nick = (nick.length>= 15?nick.substr(1, 14):nick) + '_';
				this.changeMyNick(nick);
				this.irc.nick(nick);
				//this.irc.ident(this.nickname, '8 *', this.nickname);
		}else if(responseCode == 451){
			this.irc.ident(this.nickname, '8 *', this.nickname);
		}else{
			this.showInfo(this.sanitize(cmd.args.pop()), 'error');
		}
	},
	onresponse: function(cmd){
		var responseCode = parseInt(cmd.type);
		if(responseCode==372){
			this.addMessage(this.options.systemChannel, this.options.systemUser, cmd.args[1].substr(2), 'info');
			
			if(!this.joined){
				var cnt = this.options.original_channels.length;

				for(var i = 0; i < cnt; i ++) {
					this.joinChannel(this.options.original_channels[i], i==0);
				}
				
			}
		}else if(responseCode==366){
			this.addMessage(this.options.systemChannel, this.options.systemUser, 'Joined '+cmd.args[1]+' channel.', 'info user-join');
		}
		else if(responseCode==353){
			var channel = cmd.args[2];
			var userList = cmd.args[3].split(/\s+/);
			
			userList.each(function(user){
				if(user != '')
					this.addUser(channel, user, false, false);
			}.bind(this));
			if(this.compareChannels(channel,this.currentChannel)){
				var chan = this.getCurrentChannel();
				chan.sortUsers();
				this.buildUsers();
			}

		}
		else if(responseCode==332){
			this.addMessage(cmd.args[1], this.options.systemUser, ['Topic for '+cmd.args[1]+' is: ',new Element('i',{'text':this.sanitize(cmd.args[2])})],'info topic');
		}
		else if(responseCode==333){
			var d = new Date();
			var time = parseInt(cmd.args[3], 10);
			d.setTime(time*1000);
			var when = d.toString();
			this.addMessage(cmd.args[1], this.options.systemUser, ['Topic for '+cmd.args[1]+' was set by ',this.userLink(this.parseName(cmd.args[2])),' on '+when], 'info topic');
		}
	},
	/**
	 * NAMES
	 * CTCP VERSION
	 */
	onJOIN: function(cmd){
		var chan = cmd.args[0];
		var user = this.parseName(cmd.prefix);
		this.addUser(chan, user);
	},
	onMODE: function(cmd){
		
		var chan = this.getChannel(cmd.args[0]);
		if(chan){
			var from = this.parseName(cmd.prefix);
			var user = cmd.args[2];
			var op = cmd.args[1];
			if(op.substr(0,1)=='+'){
				var build = false;
				if(op.contains('o')){
					this.addMessage(chan, this.options.systemUser,
					[this.userLink(from),' gives channel operator status to ',this.userLink(user)], 'info status');
					chan.renameUser(user, '@'+user);
					build = true;
				}else if(op.contains('v')){
					this.addMessage(chan, this.options.systemUser,
					[this.userLink(from),' gives channel operator status to ',this.userLink(user)], 'info status');
					chan.renameUser(user, '+'+user);
					build = true;
				}
				if(build){
					this.buildUsers();
				}
			}
		}
	},
	onPART: function(cmd){
		var chan = this.getChannel(cmd.args[0]);
		if(chan){
			var user = this.parseName(cmd.prefix);
			var msg = String(cmd.args[1]);

			chan.remUser(user);
			this.addMessage(chan.name, this.options.systemUser, user+' has left '+chan.name+(msg.length>0?' ('+msg+')':''), 'user-left');
			if(chan.name==this.currentChannel){
				this.remUser(user);
			}
		}
	},
	onQUIT: function(cmd){
		var user = this.parseName(cmd.prefix);
		this.channels.each(function(chan){
			if(chan.remUser(user)){
				this.addMessage(chan.name, this.options.systemUser, [this.userLink(user),' has quit ('+this.sanitize(cmd.args[0])+')'], 'info user-left');
				if(chan.name==this.currentChannel){
					this.remUser(user);
				}
			}
		}.bind(this));
	},
	onNICK: function(cmd){
		this.changeNick(this.parseName(cmd.prefix), cmd.args[0]);
	},
	onTOPIC: function(cmd){
		var user = this.parseName(cmd.prefix);
		this.addMessage(cmd.args[0], this.options.systemUser, user+' has changed the topic to: '+cmd.args[1], 'info topic');
	},
	onCTCP: function(cmd){
		if(cmd.args[1]=='VERSION'){
			this.irc.ctcp(this.parseName(cmd.prefix), 'VERSION APE TCPSocket Demo (IRC) 2 - http://www.ape-project.com v'+this.version+' On a Web Browser (' + navigator.appCodeName + ')', true);
		}else{
			
		}
	},
	onACTION: function(cmd){
		var user = this.parseName(cmd.prefix);
		var args = cmd.args;
		this.addMessage(args.shift(), this.options.systemUser, [this.userLink(user),' '+this.sanitize(args.join(' '))], 'info action');
	},
	onNOTICE: function(cmd){
		var msg = cmd.args[1];
		if(msg.charCodeAt(0)==1 && msg.charCodeAt(msg.length - 1) == 1){
			// CTCP REPLY
			msg = msg.substr(1,msg.length-2).split(' ');
			this.addCTCP(cmd.args[0], msg.shift(), msg.join(' '));
		}else{
			if(cmd.prefix && cmd.prefix.substr(0,3)=='irc')
				this.addMessage(this.options.systemChannel, this.options.systemUser, this.sanitize(cmd.args[1]), 'info notice');
			else
				this.showInfo(this.sanitize(cmd.args[1]), 'notice');
		}
	},
	onPRIVMSG: function(cmd){
		var from = this.parseName(cmd.prefix);
		var chan = cmd.args[0];
		var msg = cmd.args[1];

		if(this.compareChannels(chan, this.nickname)){
			this.joinUserChannel(from);
			this.addUser(from, from);
			chan = from;
		}
		this.addMessage(chan, from, this.sanitize(msg));
	},
	addCTCP: function(who, what, txt){
		if(txt != undefined){
			this.addMessage(this.currentChannel, '-'+who+'-', 'CTCP '+what+' REPLY: '+txt, 'ctcp');
		}else{
			this.addMessage(this.currentChannel, '>'+who+'<', 'CTCP '+what, 'ctcp')
		}
	},
	getCurrentChannel: function(){
		return this.getChannel(this.currentChannel);
	},
	joinChannel: function(channel, switchTo){
		
		if(!this.hasChannel(channel)){
			var chan = new APE.IrcClient.Channel(channel, this.irc);
			this.setChannel(channel, chan);
			chan.join();
			if(switchTo!==false) this.switchTo(channel);
			else this.buildTabs();
		}else if(switchTo!==false){
			this.switchTo(channel);
		}
		this.joined = true;
	},
	switchTo: function(chan){

		if(!this.hasChannel(chan)){
			return;
		}
		var channel = this.getChannel(chan);

		/** For history */
		channel.Hcnt = 0;

		this.currentChannel = chan;

		/* Updating messages and users*/
	   this.els.chatZone.getElements('div.msg_line').dispose();


		this.buildTabs();
		
		this.buildUsers();

		this.checkLine();
		
		this.buildMsg();
	},
	cleanNick: function(nick){
		var ret = String(nick).replace(/[^a-zA-Z0-9\-_~]/g, '');
		ret = ret.replace(/^[0-9]+/, '');
		if(ret=='') return 'Guest'+Math.round(Math.random()*100);
		
		if(nick != ret) this.showError('Invalid nick "'+this.sanitize(nick)+'" changed to "'+ret+'".');
		return ret;
	},
	changeNick: function(from, to){
		this.channels.each(function(chan){
			if(chan.renameUser(from, to)){
				this.addMessage(chan.name, this.options.systemUser, [this.userLink(from)," is now known as ", this.userLink(to)], 'info');
			}
			if(this.compareChannels(chan.name, from)){
				this.changeChannelName(from, to);
			}
		}.bind(this));
		if(this.compareChannels(from, this.nickname))
			this.changeMyNick(to);
		this.buildUsers();
	},
	changeChannelName: function(from, to){

		var chan = this.getChannel(from);
		this.eraseChannel(from);
		chan.name = to;
		this.setChannel(to, chan);
		if(this.compareChannels(from, this.currentChannel)){
			this.currentChannel = from;
		}
		this.buildTabs();
	},
	parseName: function(identity){
		return identity.split("!", 1)[0];
	},
	remUser: function(user){
		var go = new Fx.Morph($('user_line_'+user));
		go.start({
			'height': 0
		});
		go.addEvent('complete', function(el){
			el.destroy();
		})

		this.userCntAdd(-1);
	},
	addUser: function(chan, user, showMessage, addNow){

		var channel =  this.getChannel(chan);
		if(!channel){
			return;
		}
		var add;
		if(add = channel.addUser(user, addNow)){
			if(this.compareChannels(chan, this.currentChannel) && addNow !== false){

				if(channel.getLastUser().type != 'zzz' || add == 'rename'){
					this.buildUsers();
				}else{
					this.writeUser(channel.buildLastUser( this.userClick.bindWithEvent(this) ));
					this.userCntAdd(1);
				}
			}
			if(showMessage !== false)
				this.addMessage(chan, this.options.systemUser, [this.userLink(user),' has joined'+(chan==user?'':' '+chan)], 'info user-join');
		}
	},
	writeUser: function(el_user){
		this.els.userList.adopt(el_user);
	},
	addMessage: function(chan, user, txt, special){
		var channel = this.getChannel(chan);
		if(channel){
			if(user != this.options.systemUser) user = this.userLink(user);
			channel.addMessage(user, txt, special, this.nickname);
			if(this.compareChannels(chan, this.currentChannel)){
				this.writeMsg(channel.buildLastMessage());
			}
		}
	},
	writeMsg: function(el_line){
		this.els.chatZone.adopt(el_line);
		this.scrollBottom();
		this.checkLine();
	},
	checkLine: function(){
		var chan = this.getCurrentChannel();
		var lineW = chan.getLineWidth();
		var textW = 590 - lineW;

		var line_size = Math.round((textW - 10) / 6);
		chan.line_size = line_size;

		// TODO Resize long lines

		$$('.msg_user').tween('width', lineW);
		$('line').tween('left', lineW);
		$$('.msg_text').tween('width', textW);
	},
	scrollBottom: function(){
		var scrollSize = this.els.chatZone.getScrollSize();

		this.els.chatZone.scrollTo(0,scrollSize.y);
	},
	sendMsg: function(msg, checkCmd){

		var channel = this.getCurrentChannel();

		channel.history.unshift(msg);
		channel.Hcnt = 0;

		//This is a command
		if(checkCmd !== false && msg.substr(0,1)=='/')
			this.sendCmd(msg.substr(1).split(' '));
		else{
			channel.send(msg);
			// the IRC server will not echo our message back, so simulate a send.
			this.onPRIVMSG({prefix:this.nickname,type:'PRIVMSG',args:[this.currentChannel, msg]});
		}
	},
	sendCmd: function(args){
		switch(String(args[0]).toLowerCase()){
			case 'j':
			case 'join':
				if(!args[1] || !args[1].match(/^(#|&)[^ ,]+$/))
					this.showError('Invalid chan name "'+this.sanitize(args[1])+'" chan name must begin with # or & and must not contains any of \' \' (space) or \',\' (comma) and must contains at least 2 chars.')
				else
					this.joinChannel(args[1])
				break;
			case 'clear':
				if(!args[1] || args[1]!='ALL'){
					this.getCurrentChannel().clear();
				}else{
				   this.channels.each(function(chan){
					   chan.clear();
				   });
				}
				this.els.chatZone.empty();
				break;
			case 'quit':
				args.shift();
				window.location.reload();
				this.irc.quit(args.join(' '));
				break;
			case 'ctcp':
				args.shift();
				var user = args.shift();

				this.irc.ctcp(user, args.join(' '));
				this.addCTCP(user, args[0]);
				break;
			case 'me':
			case 'action':
				if(!args[1]){
					this.showError('Invalid arguments, /help for more informations');
				}
				args.shift();
				this.irc.action(this.currentChannel, args.join(' '));
				this.onACTION({prefix:this.nickname,args:[this.currentChannel, args.join(' ')]});
				break;
			case 'msg':
			case 'privmsg':
				if(args[1] == undefined || args[2] == undefined ){
					this.showError('Invalid arguments, /help for more informations');
					break;
				}
				args.shift();

				var to = String(args.shift());
				if(to.charAt(0)=='@') to = to.substr(1);
				
				var msg = args.join(' ');

				this.joinUserChannel(to);
				
				this.switchTo(to);
				this.sendMsg(msg, false);
				
				break;
			case 'nick':
				this.irc.nick(args[1]);
				//this.changeMyNick(args[1]);
				break;
			case 'h':
			case 'help':
				this.addMessage(this.currentChannel, this.options.helpUser,'\
List of available commands :\n\
\n\
	/HELP , show this help.\n\
	/H , alias for /HELP.\n\
	/JOIN , joins the channel.\n\
	/CLEAR [ALL], clear current channels, clears messages and input history.\n\
	/QUIT [], disconnects from the server.\n\
	/CTCP  , send a CTCP message to nick, VERSION and USERINFO are commonly used.\n\
	/ACTION , send a CTCP ACTION message, describing what you are doing.\n\
	/PRIVMSG [@] , sends a private message.\n\
	/MSG [@] , alias for /PRIVMSG.\n\
	/NICK , sets you nickname.\n\
\n\
Commands are case insensitive, for example you can user /join or /JOIN.','help')
				break;
			default:
				this.showError('Unknow or unsuported command "'+this.sanitize(args[0])+'".');
		}
	},
	showInfo: function(msg, type){
		this.addMessage(this.currentChannel, this.options.systemUser, msg, type);
	},
	showError: function(msg){
		this.showInfo(msg, 'error')
	},
	sendKey: function(ev){
		if(ev.key == 'enter') this.sendClick();
		else if(ev.key == 'up'){
			var chan = this.getCurrentChannel();
			if(chan.Hcnt == 0){
				chan.currMsg = this.els.input.value;
			}
			if(chan.history.length > chan.Hcnt){
				var i = chan.Hcnt++;
				this.els.input.value = chan.history[i];
			}
		}else if(ev.key == 'down'){
			var chan = this.getCurrentChannel();
			if(chan.Hcnt > 0){
				var i = --chan.Hcnt;

				this.els.input.value = i==0?chan.currMsg:chan.history[i-1];
			}
		}else if(ev.key=='tab'){
			ev.stop();
			var val = this.els.input.value;
			if(val.charAt(0)=='/' && !val.contains(' ')){
				val = val.substr(1);
				var cmd_list = new Array(
					'help',
					'join',
					'clear',
					'quit',
					'ctcp',
					'action',
					'privmsg',
					'nick',
					'msg'
				);
				var check = function(cmd, index,array){
					if(cmd.substr(0, val.length)==val){
						return true;
					}
					return false;
				}
				var ok = cmd_list.filter(check);
				if(ok.length > 0){
					this.els.input.value = '/'+ok[0].toUpperCase()+' ';
				}
			}else{
			val = val.split(' ');
			var mot = val.pop();
			if(mot.charAt(0)=='@') mot = mot.substr(1);
				if(mot.length > 0){
					var chan = this.getCurrentChannel();
					var usr = chan.search(mot);
					if(usr){
						this.els.input.value = val.join(' ')+(val.length > 0?' ':'')+'@'+usr+' ';
					}
				}
			}
		}
	},
	sendClick: function(){

		var value = this.els.input.value;
		if(value.length > 0){
			if(value.substr(0, 1)=='/' || this.currentChannel != this.options.systemChannel){
				this.sendMsg(this.els.input.value);
			}else{
				this.showError('No channel joined. Try /join #');
			}
		}
		this.els.input.value = '';
	},
	tabClick: function(event, chan){
		this.switchTo(chan);
	},
	tabCloseClick: function(event, chan){
		this.closeTab(chan);
		event.stop();
	},
	buildTabs: function(){
		/*
		
#ape-project
*/ var current = this.currentChannel; this.els.tabs.empty(); var tabs = new Array(); this.channels.each(function(chan, key){ var el_tab = new Element('div', { 'class': 'tab'+(this.compareChannels(chan.name, current)?' current':'')+(this.compareChannels(key, this.options.systemChannel)?' sys':''), 'id': 'tab_'+key }); var el_link = new Element('span', { 'text': chan.name, 'class': 'link' }); var el_close = new Element('a', { 'href': '#' }); el_tab.grab(el_close); el_tab.grab(el_link); if(!this.compareChannels(chan.name, current)) el_link.addEvent('click', this.tabClick.bindWithEvent(this, key)); if(!this.compareChannels(chan.name, this.options.sysChannel)) el_close.addEvent('click', this.tabCloseClick.bindWithEvent(this, key)); tabs.push(el_tab); }.bind(this)); this.els.tabs.adopt(tabs); this.els.input.focus(); }, closeTab: function(tab){ if(this.compareChannels(tab,this.currentChannel)){ var keys = this.channels.getKeys(); var i = keys.indexOf(tab); var k = 0; if(i==keys.length-1){ k = i - 1; }else{ k = i+1; } this.switchTo(keys[k]); } var chan = this.getChannel(tab); chan.close(''); this.eraseChannel(tab); $('tab_'+tab).destroy(); }, buildUsers: function(){ this.els.userList.empty(); var users = this.getCurrentChannel().buildUsers(this.userClick.bindWithEvent(this)); this.writeUser(users); this.userCntSet(users.length); }, userCntSet: function(cnt){ this.els.userCnt.set('text', cnt); this.els.userCnt.store('cnt', cnt); }, userCntAdd: function(num){ var cnt = this.els.userCnt.retrieve('cnt', 0); this.userCntSet(cnt+num); }, userClick: function(ev, e2){ ev.stop(); var to = String(e2).replace(/^(@|\+)/g, ''); if(!this.hasChannel(to)){ this.joinVirtualChannel(to, this.irc, true, true); } this.switchTo(to); }, buildMsg: function(){ this.writeMsg(this.getCurrentChannel().buildMessages()); }, toChan: function(str){ return String(str).toLowerCase(); }, userLink: function(user){ var link = new Element('a', { 'text': user, 'class': 'user', 'href': '#' }); link.addEvent('click', this.userClick.bindWithEvent(this, user)); return link; }, sanitize: function(str){ //all text elements are grabed via appendText, so there is no need to escape return str; } /* sanitize: (function(str) { // See http://bigdingus.com/2007/12/29/html-escaping-in-javascript/ var MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; var repl = function(c) { return MAP[c]; }; return function(s) { s = s.replace(/[&<>'"]/g, repl); return s; }; })() */ });
channel.js
APE.IrcClient.Channel = new Class({
    name: false,
    dontclose:false,
    messages: new Array(),
    users: false,
    last_user: '',
    irc: null,
    line_size: 83,
    history: new Array(),
    MAX_HISTORY: 100,
    Hcnt: 0,
    initialize: function(name, irc, dontclose){
        this.dontclose = dontclose===true;
        this.name = name;
        this.irc = irc;
        this.plop = rand_chars();
		this.users = new Hash();
    },
    addUser: function(username, sort){
		
        var type;
        switch(username.substr(0,1)){
            case '@':
                type = 'operator';
                break;
            case '+':
                type = 'voice';
                break;
            default:
                type = 'zzz';
        }

        if(type != 'zzz' && this.users.has(username.substr(1))){
            this.renameUser(username.substr(1), username);
            this.users.get(username.type = type);
            return 'rename';
        }

        if(this.users.has(String(username))){
            return false;
        }
        this.users.set(username, {
            'nick': username,
            'type': type
        });
        this.last_user = username;
        if(type != 'zzz' && sort !== false) this.sortUsers();
        return true;
    },
    sortUsers: function(){
        var a_users = this.users.getValues();
        a_users.sort(this.compareUsers);
        this.users.empty();
        a_users.each(function(usr){
            this.users.set(usr.nick, usr);
        }.bind(this));
    },
    compareUsers: function(a, b){
        if(a.type == b.type){
            return 0;
        }else if(a.type > b.type){
            return 1;
        }else{
            return -1
        }
    },
    remUser: function(username){
        if(this.users.has(username)){
            this.users.erase(username);
            return true;
        }

        return false;
    },
    renameUser: function(from, to){
        if(this.users.has(from)){
            //var user = this.users.get(from);
            this.remUser(from);
            this.addUser(to);
            return true;
        }else if(this.users.has('@'+from)){
            return this.renameUser('@'+from, (to.substr(0,1)=='@'||to.substr(0,1)=='+')?to:'@'+to);
        }else if(this.users.has('+'+from)){
            return this.renameUser('+'+from, (to.substr(0,1)=='@'||to.substr(0,1)=='+')?to:'+'+to);
        }
        return false;
    },
    getLastUser: function(){
        return this.users.get(this.last_user);
    },
    search: function(str){
        var ret = false;
        this.users.each(function(usr){
            var start = 0;
            if(usr.nick.charAt(0)=='@' || usr.nick.charAt(0)=='+') start = 1;
            if(usr.nick.substr(start, str.length) == str){
                ret = usr.nick.substr(start);
            }
        }.bind(this));
        return ret;
    },
    userCount: function(){
        return this.users.getLength();
    },
    addMessage: function(from, msg, special, user){

        var txt = new Array();
        
        msg = $type(msg)=='array'?msg:[msg];
        msg.each(function(item){
            if($type(item)!='element'){
                txt = txt.concat(this.parseText(String(item)));
            }else{
                txt.push(item);
            }
        }.bind(this))

        var message = {
            'name': from,
            'msg' : txt,
            'date': new Date(),
            'special': special,
            'usr': user
        };
        this.messages.push(this.makeMessageLine(message));

        if(this.messages.length > this.MAX_HISTORY) {
            var destroy = this.messages.slice(0, this.messages.length - this.MAX_HISTORY);
            this.messages = this.messages.slice(-this.MAX_HISTORY);
            destroy.each(function(el){
                el.destroy();
            })
        }
    },
    makeMessageLine: function(message){
        /*
        
[12:30]    Username
                Txt of the message
            
me "plop" chan name must begin with # or & and must not contains any of ' ' (sp */ var el_txt = this.splitText(message); //Highlighting if(message.usr != undefined){ var reg = new RegExp('@?'+message.usr+'([^a-z~0-9\-_]|$)', 'i'); if(el_txt.get('text').match(reg)){ message.special = message.special==undefined?'highlight':message.special+' highlight'; } } var el_line = new Element('div', { 'class': 'msg_line'+(message.special?' '+message.special:'') }); var el_divu = new Element('div', { 'style': 'width:'+this.getLineWidth()+'px', 'class': 'msg_user' }); var hours = message.date.getHours(); hours = hours > 9 ? hours : '0'+hours; var min = message.date.getMinutes(); min = min > 9 ? min : '0'+min; var el_user = new Element('pre', { 'text': '['+hours+':'+min+'] '+($type(message.name)!="element"?message.name:'') }); if($type(message.name)=='element') el_user.grab(message.name); el_divu.grab(el_user); el_line.grab(el_divu); el_line.grab(el_txt); return el_line; }, join: function(){ if(this.irc) this.irc.join(this.name); }, splitWords: function(item){ var ret = new Array(); var tmp = item.split(' '); tmp.each(function(word, k, list){ ret.push( word + (k]*>/g, '').length; } if(cur_size + length > this.line_size){ if(cur_size < this.line_size /2 && type!='element'){ while(word.length > this.line_size - cur_size){ ret.appendText(String(word).substr(0, this.line_size - cur_size -1)+'-'); word = String(word).substr(this.line_size - cur_size - 1); ret.appendText('\n'); cur_size = 0; } } ret.appendText('\n'); cur_size = 0; } if(type=='element'){ ret.grab(word); }else{ ret.appendText(String(word)); } cur_size += length; }.bind(this)); return ret; }, buildUsers: function(func){ var ret = new Array(); this.users.each(function(item, key){ ret.push(this.buildUser(key, func)); }.bind(this)); return ret; }, buildUser: function(usr, func){ /* user_name */ var user = this.users.get(usr); var el_usr = new Element('a', { 'class': 'user-item '+user.type, 'id': 'user_line_'+user.nick, 'text': user.nick, 'href': '#' }); el_usr.addEvent('click', function(ev, nick) { func(ev, user.nick)}.bindWithEvent(this, user.nick)); return el_usr; }, buildLastUser: function(func){ return this.buildUser(this.last_user, func); }, buildMessages: function(){ return this.messages; }, buildMessage: function(msg){ return this.messages[msg]; }, send: function(msg){ if(this.irc) this.irc.privmsg(this.name, msg); }, clear: function(){ this.messages = new Array(); this.history = new Array(); }, buildLastMessage: function(){ return this.buildMessage(this.messages.length -1); }, hasIrc: function(){ return this.irc?true:false; }, getMaxNicklength: function(){ // TODO Mise en cache du max et maj a l'ajout de messages var ret = 0; if(this.messages.length == 0) return 8; this.messages.each(function(msg){ var txt = msg.getElement('.msg_user').get('text'); ret = Math.max(txt.length-9, ret); }); return Math.max(ret, 8); }, getLineWidth: function(){ return (this.getMaxNicklength()+10)*6; }, parseText: function(txt){ txt = txt.replace('\03', ''); //Fo freenode special "cotes" txt = txt.replace(/\02/g, '"'); txt = txt.replace(/([a-z]{2,6}:\/\/[a-z0-9\-_#\/*+%.~,?&=]+)/gi, '\03$1\04$1\03'); txt = txt.replace(/([^a-z0-9\-_.\/]|^)((?:[a-z0-9\-_]\.?)*[a-z0-9\-_]\.(?:com|arpa|asia|pro|tel|travel|jobs|edu|gov|int|mill|net|org|biz|arpa|info|name|pro|aero|coop|museum|mobi|[a-z]{2})(?:\/[a-z0-9\-_#\/*+%.~,?&=]*)?)([^a-z]|$)/gi, '$1\03http://$2\04$2\03'); //Contains URLs if(txt.contains('\03')){ var tmp = new Array(); txt = txt.split('\03'); txt.each(function(item){ if(!item.contains('\04')){ tmp.push(item); }else{ var link = item.split('\04'); tmp.push(new Element('a', { 'href': link[0], 'text': link[1] })); } }); return tmp; } return [txt]; }, close: function(reason){ if(this.irc && !this.dontclose){ this.irc.part(this.name, reason); } } });
ssjs/proxy.js
Ape.registerCmd("PROXY_CONNECT", true, function(params, infos) {
	if (!$defined(params.host) || !$defined(params.port)) {
		return 0;
	}
	var allowed = Ape.config('proxy.conf', 'allowed_hosts');
	
	if (allowed != 'any') {
		var hosts = allowed.split(',');
		var isallowed = false;

		for (var i = 0; i < hosts.length; i++) {
			var parts = hosts[i].trim().split(':');
			if (parts[0] == params.host) {
				if (parts.length == 2 && parts[1] != params.port) continue;
				isallowed = true;
				break;
			}
		}
		if (!isallowed) return [900, "NOT_ALLOWED"];
	}
	
	var socket = new Ape.sockClient(params.port, params.host);
	socket.chl = infos.chl;
	/* TODO : Add socket to the user */
	
	socket.onConnect = function() {
		/* "this" refer to socket object */
		/* Create a new pipe (with a pubid) */
		var pipe = new Ape.pipe();
		
		infos.user.proxys.set(pipe.getProperty('pubid'), pipe);
		
		/* Set some private properties */
		pipe.link = socket;
		pipe.nouser = false;
		this.pipe = pipe;
		
		/* Called when an user send a "SEND" command on this pipe */
		pipe.onSend = function(user, params) {
			/* "this" refer to the pipe object */
			this.link.write(Ape.base64.decode(unescape(params.msg)));
		}
		
		pipe.onDettach = function() {
			this.link.close();
		}
		
		/* Send a PROXY_EVENT raw to the user and attach the pipe */
		infos.user.pipe.sendRaw("PROXY_EVENT", {"event": "connect", "chl": this.chl}, {from: this.pipe});
	}
	
	socket.onRead = function(data) {
		infos.user.pipe.sendRaw("PROXY_EVENT", {"event": "read", "data": Ape.base64.encode(data)}, {from: this.pipe});
	}
	
	socket.onDisconnect = function(data) {
		if ($defined(this.pipe)) {
			if (!this.pipe.nouser) { /* User is not available anymore */
				infos.user.pipe.sendRaw("PROXY_EVENT", {"event": "disconnect"}, {from: this.pipe});
				infos.user.proxys.erase(this.pipe.getProperty('pubid'));
			}
			/* Destroy the pipe */
			this.pipe.destroy();
		}
	}
	
	return 1;
});

Ape.addEvent("deluser", function(user) {
	user.proxys.each(function(val) {
		val.nouser = true;
		val.onDettach();
	});
});

Ape.addEvent("adduser", function(user) {
	user.proxys = new $H;
})


irc.css
#ircchat{
    width: 796px;
    height: 482px;
    margin-bottom: 10px;
    position:relative;
    margin: 0 auto;
}
/** Login **/
#ircchat #login{
    width: 800px;
    height: 485px;
    position: absolute;
    top: 0;
    left: 0;
    background: white;
    z-index: 200;
}
#ircchat #login .popup{
    border: solid 3px #235464;
    border-radius: 6px;
    -moz-border-radius:6px;
    -webkit-border-radius: 6px;
    padding: 40px 65px;
    margin: 150px 200px;
    background: #a4c9d6;
    text-align:center;
}
#ircchat #login .popup p{
    font-weight: bold;
    color: #133d4b;
    font-size: 1.25em;
    margin-bottom: 20px;
}
#ircchat #login .popup input{
    width: 125px;
}
#ircchat #login .popup button{
    border: none;
    background: none transparent;
    color: #133d4b;
    cursor: pointer;
}
/** Connected **/
#ircchat .header{
    height: 34px;
}
#ircchat .header .server{
    float: left;
    margin: 9px 12px 0 1px;
}
#ircchat .header .tab{
    width: auto;
    float:left;
    height: 29px;
    padding-right: 3px;
    background: transparent right top no-repeat url('./img/btnOff_right.png');
    margin:0 7px 4px 0;
}
#ircchat .header .tab a{
    display:block;
    height:29px;
    width: 29px;
    background: left top no-repeat url('./img/btnOff_left.png');
    float: left;
    cursor: pointer;
}
#ircchat .header .tab span.link{
    display: block;
    height: 20px;
    background: top repeat-x url('./img/btnOff_middle.png');
    padding: 9px 9px 0 0;
    color: #9a9b9c;
    cursor: pointer;
    float:left;
}
#ircchat .header .tab a:hover{
    background-position:bottom left;
}
#ircchat .header .tab.sys a{
    width: 5px;
    cursor: default;
}
#ircchat .header .tab.sys span.link{
    padding-left: 5px;
}
#ircchat .header .tab span.link:hover, #ircchat .header .tab.current span.link{
    color: #235464;
}
#ircchat .header .tab.current{
    background-image:url('./img/btnOn_right.png');
}
#ircchat .header .tab.current a{
    background-image:url('./img/btnOn_left.png');
    cursor:pointer;
}
#ircchat .header .tab.sys.current a{
    cursor: default;
}
#ircchat .header .tab.current span.link{
    background-image:url('./img/btnOn_middle.png');
    cursor: default;
}
#ircchat .chat{
    width: 796px;
    height: 446px;
    border: solid 2px #839093;
    clear: left;
}
#ircchat .chat .texte-zone{
    height: 398px;
    width: 620px;
    float: left;
    position: relative;
    z-index: 100;
}
#ircchat .chat .texte-zone .texte{
    width: 620px;
    height: 398px;
    overflow-y: scroll;
    overflow-x: hidden;
    z-index: 101;
    font-family: "Lucida Console ", monospace !important;
    font-size: 10px;
}
#ircchat .chat .texte-zone #line{
    width: 1px;
    height: 398px;
    position: absolute;
    top: 0;
    left: 100px;
    border-right: 1px solid #b5bcbc;
    z-index: 102;
}
/** Messages lines */
#ircchat .chat .texte-zone .texte .msg_line{
    clear: both;
}
#ircchat .chat .texte-zone .texte .msg_line pre{
    line-height: 13px !important;
}
#ircchat .chat .texte-zone .texte .msg_line .msg_user{
    float:left;
}
#ircchat .chat .texte-zone .texte .msg_line .msg_text{
    padding-left: 4px;
    overflow: hidden;
    float: left;
    /*For a cool effect
    float: right;
    /**/

}
#ircchat .chat .list{
    width: 175px;
    height: 398px;
    border-left: solid 1px #839095;
    float:left;
}
#ircchat .chat .list .header{
    height: 20px;
    border-bottom: solid 1px #839095;
    background-color: #133d4b;
    text-align:center;
    color: #b9c5c9;
    padding-top: 5px;
}
#ircchat .chat .list .user-list{
    overflow-y: scroll;
    height: 372px;
    background: top left url('./img/list_bg.png');
}
#ircchat .chat .list .user-list #user-list{
    overflow-y: auto;
    background: top left url('./img/list_bg.png');
}
#ircchat .chat .list .user-list #user-list .user-item{
    display: block;
    height: 15px;
    color: #FFF;
    padding: 5px 0 3px 27px;
    background: left top no-repeat;
    overflow: hidden;
}
#ircchat .chat .list .user-list #user-list a:hover{
    background-color: #336474;
}
#ircchat .chat .list .user-list #user-list .user-item.operator{
    background-image:url('./img/green_bullet.png');
}
#ircchat .chat .list .user-list #user-list .user-item.voice{
    background-image:url('./img/orange_bullet.png');
}
#ircchat .chat .input-zone{
    clear: left;
    height: 48px;
    background: repeat-x left top url('./img/send_bg.png');
}
#ircchat .chat .input-zone .show-nick{
    float:left;
    margin: 20px 0 0 8px;
    color: #5d5e5e;
    width: 105px;
    text-align: center;
}
#ircchat .chat .input-zone #chatButton{
    float:right;
    border: none;
    background:none transparent;
    margin: 10px 20px 0 20px;
    padding: 0;
    height: 29px;
    color: #133d4b;
    font-size: 13px;
    cursor: pointer;
}
#ircchat .chat .input-zone #chat-input{
    float:right;
    border: solid 1px #a1a1a1;
    border-top-color: #909090;
    border-bottom-color: #a9a9a9;
    margin-top: 10px;
    padding: 0;
    position: static;
    height: 19px;
    background: top repeat-x url('./img/input_bg.png') white;
    padding: 5px 4px;
    width: 601px;
}

/** Special Messages */
.highlight .msg_text{
    color: red;
}
.info .msg_text{
    color: gray;
}
.topic .msg_text{
    color: #B037B0;
}
.user-left .msg_text{
    color: #F44;
}
.user-join .msg_text{
    color: #080
}
.notice .msg_text{
    font-style: italic;
}
.action .msg_text{
    color: purple;
}
.ctcp .msg_text{
    color: black;
}
.error .msg_text{
    font-style: italic;
    color: #800;
}

/** Special spans */
.msg_text .special{
    font-weight: bold;
}
.msg_text a.user{
}
.msg_user a.user{
    color: inherit;
}
.msg_user a.user:hover{
    color: orange;
}

Embed and share this demo