From 17a63ec2ee8beaebb0a98b28adcbcd2ea00a20ed Mon Sep 17 00:00:00 2001 From: Alan Sikora Date: Wed, 20 Jun 2018 19:10:26 -0300 Subject: [PATCH] IRC Federation: RFC2813 implementation (ngIRCd) (#10113) * Initial progress * Direct messaging complete * Handle net-splits * Cleaned up logging * more cleanup, better log messages * Added support for IRC users to create rooms and invite RC users * Keep rooms in sync * IRC user can kick RC user * Working on transcription of coffescript to ecmascript code and fitting on the codebase. * Adds settings section for config the IRC Server bridge. * Working handles for direct messages * Working handles for direct messages * Working handles for direct messages * Working handles for direct messages * Working handles for direct messages * Working on RC server connection to a local IRC Network * first version, using a RFC2813 implementation * Fixing lint errors * Fixed partial username * Fixed problems with scope * removed parser name * Added a button to reset the IRC connection * Adjusted messages * Fixed IRC federation for new users * Ignore eslint about control character on regex * Adjusted settings strings --- .meteor/versions | 2 +- package.json | 1 + packages/rocketchat-i18n/i18n/en.i18n.json | 6 + .../rocketchat-irc/.npm/package/.gitignore | 1 + packages/rocketchat-irc/.npm/package/README | 7 + .../.npm/package/npm-shrinkwrap.json | 20 + packages/rocketchat-irc/package.js | 16 +- .../rocketchat-irc/server/irc-bridge/index.js | 138 +++ .../server/irc-bridge/localHandlers/index.js | 9 + .../irc-bridge/localHandlers/onCreateRoom.js | 15 + .../irc-bridge/localHandlers/onCreateUser.js | 32 + .../irc-bridge/localHandlers/onJoinRoom.js | 3 + .../irc-bridge/localHandlers/onLeaveRoom.js | 3 + .../irc-bridge/localHandlers/onLogin.js | 32 + .../irc-bridge/localHandlers/onLogout.js | 7 + .../irc-bridge/localHandlers/onSaveMessage.js | 32 + .../irc-bridge/peerHandlers/disconnected.js | 13 + .../server/irc-bridge/peerHandlers/index.js | 8 + .../irc-bridge/peerHandlers/joinedChannel.js | 22 + .../irc-bridge/peerHandlers/leftChannel.js | 18 + .../irc-bridge/peerHandlers/nickChanged.js | 19 + .../irc-bridge/peerHandlers/sentMessage.js | 82 ++ .../irc-bridge/peerHandlers/userRegistered.js | 42 + .../rocketchat-irc/server/irc-settings.js | 64 ++ packages/rocketchat-irc/server/irc.js | 24 + .../server/methods/resetIrcConnection.js | 52 ++ packages/rocketchat-irc/server/server.js | 439 --------- .../server/servers/RFC2813/codes.js | 519 +++++++++++ .../server/servers/RFC2813/index.js | 183 ++++ .../servers/RFC2813/localCommandHandlers.js | 73 ++ .../server/servers/RFC2813/parseMessage.js | 69 ++ .../servers/RFC2813/peerCommandHandlers.js | 112 +++ .../rocketchat-irc/server/servers/index.js | 3 + packages/rocketchat-irc/server/settings.js | 78 -- .../server/functions/createRoom.js | 3 + .../rocketchat-lib/server/models/Rooms.js | 4 + .../.app/package-lock.json | 874 ++++++++++++++++++ 37 files changed, 2500 insertions(+), 525 deletions(-) create mode 100644 packages/rocketchat-irc/.npm/package/.gitignore create mode 100644 packages/rocketchat-irc/.npm/package/README create mode 100644 packages/rocketchat-irc/.npm/package/npm-shrinkwrap.json create mode 100644 packages/rocketchat-irc/server/irc-bridge/index.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/localHandlers/index.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/localHandlers/onCreateRoom.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/localHandlers/onCreateUser.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/localHandlers/onJoinRoom.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/localHandlers/onLeaveRoom.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/localHandlers/onLogin.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/localHandlers/onLogout.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/localHandlers/onSaveMessage.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/disconnected.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/index.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/joinedChannel.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/leftChannel.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/nickChanged.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/sentMessage.js create mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/userRegistered.js create mode 100644 packages/rocketchat-irc/server/irc-settings.js create mode 100644 packages/rocketchat-irc/server/irc.js create mode 100644 packages/rocketchat-irc/server/methods/resetIrcConnection.js delete mode 100644 packages/rocketchat-irc/server/server.js create mode 100644 packages/rocketchat-irc/server/servers/RFC2813/codes.js create mode 100644 packages/rocketchat-irc/server/servers/RFC2813/index.js create mode 100644 packages/rocketchat-irc/server/servers/RFC2813/localCommandHandlers.js create mode 100644 packages/rocketchat-irc/server/servers/RFC2813/parseMessage.js create mode 100644 packages/rocketchat-irc/server/servers/RFC2813/peerCommandHandlers.js create mode 100644 packages/rocketchat-irc/server/servers/index.js delete mode 100644 packages/rocketchat-irc/server/settings.js create mode 100644 packages/rocketchat-livechat/.app/package-lock.json diff --git a/.meteor/versions b/.meteor/versions index 521b0c8e9cc3..c198fc771b99 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -171,7 +171,7 @@ rocketchat:importer-slack@0.0.1 rocketchat:importer-slack-users@1.0.0 rocketchat:integrations@0.0.1 rocketchat:internal-hubot@0.0.1 -rocketchat:irc@0.0.2 +rocketchat:irc@0.0.1 rocketchat:issuelinks@0.0.1 rocketchat:katex@0.0.1 rocketchat:lazy-load@0.0.1 diff --git a/package.json b/package.json index b12fd6cad56e..d1a92a39f1dc 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "poplib": "^0.1.7", "prom-client": "^11.0.0", "querystring": "^0.2.0", + "queue-fifo": "^0.2.4", "redis": "^2.8.0", "semver": "^5.5.0", "sharp": "^0.20.3", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index e494e6278dd6..758da563dd98 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -538,6 +538,8 @@ "Condensed": "Condensed", "Computer": "Computer", "Confirm_password": "Confirm your password", + "Connection_Closed" : "Connection closed", + "Connection_Reset" : "Connection reset", "Consulting": "Consulting", "Consumer_Goods": "Consumer Goods", "Contains_Security_Fixes": "Contains Security Fixes", @@ -1392,6 +1394,9 @@ "IRC_Channel_Users_End": "End of output of the NAMES command.", "IRC_Description": "Internet Relay Chat (IRC) is a text-based group communication tool. Users join uniquely named channels, or rooms, for open discussion. IRC also supports private messages between individual users and file sharing capabilities. This package integrates these layers of functionality with Rocket.Chat.", "IRC_Enabled": "Attempt to integrate IRC support. Changing this value requires restarting Rocket.Chat.", + "IRC_Enabled_Alert": "IRC Support is a work in progress. Use on a production system is not recommended at this time.", + "IRC_Federation": "IRC Federation", + "IRC_Federation_Disabled": "IRC Federation is disabled.", "IRC_Hostname": "The IRC host server to connect to.", "IRC_Login_Fail": "Output upon a failed connection to the IRC server.", "IRC_Login_Success": "Output upon a successful connection to the IRC server.", @@ -2058,6 +2063,7 @@ "Reset": "Reset", "Reset_password": "Reset password", "Reset_section_settings": "Reset Section Settings", + "Reset_Connection" : "Reset Connection", "Restart": "Restart", "Restart_the_server": "Restart the server", "Retail": "Retail", diff --git a/packages/rocketchat-irc/.npm/package/.gitignore b/packages/rocketchat-irc/.npm/package/.gitignore new file mode 100644 index 000000000000..3c3629e647f5 --- /dev/null +++ b/packages/rocketchat-irc/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/rocketchat-irc/.npm/package/README b/packages/rocketchat-irc/.npm/package/README new file mode 100644 index 000000000000..3d492553a438 --- /dev/null +++ b/packages/rocketchat-irc/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/rocketchat-irc/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-irc/.npm/package/npm-shrinkwrap.json new file mode 100644 index 000000000000..f6941bfcae39 --- /dev/null +++ b/packages/rocketchat-irc/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,20 @@ +{ + "lockfileVersion": 1, + "dependencies": { + "dbly-linked-list": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dbly-linked-list/-/dbly-linked-list-0.2.0.tgz", + "integrity": "sha512-Ool7y15f6JRDs0YKx7Dh9uiTb1jS1SZLNdT3Y2q16DlaEghXbMsmODS/XittjR2xztt1gJUpz7jVxpqAPF8VGg==" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, + "queue-fifo": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/queue-fifo/-/queue-fifo-0.2.4.tgz", + "integrity": "sha512-o2xWptfzdw4QLIozUUcRPnppoTNK+X1DxWGd8csnJ1gQUsATfQaDryaGB1MhAu1L48vqPMtH69PZ1kZD82zlVw==" + } + } +} diff --git a/packages/rocketchat-irc/package.js b/packages/rocketchat-irc/package.js index 65378f796430..6072d44d9f86 100644 --- a/packages/rocketchat-irc/package.js +++ b/packages/rocketchat-irc/package.js @@ -1,20 +1,22 @@ Package.describe({ name: 'rocketchat:irc', - version: '0.0.2', - summary: 'RocketChat libraries', + version: '0.0.1', + summary: 'RocketChat support for federating with IRC servers as a leaf node', git: '' }); Package.onUse(function(api) { api.use([ 'ecmascript', + 'underscore', 'rocketchat:lib' ]); - api.addFiles([ - 'server/settings.js', - 'server/server.js' - ], 'server'); + api.addFiles('server/irc.js', 'server'); + api.addFiles('server/methods/resetIrcConnection.js', 'server'); + api.addFiles('server/irc-settings.js', 'server'); +}); - api.export(['Irc'], ['server']); +Npm.depends({ + 'queue-fifo': '0.2.4' }); diff --git a/packages/rocketchat-irc/server/irc-bridge/index.js b/packages/rocketchat-irc/server/irc-bridge/index.js new file mode 100644 index 000000000000..be146555cefe --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/index.js @@ -0,0 +1,138 @@ +import Queue from 'queue-fifo'; +import * as servers from '../servers'; +import * as peerCommandHandlers from './peerHandlers'; +import * as localCommandHandlers from './localHandlers'; + +class Bridge { + constructor(config) { + // General + this.config = config; + + // Workaround for Rocket.Chat callbacks being called multiple times + this.loggedInUsers = []; + + // Server + const Server = servers[this.config.server.protocol]; + + this.server = new Server(this.config); + + this.setupPeerHandlers(); + this.setupLocalHandlers(); + + // Command queue + this.queue = new Queue(); + this.queueTimeout = 5; + } + + init() { + this.loggedInUsers = []; + this.server.register(); + + this.server.on('registered', () => { + this.logQueue('Starting...'); + + this.runQueue(); + }); + } + + stop() { + this.server.disconnect(); + } + + /** + * Log helper + */ + log(message) { + console.log(`[irc][bridge] ${ message }`); + } + + logQueue(message) { + console.log(`[irc][bridge][queue] ${ message }`); + } + + /** + * + * + * Queue + * + * + */ + onMessageReceived(from, command, ...parameters) { + this.queue.enqueue({ from, command, parameters }); + } + + async runQueue() { + // If it is empty, skip and keep the queue going + if (this.queue.isEmpty()) { + return setTimeout(this.runQueue.bind(this), this.queueTimeout); + } + + // Get the command + const item = this.queue.dequeue(); + + this.logQueue(`Processing "${ item.command }" command from "${ item.from }"`); + + // Handle the command accordingly + switch (item.from) { + case 'local': + if (!localCommandHandlers[item.command]) { + throw new Error(`Could not find handler for local:${ item.command }`); + } + + await localCommandHandlers[item.command].apply(this, item.parameters); + break; + case 'peer': + if (!peerCommandHandlers[item.command]) { + throw new Error(`Could not find handler for peer:${ item.command }`); + } + + await peerCommandHandlers[item.command].apply(this, item.parameters); + break; + } + + // Keep the queue going + setTimeout(this.runQueue.bind(this), this.queueTimeout); + } + + /** + * + * + * Peer + * + * + */ + setupPeerHandlers() { + this.server.on('peerCommand', (cmd) => { + this.onMessageReceived('peer', cmd.identifier, cmd.args); + }); + } + + /** + * + * + * Local + * + * + */ + setupLocalHandlers() { + // Auth + RocketChat.callbacks.add('afterValidateLogin', this.onMessageReceived.bind(this, 'local', 'onLogin'), RocketChat.callbacks.priority.LOW, 'irc-on-login'); + RocketChat.callbacks.add('afterCreateUser', this.onMessageReceived.bind(this, 'local', 'onCreateUser'), RocketChat.callbacks.priority.LOW, 'irc-on-create-user'); + // Joining rooms or channels + RocketChat.callbacks.add('afterCreateChannel', this.onMessageReceived.bind(this, 'local', 'onCreateRoom'), RocketChat.callbacks.priority.LOW, 'irc-on-create-channel'); + RocketChat.callbacks.add('afterCreateRoom', this.onMessageReceived.bind(this, 'local', 'onCreateRoom'), RocketChat.callbacks.priority.LOW, 'irc-on-create-room'); + RocketChat.callbacks.add('afterJoinRoom', this.onMessageReceived.bind(this, 'local', 'onJoinRoom'), RocketChat.callbacks.priority.LOW, 'irc-on-join-room'); + // Leaving rooms or channels + RocketChat.callbacks.add('afterLeaveRoom', this.onMessageReceived.bind(this, 'local', 'onLeaveRoom'), RocketChat.callbacks.priority.LOW, 'irc-on-leave-room'); + // Chatting + RocketChat.callbacks.add('afterSaveMessage', this.onMessageReceived.bind(this, 'local', 'onSaveMessage'), RocketChat.callbacks.priority.LOW, 'irc-on-save-message'); + // Leaving + RocketChat.callbacks.add('afterLogoutCleanUp', this.onMessageReceived.bind(this, 'local', 'onLogout'), RocketChat.callbacks.priority.LOW, 'irc-on-logout'); + } + + sendCommand(command, parameters) { + this.server.emit('onReceiveFromLocal', command, parameters); + } +} + +export default Bridge; diff --git a/packages/rocketchat-irc/server/irc-bridge/localHandlers/index.js b/packages/rocketchat-irc/server/irc-bridge/localHandlers/index.js new file mode 100644 index 000000000000..fcc046ebfcd2 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/localHandlers/index.js @@ -0,0 +1,9 @@ +import onCreateRoom from './onCreateRoom'; +import onJoinRoom from './onJoinRoom'; +import onLeaveRoom from './onLeaveRoom'; +import onLogin from './onLogin'; +import onLogout from './onLogout'; +import onSaveMessage from './onSaveMessage'; +import onCreateUser from './onCreateUser'; + +export { onCreateRoom, onJoinRoom, onLeaveRoom, onLogin, onLogout, onSaveMessage, onCreateUser }; diff --git a/packages/rocketchat-irc/server/irc-bridge/localHandlers/onCreateRoom.js b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onCreateRoom.js new file mode 100644 index 000000000000..7ca1dc2614de --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onCreateRoom.js @@ -0,0 +1,15 @@ +export default function handleOnCreateRoom(user, room) { + if (!room.usernames) { + return this.log(`Room ${ room.name } does not have a valid list of usernames`); + } + + for (const username of room.usernames) { + const user = RocketChat.models.Users.findOne({ username }); + + if (user.profile.irc.fromIRC) { + this.sendCommand('joinChannel', { room, user }); + } else { + this.sendCommand('joinedChannel', { room, user }); + } + } +} diff --git a/packages/rocketchat-irc/server/irc-bridge/localHandlers/onCreateUser.js b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onCreateUser.js new file mode 100644 index 000000000000..ca4366a505ed --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onCreateUser.js @@ -0,0 +1,32 @@ +export default function handleOnCreateUser(newUser) { + if (!newUser) { + return this.log('Invalid handleOnCreateUser call'); + } + if (!newUser.username) { + return this.log('Invalid handleOnCreateUser call (Missing username)'); + } + if (this.loggedInUsers.indexOf(newUser._id) !== -1) { + return this.log('Duplicate handleOnCreateUser call'); + } + + this.loggedInUsers.push(newUser._id); + + Meteor.users.update({ _id: newUser._id }, { + $set: { + 'profile.irc.fromIRC': false, + 'profile.irc.username': `${ newUser.username }-rkt`, + 'profile.irc.nick': `${ newUser.username }-rkt`, + 'profile.irc.hostname': 'rocket.chat' + } + }); + + const user = RocketChat.models.Users.findOne({ + _id: newUser._id + }); + + this.sendCommand('registerUser', user); + + const rooms = RocketChat.models.Rooms.findWithUsername(user.username).fetch(); + + rooms.forEach(room => this.sendCommand('joinedChannel', { room, user })); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/localHandlers/onJoinRoom.js b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onJoinRoom.js new file mode 100644 index 000000000000..34da322701e8 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onJoinRoom.js @@ -0,0 +1,3 @@ +export default function handleOnJoinRoom(user, room) { + this.sendCommand('joinedChannel', { room, user }); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/localHandlers/onLeaveRoom.js b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onLeaveRoom.js new file mode 100644 index 000000000000..32775f42dd97 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onLeaveRoom.js @@ -0,0 +1,3 @@ +export default function handleOnLeaveRoom(user, room) { + this.sendCommand('leftChannel', { room, user }); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/localHandlers/onLogin.js b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onLogin.js new file mode 100644 index 000000000000..7b4f918a7021 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onLogin.js @@ -0,0 +1,32 @@ +export default function handleOnLogin(login) { + if (login.user === null) { + return this.log('Invalid handleOnLogin call'); + } + if (!login.user.username) { + return this.log('Invalid handleOnLogin call (Missing username)'); + } + if (this.loggedInUsers.indexOf(login.user._id) !== -1) { + return this.log('Duplicate handleOnLogin call'); + } + + this.loggedInUsers.push(login.user._id); + + Meteor.users.update({ _id: login.user._id }, { + $set: { + 'profile.irc.fromIRC': false, + 'profile.irc.username': `${ login.user.username }-rkt`, + 'profile.irc.nick': `${ login.user.username }-rkt`, + 'profile.irc.hostname': 'rocket.chat' + } + }); + + const user = RocketChat.models.Users.findOne({ + _id: login.user._id + }); + + this.sendCommand('registerUser', user); + + const rooms = RocketChat.models.Rooms.findWithUsername(user.username).fetch(); + + rooms.forEach(room => this.sendCommand('joinedChannel', { room, user })); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/localHandlers/onLogout.js b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onLogout.js new file mode 100644 index 000000000000..a2669285ae9e --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onLogout.js @@ -0,0 +1,7 @@ +import _ from 'underscore'; + +export default function handleOnLogout(user) { + this.loggedInUsers = _.without(this.loggedInUsers, user._id); + + this.sendCommand('disconnected', { user }); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/localHandlers/onSaveMessage.js b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onSaveMessage.js new file mode 100644 index 000000000000..ebfea53dc1b8 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/localHandlers/onSaveMessage.js @@ -0,0 +1,32 @@ +export default function handleOnSaveMessage(message, to) { + let toIdentification = ''; + // Direct message + if (to.t === 'd') { + const subscriptions = RocketChat.models.Subscriptions.findByRoomId(to._id); + subscriptions.forEach((subscription) => { + if (subscription.u.username !== to.username) { + const userData = RocketChat.models.Users.findOne({ username: subscription.u.username }); + if (userData) { + if (userData.profile && userData.profile.irc && userData.profile.irc.nick) { + toIdentification = userData.profile.irc.nick; + } else { + toIdentification = userData.username; + } + } else { + toIdentification = subscription.u.username; + } + } + }); + + if (!toIdentification) { + console.error('[irc][server] Target user not found'); + return; + } + } else { + toIdentification = `#${ to.name }`; + } + + const user = RocketChat.models.Users.findOne({ _id: message.u._id }); + + this.sendCommand('sentMessage', { to: toIdentification, user, message: message.msg }); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/peerHandlers/disconnected.js b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/disconnected.js new file mode 100644 index 000000000000..86c82e926342 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/disconnected.js @@ -0,0 +1,13 @@ +export default function handleQUIT(args) { + const user = RocketChat.models.Users.findOne({ + 'profile.irc.nick': args.nick + }); + + Meteor.users.update({ _id: user._id }, { + $set: { + status: 'offline' + } + }); + + RocketChat.models.Rooms.removeUsernameFromAll(user.username); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/peerHandlers/index.js b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/index.js new file mode 100644 index 000000000000..00397f667f68 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/index.js @@ -0,0 +1,8 @@ +import disconnected from './disconnected'; +import joinedChannel from './joinedChannel'; +import leftChannel from './leftChannel'; +import nickChanged from './nickChanged'; +import sentMessage from './sentMessage'; +import userRegistered from './userRegistered'; + +export { disconnected, joinedChannel, leftChannel, nickChanged, sentMessage, userRegistered }; diff --git a/packages/rocketchat-irc/server/irc-bridge/peerHandlers/joinedChannel.js b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/joinedChannel.js new file mode 100644 index 000000000000..1dfae5873f29 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/joinedChannel.js @@ -0,0 +1,22 @@ +export default function handleJoinedChannel(args) { + const user = RocketChat.models.Users.findOne({ + 'profile.irc.nick': args.nick + }); + + if (!user) { + throw new Error(`Could not find a user with nick ${ args.nick }`); + } + + let room = RocketChat.models.Rooms.findOneByName(args.roomName); + + if (!room) { + const createdRoom = RocketChat.createRoom('c', args.roomName, user.username, [ /* usernames of the participants here */]); + room = RocketChat.models.Rooms.findOne({ _id: createdRoom.rid }); + + this.log(`${ user.username } created room ${ args.roomName }`); + } else { + RocketChat.addUserToRoom(room._id, user); + + this.log(`${ user.username } joined room ${ room.name }`); + } +} diff --git a/packages/rocketchat-irc/server/irc-bridge/peerHandlers/leftChannel.js b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/leftChannel.js new file mode 100644 index 000000000000..c8065fe3b110 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/leftChannel.js @@ -0,0 +1,18 @@ +export default function handleLeftChannel(args) { + const user = RocketChat.models.Users.findOne({ + 'profile.irc.nick': args.nick + }); + + if (!user) { + throw new Error(`Could not find a user with nick ${ args.nick }`); + } + + const room = RocketChat.models.Rooms.findOneByName(args.roomName); + + if (!room) { + throw new Error(`Could not find a room with name ${ args.roomName }`); + } + + this.log(`${ user.username } left room ${ room.name }`); + RocketChat.removeUserFromRoom(room._id, user); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/peerHandlers/nickChanged.js b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/nickChanged.js new file mode 100644 index 000000000000..9b09f369ce92 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/nickChanged.js @@ -0,0 +1,19 @@ +export default function handleNickChanged(args) { + const user = RocketChat.models.Users.findOne({ + 'profile.irc.nick': args.nick + }); + + if (!user) { + throw new Error(`Could not find an user with nick ${ args.nick }`); + } + + this.log(`${ user.username } changed nick: ${ args.nick } -> ${ args.newNick }`); + + // Update on the database + RocketChat.models.Users.update({ _id: user._id }, { + $set: { + name: args.newNick, + 'profile.irc.nick': args.newNick + } + }); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/peerHandlers/sentMessage.js b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/sentMessage.js new file mode 100644 index 000000000000..e0649026a72e --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/sentMessage.js @@ -0,0 +1,82 @@ +/* + * + * Get direct chat room helper + * + * + */ +const getDirectRoom = (source, target) => { + const rid = [ source._id, target._id ].sort().join(''); + + RocketChat.models.Rooms.upsert({ _id: rid }, { + $set: { + usernames: [source.username, target.username] + }, + $setOnInsert: { + t: 'd', + msgs: 0, + ts: new Date() + } + }); + + RocketChat.models.Subscriptions.upsert({rid, 'u._id': target._id}, { + $setOnInsert: { + name: source.username, + t: 'd', + open: false, + alert: false, + unread: 0, + u: { + _id: target._id, + username: target.username + } + } + }); + + RocketChat.models.Subscriptions.upsert({rid, 'u._id': source._id}, { + $setOnInsert: { + name: target.username, + t: 'd', + open: false, + alert: false, + unread: 0, + u: { + _id: source._id, + username: source.username + } + } + }); + + return { + _id: rid, + t: 'd' + }; +}; + +export default function handleSentMessage(args) { + const user = RocketChat.models.Users.findOne({ + 'profile.irc.nick': args.nick + }); + + if (!user) { + throw new Error(`Could not find a user with nick ${ args.nick }`); + } + + let room; + + if (args.roomName) { + room = RocketChat.models.Rooms.findOneByName(args.roomName); + } else { + const recipientUser = RocketChat.models.Users.findOne({ + 'profile.irc.nick': args.recipientNick + }); + + room = getDirectRoom(user, recipientUser); + } + + const message = { + msg: args.message, + ts: new Date() + }; + + RocketChat.sendMessage(user, message, room); +} diff --git a/packages/rocketchat-irc/server/irc-bridge/peerHandlers/userRegistered.js b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/userRegistered.js new file mode 100644 index 000000000000..d87d8764a10b --- /dev/null +++ b/packages/rocketchat-irc/server/irc-bridge/peerHandlers/userRegistered.js @@ -0,0 +1,42 @@ +export default async function handleUserRegistered(args) { + // Check if there is an user with the given username + let user = RocketChat.models.Users.findOne({ + 'profile.irc.username': args.username + }); + + // If there is no user, create one... + if (!user) { + this.log(`Registering ${ args.username } with nick: ${ args.nick }`); + + const userToInsert = { + name: args.nick, + username: `${ args.username }-irc`, + status: 'online', + utcOffset: 0, + active: true, + type: 'user', + profile: { + irc: { + fromIRC: true, + nick: args.nick, + username: args.username, + hostname: args.hostname + } + } + }; + + user = RocketChat.models.Users.create(userToInsert); + } else { + // ...otherwise, log the user in and update the information + this.log(`Logging in ${ args.username } with nick: ${ args.nick }`); + + Meteor.users.update({ _id: user._id }, { + $set: { + status: 'online', + 'profile.irc.nick': args.nick, + 'profile.irc.username': args.username, + 'profile.irc.hostname': args.hostname + } + }); + } +} diff --git a/packages/rocketchat-irc/server/irc-settings.js b/packages/rocketchat-irc/server/irc-settings.js new file mode 100644 index 000000000000..f6543891cac7 --- /dev/null +++ b/packages/rocketchat-irc/server/irc-settings.js @@ -0,0 +1,64 @@ +Meteor.startup(function() { + RocketChat.settings.addGroup('IRC_Federation', function() { + this.add('IRC_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enabled', + i18nDescription: 'IRC_Enabled', + alert: 'IRC_Enabled_Alert' + }); + + this.add('IRC_Protocol', 'RFC2813', { + type: 'select', + i18nLabel: 'Protocol', + i18nDescription: 'IRC_Protocol', + values: [ + { + key: 'RFC2813', + i18nLabel: 'RFC2813' + } + ] + }); + + this.add('IRC_Host', 'localhost', { + type: 'string', + i18nLabel: 'Host', + i18nDescription: 'IRC_Host' + }); + + this.add('IRC_Port', 6667, { + type: 'int', + i18nLabel: 'Port', + i18nDescription: 'IRC_Port' + }); + + this.add('IRC_Name', 'irc.rocket.chat', { + type: 'string', + i18nLabel: 'Name', + i18nDescription: 'IRC_Name' + }); + + this.add('IRC_Description', 'Rocket.Chat IRC Bridge', { + type: 'string', + i18nLabel: 'Description', + i18nDescription: 'IRC_Description' + }); + + this.add('IRC_Local_Password', 'password', { + type: 'string', + i18nLabel: 'Local_Password', + i18nDescription: 'IRC_Local_Password' + }); + + this.add('IRC_Peer_Password', 'password', { + type: 'string', + i18nLabel: 'Peer_Password', + i18nDescription: 'IRC_Peer_Password' + }); + + this.add('IRC_Reset_Connection', 'resetIrcConnection', { + type: 'action', + actionText: 'Reset_Connection', + i18nLabel: 'Reset_Connection' + }); + }); +}); diff --git a/packages/rocketchat-irc/server/irc.js b/packages/rocketchat-irc/server/irc.js new file mode 100644 index 000000000000..0b1a742f0a3a --- /dev/null +++ b/packages/rocketchat-irc/server/irc.js @@ -0,0 +1,24 @@ +import Bridge from './irc-bridge'; + +if (!!RocketChat.settings.get('IRC_Enabled') === true) { + // Normalize the config values + const config = { + server: { + protocol: RocketChat.settings.get('IRC_Protocol'), + host: RocketChat.settings.get('IRC_Host'), + port: RocketChat.settings.get('IRC_Port'), + name: RocketChat.settings.get('IRC_Name'), + description: RocketChat.settings.get('IRC_Description') + }, + passwords: { + local: RocketChat.settings.get('IRC_Local_Password'), + peer: RocketChat.settings.get('IRC_Peer_Password') + } + }; + + Meteor.ircBridge = new Bridge(config); + + Meteor.startup(() => { + Meteor.ircBridge.init(); + }); +} diff --git a/packages/rocketchat-irc/server/methods/resetIrcConnection.js b/packages/rocketchat-irc/server/methods/resetIrcConnection.js new file mode 100644 index 000000000000..8cb3b0695c7c --- /dev/null +++ b/packages/rocketchat-irc/server/methods/resetIrcConnection.js @@ -0,0 +1,52 @@ +import Bridge from '../irc-bridge'; + +Meteor.methods({ + resetIrcConnection() { + const ircEnabled = (!!RocketChat.settings.get('IRC_Enabled')) === true; + + if (Meteor.ircBridge) { + Meteor.ircBridge.stop(); + if (!ircEnabled) { + return { + message: 'Connection_Closed', + params: [] + }; + } + } + + if (ircEnabled) { + if (Meteor.ircBridge) { + Meteor.ircBridge.init(); + return { + message: 'Connection_Reset', + params: [] + }; + } + + // Normalize the config values + const config = { + server: { + protocol: RocketChat.settings.get('IRC_Protocol'), + host: RocketChat.settings.get('IRC_Host'), + port: RocketChat.settings.get('IRC_Port'), + name: RocketChat.settings.get('IRC_Name'), + description: RocketChat.settings.get('IRC_Description') + }, + passwords: { + local: RocketChat.settings.get('IRC_Local_Password'), + peer: RocketChat.settings.get('IRC_Peer_Password') + } + }; + + Meteor.ircBridge = new Bridge(config); + Meteor.ircBridge.init(); + + return { + message: 'Connection_Reset', + params: [] + }; + } + + throw new Meteor.Error(t('IRC_Federation_Disabled')); + } +}); diff --git a/packages/rocketchat-irc/server/server.js b/packages/rocketchat-irc/server/server.js deleted file mode 100644 index bf3814f7f7b8..000000000000 --- a/packages/rocketchat-irc/server/server.js +++ /dev/null @@ -1,439 +0,0 @@ -import _ from 'underscore'; -import net from 'net'; -import Lru from 'lru-cache'; - -/////// -// Assign values - -//Package availability -const IRC_AVAILABILITY = RocketChat.settings.get('IRC_Enabled'); - -// Cache prep -const MESSAGE_CACHE_SIZE = RocketChat.settings.get('IRC_Message_Cache_Size'); -const ircReceiveMessageCache = Lru(MESSAGE_CACHE_SIZE);//eslint-disable-line -const ircSendMessageCache = Lru(MESSAGE_CACHE_SIZE);//eslint-disable-line - -// IRC server -const IRC_PORT = RocketChat.settings.get('IRC_Port'); -const IRC_HOST = RocketChat.settings.get('IRC_Host'); - -const ircClientMap = {}; - -////// -// Core functionality - -const bind = function(f) { - const g = Meteor.bindEnvironment((self, ...args) => f.apply(self, args)); - return function(...args) { g(this, ...args); }; -}; - -const async = (f, ...args) => Meteor.wrapAsync(f)(...args); - -class IrcClient { - constructor(loginReq) { - this.loginReq = loginReq; - - this.user = this.loginReq.user; - ircClientMap[this.user._id] = this; - this.ircPort = IRC_PORT; - this.ircHost = IRC_HOST; - this.msgBuf = []; - - this.isConnected = false; - this.isDistroyed = false; - this.socket = new net.Socket; - this.socket.setNoDelay; - this.socket.setEncoding('utf-8'); - this.socket.setKeepAlive(true); - this.connect = this.connect.bind(this); - this.onConnect = this.onConnect.bind(this); - this.onConnect = bind(this.onConnect); - this.onClose = bind(this.onClose); - this.onTimeout = bind(this.onTimeout); - this.onError = bind(this.onError); - this.onReceiveRawMessage = this.onReceiveRawMessage.bind(this); - this.onReceiveRawMessage = bind(this.onReceiveRawMessage); - this.socket.on('data', this.onReceiveRawMessage); - this.socket.on('close', this.onClose); - this.socket.on('timeout', this.onTimeout); - this.socket.on('error', this.onError); - - this.isJoiningRoom = false; - this.receiveMemberListBuf = {}; - this.pendingJoinRoomBuf = []; - - this.successLoginMessageRegex = new RegExp(RocketChat.settings.get('IRC_RegEx_successLogin')); - this.failedLoginMessageRegex = new RegExp(RocketChat.settings.get('IRC_RegEx_failedLogin')); - this.receiveMessageRegex = new RegExp(RocketChat.settings.get('IRC_RegEx_receiveMessage')); - this.receiveMemberListRegex = new RegExp(RocketChat.settings.get('IRC_RegEx_receiveMemberList')); - this.endMemberListRegex = new RegExp(RocketChat.settings.get('IRC_RegEx_endMemberList')); - this.addMemberToRoomRegex = new RegExp(RocketChat.settings.get('IRC_RegEx_addMemberToRoom')); - this.removeMemberFromRoomRegex = new RegExp(RocketChat.settings.get('IRC_RegEx_removeMemberFromRoom')); - this.quitMemberRegex = new RegExp(RocketChat.settings.get('IRC_RegEx_quitMember')); - } - - connect(loginCb) { - this.loginCb = loginCb; - this.socket.connect(this.ircPort, this.ircHost, this.onConnect); - this.initRoomList(); - } - - disconnect() { - this.isDistroyed = true; - this.socket.destroy(); - } - - onConnect() { - console.log('[irc] onConnect -> '.yellow, this.user.username, 'connect success.'); - this.socket.write(`NICK ${ this.user.username }\r\n`); - this.socket.write(`USER ${ this.user.username } 0 * :${ this.user.name }\r\n`); - // message order could not make sure here - this.isConnected = true; - const messageBuf = this.msgBuf; - messageBuf.forEach(msg => this.socket.write(msg)); - } - - onClose() { - console.log('[irc] onClose -> '.yellow, this.user.username, 'connection close.'); - this.isConnected = false; - if (this.isDistroyed) { - delete ircClientMap[this.user._id]; - } else { - this.connect(); - } - } - - onTimeout() { - console.log('[irc] onTimeout -> '.yellow, this.user.username, 'connection timeout.', arguments); - } - - onError() { - console.log('[irc] onError -> '.yellow, this.user.username, 'connection error.', arguments); - } - - onReceiveRawMessage(data) { - data = data.toString().split('\n'); - - data.forEach(line => { - line = line.trim(); - console.log(`[${ this.ircHost }:${ this.ircPort }]:`, line); - - // Send heartbeat package to irc server - if (line.indexOf('PING') === 0) { - this.socket.write(line.replace('PING :', 'PONG ')); - return; - } - let matchResult = this.receiveMessageRegex.exec(line); - if (matchResult) { - this.onReceiveMessage(matchResult[1], matchResult[2], matchResult[3]); - return; - } - matchResult = this.receiveMemberListRegex.exec(line); - if (matchResult) { - this.onReceiveMemberList(matchResult[1], matchResult[2].split(' ')); - return; - } - matchResult = this.endMemberListRegex.exec(line); - if (matchResult) { - this.onEndMemberList(matchResult[1]); - return; - } - matchResult = this.addMemberToRoomRegex.exec(line); - if (matchResult) { - this.onAddMemberToRoom(matchResult[1], matchResult[2]); - return; - } - matchResult = this.removeMemberFromRoomRegex.exec(line); - if (matchResult) { - this.onRemoveMemberFromRoom(matchResult[1], matchResult[2]); - return; - } - matchResult = this.quitMemberRegex.exec(line); - if (matchResult) { - this.onQuitMember(matchResult[1]); - return; - } - matchResult = this.successLoginMessageRegex.exec(line); - if (matchResult) { - this.onSuccessLoginMessage(); - return; - } - matchResult = this.failedLoginMessageRegex.exec(line); - if (matchResult) { - this.onFailedLoginMessage(); - return; - } - }); - } - - onSuccessLoginMessage() { - console.log('[irc] onSuccessLoginMessage -> '.yellow); - if (this.loginCb) { - this.loginCb(null, this.loginReq); - } - } - - onFailedLoginMessage() { - console.log('[irc] onFailedLoginMessage -> '.yellow); - this.loginReq.allowed = false; - this.disconnect(); - if (this.loginCb) { - this.loginCb(null, this.loginReq); - } - } - - onReceiveMessage(source, target, content) { - const now = new Date; - const timestamp = now.getTime(); - let cacheKey = [source, target, content].join(','); - console.log('[irc] ircSendMessageCache.get -> '.yellow, 'key:', cacheKey, 'value:', ircSendMessageCache.get(cacheKey), 'ts:', timestamp - 1000); - if (ircSendMessageCache.get(cacheKey) > (timestamp - 1000)) { - return; - } else { - ircSendMessageCache.set(cacheKey, timestamp); - } - console.log('[irc] onReceiveMessage -> '.yellow, 'source:', source, 'target:', target, 'content:', content); - source = this.createUserWhenNotExist(source); - let room; - if (target[0] === '#') { - room = RocketChat.models.Rooms.findOneByName(target.substring(1)); - } else { - room = this.createDirectRoomWhenNotExist(source, this.user); - } - const message = { msg: content, ts: now }; - cacheKey = `${ source.username }${ timestamp }`; - ircReceiveMessageCache.set(cacheKey, true); - console.log('[irc] ircReceiveMessageCache.set -> '.yellow, 'key:', cacheKey); - RocketChat.sendMessage(source, message, room); - } - - onReceiveMemberList(roomName, members) { - this.receiveMemberListBuf[roomName] = this.receiveMemberListBuf[roomName].concat(members); - } - - onEndMemberList(roomName) { - const newMembers = this.receiveMemberListBuf[roomName]; - console.log('[irc] onEndMemberList -> '.yellow, 'room:', roomName, 'members:', newMembers.join(',')); - const room = RocketChat.models.Rooms.findOneByNameAndType(roomName, 'c'); - if (!room) { - return; - } - const oldMembers = room.usernames; - const appendMembers = _.difference(newMembers, oldMembers); - const removeMembers = _.difference(oldMembers, newMembers); - appendMembers.forEach(member => this.createUserWhenNotExist(member)); - RocketChat.models.Rooms.removeUsernamesById(room._id, removeMembers); - RocketChat.models.Rooms.addUsernamesById(room._id, appendMembers); - - this.isJoiningRoom = false; - roomName = this.pendingJoinRoomBuf.shift(); - if (roomName) { - this.joinRoom({ - t: 'c', - name: roomName - }); - } - } - - sendRawMessage(msg) { - console.log('[irc] sendRawMessage -> '.yellow, msg.slice(0, -2)); - if (this.isConnected) { - this.socket.write(msg); - } else { - this.msgBuf.push(msg); - } - } - - sendMessage(room, message) { - console.log('[irc] sendMessage -> '.yellow, 'userName:', message.u.username); - let target = ''; - if (room.t === 'c') { - target = `#${ room.name }`; - } else if (room.t === 'd') { - const usernames = room.usernames; - usernames.forEach(name => { - if (message.u.username !== name) { - target = name; - return; - } - }); - } - const cacheKey = [this.user.username, target, message.msg].join(','); - console.log('[irc] ircSendMessageCache.set -> '.yellow, 'key:', cacheKey, 'ts:', message.ts.getTime()); - ircSendMessageCache.set(cacheKey, message.ts.getTime()); - const msg = `PRIVMSG ${ target } :${ message.msg }\r\n`; - this.sendRawMessage(msg); - } - - initRoomList() { - const roomsCursor = RocketChat.models.Rooms.findByTypeContainingUsername('c', this.user.username, { fields: { name: 1, t: 1 }}); - const rooms = roomsCursor.fetch(); - rooms.forEach(room => this.joinRoom(room)); - } - - joinRoom(room) { - if (room.t !== 'c' || room.name === 'general') { - return; - } - if (this.isJoiningRoom) { - return this.pendingJoinRoomBuf.push(room.name); - } - console.log('[irc] joinRoom -> '.yellow, 'roomName:', room.name, 'pendingJoinRoomBuf:', this.pendingJoinRoomBuf.join(',')); - const msg = `JOIN #${ room.name }\r\n`; - this.receiveMemberListBuf[room.name] = []; - this.sendRawMessage(msg); - this.isJoiningRoom = true; - } - - leaveRoom(room) { - if (room.t !== 'c') { - return; - } - const msg = `PART #${ room.name }\r\n`; - this.sendRawMessage(msg); - } - - getMemberList(room) { - if (room.t !== 'c') { - return; - } - const msg = `NAMES #${ room.name }\r\n`; - this.receiveMemberListBuf[room.name] = []; - this.sendRawMessage(msg); - } - - onAddMemberToRoom(member, roomName) { - if (this.user.username === member) { - return; - } - console.log('[irc] onAddMemberToRoom -> '.yellow, 'roomName:', roomName, 'member:', member); - this.createUserWhenNotExist(member); - RocketChat.models.Rooms.addUsernameByName(roomName, member); - } - - onRemoveMemberFromRoom(member, roomName) { - console.log('[irc] onRemoveMemberFromRoom -> '.yellow, 'roomName:', roomName, 'member:', member); - RocketChat.models.Rooms.removeUsernameByName(roomName, member); - } - - onQuitMember(member) { - console.log('[irc] onQuitMember ->'.yellow, 'username:', member); - RocketChat.models.Rooms.removeUsernameFromAll(member); - Meteor.users.update({ name: member }, { $set: { status: 'offline' }}); - } - - createUserWhenNotExist(name) { - const user = Meteor.users.findOne({ name }); - if (user) { - return user; - } - console.log('[irc] createNotExistUser ->'.yellow, 'userName:', name); - Meteor.call('registerUser', { - email: `${ name }@rocketchat.org`, - pass: 'rocketchat', - name - }); - Meteor.users.update({ name }, { - $set: { - status: 'online', - username: name - } - }); - return Meteor.users.findOne({ name }); - } - - createDirectRoomWhenNotExist(source, target) { - console.log('[irc] createDirectRoomWhenNotExist -> '.yellow, 'source:', source, 'target:', target); - const rid = [source._id, target._id].sort().join(''); - const now = new Date(); - RocketChat.models.Rooms.upsert({ _id: rid}, { - $set: { - usernames: [source.username, target.username] - }, - $setOnInsert: { - t: 'd', - msgs: 0, - ts: now - } - }); - RocketChat.models.Subscriptions.upsert({ rid, $and: [{ 'u._id': target._id}]}, { - $setOnInsert: { - name: source.username, - t: 'd', - open: false, - alert: false, - unread: 0, - userMentions: 0, - groupMentions: 0, - u: { _id: target._id, username: target.username }} - }); - return { t: 'd', _id: rid }; - } -} - -IrcClient.getByUid = function(uid) { - return ircClientMap[uid]; -}; - -IrcClient.create = function(login) { - if (login.user == null) { - return login; - } - if (!(login.user._id in ircClientMap)) { - const ircClient = new IrcClient(login); - return async(ircClient.connect); - } - return login; -}; - -function IrcLoginer(login) { - console.log('[irc] validateLogin -> '.yellow, login); - return IrcClient.create(login); -} - - -function IrcSender(message) { - const name = message.u.username; - const timestamp = message.ts.getTime(); - const cacheKey = `${ name }${ timestamp }`; - if (ircReceiveMessageCache.get(cacheKey)) { - return message; - } - const room = RocketChat.models.Rooms.findOneById(message.rid, { fields: { name: 1, usernames: 1, t: 1 }}); - const ircClient = IrcClient.getByUid(message.u._id); - ircClient.sendMessage(room, message); - return message; -} - - -function IrcRoomJoiner(user, room) { - const ircClient = IrcClient.getByUid(user._id); - ircClient.joinRoom(room); - return room; -} - - -function IrcRoomLeaver(user, room) { - const ircClient = IrcClient.getByUid(user._id); - ircClient.leaveRoom(room); - return room; -} - -function IrcLogoutCleanUper(user) { - const ircClient = IrcClient.getByUid(user._id); - ircClient.disconnect(); - return user; -} - -////// -// Make magic happen - -// Only proceed if the package has been enabled -if (IRC_AVAILABILITY === true) { - RocketChat.callbacks.add('beforeValidateLogin', IrcLoginer, RocketChat.callbacks.priority.LOW, 'irc-loginer'); - RocketChat.callbacks.add('beforeSaveMessage', IrcSender, RocketChat.callbacks.priority.LOW, 'irc-sender'); - RocketChat.callbacks.add('beforeJoinRoom', IrcRoomJoiner, RocketChat.callbacks.priority.LOW, 'irc-room-joiner'); - RocketChat.callbacks.add('beforeCreateChannel', IrcRoomJoiner, RocketChat.callbacks.priority.LOW, 'irc-room-joiner-create-channel'); - RocketChat.callbacks.add('beforeLeaveRoom', IrcRoomLeaver, RocketChat.callbacks.priority.LOW, 'irc-room-leaver'); - RocketChat.callbacks.add('afterLogoutCleanUp', IrcLogoutCleanUper, RocketChat.callbacks.priority.LOW, 'irc-clean-up'); -} diff --git a/packages/rocketchat-irc/server/servers/RFC2813/codes.js b/packages/rocketchat-irc/server/servers/RFC2813/codes.js new file mode 100644 index 000000000000..3071edfeee5d --- /dev/null +++ b/packages/rocketchat-irc/server/servers/RFC2813/codes.js @@ -0,0 +1,519 @@ +/** + * This file is part of https://github.com/martynsmith/node-irc + * by https://github.com/martynsmith + */ + +module.exports = { + '001': { + name: 'rpl_welcome', + type: 'reply' + }, + '002': { + name: 'rpl_yourhost', + type: 'reply' + }, + '003': { + name: 'rpl_created', + type: 'reply' + }, + '004': { + name: 'rpl_myinfo', + type: 'reply' + }, + '005': { + name: 'rpl_isupport', + type: 'reply' + }, + 200: { + name: 'rpl_tracelink', + type: 'reply' + }, + 201: { + name: 'rpl_traceconnecting', + type: 'reply' + }, + 202: { + name: 'rpl_tracehandshake', + type: 'reply' + }, + 203: { + name: 'rpl_traceunknown', + type: 'reply' + }, + 204: { + name: 'rpl_traceoperator', + type: 'reply' + }, + 205: { + name: 'rpl_traceuser', + type: 'reply' + }, + 206: { + name: 'rpl_traceserver', + type: 'reply' + }, + 208: { + name: 'rpl_tracenewtype', + type: 'reply' + }, + 211: { + name: 'rpl_statslinkinfo', + type: 'reply' + }, + 212: { + name: 'rpl_statscommands', + type: 'reply' + }, + 213: { + name: 'rpl_statscline', + type: 'reply' + }, + 214: { + name: 'rpl_statsnline', + type: 'reply' + }, + 215: { + name: 'rpl_statsiline', + type: 'reply' + }, + 216: { + name: 'rpl_statskline', + type: 'reply' + }, + 218: { + name: 'rpl_statsyline', + type: 'reply' + }, + 219: { + name: 'rpl_endofstats', + type: 'reply' + }, + 221: { + name: 'rpl_umodeis', + type: 'reply' + }, + 241: { + name: 'rpl_statslline', + type: 'reply' + }, + 242: { + name: 'rpl_statsuptime', + type: 'reply' + }, + 243: { + name: 'rpl_statsoline', + type: 'reply' + }, + 244: { + name: 'rpl_statshline', + type: 'reply' + }, + 250: { + name: 'rpl_statsconn', + type: 'reply' + }, + 251: { + name: 'rpl_luserclient', + type: 'reply' + }, + 252: { + name: 'rpl_luserop', + type: 'reply' + }, + 253: { + name: 'rpl_luserunknown', + type: 'reply' + }, + 254: { + name: 'rpl_luserchannels', + type: 'reply' + }, + 255: { + name: 'rpl_luserme', + type: 'reply' + }, + 256: { + name: 'rpl_adminme', + type: 'reply' + }, + 257: { + name: 'rpl_adminloc1', + type: 'reply' + }, + 258: { + name: 'rpl_adminloc2', + type: 'reply' + }, + 259: { + name: 'rpl_adminemail', + type: 'reply' + }, + 261: { + name: 'rpl_tracelog', + type: 'reply' + }, + 265: { + name: 'rpl_localusers', + type: 'reply' + }, + 266: { + name: 'rpl_globalusers', + type: 'reply' + }, + 300: { + name: 'rpl_none', + type: 'reply' + }, + 301: { + name: 'rpl_away', + type: 'reply' + }, + 302: { + name: 'rpl_userhost', + type: 'reply' + }, + 303: { + name: 'rpl_ison', + type: 'reply' + }, + 305: { + name: 'rpl_unaway', + type: 'reply' + }, + 306: { + name: 'rpl_nowaway', + type: 'reply' + }, + 311: { + name: 'rpl_whoisuser', + type: 'reply' + }, + 312: { + name: 'rpl_whoisserver', + type: 'reply' + }, + 313: { + name: 'rpl_whoisoperator', + type: 'reply' + }, + 314: { + name: 'rpl_whowasuser', + type: 'reply' + }, + 315: { + name: 'rpl_endofwho', + type: 'reply' + }, + 317: { + name: 'rpl_whoisidle', + type: 'reply' + }, + 318: { + name: 'rpl_endofwhois', + type: 'reply' + }, + 319: { + name: 'rpl_whoischannels', + type: 'reply' + }, + 321: { + name: 'rpl_liststart', + type: 'reply' + }, + 322: { + name: 'rpl_list', + type: 'reply' + }, + 323: { + name: 'rpl_listend', + type: 'reply' + }, + 324: { + name: 'rpl_channelmodeis', + type: 'reply' + }, + 329: { + name: 'rpl_creationtime', + type: 'reply' + }, + 331: { + name: 'rpl_notopic', + type: 'reply' + }, + 332: { + name: 'rpl_topic', + type: 'reply' + }, + 333: { + name: 'rpl_topicwhotime', + type: 'reply' + }, + 341: { + name: 'rpl_inviting', + type: 'reply' + }, + 342: { + name: 'rpl_summoning', + type: 'reply' + }, + 351: { + name: 'rpl_version', + type: 'reply' + }, + 352: { + name: 'rpl_whoreply', + type: 'reply' + }, + 353: { + name: 'rpl_namreply', + type: 'reply' + }, + 364: { + name: 'rpl_links', + type: 'reply' + }, + 365: { + name: 'rpl_endoflinks', + type: 'reply' + }, + 366: { + name: 'rpl_endofnames', + type: 'reply' + }, + 367: { + name: 'rpl_banlist', + type: 'reply' + }, + 368: { + name: 'rpl_endofbanlist', + type: 'reply' + }, + 369: { + name: 'rpl_endofwhowas', + type: 'reply' + }, + 371: { + name: 'rpl_info', + type: 'reply' + }, + 372: { + name: 'rpl_motd', + type: 'reply' + }, + 374: { + name: 'rpl_endofinfo', + type: 'reply' + }, + 375: { + name: 'rpl_motdstart', + type: 'reply' + }, + 376: { + name: 'rpl_endofmotd', + type: 'reply' + }, + 381: { + name: 'rpl_youreoper', + type: 'reply' + }, + 382: { + name: 'rpl_rehashing', + type: 'reply' + }, + 391: { + name: 'rpl_time', + type: 'reply' + }, + 392: { + name: 'rpl_usersstart', + type: 'reply' + }, + 393: { + name: 'rpl_users', + type: 'reply' + }, + 394: { + name: 'rpl_endofusers', + type: 'reply' + }, + 395: { + name: 'rpl_nousers', + type: 'reply' + }, + 401: { + name: 'err_nosuchnick', + type: 'error' + }, + 402: { + name: 'err_nosuchserver', + type: 'error' + }, + 403: { + name: 'err_nosuchchannel', + type: 'error' + }, + 404: { + name: 'err_cannotsendtochan', + type: 'error' + }, + 405: { + name: 'err_toomanychannels', + type: 'error' + }, + 406: { + name: 'err_wasnosuchnick', + type: 'error' + }, + 407: { + name: 'err_toomanytargets', + type: 'error' + }, + 409: { + name: 'err_noorigin', + type: 'error' + }, + 411: { + name: 'err_norecipient', + type: 'error' + }, + 412: { + name: 'err_notexttosend', + type: 'error' + }, + 413: { + name: 'err_notoplevel', + type: 'error' + }, + 414: { + name: 'err_wildtoplevel', + type: 'error' + }, + 421: { + name: 'err_unknowncommand', + type: 'error' + }, + 422: { + name: 'err_nomotd', + type: 'error' + }, + 423: { + name: 'err_noadmininfo', + type: 'error' + }, + 424: { + name: 'err_fileerror', + type: 'error' + }, + 431: { + name: 'err_nonicknamegiven', + type: 'error' + }, + 432: { + name: 'err_erroneusnickname', + type: 'error' + }, + 433: { + name: 'err_nicknameinuse', + type: 'error' + }, + 436: { + name: 'err_nickcollision', + type: 'error' + }, + 441: { + name: 'err_usernotinchannel', + type: 'error' + }, + 442: { + name: 'err_notonchannel', + type: 'error' + }, + 443: { + name: 'err_useronchannel', + type: 'error' + }, + 444: { + name: 'err_nologin', + type: 'error' + }, + 445: { + name: 'err_summondisabled', + type: 'error' + }, + 446: { + name: 'err_usersdisabled', + type: 'error' + }, + 451: { + name: 'err_notregistered', + type: 'error' + }, + 461: { + name: 'err_needmoreparams', + type: 'error' + }, + 462: { + name: 'err_alreadyregistred', + type: 'error' + }, + 463: { + name: 'err_nopermforhost', + type: 'error' + }, + 464: { + name: 'err_passwdmismatch', + type: 'error' + }, + 465: { + name: 'err_yourebannedcreep', + type: 'error' + }, + 467: { + name: 'err_keyset', + type: 'error' + }, + 471: { + name: 'err_channelisfull', + type: 'error' + }, + 472: { + name: 'err_unknownmode', + type: 'error' + }, + 473: { + name: 'err_inviteonlychan', + type: 'error' + }, + 474: { + name: 'err_bannedfromchan', + type: 'error' + }, + 475: { + name: 'err_badchannelkey', + type: 'error' + }, + 481: { + name: 'err_noprivileges', + type: 'error' + }, + 482: { + name: 'err_chanoprivsneeded', + type: 'error' + }, + 483: { + name: 'err_cantkillserver', + type: 'error' + }, + 491: { + name: 'err_nooperhost', + type: 'error' + }, + 501: { + name: 'err_umodeunknownflag', + type: 'error' + }, + 502: { + name: 'err_usersdontmatch', + type: 'error' + } +}; diff --git a/packages/rocketchat-irc/server/servers/RFC2813/index.js b/packages/rocketchat-irc/server/servers/RFC2813/index.js new file mode 100644 index 000000000000..69e12c89410b --- /dev/null +++ b/packages/rocketchat-irc/server/servers/RFC2813/index.js @@ -0,0 +1,183 @@ +import net from 'net'; +import util from 'util'; +import { EventEmitter } from 'events'; + +import parseMessage from './parseMessage'; + +import peerCommandHandlers from './peerCommandHandlers'; +import localCommandHandlers from './localCommandHandlers'; + +class RFC2813 { + constructor(config) { + this.config = config; + + // Hold registered state + this.registerSteps = []; + this.isRegistered = false; + + // Hold peer server information + this.serverPrefix = null; + + // Hold the buffer while receiving + this.receiveBuffer = new Buffer(''); + + } + + /** + * Setup socket + */ + setupSocket() { + // Setup socket + this.socket = new net.Socket(); + this.socket.setNoDelay(); + this.socket.setEncoding('utf-8'); + this.socket.setKeepAlive(true); + this.socket.setTimeout(90000); + + this.socket.on('data', this.onReceiveFromPeer.bind(this)); + + this.socket.on('connect', this.onConnect.bind(this)); + this.socket.on('error', (err) => console.log('[irc][server][err]', err)); + this.socket.on('timeout', () => this.log('Timeout')); + this.socket.on('close', () => this.log('Connection Closed')); + // Setup local + this.on('onReceiveFromLocal', this.onReceiveFromLocal.bind(this)); + } + + /** + * Log helper + */ + log(message) { + console.log(`[irc][server] ${ message }`); + } + + /** + * Connect + */ + register() { + this.log(`Connecting to @${ this.config.server.host }:${ this.config.server.port }`); + + if (!this.socket) { + this.setupSocket(); + } + + this.socket.connect(this.config.server.port, this.config.server.host); + } + + /** + * Disconnect + */ + disconnect() { + this.log('Disconnecting from server.'); + + if (this.socket) { + this.socket.destroy(); + this.socket = undefined; + } + this.isRegistered = false; + this.registerSteps = []; + } + + /** + * Setup the server connection + */ + onConnect() { + this.log('Connected! Registering as server...'); + + this.write({ + command: 'PASS', + parameters: [ this.config.passwords.local, '0210', 'ngircd' ] + }); + + this.write({ + command: 'SERVER', parameters: [ this.config.server.name ], + trailer: this.config.server.description + }); + } + + /** + * Sends a command message through the socket + */ + write(command) { + let buffer = command.prefix ? `:${ command.prefix } ` : ''; + buffer += command.command; + + if (command.parameters && command.parameters.length > 0) { + buffer += ` ${ command.parameters.join(' ') }`; + } + + if (command.trailer) { + buffer += ` :${ command.trailer }`; + } + + this.log(`Sending Command: ${ buffer }`); + + return this.socket.write(`${ buffer }\r\n`); + } + + /** + * + * + * Peer message handling + * + * + */ + onReceiveFromPeer(chunk) { + if (typeof (chunk) === 'string') { + this.receiveBuffer += chunk; + } else { + this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]); + } + + const lines = this.receiveBuffer.toString().split(/\r\n|\r|\n|\u0007/); // eslint-disable-line no-control-regex + + // If the buffer does not end with \r\n, more chunks are coming + if (lines.pop()) { + return; + } + + // Reset the buffer + this.receiveBuffer = new Buffer(''); + + lines.forEach((line) => { + if (line.length && !line.startsWith('\a')) { + const parsedMessage = parseMessage(line); + + if (peerCommandHandlers[parsedMessage.command]) { + this.log(`Handling peer message: ${ line }`); + + const command = peerCommandHandlers[parsedMessage.command].call(this, parsedMessage); + + if (command) { + this.log(`Emitting peer command to local: ${ JSON.stringify(command) }`); + this.emit('peerCommand', command); + } + } else { + this.log(`Unhandled peer message: ${ JSON.stringify(parsedMessage) }`); + } + } + }); + } + + /** + * + * + * Local message handling + * + * + */ + onReceiveFromLocal(command, parameters) { + if (localCommandHandlers[command]) { + this.log(`Handling local command: ${ command }`); + + localCommandHandlers[command].call(this, parameters); + + } else { + this.log(`Unhandled local command: ${ JSON.stringify(command) }`); + } + } +} + +util.inherits(RFC2813, EventEmitter); + +export default RFC2813; diff --git a/packages/rocketchat-irc/server/servers/RFC2813/localCommandHandlers.js b/packages/rocketchat-irc/server/servers/RFC2813/localCommandHandlers.js new file mode 100644 index 000000000000..5f5c6640ee3f --- /dev/null +++ b/packages/rocketchat-irc/server/servers/RFC2813/localCommandHandlers.js @@ -0,0 +1,73 @@ +function registerUser(parameters) { + const { name, profile: { irc: { nick, username } } } = parameters; + + this.write({ + prefix: this.config.server.name, + command: 'NICK', parameters: [ nick, 1, username, 'irc.rocket.chat', 1, '+i' ], + trailer: name + }); +} + +function joinChannel(parameters) { + const { + room: { name: roomName }, + user: { profile: { irc: { nick } } } + } = parameters; + + this.write({ + prefix: this.config.server.name, + command: 'NJOIN', parameters: [ `#${ roomName }` ], + trailer: nick + }); +} + +function joinedChannel(parameters) { + const { + room: { name: roomName }, + user: { profile: { irc: { nick } } } + } = parameters; + + this.write({ + prefix: nick, + command: 'JOIN', parameters: [ `#${ roomName }` ] + }); +} + +function leftChannel(parameters) { + const { + room: { name: roomName }, + user: { profile: { irc: { nick } } } + } = parameters; + + this.write({ + prefix: nick, + command: 'PART', parameters: [ `#${ roomName }` ] + }); +} + +function sentMessage(parameters) { + const { + user: { profile: { irc: { nick } } }, + to, + message + } = parameters; + + this.write({ + prefix: nick, + command: 'PRIVMSG', parameters: [ to ], + trailer: message + }); +} + +function disconnected(parameters) { + const { + user: { profile: { irc: { nick } } } + } = parameters; + + this.write({ + prefix: nick, + command: 'QUIT' + }); +} + +export default { registerUser, joinChannel, joinedChannel, leftChannel, sentMessage, disconnected }; diff --git a/packages/rocketchat-irc/server/servers/RFC2813/parseMessage.js b/packages/rocketchat-irc/server/servers/RFC2813/parseMessage.js new file mode 100644 index 000000000000..4199e66972af --- /dev/null +++ b/packages/rocketchat-irc/server/servers/RFC2813/parseMessage.js @@ -0,0 +1,69 @@ +/** + * This file is part of https://github.com/martynsmith/node-irc + * by https://github.com/martynsmith + */ + +const replyFor = require('./codes'); + +/** + * parseMessage(line, stripColors) + * + * takes a raw "line" from the IRC server and turns it into an object with + * useful keys + * @param {String} line Raw message from IRC server. + * @return {Object} A parsed message object. + */ +module.exports = function parseMessage(line) { + const message = {}; + let match; + + // Parse prefix + match = line.match(/^:([^ ]+) +/); + if (match) { + message.prefix = match[1]; + line = line.replace(/^:[^ ]+ +/, ''); + match = message.prefix.match(/^([_a-zA-Z0-9\~\[\]\\`^{}|-]*)(!([^@]+)@(.*))?$/); + if (match) { + message.nick = match[1]; + message.user = match[3]; + message.host = match[4]; + } else { + message.server = message.prefix; + } + } + + // Parse command + match = line.match(/^([^ ]+) */); + message.command = match[1]; + message.rawCommand = match[1]; + message.commandType = 'normal'; + line = line.replace(/^[^ ]+ +/, ''); + + if (replyFor[message.rawCommand]) { + message.command = replyFor[message.rawCommand].name; + message.commandType = replyFor[message.rawCommand].type; + } + + message.args = []; + let middle; + let trailing; + + // Parse parameters + if (line.search(/^:|\s+:/) !== -1) { + match = line.match(/(.*?)(?:^:|\s+:)(.*)/); + middle = match[1].trimRight(); + trailing = match[2]; + } else { + middle = line; + } + + if (middle.length) { + message.args = middle.split(/ +/); + } + + if (typeof (trailing) !== 'undefined' && trailing.length) { + message.args.push(trailing); + } + + return message; +}; diff --git a/packages/rocketchat-irc/server/servers/RFC2813/peerCommandHandlers.js b/packages/rocketchat-irc/server/servers/RFC2813/peerCommandHandlers.js new file mode 100644 index 000000000000..f463be3de976 --- /dev/null +++ b/packages/rocketchat-irc/server/servers/RFC2813/peerCommandHandlers.js @@ -0,0 +1,112 @@ +function PASS() { + this.log('Received PASS command, continue registering...'); + + this.registerSteps.push('PASS'); +} + +function SERVER(parsedMessage) { + this.log('Received SERVER command, waiting for first PING...'); + + this.serverPrefix = parsedMessage.prefix; + + this.registerSteps.push('SERVER'); +} + +function PING() { + if (!this.isRegistered && this.registerSteps.length === 2) { + this.log('Received first PING command, server is registered!'); + + this.isRegistered = true; + + this.emit('registered'); + } + + this.write({ + prefix: this.config.server.name, + command: 'PONG', + parameters: [ this.config.server.name ] + }); +} + +function NICK(parsedMessage) { + let command; + + // Check if the message comes from the server, + // which means it is a new user + if (parsedMessage.prefix === this.serverPrefix) { + command = { + identifier: 'userRegistered', + args: { + nick: parsedMessage.args[0], + username: parsedMessage.args[2], + host: parsedMessage.args[3], + name: parsedMessage.args[6] + } + }; + } else { // Otherwise, it is a nick change + command = { + identifier: 'nickChanged', + args: { + nick: parsedMessage.nick, + newNick: parsedMessage.args[0] + } + }; + } + + return command; +} + +function JOIN(parsedMessage) { + const command = { + identifier: 'joinedChannel', + args: { + roomName: parsedMessage.args[0].substring(1), + nick: parsedMessage.prefix + } + }; + + return command; +} + +function PART(parsedMessage) { + const command = { + identifier: 'leftChannel', + args: { + roomName: parsedMessage.args[0].substring(1), + nick: parsedMessage.prefix + } + }; + + return command; +} + +function PRIVMSG(parsedMessage) { + const command = { + identifier: 'sentMessage', + args: { + nick: parsedMessage.prefix, + message: parsedMessage.args[1] + } + }; + + if (parsedMessage.args[0][0] === '#') { + command.args.roomName = parsedMessage.args[0].substring(1); + } else { + command.args.recipientNick = parsedMessage.args[0]; + } + + return command; +} + +function QUIT(parsedMessage) { + const command = { + identifier: 'disconnected', + args: { + nick: parsedMessage.prefix + } + }; + + return command; +} + +export default { PASS, SERVER, PING, NICK, JOIN, PART, PRIVMSG, QUIT }; diff --git a/packages/rocketchat-irc/server/servers/index.js b/packages/rocketchat-irc/server/servers/index.js new file mode 100644 index 000000000000..6ab8fd6b4594 --- /dev/null +++ b/packages/rocketchat-irc/server/servers/index.js @@ -0,0 +1,3 @@ +import RFC2813 from './RFC2813'; + +export { RFC2813 }; diff --git a/packages/rocketchat-irc/server/settings.js b/packages/rocketchat-irc/server/settings.js deleted file mode 100644 index 19aa6d8ca96e..000000000000 --- a/packages/rocketchat-irc/server/settings.js +++ /dev/null @@ -1,78 +0,0 @@ -Meteor.startup(function() { - RocketChat.settings.addGroup('IRC', function() { - - // Is this thing on? - this.add('IRC_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enabled', - i18nDescription: 'IRC_Enabled', - alert: 'IRC Support is a work in progress. Use on a production system is not recommended at this time.' - }); - - // The IRC host server to talk to - this.add('IRC_Host', 'irc.freenode.net', { - type: 'string', - i18nLabel: 'Host', - i18nDescription: 'IRC_Hostname' - }); - - // The port to connect on the remote server - this.add('IRC_Port', 6667, { - type: 'int', - i18nLabel: 'Port', - i18nDescription: 'IRC_Port' - }); - - // Cache size of the messages we send the host IRC server - this.add('IRC_Message_Cache_Size', 200, { - type: 'int', - i18nLabel: 'Message Cache Size', - i18nDescription: 'IRC_Message_Cache_Size' - }); - - // Expandable box for modifying regular expressions for IRC interaction - this.section('Regular_Expressions', function() { - this.add('IRC_RegEx_successLogin', 'Welcome to the freenode Internet Relay Chat Network', { - type: 'string', - i18nLabel: 'Login Successful', - i18nDescription: 'IRC_Login_Success' - }); - this.add('IRC_RegEx_failedLogin', 'You have not registered', { - type: 'string', - i18nLabel: 'Login Failed', - i18nDescription: 'IRC_Login_Fail' - }); - this.add('IRC_RegEx_receiveMessage', '^:(\S+)!~\S+ PRIVMSG (\S+) :(.+)$', { - type: 'string', - i18nLabel: 'Private Message', - i18nDescription: 'IRC_Private_Message' - }); - this.add('IRC_RegEx_receiveMemberList', '^:\S+ \d+ \S+ = #(\S+) :(.*)$', { - type: 'string', - i18nLabel: 'Channel User List Start', - i18nDescription: 'IRC_Channel_Users' - }); - this.add('IRC_RegEx_endMemberList', '^.+#(\S+) :End of \/NAMES list.$', { - type: 'string', - i18nLabel: 'Channel User List End', - i18nDescription: 'IRC_Channel_Users_End' - }); - this.add('IRC_RegEx_addMemberToRoom', '^:(\S+)!~\S+ JOIN #(\S+)$', { - type: 'string', - i18nLabel: 'Join Channel', - i18nDescription: 'IRC_Channel_Join' - }); - this.add('IRC_RegEx_removeMemberFromRoom', '^:(\S+)!~\S+ PART #(\S+)$', { - type: 'string', - i18nLabel: 'Leave Channel', - i18nDescription: 'IRC_Channel_Leave' - }); - this.add('IRC_RegEx_quitMember', '^:(\S+)!~\S+ QUIT .*$', { - type: 'string', - i18nLabel: 'Quit IRC Session', - i18nDescription: 'IRC_Quit' - }); - }); - - }); -}); diff --git a/packages/rocketchat-lib/server/functions/createRoom.js b/packages/rocketchat-lib/server/functions/createRoom.js index d028294354b0..0aa21c9e96dd 100644 --- a/packages/rocketchat-lib/server/functions/createRoom.js +++ b/packages/rocketchat-lib/server/functions/createRoom.js @@ -95,6 +95,9 @@ RocketChat.createRoom = function(type, name, owner, members, readOnly, extraData RocketChat.callbacks.run('afterCreatePrivateGroup', owner, room); }); } + Meteor.defer(() => { + RocketChat.callbacks.run('afterCreateRoom', owner, room); + }); if (Apps && Apps.isLoaded()) { // This returns a promise, but it won't mutate anything about the message diff --git a/packages/rocketchat-lib/server/models/Rooms.js b/packages/rocketchat-lib/server/models/Rooms.js index ef568eff70c1..09b04176d229 100644 --- a/packages/rocketchat-lib/server/models/Rooms.js +++ b/packages/rocketchat-lib/server/models/Rooms.js @@ -88,6 +88,10 @@ class ModelRooms extends RocketChat.models._Base { // FIND + findWithUsername(username, options) { + return this.find({ usernames: username }, options); + } + findById(roomId, options) { return this.find({ _id: roomId }, options); } diff --git a/packages/rocketchat-livechat/.app/package-lock.json b/packages/rocketchat-livechat/.app/package-lock.json new file mode 100644 index 000000000000..88f6eccd96f4 --- /dev/null +++ b/packages/rocketchat-livechat/.app/package-lock.json @@ -0,0 +1,874 @@ +{ + "name": "rocketchat-livechat", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.4" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "autolinker": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-1.6.2.tgz", + "integrity": "sha512-IKLGtYFb3jzGTtgCpb4bm//1sXmmmgmr0msKshhYoc7EsWmLCFvuyxLcEIfcZ5gbCgZGXrnXkOkcBblOFEnlog==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.3", + "regenerator-runtime": "0.11.1" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.3.tgz", + "integrity": "sha512-pRyDdo73C8Nim3jwFJ7DWe3TZCgwDfWZ6nHS5LSdU77kWbj1frruvdndP02AOavtD4y8v6Fp2dolbHgp4SDrfg==", + "requires": { + "nan": "2.6.2", + "node-pre-gyp": "0.6.36" + } + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "core-js": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.1" + } + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" + } + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jquery": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", + "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "moment": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" + }, + "node-pre-gyp": { + "version": "0.6.36", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz", + "integrity": "sha1-22BBEst04NR3VU6bUFsXq936t4Y=", + "requires": { + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.2", + "rc": "1.2.5", + "request": "2.83.0", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "2.2.1", + "tar-pack": "3.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "rc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz", + "integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=", + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "readable-stream": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz", + "integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.2.1" + } + }, + "sprintf-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", + "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", + "requires": { + "debug": "2.6.9", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.4", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "toastr": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz", + "integrity": "sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=", + "requires": { + "jquery": "3.3.1" + } + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "underscore.string": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.4.tgz", + "integrity": "sha1-LCo/n4PmR2L9xF5s6sZRQoZCE9s=", + "requires": { + "sprintf-js": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +}