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