'312': 'RPL_WHOISSERVER',
'313': 'RPL_WHOISOPERATOR',
'314': 'RPL_WHOWASUSER',
+ '315': 'RPL_ENDOFWHO',
'317': 'RPL_WHOISIDLE',
'318': 'RPL_ENDOFWHOIS',
'319': 'RPL_WHOISCHANNELS',
'321': 'RPL_LISTSTART',
'322': 'RPL_LIST',
'323': 'RPL_LISTEND',
+ '324': 'RPL_CHANNELMODEIS',
+ '328': 'RPL_CHANNEL_URL',
+ '329': 'RPL_CREATIONTIME',
+ '330': 'RPL_WHOISACCOUNT',
'331': 'RPL_NOTOPIC',
'332': 'RPL_TOPIC',
'333': 'RPL_TOPICWHOTIME',
+ '341': 'RPL_INVITING',
+ '352': 'RPL_WHOREPLY',
'353': 'RPL_NAMEREPLY',
'364': 'RPL_LINKS',
'365': 'RPL_ENDOFLINKS',
'372': 'RPL_MOTD',
'375': 'RPL_MOTDSTART',
'376': 'RPL_ENDOFMOTD',
+ '378': 'RPL_WHOISHOST',
'379': 'RPL_WHOISMODES',
'401': 'ERR_NOSUCHNICK',
'404': 'ERR_CANNOTSENDTOCHAN',
'433': 'ERR_NICKNAMEINUSE',
'441': 'ERR_USERNOTINCHANNEL',
'442': 'ERR_NOTONCHANNEL',
+ '443': 'ERR_USERONCHANNEL',
'451': 'ERR_NOTREGISTERED',
'464': 'ERR_PASSWDMISMATCH',
'470': 'ERR_LINKCHANNEL',
'481': 'ERR_NOPRIVILEGES',
'482': 'ERR_CHANOPRIVSNEEDED',
'670': 'RPL_STARTTLS',
+ '671': 'RPL_WHOISSECURE',
'900': 'RPL_SASLAUTHENTICATED',
'903': 'RPL_SASLLOGGEDIN',
'904': 'ERR_SASLNOTAUTHORISED',
};
-IrcCommands = function (irc_connection, con_num, client) {
+IrcCommands = function (irc_connection) {
this.irc_connection = irc_connection;
- this.con_num = con_num;
- this.client = client;
};
module.exports = IrcCommands;
if (handlers[command]) {
handlers[command].call(this, data);
} else {
- unknownCommand(command, data);
+ unknownCommand.call(this, command, data);
}
};
};
unknownCommand = function (command, data) {
- // TODO: Do something here, log?
-};
+ var params = _.clone(data.params);
+
+ this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' unknown_command', {
+ command: command,
+ params: params,
+ trailing: data.trailing
+ });
+
+
+/*
+ this.irc_connection.emit(namespace + ' ' + command.params[0] + ' notice', {
+ from_server: command.prefix ? true : false,
+ nick: command.nick || command.prefix || undefined,
+ ident: command.ident,
+ hostname: command.hostname,
+ target: command.params[0],
+ msg: command.trailing
+ });
+ */
+ };
handlers = {
});
},
+ 'RPL_WHOISHOST': function (command) {
+ this.irc_connection.emit('user ' + command.params[1] + ' whoishost', {
+ nick: command.params[1],
+ msg: command.trailing
+ });
+ },
+
+ 'RPL_WHOISSECURE': function (command) {
+ this.irc_connection.emit('user ' + command.params[1] + ' whoissecure', {
+ nick: command.params[1]
+ });
+ },
+
+ 'RPL_WHOISACCOUNT': function (command) {
+ this.irc_connection.emit('user ' + command.params[1] + ' whoisaccount', {
+ nick: command.params[1],
+ account: command.params[2]
+ });
+ },
+
'RPL_WHOWASUSER': function (command) {
this.irc_connection.emit('user ' + command.params[1] + ' whowas', {
nick: command.params[1],
});
},
+ 'RPL_CHANNELMODEIS': function (command) {
+ var channel = command.params[1],
+ modes = parseModeList.call(this, command.params[2], command.params.slice(3));
+
+ this.irc_connection.emit('channel ' + channel + ' info', {
+ channel: channel,
+ modes: modes
+ });
+ },
+
+ 'RPL_CREATIONTIME': function (command) {
+ var channel = command.params[1];
+
+ this.irc_connection.emit('channel ' + channel + ' info', {
+ channel: channel,
+ created_at: parseInt(command.params[2], 10)
+ });
+ },
+
+ 'RPL_CHANNEL_URL': function (command) {
+ var channel = command.params[1];
+
+ this.irc_connection.emit('channel ' + channel + ' info', {
+ channel: channel,
+ url: command.trailing
+ });
+ },
+
'RPL_MOTD': function (command) {
this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' motd', {
motd: command.trailing + '\n'
var members = command.trailing.split(' ');
var member_list = [];
var that = this;
- var i = 0;
_.each(members, function (member) {
- var j, k, modes = [];
+ var i = 0,
+ j = 0,
+ modes = [];
// Make sure we have some prefixes already
if (that.irc_connection.options.PREFIX) {
- for (j = 0; j < member.length; j++) {
- for (k = 0; k < that.irc_connection.options.PREFIX.length; k++) {
- if (member.charAt(j) === that.irc_connection.options.PREFIX[k].symbol) {
- modes.push(that.irc_connection.options.PREFIX[k].mode);
- i++;
- }
+ for (j = 0; j < that.irc_connection.options.PREFIX.length; j++) {
+ if (member.charAt(i) === that.irc_connection.options.PREFIX[j].symbol) {
+ modes.push(that.irc_connection.options.PREFIX[j].mode);
+ i++;
}
}
}
});
},
+ 'RPL_WHOREPLY': function (command) {
+ // For the time being, NOOP this command so they don't get passed
+ // down to the client. Waste of bandwidth since we do not use it yet
+ // TODO: Impliment RPL_WHOREPLY
+ },
+
+ 'RPL_ENDOFWHO': function (command) {
+ // For the time being, NOOP this command so they don't get passed
+ // down to the client. Waste of bandwidth since we do not use it yet
+ // TODO: Impliment RPL_ENDOFWHO
+ },
+
'RPL_BANLIST': function (command) {
this.irc_connection.emit('channel ' + command.params[1] + ' banlist', {
channel: command.params[1],
});
},
+ 'RPL_INVITING': function (command) {
+ this.irc_connection.emit('channel ' + command.params[1] + ' invited', {
+ nick: command.params[0],
+ channel: command.params[1]
+ });
+ },
+
'PING': function (command) {
this.irc_connection.write('PONG ' + command.trailing);
},
'JOIN': function (command) {
- var channel;
+ var channel, time;
if (typeof command.trailing === 'string' && command.trailing !== '') {
channel = command.trailing;
} else if (typeof command.params[0] === 'string' && command.params[0] !== '') {
channel = command.params[0];
}
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+
this.irc_connection.emit('channel ' + channel + ' join', {
nick: command.nick,
ident: command.ident,
hostname: command.hostname,
- channel: channel
+ channel: channel,
+ time: time
});
},
'PART': function (command) {
+ var time;
+
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+
this.irc_connection.emit('channel ' + command.params[0] + ' part', {
nick: command.nick,
ident: command.ident,
hostname: command.hostname,
channel: command.params[0],
- message: command.trailing
+ message: command.trailing,
+ time: time
});
},
'KICK': function (command) {
+ var time;
+
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+
this.irc_connection.emit('channel ' + command.params[0] + ' kick', {
kicked: command.params[1],
nick: command.nick,
ident: command.ident,
hostname: command.hostname,
channel: command.params[0],
- message: command.trailing
+ message: command.trailing,
+ time: time
});
},
'QUIT': function (command) {
+ var time;
+
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+
this.irc_connection.emit('user ' + command.nick + ' quit', {
nick: command.nick,
ident: command.ident,
hostname: command.hostname,
- message: command.trailing
+ message: command.trailing,
+ time: time
});
},
'NOTICE': function (command) {
- var namespace;
+ var namespace,
+ time;
+
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+
if ((command.trailing.charAt(0) === String.fromCharCode(1)) && (command.trailing.charAt(command.trailing.length - 1) === String.fromCharCode(1))) {
// It's a CTCP response
- namespace = (command.params[0] == this.irc_connection.nick) ? 'user' : 'channel';
+ namespace = (command.params[0].toLowerCase() == this.irc_connection.nick.toLowerCase()) ? 'user' : 'channel';
this.irc_connection.emit(namespace + ' ' + command.params[0] + ' ctcp_response', {
nick: command.nick,
ident: command.ident,
hostname: command.hostname,
channel: command.params[0],
- msg: command.trailing.substr(1, command.trailing.length - 2)
+ msg: command.trailing.substring(1, command.trailing.length - 1),
+ time: time
});
} else {
- namespace = (command.params[0] == this.irc_connection.nick || command.params[0] == '*') ?
+ namespace = (command.params[0].toLowerCase() == this.irc_connection.nick.toLowerCase() || command.params[0] == '*') ?
'user' :
'channel';
ident: command.ident,
hostname: command.hostname,
target: command.params[0],
- msg: command.trailing
+ msg: command.trailing,
+ time: time
});
}
},
'NICK': function (command) {
+ var time;
+
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+
this.irc_connection.emit('user ' + command.nick + ' nick', {
nick: command.nick,
ident: command.ident,
hostname: command.hostname,
- newnick: command.trailing || command.params[0]
+ newnick: command.trailing || command.params[0],
+ time: time
});
},
'TOPIC': function (command) {
+ var time;
+
// If we don't have an associated channel, no need to continue
if (!command.params[0]) return;
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+
var channel = command.params[0],
topic = command.trailing || '';
this.irc_connection.emit('channel ' + channel + ' topic', {
nick: command.nick,
channel: channel,
- topic: topic
+ topic: topic,
+ time: time
});
},
'MODE': function (command) {
- var chanmodes = this.irc_connection.options.CHANMODES || [],
- prefixes = this.irc_connection.options.PREFIX || [],
- always_param = (chanmodes[0] || '').concat((chanmodes[1] || '')),
- modes = [],
- has_param, i, j, add, event;
-
- prefixes = _.reduce(prefixes, function (list, prefix) {
- list.push(prefix.mode);
- return list;
- }, []);
- always_param = always_param.split('').concat(prefixes);
-
- has_param = function (mode, add) {
- if (_.find(always_param, function (m) {
- return m === mode;
- })) {
- return true;
- } else if (add && _.find((chanmodes[2] || '').split(''), function (m) {
- return m === mode;
- })) {
- return true;
- } else {
- return false;
- }
- };
-
- if (!command.params[1]) {
- command.params[1] = command.trailing;
- }
+ var modes = [], event, time;
- j = 0;
- for (i = 0; i < command.params[1].length; i++) {
- switch (command.params[1][i]) {
- case '+':
- add = true;
- break;
- case '-':
- add = false;
- break;
- default:
- if (has_param(command.params[1][i], add)) {
- modes.push({mode: (add ? '+' : '-') + command.params[1][i], param: command.params[2 + j]});
- j++;
- } else {
- modes.push({mode: (add ? '+' : '-') + command.params[1][i], param: null});
- }
- }
- }
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+ // Get a JSON representation of the modes
+ modes = parseModeList.call(this, command.params[1] || command.trailing, command.params.slice(2));
event = (_.contains(this.irc_connection.options.CHANTYPES, command.params[0][0]) ? 'channel ' : 'user ') + command.params[0] + ' mode';
this.irc_connection.emit(event, {
target: command.params[0],
nick: command.nick || command.prefix || '',
- modes: modes
+ modes: modes,
+ time: time
});
},
'PRIVMSG': function (command) {
- var tmp, namespace;
+ var tmp, namespace, time;
+
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+
if ((command.trailing.charAt(0) === String.fromCharCode(1)) && (command.trailing.charAt(command.trailing.length - 1) === String.fromCharCode(1))) {
//CTCP request
if (command.trailing.substr(1, 6) === 'ACTION') {
- 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)});
+ this.irc_connection.clientEvent('action', {
+ nick: command.nick,
+ ident: command.ident,
+ hostname: command.hostname,
+ channel: command.params[0],
+ msg: command.trailing.substring(8, command.trailing.length - 1),
+ time: time
+ });
} else if (command.trailing.substr(1, 4) === 'KIWI') {
- tmp = command.trailing.substr(6, command.trailing.length - 2);
+ tmp = command.trailing.substring(6, command.trailing.length - 1);
namespace = tmp.split(' ', 1)[0];
- this.client.sendIrcCommand('kiwi', {server: this.con_num, namespace: namespace, data: tmp.substr(namespace.length + 1)});
+ this.irc_connection.clientEvent('kiwi', {
+ namespace: namespace,
+ data: tmp.substr(namespace.length + 1),
+ time: time
+ });
} else if (command.trailing.substr(1, 7) === 'VERSION') {
this.irc_connection.write('NOTICE ' + command.nick + ' :' + String.fromCharCode(1) + 'VERSION KiwiIRC' + String.fromCharCode(1));
} else if (command.trailing.substr(1, 6) === 'SOURCE') {
ident: command.ident,
hostname: command.hostname,
target: command.params[0],
- type: (command.trailing.substr(1, command.trailing.length - 2).split(' ') || [null])[0],
- msg: command.trailing.substr(1, command.trailing.length - 2)
+ type: (command.trailing.substring(1, command.trailing.length - 1).split(' ') || [null])[0],
+ msg: command.trailing.substring(1, command.trailing.length - 1),
+ time: time
});
}
} else {
// A message to a user (private message) or to a channel?
- namespace = (command.params[0] === this.irc_connection.nick) ? 'user ' + command.nick : 'channel ' + command.params[0];
+ namespace = (command.params[0].toLowerCase() == this.irc_connection.nick.toLowerCase()) ? 'user ' + command.nick : 'channel ' + command.params[0];
this.irc_connection.emit(namespace + ' privmsg', {
nick: command.nick,
ident: command.ident,
hostname: command.hostname,
channel: command.params[0],
- msg: command.trailing
+ msg: command.trailing,
+ time: time
});
}
},
'CAP': function (command) {
// TODO: capability modifiers
// i.e. - for disable, ~ for requires ACK, = for sticky
- var capabilities = command.trailing.replace(/[\-~=]/, '').split(' ');
+ var capabilities = command.trailing.replace(/(?:^| )[\-~=]/, '').split(' ');
var request;
// Which capabilities we want to enable
- var want = ['multi-prefix', 'away-notify'];
+ var want = ['multi-prefix', 'away-notify', 'server-time', 'znc.in/server-time-iso', 'znc.in/server-time'];
if (this.irc_connection.password) {
want.push('sasl');
},
'AWAY': function (command) {
+ var time;
+
+ // Check if we have a server-time
+ time = getServerTime.call(this, command);
+
this.irc_connection.emit('user ' + command.nick + ' away', {
nick: command.nick,
- msg: command.trailing
+ msg: command.trailing,
+ time: time
});
},
});
},
+ ERR_USERONCHANNEL: function (command) {
+ this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' user_on_channel', {
+ nick: command.params[1],
+ channel: command.params[2]
+ });
+ },
+
ERR_CHANNELISFULL: function (command) {
this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' channel_is_full', {
channel: command.params[1],
if (typeof is_error !== 'boolean')
is_error = true;
- this.client.sendIrcCommand('notice', {
- server: this.con_num,
+ this.irc_connection.clientEvent('notice', {
from_server: true,
nick: command.prefix,
ident: '',
msg: msg,
numeric: parseInt(command.command, 10)
});
-}
\ No newline at end of file
+}
+
+
+/**
+ * Convert a mode string such as '+k pass', or '-i' to a readable
+ * format.
+ * [ { mode: '+k', param: 'pass' } ]
+ * [ { mode: '-i', param: null } ]
+ */
+function parseModeList(mode_string, mode_params) {
+ var chanmodes = this.irc_connection.options.CHANMODES || [],
+ prefixes = this.irc_connection.options.PREFIX || [],
+ always_param = (chanmodes[0] || '').concat((chanmodes[1] || '')),
+ modes = [],
+ has_param, i, j, add;
+
+ prefixes = _.reduce(prefixes, function (list, prefix) {
+ list.push(prefix.mode);
+ return list;
+ }, []);
+ always_param = always_param.split('').concat(prefixes);
+
+ has_param = function (mode, add) {
+ if (_.find(always_param, function (m) {
+ return m === mode;
+ })) {
+ return true;
+ } else if (add && _.find((chanmodes[2] || '').split(''), function (m) {
+ return m === mode;
+ })) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ j = 0;
+ for (i = 0; i < mode_string.length; i++) {
+ switch (mode_string[i]) {
+ case '+':
+ add = true;
+ break;
+ case '-':
+ add = false;
+ break;
+ default:
+ if (has_param(mode_string[i], add)) {
+ modes.push({mode: (add ? '+' : '-') + mode_string[i], param: mode_params[j]});
+ j++;
+ } else {
+ modes.push({mode: (add ? '+' : '-') + mode_string[i], param: null});
+ }
+ }
+ }
+
+ return modes;
+}
+
+
+function getServerTime(command) {
+ var time;
+
+ // No tags? No times.
+ if (!command.tags || command.tags.length === 0) {
+ return time;
+ }
+
+ if (capContainsAny.call(this, ['server-time', 'znc.in/server-time', 'znc.in/server-time-iso'])) {
+ time = _.find(command.tags, function (tag) {
+ return tag.tag === 'time';
+ });
+
+ time = time ? time.value : undefined;
+
+ // Convert the time value to a unixtimestamp
+ if (typeof time === 'string') {
+ if (time.indexOf('T') > -1) {
+ time = parseISO8601(time);
+
+ } else if(time.match(/^[0-9.]+$/)) {
+ // A string formatted unix timestamp
+ time = new Date(time * 1000);
+ }
+
+ time = time.getTime();
+
+ } else if (typeof time === 'number') {
+ time = new Date(time * 1000);
+ time = time.getTime();
+ }
+ }
+
+ return time;
+}
+
+
+function capContainsAny (caps) {
+ var intersection;
+ if (!caps instanceof Array) {
+ caps = [caps];
+ }
+ intersection = _.intersection(this.irc_connection.cap.enabled, caps);
+ return intersection.length > 0;
+}
+
+
+// Code based on http://anentropic.wordpress.com/2009/06/25/javascript-iso8601-parser-and-pretty-dates/#comment-154
+function parseISO8601(str) {
+ if (Date.prototype.toISOString) {
+ return new Date(str);
+ } else {
+ var parts = str.split('T'),
+ dateParts = parts[0].split('-'),
+ timeParts = parts[1].split('Z'),
+ timeSubParts = timeParts[0].split(':'),
+ timeSecParts = timeSubParts[2].split('.'),
+ timeHours = Number(timeSubParts[0]),
+ _date = new Date();
+
+ _date.setUTCFullYear(Number(dateParts[0]));
+ _date.setUTCDate(1);
+ _date.setUTCMonth(Number(dateParts[1])-1);
+ _date.setUTCDate(Number(dateParts[2]));
+ _date.setUTCHours(Number(timeHours));
+ _date.setUTCMinutes(Number(timeSubParts[1]));
+ _date.setUTCSeconds(Number(timeSecParts[0]));
+ if (timeSecParts[1]) {
+ _date.setUTCMilliseconds(Number(timeSecParts[1]));
+ }
+
+ return _date;
+ }
+}