1 var _
= require('lodash');
9 RPL_WHOISREGNICK
: '307',
11 RPL_WHOISSERVER
: '312',
12 RPL_WHOISOPERATOR
: '313',
14 RPL_ENDOFWHOIS
: '318',
15 RPL_WHOISCHANNELS
: '319',
21 RPL_TOPICWHOTIME
: '333',
24 RPL_ENDOFLINKS
: '365',
25 RPL_ENDOFNAMES
: '366',
27 RPL_ENDOFBANLIST
: '368',
31 RPL_WHOISMODES
: '379',
32 ERR_NOSUCHNICK
: '401',
33 ERR_CANNOTSENDTOCHAN
: '404',
34 ERR_TOOMANYCHANNELS
: '405',
35 ERR_UNKNOWNCOMMAND
: '421',
37 ERR_NICKNAMEINUSE
: '433',
38 ERR_USERNOTINCHANNEL
: '441',
39 ERR_NOTONCHANNEL
: '442',
40 ERR_NOTREGISTERED
: '451',
41 ERR_LINKCHANNEL
: '470',
42 ERR_CHANNELISFULL
: '471',
43 ERR_INVITEONLYCHAN
: '473',
44 ERR_BANNEDFROMCHAN
: '474',
45 ERR_BADCHANNELKEY
: '475',
46 ERR_NOPRIVILEGES
: '481',
47 ERR_CHANOPRIVSNEEDED
: '482',
49 RPL_SASLAUTHENTICATED
: '900',
50 RPL_SASLLOGGEDIN
: '903',
51 ERR_SASLNOTAUTHORISED
: '904',
52 ERR_SASLABORTED
: '906',
53 ERR_SASLALREADYAUTHED
: '907'
58 var IrcCommands = function (irc_connection
, con_num
, client
) {
59 this.irc_connection
= irc_connection
;
60 this.con_num
= con_num
;
63 module
.exports
= IrcCommands
;
65 IrcCommands
.prototype.bindEvents = function () {
68 _
.each(listeners
, function (listener
, command
) {
69 var s
= command
.substr(0, 4);
70 if ((s
=== 'RPL_') || (s
=== 'ERR_')) {
71 command
= irc_numerics
[command
];
73 that
.irc_connection
.on('irc_' + command
, function () {
74 listener
.apply(that
, arguments
);
79 IrcCommands
.prototype.dispose = function () {
80 this.removeAllListeners();
86 'RPL_WELCOME': function (command
) {
87 var nick
= command
.params
[0];
88 this.irc_connection
.registered
= true;
89 this.cap_negotation
= false;
90 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':connect', {
94 'RPL_ISUPPORT': function (command
) {
95 var options
, i
, option
, matches
, j
;
96 options
= command
.params
;
97 for (i
= 1; i
< options
.length
; i
++) {
98 option
= options
[i
].split("=", 2);
99 option
[0] = option
[0].toUpperCase();
100 this.irc_connection
.options
[option
[0]] = (typeof option
[1] !== 'undefined') ? option
[1] : true;
101 if (_
.include(['NETWORK', 'PREFIX', 'CHANTYPES', 'CHANMODES', 'NAMESX'], option
[0])) {
102 if (option
[0] === 'PREFIX') {
103 matches
= /\(([^)]*)\)(.*)/.exec(option
[1]);
104 if ((matches
) && (matches
.length
=== 3)) {
105 this.irc_connection
.options
.PREFIX
= [];
106 for (j
= 0; j
< matches
[2].length
; j
++) {
107 this.irc_connection
.options
.PREFIX
.push({symbol
: matches
[2].charAt(j
), mode
: matches
[1].charAt(j
)});
110 } else if (option
[0] === 'CHANTYPES') {
111 this.irc_connection
.options
.CHANTYPES
= this.irc_connection
.options
.CHANTYPES
.split('');
112 } else if (option
[0] === 'CHANMODES') {
113 this.irc_connection
.options
.CHANMODES
= option
[1].split(',');
114 } else if ((option
[0] === 'NAMESX') && (!_
.contains(this.irc_connection
.cap
.enabled
, 'multi-prefix'))) {
115 this.irc_connection
.write('PROTOCTL NAMESX');
119 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':options', {
120 options
: this.irc_connection
.options
,
121 cap
: this.irc_connection
.cap
.enabled
124 'RPL_ENDOFWHOIS': function (command
) {
125 this.irc_connection
.emit('user:' + command
.params
[1] + ':endofwhois', {
126 nick
: command
.params
[1],
127 msg
: command
.trailing
130 'RPL_WHOISUSER': function (command
) {
131 this.irc_connection
.emit('user:' + command
.params
[1] + ':whoisuser', {
132 nick
: command
.params
[1],
133 ident
: command
.params
[2],
134 host
: command
.params
[3],
135 msg
: command
.trailing
138 'RPL_WHOISSERVER': function (command
) {
139 this.irc_connection
.emit('user:' + command
.params
[1] + ':whoisserver', {
140 nick
: command
.params
[1],
141 irc_server
: command
.params
[2]
144 'RPL_WHOISOPERATOR': function (command
) {
145 this.irc_connection
.emit('user:' + command
.params
[1] + ':whoisoperator', {
146 nick
: command
.params
[1],
147 msg
: command
.trailing
150 'RPL_WHOISCHANNELS': function (command
) {
151 this.irc_connection
.emit('user:' + command
.params
[1] + ':whoischannels', {
152 nick
: command
.params
[1],
153 chans
: commnd
.trailing
156 'RPL_WHOISMODES': function (command
) {
157 this.irc_connection
.emit('user:' + command
.params
[1] + ':whoismodes', {
158 nick
: command
.params
[1],
159 msg
: command
.trailing
162 'RPL_WHOISIDLE': function (command
) {
163 this.irc_connection
.emit('user:' + command
.params
[1] + ':whoisidle', {
164 nick
: command
.params
[1],
165 idle
: command
.params
[2],
166 logon
: command
.params
[3] || undefined
169 'RPL_WHOISREGNICK': function (command
) {
170 this.irc_connection
.emit('user:' + command
.params
[1] + ':whoisregnick', {
171 nick
: command
.params
[1],
172 msg
: command
.trailing
175 'RPL_LISTSTART': function (command
) {
176 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':list_start', {});
177 this.client
.buffer
.list
= [];
179 'RPL_LISTEND': function (command
) {
180 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':list_end', {});
182 'RPL_LIST': function (command
) {
183 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':list_channel', {
184 channel
: command
.params
[1],
185 num_users
: parseint(command
.params
[2], 10),
186 topic
: command
.trailing
190 'RPL_MOTD': function (command
) {
191 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':motd', {
192 motd
: command
.trailing
+ '\n';
195 'RPL_MOTDSTART': function (command
) {
196 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':motd_start', {});
198 'RPL_ENDOFMOTD': function (command
) {
199 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':motd_end', {});
201 'RPL_NAMEREPLY': function (command
) {
202 var members
= command
.trailing
.split(' ');
203 var member_list
= [];
206 _
.each(members
, function (member
) {
207 var j
, k
, modes
= [];
209 // Make sure we have some prefixes already
210 if (that
.irc_connection
.options
.PREFIX
) {
211 for (j
= 0; j
< member
.length
; j
++) {
212 for (k
= 0; k
< that
.irc_connection
.options
.PREFIX
.length
; k
++) {
213 if (member
.charAt(j
) === that
.irc_connection
.options
.PREFIX
[k
].symbol
) {
214 modes
.push(that
.irc_connection
.options
.PREFIX
[k
].mode
);
221 member_list
.push({nick
: member
, modes
: modes
});
224 that
.irc_connection
.emit('channel:' + command
.params
[2] + ':userlist', {
226 channel
: command
.params
[2]
231 'RPL_ENDOFNAMES': function (command
) {
232 that
.irc_connection
.emit('channel:' + command
.params
[1] + ':userlist_end', {
233 channel
: command
.params
[1]
238 'RPL_BANLIST': function (command
) {
239 this.irc_connection
.emit('channel:' + command
.params
[1] + ':banlist', {
240 channel
: command
.params
[1],
241 banned
: command
.params
[2],
242 banned_by
: command
.params
[3],
243 banned_at
: command
.params
[4]
246 'RPL_ENDOFBANLIST': function (command
) {
247 this.irc_connection
.emit('channel:' + command
.params
[1] + ':banlist_end', {
248 channel
: commands
.params
[1]
251 'RPL_TOPIC': function (command
) {
252 this.irc_connection
.emit('channel:' + command
.params
[1] + ':topic', {
253 channel
: command
.params
[1],
254 topic
: command
.trailing
257 'RPL_NOTOPIC': function (command
) {
258 this.irc_connection
.emit('channel:' + command
.params
[1] + ':topic', {
259 channel
: command
.params
[1],
263 'RPL_TOPICWHOTIME': function (command
) {
264 this.irc_connection
.emit('channel:' + command
.params
[1] + ':topicsetby', {
265 nick
: command
.params
[2],
266 channel
: command
.params
[1],
267 when
: command
.params
[3]
270 'PING': function (command
) {
271 this.irc_connection
.write('PONG ' + command
.trailing
);
275 'JOIN': function (command
) {
277 if (typeof command
.trailing
=== 'string' && command
.trailing
!== '') {
278 channel
= command
.trailing
;
279 } else if (typeof command
.params
[0] === 'string' && command
.params
[0] !== '') {
280 channel
= command
.params
[0];
283 this.irc_connection
.emit('channel:' + channel
+ ':join', {
285 ident
: command
.ident
,
286 hostname
: command
.hostname
,
292 'PART': function (command
) {
293 this.irc_connection
.emit('channel:' + command
.params
[0] + ':part', {
295 ident
: command
.ident
,
296 hostname
: command
.hostname
,
297 channel
: command
.params
[0],
298 message
: command
.trailing
303 'KICK': function (command
) {
304 this.irc_connection
.emit('channel:' + command
.params
[0] + ':kick', {
305 kicked
: command
.params
[1],
307 ident
: command
.ident
,
308 hostname
: command
.hostname
,
309 channel
: command
.params
[0],
310 message
: command
.trailing
315 'QUIT': function (command
) {
316 this.irc_connection
.emit('user:' + command
.nick
+ ':quit', {
318 ident
: command
.ident
,
319 hostname
: command
.hostname
,
320 message
: command
.trailing
325 'NOTICE': function (command
) {
328 if ((command
.trailing
.charAt(0) === String
.fromCharCode(1)) && (command
.trailing
.charAt(command
.trailing
.length
- 1) === String
.fromCharCode(1))) {
329 // It's a CTCP response
330 namespace = (command
.params
[0] == this.irc_connection
.nick
) ? 'user' : 'channel';
331 this.irc_connection
.emit(namespace + ':' + command
.params
[0] + ':ctcp_response', {
333 ident
: command
.ident
,
334 hostname
: command
.hostname
,
335 channel
: command
.params
[0],
336 msg
: command
.trailing
.substr(1, command
.trailing
.length
- 2)
339 namespace = (command
.params
[0] == this.irc_connection
.nick
) ? 'user' : 'channel';
340 this.irc_connection
.emit(namespace + ':' + command
.params
[0] + ':notice', {
342 ident
: command
.ident
,
343 hostname
: command
.hostname
,
344 target
: command
.params
[0],
345 msg
: command
.trailing
349 'NICK': function (command
) {
350 this.irc_connection
.emit('user:' + nick
+ ':nick', {
352 ident
: command
.ident
,
353 hostname
: command
.hostname
,
354 newnick
: command
.trailing
|| command
.params
[0]
357 'TOPIC': function (command
) {
358 // If we don't have an associated channel, no need to continue
359 if (!command
.params
[0]) return;
361 var channel
= command
.params
[0],
362 topic
= command
.trailing
|| '';
364 this.irc_connection
.emit('channel:' + channel
+ ':topic', {
370 'MODE': function (command
) {
371 var chanmodes
= this.irc_connection
.options
.CHANMODES
|| [],
372 prefixes
= this.irc_connection
.options
.PREFIX
|| [],
373 always_param
= (chanmodes
[0] || '').concat((chanmodes
[1] || '')),
375 has_param
, i
, j
, add
;
377 prefixes
= _
.reduce(prefixes
, function (list
, prefix
) {
378 list
.push(prefix
.mode
);
381 always_param
= always_param
.split('').concat(prefixes
);
383 has_param = function (mode
, add
) {
384 if (_
.find(always_param
, function (m
) {
388 } else if (add
&& _
.find((chanmodes
[2] || '').split(''), function (m
) {
397 if (!command
.params
[1]) {
398 command
.params
[1] = command
.trailing
;
401 for (i
= 0; i
< command
.params
[1].length
; i
++) {
402 switch (command
.params
[1][i
]) {
410 if (has_param(command
.params
[1][i
], add
)) {
411 modes
.push({mode
: (add
? '+' : '-') + command
.params
[1][i
], param
: command
.params
[2 + j
]});
414 modes
.push({mode
: (add
? '+' : '-') + command
.params
[1][i
], param
: null});
419 this.client
.sendIrcCommand('mode', {
420 server
: this.con_num
,
421 target
: command
.params
[0],
422 nick
: command
.nick
|| command
.prefix
|| '',
426 'PRIVMSG': function (command
) {
428 if ((command
.trailing
.charAt(0) === String
.fromCharCode(1)) && (command
.trailing
.charAt(command
.trailing
.length
- 1) === String
.fromCharCode(1))) {
430 if (command
.trailing
.substr(1, 6) === 'ACTION') {
431 this.client
.sendIrcCommand('action', {server
: this.con_num
, nick
: command
.nick
, ident
: command
.ident
, hostname
: command
.hostname
, channel
: command
.params
[0], msg
: command
.trailing
.substr(7, command
.trailing
.length
- 2)});
432 } else if (command
.trailing
.substr(1, 4) === 'KIWI') {
433 tmp
= command
.trailing
.substr(6, command
.trailing
.length
- 2);
434 namespace = tmp
.split(' ', 1)[0];
435 this.client
.sendIrcCommand('kiwi', {server
: this.con_num
, namespace: namespace, data
: tmp
.substr(namespace.length
+ 1)});
436 } else if (command
.trailing
.substr(1, 7) === 'VERSION') {
437 this.irc_connection
.write('NOTICE ' + command
.nick
+ ' :' + String
.fromCharCode(1) + 'VERSION KiwiIRC' + String
.fromCharCode(1));
438 } else if (command
.trailing
.substr(1, 6) === 'SOURCE') {
439 this.irc_connection
.write('NOTICE ' + command
.nick
+ ' :' + String
.fromCharCode(1) + 'SOURCE http://www.kiwiirc.com/' + String
.fromCharCode(1));
440 } else if (command
.trailing
.substr(1, 10) === 'CLIENTINFO') {
441 this.irc_connection
.write('NOTICE ' + command
.nick
+ ' :' + String
.fromCharCode(1) + 'CLIENTINFO SOURCE VERSION TIME' + String
.fromCharCode(1));
443 namespace = (command
.target
== this.irc_connection
.nick
) ? 'user' : 'channel';
444 this.irc_connection
.emit(namespace + ':' + command
.nick
+ ':ctcp_request', {
446 ident
: command
.ident
,
447 hostname
: command
.hostname
,
448 target
: command
.params
[0],
449 type
: (command
.trailing
.substr(1, command
.trailing
.length
- 2).split(' ') || [null])[0],
450 msg
: command
.trailing
.substr(1, command
.trailing
.length
- 2)
454 // A message to a user (private message) or to a channel?
455 namespace = (command
.target
== this.irc_connection
.nick
) ? 'user' : 'channel';
456 this.irc_connection
.emit(namespace + ':' + command
.nick
+ ':privmsg', {
458 ident
: command
.ident
,
459 hostname
: command
.hostname
,
460 channel
: command
.params
[0],
461 msg
: command
.trailing
465 'CAP': function (command
) {
466 // TODO: capability modifiers
467 // i.e. - for disable, ~ for requires ACK, = for sticky
468 var capabilities
= command
.trailing
.replace(/[\-~=]/, '').split(' ');
470 var want
= ['multi-prefix', 'away-notify'];
472 if (this.irc_connection
.password
) {
476 switch (command
.params
[1]) {
478 request
= _
.intersection(capabilities
, want
);
479 if (request
.length
> 0) {
480 this.irc_connection
.cap
.requested
= request
;
481 this.irc_connection
.write('CAP REQ :' + request
.join(' '));
483 this.irc_connection
.write('CAP END');
484 this.irc_connection
.cap_negotation
= false;
488 if (capabilities
.length
> 0) {
489 this.irc_connection
.cap
.enabled
= capabilities
;
490 this.irc_connection
.cap
.requested
= _
.difference(this.irc_connection
.cap
.requested
, capabilities
);
492 if (this.irc_connection
.cap
.requested
.length
> 0) {
493 if (_
.contains(this.irc_connection
.cap
.enabled
, 'sasl')) {
494 this.irc_connection
.sasl
= true;
495 this.irc_connection
.write('AUTHENTICATE PLAIN');
497 this.irc_connection
.write('CAP END');
498 this.irc_connection
.cap_negotation
= false;
503 if (capabilities
.length
> 0) {
504 this.irc_connection
.cap
.requested
= _
.difference(this.irc_connection
.cap
.requested
, capabilities
);
506 if (this.irc_connection
.cap
.requested
.length
> 0) {
507 this.irc_connection
.write('CAP END');
508 this.irc_connection
.cap_negotation
= false;
512 // should we do anything here?
516 'AUTHENTICATE': function (command
) {
517 var b
= new Buffer(this.irc_connection
.nick
+ "\0" + this.irc_connection
.nick
+ "\0" + this.irc_connection
.password
, 'utf8');
518 var b64
= b
.toString('base64');
519 if (command
.params
[0] === '+') {
520 while (b64
.length
>= 400) {
521 this.irc_connection
.write('AUTHENTICATE ' + b64
.slice(0, 399));
522 b64
= b64
.slice(399);
524 if (b64
.length
> 0) {
525 this.irc_connection
.write('AUTHENTICATE ' + b64
);
527 this.irc_connection
.write('AUTHENTICATE +');
530 this.irc_connection
.write('CAP END');
531 this.irc_connection
.cap_negotation
= false;
534 'AWAY': function (command
) {
535 this.irc_connection
.emit('user:' + command
.nick
+ ':away', {
537 msg
: command
.trailing
540 'RPL_SASLAUTHENTICATED': function (command
) {
541 this.irc_connection
.write('CAP END');
542 this.irc_connection
.cap_negotation
= false;
543 this.irc_connection
.sasl
= true;
545 'RPL_SASLLOGGEDIN': function (command
) {
546 if (this.irc_connection
.cap_negotation
=== false) {
547 this.irc_connection
.write('CAP END');
550 'ERR_SASLNOTAUTHORISED': function (command
) {
551 this.irc_connection
.write('CAP END');
552 this.irc_connection
.cap_negotation
= false;
554 'ERR_SASLABORTED': function (command
) {
555 this.irc_connection
.write('CAP END');
556 this.irc_connection
.cap_negotation
= false;
558 'ERR_SASLALREADYAUTHED': function (command
) {
561 'ERROR': function (command
) {
562 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':error', {
563 reason
: command
.trailing
566 ERR_LINKCHANNEL: function (command
) {
567 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':channel_redirect', {
568 from: command
.params
[1],
569 to
: command
.params
[2]
572 ERR_NOSUCHNICK: function (command
) {
573 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':no_such_nick', {
574 nick
: command
.params
[1],
575 reason
: command
.trailing
578 ERR_CANNOTSENDTOCHAN: function (command
) {
579 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':cannot_send_to_chan', {
580 channel
: command
.params
[1],
581 reason
: command
.trailing
584 ERR_TOOMANYCHANNELS: function (command
) {
585 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':too_many_channels', {
586 channel
: command
.params
[1],
587 reason
: command
.trailing
590 ERR_USERNOTINCHANNEL: function (command
) {
591 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':user_not_in_channel', {
592 nick
: command
.params
[0],
593 channel
: command
.params
[1],
594 reason
: command
.trailing
597 ERR_NOTONCHANNEL: function (command
) {
598 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':not_on_channel', {
599 channel
: command
.params
[1],
600 reason
: command
.trailing
603 ERR_CHANNELISFULL: function (command
) {
604 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':channel_is_full', {
605 channel
: command
.params
[1],
606 reason
: command
.trailing
609 ERR_INVITEONLYCHAN: function (command
) {
610 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':invite_only_channel', {
611 channel
: command
.params
[1],
612 reason
: command
.trailing
615 ERR_BANNEDFROMCHAN: function (command
) {
616 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':banned_from_channel', {
617 channel
: command
.params
[1],
618 reason
: command
.trailing
621 ERR_BADCHANNELKEY: function (command
) {
622 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':bad_channel_key', {
623 channel
: command
.params
[1],
624 reason
: command
.trailing
627 ERR_CHANOPRIVSNEEDED: function (command
) {
628 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':chanop_privs_needed', {
629 channel
: command
.params
[1],
630 reason
: command
.trailing
633 ERR_NICKNAMEINUSE: function (command
) {
634 this.irc_connection
.emit('server:' + this.irc_connection
.irc_host
.hostname
+ ':nickname_in_use', {
635 nick
: command
.params
[1],
636 reason
: command
.trailing
639 ERR_NOTREGISTERED: function (command
) {
642 RPL_MAPMORE: function (command
) {
643 var params
= _
.clone(command
.params
);
645 genericNotice
.call(this, command
, params
.join(', ') + ' ' + command
.trailing
);
647 RPL_MAPEND: function (command
) {
648 var params
= _
.clone(command
.params
);
650 genericNotice
.call(this, command
, params
.join(', ') + ' ' + command
.trailing
);
653 RPL_LINKS: function (command
) {
654 var params
= _
.clone(command
.params
);
656 genericNotice
.call(this, command
, params
.join(', ') + ' ' + command
.trailing
);
658 RPL_ENDOFLINKS: function (command
) {
659 var params
= _
.clone(command
.params
);
661 genericNotice
.call(this, command
, params
.join(', ') + ' ' + command
.trailing
);
664 ERR_UNKNOWNCOMMAND: function (command
) {
665 var params
= _
.clone(command
.params
);
667 genericNotice
.call(this, command
, '`' + params
.join(', ') + '` ' + command
.trailing
);
670 ERR_NOMOTD: function (command
) {
671 var params
= _
.clone(command
.params
);
673 genericNotice
.call(this, command
, command
.trailing
);
676 ERR_NOPRIVILEGES: function (command
) {
677 var params
= _
.clone(command
.params
);
679 genericNotice
.call(this, command
, command
.trailing
);
686 function genericNotice (command
, msg
, is_error
) {
687 // Default to being an error
688 if (typeof is_error
!== 'boolean')
691 this.client
.sendIrcCommand('notice', {
692 server
: this.con_num
,
693 nick
: command
.prefix
,
696 target
: command
.params
[0],
698 numeric
: parseInt(command
.command
, 10)