autojoin and send buffered messages

This commit is contained in:
Andris Reinman 2017-09-29 14:10:23 +03:00
parent 0c341f4b92
commit 05deaab46e

View file

@ -200,11 +200,13 @@ class IRCConnection extends EventEmitter {
let clear = () =>
cursor.close(() => {
if (this.dofetch) {
return setImmediate(() => this.fetchMessages(true));
} else {
this.fetching = false;
}
db.redis.hset('irclast', this.session.auth.id.toString(), this.lastFetchedItem.toString(), () => {
if (this.dofetch) {
return setImmediate(() => this.fetchMessages(true));
} else {
this.fetching = false;
}
});
});
let processNext = () => {
@ -231,7 +233,13 @@ class IRCConnection extends EventEmitter {
return setImmediate(processNext);
}
this.send({ source: message.nick, verb: 'PRIVMSG', target: message.channel ? message.channel.name : false, message: message.message });
this.send({
time: message.time,
source: message.nick,
verb: 'PRIVMSG',
target: message.channel ? message.channel.name : false,
message: message.message
});
setImmediate(processNext);
});
};
@ -245,19 +253,75 @@ class IRCConnection extends EventEmitter {
}
if (payload && typeof payload === 'object') {
payload.source = payload.source || this.hostname;
let message = [];
let message = [':' + payload.source];
let verb = (payload.verb || '')
.toString()
.toUpperCase()
.trim();
if (payload.verb) {
let cmd = (payload.verb || '')
.toString()
.toUpperCase()
.trim();
if (codes.has(cmd)) {
cmd = codes.get(cmd);
let tags = payload.tags;
if (tags && !Array.isArray(tags) && typeof tags === 'object') {
tags = Object.keys(tags || {}).forEach(key => ({
key,
value: tags[key]
}));
}
tags = [].concat(tags || []);
if (['PRIVMSG', 'NOTICE'].includes(verb.toUpperCase())) {
let time = payload.time ? (typeof payload.time !== 'object' ? new Date(payload.time) : payload.time) : new Date();
if (this.capEnabled.has('server-time')) {
tags.push({
key: 'time',
value: time.getISOString()
});
} else if (this.capEnabled.has('znc.in/server-time')) {
tags.push({
key: 't',
value: Math.round(time.getTime() / 1000)
});
}
message.push(cmd);
}
if (tags.length) {
let tagStr = tags
.map(tag => {
if (typeof tag.value === 'boolean') {
if (tag.value === true) {
return tag.key;
}
return;
}
return (
tag.key +
'=' +
(tag.value || '').toString().replace(/[;\r\n\\ ]/g, c => {
switch (c) {
case ';':
return '\\:';
case '\r':
return '\\r';
case '\n':
return '\\n';
case ' ':
return '\\s';
}
})
);
})
.join(';');
if (tagStr.length) {
message.push('@' + tagStr);
}
}
payload.source = payload.source || this.hostname;
message.push(':' + payload.source);
if (verb) {
message.push(codes.has(verb) ? codes.get(verb) : verb);
}
if (payload.target) {
@ -424,14 +488,49 @@ class IRCConnection extends EventEmitter {
return this.processQueue();
}
let match = line.match(/^\s*(?::[^\s]+\s+)?([^\s]+)\s*/);
let match = line.match(/^\s*(?:@([^\s]+)\s+)?(?::([^\s]+)\s+)?([^\s]+)\s*/);
if (!match) {
// TODO: send error message
// Can it even happen?
return this.processQueue();
}
let verb = (match[1] || '').toString().toUpperCase();
let tags = !match[1] ? false : new Map();
(match[1] || '')
.toString()
.split(';')
.forEach(elm => {
if (!elm) {
return;
}
let eqPos = elm.indexOf('=');
if (eqPos < 0) {
tags.set(elm, true);
return;
}
let key = elm.substr(0, eqPos);
let value = elm.substr(eqPos + 1).replace(/\\(.)/g, (m, c) => {
switch (c) {
case ':':
return ';';
case 's':
return ' ';
case '\\':
return '\\';
case 'r':
return '\r';
case 'n':
return '\n';
default:
return c;
}
});
tags.set(key, value);
});
let prefix = (match[3] || '').toString() || false;
let verb = (match[3] || '').toString().toUpperCase();
let params = line.substr(match[0].length);
let data;
let separatorPos = params.indexOf(' :');
@ -460,7 +559,7 @@ class IRCConnection extends EventEmitter {
);
if (typeof this['command_' + verb] === 'function') {
this['command_' + verb](params, () => {
this['command_' + verb](tags, prefix, params, () => {
this.processQueue();
});
} else {
@ -476,6 +575,59 @@ class IRCConnection extends EventEmitter {
}
}
printNickList(channelData, fresh, next) {
db.database
.collection('nicks')
.find({
user: { $in: channelData.members }
})
.project({
_id: true,
nick: true,
user: true
})
.toArray((err, nickList) => {
if (err) {
this.send({ verb: 'ERR_FILEERROR', params: channelData.channel, message: err.message });
return next();
}
if (!nickList) {
nickList = [];
}
if (fresh) {
nickList.unshift({
nick: this.session.nick
});
}
let lines = [];
let curLine = { members: [], length: 0 };
nickList.forEach(nickData => {
curLine.members.push(nickData.nick);
curLine.length += nickData.nick.length + 1;
if (curLine.length > 400) {
lines.push(curLine);
curLine = { members: [], length: 0 };
}
});
if (curLine.length) {
lines.push(curLine);
}
this.send({ source: this.getFormattedName(), verb: 'JOIN', target: false, message: channelData.channel });
lines.forEach(line => {
this.send({ verb: 'RPL_NAMREPLY', params: ['=', channelData.channel], message: line.members.join(' ') });
});
this.send({ verb: 'RPL_ENDOFNAMES', params: channelData.channel, message: 'End of /NAMES list' });
next();
});
}
getFormattedName(skipNick, nick) {
nick = nick || this.session.nick;
return (!skipNick && nick ? nick + '!' : '') + (this.session.user || 'unknown') + '@' + this.session.clientHostname;
@ -530,12 +682,71 @@ class IRCConnection extends EventEmitter {
verb: 'NOTICE',
message: 'This server requires all users to be authenticated. Identify via /msg NickServ identify <password>'
});
} else {
this.initializeSubscriptions();
}
this.starting = false;
this.started = true;
}
initializeSubscriptions(next) {
next = next || (() => false);
db.database
.collection('channels')
.find({
members: this.session.auth.id
})
.project({
_id: true,
channel: true,
members: true
})
.toArray((err, channels) => {
if (err) {
this.server.logger.error(
{
err,
tnx: 'setup',
cid: this.id,
user: this.session.auth.id
},
'Failed loading channels. %s',
err.message
);
return next();
}
if (Array.isArray(channels)) {
channels.forEach(channelData => {
this.server.logger.info(
{
tnx: 'setup',
cid: this.id,
channel: channelData._id
},
'Joining %s to channel %s',
this.session.auth.id,
channelData._id
);
this.subscribe([this.session.ns, '#', channelData._id].join('.'));
this.printNickList(channelData, false, next);
//this.send({ source: this.getFormattedName(), verb: 'JOIN', target: false, message: channelData.channel });
});
}
// private messages
this.subscribe([this.session.ns, '%', this.session.auth.id].join('.'));
// general notifications
this.subscribe([this.session.ns, '!', '*'].join('.'));
this.fetchMessages();
return next();
});
}
authenticate(user, password, next) {
this.server.userHandler.authenticate(
user.replace(/\+/, '@'),
@ -575,60 +786,37 @@ class IRCConnection extends EventEmitter {
}
let ns = userData.ns;
if (userData.address) {
let parts = userData.address.split('@');
this.session.user = parts.shift();
this.session.clientHostname = parts.join('@');
if (!ns) {
ns = this.session.clientHostname;
db.redis.hget('irclast', userData._id.toString(), (err, ircLast) => {
if (err) {
// ignore
}
}
this.session.nick = this.session.nick || userData.username;
this.session.ns = ns || 'root';
db.database
.collection('channels')
.find({
members: userData._id
})
.project({
_id: true,
channel: true
})
.toArray((err, channels) => {
if (err) {
return next(err);
if (ircLast) {
let lastFetchedItem = new ObjectID(ircLast);
let maxAge = Math.round(Date.now() / 1000 - 2 * 24 * 3600);
if (lastFetchedItem.getTimestamp().getTime() < maxAge * 1000) {
lastFetchedItem = new ObjectID(maxAge);
}
if (Array.isArray(channels)) {
channels.forEach(channelData => {
this.server.logger.info(
{
tnx: 'setup',
cid: this.id,
channel: channelData._id
},
'Joining %s to channel %s',
userData._id,
channelData._id
);
this.subscribe([this.session.ns, '#', channelData._id].join('.'));
this.send({ source: this.getFormattedName(), verb: 'JOIN', target: false, message: channelData.channel });
});
this.lastFetchedItem = lastFetchedItem;
}
if (userData.address) {
let parts = userData.address.split('@');
this.session.user = parts.shift();
this.session.clientHostname = parts.join('@');
if (!ns) {
ns = this.session.clientHostname;
}
}
// private messages
this.subscribe([this.session.ns, '%', userData._id].join('.'));
this.session.nick = this.session.nick || userData.username;
this.session.ns = ns || 'root';
// general notifications
this.subscribe([this.session.ns, '!', '*'].join('.'));
next(null, {
id: userData._id,
username: userData.username
});
next(null, {
id: userData._id,
username: userData.username
});
});
});
}
);
@ -796,7 +984,7 @@ class IRCConnection extends EventEmitter {
this.close();
}
command_PING(params, next) {
command_PING(tags, prefix, params, next) {
if (!params.length) {
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'PING', message: 'Not enough parameters' });
return next();
@ -810,11 +998,11 @@ class IRCConnection extends EventEmitter {
return next();
}
command_PONG(params, next) {
command_PONG(tags, prefix, params, next) {
return next();
}
command_NICK(params, next) {
command_NICK(tags, prefix, params, next) {
let currentSource = this.getFormattedName();
if (params.length > 1) {
@ -839,7 +1027,7 @@ class IRCConnection extends EventEmitter {
}
}
command_PASS(params, next) {
command_PASS(tags, prefix, params, next) {
if (!params.length) {
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'PASS', message: 'Not enough parameters' });
return next();
@ -852,7 +1040,7 @@ class IRCConnection extends EventEmitter {
return next();
}
command_USER(params, next) {
command_USER(tags, prefix, params, next) {
if (this.session.user) {
this.send({ verb: 'ERR_ALREADYREGISTERED', message: 'You may not reregister' });
return next();
@ -884,7 +1072,7 @@ class IRCConnection extends EventEmitter {
}
}
command_JOIN(params, next) {
command_JOIN(tags, prefix, params, next) {
if (!this.session.user || !this.session.nick) {
this.send({ verb: 'ERR_NOTREGISTERED', params: 'JOIN', message: 'You have not registered' });
return next();
@ -925,57 +1113,7 @@ class IRCConnection extends EventEmitter {
// notify other instances of self
this.publish([this.session.ns, '%', idString].join('.'), eventData);
db.database
.collection('nicks')
.find({
user: { $in: channelData.members }
})
.project({
_id: true,
nick: true,
user: true
})
.toArray((err, nickList) => {
if (err) {
this.send({ verb: 'ERR_FILEERROR', params: channelData.channel, message: err.message });
return next();
}
if (!nickList) {
nickList = [];
}
if (fresh) {
nickList.unshift({
nick: this.session.nick
});
}
let lines = [];
let curLine = { members: [], length: 0 };
nickList.forEach(nickData => {
curLine.members.push(nickData.nick);
curLine.length += nickData.nick.length + 1;
if (curLine.length > 400) {
lines.push(curLine);
curLine = { members: [], length: 0 };
}
});
if (curLine.length) {
lines.push(curLine);
}
this.send({ source: this.getFormattedName(), verb: 'JOIN', target: false, message: channelData.channel });
lines.forEach(line => {
this.send({ verb: 'RPL_NAMREPLY', params: ['=', channelData.channel], message: line.members.join(' ') });
});
this.send({ verb: 'RPL_ENDOFNAMES', params: channelData.channel, message: 'End of /NAMES list' });
next();
});
this.printNickList(channelData, fresh, next);
};
let tryCount = 0;
@ -1056,7 +1194,7 @@ class IRCConnection extends EventEmitter {
tryGetChannel();
}
command_PART(params, next) {
command_PART(tags, prefix, params, next) {
if (!this.session.user || !this.session.nick) {
this.send({ verb: 'ERR_NOTREGISTERED', params: 'JOIN', message: 'You have not registered' });
return next();
@ -1119,7 +1257,7 @@ class IRCConnection extends EventEmitter {
});
}
command_PRIVMSG(params, next) {
command_PRIVMSG(tags, prefix, params, next) {
if (!this.session.user || !this.session.nick) {
this.send({ verb: 'ERR_NOTREGISTERED', params: 'PRIVMSG', message: 'You have not registered' });
return next();
@ -1138,6 +1276,8 @@ class IRCConnection extends EventEmitter {
if (target.trim().toLowerCase() === 'nickserv') {
return this.command_NICKSERV(
tags,
prefix,
params
.slice(1)
.join(' ')
@ -1251,13 +1391,13 @@ class IRCConnection extends EventEmitter {
});
}
command_CAP(params, next) {
command_CAP(tags, prefix, params, next) {
if (!params.length) {
this.send({ verb: 'ERR_NEEDMOREPARAMS', target: this.session.nick || '*', params: 'CAP', message: 'Not enough parameters' });
return next();
}
let allowed = ['sasl'];
let allowed = ['sasl', 'server-time', 'znc.in/server-time'];
this.capStarted = true;
let subcommand = params
@ -1323,7 +1463,7 @@ class IRCConnection extends EventEmitter {
next();
}
command_NICKSERV(params, next) {
command_NICKSERV(tags, prefix, params, next) {
if (!params.length) {
this.send({ verb: 'ERR_NEEDMOREPARAMS', target: this.session.nick || '*', params: 'CAP', message: 'Not enough parameters' });
return next();
@ -1360,7 +1500,7 @@ class IRCConnection extends EventEmitter {
target: this.session.nick,
message: 'You are now identified for ' + this.session.user
});
return this.verifyNickChange(false, next);
return this.verifyNickChange(false, () => this.initializeSubscriptions(next));
} else {
this.send({
source: 'NickServ!NickServ@services.',
@ -1376,7 +1516,7 @@ class IRCConnection extends EventEmitter {
next();
}
command_AUTHENTICATE(params, next) {
command_AUTHENTICATE(tags, prefix, params, next) {
if (!params.length) {
this.send({ verb: 'ERR_NEEDMOREPARAMS', target: this.session.nick || '*', params: 'AUTHENTICATE', message: 'Not enough parameters' });
return next();