Merge git://github.com/M2Ys4U/KiwiIRC
[KiwiIRC.git] / node / kiwi.js
1 /*jslint regexp: true, confusion: true, undef: false, node: true, sloppy: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
2
3 var tls = require('tls'),
4 net = require('net'),
5 http = require('http'),
6 fs = require('fs'),
7 ws = require('socket.io'),
8 _ = require('./lib/underscore.min.js'),
9 starttls = require('./lib/starttls.js');
10
11 var config = JSON.parse(fs.readFileSync(__dirname + '/config.json', 'ascii'));
12
13 var ircNumerics = {
14 RPL_WELCOME: '001',
15 RPL_ISUPPORT: '005',
16 RPL_WHOISUSER: '311',
17 RPL_WHOISSERVER: '312',
18 RPL_WHOISOPERATOR: '313',
19 RPL_WHOISIDLE: '317',
20 RPL_ENDOFWHOIS: '318',
21 RPL_WHOISCHANNELS: '319',
22 RPL_TOPIC: '332',
23 RPL_NAMEREPLY: '353',
24 RPL_ENDOFNAMES: '366',
25 RPL_MOTD: '372',
26 RPL_WHOISMODES: '379',
27 ERR_NOSUCHNICK: '401',
28 ERR_LINKCHANNEL: '470',
29 RPL_STARTTLS: '670'
30 };
31
32
33 var parseIRCMessage = function (websocket, ircSocket, data) {
34 /*global ircSocketDataHandler */
35 var msg, regex, opts, options, opt, i, j, matches, nick, users, chan, params, prefix, prefixes, nicklist, caps;
36 regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@([a-z0-9\.\-:]+)) )?([a-z0-9]+)(?:(?: ([^:]+))?(?: :(.+))?)$/i;
37 msg = regex.exec(data);
38 if (msg) {
39 msg = {
40 prefix: msg[1],
41 nick: msg[2],
42 ident: msg[3],
43 hostname: msg[4],
44 command: msg[5],
45 params: msg[6] || '',
46 trailing: (msg[7]) ? msg[7].trim() : ''
47 };
48 switch (msg.command.toUpperCase()) {
49 case 'PING':
50 ircSocket.write('PONG ' + msg.trailing + '\r\n');
51 break;
52 case ircNumerics.RPL_WELCOME:
53 if (ircSocket.IRC.CAP.negotiating) {
54 ircSocket.IRC.CAP.negotiating = false;
55 ircSocket.IRC.CAP.enabled = [];
56 ircSocket.IRC.CAP.requested = [];
57 }
58 websocket.emit('message', {event: 'connect', connected: true, host: null});
59 break;
60 case ircNumerics.RPL_ISUPPORT:
61 opts = msg.params.split(" ");
62 options = [];
63 for (i = 0; i < opts.length; i++) {
64 opt = opts[i].split("=", 2);
65 opt[0] = opt[0].toUpperCase();
66 ircSocket.IRC.options[opt[0]] = opt[1] || true;
67 if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES'], opt[0])) {
68 if (opt[0] === 'PREFIX') {
69 regex = /\(([^)]*)\)(.*)/;
70 matches = regex.exec(opt[1]);
71 if ((matches) && (matches.length === 3)) {
72 ircSocket.IRC.options[opt[0]] = {};
73 for (j = 0; j < matches[2].length; j++) {
74 ircSocket.IRC.options[opt[0]][matches[2].charAt(j)] = matches[1].charAt(j);
75 }
76 }
77 }
78 }
79 }
80 websocket.emit('message', {event: 'options', server: '', "options": ircSocket.IRC.options});
81 break;
82 case ircNumerics.RPL_WHOISUSER:
83 case ircNumerics.RPL_WHOISSERVER:
84 case ircNumerics.RPL_WHOISOPERATOR:
85 case ircNumerics.RPL_WHOISIDLE:
86 case ircNumerics.RPL_ENDOFWHOIS:
87 case ircNumerics.RPL_WHOISCHANNELS:
88 case ircNumerics.RPL_WHOISMODES:
89 websocket.emit('message', {event: 'whois', server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing});
90 break;
91 case ircNumerics.RPL_MOTD:
92 websocket.emit('message', {event: 'motd', server: '', "msg": msg.trailing});
93 break;
94 case ircNumerics.RPL_NAMEREPLY:
95 params = msg.params.split(" ");
96 nick = params[0];
97 chan = params[2];
98 users = msg.trailing.split(" ");
99 prefixes = _.values(ircSocket.IRC.options.PREFIX);
100 nicklist = {};
101 i = 0;
102 _.each(users, function (user) {
103 if (_.include(prefix, user.charAt(0))) {
104 prefix = user.charAt(0);
105 user = user.substring(1);
106 nicklist[user] = prefix;
107 } else {
108 nicklist[user] = '';
109 }
110 if (i++ >= 50) {
111 websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
112 nicklist = {};
113 i = 0;
114 }
115 });
116 if (i > 0) {
117 websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
118 } else {
119 console.log("oops");
120 }
121 break;
122 case ircNumerics.RPL_ENDOFNAMES:
123 websocket.emit('message', {event: 'userlist_end', server: '', channel: msg.params.split(" ")[1]});
124 break;
125 case ircNumerics.ERR_LINKCHANNEL:
126 params = msg.params.split(" ");
127 websocket.emit('message', {event: 'channel_redirect', from: params[1], to: params[2]});
128 break;
129 case ircNumerics.ERR_NOSUCHNICK:
130 //TODO: shit
131 break;
132 case 'JOIN':
133 websocket.emit('message', {event: 'join', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.trailing});
134 if (msg.nick === ircSocket.IRC.nick) {
135 ircSocket.write('NAMES ' + msg.trailing + '\r\n');
136 }
137 break;
138 case 'PART':
139 websocket.emit('message', {event: 'part', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
140 break;
141 case 'KICK':
142 params = msg.params.split(" ");
143 websocket.emit('message', {event: 'kick', kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
144 break;
145 case 'QUIT':
146 websocket.emit('message', {event: 'quit', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
147 break;
148 case 'NOTICE':
149 websocket.emit('message', {event: 'notice', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
150 break;
151 case 'NICK':
152 websocket.emit('message', {event: 'nick', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
153 break;
154 case 'TOPIC':
155 websocket.emit('message', {event: 'topic', nick: msg.nick, channel: msg.params, topic: msg.trailing});
156 break;
157 case ircNumerics.RPL_TOPIC:
158 websocket.emit('message', {event: 'topic', nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing});
159 break;
160 case 'MODE':
161 opts = msg.params.split(" ");
162 params = {event: 'mode', nick: msg.nick};
163 switch (opts.length) {
164 case 1:
165 params.effected_nick = opts[0];
166 params.mode = msg.trailing;
167 break;
168 case 2:
169 params.channel = opts[0];
170 params.mode = opts[1];
171 break;
172 default:
173 params.channel = opts[0];
174 params.mode = opts[1];
175 params.effected_nick = opts[2];
176 break;
177 }
178 websocket.emit('message', params);
179 break;
180 case 'PRIVMSG':
181 websocket.emit('message', {event: 'msg', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
182 break;
183 case 'CAP':
184 caps = config.cap_options;
185 options = msg.trailing.split(" ");
186 switch (_.first(msg.params.split(" "))) {
187 case 'LS':
188 opts = '';
189 _.each(_.intersect(caps, options), function (cap) {
190 if (opts !== '') {
191 opts += " ";
192 }
193 opts += cap;
194 ircSocket.IRC.CAP.requested.push(cap);
195 });
196 if (opts.length > 0) {
197 ircSocket.write('CAP REQ :' + opts + '\r\n');
198 } else {
199 ircSocket.write('CAP END\r\n');
200 }
201 // TLS is special
202 /*if (_.include(options, 'tls')) {
203 ircSocket.write('STARTTLS\r\n');
204 ircSocket.IRC.CAP.requested.push('tls');
205 }*/
206 break;
207 case 'ACK':
208 _.each(options, function (cap) {
209 ircSocket.IRC.CAP.enabled.push(cap);
210 });
211 if (_.last(msg.params.split(" ")) !== '*') {
212 ircSocket.IRC.CAP.requested = [];
213 ircSocket.IRC.CAP.negotiating = false;
214 ircSocket.write('CAP END\r\n');
215 }
216 break;
217 case 'NAK':
218 ircSocket.IRC.CAP.requested = [];
219 ircSocket.IRC.CAP.negotiating = false;
220 ircSocket.write('CAP END\r\n');
221 break;
222 }
223 break;
224 /*case ircNumerics.RPL_STARTTLS:
225 try {
226 IRC = ircSocket.IRC;
227 listeners = ircSocket.listeners('data');
228 ircSocket.removeAllListeners('data');
229 ssl_socket = starttls(ircSocket, {}, function () {
230 ssl_socket.on("data", function (data) {
231 ircSocketDataHandler(data, websocket, ssl_socket);
232 });
233 ircSocket = ssl_socket;
234 ircSocket.IRC = IRC;
235 _.each(listeners, function (listener) {
236 ircSocket.addListener('data', listener);
237 });
238 });
239 //console.log(ircSocket);
240 } catch (e) {
241 console.log(e);
242 }
243 break;*/
244 }
245 } else {
246 console.log("Unknown command.\r\n");
247 }
248 };
249
250 var ircSocketDataHandler = function (data, websocket, ircSocket) {
251 var i;
252 if ((ircSocket.holdLast) && (ircSocket.held !== '')) {
253 data = ircSocket.held + data;
254 ircSocket.holdLast = false;
255 ircSocket.held = '';
256 }
257 if (data.substr(-2) === '\r\n') {
258 ircSocket.holdLast = true;
259 }
260 data = data.split("\r\n");
261 for (i = 0; i < data.length; i++) {
262 if (data[i]) {
263 if ((ircSocket.holdLast) && (i === data.length - 1)) {
264 ircSocket.held = data[i];
265 break;
266 }
267 console.log("->" + data[i]);
268 parseIRCMessage(websocket, ircSocket, data[i]);
269 }
270 }
271 };
272
273 //setup websocket listener
274 if (config.listen_ssl) {
275 var io = ws.listen(config.port, {secure: true, key: fs.readFileSync(__dirname + '/' + config.ssl_key), cert: fs.readFileSync(__dirname + '/' + config.ssl_cert)});
276 } else {
277 var io = ws.listen(config.port, {secure: false});
278 }
279 io.sockets.on('connection', function (websocket) {
280 websocket.on('irc connect', function (nick, host, port, ssl, callback) {
281 var ircSocket;
282 //setup IRC connection
283 if (!ssl) {
284 ircSocket = net.createConnection(port, host);
285 } else {
286 ircSocket = tls.connect(port, host);
287 }
288 ircSocket.setEncoding('ascii');
289 ircSocket.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}};
290 websocket.ircSocket = ircSocket;
291 ircSocket.holdLast = false;
292 ircSocket.held = '';
293
294 ircSocket.on('data', function (data) {
295 ircSocketDataHandler(data, websocket, ircSocket);
296 });
297
298 ircSocket.IRC.nick = nick;
299 // Send the login data
300 ircSocket.write('CAP LS\r\n');
301 ircSocket.write('NICK ' + nick + '\r\n');
302 ircSocket.write('USER ' + nick + '_kiwi 0 0 :' + nick + '\r\n');
303
304 if ((callback) && (typeof (callback) === 'function')) {
305 callback();
306 }
307 });
308 websocket.on('message', function (msg, callback) {
309 var args;
310 try {
311 msg.data = JSON.parse(msg.data);
312 args = msg.data.args;
313 switch (msg.data.method) {
314 case 'msg':
315 if ((args.target) && (args.msg)) {
316 websocket.ircSocket.write('PRIVMSG ' + args.target + ' :' + args.msg + '\r\n');
317 }
318 break;
319 case 'action':
320 if ((args.target) && (args.msg)) {
321 websocket.ircSocket.write('PRIVMSG ' + args.target + ' :\ 1ACTION ' + args.msg + '\ 1\r\n');
322 }
323 break;
324 case 'raw':
325 websocket.ircSocket.write(args.data + '\r\n');
326 break;
327 case 'join':
328 if (args.channel) {
329 _.each(args.channel.split(","), function (chan) {
330 websocket.ircSocket.write('JOIN ' + chan + '\r\n');
331 });
332 }
333 break;
334 case 'quit':
335 websocket.ircSocket.end('QUIT :' + args.message + '\r\n');
336 websocket.sentQUIT = true;
337 websocket.ircSocket.destroySoon();
338 websocket.disconnect();
339 break;
340 default:
341 }
342 if ((callback) && (typeof (callback) === 'function')) {
343 callback();
344 }
345 } catch (e) {
346 console.log("Caught error: " + e);
347 }
348 });
349 websocket.on('disconnect', function () {
350 if ((!websocket.sentQUIT) && (websocket.ircSocket)) {
351 websocket.ircSocket.end('QUIT :' + config.quit_message + '\r\n');
352 websocket.sentQUIT = true;
353 websocket.ircSocket.destroySoon();
354 }
355 });
356 });
357