1/*
2 * IRCConnection is a simple implementation of the IRC protocol. A small
3 * subset of the IRC commands are implemented. To be functional, IRCConnection
4 * needs some mechanism of transport to be hooked up by:
5 * -Passing in |sendFunc| and |closeFunc| which an IRCConnection to use to send
6 *  an IRC message command and to close the connection respectively.
7 * -Connecting the in-bound functions |onOpened|, |onMessage|, and |onClosed|,
8 *  to the transport so that the IRCConnection can respond to the connection
9 *  being opened, a message being received and the connection being closed.
10 */
11
12function NoOp() {};
13function log(message) { console.log(message); };
14
15function IRCConnection(server, port, nick, sendFunc, closeFunc) {
16  this.server = server;
17  this.port = port;
18  this.nick = nick;
19  this.connected = false;
20
21  var that = this;
22
23  /**
24   * Client API
25   */
26  this.onConnect = NoOp;
27  this.onDisconnect = NoOp;
28  this.onText = NoOp;
29  this.onNotice = NoOp;
30  this.onNickReferenced = NoOp;
31
32  this.joinChannel = function(channel) {
33    sendCommand(commands.JOIN, [channel], "");
34  };
35
36  this.sendMessage = function(recipient, message) {
37    sendCommand(commands.PRIVMSG, [recipient], message);
38  };
39
40  this.quitChannel = function(channel) {
41    sendCommand(commands.PART, [channel], "");
42  }
43
44  this.disconnect = function(message) {
45    sendCommand(commands.QUIT, [], message);
46    closeFunc();
47  }
48
49  /**
50   * Transport Interface
51   * Whatever transport is used must provide and connect to the following
52   * in-bound events.
53   */
54  this.onOpened = function() {
55    sendFunc(that.server + ":" + that.port);
56    sendCommand(commands.NICK, [this.nick], "");
57    sendCommand(commands.USER,
58                ["chromium-irc-lib", "chromium-ircproxy", "*"],
59                "indigo");
60  };
61
62  this.onMessage = function(message) {
63    log("<< " + message);
64    if (!message || !message.length) {
65      return;
66    }
67
68    var parsed = parseMessage(message);
69
70    // Respond to PING command.
71    if (parsed.command == commands.PING) {
72      sendCommand(commands.PONG, [], parsed.body);
73      return;
74    }
75
76    // Process PRIVMSG.
77    if (parsed.command == commands.PRIVMSG) {
78      if (parsed.body.charCodeAt(0) == 1) {
79        // Ignore CTCP.
80        return;
81      }
82      that.onText(parsed.parameters[0],
83                  parsed.prefix.split("!")[0],
84                  parsed.body);
85      return;
86    }
87
88    // TODO: Other IRC commands.
89    var commandCode = parseInt(parsed.command);
90    if (commandCode == NaN) {
91      return;
92    }
93
94    switch(commandCode) {
95      case 001:  // Server welcome message.
96        that.connected = true;
97        that.onConnect(parsed.body);
98        break;
99      case 002:
100      case 003:
101      case 004:
102      case 005:
103        if (!that.connected) {
104          that.connected = true;
105          that.onConnect();
106        }
107        break;
108      case 433:  // TODO(rafaelw): Nickname in use.
109        throw "NOT IMPLEMENTED";
110        break;
111      default:
112        break;
113    }
114  }
115
116  this.onClosed = function() {
117    that.connected = false;
118    that.onDisconnect();
119  };
120
121  /**
122   * IRC Implementation
123   * What follows in a minimal implementation of the IRC protocol.
124   * Only |commands| are currently implemented.
125   */
126  var commands = {
127    JOIN: "JOIN",
128    NICK: "NICK",
129    NOTICE: "NOTICE",
130    PART: "PART",
131    PING: "PING",
132    PONG: "PONG",
133    PRIVMSG: "PRIVMSG",
134    QUIT: "QUIT",
135    USER: "USER"
136  };
137
138  function parseMessage(message) {
139    var parsed = {};
140    parsed.prefix = "";
141    parsed.command = "";
142    parsed.parameters = [];
143    parsed.body = "";
144
145    // Trim trailing CRLF.
146    var crlfIndex = message.indexOf("\r\n");
147    if(crlfIndex >= 0) {
148      message = message.substring(0, crlfIndex);
149    }
150
151    // If leading character is ':', the message starts with a prefix.
152    if (message.indexOf(':') == 0) {
153      parsed.prefix = message.substring(1, message.indexOf(" "));
154      message = message.substring(parsed.prefix.length + 2);
155
156      // Forward past extra whitespace.
157      while(message.indexOf(" ") == 0) {
158        message = message.substring(1);
159      }
160    }
161
162    // If there is still a ':', then the message has trailing body.
163    var bodyMarker = message.indexOf(':');
164    if (bodyMarker >= 0) {
165      parsed.body = message.substring(bodyMarker + 1);
166      message = message.substring(0, bodyMarker);
167    }
168
169    parsed.parameters = message.split(" ");
170    parsed.command = parsed.parameters.shift();  // First param is the command.
171
172    return parsed;
173  }
174
175  function sendCommand(command, params, message) {
176    var line = command;
177    if (params && params.length > 0) {
178      line += " " + params.join(" ");
179    }
180    if (message && message.length > 0) {
181      line += " :"  + message;
182    }
183
184    log(">> " + line);
185    line += "\r\n";
186    sendFunc(line);
187  };
188};
189