Merge branch 'master' of github.com:prawnsalad/KiwiIRC
[KiwiIRC.git] / server / irc / commands.js
1 var _ = require('lodash');
2
3 var irc_numerics = {
4 RPL_WELCOME: '001',
5 RPL_MYINFO: '004',
6 RPL_ISUPPORT: '005',
7 RPL_WHOISREGNICK: '307',
8 RPL_WHOISUSER: '311',
9 RPL_WHOISSERVER: '312',
10 RPL_WHOISOPERATOR: '313',
11 RPL_WHOISIDLE: '317',
12 RPL_ENDOFWHOIS: '318',
13 RPL_WHOISCHANNELS: '319',
14 RPL_LISTSTART: '321',
15 RPL_LIST: '322',
16 RPL_LISTEND: '323',
17 RPL_NOTOPIC: '331',
18 RPL_TOPIC: '332',
19 RPL_TOPICWHOTIME: '333',
20 RPL_NAMEREPLY: '353',
21 RPL_ENDOFNAMES: '366',
22 RPL_BANLIST: '367',
23 RPL_ENDOFBANLIST: '368',
24 RPL_MOTD: '372',
25 RPL_MOTDSTART: '375',
26 RPL_ENDOFMOTD: '376',
27 RPL_WHOISMODES: '379',
28 ERR_NOSUCHNICK: '401',
29 ERR_CANNOTSENDTOCHAN: '404',
30 ERR_TOOMANYCHANNELS: '405',
31 ERR_NICKNAMEINUSE: '433',
32 ERR_USERNOTINCHANNEL: '441',
33 ERR_NOTONCHANNEL: '442',
34 ERR_NOTREGISTERED: '451',
35 ERR_LINKCHANNEL: '470',
36 ERR_CHANNELISFULL: '471',
37 ERR_INVITEONLYCHAN: '473',
38 ERR_BANNEDFROMCHAN: '474',
39 ERR_BADCHANNELKEY: '475',
40 ERR_CHANOPRIVSNEEDED: '482',
41 RPL_STARTTLS: '670',
42 RPL_SASLAUTHENTICATED: '900',
43 RPL_SASLLOGGEDIN: '903',
44 ERR_SASLNOTAUTHORISED: '904',
45 ERR_SASLABORTED: '906',
46 ERR_SASLALREADYAUTHED: '907'
47
48 };
49
50
51 var IrcCommands = function (irc_connection, con_num, client) {
52 this.irc_connection = irc_connection;
53 this.con_num = con_num;
54 this.client = client;
55 };
56 module.exports = IrcCommands;
57
58 IrcCommands.prototype.bindEvents = function () {
59 var that = this;
60
61 _.each(listeners, function (listener, command) {
62 var s = command.substr(0, 4);
63 if ((s === 'RPL_') || (s === 'ERR_')) {
64 command = irc_numerics[command];
65 }
66 that.irc_connection.on('irc_' + command, function () {
67 listener.apply(that, arguments);
68 });
69 });
70 };
71
72 IrcCommands.prototype.dispose = function () {
73 this.removeAllListeners();
74 };
75
76
77
78 var listeners = {
79 'RPL_WELCOME': function (command) {
80 var nick = command.params[0];
81 this.irc_connection.registered = true;
82 this.cap_negotation = false;
83 this.client.sendIrcCommand('connect', {server: this.con_num, nick: nick});
84 },
85 'RPL_ISUPPORT': function (command) {
86 var options, i, option, matches, j;
87 options = command.params;
88 for (i = 1; i < options.length; i++) {
89 option = options[i].split("=", 2);
90 option[0] = option[0].toUpperCase();
91 this.irc_connection.options[option[0]] = (typeof option[1] !== 'undefined') ? option[1] : true;
92 if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES', 'CHANMODES', 'NAMESX'], option[0])) {
93 if (option[0] === 'PREFIX') {
94 matches = /\(([^)]*)\)(.*)/.exec(option[1]);
95 if ((matches) && (matches.length === 3)) {
96 this.irc_connection.options.PREFIX = [];
97 for (j = 0; j < matches[2].length; j++) {
98 this.irc_connection.options.PREFIX.push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
99 }
100 }
101 } else if (option[0] === 'CHANTYPES') {
102 this.irc_connection.options.CHANTYPES = this.irc_connection.options.CHANTYPES.split('');
103 } else if (option[0] === 'CHANMODES') {
104 this.irc_connection.options.CHANMODES = option[1].split(',');
105 } else if ((option[0] === 'NAMESX') && (!_.contains(this.irc_connection.cap.enabled, 'multi-prefix'))) {
106 this.irc_connection.write('PROTOCTL NAMESX');
107 }
108 }
109 }
110 this.client.sendIrcCommand('options', {server: this.con_num, options: this.irc_connection.options, cap: this.irc_connection.cap.enabled});
111 },
112 'RPL_ENDOFWHOIS': function (command) {
113 this.client.sendIrcCommand('whois', {server: this.con_num, nick: command.params[1], msg: command.trailing, end: true});
114 },
115 'RPL_WHOISUSER': function (command) {
116 this.client.sendIrcCommand('whois', {server: this.con_num, nick: command.params[1], ident: command.params[2], host: command.params[3], msg: command.trailing, end: false});
117 },
118 'RPL_WHOISSERVER': function (command) {
119 this.client.sendIrcCommand('whois', {server: this.con_num, nick: command.params[1], irc_server: command.params[2], end: false});
120 },
121 'RPL_WHOISOPERATOR': function (command) {
122 this.client.sendIrcCommand('whois', {server: this.con_num, nick: command.params[1], msg: command.trailing, end: false});
123 },
124 'RPL_WHOISCHANNELS': function (command) {
125 this.client.sendIrcCommand('whois', {server: this.con_num, nick: command.params[1], chans: command.trailing, end: false});
126 },
127 'RPL_WHOISMODES': function (command) {
128 this.client.sendIrcCommand('whois', {server: this.con_num, nick: command.params[1], msg: command.trailing, end: false});
129 },
130 'RPL_WHOISIDLE': function (command) {
131 if (command.params[3]) {
132 this.client.sendIrcCommand('whois', {server: this.con_num, nick: command.params[1], idle: command.params[2], logon: command.params[3], end: false});
133 } else {
134 this.client.sendIrcCommand('whois', {server: this.con_num, nick: command.params[1], idle: command.params[2], end: false});
135 }
136 },
137 'RPL_WHOISREGNICK': function (command) {
138 this.client.sendIrcCommand('whois', {server: this.con_num, nick: command.params[1], msg: command.trailing, end: false});
139 },
140 'RPL_LISTSTART': function (command) {
141 this.client.sendIrcCommand('list_start', {server: this.con_num});
142 this.client.buffer.list = [];
143 },
144 'RPL_LISTEND': function (command) {
145 if (this.client.buffer.list.length > 0) {
146 this.client.buffer.list = _.sortBy(this.client.buffer.list, function (channel) {
147 return channel.num_users;
148 });
149 this.client.sendIrcCommand('list_channel', {server: this.con_num, chans: this.client.buffer.list});
150 this.client.buffer.list = [];
151 }
152 this.client.sendIrcCommand('list_end', {server: this.con_num});
153 },
154 'RPL_LIST': function (command) {
155 this.client.buffer.list.push({server: this.con_num, channel: command.params[1], num_users: parseInt(command.params[2], 10), topic: command.trailing});
156 if (this.client.buffer.list.length > 200){
157 this.client.buffer.list = _.sortBy(this.client.buffer.list, function (channel) {
158 return channel.num_users;
159 });
160 this.client.sendIrcCommand('list_channel', {server: this.con_num, chans: this.client.buffer.list});
161 this.client.buffer.list = [];
162 }
163 },
164 'RPL_MOTD': function (command) {
165 this.client.buffer.motd += command.trailing + '\n';
166 },
167 'RPL_MOTDSTART': function (command) {
168 this.client.buffer.motd = '';
169 },
170 'RPL_ENDOFMOTD': function (command) {
171 this.client.sendIrcCommand('motd', {server: this.con_num, msg: this.client.buffer.motd});
172 },
173 'RPL_NAMEREPLY': function (command) {
174 var members = command.trailing.split(' ');
175 var member_list = [];
176 var that = this;
177 var i = 0;
178 _.each(members, function (member) {
179 var j, k, modes = [];
180
181 // Make sure we have some prefixes already
182 if (that.irc_connection.options.PREFIX) {
183 for (j = 0; j < member.length; j++) {
184 for (k = 0; k < that.irc_connection.options.PREFIX.length; k++) {
185 if (member.charAt(j) === that.irc_connection.options.PREFIX[k].symbol) {
186 modes.push(that.irc_connection.options.PREFIX[k].mode);
187 i++;
188 }
189 }
190 }
191 }
192
193 member_list.push({nick: member, modes: modes});
194 if (i++ >= 50) {
195 that.client.sendIrcCommand('userlist', {server: that.con_num, users: member_list, channel: command.params[2]});
196 member_list = [];
197 i = 0;
198 }
199 });
200 if (i > 0) {
201 this.client.sendIrcCommand('userlist', {server: this.con_num, users: member_list, channel: command.params[2]});
202 }
203 },
204 'RPL_ENDOFNAMES': function (command) {
205 this.client.sendIrcCommand('userlist_end', {server: this.con_num, channel: command.params[1]});
206 },
207 'RPL_BANLIST': function (command) {
208 this.client.sendIrcCommand('banlist', {server: this.con_num, channel: command.params[1], banned: command.params[2], banned_by: command.params[3], banned_at: command.params[4]});
209 },
210 'RPL_ENDOFBANLIST': function (command) {
211 this.client.sendIrcCommand('banlist_end', {server: this.con_num, channel: command.params[1]});
212 },
213 'RPL_TOPIC': function (command) {
214 this.client.sendIrcCommand('topic', {server: this.con_num, nick: '', channel: command.params[1], topic: command.trailing});
215 },
216 'RPL_NOTOPIC': function (command) {
217 this.client.sendIrcCommand('topic', {server: this.con_num, nick: '', channel: command.params[1], topic: ''});
218 },
219 'RPL_TOPICWHOTIME': function (command) {
220 this.client.sendIrcCommand('topicsetby', {server: this.con_num, nick: command.params[2], channel: command.params[1], when: command.params[3]});
221 },
222 'PING': function (command) {
223 this.irc_connection.write('PONG ' + command.trailing);
224 },
225 'JOIN': function (command) {
226 var channel;
227 if (typeof command.trailing === 'string' && command.trailing !== '') {
228 channel = command.trailing;
229 } else if (typeof command.params[0] === 'string' && command.params[0] !== '') {
230 channel = command.params[0];
231 }
232
233 this.client.sendIrcCommand('join', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: channel});
234
235 if (command.nick === this.nick) {
236 this.irc_connection.write('NAMES ' + channel);
237 }
238 },
239 'PART': function (command) {
240 this.client.sendIrcCommand('part', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: command.params[0], message: command.trailing});
241 },
242 'KICK': function (command) {
243 this.client.sendIrcCommand('kick', {server: this.con_num, kicked: command.params[1], nick: command.nick, ident: command.ident, hostname: command.hostname, channel: command.params[0], message: command.trailing});
244 },
245 'QUIT': function (command) {
246 this.client.sendIrcCommand('quit', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, message: command.trailing});
247 },
248 'NOTICE': function (command) {
249 if ((command.trailing.charAt(0) === String.fromCharCode(1)) && (command.trailing.charAt(command.trailing.length - 1) === String.fromCharCode(1))) {
250 // It's a CTCP response
251 this.client.sendIrcCommand('ctcp_response', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: command.params[0], msg: command.trailing.substr(1, command.trailing.length - 2)});
252 } else {
253 this.client.sendIrcCommand('notice', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, target: command.params[0], msg: command.trailing});
254 }
255 },
256 'NICK': function (command) {
257 this.client.sendIrcCommand('nick', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, newnick: command.trailing || command.params[0]});
258 },
259 'TOPIC': function (command) {
260 // If we don't have an associated channel, no need to continue
261 if (!command.params[0]) return;
262
263 var channel = command.params[0],
264 topic = command.trailing || '';
265
266 this.client.sendIrcCommand('topic', {server: this.con_num, nick: command.nick, channel: channel, topic: topic});
267 },
268 'MODE': function (command) {
269 var chanmodes = this.irc_connection.options.CHANMODES || [],
270 prefixes = this.irc_connection.options.PREFIX || [],
271 always_param = (chanmodes[0] || '').concat((chanmodes[1] || '')),
272 modes = [],
273 has_param, i, j, add;
274
275 prefixes = _.reduce(prefixes, function (list, prefix) {
276 list.push(prefix.mode);
277 return list;
278 }, []);
279 always_param = always_param.split('').concat(prefixes);
280
281 has_param = function (mode, add) {
282 if (_.find(always_param, function (m) {
283 return m === mode;
284 })) {
285 return true;
286 } else if (add && _.find((chanmodes[2] || '').split(''), function (m) {
287 return m === mode;
288 })) {
289 return true;
290 } else {
291 return false;
292 }
293 };
294
295 if (!command.params[1]) {
296 command.params[1] = command.trailing;
297 }
298 j = 0;
299 for (i = 0; i < command.params[1].length; i++) {
300 switch (command.params[1][i]) {
301 case '+':
302 add = true;
303 break;
304 case '-':
305 add = false;
306 break;
307 default:
308 if (has_param(command.params[1][i], add)) {
309 modes.push({mode: (add ? '+' : '-') + command.params[1][i], param: command.params[2 + j]});
310 j++;
311 } else {
312 modes.push({mode: (add ? '+' : '-') + command.params[1][i], param: null});
313 }
314 }
315 }
316
317 this.client.sendIrcCommand('mode', {
318 server: this.con_num,
319 target: command.params[0],
320 nick: command.nick || command.prefix || '',
321 modes: modes
322 });
323 },
324 'PRIVMSG': function (command) {
325 var tmp, namespace;
326 if ((command.trailing.charAt(0) === String.fromCharCode(1)) && (command.trailing.charAt(command.trailing.length - 1) === String.fromCharCode(1))) {
327 //CTCP request
328 if (command.trailing.substr(1, 6) === 'ACTION') {
329 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)});
330 } else if (command.trailing.substr(1, 4) === 'KIWI') {
331 tmp = command.trailing.substr(6, command.trailing.length - 2);
332 namespace = tmp.split(' ', 1)[0];
333 this.client.sendIrcCommand('kiwi', {server: this.con_num, namespace: namespace, data: tmp.substr(namespace.length + 1)});
334 } else if (command.trailing.substr(1, 7) === 'VERSION') {
335 this.irc_connection.write('NOTICE ' + command.nick + ' :' + String.fromCharCode(1) + 'VERSION KiwiIRC' + String.fromCharCode(1));
336 } else if (command.trailing.substr(1, 6) === 'SOURCE') {
337 this.irc_connection.write('NOTICE ' + command.nick + ' :' + String.fromCharCode(1) + 'SOURCE http://www.kiwiirc.com/' + String.fromCharCode(1));
338 } else if (command.trailing.substr(1, 10) === 'CLIENTINFO') {
339 this.irc_connection.write('NOTICE ' + command.nick + ' :' + String.fromCharCode(1) + 'CLIENTINFO SOURCE VERSION TIME' + String.fromCharCode(1));
340 } else {
341 this.client.sendIrcCommand('ctcp_request', {
342 server: this.con_num,
343 nick: command.nick,
344 ident: command.ident,
345 hostname: command.hostname,
346 target: command.params[0],
347 type: (command.trailing.substr(1, command.trailing.length - 2).split(' ') || [null])[0],
348 msg: command.trailing.substr(1, command.trailing.length - 2)
349 });
350 }
351 } else {
352 //{nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing}
353 this.client.sendIrcCommand('msg', {server: this.con_num, nick: command.nick, ident: command.ident, hostname: command.hostname, channel: command.params[0], msg: command.trailing});
354 }
355 },
356 'CAP': function (command) {
357 // TODO: capability modifiers
358 // i.e. - for disable, ~ for requires ACK, = for sticky
359 var capabilities = command.trailing.replace(/[\-~=]/, '').split(' ');
360 var request;
361 var want = ['multi-prefix', 'away-notify'];
362
363 if (this.irc_connection.password) {
364 want.push('sasl');
365 }
366
367 switch (command.params[1]) {
368 case 'LS':
369 request = _.intersection(capabilities, want);
370 if (request.length > 0) {
371 this.irc_connection.cap.requested = request;
372 this.irc_connection.write('CAP REQ :' + request.join(' '));
373 } else {
374 this.irc_connection.write('CAP END');
375 this.irc_connection.cap_negotation = false;
376 }
377 break;
378 case 'ACK':
379 if (capabilities.length > 0) {
380 this.irc_connection.cap.enabled = capabilities;
381 this.irc_connection.cap.requested = _.difference(this.irc_connection.cap.requested, capabilities);
382 }
383 if (this.irc_connection.cap.requested.length > 0) {
384 if (_.contains(this.irc_connection.cap.enabled, 'sasl')) {
385 this.irc_connection.sasl = true;
386 this.irc_connection.write('AUTHENTICATE PLAIN');
387 } else {
388 this.irc_connection.write('CAP END');
389 this.irc_connection.cap_negotation = false;
390 }
391 }
392 break;
393 case 'NAK':
394 if (capabilities.length > 0) {
395 this.irc_connection.cap.requested = _.difference(this.irc_connection.cap.requested, capabilities);
396 }
397 if (this.irc_connection.cap.requested.length > 0) {
398 this.irc_connection.write('CAP END');
399 this.irc_connection.cap_negotation = false;
400 }
401 break;
402 case 'LIST':
403 // should we do anything here?
404 break;
405 }
406 },
407 'AUTHENTICATE': function (command) {
408 var b = new Buffer(this.irc_connection.nick + "\0" + this.irc_connection.nick + "\0" + this.irc_connection.password, 'utf8');
409 var b64 = b.toString('base64');
410 if (command.params[0] === '+') {
411 while (b64.length >= 400) {
412 this.irc_connection.write('AUTHENTICATE ' + b64.slice(0, 399));
413 b64 = b64.slice(399);
414 }
415 if (b64.length > 0) {
416 this.irc_connection.write('AUTHENTICATE ' + b64);
417 } else {
418 this.irc_connection.write('AUTHENTICATE +');
419 }
420 } else {
421 this.irc_connection.write('CAP END');
422 this.irc_connection.cap_negotation = false;
423 }
424 },
425 'AWAY': function (command) {
426 this.client.sendIrcCommand('away', {server: this.con_num, nick: command.nick, msg: command.trailing});
427 },
428 'RPL_SASLAUTHENTICATED': function (command) {
429 this.irc_connection.write('CAP END');
430 this.irc_connection.cap_negotation = false;
431 this.irc_connection.sasl = true;
432 },
433 'RPL_SASLLOGGEDIN': function (command) {
434 if (this.irc_connection.cap_negotation === false) {
435 this.irc_connection.write('CAP END');
436 }
437 },
438 'ERR_SASLNOTAUTHORISED': function (command) {
439 this.irc_connection.write('CAP END');
440 this.irc_connection.cap_negotation = false;
441 },
442 'ERR_SASLABORTED': function (command) {
443 this.irc_connection.write('CAP END');
444 this.irc_connection.cap_negotation = false;
445 },
446 'ERR_SASLALREADYAUTHED': function (command) {
447 // noop
448 },
449 'ERROR': function (command) {
450 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'error', reason: command.trailing});
451 },
452 ERR_LINKCHANNEL: function (command) {
453 this.client.sendIrcCommand('channel_redirect', {server: this.con_num, from: command.params[1], to: command.params[2]});
454 },
455 ERR_NOSUCHNICK: function (command) {
456 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'no_such_nick', nick: command.params[1], reason: command.trailing});
457 },
458 ERR_CANNOTSENDTOCHAN: function (command) {
459 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'cannot_send_to_chan', channel: command.params[1], reason: command.trailing});
460 },
461 ERR_TOOMANYCHANNELS: function (command) {
462 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'too_many_channels', channel: command.params[1], reason: command.trailing});
463 },
464 ERR_USERNOTINCHANNEL: function (command) {
465 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'user_not_in_channel', nick: command.params[0], channel: command.params[1], reason: command.trailing});
466 },
467 ERR_NOTONCHANNEL: function (command) {
468 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'not_on_channel', channel: command.params[1], reason: command.trailing});
469 },
470 ERR_CHANNELISFULL: function (command) {
471 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'channel_is_full', channel: command.params[1], reason: command.trailing});
472 },
473 ERR_INVITEONLYCHAN: function (command) {
474 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'invite_only_channel', channel: command.params[1], reason: command.trailing});
475 },
476 ERR_BANNEDFROMCHAN: function (command) {
477 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'banned_from_channel', channel: command.params[1], reason: command.trailing});
478 },
479 ERR_BADCHANNELKEY: function (command) {
480 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'bad_channel_key', channel: command.params[1], reason: command.trailing});
481 },
482 ERR_CHANOPRIVSNEEDED: function (command) {
483 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'chanop_privs_needed', channel: command.params[1], reason: command.trailing});
484 },
485 ERR_NICKNAMEINUSE: function (command) {
486 this.client.sendIrcCommand('irc_error', {server: this.con_num, error: 'nickname_in_use', nick: command.params[1], reason: command.trailing});
487 },
488 ERR_NOTREGISTERED: function (command) {
489 }
490 };