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