b403dedf1bba6af87b1d15137bb574f92ba0cc6b
[KiwiIRC.git] / server / irc / commands.js
1 var _ = require('lodash'),
2 irc_numerics,
3 IrcCommands,
4 handlers,
5 unknownCommand;
6
7 irc_numerics = {
8 '001': 'RPL_WELCOME',
9 '004': 'RPL_MYINFO',
10 '005': 'RPL_ISUPPORT',
11 '006': 'RPL_MAPMORE',
12 '007': 'RPL_MAPEND',
13 '250': 'RPL_STATSCONN',
14 '251': 'RPL_LUSERCLIENT',
15 '252': 'RPL_LUSEROP',
16 '253': 'RPL_LUSERUNKNOWN',
17 '254': 'RPL_LUSERCHANNELS',
18 '255': 'RPL_LUSERME',
19 '265': 'RPL_LOCALUSERS',
20 '266': 'RPL_GLOBALUSERS',
21 '301': 'RPL_AWAY',
22 '307': 'RPL_WHOISREGNICK',
23 '310': 'RPL_WHOIHELPOP',
24 '311': 'RPL_WHOISUSER',
25 '312': 'RPL_WHOISSERVER',
26 '313': 'RPL_WHOISOPERATOR',
27 '314': 'RPL_WHOWASUSER',
28 '315': 'RPL_ENDOFWHO',
29 '317': 'RPL_WHOISIDLE',
30 '318': 'RPL_ENDOFWHOIS',
31 '319': 'RPL_WHOISCHANNELS',
32 '321': 'RPL_LISTSTART',
33 '322': 'RPL_LIST',
34 '323': 'RPL_LISTEND',
35 '324': 'RPL_CHANNELMODEIS',
36 '328': 'RPL_CHANNEL_URL',
37 '329': 'RPL_CREATIONTIME',
38 '330': 'RPL_WHOISACCOUNT',
39 '331': 'RPL_NOTOPIC',
40 '332': 'RPL_TOPIC',
41 '333': 'RPL_TOPICWHOTIME',
42 '335': 'RPL_WHOISBOT',
43 '341': 'RPL_INVITING',
44 '352': 'RPL_WHOREPLY',
45 '353': 'RPL_NAMEREPLY',
46 '364': 'RPL_LINKS',
47 '365': 'RPL_ENDOFLINKS',
48 '366': 'RPL_ENDOFNAMES',
49 '367': 'RPL_BANLIST',
50 '368': 'RPL_ENDOFBANLIST',
51 '369': 'RPL_ENDOFWHOWAS',
52 '372': 'RPL_MOTD',
53 '375': 'RPL_MOTDSTART',
54 '376': 'RPL_ENDOFMOTD',
55 '378': 'RPL_WHOISHOST',
56 '379': 'RPL_WHOISMODES',
57 '396': 'RPL_HOSTCLOACKING',
58 '401': 'ERR_NOSUCHNICK',
59 '404': 'ERR_CANNOTSENDTOCHAN',
60 '405': 'ERR_TOOMANYCHANNELS',
61 '406': 'ERR_WASNOSUCHNICK',
62 '421': 'ERR_UNKNOWNCOMMAND',
63 '422': 'ERR_NOMOTD',
64 '432': 'ERR_ERRONEUSNICKNAME',
65 '433': 'ERR_NICKNAMEINUSE',
66 '441': 'ERR_USERNOTINCHANNEL',
67 '442': 'ERR_NOTONCHANNEL',
68 '443': 'ERR_USERONCHANNEL',
69 '451': 'ERR_NOTREGISTERED',
70 '464': 'ERR_PASSWDMISMATCH',
71 '470': 'ERR_LINKCHANNEL',
72 '471': 'ERR_CHANNELISFULL',
73 '473': 'ERR_INVITEONLYCHAN',
74 '474': 'ERR_BANNEDFROMCHAN',
75 '475': 'ERR_BADCHANNELKEY',
76 '481': 'ERR_NOPRIVILEGES',
77 '482': 'ERR_CHANOPRIVSNEEDED',
78 '670': 'RPL_STARTTLS',
79 '671': 'RPL_WHOISSECURE',
80 '900': 'RPL_SASLAUTHENTICATED',
81 '903': 'RPL_SASLLOGGEDIN',
82 '904': 'ERR_SASLNOTAUTHORISED',
83 '906': 'ERR_SASLABORTED',
84 '907': 'ERR_SASLALREADYAUTHED'
85 };
86
87
88 IrcCommands = function (irc_connection) {
89 this.irc_connection = irc_connection;
90 };
91 module.exports = IrcCommands;
92
93 IrcCommands.prototype.dispatch = function (command, data) {
94 command += '';
95 if (irc_numerics[command]) {
96 command = irc_numerics[command];
97 }
98 if (handlers[command]) {
99 handlers[command].call(this, data);
100 } else {
101 unknownCommand.call(this, command, data);
102 }
103 };
104
105 IrcCommands.addHandler = function (command, handler) {
106 if (typeof handler !== 'function') {
107 return false;
108 }
109 handlers[command] = handler;
110 };
111
112 IrcCommands.addNumeric = function (numeric, handler_name) {
113 irc_numerics[numeric + ''] = handler_name +'';
114 };
115
116 unknownCommand = function (command, data) {
117 var params = _.clone(data.params);
118
119 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' unknown_command', {
120 command: command,
121 params: params
122 });
123 };
124
125
126 handlers = {
127 'RPL_WELCOME': function (command) {
128 var nick = command.params[0];
129 this.irc_connection.registered = true;
130 this.cap_negotiation = false;
131 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' connect', {
132 nick: nick
133 });
134 },
135
136 'RPL_ISUPPORT': function (command) {
137 var options, i, option, matches, j;
138 options = command.params;
139 for (i = 1; i < options.length; i++) {
140 option = options[i].split("=", 2);
141 option[0] = option[0].toUpperCase();
142 this.irc_connection.options[option[0]] = (typeof option[1] !== 'undefined') ? option[1] : true;
143 if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES', 'CHANMODES', 'NAMESX'], option[0])) {
144 if (option[0] === 'PREFIX') {
145 matches = /\(([^)]*)\)(.*)/.exec(option[1]);
146 if ((matches) && (matches.length === 3)) {
147 this.irc_connection.options.PREFIX = [];
148 for (j = 0; j < matches[2].length; j++) {
149 this.irc_connection.options.PREFIX.push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
150 }
151 }
152 } else if (option[0] === 'CHANTYPES') {
153 this.irc_connection.options.CHANTYPES = this.irc_connection.options.CHANTYPES.split('');
154 } else if (option[0] === 'CHANMODES') {
155 this.irc_connection.options.CHANMODES = option[1].split(',');
156 } else if ((option[0] === 'NAMESX') && (!_.contains(this.irc_connection.cap.enabled, 'multi-prefix'))) {
157 this.irc_connection.write('PROTOCTL NAMESX');
158 }
159 }
160 }
161 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' options', {
162 options: this.irc_connection.options,
163 cap: this.irc_connection.cap.enabled
164 });
165 },
166
167 'RPL_ENDOFWHOIS': function (command) {
168 this.irc_connection.emit('user ' + command.params[1] + ' endofwhois', {
169 nick: command.params[1],
170 msg: command.params[command.params.length - 1]
171 });
172 },
173
174 'RPL_AWAY': function (command) {
175 this.irc_connection.emit('user ' + command.params[1] + ' whoisaway', {
176 nick: command.params[1],
177 reason: command.params[command.params.length - 1]
178 });
179 },
180
181 'RPL_WHOISUSER': function (command) {
182 this.irc_connection.emit('user ' + command.params[1] + ' whoisuser', {
183 nick: command.params[1],
184 ident: command.params[2],
185 host: command.params[3],
186 msg: command.params[command.params.length - 1]
187 });
188 },
189
190 'RPL_WHOISHELPOP': function (command) {
191 this.irc_connection.emit('user ' + command.params[1] + ' whoishelpop', {
192 nick: command.params[1],
193 msg: command.params[command.params.length - 1]
194 });
195 },
196
197 'RPL_WHOISBOT': function (command) {
198 this.irc_connection.emit('user ' + command.params[1] + ' whoisbot', {
199 nick: command.params[1],
200 msg: command.params[command.params.length - 1]
201 });
202 },
203
204 'RPL_WHOISSERVER': function (command) {
205 this.irc_connection.emit('user ' + command.params[1] + ' whoisserver', {
206 nick: command.params[1],
207 irc_server: command.params[2],
208 server_info: command.params[command.params.length - 1]
209 });
210 },
211
212 'RPL_WHOISOPERATOR': function (command) {
213 this.irc_connection.emit('user ' + command.params[1] + ' whoisoperator', {
214 nick: command.params[1],
215 msg: command.params[command.params.length - 1]
216 });
217 },
218
219 'RPL_WHOISCHANNELS': function (command) {
220 this.irc_connection.emit('user ' + command.params[1] + ' whoischannels', {
221 nick: command.params[1],
222 chans: command.params[command.params.length - 1]
223 });
224 },
225
226 'RPL_WHOISMODES': function (command) {
227 this.irc_connection.emit('user ' + command.params[1] + ' whoismodes', {
228 nick: command.params[1],
229 msg: command.params[command.params.length - 1]
230 });
231 },
232
233 'RPL_WHOISIDLE': function (command) {
234 this.irc_connection.emit('user ' + command.params[1] + ' whoisidle', {
235 nick: command.params[1],
236 idle: command.params[2],
237 logon: command.params[3] || undefined
238 });
239 },
240
241 'RPL_WHOISREGNICK': function (command) {
242 this.irc_connection.emit('user ' + command.params[1] + ' whoisregnick', {
243 nick: command.params[1],
244 msg: command.params[command.params.length - 1]
245 });
246 },
247
248 'RPL_WHOISHOST': function (command) {
249 this.irc_connection.emit('user ' + command.params[1] + ' whoishost', {
250 nick: command.params[1],
251 msg: command.params[command.params.length - 1]
252 });
253 },
254
255 'RPL_WHOISSECURE': function (command) {
256 this.irc_connection.emit('user ' + command.params[1] + ' whoissecure', {
257 nick: command.params[1]
258 });
259 },
260
261 'RPL_WHOISACCOUNT': function (command) {
262 this.irc_connection.emit('user ' + command.params[1] + ' whoisaccount', {
263 nick: command.params[1],
264 account: command.params[2]
265 });
266 },
267
268 'RPL_WHOWASUSER': function (command) {
269 this.irc_connection.emit('user ' + command.params[1] + ' whowas', {
270 nick: command.params[1],
271 ident: command.params[2],
272 host: command.params[3],
273 real_name: command.params[command.params.length - 1]
274 });
275 },
276
277 'RPL_ENDOFWHOWAS': function (command) {
278 this.irc_connection.emit('user ' + command.params[1] + ' endofwhowas', {
279 nick: command.params[1]
280 });
281 },
282
283 'ERR_WASNOSUCHNICK': function (command) {
284 this.irc_connection.emit('user ' + command.params[1] + ' wasnosucknick', {
285 nick: command.params[1]
286 });
287 },
288
289 'RPL_LISTSTART': function (command) {
290 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' list_start', {});
291 },
292
293 'RPL_LISTEND': function (command) {
294 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' list_end', {});
295 },
296
297 'RPL_LIST': function (command) {
298 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' list_channel', {
299 channel: command.params[1],
300 num_users: parseInt(command.params[2], 10),
301 topic: command.params[3] || ''
302 });
303 },
304
305 'RPL_CHANNELMODEIS': function (command) {
306 var channel = command.params[1],
307 modes = parseModeList.call(this, command.params[2], command.params.slice(3));
308
309 this.irc_connection.emit('channel ' + channel + ' info', {
310 channel: channel,
311 modes: modes
312 });
313 },
314
315 'RPL_CREATIONTIME': function (command) {
316 var channel = command.params[1];
317
318 this.irc_connection.emit('channel ' + channel + ' info', {
319 channel: channel,
320 created_at: parseInt(command.params[2], 10)
321 });
322 },
323
324 'RPL_CHANNEL_URL': function (command) {
325 var channel = command.params[1];
326
327 this.irc_connection.emit('channel ' + channel + ' info', {
328 channel: channel,
329 url: command.params[command.params.length - 1]
330 });
331 },
332
333 'RPL_MOTD': function (command) {
334 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' motd', {
335 motd: command.params[command.params.length - 1] + '\n'
336 });
337 },
338
339 'RPL_MOTDSTART': function (command) {
340 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' motd_start', {});
341 },
342
343 'RPL_ENDOFMOTD': function (command) {
344 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' motd_end', {});
345 },
346
347 'RPL_NAMEREPLY': function (command) {
348 var members = command.params[command.params.length - 1].split(' ');
349 var member_list = [];
350 var that = this;
351 _.each(members, function (member) {
352 var i = 0,
353 j = 0,
354 modes = [];
355
356 // Make sure we have some prefixes already
357 if (that.irc_connection.options.PREFIX) {
358 for (j = 0; j < that.irc_connection.options.PREFIX.length; j++) {
359 if (member.charAt(i) === that.irc_connection.options.PREFIX[j].symbol) {
360 modes.push(that.irc_connection.options.PREFIX[j].mode);
361 i++;
362 }
363 }
364 }
365
366 member_list.push({nick: member, modes: modes});
367 });
368
369 this.irc_connection.emit('channel ' + command.params[2] + ' userlist', {
370 users: member_list,
371 channel: command.params[2]
372 });
373 },
374
375 'RPL_ENDOFNAMES': function (command) {
376 this.irc_connection.emit('channel ' + command.params[1] + ' userlist_end', {
377 channel: command.params[1]
378 });
379 },
380
381 'RPL_WHOREPLY': function (command) {
382 // For the time being, NOOP this command so they don't get passed
383 // down to the client. Waste of bandwidth since we do not use it yet
384 // TODO: Impliment RPL_WHOREPLY
385 },
386
387 'RPL_ENDOFWHO': function (command) {
388 // For the time being, NOOP this command so they don't get passed
389 // down to the client. Waste of bandwidth since we do not use it yet
390 // TODO: Impliment RPL_ENDOFWHO
391 },
392
393 'RPL_BANLIST': function (command) {
394 this.irc_connection.emit('channel ' + command.params[1] + ' banlist', {
395 channel: command.params[1],
396 banned: command.params[2],
397 banned_by: command.params[3],
398 banned_at: command.params[4]
399 });
400 },
401
402 'RPL_ENDOFBANLIST': function (command) {
403 this.irc_connection.emit('channel ' + command.params[1] + ' banlist_end', {
404 channel: command.params[1]
405 });
406 },
407
408 'RPL_TOPIC': function (command) {
409 this.irc_connection.emit('channel ' + command.params[1] + ' topic', {
410 channel: command.params[1],
411 topic: command.params[command.params.length - 1]
412 });
413 },
414
415 'RPL_NOTOPIC': function (command) {
416 this.irc_connection.emit('channel ' + command.params[1] + ' topic', {
417 channel: command.params[1],
418 topic: ''
419 });
420 },
421
422 'RPL_TOPICWHOTIME': function (command) {
423 this.irc_connection.emit('channel ' + command.params[1] + ' topicsetby', {
424 nick: command.params[2],
425 channel: command.params[1],
426 when: command.params[3]
427 });
428 },
429
430 'RPL_INVITING': function (command) {
431 this.irc_connection.emit('channel ' + command.params[1] + ' invited', {
432 nick: command.params[0],
433 channel: command.params[1]
434 });
435 },
436
437 'PING': function (command) {
438 this.irc_connection.write('PONG ' + command.params[command.params.length - 1]);
439 },
440
441 'JOIN': function (command) {
442 var channel, time;
443 if (typeof command.params[0] === 'string' && command.params[0] !== '') {
444 channel = command.params[0];
445 }
446
447 // Check if we have a server-time
448 time = getServerTime.call(this, command);
449
450 this.irc_connection.emit('channel ' + channel + ' join', {
451 nick: command.nick,
452 ident: command.ident,
453 hostname: command.hostname,
454 channel: channel,
455 time: time
456 });
457 },
458
459 'PART': function (command) {
460 var time, channel, message;
461
462 // Check if we have a server-time
463 time = getServerTime.call(this, command);
464
465 channel = command.params[0];
466 if (command.params.length > 1) {
467 message = command.params[command.params.length - 1];
468 }
469
470 this.irc_connection.emit('channel ' + channel + ' part', {
471 nick: command.nick,
472 ident: command.ident,
473 hostname: command.hostname,
474 channel: channel,
475 message: message,
476 time: time
477 });
478 },
479
480 'KICK': function (command) {
481 var time;
482
483 // Check if we have a server-time
484 time = getServerTime.call(this, command);
485
486 this.irc_connection.emit('channel ' + command.params[0] + ' kick', {
487 kicked: command.params[1],
488 nick: command.nick,
489 ident: command.ident,
490 hostname: command.hostname,
491 channel: command.params[0],
492 message: command.params[command.params.length - 1],
493 time: time
494 });
495 },
496
497 'QUIT': function (command) {
498 var time;
499
500 // Check if we have a server-time
501 time = getServerTime.call(this, command);
502
503 this.irc_connection.emit('user ' + command.nick + ' quit', {
504 nick: command.nick,
505 ident: command.ident,
506 hostname: command.hostname,
507 message: command.params[command.params.length - 1],
508 time: time
509 });
510 },
511
512 'NOTICE': function (command) {
513 var namespace,
514 time,
515 msg;
516
517 // Check if we have a server-time
518 time = getServerTime.call(this, command);
519
520 msg = command.params[command.params.length - 1];
521 if ((msg.charAt(0) === String.fromCharCode(1)) && (msg.charAt(msg.length - 1) === String.fromCharCode(1))) {
522 // It's a CTCP response
523 namespace = (command.params[0].toLowerCase() === this.irc_connection.nick.toLowerCase()) ? 'user' : 'channel';
524 this.irc_connection.emit(namespace + ' ' + command.params[0] + ' ctcp_response', {
525 nick: command.nick,
526 ident: command.ident,
527 hostname: command.hostname,
528 channel: command.params[0],
529 msg: msg.substring(1, msg.length - 1),
530 time: time
531 });
532 } else {
533 namespace = (command.params[0].toLowerCase() === this.irc_connection.nick.toLowerCase() || command.params[0] === '*') ?
534 'user' :
535 'channel';
536
537 this.irc_connection.emit(namespace + ' ' + command.params[0] + ' notice', {
538 from_server: command.prefix ? true : false,
539 nick: command.nick || command.prefix || undefined,
540 ident: command.ident,
541 hostname: command.hostname,
542 target: command.params[0],
543 msg: msg,
544 time: time
545 });
546 }
547 },
548
549 'NICK': function (command) {
550 var time;
551
552 // Check if we have a server-time
553 time = getServerTime.call(this, command);
554
555 this.irc_connection.emit('user ' + command.nick + ' nick', {
556 nick: command.nick,
557 ident: command.ident,
558 hostname: command.hostname,
559 newnick: command.params[0],
560 time: time
561 });
562 },
563
564 'TOPIC': function (command) {
565 var time;
566
567 // If we don't have an associated channel, no need to continue
568 if (!command.params[0]) {
569 return;
570 }
571
572 // Check if we have a server-time
573 time = getServerTime.call(this, command);
574
575 var channel = command.params[0],
576 topic = command.params[command.params.length - 1] || '';
577
578 this.irc_connection.emit('channel ' + channel + ' topic', {
579 nick: command.nick,
580 channel: channel,
581 topic: topic,
582 time: time
583 });
584 },
585
586 'MODE': function (command) {
587 var modes = [], event, time;
588
589 // Check if we have a server-time
590 time = getServerTime.call(this, command);
591
592 // Get a JSON representation of the modes
593 modes = parseModeList.call(this, command.params[1], command.params.slice(2));
594 event = (_.contains(this.irc_connection.options.CHANTYPES, command.params[0][0]) ? 'channel ' : 'user ') + command.params[0] + ' mode';
595
596 this.irc_connection.emit(event, {
597 target: command.params[0],
598 nick: command.nick || command.prefix || '',
599 modes: modes,
600 time: time
601 });
602 },
603
604 'PRIVMSG': function (command) {
605 var tmp, namespace, time, msg, version_string, client_info;
606
607 // Check if we have a server-time
608 time = getServerTime.call(this, command);
609
610 msg = command.params[command.params.length - 1];
611 if ((msg.charAt(0) === String.fromCharCode(1)) && (msg.charAt(msg.length - 1) === String.fromCharCode(1))) {
612 //CTCP request
613 if (msg.substr(1, 6) === 'ACTION') {
614 this.irc_connection.clientEvent('action', {
615 nick: command.nick,
616 ident: command.ident,
617 hostname: command.hostname,
618 channel: command.params[0],
619 msg: msg.substring(8, msg.length - 1),
620 time: time
621 });
622 } else if (msg.substr(1, 4) === 'KIWI') {
623 tmp = msg.substring(6, msg.length - 1);
624 namespace = tmp.split(' ', 1)[0];
625 this.irc_connection.clientEvent('kiwi', {
626 namespace: namespace,
627 data: tmp.substr(namespace.length + 1),
628 time: time
629 });
630 } else if (msg.substr(1, 7) === 'VERSION') {
631 client_info = this.irc_connection.state.client.client_info;
632 version_string = global.build_version;
633
634 // If the client build_version differs from the server, add this to the version_string
635 if (client_info && client_info.build_version !== global.build_version) {
636 version_string += ', client build: ' + client_info.build_version;
637 }
638
639 version_string = 'KiwiIRC (' + version_string + ')';
640
641 this.irc_connection.write('NOTICE ' + command.nick + ' :' + String.fromCharCode(1) + 'VERSION ' + version_string + String.fromCharCode(1));
642 } else if (msg.substr(1, 6) === 'SOURCE') {
643 this.irc_connection.write('NOTICE ' + command.nick + ' :' + String.fromCharCode(1) + 'SOURCE http://www.kiwiirc.com/' + String.fromCharCode(1));
644 } else if (msg.substr(1, 10) === 'CLIENTINFO') {
645 this.irc_connection.write('NOTICE ' + command.nick + ' :' + String.fromCharCode(1) + 'CLIENTINFO SOURCE VERSION TIME' + String.fromCharCode(1));
646 } else {
647 namespace = (command.params[0].toLowerCase() === this.irc_connection.nick.toLowerCase()) ? 'user' : 'channel';
648 this.irc_connection.emit(namespace + ' ' + command.nick + ' ctcp_request', {
649 nick: command.nick,
650 ident: command.ident,
651 hostname: command.hostname,
652 target: command.params[0],
653 type: (msg.substring(1, msg.length - 1).split(' ') || [null])[0],
654 msg: msg.substring(1, msg.length - 1),
655 time: time
656 });
657 }
658 } else {
659 // A message to a user (private message) or to a channel?
660 namespace = (command.params[0].toLowerCase() === this.irc_connection.nick.toLowerCase()) ? 'user ' + command.nick : 'channel ' + command.params[0];
661 this.irc_connection.emit(namespace + ' privmsg', {
662 nick: command.nick,
663 ident: command.ident,
664 hostname: command.hostname,
665 channel: command.params[0],
666 msg: msg,
667 time: time
668 });
669 }
670 },
671
672 'CAP': function (command) {
673 // TODO: capability modifiers
674 // i.e. - for disable, ~ for requires ACK, = for sticky
675 var capabilities = command.params[command.params.length - 1].replace(/(?:^| )[\-~=]/, '').split(' ');
676 var request;
677
678 // Which capabilities we want to enable
679 var want = ['multi-prefix', 'away-notify', 'server-time', 'znc.in/server-time-iso', 'znc.in/server-time'];
680
681 if (this.irc_connection.password) {
682 want.push('sasl');
683 }
684
685 switch (command.params[1]) {
686 case 'LS':
687 // Compute which of the available capabilities we want and request them
688 request = _.intersection(capabilities, want);
689 if (request.length > 0) {
690 this.irc_connection.cap.requested = request;
691 this.irc_connection.write('CAP REQ :' + request.join(' '));
692 } else {
693 this.irc_connection.write('CAP END');
694 this.irc_connection.cap_negotiation = false;
695 }
696 break;
697 case 'ACK':
698 if (capabilities.length > 0) {
699 // Update list of enabled capabilities
700 this.irc_connection.cap.enabled = capabilities;
701 // Update list of capabilities we would like to have but that aren't enabled
702 this.irc_connection.cap.requested = _.difference(this.irc_connection.cap.requested, capabilities);
703 }
704 if (this.irc_connection.cap.enabled.length > 0) {
705 if (_.contains(this.irc_connection.cap.enabled, 'sasl')) {
706 this.irc_connection.sasl = true;
707 this.irc_connection.write('AUTHENTICATE PLAIN');
708 } else {
709 this.irc_connection.write('CAP END');
710 this.irc_connection.cap_negotiation = false;
711 }
712 }
713 break;
714 case 'NAK':
715 if (capabilities.length > 0) {
716 this.irc_connection.cap.requested = _.difference(this.irc_connection.cap.requested, capabilities);
717 }
718 if (this.irc_connection.cap.requested.length > 0) {
719 this.irc_connection.write('CAP END');
720 this.irc_connection.cap_negotiation = false;
721 }
722 break;
723 case 'LIST':
724 // should we do anything here?
725 break;
726 }
727 },
728
729 'AUTHENTICATE': function (command) {
730 var b = new Buffer(this.irc_connection.nick + "\0" + this.irc_connection.nick + "\0" + this.irc_connection.password, 'utf8');
731 var b64 = b.toString('base64');
732 if (command.params[0] === '+') {
733 while (b64.length >= 400) {
734 this.irc_connection.write('AUTHENTICATE ' + b64.slice(0, 399));
735 b64 = b64.slice(399);
736 }
737 if (b64.length > 0) {
738 this.irc_connection.write('AUTHENTICATE ' + b64);
739 } else {
740 this.irc_connection.write('AUTHENTICATE +');
741 }
742 } else {
743 this.irc_connection.write('CAP END');
744 this.irc_connection.cap_negotiation = false;
745 }
746 },
747
748 'AWAY': function (command) {
749 var time;
750
751 // Check if we have a server-time
752 time = getServerTime.call(this, command);
753
754 this.irc_connection.emit('user ' + command.nick + ' away', {
755 nick: command.nick,
756 msg: command.params[command.params.length - 1],
757 time: time
758 });
759 },
760
761 'RPL_SASLAUTHENTICATED': function (command) {
762 this.irc_connection.write('CAP END');
763 this.irc_connection.cap_negotiation = false;
764 this.irc_connection.sasl = true;
765 },
766
767 'RPL_SASLLOGGEDIN': function (command) {
768 if (this.irc_connection.cap_negotiation === true) {
769 this.irc_connection.write('CAP END');
770 this.irc_connection.cap_negotiation = false;
771 }
772 },
773
774 'ERR_SASLNOTAUTHORISED': function (command) {
775 this.irc_connection.write('CAP END');
776 this.irc_connection.cap_negotiation = false;
777 },
778
779 'ERR_SASLABORTED': function (command) {
780 this.irc_connection.write('CAP END');
781 this.irc_connection.cap_negotiation = false;
782 },
783
784 'ERR_SASLALREADYAUTHED': function (command) {
785 // noop
786 },
787
788 'ERROR': function (command) {
789 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' error', {
790 reason: command.params[command.params.length - 1]
791 });
792 },
793 ERR_PASSWDMISMATCH: function (command) {
794 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' password_mismatch', {});
795 },
796
797 ERR_LINKCHANNEL: function (command) {
798 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' channel_redirect', {
799 from: command.params[1],
800 to: command.params[2]
801 });
802 },
803
804 ERR_NOSUCHNICK: function (command) {
805 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' no_such_nick', {
806 nick: command.params[1],
807 reason: command.params[command.params.length - 1]
808 });
809 },
810
811 ERR_CANNOTSENDTOCHAN: function (command) {
812 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' cannot_send_to_channel', {
813 channel: command.params[1],
814 reason: command.params[command.params.length - 1]
815 });
816 },
817
818 ERR_TOOMANYCHANNELS: function (command) {
819 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' too_many_channels', {
820 channel: command.params[1],
821 reason: command.params[command.params.length - 1]
822 });
823 },
824
825 ERR_USERNOTINCHANNEL: function (command) {
826 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' user_not_in_channel', {
827 nick: command.params[0],
828 channel: command.params[1],
829 reason: command.params[command.params.length - 1]
830 });
831 },
832
833 ERR_NOTONCHANNEL: function (command) {
834 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' not_on_channel', {
835 channel: command.params[1],
836 reason: command.params[command.params.length - 1]
837 });
838 },
839
840 ERR_USERONCHANNEL: function (command) {
841 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' user_on_channel', {
842 nick: command.params[1],
843 channel: command.params[2]
844 });
845 },
846
847 ERR_CHANNELISFULL: function (command) {
848 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' channel_is_full', {
849 channel: command.params[1],
850 reason: command.params[command.params.length - 1]
851 });
852 },
853
854 ERR_INVITEONLYCHAN: function (command) {
855 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' invite_only_channel', {
856 channel: command.params[1],
857 reason: command.params[command.params.length - 1]
858 });
859 },
860
861 ERR_BANNEDFROMCHAN: function (command) {
862 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' banned_from_channel', {
863 channel: command.params[1],
864 reason: command.params[command.params.length - 1]
865 });
866 },
867
868 ERR_BADCHANNELKEY: function (command) {
869 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' bad_channel_key', {
870 channel: command.params[1],
871 reason: command.params[command.params.length - 1]
872 });
873 },
874
875 ERR_CHANOPRIVSNEEDED: function (command) {
876 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' chanop_privs_needed', {
877 channel: command.params[1],
878 reason: command.params[command.params.length - 1]
879 });
880 },
881
882 ERR_NICKNAMEINUSE: function (command) {
883 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' nickname_in_use', {
884 nick: command.params[1],
885 reason: command.params[command.params.length - 1]
886 });
887 },
888
889 ERR_ERRONEUSNICKNAME: function(command) {
890 this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' erroneus_nickname', {
891 nick: command.params[1],
892 reason: command.params[command.params.length - 1]
893 });
894 },
895
896 ERR_NOTREGISTERED: function (command) {
897 },
898
899 RPL_MAPMORE: function (command) {
900 var params = _.clone(command.params);
901 params.shift();
902 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
903 },
904
905 RPL_MAPEND: function (command) {
906 var params = _.clone(command.params);
907 params.shift();
908 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
909 },
910
911 RPL_LINKS: function (command) {
912 var params = _.clone(command.params);
913 params.shift();
914 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
915 },
916
917 RPL_ENDOFLINKS: function (command) {
918 var params = _.clone(command.params);
919 params.shift();
920 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
921 },
922
923 ERR_UNKNOWNCOMMAND: function (command) {
924 var params = _.clone(command.params);
925 params.shift();
926 genericNotice.call(this, command, '`' + params.slice(0, -1).join(', ') + '` ' + command.params[command.params.length - 1]);
927 },
928
929 ERR_NOMOTD: function (command) {
930 var params = _.clone(command.params);
931 params.shift();
932 genericNotice.call(this, command, command.params[command.params.length - 1]);
933 },
934
935 ERR_NOPRIVILEGES: function (command) {
936 var params = _.clone(command.params);
937 params.shift();
938 genericNotice.call(this, command, command.params[command.params.length - 1]);
939 },
940
941 RPL_STATSCONN: function (command) {
942 var params = _.clone(command.params);
943 params.shift();
944 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
945 },
946
947 RPL_LUSERCLIENT: function (command) {
948 var params = _.clone(command.params);
949 params.shift();
950 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
951 },
952
953 RPL_LUSEROP: function (command) {
954 var params = _.clone(command.params);
955 params.shift();
956 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
957 },
958
959 RPL_LUSERUNKNOWN: function (command) {
960 var params = _.clone(command.params);
961 params.shift();
962 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
963 },
964
965 RPL_LUSERCHANNELS: function (command) {
966 var params = _.clone(command.params);
967 params.shift();
968 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
969 },
970
971 RPL_LUSERME: function (command) {
972 var params = _.clone(command.params);
973 params.shift();
974 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
975 },
976
977 RPL_LOCALUSERS: function (command) {
978 var params = _.clone(command.params);
979 params.shift();
980 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
981 },
982
983 RPL_GLOBALUSERS: function (command) {
984 var params = _.clone(command.params);
985 params.shift();
986 genericNotice.call(this, command, params.slice(0, -1).join(', ') + ' ' + command.params[command.params.length - 1]);
987 },
988
989 RPL_HOSTCLOACKING: function (command) {
990 genericNotice.call(this, command, command.params[1] + ' ' + command.params[command.params.length - 1]);
991 },
992 };
993
994
995
996
997 function genericNotice (command, msg, is_error) {
998 // Default to being an error
999 if (typeof is_error !== 'boolean')
1000 is_error = true;
1001
1002 this.irc_connection.clientEvent('notice', {
1003 from_server: true,
1004 nick: command.prefix,
1005 ident: '',
1006 hostname: '',
1007 target: command.params[0],
1008 msg: msg,
1009 numeric: parseInt(command.command, 10)
1010 });
1011 }
1012
1013
1014 /**
1015 * Convert a mode string such as '+k pass', or '-i' to a readable
1016 * format.
1017 * [ { mode: '+k', param: 'pass' } ]
1018 * [ { mode: '-i', param: null } ]
1019 */
1020 function parseModeList(mode_string, mode_params) {
1021 var chanmodes = this.irc_connection.options.CHANMODES || [],
1022 prefixes = this.irc_connection.options.PREFIX || [],
1023 always_param = (chanmodes[0] || '').concat((chanmodes[1] || '')),
1024 modes = [],
1025 has_param, i, j, add;
1026
1027 prefixes = _.reduce(prefixes, function (list, prefix) {
1028 list.push(prefix.mode);
1029 return list;
1030 }, []);
1031 always_param = always_param.split('').concat(prefixes);
1032
1033 has_param = function (mode, add) {
1034 if (_.find(always_param, function (m) {
1035 return m === mode;
1036 })) {
1037 return true;
1038 } else if (add && _.find((chanmodes[2] || '').split(''), function (m) {
1039 return m === mode;
1040 })) {
1041 return true;
1042 } else {
1043 return false;
1044 }
1045 };
1046
1047 j = 0;
1048 for (i = 0; i < mode_string.length; i++) {
1049 switch (mode_string[i]) {
1050 case '+':
1051 add = true;
1052 break;
1053 case '-':
1054 add = false;
1055 break;
1056 default:
1057 if (has_param(mode_string[i], add)) {
1058 modes.push({mode: (add ? '+' : '-') + mode_string[i], param: mode_params[j]});
1059 j++;
1060 } else {
1061 modes.push({mode: (add ? '+' : '-') + mode_string[i], param: null});
1062 }
1063 }
1064 }
1065
1066 return modes;
1067 }
1068
1069
1070 function getServerTime(command) {
1071 var time;
1072
1073 // No tags? No times.
1074 if (!command.tags || command.tags.length === 0) {
1075 return time;
1076 }
1077
1078 if (capContainsAny.call(this, ['server-time', 'znc.in/server-time', 'znc.in/server-time-iso'])) {
1079 time = _.find(command.tags, function (tag) {
1080 return tag.tag === 'time';
1081 });
1082
1083 time = time ? time.value : undefined;
1084
1085 // Convert the time value to a unixtimestamp
1086 if (typeof time === 'string') {
1087 if (time.indexOf('T') > -1) {
1088 time = parseISO8601(time);
1089
1090 } else if(time.match(/^[0-9.]+$/)) {
1091 // A string formatted unix timestamp
1092 time = new Date(time * 1000);
1093 }
1094
1095 time = time.getTime();
1096
1097 } else if (typeof time === 'number') {
1098 time = new Date(time * 1000);
1099 time = time.getTime();
1100 }
1101 }
1102
1103 return time;
1104 }
1105
1106
1107 function capContainsAny (caps) {
1108 var intersection;
1109 if (!caps instanceof Array) {
1110 caps = [caps];
1111 }
1112 intersection = _.intersection(this.irc_connection.cap.enabled, caps);
1113 return intersection.length > 0;
1114 }
1115
1116
1117 // Code based on http://anentropic.wordpress.com/2009/06/25/javascript-iso8601-parser-and-pretty-dates/#comment-154
1118 function parseISO8601(str) {
1119 if (Date.prototype.toISOString) {
1120 return new Date(str);
1121 } else {
1122 var parts = str.split('T'),
1123 dateParts = parts[0].split('-'),
1124 timeParts = parts[1].split('Z'),
1125 timeSubParts = timeParts[0].split(':'),
1126 timeSecParts = timeSubParts[2].split('.'),
1127 timeHours = Number(timeSubParts[0]),
1128 _date = new Date();
1129
1130 _date.setUTCFullYear(Number(dateParts[0]));
1131 _date.setUTCDate(1);
1132 _date.setUTCMonth(Number(dateParts[1])-1);
1133 _date.setUTCDate(Number(dateParts[2]));
1134 _date.setUTCHours(Number(timeHours));
1135 _date.setUTCMinutes(Number(timeSubParts[1]));
1136 _date.setUTCSeconds(Number(timeSecParts[0]));
1137 if (timeSecParts[1]) {
1138 _date.setUTCMilliseconds(Number(timeSecParts[1]));
1139 }
1140
1141 return _date;
1142 }
1143 }