diff --git a/indexes.yaml b/indexes.yaml index 9daa5dd5..e138f303 100644 --- a/indexes.yaml +++ b/indexes.yaml @@ -369,19 +369,47 @@ indexes: # Indexes for IRC +- collection: chat + index: + name: ns_nicks + key: + _id: 1 + rcpt: 1 + - collection: nicks index: - name: irc_nicks + name: ns_nicks unique: true key: ns: 1 nickview: 1 + +- collection: nicks + index: + name: user_nick + key: user: 1 - collection: channels index: - name: irc_channels + name: ns_channels unique: true key: ns: 1 channelview: 1 + +- collection: channels + index: + name: channel_members + unique: true + key: + ns: 1 + channelview: 1 + members: 1 + +- collection: channels + index: + name: user_channel + unique: true + key: + members: 1 diff --git a/lib/irc/connection.js b/lib/irc/connection.js index e43758ea..a87aeb9c 100644 --- a/lib/irc/connection.js +++ b/lib/irc/connection.js @@ -122,6 +122,11 @@ class IRCConnection extends EventEmitter { break; } + + case 'topic': { + this.send({ time: data.topicTime, source: data.topicAuthor, verb: 'TOPIC', target: data.channel, message: data.topic }); + break; + } } }; } @@ -228,8 +233,8 @@ class IRCConnection extends EventEmitter { } this.lastFetchedItem = message._id; - if (message.channel && message.session.toString() === this.session.id.toString()) { - // ignore messages from self + if (message.channel && message.session.toString() === this.session.id.toString() && !this.capEnabled.has('echo-message')) { + // ignore messages from self unless echo-message return setImmediate(processNext); } @@ -334,7 +339,7 @@ class IRCConnection extends EventEmitter { message = message.concat(payload.params || []); } - if (payload.message) { + if (payload.message || typeof payload.message === 'string') { message.push(':' + payload.message); } @@ -542,7 +547,7 @@ class IRCConnection extends EventEmitter { .trim() .split(/\s+/) .filter(arg => arg); - if (data) { + if (data || typeof data === 'string') { params.push(data); } @@ -618,6 +623,12 @@ class IRCConnection extends EventEmitter { this.send({ source: this.getFormattedName(), verb: 'JOIN', target: false, message: channelData.channel }); + if (channelData.topic) { + let topicTime = Math.round((channelData.topicTime || new Date()).getTime() / 1000); + this.send({ verb: 'RPL_TOPIC', params: channelData.channel, message: channelData.topic }); + this.send({ verb: 'RPL_TOPICWHOTIME', params: [channelData.channel, channelData.topicAuthor, topicTime] }); + } + lines.forEach(line => { this.send({ verb: 'RPL_NAMREPLY', params: ['=', channelData.channel], message: line.members.join(' ') }); }); @@ -700,7 +711,10 @@ class IRCConnection extends EventEmitter { .project({ _id: true, channel: true, - members: true + members: true, + topic: true, + topicTime: true, + topicAuthor: true }) .toArray((err, channels) => { if (err) { @@ -1124,7 +1138,10 @@ class IRCConnection extends EventEmitter { }, { fields: { _id: true, - channel: true + channel: true, + topic: true, + topicTime: true, + topicAuthor: true } }, (err, channelData) => { if (err) { @@ -1166,6 +1183,7 @@ class IRCConnection extends EventEmitter { }); } + let time = new Date(); channelData = { _id: new ObjectID(), channel, @@ -1173,7 +1191,11 @@ class IRCConnection extends EventEmitter { ns: this.session.ns, mode: [], owner: this.session.auth.id, - members: [this.session.auth.id] + members: [this.session.auth.id], + time, + topic: '', + topicTime: time, + topicAuthor: '' }; db.database.collection('channels').insertOne(channelData, err => { @@ -1397,7 +1419,7 @@ class IRCConnection extends EventEmitter { return next(); } - let allowed = ['sasl', 'server-time', 'znc.in/server-time']; + let allowed = ['sasl', 'server-time', 'znc.in/server-time', 'echo-message']; this.capStarted = true; let subcommand = params @@ -1587,6 +1609,153 @@ class IRCConnection extends EventEmitter { return next(); } + + command_MODE(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(); + } + + let channel = params[0].trim(); + if (channel.length < 2 || !/^[#&]/.test(channel) || /[#&\s]/.test(channel.substr(1))) { + this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' }); + return next(); + } + + if (!this.checkAuth()) { + return next(); + } + + if (params.length > 1) { + this.send({ verb: 'ERR_CHANOPRIVSNEEDED', params: channel, message: 'You are not channel operator' }); + return next(); + } + + db.database.collection('channels').findOne({ + ns: this.session.ns, + channelview: channel.toLowerCase().replace(/\./g, '') + }, { + fields: { + _id: true, + mode: true, + time: true + } + }, (err, channelData) => { + if (err) { + this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message }); + return next(); + } + + if (!channelData) { + this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' }); + return next(); + } + + let channelTime = Math.round((channelData.time || new Date()).getTime() / 1000); + + this.send({ verb: 'RPL_CHANNELMODEIS', params: [channel, '+'] }); + this.send({ verb: 'RPL_CREATIONTIME', params: [channel, channelTime] }); + + return next(); + }); + } + + command_TOPIC(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(); + } + + let channel = params[0].trim(); + if (channel.length < 2 || !/^[#&]/.test(channel) || /[#&\s]/.test(channel.substr(1))) { + this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' }); + return next(); + } + + if (!this.checkAuth()) { + return next(); + } + + let newTopic = params + .slice(1) + .join(' ') + .trim(); + + if (params.length < 2) { + db.database.collection('channels').findOne({ + ns: this.session.ns, + channelview: channel.toLowerCase().replace(/\./g, '') + }, { + fields: { + _id: true, + topic: true, + topicTime: true, + topicAuthor: true + } + }, (err, channelData) => { + if (err) { + this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message }); + return next(); + } + + if (!channelData) { + this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' }); + return next(); + } + + if (!channelData.topic) { + this.send({ verb: 'RPL_NOTOPIC', params: channel, message: 'No topic is set' }); + return next(); + } + + let topicTime = Math.round((channelData.topicTime || new Date()).getTime() / 1000); + + this.send({ verb: 'RPL_TOPIC', params: channel, message: channelData.topic }); + this.send({ verb: 'RPL_TOPICWHOTIME', params: [channel, channelData.topicAuthor, topicTime] }); + + return next(); + }); + } else { + let topicTime = new Date(); + let topicAuthor = this.getFormattedName(); + + return db.database.collection('channels').findOneAndUpdate({ + ns: this.session.ns, + channelview: channel.toLowerCase().replace(/\./g, '') + }, { + $set: { + topic: newTopic, + topicAuthor, + topicTime + } + }, { + returnOriginal: false + }, (err, result) => { + if (err) { + this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message }); + return next(); + } + + if (!result || !result.value) { + this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'Could not open channel' }); + return next(); + } + + let channelData = result.value; + + this.publish([this.session.ns, '#', channelData._id].join('.'), { + action: 'topic', + channel: channelData.channel, + session: this.session.id.toString(), + channelId: channelData._id.toString(), + topic: newTopic, + topicAuthor, + topicTime + }); + next(); + }); + } + } } module.exports = IRCConnection;